@qnote/q-ai-note 1.0.3 → 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/aiClient.d.ts.map +1 -1
- package/dist/server/aiClient.js +7 -0
- package/dist/server/aiClient.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/api/settings.d.ts.map +1 -1
- package/dist/server/api/settings.js +16 -1
- package/dist/server/api/settings.js.map +1 -1
- package/dist/server/config.d.ts +3 -2
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +6 -1
- package/dist/server/config.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 +714 -98
- package/dist/web/index.html +77 -22
- package/dist/web/styles.css +386 -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>
|
|
771
|
+
</div>
|
|
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>
|
|
381
785
|
</div>
|
|
382
|
-
<div class="entity-
|
|
383
|
-
|
|
384
|
-
<
|
|
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)}` : ''}
|
|
385
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');
|
|
@@ -1196,15 +1740,49 @@ async function loadSettings() {
|
|
|
1196
1740
|
apiKeyInput.value = '';
|
|
1197
1741
|
apiKeyInput.placeholder = state.settings.has_api_key ? '已配置,留空表示不修改' : 'sk-...';
|
|
1198
1742
|
document.getElementById('setting-model').value = state.settings.model || '';
|
|
1743
|
+
renderSettingHeadersRows(state.settings.headers || {});
|
|
1199
1744
|
}
|
|
1200
1745
|
|
|
1201
|
-
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1746
|
+
function createSettingHeaderRow(key = '', value = '') {
|
|
1747
|
+
const row = document.createElement('div');
|
|
1748
|
+
row.className = 'settings-kv-row';
|
|
1749
|
+
row.innerHTML = `
|
|
1750
|
+
<input type="text" class="setting-header-key" placeholder="Header 名称" value="${escapeHtml(String(key || ''))}">
|
|
1751
|
+
<input type="text" class="setting-header-value" placeholder="Header 值" value="${escapeHtml(String(value || ''))}">
|
|
1752
|
+
<button class="btn btn-secondary btn-sm remove-setting-header-btn" type="button">删除</button>
|
|
1753
|
+
`;
|
|
1754
|
+
row.querySelector('.remove-setting-header-btn')?.addEventListener('click', () => {
|
|
1755
|
+
row.remove();
|
|
1756
|
+
});
|
|
1757
|
+
return row;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function renderSettingHeadersRows(headers) {
|
|
1761
|
+
const list = document.getElementById('setting-headers-list');
|
|
1762
|
+
if (!(list instanceof HTMLElement)) return;
|
|
1763
|
+
list.innerHTML = '';
|
|
1764
|
+
const entries = Object.entries(headers || {}).filter(([k, v]) => String(k || '').trim() && String(v || '').trim());
|
|
1765
|
+
if (!entries.length) {
|
|
1766
|
+
list.appendChild(createSettingHeaderRow('', ''));
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
entries.forEach(([k, v]) => list.appendChild(createSettingHeaderRow(String(k), String(v))));
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function collectSettingHeadersFromUI() {
|
|
1773
|
+
const list = document.getElementById('setting-headers-list');
|
|
1774
|
+
if (!(list instanceof HTMLElement)) return {};
|
|
1775
|
+
const rows = Array.from(list.querySelectorAll('.settings-kv-row'));
|
|
1776
|
+
const headers = {};
|
|
1777
|
+
rows.forEach((row) => {
|
|
1778
|
+
const keyInput = row.querySelector('.setting-header-key');
|
|
1779
|
+
const valueInput = row.querySelector('.setting-header-value');
|
|
1780
|
+
const key = String(keyInput?.value || '').trim();
|
|
1781
|
+
const value = String(valueInput?.value || '').trim();
|
|
1782
|
+
if (!key || !value) return;
|
|
1783
|
+
headers[key] = value;
|
|
1784
|
+
});
|
|
1785
|
+
return headers;
|
|
1208
1786
|
}
|
|
1209
1787
|
|
|
1210
1788
|
function showPage(pageId) {
|
|
@@ -1237,6 +1815,7 @@ function editWorkItem(id) {
|
|
|
1237
1815
|
function initApp() {
|
|
1238
1816
|
const hash = window.location.hash;
|
|
1239
1817
|
loadWorkItemAssigneePreference();
|
|
1818
|
+
renderQuickDiaryTargetLabel();
|
|
1240
1819
|
|
|
1241
1820
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
1242
1821
|
link.addEventListener('click', (e) => {
|
|
@@ -1489,43 +2068,6 @@ function initApp() {
|
|
|
1489
2068
|
await loadSandbox(state.currentSandbox.id);
|
|
1490
2069
|
});
|
|
1491
2070
|
|
|
1492
|
-
document.getElementById('send-btn')?.addEventListener('click', async () => {
|
|
1493
|
-
const input = document.getElementById('chat-input');
|
|
1494
|
-
const content = input.value.trim();
|
|
1495
|
-
if (!content) return;
|
|
1496
|
-
|
|
1497
|
-
const messages = document.getElementById('chat-messages');
|
|
1498
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(content)}</div>`);
|
|
1499
|
-
const loadingMessage = appendLoadingMessage(messages);
|
|
1500
|
-
messages.scrollTop = messages.scrollHeight;
|
|
1501
|
-
input.value = '';
|
|
1502
|
-
|
|
1503
|
-
const btn = document.getElementById('send-btn');
|
|
1504
|
-
setButtonState(btn, { disabled: true });
|
|
1505
|
-
|
|
1506
|
-
try {
|
|
1507
|
-
await apiRequest(`${API_BASE}/chats`, {
|
|
1508
|
-
method: 'POST',
|
|
1509
|
-
body: JSON.stringify({ content }),
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
|
-
loadingMessage?.remove();
|
|
1513
|
-
await loadChats();
|
|
1514
|
-
} catch (error) {
|
|
1515
|
-
loadingMessage?.remove();
|
|
1516
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
1517
|
-
messages.scrollTop = messages.scrollHeight;
|
|
1518
|
-
} finally {
|
|
1519
|
-
setButtonState(btn, { disabled: false });
|
|
1520
|
-
}
|
|
1521
|
-
});
|
|
1522
|
-
|
|
1523
|
-
document.getElementById('chat-input')?.addEventListener('keypress', (e) => {
|
|
1524
|
-
if (e.key === 'Enter') {
|
|
1525
|
-
document.getElementById('send-btn').click();
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1528
|
-
|
|
1529
2071
|
document.getElementById('sandbox-send-btn')?.addEventListener('click', async () => {
|
|
1530
2072
|
const input = document.getElementById('sandbox-chat-input');
|
|
1531
2073
|
const content = input.value.trim();
|
|
@@ -1650,23 +2192,59 @@ function initApp() {
|
|
|
1650
2192
|
});
|
|
1651
2193
|
|
|
1652
2194
|
document.getElementById('save-diary-btn')?.addEventListener('click', async () => {
|
|
1653
|
-
const sandboxId = document.getElementById('diary-sandbox-select').value || null;
|
|
1654
2195
|
const content = document.getElementById('diary-content').value.trim();
|
|
1655
2196
|
if (!content) return;
|
|
1656
|
-
|
|
1657
|
-
await apiRequest(`${API_BASE}/diaries`, {
|
|
1658
|
-
method: 'POST',
|
|
1659
|
-
body: JSON.stringify({ sandbox_id: sandboxId, content }),
|
|
1660
|
-
});
|
|
1661
|
-
|
|
2197
|
+
await saveDiaryEntry({ content, sandboxId: null, workItemId: null });
|
|
1662
2198
|
document.getElementById('diary-content').value = '';
|
|
1663
2199
|
await loadDiaries();
|
|
1664
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
|
+
});
|
|
1665
2242
|
|
|
1666
2243
|
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
1667
2244
|
const api_url = document.getElementById('setting-api-url').value;
|
|
1668
2245
|
const api_key = document.getElementById('setting-api-key').value;
|
|
1669
2246
|
const model = document.getElementById('setting-model').value;
|
|
2247
|
+
const headers = collectSettingHeadersFromUI();
|
|
1670
2248
|
|
|
1671
2249
|
await apiRequest(`${API_BASE}/settings/api_url`, {
|
|
1672
2250
|
method: 'PUT',
|
|
@@ -1682,11 +2260,21 @@ function initApp() {
|
|
|
1682
2260
|
method: 'PUT',
|
|
1683
2261
|
body: JSON.stringify({ value: model }),
|
|
1684
2262
|
});
|
|
2263
|
+
await apiRequest(`${API_BASE}/settings/headers`, {
|
|
2264
|
+
method: 'PUT',
|
|
2265
|
+
body: JSON.stringify({ value: headers }),
|
|
2266
|
+
});
|
|
1685
2267
|
|
|
1686
2268
|
alert('设置已保存');
|
|
1687
2269
|
await loadSettings();
|
|
1688
2270
|
});
|
|
1689
2271
|
|
|
2272
|
+
document.getElementById('add-setting-header-btn')?.addEventListener('click', () => {
|
|
2273
|
+
const list = document.getElementById('setting-headers-list');
|
|
2274
|
+
if (!(list instanceof HTMLElement)) return;
|
|
2275
|
+
list.appendChild(createSettingHeaderRow('', ''));
|
|
2276
|
+
});
|
|
2277
|
+
|
|
1690
2278
|
document.getElementById('work-item-search')?.addEventListener('input', (e) => {
|
|
1691
2279
|
state.workItemSearch = e.target.value || '';
|
|
1692
2280
|
renderWorkTree();
|
|
@@ -1703,6 +2291,11 @@ function initApp() {
|
|
|
1703
2291
|
renderWorkTree();
|
|
1704
2292
|
});
|
|
1705
2293
|
|
|
2294
|
+
document.getElementById('work-item-element-preview-mode')?.addEventListener('change', (e) => {
|
|
2295
|
+
state.workItemElementPreviewMode = e.target.value || 'none';
|
|
2296
|
+
renderWorkTree();
|
|
2297
|
+
});
|
|
2298
|
+
|
|
1706
2299
|
document.getElementById('work-item-show-assignee-toggle')?.addEventListener('change', (e) => {
|
|
1707
2300
|
state.workItemShowAssignee = Boolean(e.target.checked);
|
|
1708
2301
|
persistWorkItemAssigneePreference();
|
|
@@ -1719,6 +2312,11 @@ function initApp() {
|
|
|
1719
2312
|
renderDiaries();
|
|
1720
2313
|
});
|
|
1721
2314
|
|
|
2315
|
+
document.getElementById('diary-sandbox-filter')?.addEventListener('change', (e) => {
|
|
2316
|
+
state.diarySandboxFilter = e.target.value || '';
|
|
2317
|
+
renderDiaries();
|
|
2318
|
+
});
|
|
2319
|
+
|
|
1722
2320
|
document.getElementById('generate-report-btn')?.addEventListener('click', () => {
|
|
1723
2321
|
if (!state.currentSandbox) return;
|
|
1724
2322
|
const mode = document.getElementById('report-template')?.value || 'management';
|
|
@@ -1746,16 +2344,26 @@ function initApp() {
|
|
|
1746
2344
|
document.getElementById('generate-insight-btn')?.addEventListener('click', async () => {
|
|
1747
2345
|
if (!state.currentSandbox) return;
|
|
1748
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;
|
|
1749
2349
|
output.classList.remove('hidden');
|
|
1750
|
-
output.
|
|
2350
|
+
output.removeAttribute('hidden');
|
|
2351
|
+
contentEl.textContent = '生成中...';
|
|
1751
2352
|
try {
|
|
1752
2353
|
const result = await apiRequest(`${API_BASE}/chats/sandbox/${state.currentSandbox.id}/insight`);
|
|
1753
|
-
|
|
2354
|
+
contentEl.textContent = result.insight;
|
|
1754
2355
|
} catch (error) {
|
|
1755
|
-
|
|
2356
|
+
contentEl.textContent = `生成失败:${error.message}`;
|
|
1756
2357
|
}
|
|
1757
2358
|
});
|
|
1758
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
|
+
|
|
1759
2367
|
document.getElementById('changes-sandbox-filter')?.addEventListener('change', async (e) => {
|
|
1760
2368
|
state.changesSandboxFilter = e.target.value || '';
|
|
1761
2369
|
await loadChanges();
|
|
@@ -1812,30 +2420,38 @@ function initApp() {
|
|
|
1812
2420
|
|
|
1813
2421
|
async function handleRoute() {
|
|
1814
2422
|
const hash = window.location.hash.slice(1) || '/';
|
|
2423
|
+
const [pathHash, queryString = ''] = hash.split('?');
|
|
2424
|
+
const query = new URLSearchParams(queryString);
|
|
1815
2425
|
const fullscreenRoot = getSandboxFullscreenElement();
|
|
1816
|
-
if (!
|
|
2426
|
+
if (!pathHash.startsWith('/sandbox/') && fullscreenRoot && document.fullscreenElement === fullscreenRoot) {
|
|
1817
2427
|
await document.exitFullscreen();
|
|
1818
2428
|
}
|
|
1819
2429
|
|
|
1820
|
-
if (
|
|
1821
|
-
showPage('home');
|
|
1822
|
-
await loadChats();
|
|
1823
|
-
} else if (hash === '/sandboxes') {
|
|
2430
|
+
if (pathHash === '/') {
|
|
1824
2431
|
showPage('sandboxes');
|
|
1825
2432
|
await loadSandboxes();
|
|
1826
|
-
} else if (
|
|
1827
|
-
|
|
2433
|
+
} else if (pathHash === '/sandboxes') {
|
|
2434
|
+
showPage('sandboxes');
|
|
2435
|
+
await loadSandboxes();
|
|
2436
|
+
} else if (pathHash.startsWith('/sandbox/')) {
|
|
2437
|
+
const id = decodeURIComponent(pathHash.split('/')[2] || '');
|
|
1828
2438
|
showPage('sandbox-detail');
|
|
1829
2439
|
await loadSandbox(id);
|
|
1830
|
-
|
|
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') {
|
|
1831
2447
|
showPage('diaries');
|
|
1832
2448
|
await loadDiaries();
|
|
1833
2449
|
await loadSandboxes();
|
|
1834
|
-
} else if (
|
|
2450
|
+
} else if (pathHash === '/changes') {
|
|
1835
2451
|
showPage('changes');
|
|
1836
2452
|
await loadSandboxes();
|
|
1837
2453
|
await loadChanges();
|
|
1838
|
-
} else if (
|
|
2454
|
+
} else if (pathHash === '/settings') {
|
|
1839
2455
|
showPage('settings');
|
|
1840
2456
|
await loadSettings();
|
|
1841
2457
|
}
|