@qnote/q-ai-note 1.0.11 → 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
@@ -776,8 +776,49 @@ function populateParentSelect(items, preferredParentId = null) {
776
776
  select.value = hasExpectedValue ? expectedValue : '';
777
777
  }
778
778
 
779
- function renderMarkdownSnippet(text) {
780
- 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');
781
822
  const lines = source.split('\n');
782
823
  const blocks = [];
783
824
  let listItems = [];
@@ -801,12 +842,61 @@ function renderMarkdownSnippet(text) {
801
842
  return html;
802
843
  };
803
844
 
804
- 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];
805
854
  const trimmed = line.trim();
806
855
  if (!trimmed) {
807
856
  flushList();
808
857
  continue;
809
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
+ }
810
900
  if (/^[-*]\s+/.test(trimmed)) {
811
901
  listItems.push(renderInline(trimmed.replace(/^[-*]\s+/, '')));
812
902
  continue;
@@ -824,6 +914,73 @@ function renderMarkdownSnippet(text) {
824
914
  return blocks.join('');
825
915
  }
826
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
+
827
984
  function getNodeById(nodeId) {
828
985
  return state.currentSandbox?.items?.find((item) => item.id === nodeId) || null;
829
986
  }
@@ -1601,9 +1758,10 @@ async function loadSandboxChats(sandboxId) {
1601
1758
  chats.map((chat) => renderChatEntry(chat, {
1602
1759
  safeText,
1603
1760
  renderAIActionMessage,
1604
- renderContent: (content) => renderMarkdownSnippet(content),
1761
+ renderContent: (content) => renderMarkdownSnippet(content, { enableMermaid: true }),
1605
1762
  })),
1606
1763
  );
1764
+ await renderMermaidInContainer(messages);
1607
1765
 
1608
1766
  messages.scrollTop = messages.scrollHeight;
1609
1767
  }
@@ -1635,7 +1793,7 @@ function renderAIActionMessage(action) {
1635
1793
  };
1636
1794
 
1637
1795
  if (actionType === 'response' || actionType === 'clarify') {
1638
- 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>`;
1639
1797
  }
1640
1798
  else if (actionType === 'confirm' && action.confirm_items) {
1641
1799
  // Skip confirm, go directly to done
@@ -1662,7 +1820,7 @@ function renderAIActionMessage(action) {
1662
1820
  </div>`;
1663
1821
  }
1664
1822
 
1665
- return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || '')}</div>`;
1823
+ return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || '', { enableMermaid: true })}</div>`;
1666
1824
  }
1667
1825
 
1668
1826
  window.undoOperation = async function(operationId, btn) {
@@ -2666,7 +2824,8 @@ async function initApp() {
2666
2824
  const actionType = action.action;
2667
2825
 
2668
2826
  if (actionType === 'response' || actionType === 'clarify') {
2669
- 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);
2670
2829
  messages.scrollTop = messages.scrollHeight;
2671
2830
 
2672
2831
  if (actionType === 'clarify') {
@@ -2754,11 +2913,13 @@ async function initApp() {
2754
2913
  }
2755
2914
  else if (actionType === 'done') {
2756
2915
  messages.insertAdjacentHTML('beforeend', renderAIActionMessage(action));
2916
+ await renderMermaidInContainer(messages);
2757
2917
  messages.scrollTop = messages.scrollHeight;
2758
2918
  state.pendingAction = null;
2759
2919
  }
2760
2920
  else if (actionType === 'stop') {
2761
- 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);
2762
2923
  messages.scrollTop = messages.scrollHeight;
2763
2924
  state.pendingAction = null;
2764
2925
  }
@@ -1204,6 +1204,65 @@ h2 {
1204
1204
  border-bottom-left-radius: 4px;
1205
1205
  }
1206
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
+
1207
1266
  .chat-message.assistant.loading {
1208
1267
  color: var(--text-secondary);
1209
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.11",
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",