@qnote/q-ai-note 1.0.7 → 1.0.9

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
@@ -182,8 +182,6 @@ function applyReadonlyMode() {
182
182
  if (systemSettingsNav instanceof HTMLElement) {
183
183
  systemSettingsNav.classList.toggle('hidden', !state.canAccessSystemSettings);
184
184
  }
185
- setHiddenById('page-settings', !state.pageAccess.settings);
186
- setHiddenById('page-system-settings', !state.canAccessSystemSettings);
187
185
  setHiddenById('add-sandbox-btn', state.readonly || !state.fullAccess);
188
186
  setHiddenById('add-item-btn', state.readonly || !state.currentSandboxWritable);
189
187
  setHiddenById('toggle-node-entity-form-btn', state.readonly || !state.currentSandboxWritable);
@@ -489,16 +487,50 @@ function renderWorkTree() {
489
487
  });
490
488
  await loadSandbox(state.currentSandbox.id);
491
489
  },
492
- onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId) => {
490
+ onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId, position = 'before') => {
493
491
  if (!state.currentSandbox) return;
494
- if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
492
+ if (!dragRootId) return;
493
+ if (targetRootId && dragRootId === targetRootId) return;
495
494
  const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
496
495
  const ordered = [...roots].sort((a, b) => compareSiblingOrder(a, b, 'lane_order_key'));
496
+ const missingLaneKeyRoots = ordered.filter((item) => !getNodeOrderKey(item, 'lane_order_key'));
497
+ if (missingLaneKeyRoots.length > 0) {
498
+ let leftKey = '';
499
+ for (const root of ordered) {
500
+ const currentKey = getNodeOrderKey(root, 'lane_order_key');
501
+ if (currentKey) {
502
+ leftKey = currentKey;
503
+ continue;
504
+ }
505
+ const generatedKey = rankBetween(leftKey, '');
506
+ await apiRequest(`${API_BASE}/items/${root.id}`, {
507
+ method: 'PUT',
508
+ body: JSON.stringify({
509
+ extra_data: {
510
+ ...(root.extra_data || {}),
511
+ lane_order_key: generatedKey,
512
+ },
513
+ }),
514
+ });
515
+ root.extra_data = {
516
+ ...(root.extra_data || {}),
517
+ lane_order_key: generatedKey,
518
+ };
519
+ leftKey = generatedKey;
520
+ }
521
+ }
497
522
  const fromIdx = ordered.findIndex((item) => item.id === dragRootId);
498
- const toIdx = ordered.findIndex((item) => item.id === targetRootId);
499
- if (fromIdx < 0 || toIdx < 0) return;
523
+ if (fromIdx < 0) return;
500
524
  const [moved] = ordered.splice(fromIdx, 1);
501
- ordered.splice(toIdx, 0, moved);
525
+ let insertIndex = ordered.length;
526
+ if (targetRootId) {
527
+ const targetIdx = ordered.findIndex((item) => item.id === targetRootId);
528
+ if (targetIdx < 0) return;
529
+ insertIndex = position === 'after' ? targetIdx + 1 : targetIdx;
530
+ } else if (position !== 'end') {
531
+ insertIndex = ordered.length;
532
+ }
533
+ ordered.splice(insertIndex, 0, moved);
502
534
  const movedIndex = ordered.findIndex((item) => item.id === dragRootId);
503
535
  const left = ordered[movedIndex - 1] || null;
504
536
  const right = ordered[movedIndex + 1] || null;
@@ -625,7 +657,7 @@ function getNodeOrderKey(item, keyName = 'order_key') {
625
657
  function compareSiblingOrder(a, b, keyName = 'order_key') {
626
658
  const keyA = getNodeOrderKey(a, keyName);
627
659
  const keyB = getNodeOrderKey(b, keyName);
628
- if (keyA && keyB && keyA !== keyB) return keyA.localeCompare(keyB);
660
+ if (keyA && keyB && keyA !== keyB) return keyA < keyB ? -1 : 1;
629
661
  if (keyA && !keyB) return -1;
630
662
  if (!keyA && keyB) return 1;
631
663
  const numericA = Number(a?.extra_data?.lane_order);
@@ -635,7 +667,10 @@ function compareSiblingOrder(a, b, keyName = 'order_key') {
635
667
  if (keyName === 'lane_order_key' && validA && validB && numericA !== numericB) return numericA - numericB;
636
668
  if (keyName === 'lane_order_key' && validA && !validB) return -1;
637
669
  if (keyName === 'lane_order_key' && !validA && validB) return 1;
638
- return String(a?.created_at || '').localeCompare(String(b?.created_at || ''));
670
+ const createdA = String(a?.created_at || '');
671
+ const createdB = String(b?.created_at || '');
672
+ if (createdA === createdB) return 0;
673
+ return createdA < createdB ? -1 : 1;
639
674
  }
640
675
 
641
676
  function populateParentSelect(items, preferredParentId = null) {
@@ -1470,7 +1505,14 @@ async function loadSandboxChats(sandboxId) {
1470
1505
  const chats = await apiRequest(`${API_BASE}/chats/sandbox/${sandboxId}`);
1471
1506
  state.chats = chats;
1472
1507
 
1473
- mountHtmlList('sandbox-chat-messages', chats.map((chat) => renderChatEntry(chat, { safeText, renderAIActionMessage })));
1508
+ mountHtmlList(
1509
+ 'sandbox-chat-messages',
1510
+ chats.map((chat) => renderChatEntry(chat, {
1511
+ safeText,
1512
+ renderAIActionMessage,
1513
+ renderContent: (content) => renderMarkdownSnippet(content),
1514
+ })),
1515
+ );
1474
1516
 
1475
1517
  messages.scrollTop = messages.scrollHeight;
1476
1518
  }
@@ -1502,7 +1544,7 @@ function renderAIActionMessage(action) {
1502
1544
  };
1503
1545
 
1504
1546
  if (actionType === 'response' || actionType === 'clarify') {
1505
- return `<div class="chat-message assistant">${escapeHtml(action.response || action.observation || '')}</div>`;
1547
+ return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || action.observation || '')}</div>`;
1506
1548
  }
1507
1549
  else if (actionType === 'confirm' && action.confirm_items) {
1508
1550
  // Skip confirm, go directly to done
@@ -1529,7 +1571,7 @@ function renderAIActionMessage(action) {
1529
1571
  </div>`;
1530
1572
  }
1531
1573
 
1532
- return `<div class="chat-message assistant">${escapeHtml(action.response || '')}</div>`;
1574
+ return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || '')}</div>`;
1533
1575
  }
1534
1576
 
1535
1577
  window.undoOperation = async function(operationId, btn) {
@@ -2227,7 +2269,8 @@ function showPage(pageId) {
2227
2269
  }
2228
2270
 
2229
2271
  document.querySelectorAll('.nav-list a').forEach(a => a.classList.remove('active'));
2230
- const nav = document.querySelector(`[data-nav="${pageId}"]`);
2272
+ const navPageId = pageId === 'sandbox-detail' ? 'sandboxes' : pageId;
2273
+ const nav = document.querySelector(`[data-nav="${navPageId}"]`);
2231
2274
  if (nav) nav.classList.add('active');
2232
2275
  }
2233
2276
 
@@ -2532,7 +2575,7 @@ async function initApp() {
2532
2575
  const actionType = action.action;
2533
2576
 
2534
2577
  if (actionType === 'response' || actionType === 'clarify') {
2535
- messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${safeText(action.response || action.observation || '')}</div>`);
2578
+ messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || action.observation || '')}</div>`);
2536
2579
  messages.scrollTop = messages.scrollHeight;
2537
2580
 
2538
2581
  if (actionType === 'clarify') {
@@ -2624,7 +2667,7 @@ async function initApp() {
2624
2667
  state.pendingAction = null;
2625
2668
  }
2626
2669
  else if (actionType === 'stop') {
2627
- messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${safeText(action.observation || '已取消操作')}</div>`);
2670
+ messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${renderMarkdownSnippet(action.observation || '已取消操作')}</div>`);
2628
2671
  messages.scrollTop = messages.scrollHeight;
2629
2672
  state.pendingAction = null;
2630
2673
  }
@@ -14,7 +14,7 @@ export function tryParseAssistantAction(content) {
14
14
  }
15
15
 
16
16
  export function renderChatEntry(chat, helpers) {
17
- const { safeText, renderAIActionMessage } = helpers;
17
+ const { safeText, renderAIActionMessage, renderContent } = helpers;
18
18
 
19
19
  if (!('role' in chat)) {
20
20
  return `<div class="chat-message diary">[日记] ${safeText(chat.content)}</div>`;
@@ -25,6 +25,10 @@ export function renderChatEntry(chat, helpers) {
25
25
  if (parsedAction) {
26
26
  return renderAIActionMessage(parsedAction);
27
27
  }
28
+ const contentHtml = typeof renderContent === 'function'
29
+ ? renderContent(chat.content)
30
+ : safeText(chat.content);
31
+ return `<div class="chat-message assistant">${contentHtml}</div>`;
28
32
  }
29
33
 
30
34
  return `<div class="chat-message ${chat.role}">${safeText(chat.content)}</div>`;
@@ -134,6 +134,11 @@ body {
134
134
  animation: fadeIn 0.2s ease;
135
135
  }
136
136
 
137
+ #page-sandbox-detail {
138
+ max-width: none;
139
+ margin: 0;
140
+ }
141
+
137
142
  .page.hidden {
138
143
  display: none;
139
144
  }
@@ -642,6 +647,30 @@ h2 {
642
647
  flex-wrap: nowrap;
643
648
  }
644
649
 
650
+ .dense-lane-drop-slot {
651
+ width: 14px;
652
+ min-width: 14px;
653
+ align-self: stretch;
654
+ border-radius: 6px;
655
+ min-height: 180px;
656
+ transition: background-color 0.15s ease, box-shadow 0.15s ease;
657
+ }
658
+
659
+ .dense-lane-drop-slot.end {
660
+ width: 24px;
661
+ min-width: 24px;
662
+ }
663
+
664
+ .dense-lane-drop-slot.lane-slot-drag-over {
665
+ background: rgba(59, 130, 246, 0.28);
666
+ box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.45);
667
+ }
668
+
669
+ .dense-lane-board.is-lane-dragging-active .dense-lane-drop-slot {
670
+ background: rgba(148, 163, 184, 0.14);
671
+ box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.22);
672
+ }
673
+
645
674
  .dense-lane {
646
675
  min-width: var(--lane-width, 220px);
647
676
  width: var(--lane-width, 220px);
@@ -411,6 +411,7 @@ function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId,
411
411
  const laneWidth = getDenseLaneWidthPx(root, byParent, showAssignee);
412
412
  const laneNameMax = Math.max(120, laneWidth - 86);
413
413
  return `
414
+ <div class="dense-lane-drop-slot" data-lane-slot-root-id="${esc(root.id)}" data-lane-slot-position="before" title="拖拽到此处:插入到当前泳道之前"></div>
414
415
  <section class="dense-lane" data-root-id="${esc(root.id)}" style="--lane-width:${laneWidth}px;--lane-name-max:${laneNameMax}px;">
415
416
  ${readonly ? '' : `<div class="dense-lane-drag-handle" data-lane-drag-id="${esc(root.id)}" draggable="true" title="拖拽调整泳道顺序">⋮⋮</div>`}
416
417
  <div class="dense-lane-body">
@@ -419,6 +420,7 @@ function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId,
419
420
  </section>
420
421
  `;
421
422
  }).join('')}
423
+ <div class="dense-lane-drop-slot end" data-lane-slot-end="1" title="拖拽到此处:移动到最后"></div>
422
424
  </div>
423
425
  `;
424
426
  }
@@ -608,12 +610,16 @@ export function mountWorkTree(targetId, options) {
608
610
  });
609
611
 
610
612
  const laneEls = Array.from(container.querySelectorAll('.dense-lane[data-root-id]'));
613
+ const laneSlotEls = Array.from(container.querySelectorAll('.dense-lane-drop-slot[data-lane-slot-root-id]'));
614
+ const laneEndSlotEl = container.querySelector('.dense-lane-drop-slot[data-lane-slot-end="1"]');
615
+ const laneDropPositionByRootId = new Map();
611
616
  const laneHandleEls = Array.from(container.querySelectorAll('.dense-lane-drag-handle[data-lane-drag-id]'));
612
617
  laneHandleEls.forEach((handle) => {
613
618
  const rootId = handle.getAttribute('data-lane-drag-id') || '';
614
619
  handle.addEventListener('dragstart', (event) => {
615
620
  draggingLaneId = rootId;
616
621
  draggingNodeId = '';
622
+ board?.classList.add('is-lane-dragging-active');
617
623
  const lane = handle.closest('.dense-lane');
618
624
  lane?.classList.add('is-lane-dragging');
619
625
  event.dataTransfer?.setData('text/plain', rootId);
@@ -622,6 +628,14 @@ export function mountWorkTree(targetId, options) {
622
628
  handle.addEventListener('dragend', () => {
623
629
  const lane = handle.closest('.dense-lane');
624
630
  lane?.classList.remove('is-lane-dragging');
631
+ board?.classList.remove('is-lane-dragging-active');
632
+ laneEls.forEach((laneEl) => {
633
+ laneEl.classList.remove('lane-drag-over-target');
634
+ laneEl.removeAttribute('data-lane-drop-position');
635
+ });
636
+ laneSlotEls.forEach((slotEl) => slotEl.classList.remove('lane-slot-drag-over'));
637
+ laneEndSlotEl?.classList.remove('lane-slot-drag-over');
638
+ laneDropPositionByRootId.clear();
625
639
  draggingLaneId = '';
626
640
  });
627
641
  });
@@ -630,17 +644,101 @@ export function mountWorkTree(targetId, options) {
630
644
  el.addEventListener('dragover', (event) => {
631
645
  if (!draggingLaneId || draggingLaneId === rootId) return;
632
646
  event.preventDefault();
647
+ const rect = el.getBoundingClientRect();
648
+ const offsetX = event.clientX - rect.left;
649
+ const ratio = rect.width > 0 ? offsetX / rect.width : 0.5;
650
+ const laneDropPosition = ratio >= 0.5 ? 'after' : 'before';
651
+ laneDropPositionByRootId.set(rootId, laneDropPosition);
652
+ el.setAttribute('data-lane-drop-position', laneDropPosition);
633
653
  el.classList.add('lane-drag-over-target');
634
654
  });
635
655
  el.addEventListener('dragleave', () => {
636
656
  el.classList.remove('lane-drag-over-target');
657
+ el.removeAttribute('data-lane-drop-position');
658
+ laneDropPositionByRootId.delete(rootId);
637
659
  });
638
660
  el.addEventListener('drop', (event) => {
639
661
  event.preventDefault();
640
662
  el.classList.remove('lane-drag-over-target');
663
+ let laneDropPosition = laneDropPositionByRootId.get(rootId);
664
+ if (!laneDropPosition) {
665
+ const rect = el.getBoundingClientRect();
666
+ const offsetX = event.clientX - rect.left;
667
+ const ratio = rect.width > 0 ? offsetX / rect.width : 0.5;
668
+ laneDropPosition = ratio >= 0.5 ? 'after' : 'before';
669
+ }
670
+ el.removeAttribute('data-lane-drop-position');
671
+ laneDropPositionByRootId.delete(rootId);
641
672
  if (!draggingLaneId || draggingLaneId === rootId) return;
642
- onReorderLanes?.(draggingLaneId, rootId);
673
+ if (laneDropPosition === 'before') {
674
+ event.stopPropagation();
675
+ onReorderLanes?.(draggingLaneId, rootId, 'before');
676
+ board?.classList.remove('is-lane-dragging-active');
677
+ draggingLaneId = '';
678
+ } else {
679
+ const orderedRootIds = laneEls
680
+ .map((laneEl) => laneEl.getAttribute('data-root-id') || '')
681
+ .filter((id) => id && id !== draggingLaneId);
682
+ const isLastTarget = orderedRootIds.length > 0 && orderedRootIds[orderedRootIds.length - 1] === rootId;
683
+ if (!isLastTarget) {
684
+ event.stopPropagation();
685
+ onReorderLanes?.(draggingLaneId, rootId, 'after');
686
+ board?.classList.remove('is-lane-dragging-active');
687
+ draggingLaneId = '';
688
+ }
689
+ }
690
+ });
691
+ });
692
+ laneSlotEls.forEach((slotEl) => {
693
+ const targetRootId = slotEl.getAttribute('data-lane-slot-root-id') || '';
694
+ slotEl.addEventListener('dragover', (event) => {
695
+ if (!draggingLaneId || draggingLaneId === targetRootId) return;
696
+ event.preventDefault();
697
+ slotEl.classList.add('lane-slot-drag-over');
698
+ });
699
+ slotEl.addEventListener('dragleave', () => {
700
+ slotEl.classList.remove('lane-slot-drag-over');
701
+ });
702
+ slotEl.addEventListener('drop', (event) => {
703
+ event.preventDefault();
704
+ event.stopPropagation();
705
+ slotEl.classList.remove('lane-slot-drag-over');
706
+ if (!draggingLaneId || draggingLaneId === targetRootId) return;
707
+ onReorderLanes?.(draggingLaneId, targetRootId, 'before');
708
+ board?.classList.remove('is-lane-dragging-active');
643
709
  draggingLaneId = '';
644
710
  });
645
711
  });
712
+ laneEndSlotEl?.addEventListener('dragover', (event) => {
713
+ if (!draggingLaneId) return;
714
+ event.preventDefault();
715
+ laneEndSlotEl.classList.add('lane-slot-drag-over');
716
+ });
717
+ laneEndSlotEl?.addEventListener('dragleave', () => {
718
+ laneEndSlotEl.classList.remove('lane-slot-drag-over');
719
+ });
720
+ laneEndSlotEl?.addEventListener('drop', (event) => {
721
+ if (!draggingLaneId) return;
722
+ event.preventDefault();
723
+ event.stopPropagation();
724
+ laneEndSlotEl.classList.remove('lane-slot-drag-over');
725
+ onReorderLanes?.(draggingLaneId, '', 'end');
726
+ board?.classList.remove('is-lane-dragging-active');
727
+ draggingLaneId = '';
728
+ });
729
+ board?.addEventListener('dragover', (event) => {
730
+ if (!draggingLaneId) return;
731
+ event.preventDefault();
732
+ });
733
+ board?.addEventListener('drop', (event) => {
734
+ if (!draggingLaneId) return;
735
+ event.preventDefault();
736
+ laneEls.forEach((laneEl) => {
737
+ laneEl.classList.remove('lane-drag-over-target');
738
+ laneEl.removeAttribute('data-lane-drop-position');
739
+ });
740
+ laneDropPositionByRootId.clear();
741
+ onReorderLanes?.(draggingLaneId, '', 'end');
742
+ draggingLaneId = '';
743
+ });
646
744
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",