Compare commits
7
Commits
1e3aa9b06b
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c5d540565 | ||
|
|
27a2dd743d | ||
|
|
e49b744385 | ||
|
|
b779099bcf | ||
|
|
1bb3978c9a | ||
|
|
a1e4246bf4 | ||
|
|
b23ed28b88 |
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
*.exe
|
*.exe
|
||||||
server
|
server
|
||||||
|
prompt-generator
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<link rel="stylesheet" href="/static/common.css">
|
<link rel="stylesheet" href="/static/common.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="flex" style="height: calc(100vh - 56px); margin-top: 56px;">
|
<div class="flex main-content">
|
||||||
<!-- 左侧项目树 -->
|
<!-- 左侧项目树 -->
|
||||||
<div class="w-64 border-r overflow-y-auto p-4" style="border-color: var(--border-color); background: var(--bg-primary);">
|
<div class="w-64 border-r overflow-y-auto p-4" style="border-color: var(--border-color); background: var(--bg-primary);">
|
||||||
<div class="search-box mb-4">
|
<div class="search-box mb-4">
|
||||||
|
|||||||
+101
-9
@@ -8,7 +8,7 @@
|
|||||||
<link rel="stylesheet" href="/static/common.css">
|
<link rel="stylesheet" href="/static/common.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="flex" style="height: calc(100vh - 56px); margin-top: 56px;">
|
<div class="flex main-content">
|
||||||
<!-- 左侧面板 -->
|
<!-- 左侧面板 -->
|
||||||
<div class="w-1/3 border-r overflow-y-auto p-4 flex flex-col gap-4" style="border-color: var(--border-color); background: var(--bg-primary);">
|
<div class="w-1/3 border-r overflow-y-auto p-4 flex flex-col gap-4" style="border-color: var(--border-color); background: var(--bg-primary);">
|
||||||
<!-- 项目/会话 -->
|
<!-- 项目/会话 -->
|
||||||
@@ -73,6 +73,17 @@
|
|||||||
<div class="card-body" id="snippets-container"></div>
|
<div class="card-body" id="snippets-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 参考材料 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
📦 参考材料
|
||||||
|
<button class="btn btn-sm" onclick="addMaterial()">+ 添加</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="materials-container">
|
||||||
|
<div class="text-sm text-muted" id="materials-empty">暂无参考材料</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- CLAUDE.md -->
|
<!-- CLAUDE.md -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="collapse-header" onclick="toggleCollapse(this)">
|
<div class="collapse-header" onclick="toggleCollapse(this)">
|
||||||
@@ -173,6 +184,7 @@
|
|||||||
let allSnippets = [];
|
let allSnippets = [];
|
||||||
let selectedTags = {}; // tagId -> optionId
|
let selectedTags = {}; // tagId -> optionId
|
||||||
let selectedSnippets = []; // [{snippetId, sortOrder}]
|
let selectedSnippets = []; // [{snippetId, sortOrder}]
|
||||||
|
let materials = []; // [{title, content}]
|
||||||
let suggestions = [];
|
let suggestions = [];
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
@@ -214,7 +226,7 @@
|
|||||||
const checked = selectedTags[tag.id] !== undefined;
|
const checked = selectedTags[tag.id] !== undefined;
|
||||||
const optionsHtml = tag.options.map(o => `
|
const optionsHtml = tag.options.map(o => `
|
||||||
<label class="radio-label">
|
<label class="radio-label">
|
||||||
<input type="radio" name="tag_${tag.id}" value="${o.id}" ${selectedTags[tag.id] === o.id ? 'checked' : ''} onchange="selectTagOption(${tag.id}, ${o.id})">
|
<input type="radio" name="tag_${tag.id}" value="${o.id}" ${selectedTags[tag.id] === o.id ? 'checked' : ''} onclick="selectTagOption(${tag.id}, ${o.id})">
|
||||||
<span class="text-sm">${escapeHtml(o.label)}</span>
|
<span class="text-sm">${escapeHtml(o.label)}</span>
|
||||||
</label>
|
</label>
|
||||||
`).join('');
|
`).join('');
|
||||||
@@ -255,7 +267,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function selectTagOption(tagId, optionId) {
|
function selectTagOption(tagId, optionId) {
|
||||||
selectedTags[tagId] = optionId;
|
if (selectedTags[tagId] === optionId) {
|
||||||
|
// 再次点击已选中的选项 → 取消选择(需要 renderTags 重建 DOM 以取消 radio 的 checked 状态)
|
||||||
|
delete selectedTags[tagId];
|
||||||
|
renderTags();
|
||||||
|
} else {
|
||||||
|
selectedTags[tagId] = optionId;
|
||||||
|
// 同步勾选父标签复选框
|
||||||
|
const checkbox = document.querySelector(`#tag-options-${tagId}`)
|
||||||
|
?.closest('.mb-2')
|
||||||
|
?.querySelector('input[type="checkbox"]');
|
||||||
|
if (checkbox) checkbox.checked = true;
|
||||||
|
}
|
||||||
updatePreview();
|
updatePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +362,7 @@
|
|||||||
// Custom content
|
// Custom content
|
||||||
const customContent = document.getElementById('custom-content').value.trim();
|
const customContent = document.getElementById('custom-content').value.trim();
|
||||||
if (customContent) {
|
if (customContent) {
|
||||||
parts.push(`## 用户自定义内容\n\n${customContent}`);
|
parts.push(`## 用户输入\n\n${customContent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag constraints
|
// Tag constraints
|
||||||
@@ -352,7 +375,7 @@
|
|||||||
tagConstraints.push(`- ${option.constraint_text}`);
|
tagConstraints.push(`- ${option.constraint_text}`);
|
||||||
}
|
}
|
||||||
if (tagConstraints.length > 0) {
|
if (tagConstraints.length > 0) {
|
||||||
parts.push(`## 标签约束\n\n${tagConstraints.join('\n')}`);
|
parts.push(`## 约束\n\n${tagConstraints.join('\n')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snippets
|
// Snippets
|
||||||
@@ -361,18 +384,28 @@
|
|||||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
.map(s => {
|
.map(s => {
|
||||||
const snippet = allSnippets.find(sn => sn.id === s.snippetId);
|
const snippet = allSnippets.find(sn => sn.id === s.snippetId);
|
||||||
return snippet ? `--- 片段: ${snippet.name} ---\n${snippet.content}` : '';
|
return snippet ? `--- ${snippet.name} ---\n${snippet.content}` : '';
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (snippetParts.length > 0) {
|
if (snippetParts.length > 0) {
|
||||||
parts.push(`## 片段内容\n\n${snippetParts.join('\n\n')}`);
|
parts.push(`## 参考片段\n\n${snippetParts.join('\n\n')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Materials
|
||||||
|
const validMaterials = materials.filter(m => m.content.trim());
|
||||||
|
if (validMaterials.length > 0) {
|
||||||
|
const materialParts = validMaterials.map((m, i) => {
|
||||||
|
const title = m.title.trim() || `参考材料${i + 1}`;
|
||||||
|
return `--- ${title} ---\n${m.content}`;
|
||||||
|
});
|
||||||
|
parts.push(`## 参考材料\n\n${materialParts.join('\n\n')}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Suggestions
|
// Suggestions
|
||||||
const adoptedSuggestions = suggestions.filter(s => s.adopted);
|
const adoptedSuggestions = suggestions.filter(s => s.adopted);
|
||||||
if (adoptedSuggestions.length > 0) {
|
if (adoptedSuggestions.length > 0) {
|
||||||
parts.push(`## 智能建议补充\n\n${adoptedSuggestions.map(s => s.constraint_text).join('\n')}`);
|
parts.push(`## 补充建议\n\n${adoptedSuggestions.map(s => s.constraint_text).join('\n')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const preview = parts.length > 0 ? parts.join('\n\n') : '选择标签和片段后,这里将实时预览最终的 Prompt...';
|
const preview = parts.length > 0 ? parts.join('\n\n') : '选择标签和片段后,这里将实时预览最终的 Prompt...';
|
||||||
@@ -430,6 +463,51 @@
|
|||||||
document.getElementById('tag-search')?.addEventListener('input', debounce(() => renderTags(), 300));
|
document.getElementById('tag-search')?.addEventListener('input', debounce(() => renderTags(), 300));
|
||||||
document.getElementById('snippet-search')?.addEventListener('input', debounce(() => renderSnippets(), 300));
|
document.getElementById('snippet-search')?.addEventListener('input', debounce(() => renderSnippets(), 300));
|
||||||
|
|
||||||
|
// 参考材料管理
|
||||||
|
function addMaterial() {
|
||||||
|
materials.push({ title: '', content: '' });
|
||||||
|
renderMaterials();
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMaterial(index) {
|
||||||
|
materials.splice(index, 1);
|
||||||
|
renderMaterials();
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMaterials() {
|
||||||
|
const container = document.getElementById('materials-container');
|
||||||
|
const emptyHint = document.getElementById('materials-empty');
|
||||||
|
|
||||||
|
if (materials.length === 0) {
|
||||||
|
container.innerHTML = '<div class="text-sm text-muted" id="materials-empty">暂无参考材料</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = materials.map((m, i) => `
|
||||||
|
<div class="mb-3 p-3" style="background:var(--bg-tertiary);border-radius:8px">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<input class="input" placeholder="参考材料${i + 1}" value="${escapeHtml(m.title)}"
|
||||||
|
oninput="updateMaterialTitle(${i}, this.value)" style="flex:1;margin-right:8px">
|
||||||
|
<button class="btn btn-sm" onclick="removeMaterial(${i})" style="color:var(--error-color)">✕</button>
|
||||||
|
</div>
|
||||||
|
<textarea class="textarea" placeholder="输入参考材料内容..." rows="3"
|
||||||
|
oninput="updateMaterialContent(${i}, this.value)">${escapeHtml(m.content)}</textarea>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMaterialTitle(index, title) {
|
||||||
|
materials[index].title = title;
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMaterialContent(index, content) {
|
||||||
|
materials[index].content = content;
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
// 自定义输入变化时更新预览
|
// 自定义输入变化时更新预览
|
||||||
document.getElementById('custom-content')?.addEventListener('input', debounce(() => updatePreview(), 200));
|
document.getElementById('custom-content')?.addEventListener('input', debounce(() => updatePreview(), 200));
|
||||||
document.getElementById('project-name')?.addEventListener('input', debounce(() => updatePreview(), 200));
|
document.getElementById('project-name')?.addEventListener('input', debounce(() => updatePreview(), 200));
|
||||||
@@ -580,6 +658,14 @@
|
|||||||
sort_order: s.sortOrder
|
sort_order: s.sortOrder
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const materialsData = materials
|
||||||
|
.filter(m => m.content.trim())
|
||||||
|
.map((m, i) => ({
|
||||||
|
title: m.title.trim(),
|
||||||
|
content: m.content,
|
||||||
|
sort_order: i
|
||||||
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await API.post('/api/builder/sessions', {
|
await API.post('/api/builder/sessions', {
|
||||||
title,
|
title,
|
||||||
@@ -587,7 +673,8 @@
|
|||||||
final_prompt: finalPrompt,
|
final_prompt: finalPrompt,
|
||||||
claude_session_id: claudeSessionId,
|
claude_session_id: claudeSessionId,
|
||||||
tag_option_ids: tagOptionIds,
|
tag_option_ids: tagOptionIds,
|
||||||
snippet_ids: snippetIds
|
snippet_ids: snippetIds,
|
||||||
|
materials: materialsData
|
||||||
});
|
});
|
||||||
closeSaveModal();
|
closeSaveModal();
|
||||||
Toast.success('会话已保存');
|
Toast.success('会话已保存');
|
||||||
@@ -605,6 +692,7 @@
|
|||||||
claudeMD: document.getElementById('claude-md').value,
|
claudeMD: document.getElementById('claude-md').value,
|
||||||
selectedTags: { ...selectedTags },
|
selectedTags: { ...selectedTags },
|
||||||
selectedSnippets: [...selectedSnippets],
|
selectedSnippets: [...selectedSnippets],
|
||||||
|
materials: materials.map(m => ({ title: m.title, content: m.content })),
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
@@ -622,6 +710,8 @@
|
|||||||
document.getElementById('claude-md').value = draft.claudeMD || '';
|
document.getElementById('claude-md').value = draft.claudeMD || '';
|
||||||
selectedTags = draft.selectedTags || {};
|
selectedTags = draft.selectedTags || {};
|
||||||
selectedSnippets = draft.selectedSnippets || [];
|
selectedSnippets = draft.selectedSnippets || [];
|
||||||
|
materials = draft.materials || [];
|
||||||
|
renderMaterials();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetBuilder() {
|
function resetBuilder() {
|
||||||
@@ -633,11 +723,13 @@
|
|||||||
document.getElementById('session-prompts').classList.add('hidden');
|
document.getElementById('session-prompts').classList.add('hidden');
|
||||||
selectedTags = {};
|
selectedTags = {};
|
||||||
selectedSnippets = [];
|
selectedSnippets = [];
|
||||||
|
materials = [];
|
||||||
suggestions = [];
|
suggestions = [];
|
||||||
document.getElementById('suggestions-card').style.display = 'none';
|
document.getElementById('suggestions-card').style.display = 'none';
|
||||||
Draft.clear('builder');
|
Draft.clear('builder');
|
||||||
renderTags();
|
renderTags();
|
||||||
renderSnippets();
|
renderSnippets();
|
||||||
|
renderMaterials();
|
||||||
updatePreview();
|
updatePreview();
|
||||||
Toast.success('已重置');
|
Toast.success('已重置');
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-1
@@ -8,7 +8,7 @@
|
|||||||
<link rel="stylesheet" href="/static/common.css">
|
<link rel="stylesheet" href="/static/common.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div style="max-width:640px;margin:80px auto 0;padding:0 24px">
|
<div style="max-width:640px;margin:24px auto 0;padding:0 24px">
|
||||||
<h1 style="font-size:24px;font-weight:700;margin-bottom:24px">⚙️ 设置</h1>
|
<h1 style="font-size:24px;font-weight:700;margin-bottom:24px">⚙️ 设置</h1>
|
||||||
|
|
||||||
<!-- LLM 配置 -->
|
<!-- LLM 配置 -->
|
||||||
@@ -49,6 +49,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统标签管理 -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">🏷️ 系统标签管理</div>
|
||||||
|
<div class="card-body" id="system-tags-container">
|
||||||
|
<div class="text-sm text-muted" id="system-tags-loading">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 账号 -->
|
<!-- 账号 -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">🔐 账号</div>
|
<div class="card-header">🔐 账号</div>
|
||||||
@@ -60,6 +68,11 @@
|
|||||||
|
|
||||||
<script src="/static/common.js"></script>
|
<script src="/static/common.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
function onLogin() {
|
||||||
|
loadSettings();
|
||||||
|
loadSystemTags();
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const ok = await Auth.check();
|
const ok = await Auth.check();
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -69,6 +82,7 @@
|
|||||||
renderNavbar('settings');
|
renderNavbar('settings');
|
||||||
await loadSettings();
|
await loadSettings();
|
||||||
updateThemeBtn();
|
updateThemeBtn();
|
||||||
|
await loadSystemTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
@@ -106,6 +120,111 @@
|
|||||||
document.getElementById('theme-label').textContent = Theme.isDark() ? '🌙 暗色' : '☀️ 亮色';
|
document.getElementById('theme-label').textContent = Theme.isDark() ? '🌙 暗色' : '☀️ 亮色';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 系统标签管理 ===
|
||||||
|
let systemTags = [];
|
||||||
|
|
||||||
|
async function loadSystemTags() {
|
||||||
|
const container = document.getElementById('system-tags-container');
|
||||||
|
try {
|
||||||
|
const tags = await API.get('/api/tags');
|
||||||
|
systemTags = (tags || []).filter(t => t.scope === 'system');
|
||||||
|
renderSystemTags();
|
||||||
|
} catch (e) {
|
||||||
|
container.innerHTML = `<div class="text-sm" style="color:var(--danger-color)">加载失败: ${escapeHtml(e.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSystemTags() {
|
||||||
|
const container = document.getElementById('system-tags-container');
|
||||||
|
if (!systemTags.length) {
|
||||||
|
container.innerHTML = '<div class="text-sm text-muted">暂无系统标签</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = systemTags.map(tag => `
|
||||||
|
<div class="tag-card" style="border:1px solid var(--border-color);border-radius:8px;margin-bottom:12px;overflow:hidden">
|
||||||
|
<div class="tag-card-header" style="padding:10px 14px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;background:var(--bg-secondary)"
|
||||||
|
onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'block':'none';this.querySelector('.toggle-icon').textContent=this.nextElementSibling.style.display==='none'?'▸':'▾'">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<span class="toggle-icon" style="font-size:12px;color:var(--text-muted)">▾</span>
|
||||||
|
<span class="text-sm font-medium">${escapeHtml(tag.name)}</span>
|
||||||
|
<span class="text-xs text-muted">(${(tag.options||[]).length} 个选项)</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();deleteTag(${tag.id},'${escapeHtml(tag.name)}')" title="删除标签">删除</button>
|
||||||
|
</div>
|
||||||
|
<div class="tag-card-body" style="padding:14px">
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
||||||
|
<input class="input" style="flex:1;min-width:120px" value="${escapeHtml(tag.name)}" placeholder="标签名称" id="tag-name-${tag.id}">
|
||||||
|
<input class="input" style="flex:2;min-width:180px" value="${escapeHtml(tag.description||'')}" placeholder="标签描述" id="tag-desc-${tag.id}">
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="updateTag(${tag.id})">保存</button>
|
||||||
|
</div>
|
||||||
|
<div id="tag-options-${tag.id}">
|
||||||
|
${(tag.options||[]).map(opt => renderOptionRow(tag.id, opt)).join('')}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm" style="margin-top:8px" onclick="addOption(${tag.id})">+ 添加选项</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOptionRow(tagId, opt) {
|
||||||
|
return `
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:8px;align-items:flex-start" data-opt-id="${opt.id}">
|
||||||
|
<input class="input" style="flex:1;min-width:100px" value="${escapeHtml(opt.label)}" placeholder="选项名称" id="opt-label-${opt.id}">
|
||||||
|
<textarea class="input" style="flex:2;min-width:160px;resize:vertical;min-height:36px" placeholder="约束文本" id="opt-text-${opt.id}">${escapeHtml(opt.constraint_text||'')}</textarea>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="updateOption(${opt.id},${tagId})" title="保存">✓</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick="deleteOption(${opt.id},${tagId})" title="删除">✕</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTag(tagId) {
|
||||||
|
const name = document.getElementById(`tag-name-${tagId}`).value.trim();
|
||||||
|
const desc = document.getElementById(`tag-desc-${tagId}`).value.trim();
|
||||||
|
if (!name) { Toast.error('标签名称不能为空'); return; }
|
||||||
|
try {
|
||||||
|
await API.put(`/api/tags/${tagId}`, { name, description: desc });
|
||||||
|
Toast.success('标签已更新');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTag(tagId, tagName) {
|
||||||
|
if (!confirm(`确定要删除标签「${tagName}」及其所有选项吗?`)) return;
|
||||||
|
try {
|
||||||
|
await API.del(`/api/tags/${tagId}`);
|
||||||
|
Toast.success('标签已删除');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateOption(optId, tagId) {
|
||||||
|
const label = document.getElementById(`opt-label-${optId}`).value.trim();
|
||||||
|
const text = document.getElementById(`opt-text-${optId}`).value.trim();
|
||||||
|
if (!label) { Toast.error('选项名称不能为空'); return; }
|
||||||
|
try {
|
||||||
|
await API.put(`/api/tag-options/${optId}`, { label, constraint_text: text });
|
||||||
|
Toast.success('选项已更新');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOption(optId, tagId) {
|
||||||
|
if (!confirm('确定要删除此选项吗?')) return;
|
||||||
|
try {
|
||||||
|
await API.del(`/api/tag-options/${optId}`);
|
||||||
|
Toast.success('选项已删除');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addOption(tagId) {
|
||||||
|
try {
|
||||||
|
const res = await API.post(`/api/tags/${tagId}/options`, { label: '新选项', constraint_text: '' });
|
||||||
|
Toast.success('选项已添加');
|
||||||
|
await loadSystemTags();
|
||||||
|
} catch (e) { Toast.error(e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -39,10 +39,16 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +61,17 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
position: sticky;
|
flex-shrink: 0;
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主内容区 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-brand {
|
.nav-brand {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@@ -102,6 +102,16 @@ func AutoMigrate() error {
|
|||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
KEY idx_session_id (builder_session_id)
|
KEY idx_session_id (builder_session_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
|
|
||||||
|
`CREATE TABLE IF NOT EXISTS builder_session_materials (
|
||||||
|
id bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
builder_session_id bigint NOT NULL,
|
||||||
|
title varchar(255) NOT NULL DEFAULT '',
|
||||||
|
content text NOT NULL,
|
||||||
|
sort_order int NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY idx_session_id (builder_session_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ddl := range tables {
|
for _, ddl := range tables {
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ func CreateBuilderSession(w http.ResponseWriter, r *http.Request) {
|
|||||||
SnippetID int64 `json:"snippet_id"`
|
SnippetID int64 `json:"snippet_id"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
} `json:"snippet_ids"`
|
} `json:"snippet_ids"`
|
||||||
|
Materials []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
} `json:"materials"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
fail(w, 400, "请求格式错误")
|
fail(w, 400, "请求格式错误")
|
||||||
@@ -79,6 +84,12 @@ func CreateBuilderSession(w http.ResponseWriter, r *http.Request) {
|
|||||||
sessionID, s.SnippetID, s.SortOrder)
|
sessionID, s.SnippetID, s.SortOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert materials
|
||||||
|
for _, m := range req.Materials {
|
||||||
|
db.DB.Exec("INSERT INTO builder_session_materials (builder_session_id, title, content, sort_order) VALUES (?, ?, ?, ?)",
|
||||||
|
sessionID, m.Title, m.Content, m.SortOrder)
|
||||||
|
}
|
||||||
|
|
||||||
success(w, map[string]int64{"id": sessionID})
|
success(w, map[string]int64{"id": sessionID})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,12 +132,27 @@ func GetBuilderSession(w http.ResponseWriter, r *http.Request) {
|
|||||||
snippetRows.Close()
|
snippetRows.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load materials
|
||||||
|
materialRows, err := db.DB.Query("SELECT id, builder_session_id, title, content, sort_order FROM builder_session_materials WHERE builder_session_id=? ORDER BY sort_order", id)
|
||||||
|
if err == nil {
|
||||||
|
for materialRows.Next() {
|
||||||
|
var m models.SessionMaterial
|
||||||
|
if err := materialRows.Scan(&m.ID, &m.BuilderSessionID, &m.Title, &m.Content, &m.SortOrder); err == nil {
|
||||||
|
s.Materials = append(s.Materials, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
materialRows.Close()
|
||||||
|
}
|
||||||
|
|
||||||
if s.Tags == nil {
|
if s.Tags == nil {
|
||||||
s.Tags = []models.SessionTag{}
|
s.Tags = []models.SessionTag{}
|
||||||
}
|
}
|
||||||
if s.Snippets == nil {
|
if s.Snippets == nil {
|
||||||
s.Snippets = []models.SessionSnippet{}
|
s.Snippets = []models.SessionSnippet{}
|
||||||
}
|
}
|
||||||
|
if s.Materials == nil {
|
||||||
|
s.Materials = []models.SessionMaterial{}
|
||||||
|
}
|
||||||
|
|
||||||
success(w, s)
|
success(w, s)
|
||||||
}
|
}
|
||||||
@@ -151,6 +177,11 @@ func UpdateBuilderSession(w http.ResponseWriter, r *http.Request) {
|
|||||||
SnippetID int64 `json:"snippet_id"`
|
SnippetID int64 `json:"snippet_id"`
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
} `json:"snippet_ids"`
|
} `json:"snippet_ids"`
|
||||||
|
Materials []struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
} `json:"materials"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
fail(w, 400, "请求格式错误")
|
fail(w, 400, "请求格式错误")
|
||||||
@@ -183,6 +214,13 @@ func UpdateBuilderSession(w http.ResponseWriter, r *http.Request) {
|
|||||||
id, s.SnippetID, s.SortOrder)
|
id, s.SnippetID, s.SortOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace materials
|
||||||
|
db.DB.Exec("DELETE FROM builder_session_materials WHERE builder_session_id=?", id)
|
||||||
|
for _, m := range req.Materials {
|
||||||
|
db.DB.Exec("INSERT INTO builder_session_materials (builder_session_id, title, content, sort_order) VALUES (?, ?, ?, ?)",
|
||||||
|
id, m.Title, m.Content, m.SortOrder)
|
||||||
|
}
|
||||||
|
|
||||||
success(w, nil)
|
success(w, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,12 +233,13 @@ func DeleteBuilderSession(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
db.DB.Exec("DELETE FROM builder_session_tags WHERE builder_session_id=?", id)
|
db.DB.Exec("DELETE FROM builder_session_tags WHERE builder_session_id=?", id)
|
||||||
db.DB.Exec("DELETE FROM builder_session_snippets WHERE builder_session_id=?", id)
|
db.DB.Exec("DELETE FROM builder_session_snippets WHERE builder_session_id=?", id)
|
||||||
|
db.DB.Exec("DELETE FROM builder_session_materials WHERE builder_session_id=?", id)
|
||||||
db.DB.Exec("DELETE FROM builder_sessions WHERE id=?", id)
|
db.DB.Exec("DELETE FROM builder_sessions WHERE id=?", id)
|
||||||
success(w, nil)
|
success(w, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AssemblePrompt 组装最终 Prompt
|
// AssemblePrompt 组装最终 Prompt
|
||||||
func AssemblePrompt(projectName string, customContent string, tagOptions []models.TagOption, snippets []models.Snippet, suggestions []string) string {
|
func AssemblePrompt(projectName string, customContent string, tagOptions []models.TagOption, snippets []models.Snippet, suggestions []string, materials []models.SessionMaterial) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
|
|
||||||
// Project context
|
// Project context
|
||||||
@@ -210,7 +249,7 @@ func AssemblePrompt(projectName string, customContent string, tagOptions []model
|
|||||||
|
|
||||||
// Custom content
|
// Custom content
|
||||||
if customContent != "" {
|
if customContent != "" {
|
||||||
parts = append(parts, fmt.Sprintf("## 用户自定义内容\n\n%s", customContent))
|
parts = append(parts, fmt.Sprintf("## 用户输入\n\n%s", customContent))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tag constraints
|
// Tag constraints
|
||||||
@@ -219,21 +258,34 @@ func AssemblePrompt(projectName string, customContent string, tagOptions []model
|
|||||||
for _, opt := range tagOptions {
|
for _, opt := range tagOptions {
|
||||||
constraints = append(constraints, fmt.Sprintf("- %s", opt.ConstraintText))
|
constraints = append(constraints, fmt.Sprintf("- %s", opt.ConstraintText))
|
||||||
}
|
}
|
||||||
parts = append(parts, fmt.Sprintf("## 标签约束\n\n%s", strings.Join(constraints, "\n")))
|
parts = append(parts, fmt.Sprintf("## 约束\n\n%s", strings.Join(constraints, "\n")))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snippets
|
// Snippets
|
||||||
if len(snippets) > 0 {
|
if len(snippets) > 0 {
|
||||||
var snippetParts []string
|
var snippetParts []string
|
||||||
for _, s := range snippets {
|
for _, s := range snippets {
|
||||||
snippetParts = append(snippetParts, fmt.Sprintf("--- 片段: %s ---\n%s", s.Name, s.Content))
|
snippetParts = append(snippetParts, fmt.Sprintf("--- %s ---\n%s", s.Name, s.Content))
|
||||||
}
|
}
|
||||||
parts = append(parts, fmt.Sprintf("## 片段内容\n\n%s", strings.Join(snippetParts, "\n\n")))
|
parts = append(parts, fmt.Sprintf("## 参考片段\n\n%s", strings.Join(snippetParts, "\n\n")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Materials
|
||||||
|
if len(materials) > 0 {
|
||||||
|
var materialParts []string
|
||||||
|
for i, m := range materials {
|
||||||
|
title := m.Title
|
||||||
|
if title == "" {
|
||||||
|
title = fmt.Sprintf("参考材料%d", i+1)
|
||||||
|
}
|
||||||
|
materialParts = append(materialParts, fmt.Sprintf("--- %s ---\n%s", title, m.Content))
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("## 参考材料\n\n%s", strings.Join(materialParts, "\n\n")))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suggestions
|
// Suggestions
|
||||||
if len(suggestions) > 0 {
|
if len(suggestions) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("## 智能建议补充\n\n%s", strings.Join(suggestions, "\n")))
|
parts = append(parts, fmt.Sprintf("## 补充建议\n\n%s", strings.Join(suggestions, "\n")))
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(parts, "\n\n")
|
return strings.Join(parts, "\n\n")
|
||||||
|
|||||||
@@ -70,14 +70,6 @@ func UpdateTag(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ownership
|
|
||||||
var scope string
|
|
||||||
db.DB.QueryRow("SELECT scope FROM tags WHERE id=?", id).Scan(&scope)
|
|
||||||
if scope != "personal" {
|
|
||||||
fail(w, 403, "不能修改系统标签")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
@@ -102,13 +94,6 @@ func DeleteTag(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var scope string
|
|
||||||
db.DB.QueryRow("SELECT scope FROM tags WHERE id=?", id).Scan(&scope)
|
|
||||||
if scope != "personal" {
|
|
||||||
fail(w, 403, "不能删除系统标签")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete options first
|
// Delete options first
|
||||||
db.DB.Exec("DELETE FROM tag_options WHERE tag_id=?", id)
|
db.DB.Exec("DELETE FROM tag_options WHERE tag_id=?", id)
|
||||||
db.DB.Exec("DELETE FROM tags WHERE id=?", id)
|
db.DB.Exec("DELETE FROM tags WHERE id=?", id)
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type BuilderSession struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Tags []SessionTag `json:"tags,omitempty"`
|
Tags []SessionTag `json:"tags,omitempty"`
|
||||||
Snippets []SessionSnippet `json:"snippets,omitempty"`
|
Snippets []SessionSnippet `json:"snippets,omitempty"`
|
||||||
|
Materials []SessionMaterial `json:"materials,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionTag 构建会话标签关联
|
// SessionTag 构建会话标签关联
|
||||||
@@ -61,6 +62,15 @@ type SessionSnippet struct {
|
|||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionMaterial 构建会话参考材料
|
||||||
|
type SessionMaterial struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
BuilderSessionID int64 `json:"builder_session_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
}
|
||||||
|
|
||||||
// Prompt prompts 表记录(只读)
|
// Prompt prompts 表记录(只读)
|
||||||
type Prompt struct {
|
type Prompt struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
|
|||||||
Reference in New Issue
Block a user