@qnote/q-ai-note 1.0.5 → 1.0.6

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
@@ -9,6 +9,7 @@ const state = {
9
9
  operations: [],
10
10
  chats: [],
11
11
  settings: {},
12
+ readonly: false,
12
13
  pendingAction: null,
13
14
  nodeEntities: [],
14
15
  nodeEntityStats: null,
@@ -16,7 +17,8 @@ const state = {
16
17
  nodeEntityFilter: 'all',
17
18
  editingNodeEntityId: null,
18
19
  nodeEntityFormExpanded: false,
19
- sandboxPresentationMode: false,
20
+ sandboxChatVisible: false,
21
+ sandboxChatVisibleBeforeFullscreen: false,
20
22
  sandboxFullscreenMode: false,
21
23
  workTreeViewMode: 'full',
22
24
  workItemElementPreviewMode: 'none',
@@ -38,6 +40,7 @@ let sandboxActionHandler = null;
38
40
  let sandboxEscLocked = false;
39
41
  let resizeRenderTimer = null;
40
42
  const WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY = 'q-ai-note.work-item.show-assignee';
43
+ const WORK_TREE_VIEW_MODE_STORAGE_KEY = 'q-ai-note.work-tree.view-mode';
41
44
 
42
45
  function loadWorkItemAssigneePreference() {
43
46
  try {
@@ -60,14 +63,78 @@ function persistWorkItemAssigneePreference() {
60
63
  }
61
64
  }
62
65
 
63
- function applySandboxLayoutMode() {
66
+ function loadWorkTreeViewModePreference() {
67
+ try {
68
+ const raw = String(window.localStorage.getItem(WORK_TREE_VIEW_MODE_STORAGE_KEY) || '').trim();
69
+ if (raw === 'full' || raw === 'report' || raw === 'dense') {
70
+ state.workTreeViewMode = raw;
71
+ }
72
+ } catch {
73
+ // Ignore storage failures in restricted environments.
74
+ }
75
+ }
76
+
77
+ function persistWorkTreeViewModePreference() {
78
+ try {
79
+ window.localStorage.setItem(WORK_TREE_VIEW_MODE_STORAGE_KEY, state.workTreeViewMode || 'full');
80
+ } catch {
81
+ // Ignore storage failures in restricted environments.
82
+ }
83
+ }
84
+
85
+ function applySandboxChatVisibility() {
64
86
  const layout = document.getElementById('sandbox-layout');
65
- const toggleBtn = document.getElementById('toggle-sandbox-present-btn');
87
+ const toggleBtn = document.getElementById('toggle-sandbox-chat-btn');
66
88
  if (!layout || !toggleBtn) return;
67
- const isPresentation = Boolean(state.sandboxPresentationMode);
68
- layout.classList.toggle('presentation-mode', isPresentation);
69
- toggleBtn.textContent = isPresentation ? '退出展示态' : '展示态布局';
70
- toggleBtn.setAttribute('aria-pressed', isPresentation ? 'true' : 'false');
89
+ const shouldShow = !state.readonly && Boolean(state.sandboxChatVisible);
90
+ layout.classList.toggle('show-chat', shouldShow);
91
+ toggleBtn.classList.toggle('hidden', state.readonly);
92
+ toggleBtn.innerHTML = shouldShow
93
+ ? '<span class="icon" aria-hidden="true">🤖</span><span>隐藏 AI 助手</span>'
94
+ : '<span class="icon" aria-hidden="true">🤖</span><span>显示 AI 助手</span>';
95
+ toggleBtn.setAttribute('aria-pressed', shouldShow ? 'true' : 'false');
96
+ }
97
+
98
+ async function loadRuntimeMode() {
99
+ try {
100
+ const runtime = await apiRequest(`${API_BASE}/runtime`);
101
+ state.readonly = Boolean(runtime?.readonly);
102
+ } catch {
103
+ state.readonly = false;
104
+ }
105
+ }
106
+
107
+ function setHiddenById(id, hidden = true) {
108
+ const el = document.getElementById(id);
109
+ if (!el) return;
110
+ el.classList.toggle('hidden', hidden);
111
+ }
112
+
113
+ function applyReadonlyMode() {
114
+ document.body.classList.toggle('readonly-mode', state.readonly);
115
+ const settingsNav = document.querySelector('[data-nav="settings"]');
116
+ if (settingsNav instanceof HTMLElement) {
117
+ settingsNav.classList.toggle('hidden', state.readonly);
118
+ }
119
+ setHiddenById('page-settings', state.readonly);
120
+ setHiddenById('add-sandbox-btn', state.readonly);
121
+ setHiddenById('add-item-btn', state.readonly);
122
+ setHiddenById('toggle-node-entity-form-btn', state.readonly);
123
+ setHiddenById('drawer-diary-save-btn', state.readonly);
124
+ setHiddenById('save-diary-btn', state.readonly);
125
+ const diaryForm = document.querySelector('#page-diaries .diary-form');
126
+ if (diaryForm instanceof HTMLElement) {
127
+ diaryForm.classList.toggle('hidden', state.readonly);
128
+ }
129
+ const drawerDiaryForm = document.querySelector('#node-entity-drawer .drawer-diary-quick-form');
130
+ if (drawerDiaryForm instanceof HTMLElement) {
131
+ drawerDiaryForm.classList.toggle('hidden', state.readonly);
132
+ }
133
+ if (state.readonly) {
134
+ state.sandboxChatVisible = false;
135
+ closeQuickChatPopover();
136
+ }
137
+ applySandboxChatVisibility();
71
138
  }
72
139
 
73
140
  function getSandboxLayoutElement() {
@@ -100,7 +167,14 @@ function applySandboxFullscreenState() {
100
167
  const layout = getSandboxLayoutElement();
101
168
  const fullscreenRoot = getSandboxFullscreenElement();
102
169
  const toggleBtn = document.getElementById('toggle-sandbox-fullscreen-btn');
170
+ const wasFullscreen = Boolean(state.sandboxFullscreenMode);
103
171
  const isFullscreen = Boolean(fullscreenRoot && document.fullscreenElement === fullscreenRoot);
172
+ if (!wasFullscreen && isFullscreen) {
173
+ state.sandboxChatVisibleBeforeFullscreen = Boolean(state.sandboxChatVisible);
174
+ state.sandboxChatVisible = false;
175
+ } else if (wasFullscreen && !isFullscreen) {
176
+ state.sandboxChatVisible = Boolean(state.sandboxChatVisibleBeforeFullscreen);
177
+ }
104
178
  state.sandboxFullscreenMode = isFullscreen;
105
179
  if (layout) {
106
180
  layout.classList.toggle('is-fullscreen', isFullscreen);
@@ -112,9 +186,22 @@ function applySandboxFullscreenState() {
112
186
  toggleBtn.textContent = isFullscreen ? '退出全屏' : '全屏';
113
187
  toggleBtn.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
114
188
  }
189
+ applySandboxChatVisibility();
190
+ applySandboxLayoutHeight();
115
191
  void syncSandboxEscLock(isFullscreen);
116
192
  }
117
193
 
194
+ function applySandboxLayoutHeight() {
195
+ const layout = getSandboxLayoutElement();
196
+ if (!(layout instanceof HTMLElement)) return;
197
+ const rect = layout.getBoundingClientRect();
198
+ const viewportHeight = Math.floor(window.visualViewport?.height || window.innerHeight || 0);
199
+ if (!Number.isFinite(viewportHeight) || viewportHeight <= 0) return;
200
+ const bottomGap = state.sandboxFullscreenMode ? 8 : 10;
201
+ const nextHeight = Math.max(320, viewportHeight - Math.floor(rect.top) - bottomGap);
202
+ layout.style.height = `${nextHeight}px`;
203
+ }
204
+
118
205
  async function toggleSandboxFullscreen() {
119
206
  const fullscreenRoot = getSandboxFullscreenElement();
120
207
  if (!fullscreenRoot) return;
@@ -230,7 +317,7 @@ function renderWorkTree() {
230
317
  }
231
318
 
232
319
  if (allItems.length === 0) {
233
- tree.innerHTML = '<div class="empty-state"><p>点击上方"添加"按钮创建第一个任务</p></div>';
320
+ tree.innerHTML = `<div class="empty-state"><p>${state.readonly ? '暂无任务' : '点击上方"添加"按钮创建第一个任务'}</p></div>`;
234
321
  return;
235
322
  }
236
323
 
@@ -246,7 +333,7 @@ function renderWorkTree() {
246
333
  }
247
334
  renderWorkTree();
248
335
  },
249
- onAddChild: (parentId) => {
336
+ onAddChild: state.readonly ? undefined : (parentId) => {
250
337
  document.getElementById('item-dialog-title').textContent = '添加子任务';
251
338
  document.getElementById('item-dialog').dataset.editId = '';
252
339
  document.getElementById('new-item-name').value = '';
@@ -257,14 +344,14 @@ function renderWorkTree() {
257
344
  document.getElementById('new-item-parent').value = parentId;
258
345
  document.getElementById('item-dialog').showModal();
259
346
  },
260
- onAddDiary: (nodeId) => {
347
+ onAddDiary: state.readonly ? undefined : (nodeId) => {
261
348
  showNodeEntityDrawer(nodeId, 'diary');
262
349
  const textarea = document.getElementById('drawer-diary-content');
263
350
  if (textarea instanceof HTMLTextAreaElement) {
264
351
  textarea.focus();
265
352
  }
266
353
  },
267
- onEdit: (id) => {
354
+ onEdit: state.readonly ? undefined : (id) => {
268
355
  editWorkItem(id);
269
356
  },
270
357
  onSelect: (id) => {
@@ -273,7 +360,7 @@ function renderWorkTree() {
273
360
  onSelectEntity: (nodeId, entityType) => {
274
361
  showNodeEntityDrawer(nodeId, entityType || 'all');
275
362
  },
276
- onMoveNode: async (dragNodeId, newParentId) => {
363
+ onMoveNode: state.readonly ? undefined : async (dragNodeId, newParentId) => {
277
364
  if (!state.currentSandbox) return;
278
365
  if (!dragNodeId || dragNodeId === newParentId) return;
279
366
  const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
@@ -301,7 +388,7 @@ function renderWorkTree() {
301
388
  });
302
389
  await loadSandbox(state.currentSandbox.id);
303
390
  },
304
- onReorderSiblings: async (dragNodeId, targetNodeId, position) => {
391
+ onReorderSiblings: state.readonly ? undefined : async (dragNodeId, targetNodeId, position) => {
305
392
  if (!state.currentSandbox) return;
306
393
  if (!dragNodeId || !targetNodeId || dragNodeId === targetNodeId) return;
307
394
  const byId = new Map((state.currentSandbox.items || []).map((item) => [item.id, item]));
@@ -334,7 +421,7 @@ function renderWorkTree() {
334
421
  });
335
422
  await loadSandbox(state.currentSandbox.id);
336
423
  },
337
- onReorderLanes: async (dragRootId, targetRootId) => {
424
+ onReorderLanes: state.readonly ? undefined : async (dragRootId, targetRootId) => {
338
425
  if (!state.currentSandbox) return;
339
426
  if (!dragRootId || !targetRootId || dragRootId === targetRootId) return;
340
427
  const roots = (state.currentSandbox.items || []).filter((item) => !item.parent_id);
@@ -359,19 +446,21 @@ function renderWorkTree() {
359
446
  });
360
447
  await loadSandbox(state.currentSandbox.id);
361
448
  },
362
- onDelete: async (id) => {
449
+ onDelete: state.readonly ? undefined : async (id) => {
363
450
  if (confirm('确定删除此任务?')) {
364
451
  await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
365
452
  await loadSandbox(state.currentSandbox.id);
366
453
  }
367
454
  },
368
- onQuickChat: (id, el) => {
455
+ onQuickChat: state.readonly ? undefined : (id, el) => {
369
456
  openQuickChatPopover(id, el);
370
457
  },
371
458
  renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
372
459
  showAssignee: state.workItemShowAssignee,
373
460
  elementPreviewMode: state.workItemElementPreviewMode,
374
461
  entityRowsByNodeId,
462
+ readonly: state.readonly,
463
+ selectedId: state.selectedNodeId || '',
375
464
  });
376
465
 
377
466
  populateParentSelect(allItems);
@@ -674,6 +763,7 @@ function closeNodeEntityDrawer() {
674
763
  state.nodeEntityFilter = 'all';
675
764
  state.editingNodeEntityId = null;
676
765
  state.nodeEntityFormExpanded = false;
766
+ renderWorkTree();
677
767
  }
678
768
 
679
769
  function setNodeEntityFormExpanded(expanded) {
@@ -755,13 +845,15 @@ function renderNodeEntityList(nodeId) {
755
845
  <span class="entity-type-pill">diary</span>
756
846
  <strong>${safeText('日志记录')}</strong>
757
847
  </div>
758
- <div class="entity-card-actions">
759
- <button class="btn btn-secondary btn-sm" data-diary-edit-id="${safeText(row.id)}">编辑</button>
760
- ${row.processed ? '' : `
761
- <button class="btn btn-secondary btn-sm" data-diary-confirm-id="${safeText(row.id)}">采纳</button>
762
- <button class="btn btn-secondary btn-sm" data-diary-ignore-id="${safeText(row.id)}">忽略</button>
763
- `}
764
- </div>
848
+ ${state.readonly ? '' : `
849
+ <div class="entity-card-actions">
850
+ <button class="btn btn-secondary btn-sm" data-diary-edit-id="${safeText(row.id)}">编辑</button>
851
+ ${row.processed ? '' : `
852
+ <button class="btn btn-secondary btn-sm" data-diary-confirm-id="${safeText(row.id)}">采纳</button>
853
+ <button class="btn btn-secondary btn-sm" data-diary-ignore-id="${safeText(row.id)}">忽略</button>
854
+ `}
855
+ </div>
856
+ `}
765
857
  </div>
766
858
  <div class="entity-meta">
767
859
  ${safeText(new Date(row.created_at).toLocaleString())}
@@ -778,10 +870,12 @@ function renderNodeEntityList(nodeId) {
778
870
  <span class="entity-type-pill">${safeText(row.entity_type)}</span>
779
871
  <strong>${safeText(row.title || '-')}</strong>
780
872
  </div>
781
- <div class="entity-card-actions">
782
- <button class="btn btn-secondary btn-sm" data-entity-edit-id="${safeText(row.id)}">编辑</button>
783
- <button class="btn btn-secondary btn-sm" data-entity-delete-id="${safeText(row.id)}">删除</button>
784
- </div>
873
+ ${state.readonly ? '' : `
874
+ <div class="entity-card-actions">
875
+ <button class="btn btn-secondary btn-sm" data-entity-edit-id="${safeText(row.id)}">编辑</button>
876
+ <button class="btn btn-secondary btn-sm" data-entity-delete-id="${safeText(row.id)}">删除</button>
877
+ </div>
878
+ `}
785
879
  </div>
786
880
  <div class="entity-meta">
787
881
  ${safeText(new Date(row.created_at).toLocaleString())}
@@ -920,6 +1014,7 @@ function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
920
1014
  state.nodeEntityFilter = filter;
921
1015
  title.textContent = node.name || nodeId;
922
1016
  renderNodeEntitySummary(nodeId);
1017
+ renderWorkTree();
923
1018
  renderQuickDiaryTargetLabel();
924
1019
  renderNodeEntityFilterTabs();
925
1020
  renderNodeEntityList(nodeId);
@@ -1031,6 +1126,7 @@ function composeQuickChatContent(nodeId, userQuestion) {
1031
1126
  }
1032
1127
 
1033
1128
  async function sendSandboxChatMessage(content, options = {}) {
1129
+ if (state.readonly) return;
1034
1130
  if (!content || !state.currentSandbox) return;
1035
1131
  const messages = document.getElementById('sandbox-chat-messages');
1036
1132
  const btn = document.getElementById('sandbox-send-btn');
@@ -1221,12 +1317,13 @@ function renderSandboxes() {
1221
1317
  onOpen: (id) => {
1222
1318
  window.location.hash = `/sandbox/${id}`;
1223
1319
  },
1224
- onDelete: async (id) => {
1320
+ onDelete: state.readonly ? undefined : async (id) => {
1225
1321
  if (confirm('确定删除此沙盘?')) {
1226
1322
  await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
1227
1323
  await loadSandboxes();
1228
1324
  }
1229
- }
1325
+ },
1326
+ readonly: state.readonly,
1230
1327
  });
1231
1328
  }
1232
1329
 
@@ -1247,12 +1344,19 @@ async function loadSandbox(id) {
1247
1344
  applyWorkTreeViewMode(state.workTreeViewMode || 'full');
1248
1345
  applyWorkItemAssigneeToggle();
1249
1346
  applyWorkItemElementPreviewMode();
1250
- applySandboxLayoutMode();
1347
+ applySandboxChatVisibility();
1251
1348
  applySandboxFullscreenState();
1252
1349
  renderQuickDiaryTargetLabel();
1253
1350
  renderSandboxOverview();
1254
1351
  renderWorkTree();
1255
- loadSandboxChats(id);
1352
+ applySandboxLayoutHeight();
1353
+ if (state.readonly) {
1354
+ state.chats = [];
1355
+ const messages = document.getElementById('sandbox-chat-messages');
1356
+ if (messages) messages.innerHTML = '';
1357
+ } else {
1358
+ loadSandboxChats(id);
1359
+ }
1256
1360
  }
1257
1361
 
1258
1362
  function renderSandboxOverview() {
@@ -1277,6 +1381,7 @@ function renderSandboxOverview() {
1277
1381
  }
1278
1382
 
1279
1383
  async function loadSandboxChats(sandboxId) {
1384
+ if (state.readonly) return;
1280
1385
  const messages = document.getElementById('sandbox-chat-messages');
1281
1386
  if (!messages) return;
1282
1387
 
@@ -1436,25 +1541,26 @@ function renderDiaries() {
1436
1541
  openDiaryWorkItemInSandbox(sandboxId, workItemId);
1437
1542
  },
1438
1543
  renderContent: (content) => renderMarkdownSnippet(content),
1439
- onConfirm: async (id) => {
1544
+ onConfirm: state.readonly ? undefined : async (id) => {
1440
1545
  await apiRequest(`${API_BASE}/diaries/${id}/process`, {
1441
1546
  method: 'PUT',
1442
1547
  body: JSON.stringify({ action: 'confirm' }),
1443
1548
  });
1444
1549
  await loadDiaries();
1445
1550
  },
1446
- onIgnore: async (id) => {
1551
+ onIgnore: state.readonly ? undefined : async (id) => {
1447
1552
  await apiRequest(`${API_BASE}/diaries/${id}/process`, {
1448
1553
  method: 'PUT',
1449
1554
  body: JSON.stringify({ action: 'ignore' }),
1450
1555
  });
1451
1556
  await loadDiaries();
1452
1557
  },
1453
- onEdit: async (id) => {
1558
+ onEdit: state.readonly ? undefined : async (id) => {
1454
1559
  const diary = state.diaries.find((row) => row.id === id);
1455
1560
  if (!diary) return;
1456
1561
  openDiaryEditDialog(diary);
1457
1562
  },
1563
+ readonly: state.readonly,
1458
1564
  });
1459
1565
  }
1460
1566
 
@@ -1812,10 +1918,12 @@ function editWorkItem(id) {
1812
1918
  document.getElementById('item-dialog').showModal();
1813
1919
  }
1814
1920
 
1815
- function initApp() {
1816
- const hash = window.location.hash;
1921
+ async function initApp() {
1922
+ await loadRuntimeMode();
1817
1923
  loadWorkItemAssigneePreference();
1924
+ loadWorkTreeViewModePreference();
1818
1925
  renderQuickDiaryTargetLabel();
1926
+ applyReadonlyMode();
1819
1927
 
1820
1928
  document.querySelectorAll('.nav-list a').forEach(link => {
1821
1929
  link.addEventListener('click', (e) => {
@@ -1825,6 +1933,7 @@ function initApp() {
1825
1933
  });
1826
1934
 
1827
1935
  document.getElementById('add-sandbox-btn')?.addEventListener('click', () => {
1936
+ if (state.readonly) return;
1828
1937
  document.getElementById('sandbox-dialog').showModal();
1829
1938
  });
1830
1939
 
@@ -1833,6 +1942,7 @@ function initApp() {
1833
1942
  });
1834
1943
 
1835
1944
  const createSandbox = async (e) => {
1945
+ if (state.readonly) return;
1836
1946
  e?.preventDefault?.();
1837
1947
  const name = document.getElementById('new-sandbox-name').value;
1838
1948
  const description = document.getElementById('new-sandbox-desc').value;
@@ -1856,6 +1966,7 @@ function initApp() {
1856
1966
  document.getElementById('confirm-sandbox-btn')?.addEventListener('click', createSandbox);
1857
1967
 
1858
1968
  document.getElementById('add-item-btn')?.addEventListener('click', () => {
1969
+ if (state.readonly) return;
1859
1970
  document.getElementById('item-dialog-title').textContent = '添加任务';
1860
1971
  document.getElementById('item-dialog').dataset.editId = '';
1861
1972
  document.getElementById('new-item-name').value = '';
@@ -1867,9 +1978,11 @@ function initApp() {
1867
1978
  document.getElementById('item-dialog').showModal();
1868
1979
  });
1869
1980
 
1870
- document.getElementById('toggle-sandbox-present-btn')?.addEventListener('click', () => {
1871
- state.sandboxPresentationMode = !state.sandboxPresentationMode;
1872
- applySandboxLayoutMode();
1981
+ document.getElementById('toggle-sandbox-chat-btn')?.addEventListener('click', () => {
1982
+ if (state.readonly) return;
1983
+ state.sandboxChatVisible = !state.sandboxChatVisible;
1984
+ applySandboxChatVisibility();
1985
+ applySandboxLayoutHeight();
1873
1986
  });
1874
1987
 
1875
1988
  document.getElementById('toggle-sandbox-fullscreen-btn')?.addEventListener('click', async () => {
@@ -1890,6 +2003,7 @@ function initApp() {
1890
2003
  });
1891
2004
 
1892
2005
  document.getElementById('toggle-node-entity-form-btn')?.addEventListener('click', () => {
2006
+ if (state.readonly) return;
1893
2007
  setNodeEntityFormExpanded(!state.nodeEntityFormExpanded);
1894
2008
  if (state.nodeEntityFormExpanded) {
1895
2009
  document.getElementById('entity-title-input')?.focus();
@@ -1918,6 +2032,7 @@ function initApp() {
1918
2032
  });
1919
2033
 
1920
2034
  document.getElementById('create-node-entity-btn')?.addEventListener('click', async () => {
2035
+ if (state.readonly) return;
1921
2036
  if (!state.currentSandbox || !state.selectedNodeId) return;
1922
2037
  const btn = document.getElementById('create-node-entity-btn');
1923
2038
  const title = document.getElementById('entity-title-input').value.trim();
@@ -2016,6 +2131,7 @@ function initApp() {
2016
2131
  });
2017
2132
 
2018
2133
  document.getElementById('confirm-item-btn')?.addEventListener('click', async () => {
2134
+ if (state.readonly) return;
2019
2135
  const dialog = document.getElementById('item-dialog');
2020
2136
  const editId = dialog.dataset.editId || null;
2021
2137
  const isNewItem = !editId;
@@ -2062,6 +2178,7 @@ function initApp() {
2062
2178
  });
2063
2179
 
2064
2180
  document.getElementById('rollback-btn')?.addEventListener('click', async () => {
2181
+ if (state.readonly) return;
2065
2182
  if (!state.currentSandbox?.items?.length) return;
2066
2183
  const lastItem = state.currentSandbox.items[state.currentSandbox.items.length - 1];
2067
2184
  await apiRequest(`${API_BASE}/items/${lastItem.id}/rollback`, { method: 'POST' });
@@ -2069,6 +2186,7 @@ function initApp() {
2069
2186
  });
2070
2187
 
2071
2188
  document.getElementById('sandbox-send-btn')?.addEventListener('click', async () => {
2189
+ if (state.readonly) return;
2072
2190
  const input = document.getElementById('sandbox-chat-input');
2073
2191
  const content = input.value.trim();
2074
2192
  if (!content || !state.currentSandbox) return;
@@ -2186,12 +2304,14 @@ function initApp() {
2186
2304
  sandboxActionHandler = handleAIAction;
2187
2305
 
2188
2306
  document.getElementById('sandbox-chat-input')?.addEventListener('keypress', (e) => {
2307
+ if (state.readonly) return;
2189
2308
  if (e.key === 'Enter') {
2190
2309
  document.getElementById('sandbox-send-btn').click();
2191
2310
  }
2192
2311
  });
2193
2312
 
2194
2313
  document.getElementById('save-diary-btn')?.addEventListener('click', async () => {
2314
+ if (state.readonly) return;
2195
2315
  const content = document.getElementById('diary-content').value.trim();
2196
2316
  if (!content) return;
2197
2317
  await saveDiaryEntry({ content, sandboxId: null, workItemId: null });
@@ -2200,6 +2320,7 @@ function initApp() {
2200
2320
  });
2201
2321
 
2202
2322
  document.getElementById('drawer-diary-save-btn')?.addEventListener('click', async () => {
2323
+ if (state.readonly) return;
2203
2324
  if (!state.currentSandbox || !state.selectedNodeId) return;
2204
2325
  const textarea = document.getElementById('drawer-diary-content');
2205
2326
  if (!(textarea instanceof HTMLTextAreaElement)) return;
@@ -2222,9 +2343,11 @@ function initApp() {
2222
2343
  closeDiaryEditDialog();
2223
2344
  });
2224
2345
  document.getElementById('confirm-edit-diary-btn')?.addEventListener('click', async () => {
2346
+ if (state.readonly) return;
2225
2347
  await submitDiaryEditDialog();
2226
2348
  });
2227
2349
  document.getElementById('diary-edit-content')?.addEventListener('keydown', async (event) => {
2350
+ if (state.readonly) return;
2228
2351
  const isSubmit = event.key === 'Enter' && (event.metaKey || event.ctrlKey);
2229
2352
  if (!isSubmit) return;
2230
2353
  event.preventDefault();
@@ -2241,6 +2364,7 @@ function initApp() {
2241
2364
  });
2242
2365
 
2243
2366
  document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
2367
+ if (state.readonly) return;
2244
2368
  const api_url = document.getElementById('setting-api-url').value;
2245
2369
  const api_key = document.getElementById('setting-api-key').value;
2246
2370
  const model = document.getElementById('setting-model').value;
@@ -2270,6 +2394,7 @@ function initApp() {
2270
2394
  });
2271
2395
 
2272
2396
  document.getElementById('add-setting-header-btn')?.addEventListener('click', () => {
2397
+ if (state.readonly) return;
2273
2398
  const list = document.getElementById('setting-headers-list');
2274
2399
  if (!(list instanceof HTMLElement)) return;
2275
2400
  list.appendChild(createSettingHeaderRow('', ''));
@@ -2288,6 +2413,7 @@ function initApp() {
2288
2413
  document.getElementById('work-tree-view-mode')?.addEventListener('change', (e) => {
2289
2414
  const nextMode = e.target.value || 'full';
2290
2415
  applyWorkTreeViewMode(nextMode);
2416
+ persistWorkTreeViewModePreference();
2291
2417
  renderWorkTree();
2292
2418
  });
2293
2419
 
@@ -2411,12 +2537,13 @@ function initApp() {
2411
2537
  clearTimeout(resizeRenderTimer);
2412
2538
  }
2413
2539
  resizeRenderTimer = setTimeout(() => {
2540
+ applySandboxLayoutHeight();
2414
2541
  renderWorkTree();
2415
2542
  }, 120);
2416
2543
  });
2417
2544
 
2418
2545
  window.addEventListener('hashchange', handleRoute);
2419
- handleRoute();
2546
+ await handleRoute();
2420
2547
 
2421
2548
  async function handleRoute() {
2422
2549
  const hash = window.location.hash.slice(1) || '/';
@@ -2452,6 +2579,15 @@ function initApp() {
2452
2579
  await loadSandboxes();
2453
2580
  await loadChanges();
2454
2581
  } else if (pathHash === '/settings') {
2582
+ if (state.readonly) {
2583
+ if (window.location.hash !== '#/sandboxes') {
2584
+ window.location.hash = '/sandboxes';
2585
+ return;
2586
+ }
2587
+ showPage('sandboxes');
2588
+ await loadSandboxes();
2589
+ return;
2590
+ }
2455
2591
  showPage('settings');
2456
2592
  await loadSettings();
2457
2593
  }
@@ -39,7 +39,10 @@
39
39
  <div class="action-strip">
40
40
  <button class="btn btn-secondary btn-sm" id="generate-insight-btn">智能总结</button>
41
41
  <button class="btn btn-secondary btn-sm" id="generate-report-btn">生成汇报</button>
42
- <button class="btn btn-secondary btn-sm" id="toggle-sandbox-present-btn" type="button">展示态布局</button>
42
+ <button class="btn btn-secondary btn-sm ai-toggle-btn" id="toggle-sandbox-chat-btn" type="button">
43
+ <span class="icon" aria-hidden="true">🤖</span>
44
+ <span>显示 AI 助手</span>
45
+ </button>
43
46
  <button class="btn btn-secondary btn-sm" id="toggle-sandbox-fullscreen-btn" type="button">全屏</button>
44
47
  </div>
45
48
  <div class="insight-box hidden" id="sandbox-insight-output" hidden>