@qnote/q-ai-note 1.0.2 → 1.0.4
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/server/aiClient.d.ts.map +1 -1
- package/dist/server/aiClient.js +7 -0
- package/dist/server/aiClient.js.map +1 -1
- package/dist/server/api/chat.d.ts.map +1 -1
- package/dist/server/api/chat.js +3 -0
- package/dist/server/api/chat.js.map +1 -1
- package/dist/server/api/nodeEntities.d.ts +3 -0
- package/dist/server/api/nodeEntities.d.ts.map +1 -0
- package/dist/server/api/nodeEntities.js +116 -0
- package/dist/server/api/nodeEntities.js.map +1 -0
- package/dist/server/api/settings.d.ts.map +1 -1
- package/dist/server/api/settings.js +16 -1
- package/dist/server/api/settings.js.map +1 -1
- package/dist/server/config.d.ts +3 -2
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +6 -1
- package/dist/server/config.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/nodeEntitiesStore.d.ts +78 -0
- package/dist/server/nodeEntitiesStore.d.ts.map +1 -0
- package/dist/server/nodeEntitiesStore.js +196 -0
- package/dist/server/nodeEntitiesStore.js.map +1 -0
- package/dist/server/react/agent.d.ts +3 -0
- package/dist/server/react/agent.d.ts.map +1 -1
- package/dist/server/react/agent.js +132 -2
- package/dist/server/react/agent.js.map +1 -1
- package/dist/server/react/prompts.d.ts +1 -0
- package/dist/server/react/prompts.d.ts.map +1 -1
- package/dist/server/react/prompts.js +25 -4
- package/dist/server/react/prompts.js.map +1 -1
- package/dist/server/react/tools.d.ts.map +1 -1
- package/dist/server/react/tools.js +105 -0
- package/dist/server/react/tools.js.map +1 -1
- package/dist/web/app.js +872 -57
- package/dist/web/index.html +78 -1
- package/dist/web/styles.css +783 -33
- package/dist/web/vueRenderers.js +228 -13
- package/package.json +1 -1
package/dist/web/app.js
CHANGED
|
@@ -10,6 +10,16 @@ const state = {
|
|
|
10
10
|
chats: [],
|
|
11
11
|
settings: {},
|
|
12
12
|
pendingAction: null,
|
|
13
|
+
nodeEntities: [],
|
|
14
|
+
nodeEntityStats: null,
|
|
15
|
+
selectedNodeId: null,
|
|
16
|
+
nodeEntityFilter: 'all',
|
|
17
|
+
editingNodeEntityId: null,
|
|
18
|
+
nodeEntityFormExpanded: false,
|
|
19
|
+
sandboxPresentationMode: false,
|
|
20
|
+
sandboxFullscreenMode: false,
|
|
21
|
+
workTreeViewMode: 'full',
|
|
22
|
+
workItemShowAssignee: false,
|
|
13
23
|
workItemSearch: '',
|
|
14
24
|
workItemStatusFilter: 'all',
|
|
15
25
|
diarySearch: '',
|
|
@@ -20,7 +30,102 @@ const state = {
|
|
|
20
30
|
};
|
|
21
31
|
|
|
22
32
|
const expandedNodes = new Set();
|
|
23
|
-
let
|
|
33
|
+
let quickChatPopover = null;
|
|
34
|
+
let sandboxActionHandler = null;
|
|
35
|
+
let sandboxEscLocked = false;
|
|
36
|
+
let resizeRenderTimer = null;
|
|
37
|
+
const WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY = 'q-ai-note.work-item.show-assignee';
|
|
38
|
+
|
|
39
|
+
function loadWorkItemAssigneePreference() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = window.localStorage.getItem(WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY);
|
|
42
|
+
if (raw === 'true') state.workItemShowAssignee = true;
|
|
43
|
+
if (raw === 'false') state.workItemShowAssignee = false;
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore storage failures in restricted environments.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function persistWorkItemAssigneePreference() {
|
|
50
|
+
try {
|
|
51
|
+
window.localStorage.setItem(
|
|
52
|
+
WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY,
|
|
53
|
+
state.workItemShowAssignee ? 'true' : 'false',
|
|
54
|
+
);
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore storage failures in restricted environments.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function applySandboxLayoutMode() {
|
|
61
|
+
const layout = document.getElementById('sandbox-layout');
|
|
62
|
+
const toggleBtn = document.getElementById('toggle-sandbox-present-btn');
|
|
63
|
+
if (!layout || !toggleBtn) return;
|
|
64
|
+
const isPresentation = Boolean(state.sandboxPresentationMode);
|
|
65
|
+
layout.classList.toggle('presentation-mode', isPresentation);
|
|
66
|
+
toggleBtn.textContent = isPresentation ? '退出展示态' : '展示态布局';
|
|
67
|
+
toggleBtn.setAttribute('aria-pressed', isPresentation ? 'true' : 'false');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSandboxLayoutElement() {
|
|
71
|
+
return document.getElementById('sandbox-layout');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getSandboxFullscreenElement() {
|
|
75
|
+
return document.getElementById('page-sandbox-detail');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function syncSandboxEscLock(enabled) {
|
|
79
|
+
const keyboardApi = navigator?.keyboard;
|
|
80
|
+
if (!keyboardApi) return;
|
|
81
|
+
try {
|
|
82
|
+
if (enabled && !sandboxEscLocked) {
|
|
83
|
+
await keyboardApi.lock(['Escape']);
|
|
84
|
+
sandboxEscLocked = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!enabled && sandboxEscLocked) {
|
|
88
|
+
keyboardApi.unlock();
|
|
89
|
+
sandboxEscLocked = false;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Ignore platforms without keyboard lock support.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function applySandboxFullscreenState() {
|
|
97
|
+
const layout = getSandboxLayoutElement();
|
|
98
|
+
const fullscreenRoot = getSandboxFullscreenElement();
|
|
99
|
+
const toggleBtn = document.getElementById('toggle-sandbox-fullscreen-btn');
|
|
100
|
+
const isFullscreen = Boolean(fullscreenRoot && document.fullscreenElement === fullscreenRoot);
|
|
101
|
+
state.sandboxFullscreenMode = isFullscreen;
|
|
102
|
+
if (layout) {
|
|
103
|
+
layout.classList.toggle('is-fullscreen', isFullscreen);
|
|
104
|
+
}
|
|
105
|
+
if (fullscreenRoot) {
|
|
106
|
+
fullscreenRoot.classList.toggle('is-sandbox-fullscreen', isFullscreen);
|
|
107
|
+
}
|
|
108
|
+
if (toggleBtn) {
|
|
109
|
+
toggleBtn.textContent = isFullscreen ? '退出全屏' : '全屏';
|
|
110
|
+
toggleBtn.setAttribute('aria-pressed', isFullscreen ? 'true' : 'false');
|
|
111
|
+
}
|
|
112
|
+
void syncSandboxEscLock(isFullscreen);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function toggleSandboxFullscreen() {
|
|
116
|
+
const fullscreenRoot = getSandboxFullscreenElement();
|
|
117
|
+
if (!fullscreenRoot) return;
|
|
118
|
+
try {
|
|
119
|
+
if (document.fullscreenElement === fullscreenRoot) {
|
|
120
|
+
await document.exitFullscreen();
|
|
121
|
+
} else {
|
|
122
|
+
await fullscreenRoot.requestFullscreen();
|
|
123
|
+
}
|
|
124
|
+
} catch (_error) {
|
|
125
|
+
// Keep UI fallback silent for browsers/environments that block fullscreen.
|
|
126
|
+
applySandboxFullscreenState();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
24
129
|
|
|
25
130
|
function applyWorkItemFilters(items) {
|
|
26
131
|
const query = state.workItemSearch.trim().toLowerCase();
|
|
@@ -69,15 +174,46 @@ function expandAllNodes() {
|
|
|
69
174
|
items.forEach(item => expandedNodes.add(item.id));
|
|
70
175
|
}
|
|
71
176
|
|
|
177
|
+
function getRootNodeIds(items) {
|
|
178
|
+
return items.filter((item) => !item.parent_id).map((item) => item.id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyWorkTreeViewMode(mode) {
|
|
182
|
+
state.workTreeViewMode = mode === 'report' ? 'report' : mode === 'dense' ? 'dense' : 'full';
|
|
183
|
+
const selector = document.getElementById('work-tree-view-mode');
|
|
184
|
+
if (selector instanceof HTMLSelectElement) {
|
|
185
|
+
selector.value = state.workTreeViewMode;
|
|
186
|
+
}
|
|
187
|
+
const items = state.currentSandbox?.items || [];
|
|
188
|
+
expandedNodes.clear();
|
|
189
|
+
if (state.workTreeViewMode === 'report') {
|
|
190
|
+
getRootNodeIds(items).forEach((id) => expandedNodes.add(id));
|
|
191
|
+
} else {
|
|
192
|
+
expandAllNodes();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function applyWorkItemAssigneeToggle() {
|
|
197
|
+
const toggle = document.getElementById('work-item-show-assignee-toggle');
|
|
198
|
+
if (toggle instanceof HTMLInputElement) {
|
|
199
|
+
toggle.checked = Boolean(state.workItemShowAssignee);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
72
203
|
function renderWorkTree() {
|
|
73
204
|
const tree = document.getElementById('work-tree');
|
|
74
205
|
if (!tree || !state.currentSandbox) return;
|
|
75
206
|
|
|
76
207
|
const allItems = state.currentSandbox.items || [];
|
|
77
208
|
const items = applyWorkItemFilters(allItems);
|
|
209
|
+
const entitySummaryByNodeId = buildNodeEntitySummaryByNodeId();
|
|
78
210
|
|
|
79
211
|
if (expandedNodes.size === 0 && allItems.length > 0) {
|
|
80
|
-
|
|
212
|
+
if (state.workTreeViewMode === 'report') {
|
|
213
|
+
getRootNodeIds(allItems).forEach((id) => expandedNodes.add(id));
|
|
214
|
+
} else {
|
|
215
|
+
expandAllNodes();
|
|
216
|
+
}
|
|
81
217
|
}
|
|
82
218
|
|
|
83
219
|
if (allItems.length === 0) {
|
|
@@ -88,6 +224,7 @@ function renderWorkTree() {
|
|
|
88
224
|
mountWorkTree('work-tree', {
|
|
89
225
|
items,
|
|
90
226
|
expandedIds: Array.from(expandedNodes),
|
|
227
|
+
entitySummaryByNodeId,
|
|
91
228
|
onToggleExpand: (id) => {
|
|
92
229
|
if (expandedNodes.has(id)) {
|
|
93
230
|
expandedNodes.delete(id);
|
|
@@ -110,17 +247,48 @@ function renderWorkTree() {
|
|
|
110
247
|
onEdit: (id) => {
|
|
111
248
|
editWorkItem(id);
|
|
112
249
|
},
|
|
250
|
+
onSelect: (id) => {
|
|
251
|
+
showNodeEntityDrawer(id);
|
|
252
|
+
},
|
|
113
253
|
onDelete: async (id) => {
|
|
114
254
|
if (confirm('确定删除此任务?')) {
|
|
115
255
|
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
116
256
|
await loadSandbox(state.currentSandbox.id);
|
|
117
257
|
}
|
|
118
|
-
}
|
|
258
|
+
},
|
|
259
|
+
onQuickChat: (id, el) => {
|
|
260
|
+
openQuickChatPopover(id, el);
|
|
261
|
+
},
|
|
262
|
+
renderMode: state.workTreeViewMode === 'dense' ? 'dense' : 'card',
|
|
263
|
+
showAssignee: state.workItemShowAssignee,
|
|
119
264
|
});
|
|
120
265
|
|
|
121
266
|
populateParentSelect(allItems);
|
|
122
267
|
}
|
|
123
268
|
|
|
269
|
+
function buildNodeEntitySummaryByNodeId() {
|
|
270
|
+
const summaryByNodeId = {};
|
|
271
|
+
for (const row of state.nodeEntities || []) {
|
|
272
|
+
const nodeId = String(row.work_item_id || '');
|
|
273
|
+
if (!nodeId) continue;
|
|
274
|
+
if (!summaryByNodeId[nodeId]) {
|
|
275
|
+
summaryByNodeId[nodeId] = { issue: 0, knowledge: 0, capability: 0, issue_open: 0, issue_closed: 0 };
|
|
276
|
+
}
|
|
277
|
+
if (row.entity_type === 'issue') {
|
|
278
|
+
summaryByNodeId[nodeId].issue += 1;
|
|
279
|
+
const status = String(row.status || '');
|
|
280
|
+
if (status === 'resolved' || status === 'closed') {
|
|
281
|
+
summaryByNodeId[nodeId].issue_closed += 1;
|
|
282
|
+
} else {
|
|
283
|
+
summaryByNodeId[nodeId].issue_open += 1;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (row.entity_type === 'knowledge') summaryByNodeId[nodeId].knowledge += 1;
|
|
287
|
+
if (row.entity_type === 'capability') summaryByNodeId[nodeId].capability += 1;
|
|
288
|
+
}
|
|
289
|
+
return summaryByNodeId;
|
|
290
|
+
}
|
|
291
|
+
|
|
124
292
|
function populateParentSelect(items) {
|
|
125
293
|
const select = document.getElementById('new-item-parent');
|
|
126
294
|
if (!select) return;
|
|
@@ -129,6 +297,422 @@ function populateParentSelect(items) {
|
|
|
129
297
|
items.map(i => `<option value="${i.id}">${safeText(i.name)}</option>`).join('');
|
|
130
298
|
}
|
|
131
299
|
|
|
300
|
+
function renderMarkdownSnippet(text) {
|
|
301
|
+
const escaped = safeText(String(text || ''));
|
|
302
|
+
return escaped.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_m, label, url) => {
|
|
303
|
+
const safeUrl = safeText(url);
|
|
304
|
+
const safeLabel = safeText(label);
|
|
305
|
+
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer">${safeLabel}</a>`;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getNodeById(nodeId) {
|
|
310
|
+
return state.currentSandbox?.items?.find((item) => item.id === nodeId) || null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getNodeEntitiesByNodeId(nodeId) {
|
|
314
|
+
return (state.nodeEntities || []).filter((row) => row.work_item_id === nodeId);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function closeNodeEntityDrawer() {
|
|
318
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
319
|
+
if (!drawer) return;
|
|
320
|
+
drawer.classList.add('hidden');
|
|
321
|
+
state.selectedNodeId = null;
|
|
322
|
+
state.nodeEntityFilter = 'all';
|
|
323
|
+
state.editingNodeEntityId = null;
|
|
324
|
+
state.nodeEntityFormExpanded = false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function setNodeEntityFormExpanded(expanded) {
|
|
328
|
+
state.nodeEntityFormExpanded = expanded;
|
|
329
|
+
const form = document.getElementById('drawer-create-form');
|
|
330
|
+
const toggleBtn = document.getElementById('toggle-node-entity-form-btn');
|
|
331
|
+
form?.classList.toggle('hidden', !expanded);
|
|
332
|
+
if (toggleBtn) {
|
|
333
|
+
toggleBtn.textContent = expanded ? '收起表单' : '+ 新建记录';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function deleteNodeEntityRecord(entityId) {
|
|
338
|
+
if (!state.currentSandbox) return;
|
|
339
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities/${entityId}`, {
|
|
340
|
+
method: 'DELETE',
|
|
341
|
+
});
|
|
342
|
+
await loadSandbox(state.currentSandbox.id);
|
|
343
|
+
if (state.selectedNodeId) {
|
|
344
|
+
showNodeEntityDrawer(state.selectedNodeId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function renderNodeEntitySummary(nodeId) {
|
|
349
|
+
const container = document.getElementById('node-entity-summary');
|
|
350
|
+
if (!container) return;
|
|
351
|
+
const rows = getNodeEntitiesByNodeId(nodeId);
|
|
352
|
+
const issues = rows.filter((row) => row.entity_type === 'issue');
|
|
353
|
+
const knowledges = rows.filter((row) => row.entity_type === 'knowledge');
|
|
354
|
+
const capabilities = rows.filter((row) => row.entity_type === 'capability');
|
|
355
|
+
const openIssues = issues.filter((row) => row.status === 'open' || row.status === 'in_progress' || row.status === 'blocked').length;
|
|
356
|
+
container.innerHTML = `
|
|
357
|
+
<div class="summary-card"><div class="label">Issue</div><div class="value">${issues.length}</div></div>
|
|
358
|
+
<div class="summary-card"><div class="label">Open Issue</div><div class="value">${openIssues}</div></div>
|
|
359
|
+
<div class="summary-card"><div class="label">Knowledge</div><div class="value">${knowledges.length}</div></div>
|
|
360
|
+
<div class="summary-card"><div class="label">Capability</div><div class="value">${capabilities.length}</div></div>
|
|
361
|
+
`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderNodeEntityList(nodeId) {
|
|
365
|
+
const container = document.getElementById('node-entity-list');
|
|
366
|
+
if (!container) return;
|
|
367
|
+
const allRows = getNodeEntitiesByNodeId(nodeId);
|
|
368
|
+
const rows = state.nodeEntityFilter === 'all'
|
|
369
|
+
? allRows
|
|
370
|
+
: allRows.filter((row) => row.entity_type === state.nodeEntityFilter);
|
|
371
|
+
if (!rows.length) {
|
|
372
|
+
container.innerHTML = '<div class="empty-state"><p>当前节点还没有 issue/knowledge/capability</p></div>';
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
container.innerHTML = rows.map((row) => `
|
|
376
|
+
<div class="entity-card">
|
|
377
|
+
<div class="entity-card-header">
|
|
378
|
+
<div>
|
|
379
|
+
<span class="entity-type-pill">${safeText(row.entity_type)}</span>
|
|
380
|
+
<strong>${safeText(row.title || '-')}</strong>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="entity-card-actions">
|
|
383
|
+
<button class="btn btn-secondary btn-sm" data-entity-edit-id="${safeText(row.id)}">编辑</button>
|
|
384
|
+
<button class="btn btn-secondary btn-sm" data-entity-delete-id="${safeText(row.id)}">删除</button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="entity-meta">
|
|
388
|
+
${row.status ? `<span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}${row.priority ? `· ${safeText(row.priority)}` : ''} ${row.assignee ? `· @${safeText(row.assignee)}` : ''}
|
|
389
|
+
</div>
|
|
390
|
+
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
391
|
+
</div>
|
|
392
|
+
`).join('');
|
|
393
|
+
|
|
394
|
+
container.querySelectorAll('[data-entity-delete-id]').forEach((el) => {
|
|
395
|
+
el.addEventListener('click', async (e) => {
|
|
396
|
+
e.preventDefault();
|
|
397
|
+
const id = el.getAttribute('data-entity-delete-id');
|
|
398
|
+
if (!id) return;
|
|
399
|
+
if (!confirm('确定删除这条记录?')) return;
|
|
400
|
+
await deleteNodeEntityRecord(id);
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
container.querySelectorAll('[data-entity-edit-id]').forEach((el) => {
|
|
405
|
+
el.addEventListener('click', (e) => {
|
|
406
|
+
e.preventDefault();
|
|
407
|
+
const id = el.getAttribute('data-entity-edit-id');
|
|
408
|
+
if (!id) return;
|
|
409
|
+
const row = allRows.find((item) => item.id === id);
|
|
410
|
+
if (!row) return;
|
|
411
|
+
startEditNodeEntity(row);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function resetNodeEntityForm() {
|
|
417
|
+
state.editingNodeEntityId = null;
|
|
418
|
+
const typeInput = document.getElementById('entity-type-select');
|
|
419
|
+
const titleInput = document.getElementById('entity-title-input');
|
|
420
|
+
const contentInput = document.getElementById('entity-content-input');
|
|
421
|
+
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
422
|
+
const statusInput = document.getElementById('entity-status-select');
|
|
423
|
+
const priorityInput = document.getElementById('entity-priority-select');
|
|
424
|
+
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
425
|
+
if (typeInput) typeInput.value = 'issue';
|
|
426
|
+
if (titleInput) titleInput.value = '';
|
|
427
|
+
if (contentInput) contentInput.value = '';
|
|
428
|
+
if (assigneeInput) assigneeInput.value = '';
|
|
429
|
+
if (statusInput) statusInput.value = '';
|
|
430
|
+
if (priorityInput) priorityInput.value = '';
|
|
431
|
+
if (capabilityTypeInput) capabilityTypeInput.value = '';
|
|
432
|
+
|
|
433
|
+
applyEntityFormMode();
|
|
434
|
+
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
435
|
+
if (submitBtn) submitBtn.textContent = '添加记录';
|
|
436
|
+
const cancelBtn = document.getElementById('cancel-edit-entity-btn');
|
|
437
|
+
cancelBtn?.classList.add('hidden');
|
|
438
|
+
setNodeEntityFormExpanded(false);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function startEditNodeEntity(row) {
|
|
442
|
+
state.editingNodeEntityId = row.id;
|
|
443
|
+
setNodeEntityFormExpanded(true);
|
|
444
|
+
const typeInput = document.getElementById('entity-type-select');
|
|
445
|
+
const titleInput = document.getElementById('entity-title-input');
|
|
446
|
+
const contentInput = document.getElementById('entity-content-input');
|
|
447
|
+
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
448
|
+
const statusInput = document.getElementById('entity-status-select');
|
|
449
|
+
const priorityInput = document.getElementById('entity-priority-select');
|
|
450
|
+
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
451
|
+
if (typeInput) typeInput.value = row.entity_type || 'issue';
|
|
452
|
+
applyEntityFormMode();
|
|
453
|
+
if (titleInput) titleInput.value = row.title || '';
|
|
454
|
+
if (contentInput) contentInput.value = row.content_md || '';
|
|
455
|
+
if (assigneeInput) assigneeInput.value = row.assignee || '';
|
|
456
|
+
if (statusInput) statusInput.value = row.status || '';
|
|
457
|
+
if (priorityInput) priorityInput.value = row.priority || '';
|
|
458
|
+
if (capabilityTypeInput) capabilityTypeInput.value = row.capability_type || '';
|
|
459
|
+
|
|
460
|
+
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
461
|
+
if (submitBtn) submitBtn.textContent = '保存修改';
|
|
462
|
+
const cancelBtn = document.getElementById('cancel-edit-entity-btn');
|
|
463
|
+
cancelBtn?.classList.remove('hidden');
|
|
464
|
+
titleInput?.focus();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function showNodeEntityDrawer(nodeId) {
|
|
468
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
469
|
+
const title = document.getElementById('drawer-node-title');
|
|
470
|
+
const node = getNodeById(nodeId);
|
|
471
|
+
if (!drawer || !title || !node) return;
|
|
472
|
+
state.selectedNodeId = nodeId;
|
|
473
|
+
state.nodeEntityFilter = 'all';
|
|
474
|
+
title.textContent = node.name || nodeId;
|
|
475
|
+
renderNodeEntitySummary(nodeId);
|
|
476
|
+
renderNodeEntityFilterTabs();
|
|
477
|
+
renderNodeEntityList(nodeId);
|
|
478
|
+
resetNodeEntityForm();
|
|
479
|
+
drawer.classList.remove('hidden');
|
|
480
|
+
setTimeout(() => drawer.focus(), 0);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function renderNodeEntityFilterTabs() {
|
|
484
|
+
const tabs = document.getElementById('entity-filter-tabs');
|
|
485
|
+
if (!tabs) return;
|
|
486
|
+
tabs.querySelectorAll('[data-entity-filter]').forEach((el) => {
|
|
487
|
+
const filter = el.getAttribute('data-entity-filter');
|
|
488
|
+
el.classList.toggle('active', filter === state.nodeEntityFilter);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function getStatusOptionsByEntityType(entityType) {
|
|
493
|
+
if (entityType === 'issue') {
|
|
494
|
+
return [
|
|
495
|
+
{ value: '', label: '状态(Issue,默认 open)' },
|
|
496
|
+
{ value: 'open', label: 'open' },
|
|
497
|
+
{ value: 'in_progress', label: 'in_progress' },
|
|
498
|
+
{ value: 'blocked', label: 'blocked' },
|
|
499
|
+
{ value: 'resolved', label: 'resolved' },
|
|
500
|
+
{ value: 'closed', label: 'closed' },
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
if (entityType === 'capability') {
|
|
504
|
+
return [
|
|
505
|
+
{ value: '', label: '状态(Capability,默认 building)' },
|
|
506
|
+
{ value: 'building', label: 'building' },
|
|
507
|
+
{ value: 'ready', label: 'ready' },
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
return [{ value: '', label: '无状态(Knowledge)' }];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function applyEntityFormMode() {
|
|
514
|
+
const type = document.getElementById('entity-type-select')?.value || 'issue';
|
|
515
|
+
const assigneeGroup = document.getElementById('entity-assignee-group');
|
|
516
|
+
const statusGroup = document.getElementById('entity-status-group');
|
|
517
|
+
const priorityGroup = document.getElementById('entity-priority-group');
|
|
518
|
+
const capabilityTypeGroup = document.getElementById('entity-capability-type-group');
|
|
519
|
+
const hint = document.getElementById('entity-form-hint');
|
|
520
|
+
const statusSelect = document.getElementById('entity-status-select');
|
|
521
|
+
|
|
522
|
+
if (statusSelect) {
|
|
523
|
+
statusSelect.innerHTML = getStatusOptionsByEntityType(type)
|
|
524
|
+
.map((option) => `<option value="${safeText(option.value)}">${safeText(option.label)}</option>`)
|
|
525
|
+
.join('');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
assigneeGroup?.classList.toggle('hidden', type !== 'issue');
|
|
529
|
+
priorityGroup?.classList.toggle('hidden', type !== 'issue');
|
|
530
|
+
capabilityTypeGroup?.classList.toggle('hidden', type !== 'capability');
|
|
531
|
+
statusGroup?.classList.toggle('hidden', false);
|
|
532
|
+
|
|
533
|
+
if (hint) {
|
|
534
|
+
hint.textContent = type === 'issue'
|
|
535
|
+
? 'Issue 推荐填写状态/优先级/负责人。'
|
|
536
|
+
: type === 'knowledge'
|
|
537
|
+
? 'Knowledge 可仅保存链接或少量 Markdown。'
|
|
538
|
+
: 'Capability 建议填写能力类型与简述。';
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function closeQuickChatPopover() {
|
|
543
|
+
if (!quickChatPopover) return;
|
|
544
|
+
quickChatPopover.remove();
|
|
545
|
+
quickChatPopover = null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function getNodePath(nodeId) {
|
|
549
|
+
const items = state.currentSandbox?.items || [];
|
|
550
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
551
|
+
const path = [];
|
|
552
|
+
let cursor = byId.get(nodeId);
|
|
553
|
+
while (cursor) {
|
|
554
|
+
path.unshift(cursor.name || cursor.id);
|
|
555
|
+
cursor = cursor.parent_id ? byId.get(cursor.parent_id) : null;
|
|
556
|
+
}
|
|
557
|
+
return path;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function composeQuickChatContent(nodeId, userQuestion) {
|
|
561
|
+
const node = getNodeById(nodeId);
|
|
562
|
+
if (!node || !state.currentSandbox) {
|
|
563
|
+
return userQuestion;
|
|
564
|
+
}
|
|
565
|
+
const rows = getNodeEntitiesByNodeId(nodeId);
|
|
566
|
+
const issueCount = rows.filter((row) => row.entity_type === 'issue').length;
|
|
567
|
+
const knowledgeCount = rows.filter((row) => row.entity_type === 'knowledge').length;
|
|
568
|
+
const capabilityCount = rows.filter((row) => row.entity_type === 'capability').length;
|
|
569
|
+
const path = getNodePath(nodeId).join(' > ');
|
|
570
|
+
return [
|
|
571
|
+
'[NODE_CONTEXT]',
|
|
572
|
+
`sandbox_id=${state.currentSandbox.id}`,
|
|
573
|
+
`node_type=work_item`,
|
|
574
|
+
`node_id=${node.id}`,
|
|
575
|
+
`node_name=${node.name || ''}`,
|
|
576
|
+
`node_path=${path}`,
|
|
577
|
+
`entity_counts=issue:${issueCount},knowledge:${knowledgeCount},capability:${capabilityCount}`,
|
|
578
|
+
'[/NODE_CONTEXT]',
|
|
579
|
+
'',
|
|
580
|
+
'用户问题:',
|
|
581
|
+
userQuestion,
|
|
582
|
+
].join('\n');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function sendSandboxChatMessage(content, options = {}) {
|
|
586
|
+
if (!content || !state.currentSandbox) return;
|
|
587
|
+
const messages = document.getElementById('sandbox-chat-messages');
|
|
588
|
+
const btn = document.getElementById('sandbox-send-btn');
|
|
589
|
+
if (!messages || !btn) return;
|
|
590
|
+
|
|
591
|
+
const displayContent = options.displayContent || content;
|
|
592
|
+
const payloadContent = options.payloadContent || content;
|
|
593
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(displayContent)}</div>`);
|
|
594
|
+
const loadingMessage = appendLoadingMessage(messages);
|
|
595
|
+
messages.scrollTop = messages.scrollHeight;
|
|
596
|
+
|
|
597
|
+
const originalText = btn.textContent;
|
|
598
|
+
setButtonState(btn, { disabled: true, text: '思考中' });
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const history = state.chats
|
|
602
|
+
.filter(c => c.role)
|
|
603
|
+
.slice(-10)
|
|
604
|
+
.map(c => ({ role: c.role, content: typeof c.content === 'string' ? c.content : JSON.stringify(c.content) }));
|
|
605
|
+
|
|
606
|
+
const result = await apiRequest(`${API_BASE}/chats/react`, {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
body: JSON.stringify({
|
|
609
|
+
content: payloadContent,
|
|
610
|
+
sandbox_id: state.currentSandbox.id,
|
|
611
|
+
history,
|
|
612
|
+
pending_action: state.pendingAction
|
|
613
|
+
}),
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
loadingMessage?.remove();
|
|
617
|
+
if (typeof sandboxActionHandler === 'function') {
|
|
618
|
+
await sandboxActionHandler(result.action, state.currentSandbox.id);
|
|
619
|
+
}
|
|
620
|
+
await loadSandbox(state.currentSandbox.id);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
loadingMessage?.remove();
|
|
623
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
624
|
+
messages.scrollTop = messages.scrollHeight;
|
|
625
|
+
} finally {
|
|
626
|
+
setButtonState(btn, { disabled: false, text: originalText || '发送' });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function openQuickChatPopover(nodeId, anchorEl) {
|
|
631
|
+
if (!state.currentSandbox) return;
|
|
632
|
+
closeQuickChatPopover();
|
|
633
|
+
const container = document.querySelector('.sandbox-tree-section');
|
|
634
|
+
if (!container || !(anchorEl instanceof HTMLElement)) return;
|
|
635
|
+
const node = getNodeById(nodeId);
|
|
636
|
+
if (!node) return;
|
|
637
|
+
|
|
638
|
+
const popover = document.createElement('div');
|
|
639
|
+
popover.className = 'quick-chat-popover';
|
|
640
|
+
popover.innerHTML = `
|
|
641
|
+
<div class="quick-chat-header">
|
|
642
|
+
<span>快捷提问 · ${safeText(node.name || node.id)}</span>
|
|
643
|
+
<button class="btn btn-secondary btn-sm" data-quick-chat-cancel>关闭</button>
|
|
644
|
+
</div>
|
|
645
|
+
<textarea placeholder="输入多行问题,确认后会自动提交到右侧聊天区(Ctrl/Cmd+Enter 提交)"></textarea>
|
|
646
|
+
<div class="quick-chat-meta"><span data-quick-chat-count>0</span>/1000</div>
|
|
647
|
+
<div class="quick-chat-actions">
|
|
648
|
+
<button class="btn btn-secondary btn-sm" data-quick-chat-cancel>取消</button>
|
|
649
|
+
<button class="btn btn-primary btn-sm" data-quick-chat-submit disabled>确认提交</button>
|
|
650
|
+
</div>
|
|
651
|
+
`;
|
|
652
|
+
container.appendChild(popover);
|
|
653
|
+
quickChatPopover = popover;
|
|
654
|
+
|
|
655
|
+
const containerRect = container.getBoundingClientRect();
|
|
656
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
|
657
|
+
const popoverRect = popover.getBoundingClientRect();
|
|
658
|
+
const horizontalPadding = 8;
|
|
659
|
+
const verticalPadding = 8;
|
|
660
|
+
const preferredLeft = anchorRect.right - containerRect.left - popoverRect.width + 20;
|
|
661
|
+
const preferredTop = anchorRect.bottom - containerRect.top + 6;
|
|
662
|
+
const maxLeft = Math.max(horizontalPadding, containerRect.width - popoverRect.width - horizontalPadding);
|
|
663
|
+
const maxTop = Math.max(58, containerRect.height - popoverRect.height - verticalPadding);
|
|
664
|
+
const left = Math.min(maxLeft, Math.max(horizontalPadding, preferredLeft));
|
|
665
|
+
const top = Math.min(maxTop, Math.max(58, preferredTop));
|
|
666
|
+
popover.style.left = `${left}px`;
|
|
667
|
+
popover.style.top = `${top}px`;
|
|
668
|
+
|
|
669
|
+
const textarea = popover.querySelector('textarea');
|
|
670
|
+
const submitBtn = popover.querySelector('[data-quick-chat-submit]');
|
|
671
|
+
const countEl = popover.querySelector('[data-quick-chat-count]');
|
|
672
|
+
const cancelBtns = popover.querySelectorAll('[data-quick-chat-cancel]');
|
|
673
|
+
|
|
674
|
+
const updateSubmitState = () => {
|
|
675
|
+
const value = String(textarea?.value || '');
|
|
676
|
+
const length = value.length;
|
|
677
|
+
if (countEl) {
|
|
678
|
+
countEl.textContent = String(length);
|
|
679
|
+
}
|
|
680
|
+
const disabled = length === 0 || length > 1000;
|
|
681
|
+
if (submitBtn) {
|
|
682
|
+
submitBtn.classList.toggle('disabled', disabled);
|
|
683
|
+
submitBtn.disabled = disabled;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
cancelBtns.forEach((btn) => {
|
|
688
|
+
btn.addEventListener('click', () => closeQuickChatPopover());
|
|
689
|
+
});
|
|
690
|
+
submitBtn?.addEventListener('click', async () => {
|
|
691
|
+
const question = String(textarea?.value || '').trim();
|
|
692
|
+
if (!question) return;
|
|
693
|
+
closeQuickChatPopover();
|
|
694
|
+
const payloadContent = composeQuickChatContent(nodeId, question);
|
|
695
|
+
const displayContent = `【快捷】${question}`;
|
|
696
|
+
await sendSandboxChatMessage(question, { payloadContent, displayContent });
|
|
697
|
+
});
|
|
698
|
+
textarea?.addEventListener('input', updateSubmitState);
|
|
699
|
+
textarea?.addEventListener('keydown', async (event) => {
|
|
700
|
+
if (event.key === 'Escape') {
|
|
701
|
+
event.preventDefault();
|
|
702
|
+
closeQuickChatPopover();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const isSubmit = event.key === 'Enter' && (event.ctrlKey || event.metaKey);
|
|
706
|
+
if (!isSubmit) return;
|
|
707
|
+
event.preventDefault();
|
|
708
|
+
if (submitBtn instanceof HTMLButtonElement && !submitBtn.disabled) {
|
|
709
|
+
submitBtn.click();
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
textarea?.focus();
|
|
713
|
+
updateSubmitState();
|
|
714
|
+
}
|
|
715
|
+
|
|
132
716
|
async function loadSandboxes() {
|
|
133
717
|
state.sandboxes = await apiRequest(`${API_BASE}/sandboxes`);
|
|
134
718
|
renderSandboxes();
|
|
@@ -172,14 +756,23 @@ function renderSandboxes() {
|
|
|
172
756
|
}
|
|
173
757
|
|
|
174
758
|
async function loadSandbox(id) {
|
|
175
|
-
|
|
759
|
+
closeQuickChatPopover();
|
|
760
|
+
const [sandbox, items, diaries, entities, entityStats] = await Promise.all([
|
|
176
761
|
apiRequest(`${API_BASE}/sandboxes/${id}`),
|
|
177
762
|
apiRequest(`${API_BASE}/sandboxes/${id}/items`),
|
|
178
|
-
apiRequest(`${API_BASE}/diaries?sandbox_id=${id}`)
|
|
763
|
+
apiRequest(`${API_BASE}/diaries?sandbox_id=${id}`),
|
|
764
|
+
apiRequest(`${API_BASE}/sandboxes/${id}/entities`),
|
|
765
|
+
apiRequest(`${API_BASE}/sandboxes/${id}/entities/stats`),
|
|
179
766
|
]);
|
|
180
767
|
state.currentSandbox = { ...sandbox, items, diaries };
|
|
768
|
+
state.nodeEntities = entities;
|
|
769
|
+
state.nodeEntityStats = entityStats;
|
|
181
770
|
|
|
182
771
|
document.getElementById('sandbox-title').textContent = sandbox.name;
|
|
772
|
+
applyWorkTreeViewMode(state.workTreeViewMode || 'full');
|
|
773
|
+
applyWorkItemAssigneeToggle();
|
|
774
|
+
applySandboxLayoutMode();
|
|
775
|
+
applySandboxFullscreenState();
|
|
183
776
|
renderSandboxOverview();
|
|
184
777
|
renderWorkTree();
|
|
185
778
|
loadSandboxChats(id);
|
|
@@ -195,11 +788,14 @@ function renderSandboxOverview() {
|
|
|
195
788
|
inProgress: items.filter((i) => i.status === 'in_progress').length,
|
|
196
789
|
done: items.filter((i) => i.status === 'done').length,
|
|
197
790
|
};
|
|
791
|
+
const entityStats = state.nodeEntityStats || { issue: {}, knowledge: {}, capability: {} };
|
|
198
792
|
container.innerHTML = `
|
|
199
793
|
<div class="summary-card"><div class="label">任务总数</div><div class="value">${items.length}</div></div>
|
|
200
794
|
<div class="summary-card"><div class="label">待处理/进行中</div><div class="value">${byStatus.pending}/${byStatus.inProgress}</div></div>
|
|
201
795
|
<div class="summary-card"><div class="label">已完成</div><div class="value">${byStatus.done}</div></div>
|
|
202
796
|
<div class="summary-card"><div class="label">关联日记</div><div class="value">${diaries.length}</div></div>
|
|
797
|
+
<div class="summary-card"><div class="label">Issue(Open)</div><div class="value">${entityStats.issue?.total || 0}(${(entityStats.issue?.open || 0) + (entityStats.issue?.in_progress || 0) + (entityStats.issue?.blocked || 0)})</div></div>
|
|
798
|
+
<div class="summary-card"><div class="label">Knowledge / Capability</div><div class="value">${entityStats.knowledge?.total || 0} / ${entityStats.capability?.total || 0}</div></div>
|
|
203
799
|
`;
|
|
204
800
|
}
|
|
205
801
|
|
|
@@ -252,6 +848,10 @@ function renderAIActionMessage(action) {
|
|
|
252
848
|
const status = getExecutionStatus(action);
|
|
253
849
|
const statusIcon = status === 'success' ? '✅' : status === 'partial' ? '⚠️' : '❌';
|
|
254
850
|
const statusClass = status === 'success' ? 'is-success' : status === 'partial' ? 'is-partial' : 'is-failed';
|
|
851
|
+
const details = Array.isArray(action?.execution_result?.details) ? action.execution_result.details : [];
|
|
852
|
+
const detailsHtml = details.length
|
|
853
|
+
? `<div class="result-detail-list">${details.map((line) => `<div class="result-detail-item">${escapeHtml(String(line))}</div>`).join('')}</div>`
|
|
854
|
+
: '';
|
|
255
855
|
const undoBtn = action.operationId
|
|
256
856
|
? `<button class="btn btn-sm btn-undo" data-operation-id="${action.operationId}" onclick="undoOperation('${action.operationId}', this)">撤销</button>`
|
|
257
857
|
: '';
|
|
@@ -261,6 +861,7 @@ function renderAIActionMessage(action) {
|
|
|
261
861
|
<span class="result-text">${escapeHtml(action.observation || '操作完成')}</span>
|
|
262
862
|
${undoBtn}
|
|
263
863
|
</div>
|
|
864
|
+
${detailsHtml}
|
|
264
865
|
</div>`;
|
|
265
866
|
}
|
|
266
867
|
|
|
@@ -397,6 +998,10 @@ function isKeyOperation(op, before, after) {
|
|
|
397
998
|
function formatDiffLabel(key) {
|
|
398
999
|
const labels = {
|
|
399
1000
|
name: '名称',
|
|
1001
|
+
title: '标题',
|
|
1002
|
+
entity_type: '类型',
|
|
1003
|
+
work_item_id: '节点ID',
|
|
1004
|
+
content_md: '内容',
|
|
400
1005
|
status: '状态',
|
|
401
1006
|
priority: '优先级',
|
|
402
1007
|
assignee: '负责人',
|
|
@@ -406,8 +1011,29 @@ function formatDiffLabel(key) {
|
|
|
406
1011
|
return labels[key] || key;
|
|
407
1012
|
}
|
|
408
1013
|
|
|
409
|
-
function
|
|
410
|
-
const
|
|
1014
|
+
function getOperationTarget(before, after) {
|
|
1015
|
+
const recordType = String(after?.record_type || before?.record_type || 'work_item');
|
|
1016
|
+
if (recordType === 'node_entity') {
|
|
1017
|
+
const entityType = String(after?.entity_type || before?.entity_type || 'entity');
|
|
1018
|
+
return {
|
|
1019
|
+
recordType,
|
|
1020
|
+
typeLabel: entityType.toUpperCase(),
|
|
1021
|
+
name: String(after?.title || before?.title || '-'),
|
|
1022
|
+
scopeLabel: `节点 ${String(after?.work_item_id || before?.work_item_id || '-')}`,
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return {
|
|
1026
|
+
recordType: 'work_item',
|
|
1027
|
+
typeLabel: 'WORK_ITEM',
|
|
1028
|
+
name: String(after?.name || before?.name || '-'),
|
|
1029
|
+
scopeLabel: `父节点 ${String(after?.parent_id || before?.parent_id || 'root')}`,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function buildUpdateDiffLines(before, after, recordType = 'work_item') {
|
|
1034
|
+
const trackedFields = recordType === 'node_entity'
|
|
1035
|
+
? ['title', 'status', 'priority', 'assignee', 'content_md', 'work_item_id']
|
|
1036
|
+
: ['name', 'status', 'priority', 'assignee', 'parent_id', 'description'];
|
|
411
1037
|
return trackedFields
|
|
412
1038
|
.filter((key) => (before?.[key] ?? '') !== (after?.[key] ?? ''))
|
|
413
1039
|
.map((key) => {
|
|
@@ -466,8 +1092,8 @@ function generateWeeklySummaryMarkdown(mode = 'management') {
|
|
|
466
1092
|
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
467
1093
|
const before = parseOpJson(op.data_before);
|
|
468
1094
|
const after = parseOpJson(op.data_after);
|
|
469
|
-
const
|
|
470
|
-
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${
|
|
1095
|
+
const target = getOperationTarget(before, after);
|
|
1096
|
+
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${target.typeLabel} ${target.name}`);
|
|
471
1097
|
}
|
|
472
1098
|
} else {
|
|
473
1099
|
lines.push('');
|
|
@@ -480,9 +1106,9 @@ function generateWeeklySummaryMarkdown(mode = 'management') {
|
|
|
480
1106
|
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
481
1107
|
const before = parseOpJson(op.data_before);
|
|
482
1108
|
const after = parseOpJson(op.data_after);
|
|
483
|
-
const
|
|
484
|
-
const diffLines = op.operation_type === 'update' ? buildUpdateDiffLines(before, after) : [];
|
|
485
|
-
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${
|
|
1109
|
+
const target = getOperationTarget(before, after);
|
|
1110
|
+
const diffLines = op.operation_type === 'update' ? buildUpdateDiffLines(before, after, target.recordType) : [];
|
|
1111
|
+
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${target.typeLabel} ${target.name}`);
|
|
486
1112
|
if (diffLines.length > 0) {
|
|
487
1113
|
for (const line of diffLines.slice(0, 3)) {
|
|
488
1114
|
lines.push(` - ${line}`);
|
|
@@ -514,26 +1140,28 @@ function renderChanges() {
|
|
|
514
1140
|
const items = ops.map((op) => {
|
|
515
1141
|
const before = parseOpJson(op.data_before);
|
|
516
1142
|
const after = parseOpJson(op.data_after);
|
|
1143
|
+
const target = getOperationTarget(before, after);
|
|
517
1144
|
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
518
1145
|
let detail = '';
|
|
519
1146
|
let diffLinesHtml = '';
|
|
520
1147
|
|
|
521
1148
|
if (op.operation_type === 'create') {
|
|
522
|
-
detail =
|
|
1149
|
+
detail = `创建${safeText(target.typeLabel)}:${safeText(target.name)}`;
|
|
523
1150
|
} else if (op.operation_type === 'update') {
|
|
524
|
-
detail =
|
|
525
|
-
const diffLines = buildUpdateDiffLines(before, after);
|
|
1151
|
+
detail = `更新${safeText(target.typeLabel)}:${safeText(target.name)}`;
|
|
1152
|
+
const diffLines = buildUpdateDiffLines(before, after, target.recordType);
|
|
526
1153
|
if (diffLines.length > 0) {
|
|
527
1154
|
diffLinesHtml = `<div class="change-diff-list">${diffLines.map((line) => `<div class="change-diff-line">${line}</div>`).join('')}</div>`;
|
|
528
1155
|
}
|
|
529
1156
|
} else if (op.operation_type === 'delete') {
|
|
530
|
-
detail =
|
|
1157
|
+
detail = `删除${safeText(target.typeLabel)}:${safeText(target.name)}`;
|
|
531
1158
|
}
|
|
532
1159
|
return `
|
|
533
1160
|
<div class="change-item">
|
|
534
1161
|
<div class="change-meta">
|
|
535
1162
|
<span class="change-type ${safeText(op.operation_type)}">${safeText(op.operation_type)}</span>
|
|
536
1163
|
<span>${safeText(sandboxName)}</span>
|
|
1164
|
+
<span class="change-target">${safeText(target.scopeLabel)}</span>
|
|
537
1165
|
<span>${new Date(op.created_at).toLocaleTimeString()}</span>
|
|
538
1166
|
</div>
|
|
539
1167
|
<div class="change-detail">${detail}</div>
|
|
@@ -568,6 +1196,49 @@ async function loadSettings() {
|
|
|
568
1196
|
apiKeyInput.value = '';
|
|
569
1197
|
apiKeyInput.placeholder = state.settings.has_api_key ? '已配置,留空表示不修改' : 'sk-...';
|
|
570
1198
|
document.getElementById('setting-model').value = state.settings.model || '';
|
|
1199
|
+
renderSettingHeadersRows(state.settings.headers || {});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function createSettingHeaderRow(key = '', value = '') {
|
|
1203
|
+
const row = document.createElement('div');
|
|
1204
|
+
row.className = 'settings-kv-row';
|
|
1205
|
+
row.innerHTML = `
|
|
1206
|
+
<input type="text" class="setting-header-key" placeholder="Header 名称" value="${escapeHtml(String(key || ''))}">
|
|
1207
|
+
<input type="text" class="setting-header-value" placeholder="Header 值" value="${escapeHtml(String(value || ''))}">
|
|
1208
|
+
<button class="btn btn-secondary btn-sm remove-setting-header-btn" type="button">删除</button>
|
|
1209
|
+
`;
|
|
1210
|
+
row.querySelector('.remove-setting-header-btn')?.addEventListener('click', () => {
|
|
1211
|
+
row.remove();
|
|
1212
|
+
});
|
|
1213
|
+
return row;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function renderSettingHeadersRows(headers) {
|
|
1217
|
+
const list = document.getElementById('setting-headers-list');
|
|
1218
|
+
if (!(list instanceof HTMLElement)) return;
|
|
1219
|
+
list.innerHTML = '';
|
|
1220
|
+
const entries = Object.entries(headers || {}).filter(([k, v]) => String(k || '').trim() && String(v || '').trim());
|
|
1221
|
+
if (!entries.length) {
|
|
1222
|
+
list.appendChild(createSettingHeaderRow('', ''));
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
entries.forEach(([k, v]) => list.appendChild(createSettingHeaderRow(String(k), String(v))));
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function collectSettingHeadersFromUI() {
|
|
1229
|
+
const list = document.getElementById('setting-headers-list');
|
|
1230
|
+
if (!(list instanceof HTMLElement)) return {};
|
|
1231
|
+
const rows = Array.from(list.querySelectorAll('.settings-kv-row'));
|
|
1232
|
+
const headers = {};
|
|
1233
|
+
rows.forEach((row) => {
|
|
1234
|
+
const keyInput = row.querySelector('.setting-header-key');
|
|
1235
|
+
const valueInput = row.querySelector('.setting-header-value');
|
|
1236
|
+
const key = String(keyInput?.value || '').trim();
|
|
1237
|
+
const value = String(valueInput?.value || '').trim();
|
|
1238
|
+
if (!key || !value) return;
|
|
1239
|
+
headers[key] = value;
|
|
1240
|
+
});
|
|
1241
|
+
return headers;
|
|
571
1242
|
}
|
|
572
1243
|
|
|
573
1244
|
async function loadChats() {
|
|
@@ -608,6 +1279,7 @@ function editWorkItem(id) {
|
|
|
608
1279
|
|
|
609
1280
|
function initApp() {
|
|
610
1281
|
const hash = window.location.hash;
|
|
1282
|
+
loadWorkItemAssigneePreference();
|
|
611
1283
|
|
|
612
1284
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
613
1285
|
link.addEventListener('click', (e) => {
|
|
@@ -658,10 +1330,154 @@ function initApp() {
|
|
|
658
1330
|
document.getElementById('new-item-parent').value = '';
|
|
659
1331
|
document.getElementById('item-dialog').showModal();
|
|
660
1332
|
});
|
|
1333
|
+
|
|
1334
|
+
document.getElementById('toggle-sandbox-present-btn')?.addEventListener('click', () => {
|
|
1335
|
+
state.sandboxPresentationMode = !state.sandboxPresentationMode;
|
|
1336
|
+
applySandboxLayoutMode();
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
document.getElementById('toggle-sandbox-fullscreen-btn')?.addEventListener('click', async () => {
|
|
1340
|
+
await toggleSandboxFullscreen();
|
|
1341
|
+
applySandboxFullscreenState();
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
document.addEventListener('fullscreenchange', () => {
|
|
1345
|
+
applySandboxFullscreenState();
|
|
1346
|
+
});
|
|
661
1347
|
|
|
662
1348
|
document.getElementById('cancel-item-btn')?.addEventListener('click', () => {
|
|
663
1349
|
document.getElementById('item-dialog').close();
|
|
664
1350
|
});
|
|
1351
|
+
|
|
1352
|
+
document.getElementById('close-node-drawer-btn')?.addEventListener('click', () => {
|
|
1353
|
+
closeNodeEntityDrawer();
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
document.getElementById('toggle-node-entity-form-btn')?.addEventListener('click', () => {
|
|
1357
|
+
setNodeEntityFormExpanded(!state.nodeEntityFormExpanded);
|
|
1358
|
+
if (state.nodeEntityFormExpanded) {
|
|
1359
|
+
document.getElementById('entity-title-input')?.focus();
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
document.getElementById('entity-type-select')?.addEventListener('change', () => {
|
|
1364
|
+
applyEntityFormMode();
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
document.getElementById('entity-filter-tabs')?.addEventListener('click', (event) => {
|
|
1368
|
+
const target = event.target;
|
|
1369
|
+
if (!(target instanceof HTMLElement)) return;
|
|
1370
|
+
const btn = target.closest('[data-entity-filter]');
|
|
1371
|
+
if (!(btn instanceof HTMLElement)) return;
|
|
1372
|
+
const filter = btn.getAttribute('data-entity-filter') || 'all';
|
|
1373
|
+
state.nodeEntityFilter = filter;
|
|
1374
|
+
renderNodeEntityFilterTabs();
|
|
1375
|
+
if (state.selectedNodeId) {
|
|
1376
|
+
renderNodeEntityList(state.selectedNodeId);
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
document.getElementById('cancel-edit-entity-btn')?.addEventListener('click', () => {
|
|
1381
|
+
resetNodeEntityForm();
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
document.getElementById('create-node-entity-btn')?.addEventListener('click', async () => {
|
|
1385
|
+
if (!state.currentSandbox || !state.selectedNodeId) return;
|
|
1386
|
+
const btn = document.getElementById('create-node-entity-btn');
|
|
1387
|
+
const title = document.getElementById('entity-title-input').value.trim();
|
|
1388
|
+
const entity_type = document.getElementById('entity-type-select').value;
|
|
1389
|
+
if (!title) return;
|
|
1390
|
+
const rawStatus = document.getElementById('entity-status-select').value || '';
|
|
1391
|
+
const status = rawStatus || (entity_type === 'issue' ? 'open' : entity_type === 'capability' ? 'building' : '');
|
|
1392
|
+
const payload = {
|
|
1393
|
+
work_item_id: state.selectedNodeId,
|
|
1394
|
+
entity_type,
|
|
1395
|
+
title,
|
|
1396
|
+
content_md: document.getElementById('entity-content-input').value || '',
|
|
1397
|
+
assignee: document.getElementById('entity-assignee-input').value || '',
|
|
1398
|
+
status,
|
|
1399
|
+
priority: document.getElementById('entity-priority-select').value || '',
|
|
1400
|
+
capability_type: document.getElementById('entity-capability-type-input').value || '',
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
const isEditing = Boolean(state.editingNodeEntityId);
|
|
1404
|
+
setButtonState(btn, { disabled: true, text: isEditing ? '保存中...' : '添加中...' });
|
|
1405
|
+
try {
|
|
1406
|
+
if (isEditing) {
|
|
1407
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities/${state.editingNodeEntityId}`, {
|
|
1408
|
+
method: 'PUT',
|
|
1409
|
+
body: JSON.stringify(payload),
|
|
1410
|
+
});
|
|
1411
|
+
} else {
|
|
1412
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities`, {
|
|
1413
|
+
method: 'POST',
|
|
1414
|
+
body: JSON.stringify(payload),
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
await loadSandbox(state.currentSandbox.id);
|
|
1418
|
+
showNodeEntityDrawer(state.selectedNodeId);
|
|
1419
|
+
resetNodeEntityForm();
|
|
1420
|
+
} finally {
|
|
1421
|
+
const defaultText = state.editingNodeEntityId ? '保存修改' : '添加记录';
|
|
1422
|
+
setButtonState(btn, { disabled: false, text: defaultText });
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
const entitySubmitByShortcut = async (event) => {
|
|
1427
|
+
const isSubmit = event.key === 'Enter' && (event.ctrlKey || event.metaKey);
|
|
1428
|
+
if (!isSubmit) return;
|
|
1429
|
+
event.preventDefault();
|
|
1430
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
1431
|
+
const form = document.getElementById('drawer-create-form');
|
|
1432
|
+
const submitBtn = document.getElementById('create-node-entity-btn');
|
|
1433
|
+
if (drawer?.classList.contains('hidden')) return;
|
|
1434
|
+
if (form?.classList.contains('hidden')) return;
|
|
1435
|
+
if (!(submitBtn instanceof HTMLButtonElement) || submitBtn.disabled) return;
|
|
1436
|
+
submitBtn.click();
|
|
1437
|
+
};
|
|
1438
|
+
document.getElementById('entity-title-input')?.addEventListener('keydown', entitySubmitByShortcut);
|
|
1439
|
+
document.getElementById('entity-content-input')?.addEventListener('keydown', entitySubmitByShortcut);
|
|
1440
|
+
|
|
1441
|
+
document.addEventListener('keydown', (event) => {
|
|
1442
|
+
if (event.key !== 'Escape') return;
|
|
1443
|
+
if (quickChatPopover) {
|
|
1444
|
+
event.preventDefault();
|
|
1445
|
+
event.stopPropagation();
|
|
1446
|
+
closeQuickChatPopover();
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
1450
|
+
if (drawer && !drawer.classList.contains('hidden')) {
|
|
1451
|
+
event.preventDefault();
|
|
1452
|
+
event.stopPropagation();
|
|
1453
|
+
closeNodeEntityDrawer();
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (state.sandboxFullscreenMode) {
|
|
1457
|
+
event.preventDefault();
|
|
1458
|
+
event.stopPropagation();
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
document.addEventListener('click', (event) => {
|
|
1463
|
+
const drawer = document.getElementById('node-entity-drawer');
|
|
1464
|
+
const workTree = document.getElementById('work-tree');
|
|
1465
|
+
if (!drawer || drawer.classList.contains('hidden')) return;
|
|
1466
|
+
const target = event.target;
|
|
1467
|
+
if (!(target instanceof Node)) return;
|
|
1468
|
+
if (drawer.contains(target)) return;
|
|
1469
|
+
if (workTree?.contains(target)) return;
|
|
1470
|
+
closeNodeEntityDrawer();
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
document.addEventListener('click', (event) => {
|
|
1474
|
+
if (!quickChatPopover) return;
|
|
1475
|
+
const target = event.target;
|
|
1476
|
+
if (!(target instanceof Node)) return;
|
|
1477
|
+
if (quickChatPopover.contains(target)) return;
|
|
1478
|
+
if (target instanceof HTMLElement && target.closest('[data-action="quick-chat"]')) return;
|
|
1479
|
+
closeQuickChatPopover();
|
|
1480
|
+
});
|
|
665
1481
|
|
|
666
1482
|
document.getElementById('confirm-item-btn')?.addEventListener('click', async () => {
|
|
667
1483
|
const dialog = document.getElementById('item-dialog');
|
|
@@ -757,48 +1573,8 @@ function initApp() {
|
|
|
757
1573
|
const input = document.getElementById('sandbox-chat-input');
|
|
758
1574
|
const content = input.value.trim();
|
|
759
1575
|
if (!content || !state.currentSandbox) return;
|
|
760
|
-
|
|
761
|
-
const btn = document.getElementById('sandbox-send-btn');
|
|
762
|
-
const messages = document.getElementById('sandbox-chat-messages');
|
|
763
|
-
|
|
764
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(content)}</div>`);
|
|
765
|
-
const loadingMessage = appendLoadingMessage(messages);
|
|
766
|
-
messages.scrollTop = messages.scrollHeight;
|
|
767
1576
|
input.value = '';
|
|
768
|
-
|
|
769
|
-
const originalText = btn.textContent;
|
|
770
|
-
setButtonState(btn, { disabled: true, text: '思考中' });
|
|
771
|
-
|
|
772
|
-
try {
|
|
773
|
-
const history = state.chats
|
|
774
|
-
.filter(c => c.role)
|
|
775
|
-
.slice(-10)
|
|
776
|
-
.map(c => ({ role: c.role, content: typeof c.content === 'string' ? c.content : JSON.stringify(c.content) }));
|
|
777
|
-
|
|
778
|
-
const result = await apiRequest(`${API_BASE}/chats/react`, {
|
|
779
|
-
method: 'POST',
|
|
780
|
-
body: JSON.stringify({
|
|
781
|
-
content,
|
|
782
|
-
sandbox_id: state.currentSandbox.id,
|
|
783
|
-
history,
|
|
784
|
-
pending_action: state.pendingAction
|
|
785
|
-
}),
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
console.log('Chat result:', result);
|
|
789
|
-
|
|
790
|
-
loadingMessage?.remove();
|
|
791
|
-
|
|
792
|
-
await handleAIAction(result.action, state.currentSandbox.id);
|
|
793
|
-
await loadSandbox(state.currentSandbox.id);
|
|
794
|
-
} catch (error) {
|
|
795
|
-
console.error('发送消息失败:', error);
|
|
796
|
-
loadingMessage?.remove();
|
|
797
|
-
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
798
|
-
messages.scrollTop = messages.scrollHeight;
|
|
799
|
-
} finally {
|
|
800
|
-
setButtonState(btn, { disabled: false, text: originalText || '发送' });
|
|
801
|
-
}
|
|
1577
|
+
await sendSandboxChatMessage(content);
|
|
802
1578
|
});
|
|
803
1579
|
|
|
804
1580
|
async function handleAIAction(action, sandboxId) {
|
|
@@ -907,6 +1683,8 @@ function initApp() {
|
|
|
907
1683
|
state.pendingAction = null;
|
|
908
1684
|
}
|
|
909
1685
|
}
|
|
1686
|
+
|
|
1687
|
+
sandboxActionHandler = handleAIAction;
|
|
910
1688
|
|
|
911
1689
|
document.getElementById('sandbox-chat-input')?.addEventListener('keypress', (e) => {
|
|
912
1690
|
if (e.key === 'Enter') {
|
|
@@ -932,6 +1710,7 @@ function initApp() {
|
|
|
932
1710
|
const api_url = document.getElementById('setting-api-url').value;
|
|
933
1711
|
const api_key = document.getElementById('setting-api-key').value;
|
|
934
1712
|
const model = document.getElementById('setting-model').value;
|
|
1713
|
+
const headers = collectSettingHeadersFromUI();
|
|
935
1714
|
|
|
936
1715
|
await apiRequest(`${API_BASE}/settings/api_url`, {
|
|
937
1716
|
method: 'PUT',
|
|
@@ -947,11 +1726,21 @@ function initApp() {
|
|
|
947
1726
|
method: 'PUT',
|
|
948
1727
|
body: JSON.stringify({ value: model }),
|
|
949
1728
|
});
|
|
1729
|
+
await apiRequest(`${API_BASE}/settings/headers`, {
|
|
1730
|
+
method: 'PUT',
|
|
1731
|
+
body: JSON.stringify({ value: headers }),
|
|
1732
|
+
});
|
|
950
1733
|
|
|
951
1734
|
alert('设置已保存');
|
|
952
1735
|
await loadSettings();
|
|
953
1736
|
});
|
|
954
1737
|
|
|
1738
|
+
document.getElementById('add-setting-header-btn')?.addEventListener('click', () => {
|
|
1739
|
+
const list = document.getElementById('setting-headers-list');
|
|
1740
|
+
if (!(list instanceof HTMLElement)) return;
|
|
1741
|
+
list.appendChild(createSettingHeaderRow('', ''));
|
|
1742
|
+
});
|
|
1743
|
+
|
|
955
1744
|
document.getElementById('work-item-search')?.addEventListener('input', (e) => {
|
|
956
1745
|
state.workItemSearch = e.target.value || '';
|
|
957
1746
|
renderWorkTree();
|
|
@@ -962,6 +1751,18 @@ function initApp() {
|
|
|
962
1751
|
renderWorkTree();
|
|
963
1752
|
});
|
|
964
1753
|
|
|
1754
|
+
document.getElementById('work-tree-view-mode')?.addEventListener('change', (e) => {
|
|
1755
|
+
const nextMode = e.target.value || 'full';
|
|
1756
|
+
applyWorkTreeViewMode(nextMode);
|
|
1757
|
+
renderWorkTree();
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
document.getElementById('work-item-show-assignee-toggle')?.addEventListener('change', (e) => {
|
|
1761
|
+
state.workItemShowAssignee = Boolean(e.target.checked);
|
|
1762
|
+
persistWorkItemAssigneePreference();
|
|
1763
|
+
renderWorkTree();
|
|
1764
|
+
});
|
|
1765
|
+
|
|
965
1766
|
document.getElementById('diary-search')?.addEventListener('input', (e) => {
|
|
966
1767
|
state.diarySearch = e.target.value || '';
|
|
967
1768
|
renderDiaries();
|
|
@@ -1049,12 +1850,26 @@ function initApp() {
|
|
|
1049
1850
|
await navigator.clipboard.writeText(content);
|
|
1050
1851
|
alert('周报摘要已复制');
|
|
1051
1852
|
});
|
|
1853
|
+
|
|
1854
|
+
window.addEventListener('resize', () => {
|
|
1855
|
+
if (!state.currentSandbox) return;
|
|
1856
|
+
if (resizeRenderTimer) {
|
|
1857
|
+
clearTimeout(resizeRenderTimer);
|
|
1858
|
+
}
|
|
1859
|
+
resizeRenderTimer = setTimeout(() => {
|
|
1860
|
+
renderWorkTree();
|
|
1861
|
+
}, 120);
|
|
1862
|
+
});
|
|
1052
1863
|
|
|
1053
1864
|
window.addEventListener('hashchange', handleRoute);
|
|
1054
1865
|
handleRoute();
|
|
1055
1866
|
|
|
1056
1867
|
async function handleRoute() {
|
|
1057
1868
|
const hash = window.location.hash.slice(1) || '/';
|
|
1869
|
+
const fullscreenRoot = getSandboxFullscreenElement();
|
|
1870
|
+
if (!hash.startsWith('/sandbox/') && fullscreenRoot && document.fullscreenElement === fullscreenRoot) {
|
|
1871
|
+
await document.exitFullscreen();
|
|
1872
|
+
}
|
|
1058
1873
|
|
|
1059
1874
|
if (hash === '/') {
|
|
1060
1875
|
showPage('home');
|