@qnote/q-ai-note 1.0.4 → 1.0.6
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 +59 -0
- package/dist/cli-server.d.ts +3 -0
- package/dist/cli-server.d.ts.map +1 -0
- package/dist/cli-server.js +79 -0
- package/dist/cli-server.js.map +1 -0
- package/dist/cli.js +77 -15
- package/dist/cli.js.map +1 -1
- package/dist/server/api/chat.d.ts.map +1 -1
- package/dist/server/api/chat.js +6 -112
- package/dist/server/api/chat.js.map +1 -1
- package/dist/server/api/diary.d.ts.map +1 -1
- package/dist/server/api/diary.js +34 -0
- package/dist/server/api/diary.js.map +1 -1
- package/dist/server/db.d.ts.map +1 -1
- package/dist/server/db.js +8 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.d.ts +7 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +65 -5
- package/dist/server/index.js.map +1 -1
- package/dist/web/app.js +822 -124
- package/dist/web/index.html +76 -23
- package/dist/web/styles.css +434 -13
- package/dist/web/vueRenderers.js +294 -56
- package/package.json +6 -3
package/dist/web/app.js
CHANGED
|
@@ -9,6 +9,7 @@ const state = {
|
|
|
9
9
|
operations: [],
|
|
10
10
|
chats: [],
|
|
11
11
|
settings: {},
|
|
12
|
+
readonly: false,
|
|
12
13
|
pendingAction: null,
|
|
13
14
|
nodeEntities: [],
|
|
14
15
|
nodeEntityStats: null,
|
|
@@ -16,14 +17,18 @@ const state = {
|
|
|
16
17
|
nodeEntityFilter: 'all',
|
|
17
18
|
editingNodeEntityId: null,
|
|
18
19
|
nodeEntityFormExpanded: false,
|
|
19
|
-
|
|
20
|
+
sandboxChatVisible: false,
|
|
21
|
+
sandboxChatVisibleBeforeFullscreen: false,
|
|
20
22
|
sandboxFullscreenMode: false,
|
|
21
23
|
workTreeViewMode: 'full',
|
|
24
|
+
workItemElementPreviewMode: 'none',
|
|
22
25
|
workItemShowAssignee: false,
|
|
23
26
|
workItemSearch: '',
|
|
24
27
|
workItemStatusFilter: 'all',
|
|
25
28
|
diarySearch: '',
|
|
29
|
+
diarySandboxFilter: '',
|
|
26
30
|
diaryProcessedFilter: 'all',
|
|
31
|
+
diaryWorkItemNameMap: {},
|
|
27
32
|
changesSandboxFilter: '',
|
|
28
33
|
changesTypeFilter: 'all',
|
|
29
34
|
changesQuickFilter: 'all',
|
|
@@ -35,6 +40,7 @@ let sandboxActionHandler = null;
|
|
|
35
40
|
let sandboxEscLocked = false;
|
|
36
41
|
let resizeRenderTimer = null;
|
|
37
42
|
const WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY = 'q-ai-note.work-item.show-assignee';
|
|
43
|
+
const WORK_TREE_VIEW_MODE_STORAGE_KEY = 'q-ai-note.work-tree.view-mode';
|
|
38
44
|
|
|
39
45
|
function loadWorkItemAssigneePreference() {
|
|
40
46
|
try {
|
|
@@ -57,14 +63,78 @@ function persistWorkItemAssigneePreference() {
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
function
|
|
66
|
+
function loadWorkTreeViewModePreference() {
|
|
67
|
+
try {
|
|
68
|
+
const raw = String(window.localStorage.getItem(WORK_TREE_VIEW_MODE_STORAGE_KEY) || '').trim();
|
|
69
|
+
if (raw === 'full' || raw === 'report' || raw === 'dense') {
|
|
70
|
+
state.workTreeViewMode = raw;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Ignore storage failures in restricted environments.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function persistWorkTreeViewModePreference() {
|
|
78
|
+
try {
|
|
79
|
+
window.localStorage.setItem(WORK_TREE_VIEW_MODE_STORAGE_KEY, state.workTreeViewMode || 'full');
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore storage failures in restricted environments.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function applySandboxChatVisibility() {
|
|
61
86
|
const layout = document.getElementById('sandbox-layout');
|
|
62
|
-
const toggleBtn = document.getElementById('toggle-sandbox-
|
|
87
|
+
const toggleBtn = document.getElementById('toggle-sandbox-chat-btn');
|
|
63
88
|
if (!layout || !toggleBtn) return;
|
|
64
|
-
const
|
|
65
|
-
layout.classList.toggle('
|
|
66
|
-
toggleBtn.
|
|
67
|
-
toggleBtn.
|
|
89
|
+
const shouldShow = !state.readonly && Boolean(state.sandboxChatVisible);
|
|
90
|
+
layout.classList.toggle('show-chat', shouldShow);
|
|
91
|
+
toggleBtn.classList.toggle('hidden', state.readonly);
|
|
92
|
+
toggleBtn.innerHTML = shouldShow
|
|
93
|
+
? '<span class="icon" aria-hidden="true">🤖</span><span>隐藏 AI 助手</span>'
|
|
94
|
+
: '<span class="icon" aria-hidden="true">🤖</span><span>显示 AI 助手</span>';
|
|
95
|
+
toggleBtn.setAttribute('aria-pressed', shouldShow ? 'true' : 'false');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function loadRuntimeMode() {
|
|
99
|
+
try {
|
|
100
|
+
const runtime = await apiRequest(`${API_BASE}/runtime`);
|
|
101
|
+
state.readonly = Boolean(runtime?.readonly);
|
|
102
|
+
} catch {
|
|
103
|
+
state.readonly = false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setHiddenById(id, hidden = true) {
|
|
108
|
+
const el = document.getElementById(id);
|
|
109
|
+
if (!el) return;
|
|
110
|
+
el.classList.toggle('hidden', hidden);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyReadonlyMode() {
|
|
114
|
+
document.body.classList.toggle('readonly-mode', state.readonly);
|
|
115
|
+
const settingsNav = document.querySelector('[data-nav="settings"]');
|
|
116
|
+
if (settingsNav instanceof HTMLElement) {
|
|
117
|
+
settingsNav.classList.toggle('hidden', state.readonly);
|
|
118
|
+
}
|
|
119
|
+
setHiddenById('page-settings', state.readonly);
|
|
120
|
+
setHiddenById('add-sandbox-btn', state.readonly);
|
|
121
|
+
setHiddenById('add-item-btn', state.readonly);
|
|
122
|
+
setHiddenById('toggle-node-entity-form-btn', state.readonly);
|
|
123
|
+
setHiddenById('drawer-diary-save-btn', state.readonly);
|
|
124
|
+
setHiddenById('save-diary-btn', state.readonly);
|
|
125
|
+
const diaryForm = document.querySelector('#page-diaries .diary-form');
|
|
126
|
+
if (diaryForm instanceof HTMLElement) {
|
|
127
|
+
diaryForm.classList.toggle('hidden', state.readonly);
|
|
128
|
+
}
|
|
129
|
+
const drawerDiaryForm = document.querySelector('#node-entity-drawer .drawer-diary-quick-form');
|
|
130
|
+
if (drawerDiaryForm instanceof HTMLElement) {
|
|
131
|
+
drawerDiaryForm.classList.toggle('hidden', state.readonly);
|
|
132
|
+
}
|
|
133
|
+
if (state.readonly) {
|
|
134
|
+
state.sandboxChatVisible = false;
|
|
135
|
+
closeQuickChatPopover();
|
|
136
|
+
}
|
|
137
|
+
applySandboxChatVisibility();
|
|
68
138
|
}
|
|
69
139
|
|
|
70
140
|
function getSandboxLayoutElement() {
|
|
@@ -97,7 +167,14 @@ function applySandboxFullscreenState() {
|
|
|
97
167
|
const layout = getSandboxLayoutElement();
|
|
98
168
|
const fullscreenRoot = getSandboxFullscreenElement();
|
|
99
169
|
const toggleBtn = document.getElementById('toggle-sandbox-fullscreen-btn');
|
|
170
|
+
const wasFullscreen = Boolean(state.sandboxFullscreenMode);
|
|
100
171
|
const isFullscreen = Boolean(fullscreenRoot && document.fullscreenElement === fullscreenRoot);
|
|
172
|
+
if (!wasFullscreen && isFullscreen) {
|
|
173
|
+
state.sandboxChatVisibleBeforeFullscreen = Boolean(state.sandboxChatVisible);
|
|
174
|
+
state.sandboxChatVisible = false;
|
|
175
|
+
} else if (wasFullscreen && !isFullscreen) {
|
|
176
|
+
state.sandboxChatVisible = Boolean(state.sandboxChatVisibleBeforeFullscreen);
|
|
177
|
+
}
|
|
101
178
|
state.sandboxFullscreenMode = isFullscreen;
|
|
102
179
|
if (layout) {
|
|
103
180
|
layout.classList.toggle('is-fullscreen', isFullscreen);
|
|
@@ -109,9 +186,22 @@ function applySandboxFullscreenState() {
|
|
|
109
186
|
toggleBtn.textContent = isFullscreen ? '退出全屏' : '全屏';
|
|
110
187
|
toggleBtn.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
|
|
111
188
|
}
|
|
189
|
+
applySandboxChatVisibility();
|
|
190
|
+
applySandboxLayoutHeight();
|
|
112
191
|
void syncSandboxEscLock(isFullscreen);
|
|
113
192
|
}
|
|
114
193
|
|
|
194
|
+
function applySandboxLayoutHeight() {
|
|
195
|
+
const layout = getSandboxLayoutElement();
|
|
196
|
+
if (!(layout instanceof HTMLElement)) return;
|
|
197
|
+
const rect = layout.getBoundingClientRect();
|
|
198
|
+
const viewportHeight = Math.floor(window.visualViewport?.height || window.innerHeight || 0);
|
|
199
|
+
if (!Number.isFinite(viewportHeight) || viewportHeight <= 0) return;
|
|
200
|
+
const bottomGap = state.sandboxFullscreenMode ? 8 : 10;
|
|
201
|
+
const nextHeight = Math.max(320, viewportHeight - Math.floor(rect.top) - bottomGap);
|
|
202
|
+
layout.style.height = `${nextHeight}px`;
|
|
203
|
+
}
|
|
204
|
+
|
|
115
205
|
async function toggleSandboxFullscreen() {
|
|
116
206
|
const fullscreenRoot = getSandboxFullscreenElement();
|
|
117
207
|
if (!fullscreenRoot) return;
|
|
@@ -160,12 +250,14 @@ function applyWorkItemFilters(items) {
|
|
|
160
250
|
function applyDiaryFilters(diaries) {
|
|
161
251
|
const query = state.diarySearch.trim().toLowerCase();
|
|
162
252
|
const processed = state.diaryProcessedFilter;
|
|
253
|
+
const sandboxId = state.diarySandboxFilter;
|
|
163
254
|
return diaries.filter((diary) => {
|
|
164
255
|
const queryMatched = !query || `${diary.content || ''}`.toLowerCase().includes(query);
|
|
256
|
+
const sandboxMatched = !sandboxId || String(diary.sandbox_id || '') === sandboxId;
|
|
165
257
|
const statusMatched = processed === 'all'
|
|
166
258
|
|| (processed === 'processed' && diary.processed)
|
|
167
259
|
|| (processed === 'unprocessed' && !diary.processed);
|
|
168
|
-
return queryMatched && statusMatched;
|
|
260
|
+
return queryMatched && sandboxMatched && statusMatched;
|
|
169
261
|
});
|
|
170
262
|
}
|
|
171
263
|
|
|
@@ -200,6 +292,13 @@ function applyWorkItemAssigneeToggle() {
|
|
|
200
292
|
}
|
|
201
293
|
}
|
|
202
294
|
|
|
295
|
+
function applyWorkItemElementPreviewMode() {
|
|
296
|
+
const selector = document.getElementById('work-item-element-preview-mode');
|
|
297
|
+
if (selector instanceof HTMLSelectElement) {
|
|
298
|
+
selector.value = state.workItemElementPreviewMode || 'none';
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
203
302
|
function renderWorkTree() {
|
|
204
303
|
const tree = document.getElementById('work-tree');
|
|
205
304
|
if (!tree || !state.currentSandbox) return;
|
|
@@ -207,6 +306,7 @@ function renderWorkTree() {
|
|
|
207
306
|
const allItems = state.currentSandbox.items || [];
|
|
208
307
|
const items = applyWorkItemFilters(allItems);
|
|
209
308
|
const entitySummaryByNodeId = buildNodeEntitySummaryByNodeId();
|
|
309
|
+
const entityRowsByNodeId = buildNodeEntityRowsByNodeId();
|
|
210
310
|
|
|
211
311
|
if (expandedNodes.size === 0 && allItems.length > 0) {
|
|
212
312
|
if (state.workTreeViewMode === 'report') {
|
|
@@ -217,7 +317,7 @@ function renderWorkTree() {
|
|
|
217
317
|
}
|
|
218
318
|
|
|
219
319
|
if (allItems.length === 0) {
|
|
220
|
-
tree.innerHTML =
|
|
320
|
+
tree.innerHTML = `<div class="empty-state"><p>${state.readonly ? '暂无任务' : '点击上方"添加"按钮创建第一个任务'}</p></div>`;
|
|
221
321
|
return;
|
|
222
322
|
}
|
|
223
323
|
|
|
@@ -233,7 +333,7 @@ function renderWorkTree() {
|
|
|
233
333
|
}
|
|
234
334
|
renderWorkTree();
|
|
235
335
|
},
|
|
236
|
-
onAddChild: (parentId) => {
|
|
336
|
+
onAddChild: state.readonly ? undefined : (parentId) => {
|
|
237
337
|
document.getElementById('item-dialog-title').textContent = '添加子任务';
|
|
238
338
|
document.getElementById('item-dialog').dataset.editId = '';
|
|
239
339
|
document.getElementById('new-item-name').value = '';
|
|
@@ -244,23 +344,123 @@ function renderWorkTree() {
|
|
|
244
344
|
document.getElementById('new-item-parent').value = parentId;
|
|
245
345
|
document.getElementById('item-dialog').showModal();
|
|
246
346
|
},
|
|
247
|
-
|
|
347
|
+
onAddDiary: state.readonly ? undefined : (nodeId) => {
|
|
348
|
+
showNodeEntityDrawer(nodeId, 'diary');
|
|
349
|
+
const textarea = document.getElementById('drawer-diary-content');
|
|
350
|
+
if (textarea instanceof HTMLTextAreaElement) {
|
|
351
|
+
textarea.focus();
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
onEdit: state.readonly ? undefined : (id) => {
|
|
248
355
|
editWorkItem(id);
|
|
249
356
|
},
|
|
250
357
|
onSelect: (id) => {
|
|
251
358
|
showNodeEntityDrawer(id);
|
|
252
359
|
},
|
|
253
|
-
|
|
360
|
+
onSelectEntity: (nodeId, entityType) => {
|
|
361
|
+
showNodeEntityDrawer(nodeId, entityType || 'all');
|
|
362
|
+
},
|
|
363
|
+
onMoveNode: state.readonly ? undefined : async (dragNodeId, newParentId) => {
|
|
364
|
+
if (!state.currentSandbox) return;
|
|
365
|
+
if (!dragNodeId || dragNodeId === newParentId) return;
|
|
366
|
+
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
367
|
+
const dragNode = byId.get(dragNodeId);
|
|
368
|
+
if (!dragNode) return;
|
|
369
|
+
const nextParentId = newParentId || null;
|
|
370
|
+
if (nextParentId && isDescendantNode(nextParentId, dragNodeId, byId)) {
|
|
371
|
+
alert('不能将节点拖拽到其子节点下。');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const siblingItems = (state.currentSandbox.items || [])
|
|
375
|
+
.filter((item) => (item.parent_id || null) === nextParentId && item.id !== dragNodeId)
|
|
376
|
+
.sort((a, b) => compareSiblingOrder(a, b, 'order_key'));
|
|
377
|
+
const left = siblingItems[siblingItems.length - 1] || null;
|
|
378
|
+
const nextOrderKey = rankBetween(getNodeOrderKey(left, 'order_key'), '');
|
|
379
|
+
await apiRequest(`${API_BASE}/items/${dragNodeId}`, {
|
|
380
|
+
method: 'PUT',
|
|
381
|
+
body: JSON.stringify({
|
|
382
|
+
parent_id: nextParentId,
|
|
383
|
+
extra_data: {
|
|
384
|
+
...(dragNode.extra_data || {}),
|
|
385
|
+
order_key: nextOrderKey,
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
await loadSandbox(state.currentSandbox.id);
|
|
390
|
+
},
|
|
391
|
+
onReorderSiblings: state.readonly ? undefined : async (dragNodeId, targetNodeId, position) => {
|
|
392
|
+
if (!state.currentSandbox) return;
|
|
393
|
+
if (!dragNodeId || !targetNodeId || dragNodeId === targetNodeId) return;
|
|
394
|
+
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
395
|
+
const dragNode = byId.get(dragNodeId);
|
|
396
|
+
const targetNode = byId.get(targetNodeId);
|
|
397
|
+
if (!dragNode || !targetNode) return;
|
|
398
|
+
if (isDescendantNode(targetNodeId, dragNodeId, byId)) {
|
|
399
|
+
alert('不能将节点排序到其子树内部。');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const nextParentId = targetNode.parent_id || null;
|
|
403
|
+
const siblings = (state.currentSandbox.items || [])
|
|
404
|
+
.filter((item) => (item.parent_id || null) === nextParentId && item.id !== dragNodeId)
|
|
405
|
+
.sort((a, b) => compareSiblingOrder(a, b, 'order_key'));
|
|
406
|
+
const targetIndex = siblings.findIndex((item) => item.id === targetNodeId);
|
|
407
|
+
if (targetIndex < 0) return;
|
|
408
|
+
const insertIndex = position === 'after' ? targetIndex + 1 : targetIndex;
|
|
409
|
+
const left = siblings[insertIndex - 1] || null;
|
|
410
|
+
const right = siblings[insertIndex] || null;
|
|
411
|
+
const nextOrderKey = rankBetween(getNodeOrderKey(left, 'order_key'), getNodeOrderKey(right, 'order_key'));
|
|
412
|
+
await apiRequest(`${API_BASE}/items/${dragNodeId}`, {
|
|
413
|
+
method: 'PUT',
|
|
414
|
+
body: JSON.stringify({
|
|
415
|
+
parent_id: nextParentId,
|
|
416
|
+
extra_data: {
|
|
417
|
+
...(dragNode.extra_data || {}),
|
|
418
|
+
order_key: nextOrderKey,
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
await loadSandbox(state.currentSandbox.id);
|
|
423
|
+
},
|
|
424
|
+
onReorderLanes: state.readonly ? undefined : async (dragRootId, targetRootId) => {
|
|
425
|
+
if (!state.currentSandbox) return;
|
|
426
|
+
if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
|
|
427
|
+
const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
|
|
428
|
+
const ordered = [...roots].sort((a, b) => compareSiblingOrder(a, b, 'lane_order_key'));
|
|
429
|
+
const fromIdx = ordered.findIndex((item) => item.id === dragRootId);
|
|
430
|
+
const toIdx = ordered.findIndex((item) => item.id === targetRootId);
|
|
431
|
+
if (fromIdx < 0 || toIdx < 0) return;
|
|
432
|
+
const [moved] = ordered.splice(fromIdx, 1);
|
|
433
|
+
ordered.splice(toIdx, 0, moved);
|
|
434
|
+
const movedIndex = ordered.findIndex((item) => item.id === dragRootId);
|
|
435
|
+
const left = ordered[movedIndex - 1] || null;
|
|
436
|
+
const right = ordered[movedIndex + 1] || null;
|
|
437
|
+
const nextLaneKey = rankBetween(getNodeOrderKey(left, 'lane_order_key'), getNodeOrderKey(right, 'lane_order_key'));
|
|
438
|
+
await apiRequest(`${API_BASE}/items/${dragRootId}`, {
|
|
439
|
+
method: 'PUT',
|
|
440
|
+
body: JSON.stringify({
|
|
441
|
+
extra_data: {
|
|
442
|
+
...(moved.extra_data || {}),
|
|
443
|
+
lane_order_key: nextLaneKey,
|
|
444
|
+
},
|
|
445
|
+
}),
|
|
446
|
+
});
|
|
447
|
+
await loadSandbox(state.currentSandbox.id);
|
|
448
|
+
},
|
|
449
|
+
onDelete: state.readonly ? undefined : async (id) => {
|
|
254
450
|
if (confirm('确定删除此任务?')) {
|
|
255
451
|
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
256
452
|
await loadSandbox(state.currentSandbox.id);
|
|
257
453
|
}
|
|
258
454
|
},
|
|
259
|
-
onQuickChat: (id, el) => {
|
|
455
|
+
onQuickChat: state.readonly ? undefined : (id, el) => {
|
|
260
456
|
openQuickChatPopover(id, el);
|
|
261
457
|
},
|
|
262
458
|
renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
|
|
263
459
|
showAssignee: state.workItemShowAssignee,
|
|
460
|
+
elementPreviewMode: state.workItemElementPreviewMode,
|
|
461
|
+
entityRowsByNodeId,
|
|
462
|
+
readonly: state.readonly,
|
|
463
|
+
selectedId: state.selectedNodeId || '',
|
|
264
464
|
});
|
|
265
465
|
|
|
266
466
|
populateParentSelect(allItems);
|
|
@@ -289,6 +489,87 @@ function buildNodeEntitySummaryByNodeId() {
|
|
|
289
489
|
return summaryByNodeId;
|
|
290
490
|
}
|
|
291
491
|
|
|
492
|
+
function buildNodeEntityRowsByNodeId() {
|
|
493
|
+
const rowsByNodeId = {};
|
|
494
|
+
for (const row of state.nodeEntities || []) {
|
|
495
|
+
const nodeId = String(row.work_item_id || '');
|
|
496
|
+
if (!nodeId) continue;
|
|
497
|
+
if (!rowsByNodeId[nodeId]) rowsByNodeId[nodeId] = [];
|
|
498
|
+
rowsByNodeId[nodeId].push({
|
|
499
|
+
id: row.id,
|
|
500
|
+
entity_type: row.entity_type,
|
|
501
|
+
title: row.title || '',
|
|
502
|
+
status: row.status || '',
|
|
503
|
+
capability_type: row.capability_type || '',
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return rowsByNodeId;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function isDescendantNode(candidateNodeId, parentNodeId, byId) {
|
|
510
|
+
let cursor = byId.get(candidateNodeId);
|
|
511
|
+
while (cursor && cursor.parent_id) {
|
|
512
|
+
if (cursor.parent_id === parentNodeId) return true;
|
|
513
|
+
cursor = byId.get(cursor.parent_id);
|
|
514
|
+
}
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const ORDER_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
519
|
+
const ORDER_BASE = ORDER_ALPHABET.length;
|
|
520
|
+
|
|
521
|
+
function getOrderCharIndex(ch) {
|
|
522
|
+
if (!ch) return -1;
|
|
523
|
+
return ORDER_ALPHABET.indexOf(ch);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function rankBetween(left, right) {
|
|
527
|
+
const leftKey = String(left || '');
|
|
528
|
+
const rightKey = String(right || '');
|
|
529
|
+
if (leftKey && rightKey && leftKey >= rightKey) {
|
|
530
|
+
return `${leftKey}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
531
|
+
}
|
|
532
|
+
let i = 0;
|
|
533
|
+
let prefix = '';
|
|
534
|
+
while (i < 64) {
|
|
535
|
+
const leftDigit = i < leftKey.length ? getOrderCharIndex(leftKey[i]) : -1;
|
|
536
|
+
const rightDigit = i < rightKey.length ? getOrderCharIndex(rightKey[i]) : ORDER_BASE;
|
|
537
|
+
if (leftDigit < 0 && i < leftKey.length) {
|
|
538
|
+
return `${prefix}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
539
|
+
}
|
|
540
|
+
if (rightDigit < 0 && i < rightKey.length) {
|
|
541
|
+
return `${prefix}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
542
|
+
}
|
|
543
|
+
if (rightDigit - leftDigit > 1) {
|
|
544
|
+
const mid = Math.floor((leftDigit + rightDigit) / 2);
|
|
545
|
+
return `${prefix}${ORDER_ALPHABET[mid]}`;
|
|
546
|
+
}
|
|
547
|
+
prefix += i < leftKey.length ? leftKey[i] : ORDER_ALPHABET[0];
|
|
548
|
+
i += 1;
|
|
549
|
+
}
|
|
550
|
+
return `${prefix}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function getNodeOrderKey(item, keyName = 'order_key') {
|
|
554
|
+
return String(item?.extra_data?.[keyName] || '').trim();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function compareSiblingOrder(a, b, keyName = 'order_key') {
|
|
558
|
+
const keyA = getNodeOrderKey(a, keyName);
|
|
559
|
+
const keyB = getNodeOrderKey(b, keyName);
|
|
560
|
+
if (keyA && keyB && keyA !== keyB) return keyA.localeCompare(keyB);
|
|
561
|
+
if (keyA && !keyB) return -1;
|
|
562
|
+
if (!keyA && keyB) return 1;
|
|
563
|
+
const numericA = Number(a?.extra_data?.lane_order);
|
|
564
|
+
const numericB = Number(b?.extra_data?.lane_order);
|
|
565
|
+
const validA = Number.isFinite(numericA);
|
|
566
|
+
const validB = Number.isFinite(numericB);
|
|
567
|
+
if (keyName === 'lane_order_key' && validA && validB && numericA !== numericB) return numericA - numericB;
|
|
568
|
+
if (keyName === 'lane_order_key' && validA && !validB) return -1;
|
|
569
|
+
if (keyName === 'lane_order_key' && !validA && validB) return 1;
|
|
570
|
+
return String(a?.created_at || '').localeCompare(String(b?.created_at || ''));
|
|
571
|
+
}
|
|
572
|
+
|
|
292
573
|
function populateParentSelect(items) {
|
|
293
574
|
const select = document.getElementById('new-item-parent');
|
|
294
575
|
if (!select) return;
|
|
@@ -298,22 +579,182 @@ function populateParentSelect(items) {
|
|
|
298
579
|
}
|
|
299
580
|
|
|
300
581
|
function renderMarkdownSnippet(text) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
582
|
+
const source = String(text || '').replace(/\r\n/g, '\n');
|
|
583
|
+
const lines = source.split('\n');
|
|
584
|
+
const blocks = [];
|
|
585
|
+
let listItems = [];
|
|
586
|
+
|
|
587
|
+
const flushList = () => {
|
|
588
|
+
if (!listItems.length) return;
|
|
589
|
+
blocks.push(`<ul>${listItems.map((item) => `<li>${item}</li>`).join('')}</ul>`);
|
|
590
|
+
listItems = [];
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const renderInline = (raw) => {
|
|
594
|
+
let html = safeText(String(raw || ''));
|
|
595
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
596
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
597
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
598
|
+
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_m, label, url) => {
|
|
599
|
+
const safeUrl = safeText(url);
|
|
600
|
+
const safeLabel = safeText(label);
|
|
601
|
+
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
|
|
602
|
+
});
|
|
603
|
+
return html;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
for (const line of lines) {
|
|
607
|
+
const trimmed = line.trim();
|
|
608
|
+
if (!trimmed) {
|
|
609
|
+
flushList();
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (/^[-*]\s+/.test(trimmed)) {
|
|
613
|
+
listItems.push(renderInline(trimmed.replace(/^[-*]\s+/, '')));
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
flushList();
|
|
617
|
+
const heading = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
618
|
+
if (heading) {
|
|
619
|
+
const level = Math.min(6, heading[1].length);
|
|
620
|
+
blocks.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
blocks.push(`<p>${renderInline(trimmed)}</p>`);
|
|
624
|
+
}
|
|
625
|
+
flushList();
|
|
626
|
+
return blocks.join('');
|
|
307
627
|
}
|
|
308
628
|
|
|
309
629
|
function getNodeById(nodeId) {
|
|
310
630
|
return state.currentSandbox?.items?.find((item) => item.id === nodeId) || null;
|
|
311
631
|
}
|
|
312
632
|
|
|
633
|
+
function getWorkItemNameById(nodeId) {
|
|
634
|
+
const row = getNodeById(nodeId);
|
|
635
|
+
return row?.name || String(nodeId || '');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function renderQuickDiaryTargetLabel() {
|
|
639
|
+
const el = document.getElementById('sandbox-diary-target-label');
|
|
640
|
+
if (!(el instanceof HTMLElement)) return;
|
|
641
|
+
if (!state.currentSandbox) {
|
|
642
|
+
el.textContent = '快速日记:未进入沙盘';
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const nodeId = state.selectedNodeId;
|
|
646
|
+
if (!nodeId) {
|
|
647
|
+
el.textContent = `快速日记:关联沙盘 ${state.currentSandbox.name}`;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
el.textContent = `快速日记:${state.currentSandbox.name} / ${getWorkItemNameById(nodeId)}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function saveDiaryEntry({ content, sandboxId = null, workItemId = null }) {
|
|
654
|
+
const payload = {
|
|
655
|
+
sandbox_id: sandboxId,
|
|
656
|
+
work_item_id: workItemId,
|
|
657
|
+
content: String(content || '').trim(),
|
|
658
|
+
};
|
|
659
|
+
if (!payload.content) return null;
|
|
660
|
+
return apiRequest(`${API_BASE}/diaries`, {
|
|
661
|
+
method: 'POST',
|
|
662
|
+
body: JSON.stringify(payload),
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function shouldAutoCaptureDiary(text) {
|
|
667
|
+
const normalized = String(text || '').trim().toLowerCase();
|
|
668
|
+
if (!normalized) return false;
|
|
669
|
+
if (normalized.length < 2) return false;
|
|
670
|
+
const patterns = [
|
|
671
|
+
/记录/,
|
|
672
|
+
/进展/,
|
|
673
|
+
/日志/,
|
|
674
|
+
/汇报/,
|
|
675
|
+
/同步/,
|
|
676
|
+
/今日.*完成/,
|
|
677
|
+
/今天.*完成/,
|
|
678
|
+
/progress/,
|
|
679
|
+
/update/,
|
|
680
|
+
];
|
|
681
|
+
return patterns.some((pattern) => pattern.test(normalized));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function resolveDiaryCaptureIntent(text) {
|
|
685
|
+
const raw = String(text || '').trim();
|
|
686
|
+
const auto = shouldAutoCaptureDiary(raw);
|
|
687
|
+
const directPrefix = /^(请\s*)?(帮我\s*)?(记录(日记|日志)|写(日记|日志)|日记|日志)\s*[::]?\s*/;
|
|
688
|
+
const isDirectDiaryCommand = directPrefix.test(raw);
|
|
689
|
+
const stripped = isDirectDiaryCommand ? raw.replace(directPrefix, '').trim() : raw;
|
|
690
|
+
return {
|
|
691
|
+
shouldCapture: auto,
|
|
692
|
+
isDirectDiaryCommand,
|
|
693
|
+
diaryContent: stripped || raw,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function parseNodeIdFromNodeContext(payloadContent) {
|
|
698
|
+
const text = String(payloadContent || '');
|
|
699
|
+
const match = text.match(/node_id=([^\n]+)/);
|
|
700
|
+
if (!match) return null;
|
|
701
|
+
const nodeId = String(match[1] || '').trim();
|
|
702
|
+
return nodeId || null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function openDiaryEditDialog(diary) {
|
|
706
|
+
const dialog = document.getElementById('diary-edit-dialog');
|
|
707
|
+
const textarea = document.getElementById('diary-edit-content');
|
|
708
|
+
const preview = document.getElementById('diary-edit-preview');
|
|
709
|
+
if (!(dialog instanceof HTMLDialogElement) || !(textarea instanceof HTMLTextAreaElement)) return;
|
|
710
|
+
dialog.dataset.editDiaryId = String(diary?.id || '');
|
|
711
|
+
textarea.value = String(diary?.content || '');
|
|
712
|
+
if (preview instanceof HTMLElement) {
|
|
713
|
+
const rendered = renderMarkdownSnippet(textarea.value);
|
|
714
|
+
preview.innerHTML = rendered || '<p class="is-empty">在左侧输入 Markdown,这里会实时预览。</p>';
|
|
715
|
+
preview.classList.toggle('is-empty', !rendered);
|
|
716
|
+
}
|
|
717
|
+
dialog.showModal();
|
|
718
|
+
textarea.focus();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function closeDiaryEditDialog() {
|
|
722
|
+
const dialog = document.getElementById('diary-edit-dialog');
|
|
723
|
+
if (!(dialog instanceof HTMLDialogElement)) return;
|
|
724
|
+
dialog.close();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function submitDiaryEditDialog() {
|
|
728
|
+
const dialog = document.getElementById('diary-edit-dialog');
|
|
729
|
+
const textarea = document.getElementById('diary-edit-content');
|
|
730
|
+
if (!(dialog instanceof HTMLDialogElement) || !(textarea instanceof HTMLTextAreaElement)) return;
|
|
731
|
+
const diaryId = String(dialog.dataset.editDiaryId || '').trim();
|
|
732
|
+
const content = textarea.value.trim();
|
|
733
|
+
if (!diaryId || !content) return;
|
|
734
|
+
await apiRequest(`${API_BASE}/diaries/${diaryId}`, {
|
|
735
|
+
method: 'PUT',
|
|
736
|
+
body: JSON.stringify({ content }),
|
|
737
|
+
});
|
|
738
|
+
const selectedNodeId = state.selectedNodeId;
|
|
739
|
+
const selectedFilter = state.nodeEntityFilter;
|
|
740
|
+
closeDiaryEditDialog();
|
|
741
|
+
await loadDiaries();
|
|
742
|
+
if (state.currentSandbox?.id) {
|
|
743
|
+
await loadSandbox(state.currentSandbox.id);
|
|
744
|
+
if (selectedNodeId) {
|
|
745
|
+
showNodeEntityDrawer(selectedNodeId, selectedFilter);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
313
750
|
function getNodeEntitiesByNodeId(nodeId) {
|
|
314
751
|
return (state.nodeEntities || []).filter((row) => row.work_item_id === nodeId);
|
|
315
752
|
}
|
|
316
753
|
|
|
754
|
+
function getNodeDiariesByNodeId(nodeId) {
|
|
755
|
+
return (state.currentSandbox?.diaries || []).filter((row) => row.work_item_id === nodeId);
|
|
756
|
+
}
|
|
757
|
+
|
|
317
758
|
function closeNodeEntityDrawer() {
|
|
318
759
|
const drawer = document.getElementById('node-entity-drawer');
|
|
319
760
|
if (!drawer) return;
|
|
@@ -322,6 +763,7 @@ function closeNodeEntityDrawer() {
|
|
|
322
763
|
state.nodeEntityFilter = 'all';
|
|
323
764
|
state.editingNodeEntityId = null;
|
|
324
765
|
state.nodeEntityFormExpanded = false;
|
|
766
|
+
renderWorkTree();
|
|
325
767
|
}
|
|
326
768
|
|
|
327
769
|
function setNodeEntityFormExpanded(expanded) {
|
|
@@ -349,6 +791,7 @@ function renderNodeEntitySummary(nodeId) {
|
|
|
349
791
|
const container = document.getElementById('node-entity-summary');
|
|
350
792
|
if (!container) return;
|
|
351
793
|
const rows = getNodeEntitiesByNodeId(nodeId);
|
|
794
|
+
const diaries = getNodeDiariesByNodeId(nodeId);
|
|
352
795
|
const issues = rows.filter((row) => row.entity_type === 'issue');
|
|
353
796
|
const knowledges = rows.filter((row) => row.entity_type === 'knowledge');
|
|
354
797
|
const capabilities = rows.filter((row) => row.entity_type === 'capability');
|
|
@@ -358,38 +801,92 @@ function renderNodeEntitySummary(nodeId) {
|
|
|
358
801
|
<div class="summary-card"><div class="label">Open Issue</div><div class="value">${openIssues}</div></div>
|
|
359
802
|
<div class="summary-card"><div class="label">Knowledge</div><div class="value">${knowledges.length}</div></div>
|
|
360
803
|
<div class="summary-card"><div class="label">Capability</div><div class="value">${capabilities.length}</div></div>
|
|
804
|
+
<div class="summary-card"><div class="label">Diary</div><div class="value">${diaries.length}</div></div>
|
|
361
805
|
`;
|
|
362
806
|
}
|
|
363
807
|
|
|
808
|
+
async function processNodeDiary(diaryId, action) {
|
|
809
|
+
if (!state.currentSandbox) return;
|
|
810
|
+
await apiRequest(`${API_BASE}/diaries/${diaryId}/process`, {
|
|
811
|
+
method: 'PUT',
|
|
812
|
+
body: JSON.stringify({ action }),
|
|
813
|
+
});
|
|
814
|
+
const selectedNodeId = state.selectedNodeId;
|
|
815
|
+
const selectedFilter = state.nodeEntityFilter;
|
|
816
|
+
await loadSandbox(state.currentSandbox.id);
|
|
817
|
+
await loadDiaries();
|
|
818
|
+
if (selectedNodeId) {
|
|
819
|
+
showNodeEntityDrawer(selectedNodeId, selectedFilter);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
364
823
|
function renderNodeEntityList(nodeId) {
|
|
365
824
|
const container = document.getElementById('node-entity-list');
|
|
366
825
|
if (!container) return;
|
|
367
|
-
const
|
|
826
|
+
const allEntityRows = getNodeEntitiesByNodeId(nodeId);
|
|
827
|
+
const allDiaryRows = getNodeDiariesByNodeId(nodeId);
|
|
828
|
+
const timelineRows = [
|
|
829
|
+
...allEntityRows.map((row) => ({ ...row, timeline_type: row.entity_type || 'issue' })),
|
|
830
|
+
...allDiaryRows.map((row) => ({ ...row, timeline_type: 'diary' })),
|
|
831
|
+
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
368
832
|
const rows = state.nodeEntityFilter === 'all'
|
|
369
|
-
?
|
|
370
|
-
:
|
|
833
|
+
? timelineRows
|
|
834
|
+
: timelineRows.filter((row) => row.timeline_type === state.nodeEntityFilter);
|
|
371
835
|
if (!rows.length) {
|
|
372
|
-
container.innerHTML = '<div class="empty-state"><p
|
|
836
|
+
container.innerHTML = '<div class="empty-state"><p>当前筛选下暂无记录</p></div>';
|
|
373
837
|
return;
|
|
374
838
|
}
|
|
375
|
-
container.innerHTML = rows.map((row) =>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
<div>
|
|
379
|
-
<
|
|
380
|
-
|
|
839
|
+
container.innerHTML = rows.map((row) => {
|
|
840
|
+
if (row.timeline_type === 'diary') {
|
|
841
|
+
return `
|
|
842
|
+
<div class="entity-card diary-card ${row.processed ? 'processed' : ''}">
|
|
843
|
+
<div class="entity-card-header">
|
|
844
|
+
<div>
|
|
845
|
+
<span class="entity-type-pill">diary</span>
|
|
846
|
+
<strong>${safeText('日志记录')}</strong>
|
|
847
|
+
</div>
|
|
848
|
+
${state.readonly ? '' : `
|
|
849
|
+
<div class="entity-card-actions">
|
|
850
|
+
<button class="btn btn-secondary btn-sm" data-diary-edit-id="${safeText(row.id)}">编辑</button>
|
|
851
|
+
${row.processed ? '' : `
|
|
852
|
+
<button class="btn btn-secondary btn-sm" data-diary-confirm-id="${safeText(row.id)}">采纳</button>
|
|
853
|
+
<button class="btn btn-secondary btn-sm" data-diary-ignore-id="${safeText(row.id)}">忽略</button>
|
|
854
|
+
`}
|
|
855
|
+
</div>
|
|
856
|
+
`}
|
|
857
|
+
</div>
|
|
858
|
+
<div class="entity-meta">
|
|
859
|
+
${safeText(new Date(row.created_at).toLocaleString())}
|
|
860
|
+
${row.processed ? ' · 已处理' : ' · 未处理'}
|
|
861
|
+
</div>
|
|
862
|
+
<div class="entity-content">${renderMarkdownSnippet(row.content || '')}</div>
|
|
863
|
+
</div>
|
|
864
|
+
`;
|
|
865
|
+
}
|
|
866
|
+
return `
|
|
867
|
+
<div class="entity-card">
|
|
868
|
+
<div class="entity-card-header">
|
|
869
|
+
<div>
|
|
870
|
+
<span class="entity-type-pill">${safeText(row.entity_type)}</span>
|
|
871
|
+
<strong>${safeText(row.title || '-')}</strong>
|
|
872
|
+
</div>
|
|
873
|
+
${state.readonly ? '' : `
|
|
874
|
+
<div class="entity-card-actions">
|
|
875
|
+
<button class="btn btn-secondary btn-sm" data-entity-edit-id="${safeText(row.id)}">编辑</button>
|
|
876
|
+
<button class="btn btn-secondary btn-sm" data-entity-delete-id="${safeText(row.id)}">删除</button>
|
|
877
|
+
</div>
|
|
878
|
+
`}
|
|
381
879
|
</div>
|
|
382
|
-
<div class="entity-
|
|
383
|
-
|
|
384
|
-
<
|
|
880
|
+
<div class="entity-meta">
|
|
881
|
+
${safeText(new Date(row.created_at).toLocaleString())}
|
|
882
|
+
${row.status ? ` · <span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}
|
|
883
|
+
${row.priority ? ` · ${safeText(row.priority)}` : ''}
|
|
884
|
+
${row.assignee ? ` · @${safeText(row.assignee)}` : ''}
|
|
385
885
|
</div>
|
|
886
|
+
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
386
887
|
</div>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
</div>
|
|
390
|
-
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
391
|
-
</div>
|
|
392
|
-
`).join('');
|
|
888
|
+
`;
|
|
889
|
+
}).join('');
|
|
393
890
|
|
|
394
891
|
container.querySelectorAll('[data-entity-delete-id]').forEach((el) => {
|
|
395
892
|
el.addEventListener('click', async (e) => {
|
|
@@ -406,11 +903,40 @@ function renderNodeEntityList(nodeId) {
|
|
|
406
903
|
e.preventDefault();
|
|
407
904
|
const id = el.getAttribute('data-entity-edit-id');
|
|
408
905
|
if (!id) return;
|
|
409
|
-
const row =
|
|
906
|
+
const row = allEntityRows.find((item) => item.id === id);
|
|
410
907
|
if (!row) return;
|
|
411
908
|
startEditNodeEntity(row);
|
|
412
909
|
});
|
|
413
910
|
});
|
|
911
|
+
|
|
912
|
+
container.querySelectorAll('[data-diary-edit-id]').forEach((el) => {
|
|
913
|
+
el.addEventListener('click', (e) => {
|
|
914
|
+
e.preventDefault();
|
|
915
|
+
const id = el.getAttribute('data-diary-edit-id');
|
|
916
|
+
if (!id) return;
|
|
917
|
+
const row = allDiaryRows.find((item) => item.id === id);
|
|
918
|
+
if (!row) return;
|
|
919
|
+
openDiaryEditDialog(row);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
container.querySelectorAll('[data-diary-confirm-id]').forEach((el) => {
|
|
924
|
+
el.addEventListener('click', async (e) => {
|
|
925
|
+
e.preventDefault();
|
|
926
|
+
const id = el.getAttribute('data-diary-confirm-id');
|
|
927
|
+
if (!id) return;
|
|
928
|
+
await processNodeDiary(id, 'confirm');
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
container.querySelectorAll('[data-diary-ignore-id]').forEach((el) => {
|
|
933
|
+
el.addEventListener('click', async (e) => {
|
|
934
|
+
e.preventDefault();
|
|
935
|
+
const id = el.getAttribute('data-diary-ignore-id');
|
|
936
|
+
if (!id) return;
|
|
937
|
+
await processNodeDiary(id, 'ignore');
|
|
938
|
+
});
|
|
939
|
+
});
|
|
414
940
|
}
|
|
415
941
|
|
|
416
942
|
function resetNodeEntityForm() {
|
|
@@ -438,6 +964,19 @@ function resetNodeEntityForm() {
|
|
|
438
964
|
setNodeEntityFormExpanded(false);
|
|
439
965
|
}
|
|
440
966
|
|
|
967
|
+
function ensureCapabilityTypeOption(value) {
|
|
968
|
+
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
969
|
+
if (!(capabilityTypeInput instanceof HTMLSelectElement)) return;
|
|
970
|
+
const normalized = String(value || '').trim();
|
|
971
|
+
if (!normalized) return;
|
|
972
|
+
const hasOption = Array.from(capabilityTypeInput.options).some((option) => option.value === normalized);
|
|
973
|
+
if (hasOption) return;
|
|
974
|
+
const option = document.createElement('option');
|
|
975
|
+
option.value = normalized;
|
|
976
|
+
option.textContent = `${normalized}(历史值)`;
|
|
977
|
+
capabilityTypeInput.appendChild(option);
|
|
978
|
+
}
|
|
979
|
+
|
|
441
980
|
function startEditNodeEntity(row) {
|
|
442
981
|
state.editingNodeEntityId = row.id;
|
|
443
982
|
setNodeEntityFormExpanded(true);
|
|
@@ -455,6 +994,7 @@ function startEditNodeEntity(row) {
|
|
|
455
994
|
if (assigneeInput) assigneeInput.value = row.assignee || '';
|
|
456
995
|
if (statusInput) statusInput.value = row.status || '';
|
|
457
996
|
if (priorityInput) priorityInput.value = row.priority || '';
|
|
997
|
+
ensureCapabilityTypeOption(row.capability_type || '');
|
|
458
998
|
if (capabilityTypeInput) capabilityTypeInput.value = row.capability_type || '';
|
|
459
999
|
|
|
460
1000
|
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
@@ -464,15 +1004,18 @@ function startEditNodeEntity(row) {
|
|
|
464
1004
|
titleInput?.focus();
|
|
465
1005
|
}
|
|
466
1006
|
|
|
467
|
-
function showNodeEntityDrawer(nodeId) {
|
|
1007
|
+
function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
|
|
468
1008
|
const drawer = document.getElementById('node-entity-drawer');
|
|
469
1009
|
const title = document.getElementById('drawer-node-title');
|
|
470
1010
|
const node = getNodeById(nodeId);
|
|
471
1011
|
if (!drawer || !title || !node) return;
|
|
472
1012
|
state.selectedNodeId = nodeId;
|
|
473
|
-
|
|
1013
|
+
const filter = ['all', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
|
|
1014
|
+
state.nodeEntityFilter = filter;
|
|
474
1015
|
title.textContent = node.name || nodeId;
|
|
475
1016
|
renderNodeEntitySummary(nodeId);
|
|
1017
|
+
renderWorkTree();
|
|
1018
|
+
renderQuickDiaryTargetLabel();
|
|
476
1019
|
renderNodeEntityFilterTabs();
|
|
477
1020
|
renderNodeEntityList(nodeId);
|
|
478
1021
|
resetNodeEntityForm();
|
|
@@ -583,6 +1126,7 @@ function composeQuickChatContent(nodeId, userQuestion) {
|
|
|
583
1126
|
}
|
|
584
1127
|
|
|
585
1128
|
async function sendSandboxChatMessage(content, options = {}) {
|
|
1129
|
+
if (state.readonly) return;
|
|
586
1130
|
if (!content || !state.currentSandbox) return;
|
|
587
1131
|
const messages = document.getElementById('sandbox-chat-messages');
|
|
588
1132
|
const btn = document.getElementById('sandbox-send-btn');
|
|
@@ -590,6 +1134,8 @@ async function sendSandboxChatMessage(content, options = {}) {
|
|
|
590
1134
|
|
|
591
1135
|
const displayContent = options.displayContent || content;
|
|
592
1136
|
const payloadContent = options.payloadContent || content;
|
|
1137
|
+
const autoDiaryWorkItemId = options.workItemId || parseNodeIdFromNodeContext(payloadContent);
|
|
1138
|
+
const diaryIntent = resolveDiaryCaptureIntent(displayContent || content);
|
|
593
1139
|
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(displayContent)}</div>`);
|
|
594
1140
|
const loadingMessage = appendLoadingMessage(messages);
|
|
595
1141
|
messages.scrollTop = messages.scrollHeight;
|
|
@@ -598,6 +1144,31 @@ async function sendSandboxChatMessage(content, options = {}) {
|
|
|
598
1144
|
setButtonState(btn, { disabled: true, text: '思考中' });
|
|
599
1145
|
|
|
600
1146
|
try {
|
|
1147
|
+
let createdDiary = null;
|
|
1148
|
+
if (diaryIntent.shouldCapture) {
|
|
1149
|
+
createdDiary = await saveDiaryEntry({
|
|
1150
|
+
content: diaryIntent.diaryContent,
|
|
1151
|
+
sandboxId: state.currentSandbox.id,
|
|
1152
|
+
workItemId: autoDiaryWorkItemId || null,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
if (diaryIntent.isDirectDiaryCommand) {
|
|
1156
|
+
loadingMessage?.remove();
|
|
1157
|
+
if (createdDiary && state.currentSandbox) {
|
|
1158
|
+
state.currentSandbox.diaries = [createdDiary, ...(state.currentSandbox.diaries || [])];
|
|
1159
|
+
renderSandboxOverview();
|
|
1160
|
+
if (state.selectedNodeId && String(createdDiary.work_item_id || '') === String(state.selectedNodeId)) {
|
|
1161
|
+
renderNodeEntitySummary(state.selectedNodeId);
|
|
1162
|
+
renderNodeEntityList(state.selectedNodeId);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
messages.insertAdjacentHTML('beforeend', '<div class="chat-message assistant">📝 已记录日记。</div>');
|
|
1166
|
+
messages.scrollTop = messages.scrollHeight;
|
|
1167
|
+
if (state.currentSandbox) {
|
|
1168
|
+
await loadDiaries();
|
|
1169
|
+
}
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
601
1172
|
const history = state.chats
|
|
602
1173
|
.filter(c => c.role)
|
|
603
1174
|
.slice(-10)
|
|
@@ -693,7 +1264,7 @@ function openQuickChatPopover(nodeId, anchorEl) {
|
|
|
693
1264
|
closeQuickChatPopover();
|
|
694
1265
|
const payloadContent = composeQuickChatContent(nodeId, question);
|
|
695
1266
|
const displayContent = `【快捷】${question}`;
|
|
696
|
-
await sendSandboxChatMessage(question, { payloadContent, displayContent });
|
|
1267
|
+
await sendSandboxChatMessage(question, { payloadContent, displayContent, workItemId: nodeId });
|
|
697
1268
|
});
|
|
698
1269
|
textarea?.addEventListener('input', updateSubmitState);
|
|
699
1270
|
textarea?.addEventListener('keydown', async (event) => {
|
|
@@ -746,12 +1317,13 @@ function renderSandboxes() {
|
|
|
746
1317
|
onOpen: (id) => {
|
|
747
1318
|
window.location.hash = `/sandbox/${id}`;
|
|
748
1319
|
},
|
|
749
|
-
onDelete: async (id) => {
|
|
1320
|
+
onDelete: state.readonly ? undefined : async (id) => {
|
|
750
1321
|
if (confirm('确定删除此沙盘?')) {
|
|
751
1322
|
await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
|
|
752
1323
|
await loadSandboxes();
|
|
753
1324
|
}
|
|
754
|
-
}
|
|
1325
|
+
},
|
|
1326
|
+
readonly: state.readonly,
|
|
755
1327
|
});
|
|
756
1328
|
}
|
|
757
1329
|
|
|
@@ -771,11 +1343,20 @@ async function loadSandbox(id) {
|
|
|
771
1343
|
document.getElementById('sandbox-title').textContent = sandbox.name;
|
|
772
1344
|
applyWorkTreeViewMode(state.workTreeViewMode || 'full');
|
|
773
1345
|
applyWorkItemAssigneeToggle();
|
|
774
|
-
|
|
1346
|
+
applyWorkItemElementPreviewMode();
|
|
1347
|
+
applySandboxChatVisibility();
|
|
775
1348
|
applySandboxFullscreenState();
|
|
1349
|
+
renderQuickDiaryTargetLabel();
|
|
776
1350
|
renderSandboxOverview();
|
|
777
1351
|
renderWorkTree();
|
|
778
|
-
|
|
1352
|
+
applySandboxLayoutHeight();
|
|
1353
|
+
if (state.readonly) {
|
|
1354
|
+
state.chats = [];
|
|
1355
|
+
const messages = document.getElementById('sandbox-chat-messages');
|
|
1356
|
+
if (messages) messages.innerHTML = '';
|
|
1357
|
+
} else {
|
|
1358
|
+
loadSandboxChats(id);
|
|
1359
|
+
}
|
|
779
1360
|
}
|
|
780
1361
|
|
|
781
1362
|
function renderSandboxOverview() {
|
|
@@ -800,6 +1381,7 @@ function renderSandboxOverview() {
|
|
|
800
1381
|
}
|
|
801
1382
|
|
|
802
1383
|
async function loadSandboxChats(sandboxId) {
|
|
1384
|
+
if (state.readonly) return;
|
|
803
1385
|
const messages = document.getElementById('sandbox-chat-messages');
|
|
804
1386
|
if (!messages) return;
|
|
805
1387
|
|
|
@@ -886,8 +1468,63 @@ window.undoOperation = async function(operationId, btn) {
|
|
|
886
1468
|
}
|
|
887
1469
|
};
|
|
888
1470
|
|
|
1471
|
+
async function hydrateDiaryWorkItemNames(diaries) {
|
|
1472
|
+
const map = {};
|
|
1473
|
+
const sandboxIds = Array.from(
|
|
1474
|
+
new Set(
|
|
1475
|
+
(diaries || [])
|
|
1476
|
+
.filter((row) => row?.sandbox_id && row?.work_item_id)
|
|
1477
|
+
.map((row) => String(row.sandbox_id)),
|
|
1478
|
+
),
|
|
1479
|
+
);
|
|
1480
|
+
await Promise.all(sandboxIds.map(async (sandboxId) => {
|
|
1481
|
+
try {
|
|
1482
|
+
const items = await apiRequest(`${API_BASE}/sandboxes/${sandboxId}/items`);
|
|
1483
|
+
(items || []).forEach((item) => {
|
|
1484
|
+
const key = `${sandboxId}:${item.id}`;
|
|
1485
|
+
map[key] = item.name || item.id;
|
|
1486
|
+
});
|
|
1487
|
+
} catch {
|
|
1488
|
+
// Ignore per-sandbox fetch failure to keep diary page available.
|
|
1489
|
+
}
|
|
1490
|
+
}));
|
|
1491
|
+
state.diaryWorkItemNameMap = map;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function getDiaryWorkItemName(sandboxId, workItemId) {
|
|
1495
|
+
if (!workItemId) return '';
|
|
1496
|
+
const sandboxKey = String(sandboxId || '').trim();
|
|
1497
|
+
const itemKey = String(workItemId || '').trim();
|
|
1498
|
+
if (!itemKey) return '';
|
|
1499
|
+
if (sandboxKey) {
|
|
1500
|
+
const key = `${sandboxKey}:${itemKey}`;
|
|
1501
|
+
if (state.diaryWorkItemNameMap[key]) {
|
|
1502
|
+
return state.diaryWorkItemNameMap[key];
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
if (state.currentSandbox?.id === sandboxKey) {
|
|
1506
|
+
const node = (state.currentSandbox.items || []).find((item) => item.id === itemKey);
|
|
1507
|
+
if (node?.name) return node.name;
|
|
1508
|
+
}
|
|
1509
|
+
return itemKey;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function openDiaryWorkItemInSandbox(sandboxId, workItemId) {
|
|
1513
|
+
const targetSandboxId = String(sandboxId || '').trim();
|
|
1514
|
+
const targetWorkItemId = String(workItemId || '').trim();
|
|
1515
|
+
if (!targetSandboxId || !targetWorkItemId) return;
|
|
1516
|
+
const nextHash = `/sandbox/${encodeURIComponent(targetSandboxId)}?node_id=${encodeURIComponent(targetWorkItemId)}&open_drawer=1`;
|
|
1517
|
+
const currentHash = window.location.hash.slice(1);
|
|
1518
|
+
if (currentHash === nextHash && state.currentSandbox?.id === targetSandboxId) {
|
|
1519
|
+
showNodeEntityDrawer(targetWorkItemId);
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
window.location.hash = nextHash;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
889
1525
|
async function loadDiaries() {
|
|
890
1526
|
state.diaries = await apiRequest(`${API_BASE}/diaries`);
|
|
1527
|
+
await hydrateDiaryWorkItemNames(state.diaries);
|
|
891
1528
|
renderDiaries();
|
|
892
1529
|
}
|
|
893
1530
|
|
|
@@ -899,20 +1536,31 @@ function renderDiaries() {
|
|
|
899
1536
|
mountDiaryTimeline('diary-timeline', {
|
|
900
1537
|
diaries: filtered,
|
|
901
1538
|
getSandboxName,
|
|
902
|
-
|
|
1539
|
+
getWorkItemName: getDiaryWorkItemName,
|
|
1540
|
+
onOpenWorkItem: (sandboxId, workItemId) => {
|
|
1541
|
+
openDiaryWorkItemInSandbox(sandboxId, workItemId);
|
|
1542
|
+
},
|
|
1543
|
+
renderContent: (content) => renderMarkdownSnippet(content),
|
|
1544
|
+
onConfirm: state.readonly ? undefined : async (id) => {
|
|
903
1545
|
await apiRequest(`${API_BASE}/diaries/${id}/process`, {
|
|
904
1546
|
method: 'PUT',
|
|
905
1547
|
body: JSON.stringify({ action: 'confirm' }),
|
|
906
1548
|
});
|
|
907
1549
|
await loadDiaries();
|
|
908
1550
|
},
|
|
909
|
-
onIgnore: async (id) => {
|
|
1551
|
+
onIgnore: state.readonly ? undefined : async (id) => {
|
|
910
1552
|
await apiRequest(`${API_BASE}/diaries/${id}/process`, {
|
|
911
1553
|
method: 'PUT',
|
|
912
1554
|
body: JSON.stringify({ action: 'ignore' }),
|
|
913
1555
|
});
|
|
914
1556
|
await loadDiaries();
|
|
915
|
-
}
|
|
1557
|
+
},
|
|
1558
|
+
onEdit: state.readonly ? undefined : async (id) => {
|
|
1559
|
+
const diary = state.diaries.find((row) => row.id === id);
|
|
1560
|
+
if (!diary) return;
|
|
1561
|
+
openDiaryEditDialog(diary);
|
|
1562
|
+
},
|
|
1563
|
+
readonly: state.readonly,
|
|
916
1564
|
});
|
|
917
1565
|
}
|
|
918
1566
|
|
|
@@ -922,10 +1570,12 @@ function getSandboxName(id) {
|
|
|
922
1570
|
}
|
|
923
1571
|
|
|
924
1572
|
function updateSandboxSelect() {
|
|
925
|
-
const
|
|
926
|
-
if (
|
|
927
|
-
|
|
1573
|
+
const diaryFilterSelect = document.getElementById('diary-sandbox-filter');
|
|
1574
|
+
if (diaryFilterSelect) {
|
|
1575
|
+
const current = state.diarySandboxFilter || '';
|
|
1576
|
+
diaryFilterSelect.innerHTML = '<option value="">全部关联沙盘</option>' +
|
|
928
1577
|
state.sandboxes.map(s => `<option value="${s.id}">${safeText(s.name)}</option>`).join('');
|
|
1578
|
+
diaryFilterSelect.value = current;
|
|
929
1579
|
}
|
|
930
1580
|
|
|
931
1581
|
const changesSelect = document.getElementById('changes-sandbox-filter');
|
|
@@ -1241,15 +1891,6 @@ function collectSettingHeadersFromUI() {
|
|
|
1241
1891
|
return headers;
|
|
1242
1892
|
}
|
|
1243
1893
|
|
|
1244
|
-
async function loadChats() {
|
|
1245
|
-
const messages = document.getElementById('chat-messages');
|
|
1246
|
-
if (!messages) return;
|
|
1247
|
-
|
|
1248
|
-
const chats = await apiRequest(`${API_BASE}/chats`);
|
|
1249
|
-
mountHtmlList('chat-messages', chats.map((chat) => renderChatEntry(chat, { safeText, renderAIActionMessage })));
|
|
1250
|
-
messages.scrollTop = messages.scrollHeight;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
1894
|
function showPage(pageId) {
|
|
1254
1895
|
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
|
1255
1896
|
const page = document.getElementById(`page-${pageId}`);
|
|
@@ -1277,9 +1918,12 @@ function editWorkItem(id) {
|
|
|
1277
1918
|
document.getElementById('item-dialog').showModal();
|
|
1278
1919
|
}
|
|
1279
1920
|
|
|
1280
|
-
function initApp() {
|
|
1281
|
-
|
|
1921
|
+
async function initApp() {
|
|
1922
|
+
await loadRuntimeMode();
|
|
1282
1923
|
loadWorkItemAssigneePreference();
|
|
1924
|
+
loadWorkTreeViewModePreference();
|
|
1925
|
+
renderQuickDiaryTargetLabel();
|
|
1926
|
+
applyReadonlyMode();
|
|
1283
1927
|
|
|
1284
1928
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
1285
1929
|
link.addEventListener('click', (e) => {
|
|
@@ -1289,6 +1933,7 @@ function initApp() {
|
|
|
1289
1933
|
});
|
|
1290
1934
|
|
|
1291
1935
|
document.getElementById('add-sandbox-btn')?.addEventListener('click', () => {
|
|
1936
|
+
if (state.readonly) return;
|
|
1292
1937
|
document.getElementById('sandbox-dialog').showModal();
|
|
1293
1938
|
});
|
|
1294
1939
|
|
|
@@ -1297,6 +1942,7 @@ function initApp() {
|
|
|
1297
1942
|
});
|
|
1298
1943
|
|
|
1299
1944
|
const createSandbox = async (e) => {
|
|
1945
|
+
if (state.readonly) return;
|
|
1300
1946
|
e?.preventDefault?.();
|
|
1301
1947
|
const name = document.getElementById('new-sandbox-name').value;
|
|
1302
1948
|
const description = document.getElementById('new-sandbox-desc').value;
|
|
@@ -1320,6 +1966,7 @@ function initApp() {
|
|
|
1320
1966
|
document.getElementById('confirm-sandbox-btn')?.addEventListener('click', createSandbox);
|
|
1321
1967
|
|
|
1322
1968
|
document.getElementById('add-item-btn')?.addEventListener('click', () => {
|
|
1969
|
+
if (state.readonly) return;
|
|
1323
1970
|
document.getElementById('item-dialog-title').textContent = '添加任务';
|
|
1324
1971
|
document.getElementById('item-dialog').dataset.editId = '';
|
|
1325
1972
|
document.getElementById('new-item-name').value = '';
|
|
@@ -1331,9 +1978,11 @@ function initApp() {
|
|
|
1331
1978
|
document.getElementById('item-dialog').showModal();
|
|
1332
1979
|
});
|
|
1333
1980
|
|
|
1334
|
-
document.getElementById('toggle-sandbox-
|
|
1335
|
-
state.
|
|
1336
|
-
|
|
1981
|
+
document.getElementById('toggle-sandbox-chat-btn')?.addEventListener('click', () => {
|
|
1982
|
+
if (state.readonly) return;
|
|
1983
|
+
state.sandboxChatVisible = !state.sandboxChatVisible;
|
|
1984
|
+
applySandboxChatVisibility();
|
|
1985
|
+
applySandboxLayoutHeight();
|
|
1337
1986
|
});
|
|
1338
1987
|
|
|
1339
1988
|
document.getElementById('toggle-sandbox-fullscreen-btn')?.addEventListener('click', async () => {
|
|
@@ -1354,6 +2003,7 @@ function initApp() {
|
|
|
1354
2003
|
});
|
|
1355
2004
|
|
|
1356
2005
|
document.getElementById('toggle-node-entity-form-btn')?.addEventListener('click', () => {
|
|
2006
|
+
if (state.readonly) return;
|
|
1357
2007
|
setNodeEntityFormExpanded(!state.nodeEntityFormExpanded);
|
|
1358
2008
|
if (state.nodeEntityFormExpanded) {
|
|
1359
2009
|
document.getElementById('entity-title-input')?.focus();
|
|
@@ -1382,6 +2032,7 @@ function initApp() {
|
|
|
1382
2032
|
});
|
|
1383
2033
|
|
|
1384
2034
|
document.getElementById('create-node-entity-btn')?.addEventListener('click', async () => {
|
|
2035
|
+
if (state.readonly) return;
|
|
1385
2036
|
if (!state.currentSandbox || !state.selectedNodeId) return;
|
|
1386
2037
|
const btn = document.getElementById('create-node-entity-btn');
|
|
1387
2038
|
const title = document.getElementById('entity-title-input').value.trim();
|
|
@@ -1480,6 +2131,7 @@ function initApp() {
|
|
|
1480
2131
|
});
|
|
1481
2132
|
|
|
1482
2133
|
document.getElementById('confirm-item-btn')?.addEventListener('click', async () => {
|
|
2134
|
+
if (state.readonly) return;
|
|
1483
2135
|
const dialog = document.getElementById('item-dialog');
|
|
1484
2136
|
const editId = dialog.dataset.editId || null;
|
|
1485
2137
|
const isNewItem = !editId;
|
|
@@ -1526,50 +2178,15 @@ function initApp() {
|
|
|
1526
2178
|
});
|
|
1527
2179
|
|
|
1528
2180
|
document.getElementById('rollback-btn')?.addEventListener('click', async () => {
|
|
2181
|
+
if (state.readonly) return;
|
|
1529
2182
|
if (!state.currentSandbox?.items?.length) return;
|
|
1530
2183
|
const lastItem = state.currentSandbox.items[state.currentSandbox.items.length - 1];
|
|
1531
2184
|
await apiRequest(`${API_BASE}/items/${lastItem.id}/rollback`, { method: 'POST' });
|
|
1532
2185
|
await loadSandbox(state.currentSandbox.id);
|
|
1533
2186
|
});
|
|
1534
2187
|
|
|
1535
|
-
document.getElementById('send-btn')?.addEventListener('click', async () => {
|
|
1536
|
-
const input = document.getElementById('chat-input');
|
|
1537
|
-
const content = input.value.trim();
|
|
1538
|
-
if (!content) return;
|
|
1539
|
-
|
|
1540
|
-
const messages = document.getElementById('chat-messages');
|
|
1541
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(content)}</div>`);
|
|
1542
|
-
const loadingMessage = appendLoadingMessage(messages);
|
|
1543
|
-
messages.scrollTop = messages.scrollHeight;
|
|
1544
|
-
input.value = '';
|
|
1545
|
-
|
|
1546
|
-
const btn = document.getElementById('send-btn');
|
|
1547
|
-
setButtonState(btn, { disabled: true });
|
|
1548
|
-
|
|
1549
|
-
try {
|
|
1550
|
-
await apiRequest(`${API_BASE}/chats`, {
|
|
1551
|
-
method: 'POST',
|
|
1552
|
-
body: JSON.stringify({ content }),
|
|
1553
|
-
});
|
|
1554
|
-
|
|
1555
|
-
loadingMessage?.remove();
|
|
1556
|
-
await loadChats();
|
|
1557
|
-
} catch (error) {
|
|
1558
|
-
loadingMessage?.remove();
|
|
1559
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
1560
|
-
messages.scrollTop = messages.scrollHeight;
|
|
1561
|
-
} finally {
|
|
1562
|
-
setButtonState(btn, { disabled: false });
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
1565
|
-
|
|
1566
|
-
document.getElementById('chat-input')?.addEventListener('keypress', (e) => {
|
|
1567
|
-
if (e.key === 'Enter') {
|
|
1568
|
-
document.getElementById('send-btn').click();
|
|
1569
|
-
}
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1572
2188
|
document.getElementById('sandbox-send-btn')?.addEventListener('click', async () => {
|
|
2189
|
+
if (state.readonly) return;
|
|
1573
2190
|
const input = document.getElementById('sandbox-chat-input');
|
|
1574
2191
|
const content = input.value.trim();
|
|
1575
2192
|
if (!content || !state.currentSandbox) return;
|
|
@@ -1687,26 +2304,67 @@ function initApp() {
|
|
|
1687
2304
|
sandboxActionHandler = handleAIAction;
|
|
1688
2305
|
|
|
1689
2306
|
document.getElementById('sandbox-chat-input')?.addEventListener('keypress', (e) => {
|
|
2307
|
+
if (state.readonly) return;
|
|
1690
2308
|
if (e.key === 'Enter') {
|
|
1691
2309
|
document.getElementById('sandbox-send-btn').click();
|
|
1692
2310
|
}
|
|
1693
2311
|
});
|
|
1694
2312
|
|
|
1695
2313
|
document.getElementById('save-diary-btn')?.addEventListener('click', async () => {
|
|
1696
|
-
|
|
2314
|
+
if (state.readonly) return;
|
|
1697
2315
|
const content = document.getElementById('diary-content').value.trim();
|
|
1698
2316
|
if (!content) return;
|
|
1699
|
-
|
|
1700
|
-
await apiRequest(`${API_BASE}/diaries`, {
|
|
1701
|
-
method: 'POST',
|
|
1702
|
-
body: JSON.stringify({ sandbox_id: sandboxId, content }),
|
|
1703
|
-
});
|
|
1704
|
-
|
|
2317
|
+
await saveDiaryEntry({ content, sandboxId: null, workItemId: null });
|
|
1705
2318
|
document.getElementById('diary-content').value = '';
|
|
1706
2319
|
await loadDiaries();
|
|
1707
2320
|
});
|
|
2321
|
+
|
|
2322
|
+
document.getElementById('drawer-diary-save-btn')?.addEventListener('click', async () => {
|
|
2323
|
+
if (state.readonly) return;
|
|
2324
|
+
if (!state.currentSandbox || !state.selectedNodeId) return;
|
|
2325
|
+
const textarea = document.getElementById('drawer-diary-content');
|
|
2326
|
+
if (!(textarea instanceof HTMLTextAreaElement)) return;
|
|
2327
|
+
const content = textarea.value.trim();
|
|
2328
|
+
if (!content) return;
|
|
2329
|
+
const selectedNodeId = state.selectedNodeId;
|
|
2330
|
+
const selectedFilter = state.nodeEntityFilter;
|
|
2331
|
+
await saveDiaryEntry({
|
|
2332
|
+
content,
|
|
2333
|
+
sandboxId: state.currentSandbox.id,
|
|
2334
|
+
workItemId: selectedNodeId,
|
|
2335
|
+
});
|
|
2336
|
+
textarea.value = '';
|
|
2337
|
+
await loadSandbox(state.currentSandbox.id);
|
|
2338
|
+
await loadDiaries();
|
|
2339
|
+
showNodeEntityDrawer(selectedNodeId, selectedFilter);
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
document.getElementById('cancel-edit-diary-btn')?.addEventListener('click', () => {
|
|
2343
|
+
closeDiaryEditDialog();
|
|
2344
|
+
});
|
|
2345
|
+
document.getElementById('confirm-edit-diary-btn')?.addEventListener('click', async () => {
|
|
2346
|
+
if (state.readonly) return;
|
|
2347
|
+
await submitDiaryEditDialog();
|
|
2348
|
+
});
|
|
2349
|
+
document.getElementById('diary-edit-content')?.addEventListener('keydown', async (event) => {
|
|
2350
|
+
if (state.readonly) return;
|
|
2351
|
+
const isSubmit = event.key === 'Enter' && (event.metaKey || event.ctrlKey);
|
|
2352
|
+
if (!isSubmit) return;
|
|
2353
|
+
event.preventDefault();
|
|
2354
|
+
await submitDiaryEditDialog();
|
|
2355
|
+
});
|
|
2356
|
+
document.getElementById('diary-edit-content')?.addEventListener('input', (event) => {
|
|
2357
|
+
const preview = document.getElementById('diary-edit-preview');
|
|
2358
|
+
if (!(preview instanceof HTMLElement)) return;
|
|
2359
|
+
const target = event.target;
|
|
2360
|
+
const value = target instanceof HTMLTextAreaElement ? target.value : '';
|
|
2361
|
+
const rendered = renderMarkdownSnippet(value);
|
|
2362
|
+
preview.innerHTML = rendered || '<p class="is-empty">在左侧输入 Markdown,这里会实时预览。</p>';
|
|
2363
|
+
preview.classList.toggle('is-empty', !rendered);
|
|
2364
|
+
});
|
|
1708
2365
|
|
|
1709
2366
|
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
2367
|
+
if (state.readonly) return;
|
|
1710
2368
|
const api_url = document.getElementById('setting-api-url').value;
|
|
1711
2369
|
const api_key = document.getElementById('setting-api-key').value;
|
|
1712
2370
|
const model = document.getElementById('setting-model').value;
|
|
@@ -1736,6 +2394,7 @@ function initApp() {
|
|
|
1736
2394
|
});
|
|
1737
2395
|
|
|
1738
2396
|
document.getElementById('add-setting-header-btn')?.addEventListener('click', () => {
|
|
2397
|
+
if (state.readonly) return;
|
|
1739
2398
|
const list = document.getElementById('setting-headers-list');
|
|
1740
2399
|
if (!(list instanceof HTMLElement)) return;
|
|
1741
2400
|
list.appendChild(createSettingHeaderRow('', ''));
|
|
@@ -1754,6 +2413,12 @@ function initApp() {
|
|
|
1754
2413
|
document.getElementById('work-tree-view-mode')?.addEventListener('change', (e) => {
|
|
1755
2414
|
const nextMode = e.target.value || 'full';
|
|
1756
2415
|
applyWorkTreeViewMode(nextMode);
|
|
2416
|
+
persistWorkTreeViewModePreference();
|
|
2417
|
+
renderWorkTree();
|
|
2418
|
+
});
|
|
2419
|
+
|
|
2420
|
+
document.getElementById('work-item-element-preview-mode')?.addEventListener('change', (e) => {
|
|
2421
|
+
state.workItemElementPreviewMode = e.target.value || 'none';
|
|
1757
2422
|
renderWorkTree();
|
|
1758
2423
|
});
|
|
1759
2424
|
|
|
@@ -1773,6 +2438,11 @@ function initApp() {
|
|
|
1773
2438
|
renderDiaries();
|
|
1774
2439
|
});
|
|
1775
2440
|
|
|
2441
|
+
document.getElementById('diary-sandbox-filter')?.addEventListener('change', (e) => {
|
|
2442
|
+
state.diarySandboxFilter = e.target.value || '';
|
|
2443
|
+
renderDiaries();
|
|
2444
|
+
});
|
|
2445
|
+
|
|
1776
2446
|
document.getElementById('generate-report-btn')?.addEventListener('click', () => {
|
|
1777
2447
|
if (!state.currentSandbox) return;
|
|
1778
2448
|
const mode = document.getElementById('report-template')?.value || 'management';
|
|
@@ -1800,16 +2470,26 @@ function initApp() {
|
|
|
1800
2470
|
document.getElementById('generate-insight-btn')?.addEventListener('click', async () => {
|
|
1801
2471
|
if (!state.currentSandbox) return;
|
|
1802
2472
|
const output = document.getElementById('sandbox-insight-output');
|
|
2473
|
+
const contentEl = document.getElementById('sandbox-insight-content');
|
|
2474
|
+
if (!(output instanceof HTMLElement) || !(contentEl instanceof HTMLElement)) return;
|
|
1803
2475
|
output.classList.remove('hidden');
|
|
1804
|
-
output.
|
|
2476
|
+
output.removeAttribute('hidden');
|
|
2477
|
+
contentEl.textContent = '生成中...';
|
|
1805
2478
|
try {
|
|
1806
2479
|
const result = await apiRequest(`${API_BASE}/chats/sandbox/${state.currentSandbox.id}/insight`);
|
|
1807
|
-
|
|
2480
|
+
contentEl.textContent = result.insight;
|
|
1808
2481
|
} catch (error) {
|
|
1809
|
-
|
|
2482
|
+
contentEl.textContent = `生成失败:${error.message}`;
|
|
1810
2483
|
}
|
|
1811
2484
|
});
|
|
1812
2485
|
|
|
2486
|
+
document.getElementById('close-sandbox-insight-btn')?.addEventListener('click', () => {
|
|
2487
|
+
const output = document.getElementById('sandbox-insight-output');
|
|
2488
|
+
if (!(output instanceof HTMLElement)) return;
|
|
2489
|
+
output.classList.add('hidden');
|
|
2490
|
+
output.setAttribute('hidden', 'hidden');
|
|
2491
|
+
});
|
|
2492
|
+
|
|
1813
2493
|
document.getElementById('changes-sandbox-filter')?.addEventListener('change', async (e) => {
|
|
1814
2494
|
state.changesSandboxFilter = e.target.value || '';
|
|
1815
2495
|
await loadChanges();
|
|
@@ -1857,39 +2537,57 @@ function initApp() {
|
|
|
1857
2537
|
clearTimeout(resizeRenderTimer);
|
|
1858
2538
|
}
|
|
1859
2539
|
resizeRenderTimer = setTimeout(() => {
|
|
2540
|
+
applySandboxLayoutHeight();
|
|
1860
2541
|
renderWorkTree();
|
|
1861
2542
|
}, 120);
|
|
1862
2543
|
});
|
|
1863
2544
|
|
|
1864
2545
|
window.addEventListener('hashchange', handleRoute);
|
|
1865
|
-
handleRoute();
|
|
2546
|
+
await handleRoute();
|
|
1866
2547
|
|
|
1867
2548
|
async function handleRoute() {
|
|
1868
2549
|
const hash = window.location.hash.slice(1) || '/';
|
|
2550
|
+
const [pathHash, queryString = ''] = hash.split('?');
|
|
2551
|
+
const query = new URLSearchParams(queryString);
|
|
1869
2552
|
const fullscreenRoot = getSandboxFullscreenElement();
|
|
1870
|
-
if (!
|
|
2553
|
+
if (!pathHash.startsWith('/sandbox/') && fullscreenRoot && document.fullscreenElement === fullscreenRoot) {
|
|
1871
2554
|
await document.exitFullscreen();
|
|
1872
2555
|
}
|
|
1873
2556
|
|
|
1874
|
-
if (
|
|
1875
|
-
showPage('
|
|
1876
|
-
await
|
|
1877
|
-
} else if (
|
|
2557
|
+
if (pathHash === '/') {
|
|
2558
|
+
showPage('sandboxes');
|
|
2559
|
+
await loadSandboxes();
|
|
2560
|
+
} else if (pathHash === '/sandboxes') {
|
|
1878
2561
|
showPage('sandboxes');
|
|
1879
2562
|
await loadSandboxes();
|
|
1880
|
-
} else if (
|
|
1881
|
-
const id =
|
|
2563
|
+
} else if (pathHash.startsWith('/sandbox/')) {
|
|
2564
|
+
const id = decodeURIComponent(pathHash.split('/')[2] || '');
|
|
1882
2565
|
showPage('sandbox-detail');
|
|
1883
2566
|
await loadSandbox(id);
|
|
1884
|
-
|
|
2567
|
+
const targetNodeId = String(query.get('node_id') || '').trim();
|
|
2568
|
+
const shouldOpenDrawer = query.get('open_drawer') === '1' || Boolean(targetNodeId);
|
|
2569
|
+
const preferredFilter = query.get('entity_filter') || 'all';
|
|
2570
|
+
if (shouldOpenDrawer && targetNodeId) {
|
|
2571
|
+
showNodeEntityDrawer(targetNodeId, preferredFilter);
|
|
2572
|
+
}
|
|
2573
|
+
} else if (pathHash === '/diaries') {
|
|
1885
2574
|
showPage('diaries');
|
|
1886
2575
|
await loadDiaries();
|
|
1887
2576
|
await loadSandboxes();
|
|
1888
|
-
} else if (
|
|
2577
|
+
} else if (pathHash === '/changes') {
|
|
1889
2578
|
showPage('changes');
|
|
1890
2579
|
await loadSandboxes();
|
|
1891
2580
|
await loadChanges();
|
|
1892
|
-
} else if (
|
|
2581
|
+
} else if (pathHash === '/settings') {
|
|
2582
|
+
if (state.readonly) {
|
|
2583
|
+
if (window.location.hash !== '#/sandboxes') {
|
|
2584
|
+
window.location.hash = '/sandboxes';
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
showPage('sandboxes');
|
|
2588
|
+
await loadSandboxes();
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
1893
2591
|
showPage('settings');
|
|
1894
2592
|
await loadSettings();
|
|
1895
2593
|
}
|