@qnote/q-ai-note 1.0.4 → 1.0.5
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 +15 -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 +14 -2
- 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 +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +5 -3
- package/dist/server/index.js.map +1 -1
- package/dist/web/app.js +662 -100
- package/dist/web/index.html +72 -22
- package/dist/web/styles.css +367 -6
- package/dist/web/vueRenderers.js +249 -23
- package/package.json +5 -3
package/dist/web/app.js
CHANGED
|
@@ -19,11 +19,14 @@ const state = {
|
|
|
19
19
|
sandboxPresentationMode: false,
|
|
20
20
|
sandboxFullscreenMode: false,
|
|
21
21
|
workTreeViewMode: 'full',
|
|
22
|
+
workItemElementPreviewMode: 'none',
|
|
22
23
|
workItemShowAssignee: false,
|
|
23
24
|
workItemSearch: '',
|
|
24
25
|
workItemStatusFilter: 'all',
|
|
25
26
|
diarySearch: '',
|
|
27
|
+
diarySandboxFilter: '',
|
|
26
28
|
diaryProcessedFilter: 'all',
|
|
29
|
+
diaryWorkItemNameMap: {},
|
|
27
30
|
changesSandboxFilter: '',
|
|
28
31
|
changesTypeFilter: 'all',
|
|
29
32
|
changesQuickFilter: 'all',
|
|
@@ -160,12 +163,14 @@ function applyWorkItemFilters(items) {
|
|
|
160
163
|
function applyDiaryFilters(diaries) {
|
|
161
164
|
const query = state.diarySearch.trim().toLowerCase();
|
|
162
165
|
const processed = state.diaryProcessedFilter;
|
|
166
|
+
const sandboxId = state.diarySandboxFilter;
|
|
163
167
|
return diaries.filter((diary) => {
|
|
164
168
|
const queryMatched = !query || `${diary.content || ''}`.toLowerCase().includes(query);
|
|
169
|
+
const sandboxMatched = !sandboxId || String(diary.sandbox_id || '') === sandboxId;
|
|
165
170
|
const statusMatched = processed === 'all'
|
|
166
171
|
|| (processed === 'processed' && diary.processed)
|
|
167
172
|
|| (processed === 'unprocessed' && !diary.processed);
|
|
168
|
-
return queryMatched && statusMatched;
|
|
173
|
+
return queryMatched && sandboxMatched && statusMatched;
|
|
169
174
|
});
|
|
170
175
|
}
|
|
171
176
|
|
|
@@ -200,6 +205,13 @@ function applyWorkItemAssigneeToggle() {
|
|
|
200
205
|
}
|
|
201
206
|
}
|
|
202
207
|
|
|
208
|
+
function applyWorkItemElementPreviewMode() {
|
|
209
|
+
const selector = document.getElementById('work-item-element-preview-mode');
|
|
210
|
+
if (selector instanceof HTMLSelectElement) {
|
|
211
|
+
selector.value = state.workItemElementPreviewMode || 'none';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
203
215
|
function renderWorkTree() {
|
|
204
216
|
const tree = document.getElementById('work-tree');
|
|
205
217
|
if (!tree || !state.currentSandbox) return;
|
|
@@ -207,6 +219,7 @@ function renderWorkTree() {
|
|
|
207
219
|
const allItems = state.currentSandbox.items || [];
|
|
208
220
|
const items = applyWorkItemFilters(allItems);
|
|
209
221
|
const entitySummaryByNodeId = buildNodeEntitySummaryByNodeId();
|
|
222
|
+
const entityRowsByNodeId = buildNodeEntityRowsByNodeId();
|
|
210
223
|
|
|
211
224
|
if (expandedNodes.size === 0 && allItems.length > 0) {
|
|
212
225
|
if (state.workTreeViewMode === 'report') {
|
|
@@ -244,12 +257,108 @@ function renderWorkTree() {
|
|
|
244
257
|
document.getElementById('new-item-parent').value = parentId;
|
|
245
258
|
document.getElementById('item-dialog').showModal();
|
|
246
259
|
},
|
|
260
|
+
onAddDiary: (nodeId) => {
|
|
261
|
+
showNodeEntityDrawer(nodeId, 'diary');
|
|
262
|
+
const textarea = document.getElementById('drawer-diary-content');
|
|
263
|
+
if (textarea instanceof HTMLTextAreaElement) {
|
|
264
|
+
textarea.focus();
|
|
265
|
+
}
|
|
266
|
+
},
|
|
247
267
|
onEdit: (id) => {
|
|
248
268
|
editWorkItem(id);
|
|
249
269
|
},
|
|
250
270
|
onSelect: (id) => {
|
|
251
271
|
showNodeEntityDrawer(id);
|
|
252
272
|
},
|
|
273
|
+
onSelectEntity: (nodeId, entityType) => {
|
|
274
|
+
showNodeEntityDrawer(nodeId, entityType || 'all');
|
|
275
|
+
},
|
|
276
|
+
onMoveNode: async (dragNodeId, newParentId) => {
|
|
277
|
+
if (!state.currentSandbox) return;
|
|
278
|
+
if (!dragNodeId || dragNodeId === newParentId) return;
|
|
279
|
+
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
280
|
+
const dragNode = byId.get(dragNodeId);
|
|
281
|
+
if (!dragNode) return;
|
|
282
|
+
const nextParentId = newParentId || null;
|
|
283
|
+
if (nextParentId && isDescendantNode(nextParentId, dragNodeId, byId)) {
|
|
284
|
+
alert('不能将节点拖拽到其子节点下。');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const siblingItems = (state.currentSandbox.items || [])
|
|
288
|
+
.filter((item) => (item.parent_id || null) === nextParentId && item.id !== dragNodeId)
|
|
289
|
+
.sort((a, b) => compareSiblingOrder(a, b, 'order_key'));
|
|
290
|
+
const left = siblingItems[siblingItems.length - 1] || null;
|
|
291
|
+
const nextOrderKey = rankBetween(getNodeOrderKey(left, 'order_key'), '');
|
|
292
|
+
await apiRequest(`${API_BASE}/items/${dragNodeId}`, {
|
|
293
|
+
method: 'PUT',
|
|
294
|
+
body: JSON.stringify({
|
|
295
|
+
parent_id: nextParentId,
|
|
296
|
+
extra_data: {
|
|
297
|
+
...(dragNode.extra_data || {}),
|
|
298
|
+
order_key: nextOrderKey,
|
|
299
|
+
},
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
await loadSandbox(state.currentSandbox.id);
|
|
303
|
+
},
|
|
304
|
+
onReorderSiblings: async (dragNodeId, targetNodeId, position) => {
|
|
305
|
+
if (!state.currentSandbox) return;
|
|
306
|
+
if (!dragNodeId || !targetNodeId || dragNodeId === targetNodeId) return;
|
|
307
|
+
const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
|
|
308
|
+
const dragNode = byId.get(dragNodeId);
|
|
309
|
+
const targetNode = byId.get(targetNodeId);
|
|
310
|
+
if (!dragNode || !targetNode) return;
|
|
311
|
+
if (isDescendantNode(targetNodeId, dragNodeId, byId)) {
|
|
312
|
+
alert('不能将节点排序到其子树内部。');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const nextParentId = targetNode.parent_id || null;
|
|
316
|
+
const siblings = (state.currentSandbox.items || [])
|
|
317
|
+
.filter((item) => (item.parent_id || null) === nextParentId && item.id !== dragNodeId)
|
|
318
|
+
.sort((a, b) => compareSiblingOrder(a, b, 'order_key'));
|
|
319
|
+
const targetIndex = siblings.findIndex((item) => item.id === targetNodeId);
|
|
320
|
+
if (targetIndex < 0) return;
|
|
321
|
+
const insertIndex = position === 'after' ? targetIndex + 1 : targetIndex;
|
|
322
|
+
const left = siblings[insertIndex - 1] || null;
|
|
323
|
+
const right = siblings[insertIndex] || null;
|
|
324
|
+
const nextOrderKey = rankBetween(getNodeOrderKey(left, 'order_key'), getNodeOrderKey(right, 'order_key'));
|
|
325
|
+
await apiRequest(`${API_BASE}/items/${dragNodeId}`, {
|
|
326
|
+
method: 'PUT',
|
|
327
|
+
body: JSON.stringify({
|
|
328
|
+
parent_id: nextParentId,
|
|
329
|
+
extra_data: {
|
|
330
|
+
...(dragNode.extra_data || {}),
|
|
331
|
+
order_key: nextOrderKey,
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
});
|
|
335
|
+
await loadSandbox(state.currentSandbox.id);
|
|
336
|
+
},
|
|
337
|
+
onReorderLanes: async (dragRootId, targetRootId) => {
|
|
338
|
+
if (!state.currentSandbox) return;
|
|
339
|
+
if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
|
|
340
|
+
const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
|
|
341
|
+
const ordered = [...roots].sort((a, b) => compareSiblingOrder(a, b, 'lane_order_key'));
|
|
342
|
+
const fromIdx = ordered.findIndex((item) => item.id === dragRootId);
|
|
343
|
+
const toIdx = ordered.findIndex((item) => item.id === targetRootId);
|
|
344
|
+
if (fromIdx < 0 || toIdx < 0) return;
|
|
345
|
+
const [moved] = ordered.splice(fromIdx, 1);
|
|
346
|
+
ordered.splice(toIdx, 0, moved);
|
|
347
|
+
const movedIndex = ordered.findIndex((item) => item.id === dragRootId);
|
|
348
|
+
const left = ordered[movedIndex - 1] || null;
|
|
349
|
+
const right = ordered[movedIndex + 1] || null;
|
|
350
|
+
const nextLaneKey = rankBetween(getNodeOrderKey(left, 'lane_order_key'), getNodeOrderKey(right, 'lane_order_key'));
|
|
351
|
+
await apiRequest(`${API_BASE}/items/${dragRootId}`, {
|
|
352
|
+
method: 'PUT',
|
|
353
|
+
body: JSON.stringify({
|
|
354
|
+
extra_data: {
|
|
355
|
+
...(moved.extra_data || {}),
|
|
356
|
+
lane_order_key: nextLaneKey,
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
});
|
|
360
|
+
await loadSandbox(state.currentSandbox.id);
|
|
361
|
+
},
|
|
253
362
|
onDelete: async (id) => {
|
|
254
363
|
if (confirm('确定删除此任务?')) {
|
|
255
364
|
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
@@ -261,6 +370,8 @@ function renderWorkTree() {
|
|
|
261
370
|
},
|
|
262
371
|
renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
|
|
263
372
|
showAssignee: state.workItemShowAssignee,
|
|
373
|
+
elementPreviewMode: state.workItemElementPreviewMode,
|
|
374
|
+
entityRowsByNodeId,
|
|
264
375
|
});
|
|
265
376
|
|
|
266
377
|
populateParentSelect(allItems);
|
|
@@ -289,6 +400,87 @@ function buildNodeEntitySummaryByNodeId() {
|
|
|
289
400
|
return summaryByNodeId;
|
|
290
401
|
}
|
|
291
402
|
|
|
403
|
+
function buildNodeEntityRowsByNodeId() {
|
|
404
|
+
const rowsByNodeId = {};
|
|
405
|
+
for (const row of state.nodeEntities || []) {
|
|
406
|
+
const nodeId = String(row.work_item_id || '');
|
|
407
|
+
if (!nodeId) continue;
|
|
408
|
+
if (!rowsByNodeId[nodeId]) rowsByNodeId[nodeId] = [];
|
|
409
|
+
rowsByNodeId[nodeId].push({
|
|
410
|
+
id: row.id,
|
|
411
|
+
entity_type: row.entity_type,
|
|
412
|
+
title: row.title || '',
|
|
413
|
+
status: row.status || '',
|
|
414
|
+
capability_type: row.capability_type || '',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return rowsByNodeId;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isDescendantNode(candidateNodeId, parentNodeId, byId) {
|
|
421
|
+
let cursor = byId.get(candidateNodeId);
|
|
422
|
+
while (cursor && cursor.parent_id) {
|
|
423
|
+
if (cursor.parent_id === parentNodeId) return true;
|
|
424
|
+
cursor = byId.get(cursor.parent_id);
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const ORDER_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
430
|
+
const ORDER_BASE = ORDER_ALPHABET.length;
|
|
431
|
+
|
|
432
|
+
function getOrderCharIndex(ch) {
|
|
433
|
+
if (!ch) return -1;
|
|
434
|
+
return ORDER_ALPHABET.indexOf(ch);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function rankBetween(left, right) {
|
|
438
|
+
const leftKey = String(left || '');
|
|
439
|
+
const rightKey = String(right || '');
|
|
440
|
+
if (leftKey && rightKey && leftKey >= rightKey) {
|
|
441
|
+
return `${leftKey}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
442
|
+
}
|
|
443
|
+
let i = 0;
|
|
444
|
+
let prefix = '';
|
|
445
|
+
while (i < 64) {
|
|
446
|
+
const leftDigit = i < leftKey.length ? getOrderCharIndex(leftKey[i]) : -1;
|
|
447
|
+
const rightDigit = i < rightKey.length ? getOrderCharIndex(rightKey[i]) : ORDER_BASE;
|
|
448
|
+
if (leftDigit < 0 && i < leftKey.length) {
|
|
449
|
+
return `${prefix}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
450
|
+
}
|
|
451
|
+
if (rightDigit < 0 && i < rightKey.length) {
|
|
452
|
+
return `${prefix}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
453
|
+
}
|
|
454
|
+
if (rightDigit - leftDigit > 1) {
|
|
455
|
+
const mid = Math.floor((leftDigit + rightDigit) / 2);
|
|
456
|
+
return `${prefix}${ORDER_ALPHABET[mid]}`;
|
|
457
|
+
}
|
|
458
|
+
prefix += i < leftKey.length ? leftKey[i] : ORDER_ALPHABET[0];
|
|
459
|
+
i += 1;
|
|
460
|
+
}
|
|
461
|
+
return `${prefix}${ORDER_ALPHABET[Math.floor(ORDER_BASE / 2)]}`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function getNodeOrderKey(item, keyName = 'order_key') {
|
|
465
|
+
return String(item?.extra_data?.[keyName] || '').trim();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function compareSiblingOrder(a, b, keyName = 'order_key') {
|
|
469
|
+
const keyA = getNodeOrderKey(a, keyName);
|
|
470
|
+
const keyB = getNodeOrderKey(b, keyName);
|
|
471
|
+
if (keyA && keyB && keyA !== keyB) return keyA.localeCompare(keyB);
|
|
472
|
+
if (keyA && !keyB) return -1;
|
|
473
|
+
if (!keyA && keyB) return 1;
|
|
474
|
+
const numericA = Number(a?.extra_data?.lane_order);
|
|
475
|
+
const numericB = Number(b?.extra_data?.lane_order);
|
|
476
|
+
const validA = Number.isFinite(numericA);
|
|
477
|
+
const validB = Number.isFinite(numericB);
|
|
478
|
+
if (keyName === 'lane_order_key' && validA && validB && numericA !== numericB) return numericA - numericB;
|
|
479
|
+
if (keyName === 'lane_order_key' && validA && !validB) return -1;
|
|
480
|
+
if (keyName === 'lane_order_key' && !validA && validB) return 1;
|
|
481
|
+
return String(a?.created_at || '').localeCompare(String(b?.created_at || ''));
|
|
482
|
+
}
|
|
483
|
+
|
|
292
484
|
function populateParentSelect(items) {
|
|
293
485
|
const select = document.getElementById('new-item-parent');
|
|
294
486
|
if (!select) return;
|
|
@@ -298,22 +490,182 @@ function populateParentSelect(items) {
|
|
|
298
490
|
}
|
|
299
491
|
|
|
300
492
|
function renderMarkdownSnippet(text) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
493
|
+
const source = String(text || '').replace(/\r\n/g, '\n');
|
|
494
|
+
const lines = source.split('\n');
|
|
495
|
+
const blocks = [];
|
|
496
|
+
let listItems = [];
|
|
497
|
+
|
|
498
|
+
const flushList = () => {
|
|
499
|
+
if (!listItems.length) return;
|
|
500
|
+
blocks.push(`<ul>${listItems.map((item) => `<li>${item}</li>`).join('')}</ul>`);
|
|
501
|
+
listItems = [];
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const renderInline = (raw) => {
|
|
505
|
+
let html = safeText(String(raw || ''));
|
|
506
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
507
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
508
|
+
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
509
|
+
html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_m, label, url) => {
|
|
510
|
+
const safeUrl = safeText(url);
|
|
511
|
+
const safeLabel = safeText(label);
|
|
512
|
+
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
|
|
513
|
+
});
|
|
514
|
+
return html;
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
for (const line of lines) {
|
|
518
|
+
const trimmed = line.trim();
|
|
519
|
+
if (!trimmed) {
|
|
520
|
+
flushList();
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (/^[-*]\s+/.test(trimmed)) {
|
|
524
|
+
listItems.push(renderInline(trimmed.replace(/^[-*]\s+/, '')));
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
flushList();
|
|
528
|
+
const heading = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
529
|
+
if (heading) {
|
|
530
|
+
const level = Math.min(6, heading[1].length);
|
|
531
|
+
blocks.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
blocks.push(`<p>${renderInline(trimmed)}</p>`);
|
|
535
|
+
}
|
|
536
|
+
flushList();
|
|
537
|
+
return blocks.join('');
|
|
307
538
|
}
|
|
308
539
|
|
|
309
540
|
function getNodeById(nodeId) {
|
|
310
541
|
return state.currentSandbox?.items?.find((item) => item.id === nodeId) || null;
|
|
311
542
|
}
|
|
312
543
|
|
|
544
|
+
function getWorkItemNameById(nodeId) {
|
|
545
|
+
const row = getNodeById(nodeId);
|
|
546
|
+
return row?.name || String(nodeId || '');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function renderQuickDiaryTargetLabel() {
|
|
550
|
+
const el = document.getElementById('sandbox-diary-target-label');
|
|
551
|
+
if (!(el instanceof HTMLElement)) return;
|
|
552
|
+
if (!state.currentSandbox) {
|
|
553
|
+
el.textContent = '快速日记:未进入沙盘';
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const nodeId = state.selectedNodeId;
|
|
557
|
+
if (!nodeId) {
|
|
558
|
+
el.textContent = `快速日记:关联沙盘 ${state.currentSandbox.name}`;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
el.textContent = `快速日记:${state.currentSandbox.name} / ${getWorkItemNameById(nodeId)}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function saveDiaryEntry({ content, sandboxId = null, workItemId = null }) {
|
|
565
|
+
const payload = {
|
|
566
|
+
sandbox_id: sandboxId,
|
|
567
|
+
work_item_id: workItemId,
|
|
568
|
+
content: String(content || '').trim(),
|
|
569
|
+
};
|
|
570
|
+
if (!payload.content) return null;
|
|
571
|
+
return apiRequest(`${API_BASE}/diaries`, {
|
|
572
|
+
method: 'POST',
|
|
573
|
+
body: JSON.stringify(payload),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function shouldAutoCaptureDiary(text) {
|
|
578
|
+
const normalized = String(text || '').trim().toLowerCase();
|
|
579
|
+
if (!normalized) return false;
|
|
580
|
+
if (normalized.length < 2) return false;
|
|
581
|
+
const patterns = [
|
|
582
|
+
/记录/,
|
|
583
|
+
/进展/,
|
|
584
|
+
/日志/,
|
|
585
|
+
/汇报/,
|
|
586
|
+
/同步/,
|
|
587
|
+
/今日.*完成/,
|
|
588
|
+
/今天.*完成/,
|
|
589
|
+
/progress/,
|
|
590
|
+
/update/,
|
|
591
|
+
];
|
|
592
|
+
return patterns.some((pattern) => pattern.test(normalized));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function resolveDiaryCaptureIntent(text) {
|
|
596
|
+
const raw = String(text || '').trim();
|
|
597
|
+
const auto = shouldAutoCaptureDiary(raw);
|
|
598
|
+
const directPrefix = /^(请\s*)?(帮我\s*)?(记录(日记|日志)|写(日记|日志)|日记|日志)\s*[::]?\s*/;
|
|
599
|
+
const isDirectDiaryCommand = directPrefix.test(raw);
|
|
600
|
+
const stripped = isDirectDiaryCommand ? raw.replace(directPrefix, '').trim() : raw;
|
|
601
|
+
return {
|
|
602
|
+
shouldCapture: auto,
|
|
603
|
+
isDirectDiaryCommand,
|
|
604
|
+
diaryContent: stripped || raw,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function parseNodeIdFromNodeContext(payloadContent) {
|
|
609
|
+
const text = String(payloadContent || '');
|
|
610
|
+
const match = text.match(/node_id=([^\n]+)/);
|
|
611
|
+
if (!match) return null;
|
|
612
|
+
const nodeId = String(match[1] || '').trim();
|
|
613
|
+
return nodeId || null;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function openDiaryEditDialog(diary) {
|
|
617
|
+
const dialog = document.getElementById('diary-edit-dialog');
|
|
618
|
+
const textarea = document.getElementById('diary-edit-content');
|
|
619
|
+
const preview = document.getElementById('diary-edit-preview');
|
|
620
|
+
if (!(dialog instanceof HTMLDialogElement) || !(textarea instanceof HTMLTextAreaElement)) return;
|
|
621
|
+
dialog.dataset.editDiaryId = String(diary?.id || '');
|
|
622
|
+
textarea.value = String(diary?.content || '');
|
|
623
|
+
if (preview instanceof HTMLElement) {
|
|
624
|
+
const rendered = renderMarkdownSnippet(textarea.value);
|
|
625
|
+
preview.innerHTML = rendered || '<p class="is-empty">在左侧输入 Markdown,这里会实时预览。</p>';
|
|
626
|
+
preview.classList.toggle('is-empty', !rendered);
|
|
627
|
+
}
|
|
628
|
+
dialog.showModal();
|
|
629
|
+
textarea.focus();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function closeDiaryEditDialog() {
|
|
633
|
+
const dialog = document.getElementById('diary-edit-dialog');
|
|
634
|
+
if (!(dialog instanceof HTMLDialogElement)) return;
|
|
635
|
+
dialog.close();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function submitDiaryEditDialog() {
|
|
639
|
+
const dialog = document.getElementById('diary-edit-dialog');
|
|
640
|
+
const textarea = document.getElementById('diary-edit-content');
|
|
641
|
+
if (!(dialog instanceof HTMLDialogElement) || !(textarea instanceof HTMLTextAreaElement)) return;
|
|
642
|
+
const diaryId = String(dialog.dataset.editDiaryId || '').trim();
|
|
643
|
+
const content = textarea.value.trim();
|
|
644
|
+
if (!diaryId || !content) return;
|
|
645
|
+
await apiRequest(`${API_BASE}/diaries/${diaryId}`, {
|
|
646
|
+
method: 'PUT',
|
|
647
|
+
body: JSON.stringify({ content }),
|
|
648
|
+
});
|
|
649
|
+
const selectedNodeId = state.selectedNodeId;
|
|
650
|
+
const selectedFilter = state.nodeEntityFilter;
|
|
651
|
+
closeDiaryEditDialog();
|
|
652
|
+
await loadDiaries();
|
|
653
|
+
if (state.currentSandbox?.id) {
|
|
654
|
+
await loadSandbox(state.currentSandbox.id);
|
|
655
|
+
if (selectedNodeId) {
|
|
656
|
+
showNodeEntityDrawer(selectedNodeId, selectedFilter);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
313
661
|
function getNodeEntitiesByNodeId(nodeId) {
|
|
314
662
|
return (state.nodeEntities || []).filter((row) => row.work_item_id === nodeId);
|
|
315
663
|
}
|
|
316
664
|
|
|
665
|
+
function getNodeDiariesByNodeId(nodeId) {
|
|
666
|
+
return (state.currentSandbox?.diaries || []).filter((row) => row.work_item_id === nodeId);
|
|
667
|
+
}
|
|
668
|
+
|
|
317
669
|
function closeNodeEntityDrawer() {
|
|
318
670
|
const drawer = document.getElementById('node-entity-drawer');
|
|
319
671
|
if (!drawer) return;
|
|
@@ -349,6 +701,7 @@ function renderNodeEntitySummary(nodeId) {
|
|
|
349
701
|
const container = document.getElementById('node-entity-summary');
|
|
350
702
|
if (!container) return;
|
|
351
703
|
const rows = getNodeEntitiesByNodeId(nodeId);
|
|
704
|
+
const diaries = getNodeDiariesByNodeId(nodeId);
|
|
352
705
|
const issues = rows.filter((row) => row.entity_type === 'issue');
|
|
353
706
|
const knowledges = rows.filter((row) => row.entity_type === 'knowledge');
|
|
354
707
|
const capabilities = rows.filter((row) => row.entity_type === 'capability');
|
|
@@ -358,38 +711,88 @@ function renderNodeEntitySummary(nodeId) {
|
|
|
358
711
|
<div class="summary-card"><div class="label">Open Issue</div><div class="value">${openIssues}</div></div>
|
|
359
712
|
<div class="summary-card"><div class="label">Knowledge</div><div class="value">${knowledges.length}</div></div>
|
|
360
713
|
<div class="summary-card"><div class="label">Capability</div><div class="value">${capabilities.length}</div></div>
|
|
714
|
+
<div class="summary-card"><div class="label">Diary</div><div class="value">${diaries.length}</div></div>
|
|
361
715
|
`;
|
|
362
716
|
}
|
|
363
717
|
|
|
718
|
+
async function processNodeDiary(diaryId, action) {
|
|
719
|
+
if (!state.currentSandbox) return;
|
|
720
|
+
await apiRequest(`${API_BASE}/diaries/${diaryId}/process`, {
|
|
721
|
+
method: 'PUT',
|
|
722
|
+
body: JSON.stringify({ action }),
|
|
723
|
+
});
|
|
724
|
+
const selectedNodeId = state.selectedNodeId;
|
|
725
|
+
const selectedFilter = state.nodeEntityFilter;
|
|
726
|
+
await loadSandbox(state.currentSandbox.id);
|
|
727
|
+
await loadDiaries();
|
|
728
|
+
if (selectedNodeId) {
|
|
729
|
+
showNodeEntityDrawer(selectedNodeId, selectedFilter);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
364
733
|
function renderNodeEntityList(nodeId) {
|
|
365
734
|
const container = document.getElementById('node-entity-list');
|
|
366
735
|
if (!container) return;
|
|
367
|
-
const
|
|
736
|
+
const allEntityRows = getNodeEntitiesByNodeId(nodeId);
|
|
737
|
+
const allDiaryRows = getNodeDiariesByNodeId(nodeId);
|
|
738
|
+
const timelineRows = [
|
|
739
|
+
...allEntityRows.map((row) => ({ ...row, timeline_type: row.entity_type || 'issue' })),
|
|
740
|
+
...allDiaryRows.map((row) => ({ ...row, timeline_type: 'diary' })),
|
|
741
|
+
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
368
742
|
const rows = state.nodeEntityFilter === 'all'
|
|
369
|
-
?
|
|
370
|
-
:
|
|
743
|
+
? timelineRows
|
|
744
|
+
: timelineRows.filter((row) => row.timeline_type === state.nodeEntityFilter);
|
|
371
745
|
if (!rows.length) {
|
|
372
|
-
container.innerHTML = '<div class="empty-state"><p
|
|
746
|
+
container.innerHTML = '<div class="empty-state"><p>当前筛选下暂无记录</p></div>';
|
|
373
747
|
return;
|
|
374
748
|
}
|
|
375
|
-
container.innerHTML = rows.map((row) =>
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
<div>
|
|
379
|
-
<
|
|
380
|
-
|
|
749
|
+
container.innerHTML = rows.map((row) => {
|
|
750
|
+
if (row.timeline_type === 'diary') {
|
|
751
|
+
return `
|
|
752
|
+
<div class="entity-card diary-card ${row.processed ? 'processed' : ''}">
|
|
753
|
+
<div class="entity-card-header">
|
|
754
|
+
<div>
|
|
755
|
+
<span class="entity-type-pill">diary</span>
|
|
756
|
+
<strong>${safeText('日志记录')}</strong>
|
|
757
|
+
</div>
|
|
758
|
+
<div class="entity-card-actions">
|
|
759
|
+
<button class="btn btn-secondary btn-sm" data-diary-edit-id="${safeText(row.id)}">编辑</button>
|
|
760
|
+
${row.processed ? '' : `
|
|
761
|
+
<button class="btn btn-secondary btn-sm" data-diary-confirm-id="${safeText(row.id)}">采纳</button>
|
|
762
|
+
<button class="btn btn-secondary btn-sm" data-diary-ignore-id="${safeText(row.id)}">忽略</button>
|
|
763
|
+
`}
|
|
764
|
+
</div>
|
|
765
|
+
</div>
|
|
766
|
+
<div class="entity-meta">
|
|
767
|
+
${safeText(new Date(row.created_at).toLocaleString())}
|
|
768
|
+
${row.processed ? ' · 已处理' : ' · 未处理'}
|
|
769
|
+
</div>
|
|
770
|
+
<div class="entity-content">${renderMarkdownSnippet(row.content || '')}</div>
|
|
381
771
|
</div>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
return `
|
|
775
|
+
<div class="entity-card">
|
|
776
|
+
<div class="entity-card-header">
|
|
777
|
+
<div>
|
|
778
|
+
<span class="entity-type-pill">${safeText(row.entity_type)}</span>
|
|
779
|
+
<strong>${safeText(row.title || '-')}</strong>
|
|
780
|
+
</div>
|
|
781
|
+
<div class="entity-card-actions">
|
|
782
|
+
<button class="btn btn-secondary btn-sm" data-entity-edit-id="${safeText(row.id)}">编辑</button>
|
|
783
|
+
<button class="btn btn-secondary btn-sm" data-entity-delete-id="${safeText(row.id)}">删除</button>
|
|
784
|
+
</div>
|
|
385
785
|
</div>
|
|
786
|
+
<div class="entity-meta">
|
|
787
|
+
${safeText(new Date(row.created_at).toLocaleString())}
|
|
788
|
+
${row.status ? ` · <span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}
|
|
789
|
+
${row.priority ? ` · ${safeText(row.priority)}` : ''}
|
|
790
|
+
${row.assignee ? ` · @${safeText(row.assignee)}` : ''}
|
|
791
|
+
</div>
|
|
792
|
+
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
386
793
|
</div>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
</div>
|
|
390
|
-
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
391
|
-
</div>
|
|
392
|
-
`).join('');
|
|
794
|
+
`;
|
|
795
|
+
}).join('');
|
|
393
796
|
|
|
394
797
|
container.querySelectorAll('[data-entity-delete-id]').forEach((el) => {
|
|
395
798
|
el.addEventListener('click', async (e) => {
|
|
@@ -406,11 +809,40 @@ function renderNodeEntityList(nodeId) {
|
|
|
406
809
|
e.preventDefault();
|
|
407
810
|
const id = el.getAttribute('data-entity-edit-id');
|
|
408
811
|
if (!id) return;
|
|
409
|
-
const row =
|
|
812
|
+
const row = allEntityRows.find((item) => item.id === id);
|
|
410
813
|
if (!row) return;
|
|
411
814
|
startEditNodeEntity(row);
|
|
412
815
|
});
|
|
413
816
|
});
|
|
817
|
+
|
|
818
|
+
container.querySelectorAll('[data-diary-edit-id]').forEach((el) => {
|
|
819
|
+
el.addEventListener('click', (e) => {
|
|
820
|
+
e.preventDefault();
|
|
821
|
+
const id = el.getAttribute('data-diary-edit-id');
|
|
822
|
+
if (!id) return;
|
|
823
|
+
const row = allDiaryRows.find((item) => item.id === id);
|
|
824
|
+
if (!row) return;
|
|
825
|
+
openDiaryEditDialog(row);
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
container.querySelectorAll('[data-diary-confirm-id]').forEach((el) => {
|
|
830
|
+
el.addEventListener('click', async (e) => {
|
|
831
|
+
e.preventDefault();
|
|
832
|
+
const id = el.getAttribute('data-diary-confirm-id');
|
|
833
|
+
if (!id) return;
|
|
834
|
+
await processNodeDiary(id, 'confirm');
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
container.querySelectorAll('[data-diary-ignore-id]').forEach((el) => {
|
|
839
|
+
el.addEventListener('click', async (e) => {
|
|
840
|
+
e.preventDefault();
|
|
841
|
+
const id = el.getAttribute('data-diary-ignore-id');
|
|
842
|
+
if (!id) return;
|
|
843
|
+
await processNodeDiary(id, 'ignore');
|
|
844
|
+
});
|
|
845
|
+
});
|
|
414
846
|
}
|
|
415
847
|
|
|
416
848
|
function resetNodeEntityForm() {
|
|
@@ -438,6 +870,19 @@ function resetNodeEntityForm() {
|
|
|
438
870
|
setNodeEntityFormExpanded(false);
|
|
439
871
|
}
|
|
440
872
|
|
|
873
|
+
function ensureCapabilityTypeOption(value) {
|
|
874
|
+
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
875
|
+
if (!(capabilityTypeInput instanceof HTMLSelectElement)) return;
|
|
876
|
+
const normalized = String(value || '').trim();
|
|
877
|
+
if (!normalized) return;
|
|
878
|
+
const hasOption = Array.from(capabilityTypeInput.options).some((option) => option.value === normalized);
|
|
879
|
+
if (hasOption) return;
|
|
880
|
+
const option = document.createElement('option');
|
|
881
|
+
option.value = normalized;
|
|
882
|
+
option.textContent = `${normalized}(历史值)`;
|
|
883
|
+
capabilityTypeInput.appendChild(option);
|
|
884
|
+
}
|
|
885
|
+
|
|
441
886
|
function startEditNodeEntity(row) {
|
|
442
887
|
state.editingNodeEntityId = row.id;
|
|
443
888
|
setNodeEntityFormExpanded(true);
|
|
@@ -455,6 +900,7 @@ function startEditNodeEntity(row) {
|
|
|
455
900
|
if (assigneeInput) assigneeInput.value = row.assignee || '';
|
|
456
901
|
if (statusInput) statusInput.value = row.status || '';
|
|
457
902
|
if (priorityInput) priorityInput.value = row.priority || '';
|
|
903
|
+
ensureCapabilityTypeOption(row.capability_type || '');
|
|
458
904
|
if (capabilityTypeInput) capabilityTypeInput.value = row.capability_type || '';
|
|
459
905
|
|
|
460
906
|
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
@@ -464,15 +910,17 @@ function startEditNodeEntity(row) {
|
|
|
464
910
|
titleInput?.focus();
|
|
465
911
|
}
|
|
466
912
|
|
|
467
|
-
function showNodeEntityDrawer(nodeId) {
|
|
913
|
+
function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
|
|
468
914
|
const drawer = document.getElementById('node-entity-drawer');
|
|
469
915
|
const title = document.getElementById('drawer-node-title');
|
|
470
916
|
const node = getNodeById(nodeId);
|
|
471
917
|
if (!drawer || !title || !node) return;
|
|
472
918
|
state.selectedNodeId = nodeId;
|
|
473
|
-
|
|
919
|
+
const filter = ['all', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
|
|
920
|
+
state.nodeEntityFilter = filter;
|
|
474
921
|
title.textContent = node.name || nodeId;
|
|
475
922
|
renderNodeEntitySummary(nodeId);
|
|
923
|
+
renderQuickDiaryTargetLabel();
|
|
476
924
|
renderNodeEntityFilterTabs();
|
|
477
925
|
renderNodeEntityList(nodeId);
|
|
478
926
|
resetNodeEntityForm();
|
|
@@ -590,6 +1038,8 @@ async function sendSandboxChatMessage(content, options = {}) {
|
|
|
590
1038
|
|
|
591
1039
|
const displayContent = options.displayContent || content;
|
|
592
1040
|
const payloadContent = options.payloadContent || content;
|
|
1041
|
+
const autoDiaryWorkItemId = options.workItemId || parseNodeIdFromNodeContext(payloadContent);
|
|
1042
|
+
const diaryIntent = resolveDiaryCaptureIntent(displayContent || content);
|
|
593
1043
|
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(displayContent)}</div>`);
|
|
594
1044
|
const loadingMessage = appendLoadingMessage(messages);
|
|
595
1045
|
messages.scrollTop = messages.scrollHeight;
|
|
@@ -598,6 +1048,31 @@ async function sendSandboxChatMessage(content, options = {}) {
|
|
|
598
1048
|
setButtonState(btn, { disabled: true, text: '思考中' });
|
|
599
1049
|
|
|
600
1050
|
try {
|
|
1051
|
+
let createdDiary = null;
|
|
1052
|
+
if (diaryIntent.shouldCapture) {
|
|
1053
|
+
createdDiary = await saveDiaryEntry({
|
|
1054
|
+
content: diaryIntent.diaryContent,
|
|
1055
|
+
sandboxId: state.currentSandbox.id,
|
|
1056
|
+
workItemId: autoDiaryWorkItemId || null,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
if (diaryIntent.isDirectDiaryCommand) {
|
|
1060
|
+
loadingMessage?.remove();
|
|
1061
|
+
if (createdDiary && state.currentSandbox) {
|
|
1062
|
+
state.currentSandbox.diaries = [createdDiary, ...(state.currentSandbox.diaries || [])];
|
|
1063
|
+
renderSandboxOverview();
|
|
1064
|
+
if (state.selectedNodeId && String(createdDiary.work_item_id || '') === String(state.selectedNodeId)) {
|
|
1065
|
+
renderNodeEntitySummary(state.selectedNodeId);
|
|
1066
|
+
renderNodeEntityList(state.selectedNodeId);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
messages.insertAdjacentHTML('beforeend', '<div class="chat-message assistant">📝 已记录日记。</div>');
|
|
1070
|
+
messages.scrollTop = messages.scrollHeight;
|
|
1071
|
+
if (state.currentSandbox) {
|
|
1072
|
+
await loadDiaries();
|
|
1073
|
+
}
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
601
1076
|
const history = state.chats
|
|
602
1077
|
.filter(c => c.role)
|
|
603
1078
|
.slice(-10)
|
|
@@ -693,7 +1168,7 @@ function openQuickChatPopover(nodeId, anchorEl) {
|
|
|
693
1168
|
closeQuickChatPopover();
|
|
694
1169
|
const payloadContent = composeQuickChatContent(nodeId, question);
|
|
695
1170
|
const displayContent = `【快捷】${question}`;
|
|
696
|
-
await sendSandboxChatMessage(question, { payloadContent, displayContent });
|
|
1171
|
+
await sendSandboxChatMessage(question, { payloadContent, displayContent, workItemId: nodeId });
|
|
697
1172
|
});
|
|
698
1173
|
textarea?.addEventListener('input', updateSubmitState);
|
|
699
1174
|
textarea?.addEventListener('keydown', async (event) => {
|
|
@@ -771,8 +1246,10 @@ async function loadSandbox(id) {
|
|
|
771
1246
|
document.getElementById('sandbox-title').textContent = sandbox.name;
|
|
772
1247
|
applyWorkTreeViewMode(state.workTreeViewMode || 'full');
|
|
773
1248
|
applyWorkItemAssigneeToggle();
|
|
1249
|
+
applyWorkItemElementPreviewMode();
|
|
774
1250
|
applySandboxLayoutMode();
|
|
775
1251
|
applySandboxFullscreenState();
|
|
1252
|
+
renderQuickDiaryTargetLabel();
|
|
776
1253
|
renderSandboxOverview();
|
|
777
1254
|
renderWorkTree();
|
|
778
1255
|
loadSandboxChats(id);
|
|
@@ -886,8 +1363,63 @@ window.undoOperation = async function(operationId, btn) {
|
|
|
886
1363
|
}
|
|
887
1364
|
};
|
|
888
1365
|
|
|
1366
|
+
async function hydrateDiaryWorkItemNames(diaries) {
|
|
1367
|
+
const map = {};
|
|
1368
|
+
const sandboxIds = Array.from(
|
|
1369
|
+
new Set(
|
|
1370
|
+
(diaries || [])
|
|
1371
|
+
.filter((row) => row?.sandbox_id && row?.work_item_id)
|
|
1372
|
+
.map((row) => String(row.sandbox_id)),
|
|
1373
|
+
),
|
|
1374
|
+
);
|
|
1375
|
+
await Promise.all(sandboxIds.map(async (sandboxId) => {
|
|
1376
|
+
try {
|
|
1377
|
+
const items = await apiRequest(`${API_BASE}/sandboxes/${sandboxId}/items`);
|
|
1378
|
+
(items || []).forEach((item) => {
|
|
1379
|
+
const key = `${sandboxId}:${item.id}`;
|
|
1380
|
+
map[key] = item.name || item.id;
|
|
1381
|
+
});
|
|
1382
|
+
} catch {
|
|
1383
|
+
// Ignore per-sandbox fetch failure to keep diary page available.
|
|
1384
|
+
}
|
|
1385
|
+
}));
|
|
1386
|
+
state.diaryWorkItemNameMap = map;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function getDiaryWorkItemName(sandboxId, workItemId) {
|
|
1390
|
+
if (!workItemId) return '';
|
|
1391
|
+
const sandboxKey = String(sandboxId || '').trim();
|
|
1392
|
+
const itemKey = String(workItemId || '').trim();
|
|
1393
|
+
if (!itemKey) return '';
|
|
1394
|
+
if (sandboxKey) {
|
|
1395
|
+
const key = `${sandboxKey}:${itemKey}`;
|
|
1396
|
+
if (state.diaryWorkItemNameMap[key]) {
|
|
1397
|
+
return state.diaryWorkItemNameMap[key];
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (state.currentSandbox?.id === sandboxKey) {
|
|
1401
|
+
const node = (state.currentSandbox.items || []).find((item) => item.id === itemKey);
|
|
1402
|
+
if (node?.name) return node.name;
|
|
1403
|
+
}
|
|
1404
|
+
return itemKey;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function openDiaryWorkItemInSandbox(sandboxId, workItemId) {
|
|
1408
|
+
const targetSandboxId = String(sandboxId || '').trim();
|
|
1409
|
+
const targetWorkItemId = String(workItemId || '').trim();
|
|
1410
|
+
if (!targetSandboxId || !targetWorkItemId) return;
|
|
1411
|
+
const nextHash = `/sandbox/${encodeURIComponent(targetSandboxId)}?node_id=${encodeURIComponent(targetWorkItemId)}&open_drawer=1`;
|
|
1412
|
+
const currentHash = window.location.hash.slice(1);
|
|
1413
|
+
if (currentHash === nextHash && state.currentSandbox?.id === targetSandboxId) {
|
|
1414
|
+
showNodeEntityDrawer(targetWorkItemId);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
window.location.hash = nextHash;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
889
1420
|
async function loadDiaries() {
|
|
890
1421
|
state.diaries = await apiRequest(`${API_BASE}/diaries`);
|
|
1422
|
+
await hydrateDiaryWorkItemNames(state.diaries);
|
|
891
1423
|
renderDiaries();
|
|
892
1424
|
}
|
|
893
1425
|
|
|
@@ -899,6 +1431,11 @@ function renderDiaries() {
|
|
|
899
1431
|
mountDiaryTimeline('diary-timeline', {
|
|
900
1432
|
diaries: filtered,
|
|
901
1433
|
getSandboxName,
|
|
1434
|
+
getWorkItemName: getDiaryWorkItemName,
|
|
1435
|
+
onOpenWorkItem: (sandboxId, workItemId) => {
|
|
1436
|
+
openDiaryWorkItemInSandbox(sandboxId, workItemId);
|
|
1437
|
+
},
|
|
1438
|
+
renderContent: (content) => renderMarkdownSnippet(content),
|
|
902
1439
|
onConfirm: async (id) => {
|
|
903
1440
|
await apiRequest(`${API_BASE}/diaries/${id}/process`, {
|
|
904
1441
|
method: 'PUT',
|
|
@@ -912,7 +1449,12 @@ function renderDiaries() {
|
|
|
912
1449
|
body: JSON.stringify({ action: 'ignore' }),
|
|
913
1450
|
});
|
|
914
1451
|
await loadDiaries();
|
|
915
|
-
}
|
|
1452
|
+
},
|
|
1453
|
+
onEdit: async (id) => {
|
|
1454
|
+
const diary = state.diaries.find((row) => row.id === id);
|
|
1455
|
+
if (!diary) return;
|
|
1456
|
+
openDiaryEditDialog(diary);
|
|
1457
|
+
},
|
|
916
1458
|
});
|
|
917
1459
|
}
|
|
918
1460
|
|
|
@@ -922,10 +1464,12 @@ function getSandboxName(id) {
|
|
|
922
1464
|
}
|
|
923
1465
|
|
|
924
1466
|
function updateSandboxSelect() {
|
|
925
|
-
const
|
|
926
|
-
if (
|
|
927
|
-
|
|
1467
|
+
const diaryFilterSelect = document.getElementById('diary-sandbox-filter');
|
|
1468
|
+
if (diaryFilterSelect) {
|
|
1469
|
+
const current = state.diarySandboxFilter || '';
|
|
1470
|
+
diaryFilterSelect.innerHTML = '<option value="">全部关联沙盘</option>' +
|
|
928
1471
|
state.sandboxes.map(s => `<option value="${s.id}">${safeText(s.name)}</option>`).join('');
|
|
1472
|
+
diaryFilterSelect.value = current;
|
|
929
1473
|
}
|
|
930
1474
|
|
|
931
1475
|
const changesSelect = document.getElementById('changes-sandbox-filter');
|
|
@@ -1241,15 +1785,6 @@ function collectSettingHeadersFromUI() {
|
|
|
1241
1785
|
return headers;
|
|
1242
1786
|
}
|
|
1243
1787
|
|
|
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
1788
|
function showPage(pageId) {
|
|
1254
1789
|
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
|
1255
1790
|
const page = document.getElementById(`page-${pageId}`);
|
|
@@ -1280,6 +1815,7 @@ function editWorkItem(id) {
|
|
|
1280
1815
|
function initApp() {
|
|
1281
1816
|
const hash = window.location.hash;
|
|
1282
1817
|
loadWorkItemAssigneePreference();
|
|
1818
|
+
renderQuickDiaryTargetLabel();
|
|
1283
1819
|
|
|
1284
1820
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
1285
1821
|
link.addEventListener('click', (e) => {
|
|
@@ -1532,43 +2068,6 @@ function initApp() {
|
|
|
1532
2068
|
await loadSandbox(state.currentSandbox.id);
|
|
1533
2069
|
});
|
|
1534
2070
|
|
|
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
2071
|
document.getElementById('sandbox-send-btn')?.addEventListener('click', async () => {
|
|
1573
2072
|
const input = document.getElementById('sandbox-chat-input');
|
|
1574
2073
|
const content = input.value.trim();
|
|
@@ -1693,18 +2192,53 @@ function initApp() {
|
|
|
1693
2192
|
});
|
|
1694
2193
|
|
|
1695
2194
|
document.getElementById('save-diary-btn')?.addEventListener('click', async () => {
|
|
1696
|
-
const sandboxId = document.getElementById('diary-sandbox-select').value || null;
|
|
1697
2195
|
const content = document.getElementById('diary-content').value.trim();
|
|
1698
2196
|
if (!content) return;
|
|
1699
|
-
|
|
1700
|
-
await apiRequest(`${API_BASE}/diaries`, {
|
|
1701
|
-
method: 'POST',
|
|
1702
|
-
body: JSON.stringify({ sandbox_id: sandboxId, content }),
|
|
1703
|
-
});
|
|
1704
|
-
|
|
2197
|
+
await saveDiaryEntry({ content, sandboxId: null, workItemId: null });
|
|
1705
2198
|
document.getElementById('diary-content').value = '';
|
|
1706
2199
|
await loadDiaries();
|
|
1707
2200
|
});
|
|
2201
|
+
|
|
2202
|
+
document.getElementById('drawer-diary-save-btn')?.addEventListener('click', async () => {
|
|
2203
|
+
if (!state.currentSandbox || !state.selectedNodeId) return;
|
|
2204
|
+
const textarea = document.getElementById('drawer-diary-content');
|
|
2205
|
+
if (!(textarea instanceof HTMLTextAreaElement)) return;
|
|
2206
|
+
const content = textarea.value.trim();
|
|
2207
|
+
if (!content) return;
|
|
2208
|
+
const selectedNodeId = state.selectedNodeId;
|
|
2209
|
+
const selectedFilter = state.nodeEntityFilter;
|
|
2210
|
+
await saveDiaryEntry({
|
|
2211
|
+
content,
|
|
2212
|
+
sandboxId: state.currentSandbox.id,
|
|
2213
|
+
workItemId: selectedNodeId,
|
|
2214
|
+
});
|
|
2215
|
+
textarea.value = '';
|
|
2216
|
+
await loadSandbox(state.currentSandbox.id);
|
|
2217
|
+
await loadDiaries();
|
|
2218
|
+
showNodeEntityDrawer(selectedNodeId, selectedFilter);
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2221
|
+
document.getElementById('cancel-edit-diary-btn')?.addEventListener('click', () => {
|
|
2222
|
+
closeDiaryEditDialog();
|
|
2223
|
+
});
|
|
2224
|
+
document.getElementById('confirm-edit-diary-btn')?.addEventListener('click', async () => {
|
|
2225
|
+
await submitDiaryEditDialog();
|
|
2226
|
+
});
|
|
2227
|
+
document.getElementById('diary-edit-content')?.addEventListener('keydown', async (event) => {
|
|
2228
|
+
const isSubmit = event.key === 'Enter' && (event.metaKey || event.ctrlKey);
|
|
2229
|
+
if (!isSubmit) return;
|
|
2230
|
+
event.preventDefault();
|
|
2231
|
+
await submitDiaryEditDialog();
|
|
2232
|
+
});
|
|
2233
|
+
document.getElementById('diary-edit-content')?.addEventListener('input', (event) => {
|
|
2234
|
+
const preview = document.getElementById('diary-edit-preview');
|
|
2235
|
+
if (!(preview instanceof HTMLElement)) return;
|
|
2236
|
+
const target = event.target;
|
|
2237
|
+
const value = target instanceof HTMLTextAreaElement ? target.value : '';
|
|
2238
|
+
const rendered = renderMarkdownSnippet(value);
|
|
2239
|
+
preview.innerHTML = rendered || '<p class="is-empty">在左侧输入 Markdown,这里会实时预览。</p>';
|
|
2240
|
+
preview.classList.toggle('is-empty', !rendered);
|
|
2241
|
+
});
|
|
1708
2242
|
|
|
1709
2243
|
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
1710
2244
|
const api_url = document.getElementById('setting-api-url').value;
|
|
@@ -1757,6 +2291,11 @@ function initApp() {
|
|
|
1757
2291
|
renderWorkTree();
|
|
1758
2292
|
});
|
|
1759
2293
|
|
|
2294
|
+
document.getElementById('work-item-element-preview-mode')?.addEventListener('change', (e) => {
|
|
2295
|
+
state.workItemElementPreviewMode = e.target.value || 'none';
|
|
2296
|
+
renderWorkTree();
|
|
2297
|
+
});
|
|
2298
|
+
|
|
1760
2299
|
document.getElementById('work-item-show-assignee-toggle')?.addEventListener('change', (e) => {
|
|
1761
2300
|
state.workItemShowAssignee = Boolean(e.target.checked);
|
|
1762
2301
|
persistWorkItemAssigneePreference();
|
|
@@ -1773,6 +2312,11 @@ function initApp() {
|
|
|
1773
2312
|
renderDiaries();
|
|
1774
2313
|
});
|
|
1775
2314
|
|
|
2315
|
+
document.getElementById('diary-sandbox-filter')?.addEventListener('change', (e) => {
|
|
2316
|
+
state.diarySandboxFilter = e.target.value || '';
|
|
2317
|
+
renderDiaries();
|
|
2318
|
+
});
|
|
2319
|
+
|
|
1776
2320
|
document.getElementById('generate-report-btn')?.addEventListener('click', () => {
|
|
1777
2321
|
if (!state.currentSandbox) return;
|
|
1778
2322
|
const mode = document.getElementById('report-template')?.value || 'management';
|
|
@@ -1800,16 +2344,26 @@ function initApp() {
|
|
|
1800
2344
|
document.getElementById('generate-insight-btn')?.addEventListener('click', async () => {
|
|
1801
2345
|
if (!state.currentSandbox) return;
|
|
1802
2346
|
const output = document.getElementById('sandbox-insight-output');
|
|
2347
|
+
const contentEl = document.getElementById('sandbox-insight-content');
|
|
2348
|
+
if (!(output instanceof HTMLElement) || !(contentEl instanceof HTMLElement)) return;
|
|
1803
2349
|
output.classList.remove('hidden');
|
|
1804
|
-
output.
|
|
2350
|
+
output.removeAttribute('hidden');
|
|
2351
|
+
contentEl.textContent = '生成中...';
|
|
1805
2352
|
try {
|
|
1806
2353
|
const result = await apiRequest(`${API_BASE}/chats/sandbox/${state.currentSandbox.id}/insight`);
|
|
1807
|
-
|
|
2354
|
+
contentEl.textContent = result.insight;
|
|
1808
2355
|
} catch (error) {
|
|
1809
|
-
|
|
2356
|
+
contentEl.textContent = `生成失败:${error.message}`;
|
|
1810
2357
|
}
|
|
1811
2358
|
});
|
|
1812
2359
|
|
|
2360
|
+
document.getElementById('close-sandbox-insight-btn')?.addEventListener('click', () => {
|
|
2361
|
+
const output = document.getElementById('sandbox-insight-output');
|
|
2362
|
+
if (!(output instanceof HTMLElement)) return;
|
|
2363
|
+
output.classList.add('hidden');
|
|
2364
|
+
output.setAttribute('hidden', 'hidden');
|
|
2365
|
+
});
|
|
2366
|
+
|
|
1813
2367
|
document.getElementById('changes-sandbox-filter')?.addEventListener('change', async (e) => {
|
|
1814
2368
|
state.changesSandboxFilter = e.target.value || '';
|
|
1815
2369
|
await loadChanges();
|
|
@@ -1866,30 +2420,38 @@ function initApp() {
|
|
|
1866
2420
|
|
|
1867
2421
|
async function handleRoute() {
|
|
1868
2422
|
const hash = window.location.hash.slice(1) || '/';
|
|
2423
|
+
const [pathHash, queryString = ''] = hash.split('?');
|
|
2424
|
+
const query = new URLSearchParams(queryString);
|
|
1869
2425
|
const fullscreenRoot = getSandboxFullscreenElement();
|
|
1870
|
-
if (!
|
|
2426
|
+
if (!pathHash.startsWith('/sandbox/') && fullscreenRoot && document.fullscreenElement === fullscreenRoot) {
|
|
1871
2427
|
await document.exitFullscreen();
|
|
1872
2428
|
}
|
|
1873
2429
|
|
|
1874
|
-
if (
|
|
1875
|
-
showPage('home');
|
|
1876
|
-
await loadChats();
|
|
1877
|
-
} else if (hash === '/sandboxes') {
|
|
2430
|
+
if (pathHash === '/') {
|
|
1878
2431
|
showPage('sandboxes');
|
|
1879
2432
|
await loadSandboxes();
|
|
1880
|
-
} else if (
|
|
1881
|
-
|
|
2433
|
+
} else if (pathHash === '/sandboxes') {
|
|
2434
|
+
showPage('sandboxes');
|
|
2435
|
+
await loadSandboxes();
|
|
2436
|
+
} else if (pathHash.startsWith('/sandbox/')) {
|
|
2437
|
+
const id = decodeURIComponent(pathHash.split('/')[2] || '');
|
|
1882
2438
|
showPage('sandbox-detail');
|
|
1883
2439
|
await loadSandbox(id);
|
|
1884
|
-
|
|
2440
|
+
const targetNodeId = String(query.get('node_id') || '').trim();
|
|
2441
|
+
const shouldOpenDrawer = query.get('open_drawer') === '1' || Boolean(targetNodeId);
|
|
2442
|
+
const preferredFilter = query.get('entity_filter') || 'all';
|
|
2443
|
+
if (shouldOpenDrawer && targetNodeId) {
|
|
2444
|
+
showNodeEntityDrawer(targetNodeId, preferredFilter);
|
|
2445
|
+
}
|
|
2446
|
+
} else if (pathHash === '/diaries') {
|
|
1885
2447
|
showPage('diaries');
|
|
1886
2448
|
await loadDiaries();
|
|
1887
2449
|
await loadSandboxes();
|
|
1888
|
-
} else if (
|
|
2450
|
+
} else if (pathHash === '/changes') {
|
|
1889
2451
|
showPage('changes');
|
|
1890
2452
|
await loadSandboxes();
|
|
1891
2453
|
await loadChanges();
|
|
1892
|
-
} else if (
|
|
2454
|
+
} else if (pathHash === '/settings') {
|
|
1893
2455
|
showPage('settings');
|
|
1894
2456
|
await loadSettings();
|
|
1895
2457
|
}
|