@qnote/q-ai-note 1.0.18 → 1.0.19

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
@@ -32,6 +32,7 @@ const state = {
32
32
  nodeEntityFilter: 'all',
33
33
  editingNodeEntityId: null,
34
34
  nodeEntityFormExpanded: false,
35
+ drawerWorkItemEditMode: false,
35
36
  sandboxChatVisible: false,
36
37
  sandboxChatVisibleBeforeFullscreen: false,
37
38
  sandboxFullscreenMode: false,
@@ -1645,6 +1646,40 @@ function renderQuickDiaryTargetLabel() {
1645
1646
  el.textContent = `快速日记:${state.currentSandbox.name} / ${getWorkItemNameById(nodeId)}`;
1646
1647
  }
1647
1648
 
1649
+ function setDrawerWorkItemQuickEditMode(editing) {
1650
+ state.drawerWorkItemEditMode = Boolean(editing);
1651
+ const readPanel = document.getElementById('drawer-work-item-quick-read');
1652
+ const editPanel = document.getElementById('drawer-work-item-quick-edit');
1653
+ readPanel?.classList.toggle('hidden', state.drawerWorkItemEditMode);
1654
+ editPanel?.classList.toggle('hidden', !state.drawerWorkItemEditMode);
1655
+ }
1656
+
1657
+ function renderDrawerWorkItemQuickRead(row) {
1658
+ const titleEl = document.getElementById('drawer-work-item-read-title');
1659
+ const metaEl = document.getElementById('drawer-work-item-read-meta');
1660
+ const descriptionEl = document.getElementById('drawer-work-item-read-description');
1661
+ if (!(titleEl instanceof HTMLElement) || !(metaEl instanceof HTMLElement) || !(descriptionEl instanceof HTMLElement)) return;
1662
+ if (!row) {
1663
+ titleEl.textContent = '-';
1664
+ metaEl.innerHTML = '';
1665
+ descriptionEl.innerHTML = '';
1666
+ return;
1667
+ }
1668
+ titleEl.textContent = String(row.name || '-');
1669
+ const assignee = String(row.assignee || '').trim() || '-';
1670
+ const status = String(row.status || 'pending').trim() || 'pending';
1671
+ const priority = String(row.priority || 'medium').trim() || 'medium';
1672
+ metaEl.innerHTML = [
1673
+ `<span class="drawer-work-item-read-meta-item">负责人: ${safeText(assignee)}</span>`,
1674
+ `<span class="drawer-work-item-read-meta-item">状态: ${safeText(status)}</span>`,
1675
+ `<span class="drawer-work-item-read-meta-item">优先级: ${safeText(priority)}</span>`,
1676
+ ].join('');
1677
+ const description = String(row.description || '').trim();
1678
+ descriptionEl.innerHTML = description
1679
+ ? renderMarkdownSnippet(description)
1680
+ : '<div class="empty-state"><p>暂无描述(双击进入编辑)</p></div>';
1681
+ }
1682
+
1648
1683
  function renderDrawerWorkItemQuickEdit(nodeId) {
1649
1684
  const nameInput = document.getElementById('drawer-work-item-name');
1650
1685
  const assigneeInput = document.getElementById('drawer-work-item-assignee');
@@ -1652,8 +1687,11 @@ function renderDrawerWorkItemQuickEdit(nodeId) {
1652
1687
  const priorityInput = document.getElementById('drawer-work-item-priority');
1653
1688
  const descriptionInput = document.getElementById('drawer-work-item-description');
1654
1689
  const saveBtn = document.getElementById('save-drawer-work-item-btn');
1655
- if (!nameInput || !assigneeInput || !statusInput || !priorityInput || !descriptionInput || !saveBtn) return;
1690
+ const dirtyHint = document.getElementById('drawer-work-item-dirty-hint');
1691
+ if (!nameInput || !assigneeInput || !statusInput || !priorityInput || !descriptionInput || !saveBtn || !dirtyHint) return;
1656
1692
  const row = getNodeById(nodeId);
1693
+ renderDrawerWorkItemQuickRead(row);
1694
+ setDrawerWorkItemQuickEditMode(state.drawerWorkItemEditMode);
1657
1695
  const canEdit = !state.readonly && state.currentSandboxWritable;
1658
1696
  if (!row) {
1659
1697
  nameInput.value = '';
@@ -1664,16 +1702,89 @@ function renderDrawerWorkItemQuickEdit(nodeId) {
1664
1702
  [nameInput, assigneeInput, statusInput, priorityInput, descriptionInput, saveBtn].forEach((el) => {
1665
1703
  el.disabled = true;
1666
1704
  });
1705
+ saveBtn.dataset.original = '';
1706
+ dirtyHint.textContent = '无修改';
1707
+ dirtyHint.classList.remove('dirty');
1667
1708
  return;
1668
1709
  }
1669
- nameInput.value = String(row.name || '');
1670
- assigneeInput.value = String(row.assignee || '');
1671
- statusInput.value = String(row.status || 'pending');
1672
- priorityInput.value = String(row.priority || 'medium');
1673
- descriptionInput.value = String(row.description || '');
1710
+ const baseline = {
1711
+ name: String(row.name || '').trim(),
1712
+ assignee: String(row.assignee || '').trim(),
1713
+ status: String(row.status || 'pending').trim() || 'pending',
1714
+ priority: String(row.priority || 'medium').trim() || 'medium',
1715
+ description: String(row.description || '').trim(),
1716
+ };
1717
+ nameInput.value = baseline.name;
1718
+ assigneeInput.value = baseline.assignee;
1719
+ statusInput.value = baseline.status;
1720
+ priorityInput.value = baseline.priority;
1721
+ descriptionInput.value = baseline.description;
1722
+ saveBtn.dataset.original = JSON.stringify(baseline);
1674
1723
  [nameInput, assigneeInput, statusInput, priorityInput, descriptionInput, saveBtn].forEach((el) => {
1675
1724
  el.disabled = !canEdit;
1676
1725
  });
1726
+ if (canEdit) {
1727
+ saveBtn.disabled = true;
1728
+ dirtyHint.textContent = '无修改';
1729
+ dirtyHint.classList.remove('dirty');
1730
+ } else {
1731
+ dirtyHint.textContent = '只读模式';
1732
+ dirtyHint.classList.remove('dirty');
1733
+ setDrawerWorkItemQuickEditMode(false);
1734
+ }
1735
+ }
1736
+
1737
+ function refreshDrawerWorkItemSaveButtonState() {
1738
+ const nameInput = document.getElementById('drawer-work-item-name');
1739
+ const assigneeInput = document.getElementById('drawer-work-item-assignee');
1740
+ const statusInput = document.getElementById('drawer-work-item-status');
1741
+ const priorityInput = document.getElementById('drawer-work-item-priority');
1742
+ const descriptionInput = document.getElementById('drawer-work-item-description');
1743
+ const saveBtn = document.getElementById('save-drawer-work-item-btn');
1744
+ const dirtyHint = document.getElementById('drawer-work-item-dirty-hint');
1745
+ if (!nameInput || !assigneeInput || !statusInput || !priorityInput || !descriptionInput || !saveBtn || !dirtyHint) return;
1746
+ if (state.readonly || !state.currentSandboxWritable || !state.selectedNodeId) {
1747
+ saveBtn.disabled = true;
1748
+ dirtyHint.textContent = '只读模式';
1749
+ dirtyHint.classList.remove('dirty');
1750
+ return;
1751
+ }
1752
+ const originalRaw = String(saveBtn.dataset.original || '').trim();
1753
+ if (!originalRaw) {
1754
+ saveBtn.disabled = true;
1755
+ dirtyHint.textContent = '无修改';
1756
+ dirtyHint.classList.remove('dirty');
1757
+ return;
1758
+ }
1759
+ let original = null;
1760
+ try {
1761
+ original = JSON.parse(originalRaw);
1762
+ } catch {
1763
+ original = null;
1764
+ }
1765
+ if (!original || typeof original !== 'object') {
1766
+ saveBtn.disabled = true;
1767
+ dirtyHint.textContent = '无修改';
1768
+ dirtyHint.classList.remove('dirty');
1769
+ return;
1770
+ }
1771
+ const current = {
1772
+ name: String(nameInput.value || '').trim(),
1773
+ assignee: String(assigneeInput.value || '').trim(),
1774
+ status: String(statusInput.value || 'pending').trim() || 'pending',
1775
+ priority: String(priorityInput.value || 'medium').trim() || 'medium',
1776
+ description: String(descriptionInput.value || '').trim(),
1777
+ };
1778
+ if (!current.name) {
1779
+ saveBtn.disabled = true;
1780
+ dirtyHint.textContent = '名称不能为空';
1781
+ dirtyHint.classList.add('dirty');
1782
+ return;
1783
+ }
1784
+ const hasChanged = JSON.stringify(current) !== JSON.stringify(original);
1785
+ saveBtn.disabled = !hasChanged;
1786
+ dirtyHint.textContent = hasChanged ? '有未保存修改' : '无修改';
1787
+ dirtyHint.classList.toggle('dirty', hasChanged);
1677
1788
  }
1678
1789
 
1679
1790
  async function saveDiaryEntry({ content, sandboxId = null, workItemId = null }) {
@@ -1797,9 +1908,35 @@ function closeNodeEntityDrawer() {
1797
1908
  state.nodeEntityFilter = 'all';
1798
1909
  state.editingNodeEntityId = null;
1799
1910
  state.nodeEntityFormExpanded = false;
1911
+ state.drawerWorkItemEditMode = false;
1800
1912
  renderWorkTree();
1801
1913
  }
1802
1914
 
1915
+ function getVisibleTreeSelectNodeIds() {
1916
+ const tree = document.getElementById('work-tree');
1917
+ if (!(tree instanceof HTMLElement)) return [];
1918
+ const seen = new Set();
1919
+ const ids = [];
1920
+ tree.querySelectorAll('[data-select-id]').forEach((el) => {
1921
+ if (!(el instanceof HTMLElement)) return;
1922
+ if (el.offsetParent === null) return;
1923
+ const id = String(el.getAttribute('data-select-id') || '').trim();
1924
+ if (!id || seen.has(id)) return;
1925
+ seen.add(id);
1926
+ ids.push(id);
1927
+ });
1928
+ return ids;
1929
+ }
1930
+
1931
+ function isTypingElement(target) {
1932
+ if (!(target instanceof HTMLElement)) return false;
1933
+ if (target.isContentEditable) return true;
1934
+ if (target instanceof HTMLInputElement) return true;
1935
+ if (target instanceof HTMLTextAreaElement) return true;
1936
+ if (target instanceof HTMLSelectElement) return true;
1937
+ return false;
1938
+ }
1939
+
1803
1940
  function setNodeEntityFormExpanded(expanded) {
1804
1941
  state.nodeEntityFormExpanded = expanded;
1805
1942
  const form = document.getElementById('drawer-create-form');
@@ -2123,6 +2260,7 @@ function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
2123
2260
  const node = getNodeById(nodeId);
2124
2261
  if (!drawer || !title || !node) return;
2125
2262
  state.selectedNodeId = nodeId;
2263
+ state.drawerWorkItemEditMode = false;
2126
2264
  const filter = ['all', 'todo', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
2127
2265
  state.nodeEntityFilter = filter;
2128
2266
  title.textContent = node.name || nodeId;
@@ -3947,6 +4085,26 @@ async function initApp() {
3947
4085
  document.getElementById('entity-content-input')?.addEventListener('keydown', entitySubmitByShortcut);
3948
4086
 
3949
4087
  document.addEventListener('keydown', (event) => {
4088
+ const drawer = document.getElementById('node-entity-drawer');
4089
+ const isDrawerOpen = Boolean(drawer && !drawer.classList.contains('hidden'));
4090
+ const isArrowKey = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key);
4091
+ if (isDrawerOpen && isArrowKey && !event.metaKey && !event.ctrlKey && !event.altKey && !isTypingElement(event.target)) {
4092
+ const ids = getVisibleTreeSelectNodeIds();
4093
+ if (ids.length > 0) {
4094
+ const currentId = String(state.selectedNodeId || '');
4095
+ const currentIdx = ids.indexOf(currentId);
4096
+ const baseIdx = currentIdx >= 0 ? currentIdx : 0;
4097
+ const offset = (event.key === 'ArrowUp' || event.key === 'ArrowLeft') ? -1 : 1;
4098
+ const nextIdx = Math.max(0, Math.min(ids.length - 1, baseIdx + offset));
4099
+ const nextId = ids[nextIdx];
4100
+ if (nextId && nextId !== currentId) {
4101
+ event.preventDefault();
4102
+ event.stopPropagation();
4103
+ showNodeEntityDrawer(nextId, state.nodeEntityFilter || 'all');
4104
+ return;
4105
+ }
4106
+ }
4107
+ }
3950
4108
  if (event.key !== 'Escape') return;
3951
4109
  if (quickChatPopover) {
3952
4110
  event.preventDefault();
@@ -3954,8 +4112,7 @@ async function initApp() {
3954
4112
  closeQuickChatPopover();
3955
4113
  return;
3956
4114
  }
3957
- const drawer = document.getElementById('node-entity-drawer');
3958
- if (drawer && !drawer.classList.contains('hidden')) {
4115
+ if (isDrawerOpen) {
3959
4116
  event.preventDefault();
3960
4117
  event.stopPropagation();
3961
4118
  closeNodeEntityDrawer();
@@ -4075,8 +4232,31 @@ async function initApp() {
4075
4232
  showNodeEntityDrawer(nodeId, activeFilter);
4076
4233
  } finally {
4077
4234
  if (saveBtn) setButtonState(saveBtn, { disabled: false, text: originalText });
4235
+ refreshDrawerWorkItemSaveButtonState();
4078
4236
  }
4079
4237
  });
4238
+
4239
+ const bindDrawerQuickEditDirtyState = (eventName) => {
4240
+ ['drawer-work-item-name', 'drawer-work-item-assignee', 'drawer-work-item-status', 'drawer-work-item-priority', 'drawer-work-item-description']
4241
+ .forEach((id) => {
4242
+ document.getElementById(id)?.addEventListener(eventName, refreshDrawerWorkItemSaveButtonState);
4243
+ });
4244
+ };
4245
+ bindDrawerQuickEditDirtyState('input');
4246
+ bindDrawerQuickEditDirtyState('change');
4247
+
4248
+ document.getElementById('drawer-work-item-quick-read')?.addEventListener('dblclick', () => {
4249
+ if (state.readonly || !state.currentSandboxWritable || !state.selectedNodeId) return;
4250
+ setDrawerWorkItemQuickEditMode(true);
4251
+ const nameInput = document.getElementById('drawer-work-item-name');
4252
+ if (nameInput instanceof HTMLInputElement) nameInput.focus();
4253
+ });
4254
+
4255
+ document.getElementById('cancel-drawer-work-item-edit-btn')?.addEventListener('click', () => {
4256
+ if (!state.selectedNodeId) return;
4257
+ state.drawerWorkItemEditMode = false;
4258
+ renderDrawerWorkItemQuickEdit(state.selectedNodeId);
4259
+ });
4080
4260
 
4081
4261
  document.getElementById('rollback-btn')?.addEventListener('click', async () => {
4082
4262
  if (state.readonly) return;
@@ -152,25 +152,34 @@
152
152
  </div>
153
153
  <section class="drawer-section">
154
154
  <div class="drawer-section-title">工作项快速编辑</div>
155
- <div class="drawer-work-item-quick-edit">
156
- <div class="drawer-work-item-quick-edit-grid">
157
- <input type="text" id="drawer-work-item-name" placeholder="工作项名称">
158
- <input type="text" id="drawer-work-item-assignee" placeholder="负责人(可选)">
159
- <select id="drawer-work-item-status">
160
- <option value="pending">待处理</option>
161
- <option value="in_progress">进行中</option>
162
- <option value="done">已完成</option>
163
- <option value="archived">已归档</option>
164
- </select>
165
- <select id="drawer-work-item-priority">
166
- <option value="low">低优先级</option>
167
- <option value="medium">中优先级</option>
168
- <option value="high">高优先级</option>
169
- </select>
155
+ <div class="drawer-work-item-quick" id="drawer-work-item-quick">
156
+ <div class="drawer-work-item-quick-read" id="drawer-work-item-quick-read" title="双击进入编辑">
157
+ <div class="drawer-work-item-read-title" id="drawer-work-item-read-title">-</div>
158
+ <div class="drawer-work-item-read-meta" id="drawer-work-item-read-meta"></div>
159
+ <div class="drawer-work-item-read-description diary-content" id="drawer-work-item-read-description"></div>
170
160
  </div>
171
- <textarea id="drawer-work-item-description" rows="3" placeholder="描述(可选)"></textarea>
172
- <div class="drawer-work-item-quick-edit-actions">
173
- <button class="btn btn-primary btn-sm" id="save-drawer-work-item-btn" type="button">保存工作项</button>
161
+ <div class="drawer-work-item-quick-edit hidden" id="drawer-work-item-quick-edit">
162
+ <div class="drawer-work-item-quick-edit-grid">
163
+ <input type="text" id="drawer-work-item-name" placeholder="工作项名称">
164
+ <input type="text" id="drawer-work-item-assignee" placeholder="负责人(可选)">
165
+ <select id="drawer-work-item-status">
166
+ <option value="pending">待处理</option>
167
+ <option value="in_progress">进行中</option>
168
+ <option value="done">已完成</option>
169
+ <option value="archived">已归档</option>
170
+ </select>
171
+ <select id="drawer-work-item-priority">
172
+ <option value="low">低优先级</option>
173
+ <option value="medium">中优先级</option>
174
+ <option value="high">高优先级</option>
175
+ </select>
176
+ </div>
177
+ <textarea id="drawer-work-item-description" rows="3" placeholder="描述(可选,支持 Markdown)"></textarea>
178
+ <div class="drawer-work-item-quick-edit-actions">
179
+ <span class="drawer-work-item-dirty-hint" id="drawer-work-item-dirty-hint">无修改</span>
180
+ <button class="btn btn-secondary btn-sm" id="cancel-drawer-work-item-edit-btn" type="button">取消编辑</button>
181
+ <button class="btn btn-primary btn-sm" id="save-drawer-work-item-btn" type="button">保存工作项</button>
182
+ </div>
174
183
  </div>
175
184
  </div>
176
185
  </section>
@@ -3670,6 +3670,62 @@ dialog::backdrop {
3670
3670
  margin-bottom: 8px;
3671
3671
  }
3672
3672
 
3673
+ .drawer-work-item-quick {
3674
+ display: grid;
3675
+ gap: 8px;
3676
+ }
3677
+
3678
+ .drawer-work-item-quick-read {
3679
+ display: grid;
3680
+ gap: 8px;
3681
+ padding: 10px;
3682
+ background: #fff;
3683
+ border: 1px dashed var(--border);
3684
+ border-radius: 10px;
3685
+ cursor: default;
3686
+ }
3687
+
3688
+ .drawer-work-item-quick-read:hover {
3689
+ border-color: #c7dafc;
3690
+ background: #f8fbff;
3691
+ }
3692
+
3693
+ .drawer-work-item-read-title {
3694
+ font-size: 13px;
3695
+ font-weight: 600;
3696
+ color: var(--text-primary);
3697
+ }
3698
+
3699
+ .drawer-work-item-read-meta {
3700
+ display: flex;
3701
+ flex-wrap: wrap;
3702
+ gap: 6px;
3703
+ }
3704
+
3705
+ .drawer-work-item-read-meta-item {
3706
+ font-size: 12px;
3707
+ color: var(--text-secondary);
3708
+ background: #f3f4f6;
3709
+ border: 1px solid var(--border);
3710
+ border-radius: 999px;
3711
+ padding: 2px 8px;
3712
+ }
3713
+
3714
+ .drawer-work-item-read-description {
3715
+ font-size: 13px;
3716
+ color: var(--text-primary);
3717
+ }
3718
+
3719
+ .drawer-work-item-read-description .empty-state {
3720
+ padding: 6px 0;
3721
+ text-align: left;
3722
+ color: var(--text-secondary);
3723
+ }
3724
+
3725
+ .drawer-work-item-read-description .empty-state p {
3726
+ margin: 0;
3727
+ }
3728
+
3673
3729
  .drawer-work-item-quick-edit {
3674
3730
  display: grid;
3675
3731
  gap: 8px;
@@ -3679,6 +3735,10 @@ dialog::backdrop {
3679
3735
  border-radius: 10px;
3680
3736
  }
3681
3737
 
3738
+ .drawer-work-item-quick-edit.hidden {
3739
+ display: none;
3740
+ }
3741
+
3682
3742
  .drawer-work-item-quick-edit-grid {
3683
3743
  display: grid;
3684
3744
  grid-template-columns: 1fr 1fr;
@@ -3698,7 +3758,23 @@ dialog::backdrop {
3698
3758
 
3699
3759
  .drawer-work-item-quick-edit-actions {
3700
3760
  display: flex;
3761
+ align-items: center;
3701
3762
  justify-content: flex-end;
3763
+ gap: 8px;
3764
+ }
3765
+
3766
+ .drawer-work-item-dirty-hint {
3767
+ font-size: 12px;
3768
+ color: var(--text-secondary);
3769
+ }
3770
+
3771
+ .drawer-work-item-dirty-hint.dirty {
3772
+ color: #92400e;
3773
+ font-weight: 600;
3774
+ background: #fef3c7;
3775
+ border: 1px solid #f59e0b;
3776
+ border-radius: 999px;
3777
+ padding: 2px 8px;
3702
3778
  }
3703
3779
 
3704
3780
  .drawer-create-form {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.18",
3
+ "version": "1.0.19",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",