@qnote/q-ai-note 1.0.8 → 1.0.10

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
@@ -487,16 +487,50 @@ function renderWorkTree() {
487
487
  });
488
488
  await loadSandbox(state.currentSandbox.id);
489
489
  },
490
- onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId) => {
490
+ onReorderLanes: treeReadonly ? undefined : async (dragRootId, targetRootId, position = 'before') => {
491
491
  if (!state.currentSandbox) return;
492
- if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
492
+ if (!dragRootId) return;
493
+ if (targetRootId && dragRootId === targetRootId) return;
493
494
  const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
494
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
+ }
495
522
  const fromIdx = ordered.findIndex((item) => item.id === dragRootId);
496
- const toIdx = ordered.findIndex((item) => item.id === targetRootId);
497
- if (fromIdx < 0 || toIdx < 0) return;
523
+ if (fromIdx < 0) return;
498
524
  const [moved] = ordered.splice(fromIdx, 1);
499
- 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);
500
534
  const movedIndex = ordered.findIndex((item) => item.id === dragRootId);
501
535
  const left = ordered[movedIndex - 1] || null;
502
536
  const right = ordered[movedIndex + 1] || null;
@@ -623,7 +657,7 @@ function getNodeOrderKey(item, keyName = 'order_key') {
623
657
  function compareSiblingOrder(a, b, keyName = 'order_key') {
624
658
  const keyA = getNodeOrderKey(a, keyName);
625
659
  const keyB = getNodeOrderKey(b, keyName);
626
- if (keyA && keyB && keyA !== keyB) return keyA.localeCompare(keyB);
660
+ if (keyA && keyB && keyA !== keyB) return keyA < keyB ? -1 : 1;
627
661
  if (keyA && !keyB) return -1;
628
662
  if (!keyA && keyB) return 1;
629
663
  const numericA = Number(a?.extra_data?.lane_order);
@@ -633,7 +667,10 @@ function compareSiblingOrder(a, b, keyName = 'order_key') {
633
667
  if (keyName === 'lane_order_key' && validA && validB && numericA !== numericB) return numericA - numericB;
634
668
  if (keyName === 'lane_order_key' && validA && !validB) return -1;
635
669
  if (keyName === 'lane_order_key' && !validA && validB) return 1;
636
- 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;
637
674
  }
638
675
 
639
676
  function populateParentSelect(items, preferredParentId = null) {
@@ -1468,7 +1505,14 @@ async function loadSandboxChats(sandboxId) {
1468
1505
  const chats = await apiRequest(`${API_BASE}/chats/sandbox/${sandboxId}`);
1469
1506
  state.chats = chats;
1470
1507
 
1471
- 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
+ );
1472
1516
 
1473
1517
  messages.scrollTop = messages.scrollHeight;
1474
1518
  }
@@ -1500,7 +1544,7 @@ function renderAIActionMessage(action) {
1500
1544
  };
1501
1545
 
1502
1546
  if (actionType === 'response' || actionType === 'clarify') {
1503
- 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>`;
1504
1548
  }
1505
1549
  else if (actionType === 'confirm' && action.confirm_items) {
1506
1550
  // Skip confirm, go directly to done
@@ -1527,7 +1571,7 @@ function renderAIActionMessage(action) {
1527
1571
  </div>`;
1528
1572
  }
1529
1573
 
1530
- return `<div class="chat-message assistant">${escapeHtml(action.response || '')}</div>`;
1574
+ return `<div class="chat-message assistant">${renderMarkdownSnippet(action.response || '')}</div>`;
1531
1575
  }
1532
1576
 
1533
1577
  window.undoOperation = async function(operationId, btn) {
@@ -2531,7 +2575,7 @@ async function initApp() {
2531
2575
  const actionType = action.action;
2532
2576
 
2533
2577
  if (actionType === 'response' || actionType === 'clarify') {
2534
- 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>`);
2535
2579
  messages.scrollTop = messages.scrollHeight;
2536
2580
 
2537
2581
  if (actionType === 'clarify') {
@@ -2623,7 +2667,7 @@ async function initApp() {
2623
2667
  state.pendingAction = null;
2624
2668
  }
2625
2669
  else if (actionType === 'stop') {
2626
- 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>`);
2627
2671
  messages.scrollTop = messages.scrollHeight;
2628
2672
  state.pendingAction = null;
2629
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>`;
@@ -647,6 +647,30 @@ h2 {
647
647
  flex-wrap: nowrap;
648
648
  }
649
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
+
650
674
  .dense-lane {
651
675
  min-width: var(--lane-width, 220px);
652
676
  width: var(--lane-width, 220px);
@@ -746,21 +770,26 @@ h2 {
746
770
  }
747
771
 
748
772
  .lane-tree-title-line {
749
- display: inline-flex;
773
+ display: flex;
774
+ flex-wrap: wrap;
750
775
  align-items: center;
751
776
  gap: 4px;
752
777
  min-width: 0;
753
778
  }
754
779
 
755
- .lane-tree-summary-line {
756
- display: inline-flex;
757
- align-items: center;
758
- gap: 4px;
759
- min-height: 14px;
780
+ .lane-tree-title-line .dense-node-name {
781
+ flex: 1 1 auto;
782
+ min-width: 96px;
760
783
  }
761
784
 
762
- .lane-tree-node-main.stack-summary .dense-node-name {
763
- max-width: calc(var(--lane-name-max, 154px) + 28px);
785
+ .lane-tree-title-line .node-entity-mini-badges,
786
+ .lane-tree-title-line .node-meta {
787
+ flex: 0 0 auto;
788
+ max-width: 100%;
789
+ }
790
+
791
+ .lane-tree-title-line .node-meta {
792
+ white-space: nowrap;
764
793
  }
765
794
 
766
795
  .lane-tree-node {
@@ -318,7 +318,6 @@ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeI
318
318
  const hasChildren = children.length > 0;
319
319
  const isExpanded = expandedIdSet.has(node.id);
320
320
  const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
321
- const useStackSummary = depth >= 3;
322
321
  const summaryBadgeHtml = renderEntityBadges(nodeSummary);
323
322
  const summaryAssigneeHtml = showAssignee && node.assignee ? `<span class="node-meta">@${esc(node.assignee)}</span>` : '';
324
323
  const previewHtml = renderEntityPreviewBoxes(node.id, entityRowsByNodeId, elementPreviewMode);
@@ -342,18 +341,12 @@ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeI
342
341
  </button>
343
342
  ` : ''}
344
343
  <span class="node-status ${esc(node.status)}"></span>
345
- <div class="lane-tree-node-main ${useStackSummary ? 'stack-summary' : ''}">
344
+ <div class="lane-tree-node-main">
346
345
  <div class="lane-tree-title-line">
347
346
  <span class="dense-node-name">${esc(node.name)}</span>
348
- ${!useStackSummary ? summaryBadgeHtml : ''}
349
- ${!useStackSummary ? summaryAssigneeHtml : ''}
347
+ ${summaryBadgeHtml}
348
+ ${summaryAssigneeHtml}
350
349
  </div>
351
- ${useStackSummary && (summaryBadgeHtml || summaryAssigneeHtml) ? `
352
- <div class="lane-tree-summary-line">
353
- ${summaryBadgeHtml}
354
- ${summaryAssigneeHtml}
355
- </div>
356
- ` : ''}
357
350
  </div>
358
351
  ${readonly ? '' : `
359
352
  <div class="node-actions">
@@ -411,6 +404,7 @@ function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId,
411
404
  const laneWidth = getDenseLaneWidthPx(root, byParent, showAssignee);
412
405
  const laneNameMax = Math.max(120, laneWidth - 86);
413
406
  return `
407
+ <div class="dense-lane-drop-slot" data-lane-slot-root-id="${esc(root.id)}" data-lane-slot-position="before" title="拖拽到此处:插入到当前泳道之前"></div>
414
408
  <section class="dense-lane" data-root-id="${esc(root.id)}" style="--lane-width:${laneWidth}px;--lane-name-max:${laneNameMax}px;">
415
409
  ${readonly ? '' : `<div class="dense-lane-drag-handle" data-lane-drag-id="${esc(root.id)}" draggable="true" title="拖拽调整泳道顺序">⋮⋮</div>`}
416
410
  <div class="dense-lane-body">
@@ -419,6 +413,7 @@ function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId,
419
413
  </section>
420
414
  `;
421
415
  }).join('')}
416
+ <div class="dense-lane-drop-slot end" data-lane-slot-end="1" title="拖拽到此处:移动到最后"></div>
422
417
  </div>
423
418
  `;
424
419
  }
@@ -608,12 +603,16 @@ export function mountWorkTree(targetId, options) {
608
603
  });
609
604
 
610
605
  const laneEls = Array.from(container.querySelectorAll('.dense-lane[data-root-id]'));
606
+ const laneSlotEls = Array.from(container.querySelectorAll('.dense-lane-drop-slot[data-lane-slot-root-id]'));
607
+ const laneEndSlotEl = container.querySelector('.dense-lane-drop-slot[data-lane-slot-end="1"]');
608
+ const laneDropPositionByRootId = new Map();
611
609
  const laneHandleEls = Array.from(container.querySelectorAll('.dense-lane-drag-handle[data-lane-drag-id]'));
612
610
  laneHandleEls.forEach((handle) => {
613
611
  const rootId = handle.getAttribute('data-lane-drag-id') || '';
614
612
  handle.addEventListener('dragstart', (event) => {
615
613
  draggingLaneId = rootId;
616
614
  draggingNodeId = '';
615
+ board?.classList.add('is-lane-dragging-active');
617
616
  const lane = handle.closest('.dense-lane');
618
617
  lane?.classList.add('is-lane-dragging');
619
618
  event.dataTransfer?.setData('text/plain', rootId);
@@ -622,6 +621,14 @@ export function mountWorkTree(targetId, options) {
622
621
  handle.addEventListener('dragend', () => {
623
622
  const lane = handle.closest('.dense-lane');
624
623
  lane?.classList.remove('is-lane-dragging');
624
+ board?.classList.remove('is-lane-dragging-active');
625
+ laneEls.forEach((laneEl) => {
626
+ laneEl.classList.remove('lane-drag-over-target');
627
+ laneEl.removeAttribute('data-lane-drop-position');
628
+ });
629
+ laneSlotEls.forEach((slotEl) => slotEl.classList.remove('lane-slot-drag-over'));
630
+ laneEndSlotEl?.classList.remove('lane-slot-drag-over');
631
+ laneDropPositionByRootId.clear();
625
632
  draggingLaneId = '';
626
633
  });
627
634
  });
@@ -630,17 +637,101 @@ export function mountWorkTree(targetId, options) {
630
637
  el.addEventListener('dragover', (event) => {
631
638
  if (!draggingLaneId || draggingLaneId === rootId) return;
632
639
  event.preventDefault();
640
+ const rect = el.getBoundingClientRect();
641
+ const offsetX = event.clientX - rect.left;
642
+ const ratio = rect.width > 0 ? offsetX / rect.width : 0.5;
643
+ const laneDropPosition = ratio >= 0.5 ? 'after' : 'before';
644
+ laneDropPositionByRootId.set(rootId, laneDropPosition);
645
+ el.setAttribute('data-lane-drop-position', laneDropPosition);
633
646
  el.classList.add('lane-drag-over-target');
634
647
  });
635
648
  el.addEventListener('dragleave', () => {
636
649
  el.classList.remove('lane-drag-over-target');
650
+ el.removeAttribute('data-lane-drop-position');
651
+ laneDropPositionByRootId.delete(rootId);
637
652
  });
638
653
  el.addEventListener('drop', (event) => {
639
654
  event.preventDefault();
640
655
  el.classList.remove('lane-drag-over-target');
656
+ let laneDropPosition = laneDropPositionByRootId.get(rootId);
657
+ if (!laneDropPosition) {
658
+ const rect = el.getBoundingClientRect();
659
+ const offsetX = event.clientX - rect.left;
660
+ const ratio = rect.width > 0 ? offsetX / rect.width : 0.5;
661
+ laneDropPosition = ratio >= 0.5 ? 'after' : 'before';
662
+ }
663
+ el.removeAttribute('data-lane-drop-position');
664
+ laneDropPositionByRootId.delete(rootId);
641
665
  if (!draggingLaneId || draggingLaneId === rootId) return;
642
- onReorderLanes?.(draggingLaneId, rootId);
666
+ if (laneDropPosition === 'before') {
667
+ event.stopPropagation();
668
+ onReorderLanes?.(draggingLaneId, rootId, 'before');
669
+ board?.classList.remove('is-lane-dragging-active');
670
+ draggingLaneId = '';
671
+ } else {
672
+ const orderedRootIds = laneEls
673
+ .map((laneEl) => laneEl.getAttribute('data-root-id') || '')
674
+ .filter((id) => id && id !== draggingLaneId);
675
+ const isLastTarget = orderedRootIds.length > 0 && orderedRootIds[orderedRootIds.length - 1] === rootId;
676
+ if (!isLastTarget) {
677
+ event.stopPropagation();
678
+ onReorderLanes?.(draggingLaneId, rootId, 'after');
679
+ board?.classList.remove('is-lane-dragging-active');
680
+ draggingLaneId = '';
681
+ }
682
+ }
683
+ });
684
+ });
685
+ laneSlotEls.forEach((slotEl) => {
686
+ const targetRootId = slotEl.getAttribute('data-lane-slot-root-id') || '';
687
+ slotEl.addEventListener('dragover', (event) => {
688
+ if (!draggingLaneId || draggingLaneId === targetRootId) return;
689
+ event.preventDefault();
690
+ slotEl.classList.add('lane-slot-drag-over');
691
+ });
692
+ slotEl.addEventListener('dragleave', () => {
693
+ slotEl.classList.remove('lane-slot-drag-over');
694
+ });
695
+ slotEl.addEventListener('drop', (event) => {
696
+ event.preventDefault();
697
+ event.stopPropagation();
698
+ slotEl.classList.remove('lane-slot-drag-over');
699
+ if (!draggingLaneId || draggingLaneId === targetRootId) return;
700
+ onReorderLanes?.(draggingLaneId, targetRootId, 'before');
701
+ board?.classList.remove('is-lane-dragging-active');
643
702
  draggingLaneId = '';
644
703
  });
645
704
  });
705
+ laneEndSlotEl?.addEventListener('dragover', (event) => {
706
+ if (!draggingLaneId) return;
707
+ event.preventDefault();
708
+ laneEndSlotEl.classList.add('lane-slot-drag-over');
709
+ });
710
+ laneEndSlotEl?.addEventListener('dragleave', () => {
711
+ laneEndSlotEl.classList.remove('lane-slot-drag-over');
712
+ });
713
+ laneEndSlotEl?.addEventListener('drop', (event) => {
714
+ if (!draggingLaneId) return;
715
+ event.preventDefault();
716
+ event.stopPropagation();
717
+ laneEndSlotEl.classList.remove('lane-slot-drag-over');
718
+ onReorderLanes?.(draggingLaneId, '', 'end');
719
+ board?.classList.remove('is-lane-dragging-active');
720
+ draggingLaneId = '';
721
+ });
722
+ board?.addEventListener('dragover', (event) => {
723
+ if (!draggingLaneId) return;
724
+ event.preventDefault();
725
+ });
726
+ board?.addEventListener('drop', (event) => {
727
+ if (!draggingLaneId) return;
728
+ event.preventDefault();
729
+ laneEls.forEach((laneEl) => {
730
+ laneEl.classList.remove('lane-drag-over-target');
731
+ laneEl.removeAttribute('data-lane-drop-position');
732
+ });
733
+ laneDropPositionByRootId.clear();
734
+ onReorderLanes?.(draggingLaneId, '', 'end');
735
+ draggingLaneId = '';
736
+ });
646
737
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",