@qnote/q-ai-note 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/cli.js +16 -2
- package/dist/cli.js.map +1 -1
- package/dist/server/api/chat.d.ts.map +1 -1
- package/dist/server/api/chat.js +3 -0
- package/dist/server/api/chat.js.map +1 -1
- package/dist/server/api/nodeEntities.d.ts +3 -0
- package/dist/server/api/nodeEntities.d.ts.map +1 -0
- package/dist/server/api/nodeEntities.js +116 -0
- package/dist/server/api/nodeEntities.js.map +1 -0
- package/dist/server/db.d.ts +13 -2
- package/dist/server/db.d.ts.map +1 -1
- package/dist/server/db.js +443 -94
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/nodeEntitiesStore.d.ts +78 -0
- package/dist/server/nodeEntitiesStore.d.ts.map +1 -0
- package/dist/server/nodeEntitiesStore.js +196 -0
- package/dist/server/nodeEntitiesStore.js.map +1 -0
- package/dist/server/react/agent.d.ts +3 -0
- package/dist/server/react/agent.d.ts.map +1 -1
- package/dist/server/react/agent.js +132 -2
- package/dist/server/react/agent.js.map +1 -1
- package/dist/server/react/prompts.d.ts +1 -0
- package/dist/server/react/prompts.d.ts.map +1 -1
- package/dist/server/react/prompts.js +25 -4
- package/dist/server/react/prompts.js.map +1 -1
- package/dist/server/react/tools.d.ts.map +1 -1
- package/dist/server/react/tools.js +105 -0
- package/dist/server/react/tools.js.map +1 -1
- package/dist/web/app.js +824 -60
- package/dist/web/index.html +73 -1
- package/dist/web/styles.css +764 -33
- package/dist/web/vueRenderers.js +228 -13
- package/package.json +1 -3
package/dist/web/app.js
CHANGED
|
@@ -10,6 +10,16 @@ const state = {
|
|
|
10
10
|
chats: [],
|
|
11
11
|
settings: {},
|
|
12
12
|
pendingAction: null,
|
|
13
|
+
nodeEntities: [],
|
|
14
|
+
nodeEntityStats: null,
|
|
15
|
+
selectedNodeId: null,
|
|
16
|
+
nodeEntityFilter: 'all',
|
|
17
|
+
editingNodeEntityId: null,
|
|
18
|
+
nodeEntityFormExpanded: false,
|
|
19
|
+
sandboxPresentationMode: false,
|
|
20
|
+
sandboxFullscreenMode: false,
|
|
21
|
+
workTreeViewMode: 'full',
|
|
22
|
+
workItemShowAssignee: false,
|
|
13
23
|
workItemSearch: '',
|
|
14
24
|
workItemStatusFilter: 'all',
|
|
15
25
|
diarySearch: '',
|
|
@@ -20,7 +30,102 @@ const state = {
|
|
|
20
30
|
};
|
|
21
31
|
|
|
22
32
|
const expandedNodes = new Set();
|
|
23
|
-
let
|
|
33
|
+
let quickChatPopover = null;
|
|
34
|
+
let sandboxActionHandler = null;
|
|
35
|
+
let sandboxEscLocked = false;
|
|
36
|
+
let resizeRenderTimer = null;
|
|
37
|
+
const WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY = 'q-ai-note.work-item.show-assignee';
|
|
38
|
+
|
|
39
|
+
function loadWorkItemAssigneePreference() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = window.localStorage.getItem(WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY);
|
|
42
|
+
if (raw === 'true') state.workItemShowAssignee = true;
|
|
43
|
+
if (raw === 'false') state.workItemShowAssignee = false;
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore storage failures in restricted environments.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function persistWorkItemAssigneePreference() {
|
|
50
|
+
try {
|
|
51
|
+
window.localStorage.setItem(
|
|
52
|
+
WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY,
|
|
53
|
+
state.workItemShowAssignee ? 'true' : 'false',
|
|
54
|
+
);
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore storage failures in restricted environments.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function applySandboxLayoutMode() {
|
|
61
|
+
const layout = document.getElementById('sandbox-layout');
|
|
62
|
+
const toggleBtn = document.getElementById('toggle-sandbox-present-btn');
|
|
63
|
+
if (!layout || !toggleBtn) return;
|
|
64
|
+
const isPresentation = Boolean(state.sandboxPresentationMode);
|
|
65
|
+
layout.classList.toggle('presentation-mode', isPresentation);
|
|
66
|
+
toggleBtn.textContent = isPresentation ? '退出展示态' : '展示态布局';
|
|
67
|
+
toggleBtn.setAttribute('aria-pressed', isPresentation ? 'true' : 'false');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSandboxLayoutElement() {
|
|
71
|
+
return document.getElementById('sandbox-layout');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getSandboxFullscreenElement() {
|
|
75
|
+
return document.getElementById('page-sandbox-detail');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function syncSandboxEscLock(enabled) {
|
|
79
|
+
const keyboardApi = navigator?.keyboard;
|
|
80
|
+
if (!keyboardApi) return;
|
|
81
|
+
try {
|
|
82
|
+
if (enabled && !sandboxEscLocked) {
|
|
83
|
+
await keyboardApi.lock(['Escape']);
|
|
84
|
+
sandboxEscLocked = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!enabled && sandboxEscLocked) {
|
|
88
|
+
keyboardApi.unlock();
|
|
89
|
+
sandboxEscLocked = false;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore platforms without keyboard lock support.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function applySandboxFullscreenState() {
|
|
97
|
+
const layout = getSandboxLayoutElement();
|
|
98
|
+
const fullscreenRoot = getSandboxFullscreenElement();
|
|
99
|
+
const toggleBtn = document.getElementById('toggle-sandbox-fullscreen-btn');
|
|
100
|
+
const isFullscreen = Boolean(fullscreenRoot && document.fullscreenElement === fullscreenRoot);
|
|
101
|
+
state.sandboxFullscreenMode = isFullscreen;
|
|
102
|
+
if (layout) {
|
|
103
|
+
layout.classList.toggle('is-fullscreen', isFullscreen);
|
|
104
|
+
}
|
|
105
|
+
if (fullscreenRoot) {
|
|
106
|
+
fullscreenRoot.classList.toggle('is-sandbox-fullscreen', isFullscreen);
|
|
107
|
+
}
|
|
108
|
+
if (toggleBtn) {
|
|
109
|
+
toggleBtn.textContent = isFullscreen ? '退出全屏' : '全屏';
|
|
110
|
+
toggleBtn.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
|
|
111
|
+
}
|
|
112
|
+
void syncSandboxEscLock(isFullscreen);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function toggleSandboxFullscreen() {
|
|
116
|
+
const fullscreenRoot = getSandboxFullscreenElement();
|
|
117
|
+
if (!fullscreenRoot) return;
|
|
118
|
+
try {
|
|
119
|
+
if (document.fullscreenElement === fullscreenRoot) {
|
|
120
|
+
await document.exitFullscreen();
|
|
121
|
+
} else {
|
|
122
|
+
await fullscreenRoot.requestFullscreen();
|
|
123
|
+
}
|
|
124
|
+
} catch (_error) {
|
|
125
|
+
// Keep UI fallback silent for browsers/environments that block fullscreen.
|
|
126
|
+
applySandboxFullscreenState();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
24
129
|
|
|
25
130
|
function applyWorkItemFilters(items) {
|
|
26
131
|
const query = state.workItemSearch.trim().toLowerCase();
|
|
@@ -69,15 +174,46 @@ function expandAllNodes() {
|
|
|
69
174
|
items.forEach(item => expandedNodes.add(item.id));
|
|
70
175
|
}
|
|
71
176
|
|
|
177
|
+
function getRootNodeIds(items) {
|
|
178
|
+
return items.filter((item) => !item.parent_id).map((item) => item.id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyWorkTreeViewMode(mode) {
|
|
182
|
+
state.workTreeViewMode = mode === 'report' ? 'report' : mode === 'dense' ? 'dense' : 'full';
|
|
183
|
+
const selector = document.getElementById('work-tree-view-mode');
|
|
184
|
+
if (selector instanceof HTMLSelectElement) {
|
|
185
|
+
selector.value = state.workTreeViewMode;
|
|
186
|
+
}
|
|
187
|
+
const items = state.currentSandbox?.items || [];
|
|
188
|
+
expandedNodes.clear();
|
|
189
|
+
if (state.workTreeViewMode === 'report') {
|
|
190
|
+
getRootNodeIds(items).forEach((id) => expandedNodes.add(id));
|
|
191
|
+
} else {
|
|
192
|
+
expandAllNodes();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function applyWorkItemAssigneeToggle() {
|
|
197
|
+
const toggle = document.getElementById('work-item-show-assignee-toggle');
|
|
198
|
+
if (toggle instanceof HTMLInputElement) {
|
|
199
|
+
toggle.checked = Boolean(state.workItemShowAssignee);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
72
203
|
function renderWorkTree() {
|
|
73
204
|
const tree = document.getElementById('work-tree');
|
|
74
205
|
if (!tree || !state.currentSandbox) return;
|
|
75
206
|
|
|
76
207
|
const allItems = state.currentSandbox.items || [];
|
|
77
208
|
const items = applyWorkItemFilters(allItems);
|
|
209
|
+
const entitySummaryByNodeId = buildNodeEntitySummaryByNodeId();
|
|
78
210
|
|
|
79
211
|
if (expandedNodes.size === 0 && allItems.length > 0) {
|
|
80
|
-
|
|
212
|
+
if (state.workTreeViewMode === 'report') {
|
|
213
|
+
getRootNodeIds(allItems).forEach((id) => expandedNodes.add(id));
|
|
214
|
+
} else {
|
|
215
|
+
expandAllNodes();
|
|
216
|
+
}
|
|
81
217
|
}
|
|
82
218
|
|
|
83
219
|
if (allItems.length === 0) {
|
|
@@ -88,6 +224,7 @@ function renderWorkTree() {
|
|
|
88
224
|
mountWorkTree('work-tree', {
|
|
89
225
|
items,
|
|
90
226
|
expandedIds: Array.from(expandedNodes),
|
|
227
|
+
entitySummaryByNodeId,
|
|
91
228
|
onToggleExpand: (id) => {
|
|
92
229
|
if (expandedNodes.has(id)) {
|
|
93
230
|
expandedNodes.delete(id);
|
|
@@ -110,17 +247,48 @@ function renderWorkTree() {
|
|
|
110
247
|
onEdit: (id) => {
|
|
111
248
|
editWorkItem(id);
|
|
112
249
|
},
|
|
250
|
+
onSelect: (id) => {
|
|
251
|
+
showNodeEntityDrawer(id);
|
|
252
|
+
},
|
|
113
253
|
onDelete: async (id) => {
|
|
114
254
|
if (confirm('确定删除此任务?')) {
|
|
115
255
|
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
116
256
|
await loadSandbox(state.currentSandbox.id);
|
|
117
257
|
}
|
|
118
|
-
}
|
|
258
|
+
},
|
|
259
|
+
onQuickChat: (id, el) => {
|
|
260
|
+
openQuickChatPopover(id, el);
|
|
261
|
+
},
|
|
262
|
+
renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
|
|
263
|
+
showAssignee: state.workItemShowAssignee,
|
|
119
264
|
});
|
|
120
265
|
|
|
121
266
|
populateParentSelect(allItems);
|
|
122
267
|
}
|
|
123
268
|
|
|
269
|
+
function buildNodeEntitySummaryByNodeId() {
|
|
270
|
+
const summaryByNodeId = {};
|
|
271
|
+
for (const row of state.nodeEntities || []) {
|
|
272
|
+
const nodeId = String(row.work_item_id || '');
|
|
273
|
+
if (!nodeId) continue;
|
|
274
|
+
if (!summaryByNodeId[nodeId]) {
|
|
275
|
+
summaryByNodeId[nodeId] = { issue: 0, knowledge: 0, capability: 0, issue_open: 0, issue_closed: 0 };
|
|
276
|
+
}
|
|
277
|
+
if (row.entity_type === 'issue') {
|
|
278
|
+
summaryByNodeId[nodeId].issue += 1;
|
|
279
|
+
const status = String(row.status || '');
|
|
280
|
+
if (status === 'resolved' || status === 'closed') {
|
|
281
|
+
summaryByNodeId[nodeId].issue_closed += 1;
|
|
282
|
+
} else {
|
|
283
|
+
summaryByNodeId[nodeId].issue_open += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (row.entity_type === 'knowledge') summaryByNodeId[nodeId].knowledge += 1;
|
|
287
|
+
if (row.entity_type === 'capability') summaryByNodeId[nodeId].capability += 1;
|
|
288
|
+
}
|
|
289
|
+
return summaryByNodeId;
|
|
290
|
+
}
|
|
291
|
+
|
|
124
292
|
function populateParentSelect(items) {
|
|
125
293
|
const select = document.getElementById('new-item-parent');
|
|
126
294
|
if (!select) return;
|
|
@@ -129,6 +297,422 @@ function populateParentSelect(items) {
|
|
|
129
297
|
items.map(i => `<option value="${i.id}">${safeText(i.name)}</option>`).join('');
|
|
130
298
|
}
|
|
131
299
|
|
|
300
|
+
function renderMarkdownSnippet(text) {
|
|
301
|
+
const escaped = safeText(String(text || ''));
|
|
302
|
+
return escaped.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_m, label, url) => {
|
|
303
|
+
const safeUrl = safeText(url);
|
|
304
|
+
const safeLabel = safeText(label);
|
|
305
|
+
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getNodeById(nodeId) {
|
|
310
|
+
return state.currentSandbox?.items?.find((item) => item.id === nodeId) || null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getNodeEntitiesByNodeId(nodeId) {
|
|
314
|
+
return (state.nodeEntities || []).filter((row) => row.work_item_id === nodeId);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function closeNodeEntityDrawer() {
|
|
318
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
319
|
+
if (!drawer) return;
|
|
320
|
+
drawer.classList.add('hidden');
|
|
321
|
+
state.selectedNodeId = null;
|
|
322
|
+
state.nodeEntityFilter = 'all';
|
|
323
|
+
state.editingNodeEntityId = null;
|
|
324
|
+
state.nodeEntityFormExpanded = false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function setNodeEntityFormExpanded(expanded) {
|
|
328
|
+
state.nodeEntityFormExpanded = expanded;
|
|
329
|
+
const form = document.getElementById('drawer-create-form');
|
|
330
|
+
const toggleBtn = document.getElementById('toggle-node-entity-form-btn');
|
|
331
|
+
form?.classList.toggle('hidden', !expanded);
|
|
332
|
+
if (toggleBtn) {
|
|
333
|
+
toggleBtn.textContent = expanded ? '收起表单' : '+ 新建记录';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function deleteNodeEntityRecord(entityId) {
|
|
338
|
+
if (!state.currentSandbox) return;
|
|
339
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities/${entityId}`, {
|
|
340
|
+
method: 'DELETE',
|
|
341
|
+
});
|
|
342
|
+
await loadSandbox(state.currentSandbox.id);
|
|
343
|
+
if (state.selectedNodeId) {
|
|
344
|
+
showNodeEntityDrawer(state.selectedNodeId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function renderNodeEntitySummary(nodeId) {
|
|
349
|
+
const container = document.getElementById('node-entity-summary');
|
|
350
|
+
if (!container) return;
|
|
351
|
+
const rows = getNodeEntitiesByNodeId(nodeId);
|
|
352
|
+
const issues = rows.filter((row) => row.entity_type === 'issue');
|
|
353
|
+
const knowledges = rows.filter((row) => row.entity_type === 'knowledge');
|
|
354
|
+
const capabilities = rows.filter((row) => row.entity_type === 'capability');
|
|
355
|
+
const openIssues = issues.filter((row) => row.status === 'open' || row.status === 'in_progress' || row.status === 'blocked').length;
|
|
356
|
+
container.innerHTML = `
|
|
357
|
+
<div class="summary-card"><div class="label">Issue</div><div class="value">${issues.length}</div></div>
|
|
358
|
+
<div class="summary-card"><div class="label">Open Issue</div><div class="value">${openIssues}</div></div>
|
|
359
|
+
<div class="summary-card"><div class="label">Knowledge</div><div class="value">${knowledges.length}</div></div>
|
|
360
|
+
<div class="summary-card"><div class="label">Capability</div><div class="value">${capabilities.length}</div></div>
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderNodeEntityList(nodeId) {
|
|
365
|
+
const container = document.getElementById('node-entity-list');
|
|
366
|
+
if (!container) return;
|
|
367
|
+
const allRows = getNodeEntitiesByNodeId(nodeId);
|
|
368
|
+
const rows = state.nodeEntityFilter === 'all'
|
|
369
|
+
? allRows
|
|
370
|
+
: allRows.filter((row) => row.entity_type === state.nodeEntityFilter);
|
|
371
|
+
if (!rows.length) {
|
|
372
|
+
container.innerHTML = '<div class="empty-state"><p>当前节点还没有 issue/knowledge/capability</p></div>';
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
container.innerHTML = rows.map((row) => `
|
|
376
|
+
<div class="entity-card">
|
|
377
|
+
<div class="entity-card-header">
|
|
378
|
+
<div>
|
|
379
|
+
<span class="entity-type-pill">${safeText(row.entity_type)}</span>
|
|
380
|
+
<strong>${safeText(row.title || '-')}</strong>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="entity-card-actions">
|
|
383
|
+
<button class="btn btn-secondary btn-sm" data-entity-edit-id="${safeText(row.id)}">编辑</button>
|
|
384
|
+
<button class="btn btn-secondary btn-sm" data-entity-delete-id="${safeText(row.id)}">删除</button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="entity-meta">
|
|
388
|
+
${row.status ? `<span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}${row.priority ? `· ${safeText(row.priority)}` : ''} ${row.assignee ? `· @${safeText(row.assignee)}` : ''}
|
|
389
|
+
</div>
|
|
390
|
+
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
391
|
+
</div>
|
|
392
|
+
`).join('');
|
|
393
|
+
|
|
394
|
+
container.querySelectorAll('[data-entity-delete-id]').forEach((el) => {
|
|
395
|
+
el.addEventListener('click', async (e) => {
|
|
396
|
+
e.preventDefault();
|
|
397
|
+
const id = el.getAttribute('data-entity-delete-id');
|
|
398
|
+
if (!id) return;
|
|
399
|
+
if (!confirm('确定删除这条记录?')) return;
|
|
400
|
+
await deleteNodeEntityRecord(id);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
container.querySelectorAll('[data-entity-edit-id]').forEach((el) => {
|
|
405
|
+
el.addEventListener('click', (e) => {
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
const id = el.getAttribute('data-entity-edit-id');
|
|
408
|
+
if (!id) return;
|
|
409
|
+
const row = allRows.find((item) => item.id === id);
|
|
410
|
+
if (!row) return;
|
|
411
|
+
startEditNodeEntity(row);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function resetNodeEntityForm() {
|
|
417
|
+
state.editingNodeEntityId = null;
|
|
418
|
+
const typeInput = document.getElementById('entity-type-select');
|
|
419
|
+
const titleInput = document.getElementById('entity-title-input');
|
|
420
|
+
const contentInput = document.getElementById('entity-content-input');
|
|
421
|
+
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
422
|
+
const statusInput = document.getElementById('entity-status-select');
|
|
423
|
+
const priorityInput = document.getElementById('entity-priority-select');
|
|
424
|
+
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
425
|
+
if (typeInput) typeInput.value = 'issue';
|
|
426
|
+
if (titleInput) titleInput.value = '';
|
|
427
|
+
if (contentInput) contentInput.value = '';
|
|
428
|
+
if (assigneeInput) assigneeInput.value = '';
|
|
429
|
+
if (statusInput) statusInput.value = '';
|
|
430
|
+
if (priorityInput) priorityInput.value = '';
|
|
431
|
+
if (capabilityTypeInput) capabilityTypeInput.value = '';
|
|
432
|
+
|
|
433
|
+
applyEntityFormMode();
|
|
434
|
+
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
435
|
+
if (submitBtn) submitBtn.textContent = '添加记录';
|
|
436
|
+
const cancelBtn = document.getElementById('cancel-edit-entity-btn');
|
|
437
|
+
cancelBtn?.classList.add('hidden');
|
|
438
|
+
setNodeEntityFormExpanded(false);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function startEditNodeEntity(row) {
|
|
442
|
+
state.editingNodeEntityId = row.id;
|
|
443
|
+
setNodeEntityFormExpanded(true);
|
|
444
|
+
const typeInput = document.getElementById('entity-type-select');
|
|
445
|
+
const titleInput = document.getElementById('entity-title-input');
|
|
446
|
+
const contentInput = document.getElementById('entity-content-input');
|
|
447
|
+
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
448
|
+
const statusInput = document.getElementById('entity-status-select');
|
|
449
|
+
const priorityInput = document.getElementById('entity-priority-select');
|
|
450
|
+
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
451
|
+
if (typeInput) typeInput.value = row.entity_type || 'issue';
|
|
452
|
+
applyEntityFormMode();
|
|
453
|
+
if (titleInput) titleInput.value = row.title || '';
|
|
454
|
+
if (contentInput) contentInput.value = row.content_md || '';
|
|
455
|
+
if (assigneeInput) assigneeInput.value = row.assignee || '';
|
|
456
|
+
if (statusInput) statusInput.value = row.status || '';
|
|
457
|
+
if (priorityInput) priorityInput.value = row.priority || '';
|
|
458
|
+
if (capabilityTypeInput) capabilityTypeInput.value = row.capability_type || '';
|
|
459
|
+
|
|
460
|
+
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
461
|
+
if (submitBtn) submitBtn.textContent = '保存修改';
|
|
462
|
+
const cancelBtn = document.getElementById('cancel-edit-entity-btn');
|
|
463
|
+
cancelBtn?.classList.remove('hidden');
|
|
464
|
+
titleInput?.focus();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function showNodeEntityDrawer(nodeId) {
|
|
468
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
469
|
+
const title = document.getElementById('drawer-node-title');
|
|
470
|
+
const node = getNodeById(nodeId);
|
|
471
|
+
if (!drawer || !title || !node) return;
|
|
472
|
+
state.selectedNodeId = nodeId;
|
|
473
|
+
state.nodeEntityFilter = 'all';
|
|
474
|
+
title.textContent = node.name || nodeId;
|
|
475
|
+
renderNodeEntitySummary(nodeId);
|
|
476
|
+
renderNodeEntityFilterTabs();
|
|
477
|
+
renderNodeEntityList(nodeId);
|
|
478
|
+
resetNodeEntityForm();
|
|
479
|
+
drawer.classList.remove('hidden');
|
|
480
|
+
setTimeout(() => drawer.focus(), 0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function renderNodeEntityFilterTabs() {
|
|
484
|
+
const tabs = document.getElementById('entity-filter-tabs');
|
|
485
|
+
if (!tabs) return;
|
|
486
|
+
tabs.querySelectorAll('[data-entity-filter]').forEach((el) => {
|
|
487
|
+
const filter = el.getAttribute('data-entity-filter');
|
|
488
|
+
el.classList.toggle('active', filter === state.nodeEntityFilter);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function getStatusOptionsByEntityType(entityType) {
|
|
493
|
+
if (entityType === 'issue') {
|
|
494
|
+
return [
|
|
495
|
+
{ value: '', label: '状态(Issue,默认 open)' },
|
|
496
|
+
{ value: 'open', label: 'open' },
|
|
497
|
+
{ value: 'in_progress', label: 'in_progress' },
|
|
498
|
+
{ value: 'blocked', label: 'blocked' },
|
|
499
|
+
{ value: 'resolved', label: 'resolved' },
|
|
500
|
+
{ value: 'closed', label: 'closed' },
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
if (entityType === 'capability') {
|
|
504
|
+
return [
|
|
505
|
+
{ value: '', label: '状态(Capability,默认 building)' },
|
|
506
|
+
{ value: 'building', label: 'building' },
|
|
507
|
+
{ value: 'ready', label: 'ready' },
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
return [{ value: '', label: '无状态(Knowledge)' }];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function applyEntityFormMode() {
|
|
514
|
+
const type = document.getElementById('entity-type-select')?.value || 'issue';
|
|
515
|
+
const assigneeGroup = document.getElementById('entity-assignee-group');
|
|
516
|
+
const statusGroup = document.getElementById('entity-status-group');
|
|
517
|
+
const priorityGroup = document.getElementById('entity-priority-group');
|
|
518
|
+
const capabilityTypeGroup = document.getElementById('entity-capability-type-group');
|
|
519
|
+
const hint = document.getElementById('entity-form-hint');
|
|
520
|
+
const statusSelect = document.getElementById('entity-status-select');
|
|
521
|
+
|
|
522
|
+
if (statusSelect) {
|
|
523
|
+
statusSelect.innerHTML = getStatusOptionsByEntityType(type)
|
|
524
|
+
.map((option) => `<option value="${safeText(option.value)}">${safeText(option.label)}</option>`)
|
|
525
|
+
.join('');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
assigneeGroup?.classList.toggle('hidden', type !== 'issue');
|
|
529
|
+
priorityGroup?.classList.toggle('hidden', type !== 'issue');
|
|
530
|
+
capabilityTypeGroup?.classList.toggle('hidden', type !== 'capability');
|
|
531
|
+
statusGroup?.classList.toggle('hidden', false);
|
|
532
|
+
|
|
533
|
+
if (hint) {
|
|
534
|
+
hint.textContent = type === 'issue'
|
|
535
|
+
? 'Issue 推荐填写状态/优先级/负责人。'
|
|
536
|
+
: type === 'knowledge'
|
|
537
|
+
? 'Knowledge 可仅保存链接或少量 Markdown。'
|
|
538
|
+
: 'Capability 建议填写能力类型与简述。';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function closeQuickChatPopover() {
|
|
543
|
+
if (!quickChatPopover) return;
|
|
544
|
+
quickChatPopover.remove();
|
|
545
|
+
quickChatPopover = null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function getNodePath(nodeId) {
|
|
549
|
+
const items = state.currentSandbox?.items || [];
|
|
550
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
551
|
+
const path = [];
|
|
552
|
+
let cursor = byId.get(nodeId);
|
|
553
|
+
while (cursor) {
|
|
554
|
+
path.unshift(cursor.name || cursor.id);
|
|
555
|
+
cursor = cursor.parent_id ? byId.get(cursor.parent_id) : null;
|
|
556
|
+
}
|
|
557
|
+
return path;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function composeQuickChatContent(nodeId, userQuestion) {
|
|
561
|
+
const node = getNodeById(nodeId);
|
|
562
|
+
if (!node || !state.currentSandbox) {
|
|
563
|
+
return userQuestion;
|
|
564
|
+
}
|
|
565
|
+
const rows = getNodeEntitiesByNodeId(nodeId);
|
|
566
|
+
const issueCount = rows.filter((row) => row.entity_type === 'issue').length;
|
|
567
|
+
const knowledgeCount = rows.filter((row) => row.entity_type === 'knowledge').length;
|
|
568
|
+
const capabilityCount = rows.filter((row) => row.entity_type === 'capability').length;
|
|
569
|
+
const path = getNodePath(nodeId).join(' > ');
|
|
570
|
+
return [
|
|
571
|
+
'[NODE_CONTEXT]',
|
|
572
|
+
`sandbox_id=${state.currentSandbox.id}`,
|
|
573
|
+
`node_type=work_item`,
|
|
574
|
+
`node_id=${node.id}`,
|
|
575
|
+
`node_name=${node.name || ''}`,
|
|
576
|
+
`node_path=${path}`,
|
|
577
|
+
`entity_counts=issue:${issueCount},knowledge:${knowledgeCount},capability:${capabilityCount}`,
|
|
578
|
+
'[/NODE_CONTEXT]',
|
|
579
|
+
'',
|
|
580
|
+
'用户问题:',
|
|
581
|
+
userQuestion,
|
|
582
|
+
].join('\n');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function sendSandboxChatMessage(content, options = {}) {
|
|
586
|
+
if (!content || !state.currentSandbox) return;
|
|
587
|
+
const messages = document.getElementById('sandbox-chat-messages');
|
|
588
|
+
const btn = document.getElementById('sandbox-send-btn');
|
|
589
|
+
if (!messages || !btn) return;
|
|
590
|
+
|
|
591
|
+
const displayContent = options.displayContent || content;
|
|
592
|
+
const payloadContent = options.payloadContent || content;
|
|
593
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(displayContent)}</div>`);
|
|
594
|
+
const loadingMessage = appendLoadingMessage(messages);
|
|
595
|
+
messages.scrollTop = messages.scrollHeight;
|
|
596
|
+
|
|
597
|
+
const originalText = btn.textContent;
|
|
598
|
+
setButtonState(btn, { disabled: true, text: '思考中' });
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const history = state.chats
|
|
602
|
+
.filter(c => c.role)
|
|
603
|
+
.slice(-10)
|
|
604
|
+
.map(c => ({ role: c.role, content: typeof c.content === 'string' ? c.content : JSON.stringify(c.content) }));
|
|
605
|
+
|
|
606
|
+
const result = await apiRequest(`${API_BASE}/chats/react`, {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
body: JSON.stringify({
|
|
609
|
+
content: payloadContent,
|
|
610
|
+
sandbox_id: state.currentSandbox.id,
|
|
611
|
+
history,
|
|
612
|
+
pending_action: state.pendingAction
|
|
613
|
+
}),
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
loadingMessage?.remove();
|
|
617
|
+
if (typeof sandboxActionHandler === 'function') {
|
|
618
|
+
await sandboxActionHandler(result.action, state.currentSandbox.id);
|
|
619
|
+
}
|
|
620
|
+
await loadSandbox(state.currentSandbox.id);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
loadingMessage?.remove();
|
|
623
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
624
|
+
messages.scrollTop = messages.scrollHeight;
|
|
625
|
+
} finally {
|
|
626
|
+
setButtonState(btn, { disabled: false, text: originalText || '发送' });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function openQuickChatPopover(nodeId, anchorEl) {
|
|
631
|
+
if (!state.currentSandbox) return;
|
|
632
|
+
closeQuickChatPopover();
|
|
633
|
+
const container = document.querySelector('.sandbox-tree-section');
|
|
634
|
+
if (!container || !(anchorEl instanceof HTMLElement)) return;
|
|
635
|
+
const node = getNodeById(nodeId);
|
|
636
|
+
if (!node) return;
|
|
637
|
+
|
|
638
|
+
const popover = document.createElement('div');
|
|
639
|
+
popover.className = 'quick-chat-popover';
|
|
640
|
+
popover.innerHTML = `
|
|
641
|
+
<div class="quick-chat-header">
|
|
642
|
+
<span>快捷提问 · ${safeText(node.name || node.id)}</span>
|
|
643
|
+
<button class="btn btn-secondary btn-sm" data-quick-chat-cancel>关闭</button>
|
|
644
|
+
</div>
|
|
645
|
+
<textarea placeholder="输入多行问题,确认后会自动提交到右侧聊天区(Ctrl/Cmd+Enter 提交)"></textarea>
|
|
646
|
+
<div class="quick-chat-meta"><span data-quick-chat-count>0</span>/1000</div>
|
|
647
|
+
<div class="quick-chat-actions">
|
|
648
|
+
<button class="btn btn-secondary btn-sm" data-quick-chat-cancel>取消</button>
|
|
649
|
+
<button class="btn btn-primary btn-sm" data-quick-chat-submit disabled>确认提交</button>
|
|
650
|
+
</div>
|
|
651
|
+
`;
|
|
652
|
+
container.appendChild(popover);
|
|
653
|
+
quickChatPopover = popover;
|
|
654
|
+
|
|
655
|
+
const containerRect = container.getBoundingClientRect();
|
|
656
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
|
657
|
+
const popoverRect = popover.getBoundingClientRect();
|
|
658
|
+
const horizontalPadding = 8;
|
|
659
|
+
const verticalPadding = 8;
|
|
660
|
+
const preferredLeft = anchorRect.right - containerRect.left - popoverRect.width + 20;
|
|
661
|
+
const preferredTop = anchorRect.bottom - containerRect.top + 6;
|
|
662
|
+
const maxLeft = Math.max(horizontalPadding, containerRect.width - popoverRect.width - horizontalPadding);
|
|
663
|
+
const maxTop = Math.max(58, containerRect.height - popoverRect.height - verticalPadding);
|
|
664
|
+
const left = Math.min(maxLeft, Math.max(horizontalPadding, preferredLeft));
|
|
665
|
+
const top = Math.min(maxTop, Math.max(58, preferredTop));
|
|
666
|
+
popover.style.left = `${left}px`;
|
|
667
|
+
popover.style.top = `${top}px`;
|
|
668
|
+
|
|
669
|
+
const textarea = popover.querySelector('textarea');
|
|
670
|
+
const submitBtn = popover.querySelector('[data-quick-chat-submit]');
|
|
671
|
+
const countEl = popover.querySelector('[data-quick-chat-count]');
|
|
672
|
+
const cancelBtns = popover.querySelectorAll('[data-quick-chat-cancel]');
|
|
673
|
+
|
|
674
|
+
const updateSubmitState = () => {
|
|
675
|
+
const value = String(textarea?.value || '');
|
|
676
|
+
const length = value.length;
|
|
677
|
+
if (countEl) {
|
|
678
|
+
countEl.textContent = String(length);
|
|
679
|
+
}
|
|
680
|
+
const disabled = length === 0 || length > 1000;
|
|
681
|
+
if (submitBtn) {
|
|
682
|
+
submitBtn.classList.toggle('disabled', disabled);
|
|
683
|
+
submitBtn.disabled = disabled;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
cancelBtns.forEach((btn) => {
|
|
688
|
+
btn.addEventListener('click', () => closeQuickChatPopover());
|
|
689
|
+
});
|
|
690
|
+
submitBtn?.addEventListener('click', async () => {
|
|
691
|
+
const question = String(textarea?.value || '').trim();
|
|
692
|
+
if (!question) return;
|
|
693
|
+
closeQuickChatPopover();
|
|
694
|
+
const payloadContent = composeQuickChatContent(nodeId, question);
|
|
695
|
+
const displayContent = `【快捷】${question}`;
|
|
696
|
+
await sendSandboxChatMessage(question, { payloadContent, displayContent });
|
|
697
|
+
});
|
|
698
|
+
textarea?.addEventListener('input', updateSubmitState);
|
|
699
|
+
textarea?.addEventListener('keydown', async (event) => {
|
|
700
|
+
if (event.key === 'Escape') {
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
closeQuickChatPopover();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const isSubmit = event.key === 'Enter' && (event.ctrlKey || event.metaKey);
|
|
706
|
+
if (!isSubmit) return;
|
|
707
|
+
event.preventDefault();
|
|
708
|
+
if (submitBtn instanceof HTMLButtonElement && !submitBtn.disabled) {
|
|
709
|
+
submitBtn.click();
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
textarea?.focus();
|
|
713
|
+
updateSubmitState();
|
|
714
|
+
}
|
|
715
|
+
|
|
132
716
|
async function loadSandboxes() {
|
|
133
717
|
state.sandboxes = await apiRequest(`${API_BASE}/sandboxes`);
|
|
134
718
|
renderSandboxes();
|
|
@@ -172,14 +756,23 @@ function renderSandboxes() {
|
|
|
172
756
|
}
|
|
173
757
|
|
|
174
758
|
async function loadSandbox(id) {
|
|
175
|
-
|
|
759
|
+
closeQuickChatPopover();
|
|
760
|
+
const [sandbox, items, diaries, entities, entityStats] = await Promise.all([
|
|
176
761
|
apiRequest(`${API_BASE}/sandboxes/${id}`),
|
|
177
762
|
apiRequest(`${API_BASE}/sandboxes/${id}/items`),
|
|
178
|
-
apiRequest(`${API_BASE}/diaries?sandbox_id=${id}`)
|
|
763
|
+
apiRequest(`${API_BASE}/diaries?sandbox_id=${id}`),
|
|
764
|
+
apiRequest(`${API_BASE}/sandboxes/${id}/entities`),
|
|
765
|
+
apiRequest(`${API_BASE}/sandboxes/${id}/entities/stats`),
|
|
179
766
|
]);
|
|
180
767
|
state.currentSandbox = { ...sandbox, items, diaries };
|
|
768
|
+
state.nodeEntities = entities;
|
|
769
|
+
state.nodeEntityStats = entityStats;
|
|
181
770
|
|
|
182
771
|
document.getElementById('sandbox-title').textContent = sandbox.name;
|
|
772
|
+
applyWorkTreeViewMode(state.workTreeViewMode || 'full');
|
|
773
|
+
applyWorkItemAssigneeToggle();
|
|
774
|
+
applySandboxLayoutMode();
|
|
775
|
+
applySandboxFullscreenState();
|
|
183
776
|
renderSandboxOverview();
|
|
184
777
|
renderWorkTree();
|
|
185
778
|
loadSandboxChats(id);
|
|
@@ -195,11 +788,14 @@ function renderSandboxOverview() {
|
|
|
195
788
|
inProgress: items.filter((i) => i.status === 'in_progress').length,
|
|
196
789
|
done: items.filter((i) => i.status === 'done').length,
|
|
197
790
|
};
|
|
791
|
+
const entityStats = state.nodeEntityStats || { issue: {}, knowledge: {}, capability: {} };
|
|
198
792
|
container.innerHTML = `
|
|
199
793
|
<div class="summary-card"><div class="label">任务总数</div><div class="value">${items.length}</div></div>
|
|
200
794
|
<div class="summary-card"><div class="label">待处理/进行中</div><div class="value">${byStatus.pending}/${byStatus.inProgress}</div></div>
|
|
201
795
|
<div class="summary-card"><div class="label">已完成</div><div class="value">${byStatus.done}</div></div>
|
|
202
796
|
<div class="summary-card"><div class="label">关联日记</div><div class="value">${diaries.length}</div></div>
|
|
797
|
+
<div class="summary-card"><div class="label">Issue(Open)</div><div class="value">${entityStats.issue?.total || 0}(${(entityStats.issue?.open || 0) + (entityStats.issue?.in_progress || 0) + (entityStats.issue?.blocked || 0)})</div></div>
|
|
798
|
+
<div class="summary-card"><div class="label">Knowledge / Capability</div><div class="value">${entityStats.knowledge?.total || 0} / ${entityStats.capability?.total || 0}</div></div>
|
|
203
799
|
`;
|
|
204
800
|
}
|
|
205
801
|
|
|
@@ -252,6 +848,10 @@ function renderAIActionMessage(action) {
|
|
|
252
848
|
const status = getExecutionStatus(action);
|
|
253
849
|
const statusIcon = status === 'success' ? '✅' : status === 'partial' ? '⚠️' : '❌';
|
|
254
850
|
const statusClass = status === 'success' ? 'is-success' : status === 'partial' ? 'is-partial' : 'is-failed';
|
|
851
|
+
const details = Array.isArray(action?.execution_result?.details) ? action.execution_result.details : [];
|
|
852
|
+
const detailsHtml = details.length
|
|
853
|
+
? `<div class="result-detail-list">${details.map((line) => `<div class="result-detail-item">${escapeHtml(String(line))}</div>`).join('')}</div>`
|
|
854
|
+
: '';
|
|
255
855
|
const undoBtn = action.operationId
|
|
256
856
|
? `<button class="btn btn-sm btn-undo" data-operation-id="${action.operationId}" onclick="undoOperation('${action.operationId}', this)">撤销</button>`
|
|
257
857
|
: '';
|
|
@@ -261,6 +861,7 @@ function renderAIActionMessage(action) {
|
|
|
261
861
|
<span class="result-text">${escapeHtml(action.observation || '操作完成')}</span>
|
|
262
862
|
${undoBtn}
|
|
263
863
|
</div>
|
|
864
|
+
${detailsHtml}
|
|
264
865
|
</div>`;
|
|
265
866
|
}
|
|
266
867
|
|
|
@@ -397,6 +998,10 @@ function isKeyOperation(op, before, after) {
|
|
|
397
998
|
function formatDiffLabel(key) {
|
|
398
999
|
const labels = {
|
|
399
1000
|
name: '名称',
|
|
1001
|
+
title: '标题',
|
|
1002
|
+
entity_type: '类型',
|
|
1003
|
+
work_item_id: '节点ID',
|
|
1004
|
+
content_md: '内容',
|
|
400
1005
|
status: '状态',
|
|
401
1006
|
priority: '优先级',
|
|
402
1007
|
assignee: '负责人',
|
|
@@ -406,8 +1011,29 @@ function formatDiffLabel(key) {
|
|
|
406
1011
|
return labels[key] || key;
|
|
407
1012
|
}
|
|
408
1013
|
|
|
409
|
-
function
|
|
410
|
-
const
|
|
1014
|
+
function getOperationTarget(before, after) {
|
|
1015
|
+
const recordType = String(after?.record_type || before?.record_type || 'work_item');
|
|
1016
|
+
if (recordType === 'node_entity') {
|
|
1017
|
+
const entityType = String(after?.entity_type || before?.entity_type || 'entity');
|
|
1018
|
+
return {
|
|
1019
|
+
recordType,
|
|
1020
|
+
typeLabel: entityType.toUpperCase(),
|
|
1021
|
+
name: String(after?.title || before?.title || '-'),
|
|
1022
|
+
scopeLabel: `节点 ${String(after?.work_item_id || before?.work_item_id || '-')}`,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
recordType: 'work_item',
|
|
1027
|
+
typeLabel: 'WORK_ITEM',
|
|
1028
|
+
name: String(after?.name || before?.name || '-'),
|
|
1029
|
+
scopeLabel: `父节点 ${String(after?.parent_id || before?.parent_id || 'root')}`,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function buildUpdateDiffLines(before, after, recordType = 'work_item') {
|
|
1034
|
+
const trackedFields = recordType === 'node_entity'
|
|
1035
|
+
? ['title', 'status', 'priority', 'assignee', 'content_md', 'work_item_id']
|
|
1036
|
+
: ['name', 'status', 'priority', 'assignee', 'parent_id', 'description'];
|
|
411
1037
|
return trackedFields
|
|
412
1038
|
.filter((key) => (before?.[key] ?? '') !== (after?.[key] ?? ''))
|
|
413
1039
|
.map((key) => {
|
|
@@ -466,8 +1092,8 @@ function generateWeeklySummaryMarkdown(mode = 'management') {
|
|
|
466
1092
|
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
467
1093
|
const before = parseOpJson(op.data_before);
|
|
468
1094
|
const after = parseOpJson(op.data_after);
|
|
469
|
-
const
|
|
470
|
-
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${
|
|
1095
|
+
const target = getOperationTarget(before, after);
|
|
1096
|
+
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${target.typeLabel} ${target.name}`);
|
|
471
1097
|
}
|
|
472
1098
|
} else {
|
|
473
1099
|
lines.push('');
|
|
@@ -480,9 +1106,9 @@ function generateWeeklySummaryMarkdown(mode = 'management') {
|
|
|
480
1106
|
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
481
1107
|
const before = parseOpJson(op.data_before);
|
|
482
1108
|
const after = parseOpJson(op.data_after);
|
|
483
|
-
const
|
|
484
|
-
const diffLines = op.operation_type === 'update' ? buildUpdateDiffLines(before, after) : [];
|
|
485
|
-
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${
|
|
1109
|
+
const target = getOperationTarget(before, after);
|
|
1110
|
+
const diffLines = op.operation_type === 'update' ? buildUpdateDiffLines(before, after, target.recordType) : [];
|
|
1111
|
+
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${target.typeLabel} ${target.name}`);
|
|
486
1112
|
if (diffLines.length > 0) {
|
|
487
1113
|
for (const line of diffLines.slice(0, 3)) {
|
|
488
1114
|
lines.push(` - ${line}`);
|
|
@@ -514,26 +1140,28 @@ function renderChanges() {
|
|
|
514
1140
|
const items = ops.map((op) => {
|
|
515
1141
|
const before = parseOpJson(op.data_before);
|
|
516
1142
|
const after = parseOpJson(op.data_after);
|
|
1143
|
+
const target = getOperationTarget(before, after);
|
|
517
1144
|
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
518
1145
|
let detail = '';
|
|
519
1146
|
let diffLinesHtml = '';
|
|
520
1147
|
|
|
521
1148
|
if (op.operation_type === 'create') {
|
|
522
|
-
detail =
|
|
1149
|
+
detail = `创建${safeText(target.typeLabel)}:${safeText(target.name)}`;
|
|
523
1150
|
} else if (op.operation_type === 'update') {
|
|
524
|
-
detail =
|
|
525
|
-
const diffLines = buildUpdateDiffLines(before, after);
|
|
1151
|
+
detail = `更新${safeText(target.typeLabel)}:${safeText(target.name)}`;
|
|
1152
|
+
const diffLines = buildUpdateDiffLines(before, after, target.recordType);
|
|
526
1153
|
if (diffLines.length > 0) {
|
|
527
1154
|
diffLinesHtml = `<div class="change-diff-list">${diffLines.map((line) => `<div class="change-diff-line">${line}</div>`).join('')}</div>`;
|
|
528
1155
|
}
|
|
529
1156
|
} else if (op.operation_type === 'delete') {
|
|
530
|
-
detail =
|
|
1157
|
+
detail = `删除${safeText(target.typeLabel)}:${safeText(target.name)}`;
|
|
531
1158
|
}
|
|
532
1159
|
return `
|
|
533
1160
|
<div class="change-item">
|
|
534
1161
|
<div class="change-meta">
|
|
535
1162
|
<span class="change-type ${safeText(op.operation_type)}">${safeText(op.operation_type)}</span>
|
|
536
1163
|
<span>${safeText(sandboxName)}</span>
|
|
1164
|
+
<span class="change-target">${safeText(target.scopeLabel)}</span>
|
|
537
1165
|
<span>${new Date(op.created_at).toLocaleTimeString()}</span>
|
|
538
1166
|
</div>
|
|
539
1167
|
<div class="change-detail">${detail}</div>
|
|
@@ -608,6 +1236,7 @@ function editWorkItem(id) {
|
|
|
608
1236
|
|
|
609
1237
|
function initApp() {
|
|
610
1238
|
const hash = window.location.hash;
|
|
1239
|
+
loadWorkItemAssigneePreference();
|
|
611
1240
|
|
|
612
1241
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
613
1242
|
link.addEventListener('click', (e) => {
|
|
@@ -624,8 +1253,8 @@ function initApp() {
|
|
|
624
1253
|
document.getElementById('sandbox-dialog').close();
|
|
625
1254
|
});
|
|
626
1255
|
|
|
627
|
-
|
|
628
|
-
e
|
|
1256
|
+
const createSandbox = async (e) => {
|
|
1257
|
+
e?.preventDefault?.();
|
|
629
1258
|
const name = document.getElementById('new-sandbox-name').value;
|
|
630
1259
|
const description = document.getElementById('new-sandbox-desc').value;
|
|
631
1260
|
const dialog = document.getElementById('sandbox-dialog');
|
|
@@ -642,7 +1271,10 @@ function initApp() {
|
|
|
642
1271
|
} catch (error) {
|
|
643
1272
|
alert('创建失败: ' + error.message);
|
|
644
1273
|
}
|
|
645
|
-
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
document.getElementById('sandbox-dialog')?.addEventListener('submit', createSandbox);
|
|
1277
|
+
document.getElementById('confirm-sandbox-btn')?.addEventListener('click', createSandbox);
|
|
646
1278
|
|
|
647
1279
|
document.getElementById('add-item-btn')?.addEventListener('click', () => {
|
|
648
1280
|
document.getElementById('item-dialog-title').textContent = '添加任务';
|
|
@@ -655,10 +1287,154 @@ function initApp() {
|
|
|
655
1287
|
document.getElementById('new-item-parent').value = '';
|
|
656
1288
|
document.getElementById('item-dialog').showModal();
|
|
657
1289
|
});
|
|
1290
|
+
|
|
1291
|
+
document.getElementById('toggle-sandbox-present-btn')?.addEventListener('click', () => {
|
|
1292
|
+
state.sandboxPresentationMode = !state.sandboxPresentationMode;
|
|
1293
|
+
applySandboxLayoutMode();
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
document.getElementById('toggle-sandbox-fullscreen-btn')?.addEventListener('click', async () => {
|
|
1297
|
+
await toggleSandboxFullscreen();
|
|
1298
|
+
applySandboxFullscreenState();
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
document.addEventListener('fullscreenchange', () => {
|
|
1302
|
+
applySandboxFullscreenState();
|
|
1303
|
+
});
|
|
658
1304
|
|
|
659
1305
|
document.getElementById('cancel-item-btn')?.addEventListener('click', () => {
|
|
660
1306
|
document.getElementById('item-dialog').close();
|
|
661
1307
|
});
|
|
1308
|
+
|
|
1309
|
+
document.getElementById('close-node-drawer-btn')?.addEventListener('click', () => {
|
|
1310
|
+
closeNodeEntityDrawer();
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
document.getElementById('toggle-node-entity-form-btn')?.addEventListener('click', () => {
|
|
1314
|
+
setNodeEntityFormExpanded(!state.nodeEntityFormExpanded);
|
|
1315
|
+
if (state.nodeEntityFormExpanded) {
|
|
1316
|
+
document.getElementById('entity-title-input')?.focus();
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
document.getElementById('entity-type-select')?.addEventListener('change', () => {
|
|
1321
|
+
applyEntityFormMode();
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
document.getElementById('entity-filter-tabs')?.addEventListener('click', (event) => {
|
|
1325
|
+
const target = event.target;
|
|
1326
|
+
if (!(target instanceof HTMLElement)) return;
|
|
1327
|
+
const btn = target.closest('[data-entity-filter]');
|
|
1328
|
+
if (!(btn instanceof HTMLElement)) return;
|
|
1329
|
+
const filter = btn.getAttribute('data-entity-filter') || 'all';
|
|
1330
|
+
state.nodeEntityFilter = filter;
|
|
1331
|
+
renderNodeEntityFilterTabs();
|
|
1332
|
+
if (state.selectedNodeId) {
|
|
1333
|
+
renderNodeEntityList(state.selectedNodeId);
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
document.getElementById('cancel-edit-entity-btn')?.addEventListener('click', () => {
|
|
1338
|
+
resetNodeEntityForm();
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
document.getElementById('create-node-entity-btn')?.addEventListener('click', async () => {
|
|
1342
|
+
if (!state.currentSandbox || !state.selectedNodeId) return;
|
|
1343
|
+
const btn = document.getElementById('create-node-entity-btn');
|
|
1344
|
+
const title = document.getElementById('entity-title-input').value.trim();
|
|
1345
|
+
const entity_type = document.getElementById('entity-type-select').value;
|
|
1346
|
+
if (!title) return;
|
|
1347
|
+
const rawStatus = document.getElementById('entity-status-select').value || '';
|
|
1348
|
+
const status = rawStatus || (entity_type === 'issue' ? 'open' : entity_type === 'capability' ? 'building' : '');
|
|
1349
|
+
const payload = {
|
|
1350
|
+
work_item_id: state.selectedNodeId,
|
|
1351
|
+
entity_type,
|
|
1352
|
+
title,
|
|
1353
|
+
content_md: document.getElementById('entity-content-input').value || '',
|
|
1354
|
+
assignee: document.getElementById('entity-assignee-input').value || '',
|
|
1355
|
+
status,
|
|
1356
|
+
priority: document.getElementById('entity-priority-select').value || '',
|
|
1357
|
+
capability_type: document.getElementById('entity-capability-type-input').value || '',
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
const isEditing = Boolean(state.editingNodeEntityId);
|
|
1361
|
+
setButtonState(btn, { disabled: true, text: isEditing ? '保存中...' : '添加中...' });
|
|
1362
|
+
try {
|
|
1363
|
+
if (isEditing) {
|
|
1364
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities/${state.editingNodeEntityId}`, {
|
|
1365
|
+
method: 'PUT',
|
|
1366
|
+
body: JSON.stringify(payload),
|
|
1367
|
+
});
|
|
1368
|
+
} else {
|
|
1369
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities`, {
|
|
1370
|
+
method: 'POST',
|
|
1371
|
+
body: JSON.stringify(payload),
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
await loadSandbox(state.currentSandbox.id);
|
|
1375
|
+
showNodeEntityDrawer(state.selectedNodeId);
|
|
1376
|
+
resetNodeEntityForm();
|
|
1377
|
+
} finally {
|
|
1378
|
+
const defaultText = state.editingNodeEntityId ? '保存修改' : '添加记录';
|
|
1379
|
+
setButtonState(btn, { disabled: false, text: defaultText });
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
const entitySubmitByShortcut = async (event) => {
|
|
1384
|
+
const isSubmit = event.key === 'Enter' && (event.ctrlKey || event.metaKey);
|
|
1385
|
+
if (!isSubmit) return;
|
|
1386
|
+
event.preventDefault();
|
|
1387
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
1388
|
+
const form = document.getElementById('drawer-create-form');
|
|
1389
|
+
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
1390
|
+
if (drawer?.classList.contains('hidden')) return;
|
|
1391
|
+
if (form?.classList.contains('hidden')) return;
|
|
1392
|
+
if (!(submitBtn instanceof HTMLButtonElement) || submitBtn.disabled) return;
|
|
1393
|
+
submitBtn.click();
|
|
1394
|
+
};
|
|
1395
|
+
document.getElementById('entity-title-input')?.addEventListener('keydown', entitySubmitByShortcut);
|
|
1396
|
+
document.getElementById('entity-content-input')?.addEventListener('keydown', entitySubmitByShortcut);
|
|
1397
|
+
|
|
1398
|
+
document.addEventListener('keydown', (event) => {
|
|
1399
|
+
if (event.key !== 'Escape') return;
|
|
1400
|
+
if (quickChatPopover) {
|
|
1401
|
+
event.preventDefault();
|
|
1402
|
+
event.stopPropagation();
|
|
1403
|
+
closeQuickChatPopover();
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
1407
|
+
if (drawer && !drawer.classList.contains('hidden')) {
|
|
1408
|
+
event.preventDefault();
|
|
1409
|
+
event.stopPropagation();
|
|
1410
|
+
closeNodeEntityDrawer();
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (state.sandboxFullscreenMode) {
|
|
1414
|
+
event.preventDefault();
|
|
1415
|
+
event.stopPropagation();
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
document.addEventListener('click', (event) => {
|
|
1420
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
1421
|
+
const workTree = document.getElementById('work-tree');
|
|
1422
|
+
if (!drawer || drawer.classList.contains('hidden')) return;
|
|
1423
|
+
const target = event.target;
|
|
1424
|
+
if (!(target instanceof Node)) return;
|
|
1425
|
+
if (drawer.contains(target)) return;
|
|
1426
|
+
if (workTree?.contains(target)) return;
|
|
1427
|
+
closeNodeEntityDrawer();
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
document.addEventListener('click', (event) => {
|
|
1431
|
+
if (!quickChatPopover) return;
|
|
1432
|
+
const target = event.target;
|
|
1433
|
+
if (!(target instanceof Node)) return;
|
|
1434
|
+
if (quickChatPopover.contains(target)) return;
|
|
1435
|
+
if (target instanceof HTMLElement && target.closest('[data-action="quick-chat"]')) return;
|
|
1436
|
+
closeQuickChatPopover();
|
|
1437
|
+
});
|
|
662
1438
|
|
|
663
1439
|
document.getElementById('confirm-item-btn')?.addEventListener('click', async () => {
|
|
664
1440
|
const dialog = document.getElementById('item-dialog');
|
|
@@ -754,48 +1530,8 @@ function initApp() {
|
|
|
754
1530
|
const input = document.getElementById('sandbox-chat-input');
|
|
755
1531
|
const content = input.value.trim();
|
|
756
1532
|
if (!content || !state.currentSandbox) return;
|
|
757
|
-
|
|
758
|
-
const btn = document.getElementById('sandbox-send-btn');
|
|
759
|
-
const messages = document.getElementById('sandbox-chat-messages');
|
|
760
|
-
|
|
761
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(content)}</div>`);
|
|
762
|
-
const loadingMessage = appendLoadingMessage(messages);
|
|
763
|
-
messages.scrollTop = messages.scrollHeight;
|
|
764
1533
|
input.value = '';
|
|
765
|
-
|
|
766
|
-
const originalText = btn.textContent;
|
|
767
|
-
setButtonState(btn, { disabled: true, text: '思考中' });
|
|
768
|
-
|
|
769
|
-
try {
|
|
770
|
-
const history = state.chats
|
|
771
|
-
.filter(c => c.role)
|
|
772
|
-
.slice(-10)
|
|
773
|
-
.map(c => ({ role: c.role, content: typeof c.content === 'string' ? c.content : JSON.stringify(c.content) }));
|
|
774
|
-
|
|
775
|
-
const result = await apiRequest(`${API_BASE}/chats/react`, {
|
|
776
|
-
method: 'POST',
|
|
777
|
-
body: JSON.stringify({
|
|
778
|
-
content,
|
|
779
|
-
sandbox_id: state.currentSandbox.id,
|
|
780
|
-
history,
|
|
781
|
-
pending_action: state.pendingAction
|
|
782
|
-
}),
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
console.log('Chat result:', result);
|
|
786
|
-
|
|
787
|
-
loadingMessage?.remove();
|
|
788
|
-
|
|
789
|
-
await handleAIAction(result.action, state.currentSandbox.id);
|
|
790
|
-
await loadSandbox(state.currentSandbox.id);
|
|
791
|
-
} catch (error) {
|
|
792
|
-
console.error('发送消息失败:', error);
|
|
793
|
-
loadingMessage?.remove();
|
|
794
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
795
|
-
messages.scrollTop = messages.scrollHeight;
|
|
796
|
-
} finally {
|
|
797
|
-
setButtonState(btn, { disabled: false, text: originalText || '发送' });
|
|
798
|
-
}
|
|
1534
|
+
await sendSandboxChatMessage(content);
|
|
799
1535
|
});
|
|
800
1536
|
|
|
801
1537
|
async function handleAIAction(action, sandboxId) {
|
|
@@ -904,6 +1640,8 @@ function initApp() {
|
|
|
904
1640
|
state.pendingAction = null;
|
|
905
1641
|
}
|
|
906
1642
|
}
|
|
1643
|
+
|
|
1644
|
+
sandboxActionHandler = handleAIAction;
|
|
907
1645
|
|
|
908
1646
|
document.getElementById('sandbox-chat-input')?.addEventListener('keypress', (e) => {
|
|
909
1647
|
if (e.key === 'Enter') {
|
|
@@ -959,6 +1697,18 @@ function initApp() {
|
|
|
959
1697
|
renderWorkTree();
|
|
960
1698
|
});
|
|
961
1699
|
|
|
1700
|
+
document.getElementById('work-tree-view-mode')?.addEventListener('change', (e) => {
|
|
1701
|
+
const nextMode = e.target.value || 'full';
|
|
1702
|
+
applyWorkTreeViewMode(nextMode);
|
|
1703
|
+
renderWorkTree();
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
document.getElementById('work-item-show-assignee-toggle')?.addEventListener('change', (e) => {
|
|
1707
|
+
state.workItemShowAssignee = Boolean(e.target.checked);
|
|
1708
|
+
persistWorkItemAssigneePreference();
|
|
1709
|
+
renderWorkTree();
|
|
1710
|
+
});
|
|
1711
|
+
|
|
962
1712
|
document.getElementById('diary-search')?.addEventListener('input', (e) => {
|
|
963
1713
|
state.diarySearch = e.target.value || '';
|
|
964
1714
|
renderDiaries();
|
|
@@ -1046,12 +1796,26 @@ function initApp() {
|
|
|
1046
1796
|
await navigator.clipboard.writeText(content);
|
|
1047
1797
|
alert('周报摘要已复制');
|
|
1048
1798
|
});
|
|
1799
|
+
|
|
1800
|
+
window.addEventListener('resize', () => {
|
|
1801
|
+
if (!state.currentSandbox) return;
|
|
1802
|
+
if (resizeRenderTimer) {
|
|
1803
|
+
clearTimeout(resizeRenderTimer);
|
|
1804
|
+
}
|
|
1805
|
+
resizeRenderTimer = setTimeout(() => {
|
|
1806
|
+
renderWorkTree();
|
|
1807
|
+
}, 120);
|
|
1808
|
+
});
|
|
1049
1809
|
|
|
1050
1810
|
window.addEventListener('hashchange', handleRoute);
|
|
1051
1811
|
handleRoute();
|
|
1052
1812
|
|
|
1053
1813
|
async function handleRoute() {
|
|
1054
1814
|
const hash = window.location.hash.slice(1) || '/';
|
|
1815
|
+
const fullscreenRoot = getSandboxFullscreenElement();
|
|
1816
|
+
if (!hash.startsWith('/sandbox/') && fullscreenRoot && document.fullscreenElement === fullscreenRoot) {
|
|
1817
|
+
await document.exitFullscreen();
|
|
1818
|
+
}
|
|
1055
1819
|
|
|
1056
1820
|
if (hash === '/') {
|
|
1057
1821
|
showPage('home');
|