@qnote/q-ai-note 1.0.17 → 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,
@@ -804,6 +805,9 @@ function applyReadonlyMode() {
804
805
  closeQuickChatPopover();
805
806
  }
806
807
  applySandboxChatVisibility();
808
+ if (state.selectedNodeId) {
809
+ renderDrawerWorkItemQuickEdit(state.selectedNodeId);
810
+ }
807
811
  if (aserRuntimeView) {
808
812
  aserRuntimeView.setAccessContext({
809
813
  readonly: state.readonly,
@@ -1642,6 +1646,147 @@ function renderQuickDiaryTargetLabel() {
1642
1646
  el.textContent = `快速日记:${state.currentSandbox.name} / ${getWorkItemNameById(nodeId)}`;
1643
1647
  }
1644
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
+
1683
+ function renderDrawerWorkItemQuickEdit(nodeId) {
1684
+ const nameInput = document.getElementById('drawer-work-item-name');
1685
+ const assigneeInput = document.getElementById('drawer-work-item-assignee');
1686
+ const statusInput = document.getElementById('drawer-work-item-status');
1687
+ const priorityInput = document.getElementById('drawer-work-item-priority');
1688
+ const descriptionInput = document.getElementById('drawer-work-item-description');
1689
+ const saveBtn = document.getElementById('save-drawer-work-item-btn');
1690
+ const dirtyHint = document.getElementById('drawer-work-item-dirty-hint');
1691
+ if (!nameInput || !assigneeInput || !statusInput || !priorityInput || !descriptionInput || !saveBtn || !dirtyHint) return;
1692
+ const row = getNodeById(nodeId);
1693
+ renderDrawerWorkItemQuickRead(row);
1694
+ setDrawerWorkItemQuickEditMode(state.drawerWorkItemEditMode);
1695
+ const canEdit = !state.readonly && state.currentSandboxWritable;
1696
+ if (!row) {
1697
+ nameInput.value = '';
1698
+ assigneeInput.value = '';
1699
+ statusInput.value = 'pending';
1700
+ priorityInput.value = 'medium';
1701
+ descriptionInput.value = '';
1702
+ [nameInput, assigneeInput, statusInput, priorityInput, descriptionInput, saveBtn].forEach((el) => {
1703
+ el.disabled = true;
1704
+ });
1705
+ saveBtn.dataset.original = '';
1706
+ dirtyHint.textContent = '无修改';
1707
+ dirtyHint.classList.remove('dirty');
1708
+ return;
1709
+ }
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);
1723
+ [nameInput, assigneeInput, statusInput, priorityInput, descriptionInput, saveBtn].forEach((el) => {
1724
+ el.disabled = !canEdit;
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);
1788
+ }
1789
+
1645
1790
  async function saveDiaryEntry({ content, sandboxId = null, workItemId = null }) {
1646
1791
  const payload = {
1647
1792
  sandbox_id: sandboxId,
@@ -1763,9 +1908,35 @@ function closeNodeEntityDrawer() {
1763
1908
  state.nodeEntityFilter = 'all';
1764
1909
  state.editingNodeEntityId = null;
1765
1910
  state.nodeEntityFormExpanded = false;
1911
+ state.drawerWorkItemEditMode = false;
1766
1912
  renderWorkTree();
1767
1913
  }
1768
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
+
1769
1940
  function setNodeEntityFormExpanded(expanded) {
1770
1941
  state.nodeEntityFormExpanded = expanded;
1771
1942
  const form = document.getElementById('drawer-create-form');
@@ -2089,9 +2260,11 @@ function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
2089
2260
  const node = getNodeById(nodeId);
2090
2261
  if (!drawer || !title || !node) return;
2091
2262
  state.selectedNodeId = nodeId;
2263
+ state.drawerWorkItemEditMode = false;
2092
2264
  const filter = ['all', 'todo', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
2093
2265
  state.nodeEntityFilter = filter;
2094
2266
  title.textContent = node.name || nodeId;
2267
+ renderDrawerWorkItemQuickEdit(nodeId);
2095
2268
  renderNodeEntitySummary(nodeId);
2096
2269
  renderWorkTree();
2097
2270
  renderQuickDiaryTargetLabel();
@@ -3912,6 +4085,26 @@ async function initApp() {
3912
4085
  document.getElementById('entity-content-input')?.addEventListener('keydown', entitySubmitByShortcut);
3913
4086
 
3914
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
+ }
3915
4108
  if (event.key !== 'Escape') return;
3916
4109
  if (quickChatPopover) {
3917
4110
  event.preventDefault();
@@ -3919,8 +4112,7 @@ async function initApp() {
3919
4112
  closeQuickChatPopover();
3920
4113
  return;
3921
4114
  }
3922
- const drawer = document.getElementById('node-entity-drawer');
3923
- if (drawer && !drawer.classList.contains('hidden')) {
4115
+ if (isDrawerOpen) {
3924
4116
  event.preventDefault();
3925
4117
  event.stopPropagation();
3926
4118
  closeNodeEntityDrawer();
@@ -4004,6 +4196,67 @@ async function initApp() {
4004
4196
  setButtonState(btn, { disabled: false, text: originalText || '保存' });
4005
4197
  }
4006
4198
  });
4199
+
4200
+ document.getElementById('save-drawer-work-item-btn')?.addEventListener('click', async () => {
4201
+ if (state.readonly || !state.currentSandboxWritable) return;
4202
+ if (!state.currentSandbox || !state.selectedNodeId) return;
4203
+ const nodeId = String(state.selectedNodeId || '');
4204
+ const current = getNodeById(nodeId);
4205
+ if (!current) return;
4206
+ const name = String(document.getElementById('drawer-work-item-name')?.value || '').trim();
4207
+ if (!name) {
4208
+ alert('请输入任务名称');
4209
+ return;
4210
+ }
4211
+ const payload = {
4212
+ name,
4213
+ description: String(document.getElementById('drawer-work-item-description')?.value || '').trim(),
4214
+ assignee: String(document.getElementById('drawer-work-item-assignee')?.value || '').trim(),
4215
+ status: String(document.getElementById('drawer-work-item-status')?.value || 'pending'),
4216
+ priority: String(document.getElementById('drawer-work-item-priority')?.value || 'medium'),
4217
+ parent_id: current.parent_id || null,
4218
+ extra_data: {
4219
+ ...(current.extra_data || {}),
4220
+ },
4221
+ };
4222
+ const saveBtn = document.getElementById('save-drawer-work-item-btn');
4223
+ const originalText = saveBtn?.textContent || '保存工作项';
4224
+ if (saveBtn) setButtonState(saveBtn, { disabled: true, text: '保存中...' });
4225
+ try {
4226
+ await apiRequest(`${API_BASE}/items/${nodeId}`, {
4227
+ method: 'PUT',
4228
+ body: JSON.stringify(payload),
4229
+ });
4230
+ const activeFilter = state.nodeEntityFilter || 'all';
4231
+ await loadSandbox(state.currentSandbox.id);
4232
+ showNodeEntityDrawer(nodeId, activeFilter);
4233
+ } finally {
4234
+ if (saveBtn) setButtonState(saveBtn, { disabled: false, text: originalText });
4235
+ refreshDrawerWorkItemSaveButtonState();
4236
+ }
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
+ });
4007
4260
 
4008
4261
  document.getElementById('rollback-btn')?.addEventListener('click', async () => {
4009
4262
  if (state.readonly) return;
@@ -150,6 +150,39 @@
150
150
  <button class="btn btn-secondary btn-sm" id="close-node-drawer-btn">关闭</button>
151
151
  </div>
152
152
  </div>
153
+ <section class="drawer-section">
154
+ <div class="drawer-section-title">工作项快速编辑</div>
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>
160
+ </div>
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>
183
+ </div>
184
+ </div>
185
+ </section>
153
186
  <section class="drawer-section">
154
187
  <div class="drawer-section-title">节点概览</div>
155
188
  <div class="summary-strip compact" id="node-entity-summary"></div>
@@ -3670,6 +3670,113 @@ 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
+
3729
+ .drawer-work-item-quick-edit {
3730
+ display: grid;
3731
+ gap: 8px;
3732
+ padding: 10px;
3733
+ background: #f8f9fb;
3734
+ border: 1px solid var(--border);
3735
+ border-radius: 10px;
3736
+ }
3737
+
3738
+ .drawer-work-item-quick-edit.hidden {
3739
+ display: none;
3740
+ }
3741
+
3742
+ .drawer-work-item-quick-edit-grid {
3743
+ display: grid;
3744
+ grid-template-columns: 1fr 1fr;
3745
+ gap: 8px;
3746
+ }
3747
+
3748
+ .drawer-work-item-quick-edit input,
3749
+ .drawer-work-item-quick-edit select,
3750
+ .drawer-work-item-quick-edit textarea {
3751
+ width: 100%;
3752
+ border: 1px solid var(--border);
3753
+ border-radius: 8px;
3754
+ padding: 8px 10px;
3755
+ font-size: 13px;
3756
+ background: #fff;
3757
+ }
3758
+
3759
+ .drawer-work-item-quick-edit-actions {
3760
+ display: flex;
3761
+ align-items: center;
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;
3778
+ }
3779
+
3673
3780
  .drawer-create-form {
3674
3781
  display: grid;
3675
3782
  gap: 8px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.17",
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",