@qnote/q-ai-note 1.0.10 → 1.0.12

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/dist/web/app.js CHANGED
@@ -45,6 +45,7 @@ const state = {
45
45
  changesSandboxFilter: '',
46
46
  changesTypeFilter: 'all',
47
47
  changesQuickFilter: 'all',
48
+ workTreeExpandInitialized: false,
48
49
  };
49
50
 
50
51
  const expandedNodes = new Set();
@@ -331,6 +332,91 @@ function expandAllNodes() {
331
332
  items.forEach(item => expandedNodes.add(item.id));
332
333
  }
333
334
 
335
+ function buildWorkItemDepthMap(items) {
336
+ const byId = new Map(items.map((item) => [item.id, item]));
337
+ const memo = new Map();
338
+ const visiting = new Set();
339
+ const walk = (id) => {
340
+ if (memo.has(id)) return memo.get(id);
341
+ if (visiting.has(id)) return 0;
342
+ visiting.add(id);
343
+ const item = byId.get(id);
344
+ if (!item) {
345
+ visiting.delete(id);
346
+ return 0;
347
+ }
348
+ const parentId = item.parent_id;
349
+ const depth = parentId && byId.has(parentId) ? walk(parentId) + 1 : 0;
350
+ memo.set(id, depth);
351
+ visiting.delete(id);
352
+ return depth;
353
+ };
354
+ items.forEach((item) => walk(item.id));
355
+ return memo;
356
+ }
357
+
358
+ function getWorkTreeMaxDepth(items) {
359
+ const depthMap = buildWorkItemDepthMap(items || []);
360
+ let maxDepth = 0;
361
+ depthMap.forEach((depth) => {
362
+ if (Number.isFinite(depth)) maxDepth = Math.max(maxDepth, Number(depth));
363
+ });
364
+ return maxDepth;
365
+ }
366
+
367
+ function expandNodesToLevel(level, items) {
368
+ const safeLevel = Math.max(1, Number(level) || 1);
369
+ const depthMap = buildWorkItemDepthMap(items || []);
370
+ expandedNodes.clear();
371
+ (items || []).forEach((item) => {
372
+ const depth = Number(depthMap.get(item.id) || 0);
373
+ if (depth < safeLevel) expandedNodes.add(item.id);
374
+ });
375
+ }
376
+
377
+ function applyWorkTreeCollapseAction(action) {
378
+ if (!state.currentSandbox) return;
379
+ const items = state.currentSandbox.items || [];
380
+ if (!items.length) return;
381
+ if (action === 'expand-all') {
382
+ expandedNodes.clear();
383
+ expandAllNodes();
384
+ } else if (action === 'collapse-all') {
385
+ expandedNodes.clear();
386
+ } else if (String(action || '').startsWith('collapse-level:')) {
387
+ const level = Number(String(action).split(':')[1] || '1');
388
+ expandNodesToLevel(level, items);
389
+ }
390
+ state.workTreeExpandInitialized = true;
391
+ }
392
+
393
+ function updateWorkTreeCollapseMenu(items) {
394
+ const selector = document.getElementById('work-tree-collapse-action');
395
+ if (!(selector instanceof HTMLSelectElement)) return;
396
+ const denseMode = state.workTreeViewMode === 'dense';
397
+ const hasItems = (items || []).length > 0;
398
+ selector.classList.toggle('hidden', !denseMode);
399
+ selector.disabled = !denseMode || !hasItems;
400
+ if (!denseMode || !hasItems) {
401
+ selector.innerHTML = '<option value="">层级操作</option>';
402
+ selector.value = '';
403
+ return;
404
+ }
405
+ const maxDepth = getWorkTreeMaxDepth(items);
406
+ const maxLevel = Math.max(1, maxDepth + 1);
407
+ const levelOptions = Array.from({ length: maxLevel }, (_v, idx) => {
408
+ const level = idx + 1;
409
+ return `<option value="collapse-level:${level}">折叠到 L${level}</option>`;
410
+ }).join('');
411
+ selector.innerHTML = `
412
+ <option value="">层级操作</option>
413
+ <option value="expand-all">全部展开</option>
414
+ <option value="collapse-all">全部折叠</option>
415
+ ${levelOptions}
416
+ `;
417
+ selector.value = '';
418
+ }
419
+
334
420
  function getRootNodeIds(items) {
335
421
  return items.filter((item) => !item.parent_id).map((item) => item.id);
336
422
  }
@@ -348,6 +434,8 @@ function applyWorkTreeViewMode(mode) {
348
434
  } else {
349
435
  expandAllNodes();
350
436
  }
437
+ state.workTreeExpandInitialized = true;
438
+ updateWorkTreeCollapseMenu(items);
351
439
  }
352
440
 
353
441
  function applyWorkItemAssigneeToggle() {
@@ -374,13 +462,15 @@ function renderWorkTree() {
374
462
  const entitySummaryByNodeId = buildNodeEntitySummaryByNodeId();
375
463
  const entityRowsByNodeId = buildNodeEntityRowsByNodeId();
376
464
 
377
- if (expandedNodes.size === 0 && allItems.length > 0) {
465
+ if (!state.workTreeExpandInitialized && allItems.length > 0) {
378
466
  if (state.workTreeViewMode === 'report') {
379
467
  getRootNodeIds(allItems).forEach((id) => expandedNodes.add(id));
380
468
  } else {
381
469
  expandAllNodes();
382
470
  }
471
+ state.workTreeExpandInitialized = true;
383
472
  }
473
+ updateWorkTreeCollapseMenu(allItems);
384
474
 
385
475
  if (allItems.length === 0) {
386
476
  tree.innerHTML = `<div class="empty-state"><p>${treeReadonly ? '暂无任务' : '点击上方"添加"按钮创建第一个任务'}</p></div>`;
@@ -686,8 +776,49 @@ function populateParentSelect(items, preferredParentId = null) {
686
776
  select.value = hasExpectedValue ? expectedValue : '';
687
777
  }
688
778
 
689
- function renderMarkdownSnippet(text) {
690
- const source = String(text || '').replace(/\r\n/g, '\n');
779
+ function normalizeAssistantResponseText(raw) {
780
+ let text = String(raw || '').trim();
781
+ if (!text) return '';
782
+ // Recover nested response payload when parser fallback returns a JSON string.
783
+ for (let depth = 0; depth < 2; depth += 1) {
784
+ const maybeJson = text.trim();
785
+ if (!maybeJson.startsWith('{') || !maybeJson.endsWith('}')) break;
786
+ try {
787
+ const parsed = JSON.parse(maybeJson);
788
+ if (parsed && typeof parsed.response === 'string' && parsed.response.trim()) {
789
+ text = String(parsed.response).trim();
790
+ continue;
791
+ }
792
+ } catch {
793
+ // Ignore invalid json and keep original text.
794
+ }
795
+ break;
796
+ }
797
+ return text;
798
+ }
799
+
800
+ function unwrapMarkdownFence(rawText) {
801
+ const text = String(rawText || '').replace(/\r\n/g, '\n').trim();
802
+ const start = text.match(/^```(?:markdown|md)\s*\n?/i);
803
+ if (!start) return null;
804
+ let body = text.slice(start[0].length);
805
+ const endIdx = body.lastIndexOf('\n```');
806
+ if (endIdx >= 0) {
807
+ body = body.slice(0, endIdx);
808
+ } else {
809
+ body = body.replace(/```$/m, '');
810
+ }
811
+ return body.trim();
812
+ }
813
+
814
+ function renderMarkdownSnippet(text, options = {}) {
815
+ const enableMermaid = Boolean(options.enableMermaid);
816
+ const normalizedSource = normalizeAssistantResponseText(text);
817
+ const fencedMarkdown = unwrapMarkdownFence(normalizedSource);
818
+ if (fencedMarkdown !== null) {
819
+ return renderMarkdownSnippet(fencedMarkdown, options);
820
+ }
821
+ const source = String(normalizedSource || '').replace(/\r\n/g, '\n');
691
822
  const lines = source.split('\n');
692
823
  const blocks = [];
693
824
  let listItems = [];
@@ -711,12 +842,61 @@ function renderMarkdownSnippet(text) {
711
842
  return html;
712
843
  };
713
844
 
714
- for (const line of lines) {
845
+ const splitTableCells = (line) => {
846
+ let raw = String(line || '').trim();
847
+ if (raw.startsWith('|')) raw = raw.slice(1);
848
+ if (raw.endsWith('|')) raw = raw.slice(0, -1);
849
+ return raw.split('|').map((cell) => String(cell || '').trim());
850
+ };
851
+
852
+ for (let idx = 0; idx < lines.length; idx += 1) {
853
+ const line = lines[idx];
715
854
  const trimmed = line.trim();
716
855
  if (!trimmed) {
717
856
  flushList();
718
857
  continue;
719
858
  }
859
+ const fenceMatch = trimmed.match(/^```([a-zA-Z0-9_-]+)?\s*$/);
860
+ if (fenceMatch) {
861
+ flushList();
862
+ const fenceLang = String(fenceMatch[1] || '').toLowerCase();
863
+ const codeLines = [];
864
+ idx += 1;
865
+ while (idx < lines.length && !/^```/.test(String(lines[idx] || '').trim())) {
866
+ codeLines.push(lines[idx]);
867
+ idx += 1;
868
+ }
869
+ const code = codeLines.join('\n');
870
+ if (fenceLang === 'mermaid' && enableMermaid) {
871
+ blocks.push(`<div class="chat-mermaid" data-mermaid-code="${encodeURIComponent(code)}"></div>`);
872
+ } else {
873
+ blocks.push(`<pre><code>${safeText(code)}</code></pre>`);
874
+ }
875
+ continue;
876
+ }
877
+ if (trimmed.includes('|') && idx + 1 < lines.length) {
878
+ const nextTrimmed = String(lines[idx + 1] || '').trim();
879
+ const isTableDivider = /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(nextTrimmed);
880
+ if (isTableDivider) {
881
+ flushList();
882
+ const headerCells = splitTableCells(trimmed);
883
+ const bodyRows = [];
884
+ idx += 2;
885
+ while (idx < lines.length) {
886
+ const row = String(lines[idx] || '').trim();
887
+ if (!row || !row.includes('|')) {
888
+ idx -= 1;
889
+ break;
890
+ }
891
+ bodyRows.push(splitTableCells(row));
892
+ idx += 1;
893
+ }
894
+ const headerHtml = `<tr>${headerCells.map((cell) => `<th>${renderInline(cell)}</th>`).join('')}</tr>`;
895
+ const bodyHtml = bodyRows.map((cells) => `<tr>${cells.map((cell) => `<td>${renderInline(cell)}</td>`).join('')}</tr>`).join('');
896
+ blocks.push(`<table class="md-table"><thead>${headerHtml}</thead><tbody>${bodyHtml}</tbody></table>`);
897
+ continue;
898
+ }
899
+ }
720
900
  if (/^[-*]\s+/.test(trimmed)) {
721
901
  listItems.push(renderInline(trimmed.replace(/^[-*]\s+/, '')));
722
902
  continue;
@@ -734,6 +914,73 @@ function renderMarkdownSnippet(text) {
734
914
  return blocks.join('');
735
915
  }
736
916
 
917
+ let mermaidLoadPromise = null;
918
+
919
+ function ensureMermaidRuntime() {
920
+ if (window.mermaid && typeof window.mermaid.render === 'function') {
921
+ return Promise.resolve(window.mermaid);
922
+ }
923
+ if (mermaidLoadPromise) return mermaidLoadPromise;
924
+ mermaidLoadPromise = new Promise((resolve, reject) => {
925
+ const existing = document.querySelector('script[data-mermaid-runtime="1"]');
926
+ if (existing) {
927
+ existing.addEventListener('load', () => {
928
+ if (window.mermaid && typeof window.mermaid.initialize === 'function') {
929
+ window.mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });
930
+ }
931
+ resolve(window.mermaid);
932
+ }, { once: true });
933
+ existing.addEventListener('error', () => reject(new Error('Mermaid script load failed')), { once: true });
934
+ return;
935
+ }
936
+ const script = document.createElement('script');
937
+ script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
938
+ script.async = true;
939
+ script.setAttribute('data-mermaid-runtime', '1');
940
+ script.addEventListener('load', () => {
941
+ if (window.mermaid && typeof window.mermaid.initialize === 'function') {
942
+ window.mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });
943
+ }
944
+ resolve(window.mermaid);
945
+ }, { once: true });
946
+ script.addEventListener('error', () => reject(new Error('Mermaid script load failed')), { once: true });
947
+ document.head.appendChild(script);
948
+ });
949
+ return mermaidLoadPromise;
950
+ }
951
+
952
+ async function renderMermaidInContainer(container) {
953
+ if (!(container instanceof HTMLElement)) return;
954
+ const blocks = Array.from(container.querySelectorAll('.chat-mermaid[data-mermaid-code]:not([data-mermaid-rendered="1"])'));
955
+ if (!blocks.length) return;
956
+ let mermaid = null;
957
+ try {
958
+ mermaid = await ensureMermaidRuntime();
959
+ } catch {
960
+ mermaid = null;
961
+ }
962
+ for (let idx = 0; idx < blocks.length; idx += 1) {
963
+ const block = blocks[idx];
964
+ const code = decodeURIComponent(String(block.getAttribute('data-mermaid-code') || ''));
965
+ if (!code) {
966
+ block.setAttribute('data-mermaid-rendered', '1');
967
+ continue;
968
+ }
969
+ if (mermaid && typeof mermaid.render === 'function') {
970
+ try {
971
+ const id = `chat-mermaid-${Date.now()}-${idx}`;
972
+ const rendered = await mermaid.render(id, code);
973
+ block.innerHTML = rendered?.svg || `<pre><code>${safeText(code)}</code></pre>`;
974
+ } catch {
975
+ block.innerHTML = `<pre><code>${safeText(code)}</code></pre>`;
976
+ }
977
+ } else {
978
+ block.innerHTML = `<pre><code>${safeText(code)}</code></pre>`;
979
+ }
980
+ block.setAttribute('data-mermaid-rendered', '1');
981
+ }
982
+ }
983
+
737
984
  function getNodeById(nodeId) {
738
985
  return state.currentSandbox?.items?.find((item) => item.id === nodeId) || null;
739
986
  }
@@ -1453,6 +1700,7 @@ async function loadSandbox(id) {
1453
1700
  apiRequest(`${API_BASE}/sandboxes/${id}/entities/stats`),
1454
1701
  ]);
1455
1702
  state.currentSandbox = { ...sandbox, items, diaries };
1703
+ state.workTreeExpandInitialized = false;
1456
1704
  state.nodeEntities = entities;
1457
1705
  state.nodeEntityStats = entityStats;
1458
1706
 
@@ -1510,9 +1758,10 @@ async function loadSandboxChats(sandboxId) {
1510
1758
  chats.map((chat) => renderChatEntry(chat, {
1511
1759
  safeText,
1512
1760
  renderAIActionMessage,
1513
- renderContent: (content) => renderMarkdownSnippet(content),
1761
+ renderContent: (content) => renderMarkdownSnippet(content, { enableMermaid: true }),
1514
1762
  })),
1515
1763
  );
1764
+ await renderMermaidInContainer(messages);
1516
1765
 
1517
1766
  messages.scrollTop = messages.scrollHeight;
1518
1767
  }
@@ -1544,7 +1793,7 @@ function renderAIActionMessage(action) {
1544
1793
  };
1545
1794
 
1546
1795
  if (actionType === 'response' || actionType === 'clarify') {
1547
- return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || action.observation || '')}</div>`;
1796
+ return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || action.observation || '', { enableMermaid: true })}</div>`;
1548
1797
  }
1549
1798
  else if (actionType === 'confirm' && action.confirm_items) {
1550
1799
  // Skip confirm, go directly to done
@@ -1571,7 +1820,7 @@ function renderAIActionMessage(action) {
1571
1820
  </div>`;
1572
1821
  }
1573
1822
 
1574
- return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || '')}</div>`;
1823
+ return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || '', { enableMermaid: true })}</div>`;
1575
1824
  }
1576
1825
 
1577
1826
  window.undoOperation = async function(operationId, btn) {
@@ -2575,7 +2824,8 @@ async function initApp() {
2575
2824
  const actionType = action.action;
2576
2825
 
2577
2826
  if (actionType === 'response' || actionType === 'clarify') {
2578
- messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || action.observation || '')}</div>`);
2827
+ messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || action.observation || '', { enableMermaid: true })}</div>`);
2828
+ await renderMermaidInContainer(messages);
2579
2829
  messages.scrollTop = messages.scrollHeight;
2580
2830
 
2581
2831
  if (actionType === 'clarify') {
@@ -2663,11 +2913,13 @@ async function initApp() {
2663
2913
  }
2664
2914
  else if (actionType === 'done') {
2665
2915
  messages.insertAdjacentHTML('beforeend', renderAIActionMessage(action));
2916
+ await renderMermaidInContainer(messages);
2666
2917
  messages.scrollTop = messages.scrollHeight;
2667
2918
  state.pendingAction = null;
2668
2919
  }
2669
2920
  else if (actionType === 'stop') {
2670
- messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${renderMarkdownSnippet(action.observation || '已取消操作')}</div>`);
2921
+ messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${renderMarkdownSnippet(action.observation || '已取消操作', { enableMermaid: true })}</div>`);
2922
+ await renderMermaidInContainer(messages);
2671
2923
  messages.scrollTop = messages.scrollHeight;
2672
2924
  state.pendingAction = null;
2673
2925
  }
@@ -3012,6 +3264,13 @@ async function initApp() {
3012
3264
  renderWorkTree();
3013
3265
  });
3014
3266
 
3267
+ document.getElementById('work-tree-collapse-action')?.addEventListener('change', (e) => {
3268
+ const action = e.target.value || '';
3269
+ if (!action) return;
3270
+ applyWorkTreeCollapseAction(action);
3271
+ renderWorkTree();
3272
+ });
3273
+
3015
3274
  document.getElementById('work-item-element-preview-mode')?.addEventListener('change', (e) => {
3016
3275
  state.workItemElementPreviewMode = e.target.value || 'none';
3017
3276
  renderWorkTree();
@@ -63,6 +63,11 @@
63
63
  <option value="report">汇报折叠模式</option>
64
64
  <option value="dense">树模式</option>
65
65
  </select>
66
+ <select id="work-tree-collapse-action" title="树模式层级操作">
67
+ <option value="">层级操作</option>
68
+ <option value="expand-all">全部展开</option>
69
+ <option value="collapse-all">全部折叠</option>
70
+ </select>
66
71
  <select id="work-item-element-preview-mode">
67
72
  <option value="none">能力要素:不显示</option>
68
73
  <option value="issue">能力要素:Issue</option>
@@ -221,6 +221,10 @@ h2 {
221
221
  gap: 8px;
222
222
  }
223
223
 
224
+ #work-tree-collapse-action {
225
+ min-width: 132px;
226
+ }
227
+
224
228
  .inline-checkbox {
225
229
  display: inline-flex;
226
230
  align-items: center;
@@ -1200,6 +1204,65 @@ h2 {
1200
1204
  border-bottom-left-radius: 4px;
1201
1205
  }
1202
1206
 
1207
+ .chat-message.assistant h1,
1208
+ .chat-message.assistant h2,
1209
+ .chat-message.assistant h3,
1210
+ .chat-message.assistant h4 {
1211
+ margin: 2px 0 6px;
1212
+ line-height: 1.35;
1213
+ font-weight: 600;
1214
+ }
1215
+
1216
+ .chat-message.assistant h1 {
1217
+ font-size: 16px;
1218
+ }
1219
+
1220
+ .chat-message.assistant h2 {
1221
+ font-size: 15px;
1222
+ }
1223
+
1224
+ .chat-message.assistant h3,
1225
+ .chat-message.assistant h4 {
1226
+ font-size: 14px;
1227
+ }
1228
+
1229
+ .chat-message.assistant p,
1230
+ .chat-message.assistant ul,
1231
+ .chat-message.assistant ol,
1232
+ .chat-message.assistant pre {
1233
+ margin: 4px 0;
1234
+ }
1235
+
1236
+ .chat-message.assistant .md-table {
1237
+ border-collapse: collapse;
1238
+ width: 100%;
1239
+ margin: 6px 0;
1240
+ font-size: 12px;
1241
+ }
1242
+
1243
+ .chat-message.assistant .md-table th,
1244
+ .chat-message.assistant .md-table td {
1245
+ border: 1px solid #d5deed;
1246
+ padding: 4px 6px;
1247
+ text-align: left;
1248
+ vertical-align: top;
1249
+ }
1250
+
1251
+ .chat-message.assistant .md-table th {
1252
+ background: #eef4ff;
1253
+ font-weight: 600;
1254
+ }
1255
+
1256
+ .chat-message.assistant .chat-mermaid {
1257
+ width: 100%;
1258
+ overflow-x: auto;
1259
+ background: #fff;
1260
+ border: 1px solid #d5deed;
1261
+ border-radius: 8px;
1262
+ padding: 6px;
1263
+ margin: 6px 0;
1264
+ }
1265
+
1203
1266
  .chat-message.assistant.loading {
1204
1267
  color: var(--text-secondary);
1205
1268
  font-style: italic;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",