@xpert-ai/plugin-excalidraw 0.1.0
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/.xpertai-plugin/plugin.json +118 -0
- package/README.md +5 -0
- package/assets/composerIcon.svg +5 -0
- package/assets/logo.svg +8 -0
- package/dist/docs/excalidraw-agent-skill.md +32 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/constants.d.ts +24 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +42 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/entities/excalidraw-action-log.entity.d.ts +18 -0
- package/dist/lib/entities/excalidraw-action-log.entity.d.ts.map +1 -0
- package/dist/lib/entities/excalidraw-action-log.entity.js +69 -0
- package/dist/lib/entities/excalidraw-action-log.entity.js.map +1 -0
- package/dist/lib/entities/excalidraw-drawing-version.entity.d.ts +21 -0
- package/dist/lib/entities/excalidraw-drawing-version.entity.d.ts.map +1 -0
- package/dist/lib/entities/excalidraw-drawing-version.entity.js +82 -0
- package/dist/lib/entities/excalidraw-drawing-version.entity.js.map +1 -0
- package/dist/lib/entities/excalidraw-drawing.entity.d.ts +24 -0
- package/dist/lib/entities/excalidraw-drawing.entity.d.ts.map +1 -0
- package/dist/lib/entities/excalidraw-drawing.entity.js +94 -0
- package/dist/lib/entities/excalidraw-drawing.entity.js.map +1 -0
- package/dist/lib/entities/index.d.ts +4 -0
- package/dist/lib/entities/index.d.ts.map +1 -0
- package/dist/lib/entities/index.js +4 -0
- package/dist/lib/entities/index.js.map +1 -0
- package/dist/lib/excalidraw-view.provider.d.ts +14 -0
- package/dist/lib/excalidraw-view.provider.d.ts.map +1 -0
- package/dist/lib/excalidraw-view.provider.js +423 -0
- package/dist/lib/excalidraw-view.provider.js.map +1 -0
- package/dist/lib/excalidraw.middleware.d.ts +10 -0
- package/dist/lib/excalidraw.middleware.d.ts.map +1 -0
- package/dist/lib/excalidraw.middleware.js +173 -0
- package/dist/lib/excalidraw.middleware.js.map +1 -0
- package/dist/lib/excalidraw.plugin.d.ts +8 -0
- package/dist/lib/excalidraw.plugin.d.ts.map +1 -0
- package/dist/lib/excalidraw.plugin.js +27 -0
- package/dist/lib/excalidraw.plugin.js.map +1 -0
- package/dist/lib/excalidraw.service.d.ts +169 -0
- package/dist/lib/excalidraw.service.d.ts.map +1 -0
- package/dist/lib/excalidraw.service.js +441 -0
- package/dist/lib/excalidraw.service.js.map +1 -0
- package/dist/lib/excalidraw.templates.d.ts +3 -0
- package/dist/lib/excalidraw.templates.d.ts.map +1 -0
- package/dist/lib/excalidraw.templates.js +78 -0
- package/dist/lib/excalidraw.templates.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/app.css +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/app.js +5105 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/i18n.d.ts +3 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/i18n.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/i18n.js +103 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/i18n.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/i18n.ts +151 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/main.tsx +1411 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.d.ts +3 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.js +4 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.ts +4 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.d.ts +11 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.js +11 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.ts +11 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.d.ts +5 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.js +8 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.ts +8 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.d.ts +36 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.js +36 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.ts +36 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/runtime.d.ts +21 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/runtime.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/runtime.js +198 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/runtime.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/runtime.ts +228 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/styles.d.ts +2 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/styles.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/styles.js +324 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/styles.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/styles.ts +323 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/vendor.d.ts +4 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/vendor.d.ts.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/vendor.js +4 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/vendor.js.map +1 -0
- package/dist/lib/remote-components/excalidraw-workbench/src/vendor.ts +3 -0
- package/dist/lib/types.d.ts +74 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/xpert-excalidraw-assistant.yaml +129 -0
- package/package.json +87 -0
- package/skills/index/SKILL.md +46 -0
- package/skills/index/agents/xpertai.yaml +6 -0
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
import '@excalidraw/excalidraw/index.css'
|
|
2
|
+
import {
|
|
3
|
+
Excalidraw,
|
|
4
|
+
exportToBlob,
|
|
5
|
+
exportToSvg,
|
|
6
|
+
serializeAsJSON,
|
|
7
|
+
convertToExcalidrawElements
|
|
8
|
+
} from '@excalidraw/excalidraw'
|
|
9
|
+
import { parseMermaidToExcalidraw } from '@excalidraw/mermaid-to-excalidraw'
|
|
10
|
+
import {
|
|
11
|
+
Archive,
|
|
12
|
+
Badge,
|
|
13
|
+
Button,
|
|
14
|
+
Check,
|
|
15
|
+
FileJson,
|
|
16
|
+
Image,
|
|
17
|
+
Input,
|
|
18
|
+
PanelLeftClose,
|
|
19
|
+
PanelLeftOpen,
|
|
20
|
+
PanelRightClose,
|
|
21
|
+
PanelRightOpen,
|
|
22
|
+
Plus,
|
|
23
|
+
RotateCcw,
|
|
24
|
+
Save,
|
|
25
|
+
ScrollArea,
|
|
26
|
+
Select,
|
|
27
|
+
SelectContent,
|
|
28
|
+
SelectItem,
|
|
29
|
+
SelectTrigger,
|
|
30
|
+
SelectValue,
|
|
31
|
+
Send,
|
|
32
|
+
Sidebar,
|
|
33
|
+
SidebarContent,
|
|
34
|
+
SidebarHeader,
|
|
35
|
+
SidebarMenu,
|
|
36
|
+
SidebarMenuButton,
|
|
37
|
+
SidebarMenuItem,
|
|
38
|
+
SidebarRail,
|
|
39
|
+
SidebarTitle,
|
|
40
|
+
SidebarTrigger,
|
|
41
|
+
Textarea,
|
|
42
|
+
Upload,
|
|
43
|
+
installShadcnThemeVars
|
|
44
|
+
} from '@xpert-ai/plugin-shadcn-ui'
|
|
45
|
+
import { React, ReactDOM, h } from './vendor'
|
|
46
|
+
import { createTranslator, TranslationKey } from './i18n'
|
|
47
|
+
import { injectStyles } from './styles'
|
|
48
|
+
import {
|
|
49
|
+
executeAction,
|
|
50
|
+
executeFileAction,
|
|
51
|
+
getErrorMessage,
|
|
52
|
+
getResponsePayload,
|
|
53
|
+
invokeClientCommand,
|
|
54
|
+
notify,
|
|
55
|
+
post,
|
|
56
|
+
reportResize,
|
|
57
|
+
requestData,
|
|
58
|
+
resolveMessage,
|
|
59
|
+
setRuntimeText,
|
|
60
|
+
startRemoteBridge
|
|
61
|
+
} from './runtime'
|
|
62
|
+
|
|
63
|
+
type StatusFilter = '' | 'draft' | 'reviewed' | 'archived'
|
|
64
|
+
type Drawing = Record<string, any>
|
|
65
|
+
type DrawingVersion = Record<string, any>
|
|
66
|
+
type ExcalidrawTheme = 'light' | 'dark'
|
|
67
|
+
type DetailPayload = {
|
|
68
|
+
item?: Drawing
|
|
69
|
+
currentVersion?: DrawingVersion | null
|
|
70
|
+
versions?: DrawingVersion[]
|
|
71
|
+
logs?: any[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DEFAULT_MERMAID = `flowchart TD
|
|
75
|
+
A[User Request] --> B[Agent Plans Diagram]
|
|
76
|
+
B --> C{Best Format?}
|
|
77
|
+
C -->|Flow| D[Save Mermaid Draft]
|
|
78
|
+
C -->|Precise Layout| E[Save Excalidraw JSON]
|
|
79
|
+
D --> F[Workbench Converts]
|
|
80
|
+
E --> G[Human Review]
|
|
81
|
+
F --> G`
|
|
82
|
+
|
|
83
|
+
const SAVE_MERMAID_DRAFT_TOOL_NAME = 'excalidraw_save_mermaid_draft'
|
|
84
|
+
|
|
85
|
+
const EXCALIDRAW_TOOL_NAMES = new Set([
|
|
86
|
+
'excalidraw_create_drawing',
|
|
87
|
+
'excalidraw_save_scene_version',
|
|
88
|
+
'excalidraw_patch_scene',
|
|
89
|
+
SAVE_MERMAID_DRAFT_TOOL_NAME,
|
|
90
|
+
'excalidraw_search_drawings',
|
|
91
|
+
'excalidraw_get_drawing',
|
|
92
|
+
'excalidraw_update_drawing_status',
|
|
93
|
+
'excalidraw_report_failure'
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
const EXCALIDRAW_MUTATION_TOOL_NAMES = new Set([
|
|
97
|
+
'excalidraw_create_drawing',
|
|
98
|
+
'excalidraw_save_scene_version',
|
|
99
|
+
'excalidraw_patch_scene',
|
|
100
|
+
SAVE_MERMAID_DRAFT_TOOL_NAME,
|
|
101
|
+
'excalidraw_update_drawing_status',
|
|
102
|
+
'excalidraw_report_failure'
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
const SCENE_APP_STATE_SIGNATURE_KEYS = [
|
|
106
|
+
'viewBackgroundColor',
|
|
107
|
+
'gridSize',
|
|
108
|
+
'objectsSnapModeEnabled',
|
|
109
|
+
'frameRendering'
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
installShadcnThemeVars({ styleId: 'excalidraw-workbench-shadcn-ui-vars' })
|
|
113
|
+
injectStyles()
|
|
114
|
+
|
|
115
|
+
function App() {
|
|
116
|
+
const [context, setContext] = React.useState<any>(null)
|
|
117
|
+
const [drawings, setDrawings] = React.useState<Drawing[]>([])
|
|
118
|
+
const [detail, setDetail] = React.useState<DetailPayload | null>(null)
|
|
119
|
+
const [selectedId, setSelectedId] = React.useState<string>('')
|
|
120
|
+
const [search, setSearch] = React.useState('')
|
|
121
|
+
const [status, setStatus] = React.useState<StatusFilter>('')
|
|
122
|
+
const [busy, setBusy] = React.useState(false)
|
|
123
|
+
const [dirty, setDirty] = React.useState(false)
|
|
124
|
+
const [newTitle, setNewTitle] = React.useState('')
|
|
125
|
+
const [newDescription, setNewDescription] = React.useState('')
|
|
126
|
+
const [changeSummary, setChangeSummary] = React.useState('')
|
|
127
|
+
const [assistantPrompt, setAssistantPrompt] = React.useState('')
|
|
128
|
+
const [mermaidSource, setMermaidSource] = React.useState(DEFAULT_MERMAID)
|
|
129
|
+
const [leftPanelCollapsed, setLeftPanelCollapsed] = React.useState(true)
|
|
130
|
+
const [rightPanelCollapsed, setRightPanelCollapsed] = React.useState(true)
|
|
131
|
+
const [excalidrawTheme, setExcalidrawTheme] = React.useState<ExcalidrawTheme>(() => resolveExcalidrawTheme(null))
|
|
132
|
+
const [api, setApi] = React.useState<any>(null)
|
|
133
|
+
const fileInputRef = React.useRef<HTMLInputElement | null>(null)
|
|
134
|
+
const contextRef = React.useRef<any>(null)
|
|
135
|
+
const selectedIdRef = React.useRef('')
|
|
136
|
+
const searchRef = React.useRef('')
|
|
137
|
+
const statusRef = React.useRef<StatusFilter>('')
|
|
138
|
+
const hostEventSequenceRef = React.useRef(0)
|
|
139
|
+
const pendingMermaidPreviewRef = React.useRef<{ versionId?: string; source: string } | null>(null)
|
|
140
|
+
const excalidrawThemeRef = React.useRef<ExcalidrawTheme>(excalidrawTheme)
|
|
141
|
+
const themeSyncRef = React.useRef(false)
|
|
142
|
+
const elementsRef = React.useRef<any[]>([])
|
|
143
|
+
const appStateRef = React.useRef<Record<string, unknown>>({})
|
|
144
|
+
const filesRef = React.useRef<Record<string, unknown>>({})
|
|
145
|
+
const mermaidSourceRef = React.useRef(DEFAULT_MERMAID)
|
|
146
|
+
const savedSceneSignatureRef = React.useRef('')
|
|
147
|
+
const t = createTranslator(context?.locale)
|
|
148
|
+
|
|
149
|
+
React.useEffect(() => {
|
|
150
|
+
setRuntimeText({
|
|
151
|
+
requestTimeout: t('requestTimeout'),
|
|
152
|
+
remoteRequestFailed: t('remoteRequestFailed'),
|
|
153
|
+
unknownError: t('unknownError')
|
|
154
|
+
})
|
|
155
|
+
}, [context?.locale])
|
|
156
|
+
|
|
157
|
+
React.useEffect(() => {
|
|
158
|
+
selectedIdRef.current = selectedId
|
|
159
|
+
}, [selectedId])
|
|
160
|
+
|
|
161
|
+
React.useEffect(() => {
|
|
162
|
+
searchRef.current = search
|
|
163
|
+
}, [search])
|
|
164
|
+
|
|
165
|
+
React.useEffect(() => {
|
|
166
|
+
statusRef.current = status
|
|
167
|
+
}, [status])
|
|
168
|
+
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
excalidrawThemeRef.current = excalidrawTheme
|
|
171
|
+
}, [excalidrawTheme])
|
|
172
|
+
|
|
173
|
+
React.useEffect(() => {
|
|
174
|
+
mermaidSourceRef.current = mermaidSource
|
|
175
|
+
}, [mermaidSource])
|
|
176
|
+
|
|
177
|
+
React.useEffect(() => {
|
|
178
|
+
const syncTheme = () => setExcalidrawTheme(resolveExcalidrawTheme(contextRef.current?.theme))
|
|
179
|
+
syncTheme()
|
|
180
|
+
|
|
181
|
+
const media = window.matchMedia?.('(prefers-color-scheme: dark)')
|
|
182
|
+
media?.addEventListener?.('change', syncTheme)
|
|
183
|
+
const observer = new MutationObserver(syncTheme)
|
|
184
|
+
observer.observe(document.documentElement, {
|
|
185
|
+
attributes: true,
|
|
186
|
+
attributeFilter: ['class', 'style', 'data-theme', 'data-color-scheme']
|
|
187
|
+
})
|
|
188
|
+
if (document.body) {
|
|
189
|
+
observer.observe(document.body, {
|
|
190
|
+
attributes: true,
|
|
191
|
+
attributeFilter: ['class', 'style', 'data-theme', 'data-color-scheme']
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
media?.removeEventListener?.('change', syncTheme)
|
|
197
|
+
observer.disconnect()
|
|
198
|
+
}
|
|
199
|
+
}, [context?.theme])
|
|
200
|
+
|
|
201
|
+
React.useEffect(() => {
|
|
202
|
+
startRemoteBridge(
|
|
203
|
+
(nextContext) => {
|
|
204
|
+
contextRef.current = nextContext
|
|
205
|
+
setContext(nextContext)
|
|
206
|
+
setExcalidrawTheme(resolveExcalidrawTheme(nextContext?.theme))
|
|
207
|
+
const payload = nextContext.payload || null
|
|
208
|
+
hydratePayload(payload)
|
|
209
|
+
setTimeout(() => reloadList(), 0)
|
|
210
|
+
},
|
|
211
|
+
(event) => {
|
|
212
|
+
void reloadAfterHostEvent(event)
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
post('ready')
|
|
216
|
+
}, [])
|
|
217
|
+
|
|
218
|
+
React.useEffect(reportResize, [drawings, detail, busy, dirty, leftPanelCollapsed, rightPanelCollapsed])
|
|
219
|
+
|
|
220
|
+
React.useEffect(() => {
|
|
221
|
+
if (!api) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
const nextAppState = withHostThemeAppState(appStateRef.current, excalidrawTheme)
|
|
225
|
+
appStateRef.current = nextAppState
|
|
226
|
+
themeSyncRef.current = true
|
|
227
|
+
api.updateScene({
|
|
228
|
+
appState: nextAppState
|
|
229
|
+
})
|
|
230
|
+
window.setTimeout(() => {
|
|
231
|
+
themeSyncRef.current = false
|
|
232
|
+
}, 0)
|
|
233
|
+
}, [api, excalidrawTheme])
|
|
234
|
+
|
|
235
|
+
React.useEffect(() => {
|
|
236
|
+
const currentVersion = detail?.currentVersion
|
|
237
|
+
if (!api || !detail?.item) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (!currentVersion) {
|
|
241
|
+
applyBlankScene({ clearMermaid: true })
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
applyVersion(currentVersion)
|
|
245
|
+
const pendingPreview = pendingMermaidPreviewRef.current
|
|
246
|
+
if (pendingPreview && (!pendingPreview.versionId || pendingPreview.versionId === currentVersion.id)) {
|
|
247
|
+
pendingMermaidPreviewRef.current = null
|
|
248
|
+
void previewMermaidSource(pendingPreview.source, { automatic: true })
|
|
249
|
+
}
|
|
250
|
+
}, [api, detail?.item?.id, detail?.currentVersion?.id])
|
|
251
|
+
|
|
252
|
+
function hydratePayload(payload: any) {
|
|
253
|
+
if (!payload) {
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
if (Array.isArray(payload.items)) {
|
|
257
|
+
setDrawings(payload.items)
|
|
258
|
+
if (!selectedIdRef.current && payload.items[0]?.id) {
|
|
259
|
+
selectDrawing(payload.items[0].id)
|
|
260
|
+
}
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if (payload.item) {
|
|
264
|
+
setDetail(payload)
|
|
265
|
+
setDirty(false)
|
|
266
|
+
const drawingId = payload.item.id || ''
|
|
267
|
+
selectedIdRef.current = drawingId
|
|
268
|
+
setSelectedId(drawingId)
|
|
269
|
+
if (payload.currentVersion?.mermaidSource) {
|
|
270
|
+
updateMermaidSource(payload.currentVersion.mermaidSource)
|
|
271
|
+
} else {
|
|
272
|
+
updateMermaidSource('')
|
|
273
|
+
}
|
|
274
|
+
if (!payload.currentVersion) {
|
|
275
|
+
applyBlankScene({ clearMermaid: true })
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function reloadAfterHostEvent(event: unknown) {
|
|
281
|
+
const toolName = extractToolNameFromHostEvent(event)
|
|
282
|
+
if (toolName && !EXCALIDRAW_TOOL_NAMES.has(toolName)) {
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const sequence = ++hostEventSequenceRef.current
|
|
287
|
+
const eventDrawingId = extractDrawingIdFromHostEvent(event)
|
|
288
|
+
const items = await reloadList()
|
|
289
|
+
if (sequence !== hostEventSequenceRef.current) {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const nextDrawingId = eventDrawingId ?? selectedIdRef.current ?? items[0]?.id
|
|
294
|
+
let selectedPayload: DetailPayload | null = null
|
|
295
|
+
if (nextDrawingId) {
|
|
296
|
+
selectedPayload = await selectDrawing(nextDrawingId)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (toolName === SAVE_MERMAID_DRAFT_TOOL_NAME && selectedPayload?.currentVersion?.mermaidSource) {
|
|
300
|
+
queueMermaidPreview(selectedPayload.currentVersion)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!toolName || EXCALIDRAW_MUTATION_TOOL_NAMES.has(toolName)) {
|
|
305
|
+
const translate = createTranslator(contextRef.current?.locale)
|
|
306
|
+
notify('info', translate('agentDrawingUpdated'))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function reloadList(overrides: Partial<{ search: string; status: StatusFilter }> = {}) {
|
|
311
|
+
const nextSearch = overrides.search ?? searchRef.current
|
|
312
|
+
const nextStatus = overrides.status ?? statusRef.current
|
|
313
|
+
setBusy(true)
|
|
314
|
+
try {
|
|
315
|
+
const response = await requestData({
|
|
316
|
+
page: 1,
|
|
317
|
+
pageSize: 50,
|
|
318
|
+
search: nextSearch,
|
|
319
|
+
parameters: {
|
|
320
|
+
...(nextStatus ? { status: nextStatus } : {})
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
const payload = getResponsePayload(response) || {}
|
|
324
|
+
const items = Array.isArray(payload.items) ? payload.items : []
|
|
325
|
+
setDrawings(items)
|
|
326
|
+
if (!selectedIdRef.current && items[0]?.id) {
|
|
327
|
+
await selectDrawing(items[0].id)
|
|
328
|
+
}
|
|
329
|
+
return items
|
|
330
|
+
} catch (error) {
|
|
331
|
+
notify('error', getErrorMessage(error))
|
|
332
|
+
return []
|
|
333
|
+
} finally {
|
|
334
|
+
setBusy(false)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function selectDrawing(drawingId: string): Promise<DetailPayload | null> {
|
|
339
|
+
if (!drawingId) {
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
setBusy(true)
|
|
343
|
+
try {
|
|
344
|
+
const response = await requestData({
|
|
345
|
+
parameters: {
|
|
346
|
+
drawingId
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
const payload = getResponsePayload(response) || {}
|
|
350
|
+
selectedIdRef.current = drawingId
|
|
351
|
+
setSelectedId(drawingId)
|
|
352
|
+
setDetail(payload)
|
|
353
|
+
setDirty(false)
|
|
354
|
+
setChangeSummary('')
|
|
355
|
+
if (payload.currentVersion?.mermaidSource) {
|
|
356
|
+
updateMermaidSource(payload.currentVersion.mermaidSource)
|
|
357
|
+
} else {
|
|
358
|
+
updateMermaidSource('')
|
|
359
|
+
}
|
|
360
|
+
if (api && payload.currentVersion) {
|
|
361
|
+
applyVersion(payload.currentVersion)
|
|
362
|
+
} else if (api) {
|
|
363
|
+
applyBlankScene({ clearMermaid: true })
|
|
364
|
+
}
|
|
365
|
+
return payload
|
|
366
|
+
} catch (error) {
|
|
367
|
+
notify('error', getErrorMessage(error))
|
|
368
|
+
return null
|
|
369
|
+
} finally {
|
|
370
|
+
setBusy(false)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function createDrawing() {
|
|
375
|
+
const title = newTitle.trim() || t('untitled')
|
|
376
|
+
setBusy(true)
|
|
377
|
+
try {
|
|
378
|
+
const response = await executeAction('create_drawing', null, {
|
|
379
|
+
title,
|
|
380
|
+
description: newDescription
|
|
381
|
+
})
|
|
382
|
+
const result = getResponsePayload(response)
|
|
383
|
+
notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('drawingCreated'))
|
|
384
|
+
const drawingId = result?.item?.id || result?.data?.item?.id
|
|
385
|
+
const drawingItem = result?.item || result?.data?.item || null
|
|
386
|
+
setNewTitle('')
|
|
387
|
+
setNewDescription('')
|
|
388
|
+
setChangeSummary('')
|
|
389
|
+
if (drawingId) {
|
|
390
|
+
selectedIdRef.current = drawingId
|
|
391
|
+
setSelectedId(drawingId)
|
|
392
|
+
setDetail({
|
|
393
|
+
item: drawingItem || { id: drawingId, title, currentVersionNumber: 0, status: 'draft' },
|
|
394
|
+
currentVersion: null,
|
|
395
|
+
versions: [],
|
|
396
|
+
logs: []
|
|
397
|
+
})
|
|
398
|
+
applyBlankScene({ clearMermaid: true })
|
|
399
|
+
}
|
|
400
|
+
await reloadList()
|
|
401
|
+
if (drawingId) {
|
|
402
|
+
await selectDrawing(drawingId)
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
notify('error', getErrorMessage(error))
|
|
406
|
+
} finally {
|
|
407
|
+
setBusy(false)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function saveCurrentScene(sourceAction = 'save_scene_version') {
|
|
412
|
+
if (!selectedId) {
|
|
413
|
+
notify('warning', t('noDrawing'))
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
if (!dirty) {
|
|
417
|
+
notify('info', t('saveNoChanges'))
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
setBusy(true)
|
|
421
|
+
try {
|
|
422
|
+
const scene = currentSerializableScene()
|
|
423
|
+
const response = await executeAction(sourceAction, selectedId, {
|
|
424
|
+
drawingId: selectedId,
|
|
425
|
+
elements: scene.elements,
|
|
426
|
+
appState: scene.appState,
|
|
427
|
+
files: scene.files,
|
|
428
|
+
mermaidSource,
|
|
429
|
+
changeSummary: changeSummary.trim() || undefined
|
|
430
|
+
})
|
|
431
|
+
const result = getResponsePayload(response)
|
|
432
|
+
notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
|
|
433
|
+
markCurrentSceneSaved()
|
|
434
|
+
setChangeSummary('')
|
|
435
|
+
await selectDrawing(selectedId)
|
|
436
|
+
await reloadList()
|
|
437
|
+
} catch (error) {
|
|
438
|
+
notify('error', getErrorMessage(error))
|
|
439
|
+
} finally {
|
|
440
|
+
setBusy(false)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function restoreVersion(versionId: string) {
|
|
445
|
+
if (!selectedId || !versionId) {
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
setBusy(true)
|
|
449
|
+
try {
|
|
450
|
+
const response = await executeAction('restore_version', selectedId, {
|
|
451
|
+
drawingId: selectedId,
|
|
452
|
+
versionId,
|
|
453
|
+
changeSummary: changeSummary.trim() || undefined
|
|
454
|
+
})
|
|
455
|
+
const result = getResponsePayload(response)
|
|
456
|
+
notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
|
|
457
|
+
await selectDrawing(selectedId)
|
|
458
|
+
await reloadList()
|
|
459
|
+
} catch (error) {
|
|
460
|
+
notify('error', getErrorMessage(error))
|
|
461
|
+
} finally {
|
|
462
|
+
setBusy(false)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function archiveDrawing() {
|
|
467
|
+
if (!selectedId) {
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
setBusy(true)
|
|
471
|
+
try {
|
|
472
|
+
await executeAction('archive_drawing', selectedId, {
|
|
473
|
+
drawingId: selectedId
|
|
474
|
+
})
|
|
475
|
+
notify('success', t('operationCompleted'))
|
|
476
|
+
setDetail(null)
|
|
477
|
+
setSelectedId('')
|
|
478
|
+
await reloadList()
|
|
479
|
+
} catch (error) {
|
|
480
|
+
notify('error', getErrorMessage(error))
|
|
481
|
+
} finally {
|
|
482
|
+
setBusy(false)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function setDrawingReviewStatus(nextStatus: 'draft' | 'reviewed') {
|
|
487
|
+
if (!selectedId) {
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
setBusy(true)
|
|
491
|
+
try {
|
|
492
|
+
const response = await executeAction(nextStatus === 'reviewed' ? 'mark_reviewed' : 'mark_draft', selectedId, {
|
|
493
|
+
drawingId: selectedId,
|
|
494
|
+
reason: changeSummary.trim() || undefined
|
|
495
|
+
})
|
|
496
|
+
const result = getResponsePayload(response)
|
|
497
|
+
notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
|
|
498
|
+
setChangeSummary('')
|
|
499
|
+
await selectDrawing(selectedId)
|
|
500
|
+
if (statusRef.current && statusRef.current !== nextStatus) {
|
|
501
|
+
statusRef.current = ''
|
|
502
|
+
setStatus('')
|
|
503
|
+
await reloadList({ status: '' })
|
|
504
|
+
} else {
|
|
505
|
+
await reloadList()
|
|
506
|
+
}
|
|
507
|
+
} catch (error) {
|
|
508
|
+
notify('error', getErrorMessage(error))
|
|
509
|
+
} finally {
|
|
510
|
+
setBusy(false)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function convertMermaid() {
|
|
515
|
+
await previewMermaidSource(mermaidSource)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function previewMermaidSource(sourceValue: string, options: { automatic?: boolean } = {}) {
|
|
519
|
+
const source = sourceValue.trim()
|
|
520
|
+
if (!source || !api) {
|
|
521
|
+
return false
|
|
522
|
+
}
|
|
523
|
+
const translate = createTranslator(contextRef.current?.locale)
|
|
524
|
+
setBusy(true)
|
|
525
|
+
try {
|
|
526
|
+
const result = await parseMermaidToExcalidraw(source, {
|
|
527
|
+
themeVariables: {
|
|
528
|
+
fontSize: '25px'
|
|
529
|
+
},
|
|
530
|
+
maxEdges: 1000,
|
|
531
|
+
maxTextSize: 50000
|
|
532
|
+
})
|
|
533
|
+
const elements = convertToExcalidrawElements(result.elements || [])
|
|
534
|
+
const files = result.files || {}
|
|
535
|
+
const appState = {
|
|
536
|
+
...(appStateRef.current || {}),
|
|
537
|
+
theme: excalidrawThemeRef.current,
|
|
538
|
+
viewBackgroundColor: defaultCanvasBackground(excalidrawThemeRef.current)
|
|
539
|
+
}
|
|
540
|
+
api?.updateScene({
|
|
541
|
+
elements,
|
|
542
|
+
appState
|
|
543
|
+
})
|
|
544
|
+
if (files && api?.addFiles) {
|
|
545
|
+
api.addFiles(Object.values(files))
|
|
546
|
+
}
|
|
547
|
+
updateMermaidSource(source)
|
|
548
|
+
elementsRef.current = elements as any[]
|
|
549
|
+
appStateRef.current = appState
|
|
550
|
+
filesRef.current = files
|
|
551
|
+
updateDirtyState(source)
|
|
552
|
+
notify(options.automatic ? 'info' : 'success', options.automatic ? translate('mermaidAutoPreviewed') : translate('operationCompleted'))
|
|
553
|
+
return true
|
|
554
|
+
} catch (error) {
|
|
555
|
+
notify('error', `${translate('convertFailed')}: ${getErrorMessage(error)}`)
|
|
556
|
+
return false
|
|
557
|
+
} finally {
|
|
558
|
+
setBusy(false)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function queueMermaidPreview(version: DrawingVersion) {
|
|
563
|
+
const source = typeof version.mermaidSource === 'string' ? version.mermaidSource.trim() : ''
|
|
564
|
+
if (!source) {
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
pendingMermaidPreviewRef.current = {
|
|
568
|
+
versionId: typeof version.id === 'string' ? version.id : undefined,
|
|
569
|
+
source
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function sendAssistantPrompt() {
|
|
574
|
+
const prompt = assistantPrompt.trim()
|
|
575
|
+
if (!prompt) {
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
setBusy(true)
|
|
579
|
+
try {
|
|
580
|
+
const response = await executeAction('prepare_agent_draw_message', selectedId || null, {
|
|
581
|
+
drawingId: selectedId || undefined,
|
|
582
|
+
prompt
|
|
583
|
+
})
|
|
584
|
+
const result = getResponsePayload(response)
|
|
585
|
+
const commandKey = result?.data?.commandKey || result?.commandKey
|
|
586
|
+
const payload = result?.data?.payload || result?.payload
|
|
587
|
+
if (commandKey && payload) {
|
|
588
|
+
await invokeClientCommand(commandKey, payload)
|
|
589
|
+
}
|
|
590
|
+
setAssistantPrompt('')
|
|
591
|
+
notify('success', t('operationCompleted'))
|
|
592
|
+
} catch (error) {
|
|
593
|
+
notify('error', getErrorMessage(error))
|
|
594
|
+
} finally {
|
|
595
|
+
setBusy(false)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async function importFile(file: File | null) {
|
|
600
|
+
if (!file) {
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
setBusy(true)
|
|
604
|
+
try {
|
|
605
|
+
const response = await executeFileAction(
|
|
606
|
+
'import_scene_file',
|
|
607
|
+
selectedId || null,
|
|
608
|
+
{
|
|
609
|
+
drawingId: selectedId || undefined,
|
|
610
|
+
title: removeExcalidrawExtension(file.name)
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
drawingId: selectedId || undefined
|
|
614
|
+
},
|
|
615
|
+
file
|
|
616
|
+
)
|
|
617
|
+
const result = getResponsePayload(response)
|
|
618
|
+
notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
|
|
619
|
+
const drawingId = result?.data?.item?.id || result?.item?.id || selectedId
|
|
620
|
+
await reloadList()
|
|
621
|
+
if (drawingId) {
|
|
622
|
+
await selectDrawing(drawingId)
|
|
623
|
+
}
|
|
624
|
+
} catch (error) {
|
|
625
|
+
notify('error', getErrorMessage(error))
|
|
626
|
+
} finally {
|
|
627
|
+
setBusy(false)
|
|
628
|
+
if (fileInputRef.current) {
|
|
629
|
+
fileInputRef.current.value = ''
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function applyVersion(version: DrawingVersion) {
|
|
635
|
+
const elements = Array.isArray(version.elements) ? version.elements : []
|
|
636
|
+
const appState = withHostThemeAppState(isObject(version.appState) ? version.appState : {}, excalidrawThemeRef.current)
|
|
637
|
+
const files = isObject(version.files) ? version.files : {}
|
|
638
|
+
const mermaid = typeof version.mermaidSource === 'string' ? version.mermaidSource : ''
|
|
639
|
+
elementsRef.current = elements
|
|
640
|
+
appStateRef.current = appState
|
|
641
|
+
filesRef.current = files
|
|
642
|
+
updateMermaidSource(mermaid)
|
|
643
|
+
themeSyncRef.current = true
|
|
644
|
+
api?.updateScene({
|
|
645
|
+
elements,
|
|
646
|
+
appState,
|
|
647
|
+
collaborators: new Map()
|
|
648
|
+
})
|
|
649
|
+
if (api?.addFiles && Object.keys(files).length > 0) {
|
|
650
|
+
api.addFiles(Object.values(files))
|
|
651
|
+
}
|
|
652
|
+
window.setTimeout(() => {
|
|
653
|
+
themeSyncRef.current = false
|
|
654
|
+
}, 0)
|
|
655
|
+
markCurrentSceneSaved(mermaid)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function applyBlankScene(options: { clearMermaid?: boolean } = {}) {
|
|
659
|
+
const appState = withHostThemeAppState({}, excalidrawThemeRef.current)
|
|
660
|
+
elementsRef.current = []
|
|
661
|
+
appStateRef.current = appState
|
|
662
|
+
filesRef.current = {}
|
|
663
|
+
if (options.clearMermaid) {
|
|
664
|
+
updateMermaidSource('')
|
|
665
|
+
}
|
|
666
|
+
themeSyncRef.current = true
|
|
667
|
+
api?.updateScene({
|
|
668
|
+
elements: [],
|
|
669
|
+
appState,
|
|
670
|
+
collaborators: new Map()
|
|
671
|
+
})
|
|
672
|
+
window.setTimeout(() => {
|
|
673
|
+
themeSyncRef.current = false
|
|
674
|
+
}, 0)
|
|
675
|
+
markCurrentSceneSaved()
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function updateMermaidSource(nextSource: string, options: { compareDirty?: boolean } = {}) {
|
|
679
|
+
mermaidSourceRef.current = nextSource
|
|
680
|
+
setMermaidSource(nextSource)
|
|
681
|
+
if (options.compareDirty) {
|
|
682
|
+
updateDirtyState(nextSource)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function markCurrentSceneSaved(mermaidOverride = mermaidSourceRef.current) {
|
|
687
|
+
savedSceneSignatureRef.current = createSceneSignature(
|
|
688
|
+
elementsRef.current,
|
|
689
|
+
appStateRef.current,
|
|
690
|
+
filesRef.current,
|
|
691
|
+
mermaidOverride
|
|
692
|
+
)
|
|
693
|
+
setDirty(false)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function updateDirtyState(mermaidOverride = mermaidSourceRef.current) {
|
|
697
|
+
if (!selectedIdRef.current) {
|
|
698
|
+
setDirty(false)
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
const currentSceneSignature = createSceneSignature(
|
|
702
|
+
elementsRef.current,
|
|
703
|
+
appStateRef.current,
|
|
704
|
+
filesRef.current,
|
|
705
|
+
mermaidOverride
|
|
706
|
+
)
|
|
707
|
+
setDirty(currentSceneSignature !== savedSceneSignatureRef.current)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function currentSerializableScene() {
|
|
711
|
+
try {
|
|
712
|
+
const json = serializeAsJSON(elementsRef.current as any, appStateRef.current as any, filesRef.current as any, 'local')
|
|
713
|
+
const parsed = JSON.parse(json)
|
|
714
|
+
return {
|
|
715
|
+
elements: Array.isArray(parsed.elements) ? parsed.elements : elementsRef.current,
|
|
716
|
+
appState: isObject(parsed.appState) ? parsed.appState : appStateRef.current,
|
|
717
|
+
files: isObject(parsed.files) ? parsed.files : filesRef.current
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
return {
|
|
721
|
+
elements: elementsRef.current,
|
|
722
|
+
appState: appStateRef.current,
|
|
723
|
+
files: filesRef.current
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function exportJson() {
|
|
729
|
+
const scene = currentSerializableScene()
|
|
730
|
+
downloadBlob(
|
|
731
|
+
new Blob([JSON.stringify({ type: 'excalidraw', version: 2, source: 'xpert-excalidraw', ...scene }, null, 2)], {
|
|
732
|
+
type: 'application/json'
|
|
733
|
+
}),
|
|
734
|
+
`${detail?.item?.title || 'drawing'}.excalidraw`
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function exportPng() {
|
|
739
|
+
const scene = currentSerializableScene()
|
|
740
|
+
const blob = await exportToBlob({
|
|
741
|
+
elements: scene.elements as any,
|
|
742
|
+
appState: scene.appState as any,
|
|
743
|
+
files: scene.files as any,
|
|
744
|
+
mimeType: 'image/png'
|
|
745
|
+
} as any)
|
|
746
|
+
downloadBlob(blob, `${detail?.item?.title || 'drawing'}.png`)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function exportSvgFile() {
|
|
750
|
+
const scene = currentSerializableScene()
|
|
751
|
+
const svg = await exportToSvg({
|
|
752
|
+
elements: scene.elements as any,
|
|
753
|
+
appState: scene.appState as any,
|
|
754
|
+
files: scene.files as any
|
|
755
|
+
} as any)
|
|
756
|
+
downloadBlob(new Blob([svg.outerHTML], { type: 'image/svg+xml' }), `${detail?.item?.title || 'drawing'}.svg`)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const currentVersion = detail?.currentVersion || null
|
|
760
|
+
const drawingStatus = (detail?.item?.status || 'draft') as StatusFilter
|
|
761
|
+
const canSaveScene = Boolean(selectedId && dirty && !busy)
|
|
762
|
+
const saveButtonTitle = !selectedId ? t('noDrawing') : dirty ? t('saveChanges') : t('saveNoChanges')
|
|
763
|
+
const initialData = {
|
|
764
|
+
elements: currentVersion?.elements || [],
|
|
765
|
+
appState: withHostThemeAppState(
|
|
766
|
+
isObject(currentVersion?.appState) ? currentVersion?.appState : {},
|
|
767
|
+
excalidrawTheme
|
|
768
|
+
),
|
|
769
|
+
files: currentVersion?.files || {}
|
|
770
|
+
}
|
|
771
|
+
const shellClassName = `exw-shell ${leftPanelCollapsed ? 'left-collapsed' : ''} ${rightPanelCollapsed ? 'right-collapsed' : ''}`
|
|
772
|
+
|
|
773
|
+
return (
|
|
774
|
+
<div className={shellClassName}>
|
|
775
|
+
<Sidebar className="exw-sidebar" side="left" collapsed={leftPanelCollapsed}>
|
|
776
|
+
<SidebarHeader>
|
|
777
|
+
<SidebarTrigger
|
|
778
|
+
variant="ghost"
|
|
779
|
+
size="icon"
|
|
780
|
+
aria-label={leftPanelCollapsed ? t('expandDrawings') : t('collapseDrawings')}
|
|
781
|
+
title={leftPanelCollapsed ? t('expandDrawings') : t('collapseDrawings')}
|
|
782
|
+
onClick={() => setLeftPanelCollapsed((value) => !value)}
|
|
783
|
+
>
|
|
784
|
+
{leftPanelCollapsed ? <PanelLeftOpen className="exw-button-icon" aria-hidden="true" /> : <PanelLeftClose className="exw-button-icon" aria-hidden="true" />}
|
|
785
|
+
</SidebarTrigger>
|
|
786
|
+
{!leftPanelCollapsed ? (
|
|
787
|
+
<>
|
|
788
|
+
<SidebarTitle>{t('drawings')}</SidebarTitle>
|
|
789
|
+
<Badge variant="secondary">{drawings.length}</Badge>
|
|
790
|
+
</>
|
|
791
|
+
) : null}
|
|
792
|
+
</SidebarHeader>
|
|
793
|
+
{leftPanelCollapsed ? (
|
|
794
|
+
<SidebarRail>
|
|
795
|
+
<span>{t('drawings')}</span>
|
|
796
|
+
</SidebarRail>
|
|
797
|
+
) : (
|
|
798
|
+
<SidebarContent>
|
|
799
|
+
<div className="exw-sidebar-controls">
|
|
800
|
+
<Input
|
|
801
|
+
value={search}
|
|
802
|
+
placeholder={t('search')}
|
|
803
|
+
onChange={(event: any) => {
|
|
804
|
+
const next = event.target.value
|
|
805
|
+
searchRef.current = next
|
|
806
|
+
setSearch(next)
|
|
807
|
+
reloadList({ search: next })
|
|
808
|
+
}}
|
|
809
|
+
/>
|
|
810
|
+
<Select
|
|
811
|
+
value={status || 'all'}
|
|
812
|
+
onValueChange={(value: string) => {
|
|
813
|
+
const next = value === 'all' ? '' : (value as StatusFilter)
|
|
814
|
+
statusRef.current = next
|
|
815
|
+
setStatus(next)
|
|
816
|
+
reloadList({ status: next })
|
|
817
|
+
}}
|
|
818
|
+
>
|
|
819
|
+
<SelectTrigger aria-label={t('allStatuses')}>
|
|
820
|
+
<SelectValue placeholder={t('allStatuses')} />
|
|
821
|
+
</SelectTrigger>
|
|
822
|
+
<SelectContent>
|
|
823
|
+
<SelectItem value="all">{t('allStatuses')}</SelectItem>
|
|
824
|
+
<SelectItem value="draft">{t('draft')}</SelectItem>
|
|
825
|
+
<SelectItem value="reviewed">{t('reviewed')}</SelectItem>
|
|
826
|
+
<SelectItem value="archived">{t('archived')}</SelectItem>
|
|
827
|
+
</SelectContent>
|
|
828
|
+
</Select>
|
|
829
|
+
</div>
|
|
830
|
+
<ScrollArea className="exw-list">
|
|
831
|
+
<SidebarMenu>
|
|
832
|
+
{drawings.map((drawing) => (
|
|
833
|
+
<SidebarMenuItem key={drawing.id}>
|
|
834
|
+
<SidebarMenuButton type="button" active={drawing.id === selectedId} onClick={() => selectDrawing(drawing.id)}>
|
|
835
|
+
<span className="exw-item-title">{drawing.title || t('untitled')}</span>
|
|
836
|
+
<span className="exw-item-meta">
|
|
837
|
+
v{drawing.currentVersionNumber || 0} · {t((drawing.status || 'draft') as TranslationKey)}
|
|
838
|
+
</span>
|
|
839
|
+
</SidebarMenuButton>
|
|
840
|
+
</SidebarMenuItem>
|
|
841
|
+
))}
|
|
842
|
+
</SidebarMenu>
|
|
843
|
+
</ScrollArea>
|
|
844
|
+
</SidebarContent>
|
|
845
|
+
)}
|
|
846
|
+
</Sidebar>
|
|
847
|
+
|
|
848
|
+
<main className="exw-main">
|
|
849
|
+
<div className="exw-toolbar">
|
|
850
|
+
<div className="exw-toolbar-title">
|
|
851
|
+
<Input
|
|
852
|
+
className="exw-title-input"
|
|
853
|
+
value={newTitle}
|
|
854
|
+
placeholder={t('title')}
|
|
855
|
+
onChange={(event: any) => setNewTitle(event.target.value)}
|
|
856
|
+
/>
|
|
857
|
+
</div>
|
|
858
|
+
<div className="exw-toolbar-actions">
|
|
859
|
+
<Button type="button" variant="outline" size="sm" disabled={busy} onClick={createDrawing}>
|
|
860
|
+
<Plus className="exw-button-icon" aria-hidden="true" />
|
|
861
|
+
{t('newDrawing')}
|
|
862
|
+
</Button>
|
|
863
|
+
<Button
|
|
864
|
+
type="button"
|
|
865
|
+
size="sm"
|
|
866
|
+
disabled={!canSaveScene}
|
|
867
|
+
title={saveButtonTitle}
|
|
868
|
+
aria-label={saveButtonTitle}
|
|
869
|
+
onClick={() => saveCurrentScene()}
|
|
870
|
+
>
|
|
871
|
+
<Save className="exw-button-icon" aria-hidden="true" />
|
|
872
|
+
{t('save')}
|
|
873
|
+
</Button>
|
|
874
|
+
<Button type="button" variant="outline" size="sm" disabled={busy} onClick={() => fileInputRef.current?.click()}>
|
|
875
|
+
<Upload className="exw-button-icon" aria-hidden="true" />
|
|
876
|
+
{t('import')}
|
|
877
|
+
</Button>
|
|
878
|
+
<Button type="button" variant="outline" size="sm" disabled={!selectedId} onClick={exportJson}>
|
|
879
|
+
<FileJson className="exw-button-icon" aria-hidden="true" />
|
|
880
|
+
{t('exportJson')}
|
|
881
|
+
</Button>
|
|
882
|
+
<Button type="button" variant="outline" size="sm" disabled={!selectedId} onClick={exportPng}>
|
|
883
|
+
<Image className="exw-button-icon" aria-hidden="true" />
|
|
884
|
+
{t('exportPng')}
|
|
885
|
+
</Button>
|
|
886
|
+
<Button type="button" variant="outline" size="sm" disabled={!selectedId} onClick={exportSvgFile}>
|
|
887
|
+
<Image className="exw-button-icon" aria-hidden="true" />
|
|
888
|
+
{t('exportSvg')}
|
|
889
|
+
</Button>
|
|
890
|
+
<Badge className="exw-status" variant={dirty ? 'warning' : 'secondary'}>
|
|
891
|
+
{dirty ? t('dirty') : t('saved')}
|
|
892
|
+
</Badge>
|
|
893
|
+
</div>
|
|
894
|
+
<input
|
|
895
|
+
ref={fileInputRef}
|
|
896
|
+
className="exw-hidden-file"
|
|
897
|
+
type="file"
|
|
898
|
+
accept=".excalidraw,.json,application/json"
|
|
899
|
+
onChange={(event: any) => importFile(event.target.files?.[0] || null)}
|
|
900
|
+
/>
|
|
901
|
+
</div>
|
|
902
|
+
<div className="exw-canvas">
|
|
903
|
+
{selectedId || currentVersion ? (
|
|
904
|
+
<Excalidraw
|
|
905
|
+
initialData={initialData as any}
|
|
906
|
+
theme={excalidrawTheme}
|
|
907
|
+
excalidrawAPI={(nextApi: any) => setApi(nextApi)}
|
|
908
|
+
onChange={(elements: any[], appState: Record<string, unknown>, files: Record<string, unknown>) => {
|
|
909
|
+
elementsRef.current = elements || []
|
|
910
|
+
appStateRef.current = appState || {}
|
|
911
|
+
filesRef.current = files || {}
|
|
912
|
+
if (!themeSyncRef.current) {
|
|
913
|
+
updateDirtyState()
|
|
914
|
+
}
|
|
915
|
+
}}
|
|
916
|
+
/>
|
|
917
|
+
) : (
|
|
918
|
+
<div className="exw-empty">{t('noDrawing')}</div>
|
|
919
|
+
)}
|
|
920
|
+
</div>
|
|
921
|
+
</main>
|
|
922
|
+
|
|
923
|
+
<Sidebar className="exw-inspector" side="right" collapsed={rightPanelCollapsed}>
|
|
924
|
+
<SidebarHeader>
|
|
925
|
+
{!rightPanelCollapsed ? (
|
|
926
|
+
<>
|
|
927
|
+
<div className="exw-inspector-actions">
|
|
928
|
+
{drawingStatus === 'archived' ? (
|
|
929
|
+
<Badge variant="secondary">{t('archived')}</Badge>
|
|
930
|
+
) : drawingStatus === 'reviewed' ? (
|
|
931
|
+
<Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDrawingReviewStatus('draft')}>
|
|
932
|
+
<RotateCcw className="exw-button-icon" aria-hidden="true" />
|
|
933
|
+
{t('backToDraft')}
|
|
934
|
+
</Button>
|
|
935
|
+
) : (
|
|
936
|
+
<Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDrawingReviewStatus('reviewed')}>
|
|
937
|
+
<Check className="exw-button-icon" aria-hidden="true" />
|
|
938
|
+
{t('markReviewed')}
|
|
939
|
+
</Button>
|
|
940
|
+
)}
|
|
941
|
+
<Button type="button" variant="destructiveOutline" size="sm" disabled={busy || !selectedId || drawingStatus === 'archived'} onClick={archiveDrawing}>
|
|
942
|
+
<Archive className="exw-button-icon" aria-hidden="true" />
|
|
943
|
+
{t('archive')}
|
|
944
|
+
</Button>
|
|
945
|
+
</div>
|
|
946
|
+
<SidebarTitle className="exw-sidebar-title-truncate">{detail?.item?.title || t('inspector')}</SidebarTitle>
|
|
947
|
+
</>
|
|
948
|
+
) : null}
|
|
949
|
+
<SidebarTrigger
|
|
950
|
+
className="exw-sidebar-trigger-right"
|
|
951
|
+
variant="ghost"
|
|
952
|
+
size="icon"
|
|
953
|
+
aria-label={rightPanelCollapsed ? t('expandInspector') : t('collapseInspector')}
|
|
954
|
+
title={rightPanelCollapsed ? t('expandInspector') : t('collapseInspector')}
|
|
955
|
+
onClick={() => setRightPanelCollapsed((value) => !value)}
|
|
956
|
+
>
|
|
957
|
+
{rightPanelCollapsed ? <PanelRightOpen className="exw-button-icon" aria-hidden="true" /> : <PanelRightClose className="exw-button-icon" aria-hidden="true" />}
|
|
958
|
+
</SidebarTrigger>
|
|
959
|
+
</SidebarHeader>
|
|
960
|
+
{rightPanelCollapsed ? (
|
|
961
|
+
<SidebarRail>
|
|
962
|
+
<span>{t('inspector')}</span>
|
|
963
|
+
</SidebarRail>
|
|
964
|
+
) : (
|
|
965
|
+
<SidebarContent>
|
|
966
|
+
<ScrollArea className="exw-inspector-scroll">
|
|
967
|
+
<div className="exw-inspector-stack">
|
|
968
|
+
<section className="exw-section">
|
|
969
|
+
<div className="exw-section-title">{t('changeSummary')}</div>
|
|
970
|
+
<Input
|
|
971
|
+
value={changeSummary}
|
|
972
|
+
placeholder={t('changeSummary')}
|
|
973
|
+
onChange={(event: any) => setChangeSummary(event.target.value)}
|
|
974
|
+
/>
|
|
975
|
+
</section>
|
|
976
|
+
|
|
977
|
+
<section className="exw-section">
|
|
978
|
+
<div className="exw-section-title">{t('versions')}</div>
|
|
979
|
+
{(detail?.versions || []).map((version) => (
|
|
980
|
+
<div className="exw-version" key={version.id}>
|
|
981
|
+
<div>
|
|
982
|
+
<div>v{version.versionNumber}</div>
|
|
983
|
+
<div className="exw-muted">{version.sourceType || 'workbench'}</div>
|
|
984
|
+
</div>
|
|
985
|
+
<Button
|
|
986
|
+
className="exw-version-action"
|
|
987
|
+
type="button"
|
|
988
|
+
variant="outline"
|
|
989
|
+
size="icon"
|
|
990
|
+
title={t('restore')}
|
|
991
|
+
aria-label={`${t('restore')} v${version.versionNumber}`}
|
|
992
|
+
disabled={busy}
|
|
993
|
+
onClick={() => restoreVersion(version.id)}
|
|
994
|
+
>
|
|
995
|
+
<RotateCcw className="exw-button-icon" aria-hidden="true" />
|
|
996
|
+
</Button>
|
|
997
|
+
</div>
|
|
998
|
+
))}
|
|
999
|
+
</section>
|
|
1000
|
+
|
|
1001
|
+
<section className="exw-section">
|
|
1002
|
+
<div className="exw-section-title">{t('mermaid')}</div>
|
|
1003
|
+
<Textarea value={mermaidSource} onChange={(event: any) => updateMermaidSource(event.target.value, { compareDirty: true })} />
|
|
1004
|
+
<div className="exw-muted">{t('mermaidNotice')}</div>
|
|
1005
|
+
<div className="exw-inline-actions">
|
|
1006
|
+
<Button type="button" variant="outline" size="sm" disabled={busy} onClick={convertMermaid}>
|
|
1007
|
+
{t('convert')}
|
|
1008
|
+
</Button>
|
|
1009
|
+
<Button
|
|
1010
|
+
type="button"
|
|
1011
|
+
size="sm"
|
|
1012
|
+
disabled={!canSaveScene}
|
|
1013
|
+
title={saveButtonTitle}
|
|
1014
|
+
aria-label={saveButtonTitle}
|
|
1015
|
+
onClick={() => saveCurrentScene('save_converted_mermaid_scene')}
|
|
1016
|
+
>
|
|
1017
|
+
{t('saveConverted')}
|
|
1018
|
+
</Button>
|
|
1019
|
+
</div>
|
|
1020
|
+
</section>
|
|
1021
|
+
|
|
1022
|
+
<section className="exw-section">
|
|
1023
|
+
<div className="exw-section-title">{t('drawingRequest')}</div>
|
|
1024
|
+
<Textarea
|
|
1025
|
+
value={assistantPrompt}
|
|
1026
|
+
placeholder={t('drawingRequest')}
|
|
1027
|
+
onChange={(event: any) => setAssistantPrompt(event.target.value)}
|
|
1028
|
+
/>
|
|
1029
|
+
<Button type="button" disabled={busy || !assistantPrompt.trim()} onClick={sendAssistantPrompt}>
|
|
1030
|
+
<Send className="exw-button-icon" aria-hidden="true" />
|
|
1031
|
+
{t('askAssistant')}
|
|
1032
|
+
</Button>
|
|
1033
|
+
</section>
|
|
1034
|
+
|
|
1035
|
+
<section className="exw-section">
|
|
1036
|
+
<div className="exw-section-title">{t('description')}</div>
|
|
1037
|
+
<Textarea
|
|
1038
|
+
value={newDescription}
|
|
1039
|
+
placeholder={t('description')}
|
|
1040
|
+
onChange={(event: any) => setNewDescription(event.target.value)}
|
|
1041
|
+
/>
|
|
1042
|
+
</section>
|
|
1043
|
+
</div>
|
|
1044
|
+
</ScrollArea>
|
|
1045
|
+
</SidebarContent>
|
|
1046
|
+
)}
|
|
1047
|
+
</Sidebar>
|
|
1048
|
+
</div>
|
|
1049
|
+
)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function withHostThemeAppState(appState: Record<string, unknown>, theme: ExcalidrawTheme) {
|
|
1053
|
+
return {
|
|
1054
|
+
...appState,
|
|
1055
|
+
theme,
|
|
1056
|
+
viewBackgroundColor: typeof appState.viewBackgroundColor === 'string' && appState.viewBackgroundColor
|
|
1057
|
+
? appState.viewBackgroundColor
|
|
1058
|
+
: defaultCanvasBackground(theme)
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function defaultCanvasBackground(theme: ExcalidrawTheme) {
|
|
1063
|
+
return theme === 'dark' ? '#121212' : '#ffffff'
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function resolveExcalidrawTheme(hostTheme: unknown): ExcalidrawTheme {
|
|
1067
|
+
const explicitTheme = normalizeThemeInput(hostTheme)
|
|
1068
|
+
if (explicitTheme) {
|
|
1069
|
+
return explicitTheme
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const documentTheme = normalizeThemeInput(document.documentElement.dataset.theme)
|
|
1073
|
+
?? normalizeThemeInput(document.documentElement.dataset.colorScheme)
|
|
1074
|
+
?? normalizeThemeInput(document.body?.dataset.theme)
|
|
1075
|
+
?? normalizeThemeInput(document.body?.dataset.colorScheme)
|
|
1076
|
+
?? normalizeThemeInput(document.documentElement.className)
|
|
1077
|
+
?? normalizeThemeInput(document.body?.className)
|
|
1078
|
+
if (documentTheme) {
|
|
1079
|
+
return documentTheme
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const backgroundTheme = themeFromCssColor(readCssColor('--xps-background') || readCssColor('--xui-color-background'))
|
|
1083
|
+
if (backgroundTheme) {
|
|
1084
|
+
return backgroundTheme
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function normalizeThemeInput(value: unknown): ExcalidrawTheme | null {
|
|
1091
|
+
if (typeof value === 'boolean') {
|
|
1092
|
+
return value ? 'dark' : 'light'
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof value === 'string') {
|
|
1095
|
+
const normalized = value.toLowerCase()
|
|
1096
|
+
if (normalized.includes('dark') || normalized.includes('night')) {
|
|
1097
|
+
return 'dark'
|
|
1098
|
+
}
|
|
1099
|
+
if (normalized.includes('light') || normalized.includes('day')) {
|
|
1100
|
+
return 'light'
|
|
1101
|
+
}
|
|
1102
|
+
return null
|
|
1103
|
+
}
|
|
1104
|
+
if (!isObject(value)) {
|
|
1105
|
+
return null
|
|
1106
|
+
}
|
|
1107
|
+
if (value.isDark === true || value.dark === true) {
|
|
1108
|
+
return 'dark'
|
|
1109
|
+
}
|
|
1110
|
+
if (value.isDark === false || value.dark === false) {
|
|
1111
|
+
return 'light'
|
|
1112
|
+
}
|
|
1113
|
+
for (const key of ['mode', 'theme', 'colorScheme', 'appearance', 'name', 'type']) {
|
|
1114
|
+
const resolved = normalizeThemeInput(value[key])
|
|
1115
|
+
if (resolved) {
|
|
1116
|
+
return resolved
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return null
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function readCssColor(variableName: string) {
|
|
1123
|
+
if (typeof window === 'undefined') {
|
|
1124
|
+
return ''
|
|
1125
|
+
}
|
|
1126
|
+
return getComputedStyle(document.documentElement).getPropertyValue(variableName).trim()
|
|
1127
|
+
|| getComputedStyle(document.body).getPropertyValue(variableName).trim()
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function themeFromCssColor(color: string): ExcalidrawTheme | null {
|
|
1131
|
+
const rgb = parseCssColor(color)
|
|
1132
|
+
if (!rgb) {
|
|
1133
|
+
return null
|
|
1134
|
+
}
|
|
1135
|
+
const [r, g, b] = rgb.map((value) => {
|
|
1136
|
+
const normalized = value / 255
|
|
1137
|
+
return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4)
|
|
1138
|
+
})
|
|
1139
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
1140
|
+
return luminance < 0.36 ? 'dark' : 'light'
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function parseCssColor(color: string): [number, number, number] | null {
|
|
1144
|
+
const trimmed = color.trim()
|
|
1145
|
+
if (!trimmed) {
|
|
1146
|
+
return null
|
|
1147
|
+
}
|
|
1148
|
+
const hex = trimmed.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)
|
|
1149
|
+
if (hex) {
|
|
1150
|
+
const value = hex[1].length === 3
|
|
1151
|
+
? hex[1].split('').map((part) => part + part).join('')
|
|
1152
|
+
: hex[1]
|
|
1153
|
+
return [
|
|
1154
|
+
Number.parseInt(value.slice(0, 2), 16),
|
|
1155
|
+
Number.parseInt(value.slice(2, 4), 16),
|
|
1156
|
+
Number.parseInt(value.slice(4, 6), 16)
|
|
1157
|
+
]
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const rgb = trimmed.match(/^rgba?\(([^)]+)\)$/i)
|
|
1161
|
+
if (rgb) {
|
|
1162
|
+
const parts = rgb[1].split(',').slice(0, 3).map((part) => Number.parseFloat(part.trim()))
|
|
1163
|
+
if (parts.length === 3 && parts.every((part) => Number.isFinite(part))) {
|
|
1164
|
+
return parts as [number, number, number]
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return null
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function createSceneSignature(
|
|
1171
|
+
elements: unknown[],
|
|
1172
|
+
appState: Record<string, unknown>,
|
|
1173
|
+
files: Record<string, unknown>,
|
|
1174
|
+
mermaidSource: string
|
|
1175
|
+
) {
|
|
1176
|
+
const comparableAppState = SCENE_APP_STATE_SIGNATURE_KEYS.reduce<Record<string, unknown>>((acc, key) => {
|
|
1177
|
+
if (Object.prototype.hasOwnProperty.call(appState, key)) {
|
|
1178
|
+
acc[key] = appState[key]
|
|
1179
|
+
}
|
|
1180
|
+
return acc
|
|
1181
|
+
}, {})
|
|
1182
|
+
return stableStringify({
|
|
1183
|
+
elements,
|
|
1184
|
+
appState: comparableAppState,
|
|
1185
|
+
files,
|
|
1186
|
+
mermaidSource: mermaidSource.replace(/\r\n/g, '\n')
|
|
1187
|
+
})
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function stableStringify(value: unknown) {
|
|
1191
|
+
return JSON.stringify(normalizeJsonValue(value))
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function normalizeJsonValue(value: unknown, seen = new WeakSet<object>()): unknown {
|
|
1195
|
+
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
|
1196
|
+
return undefined
|
|
1197
|
+
}
|
|
1198
|
+
if (value === null || typeof value !== 'object') {
|
|
1199
|
+
return value
|
|
1200
|
+
}
|
|
1201
|
+
if (value instanceof Date) {
|
|
1202
|
+
return value.toISOString()
|
|
1203
|
+
}
|
|
1204
|
+
if (seen.has(value)) {
|
|
1205
|
+
return '[Circular]'
|
|
1206
|
+
}
|
|
1207
|
+
seen.add(value)
|
|
1208
|
+
if (Array.isArray(value)) {
|
|
1209
|
+
return value.map((item) => {
|
|
1210
|
+
const normalized = normalizeJsonValue(item, seen)
|
|
1211
|
+
return normalized === undefined ? null : normalized
|
|
1212
|
+
})
|
|
1213
|
+
}
|
|
1214
|
+
if (value instanceof Map) {
|
|
1215
|
+
return Array.from(value.entries())
|
|
1216
|
+
.map(([key, mapValue]) => [String(key), normalizeJsonValue(mapValue, seen)] as const)
|
|
1217
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1218
|
+
}
|
|
1219
|
+
if (value instanceof Set) {
|
|
1220
|
+
return Array.from(value.values()).map((item) => normalizeJsonValue(item, seen))
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return Object.keys(value as Record<string, unknown>)
|
|
1224
|
+
.sort()
|
|
1225
|
+
.reduce<Record<string, unknown>>((acc, key) => {
|
|
1226
|
+
const normalized = normalizeJsonValue((value as Record<string, unknown>)[key], seen)
|
|
1227
|
+
if (normalized !== undefined) {
|
|
1228
|
+
acc[key] = normalized
|
|
1229
|
+
}
|
|
1230
|
+
return acc
|
|
1231
|
+
}, {})
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
1235
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function extractToolNameFromHostEvent(event: unknown) {
|
|
1239
|
+
for (const candidate of expandHostEventCandidates(event)) {
|
|
1240
|
+
if (!isObject(candidate)) {
|
|
1241
|
+
continue
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const direct = readString(candidate, 'toolName') ?? readString(candidate, 'tool_name') ?? readString(candidate, 'name')
|
|
1245
|
+
if (direct && EXCALIDRAW_TOOL_NAMES.has(direct)) {
|
|
1246
|
+
return direct
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const tool = candidate.tool
|
|
1250
|
+
if (isObject(tool)) {
|
|
1251
|
+
const toolName = readString(tool, 'name') ?? readString(tool, 'toolName') ?? readString(tool, 'tool_name')
|
|
1252
|
+
if (toolName && EXCALIDRAW_TOOL_NAMES.has(toolName)) {
|
|
1253
|
+
return toolName
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const toolCall = candidate.toolCall ?? candidate.tool_call
|
|
1258
|
+
if (isObject(toolCall)) {
|
|
1259
|
+
const toolName =
|
|
1260
|
+
readString(toolCall, 'name') ??
|
|
1261
|
+
readString(toolCall, 'toolName') ??
|
|
1262
|
+
readString(toolCall, 'tool_name') ??
|
|
1263
|
+
(isObject(toolCall.function) ? readString(toolCall.function, 'name') : null)
|
|
1264
|
+
if (toolName && EXCALIDRAW_TOOL_NAMES.has(toolName)) {
|
|
1265
|
+
return toolName
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
return null
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function extractDrawingIdFromHostEvent(event: unknown) {
|
|
1273
|
+
for (const candidate of expandHostEventCandidates(event)) {
|
|
1274
|
+
if (!isObject(candidate)) {
|
|
1275
|
+
continue
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const direct = readString(candidate, 'drawingId') ?? readString(candidate, 'drawing_id')
|
|
1279
|
+
if (direct) {
|
|
1280
|
+
return direct
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (isObject(candidate.item)) {
|
|
1284
|
+
const itemId = readString(candidate.item, 'id')
|
|
1285
|
+
if (itemId) {
|
|
1286
|
+
return itemId
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (isObject(candidate.drawing)) {
|
|
1291
|
+
const drawingId =
|
|
1292
|
+
readString(candidate.drawing, 'drawingId') ??
|
|
1293
|
+
readString(candidate.drawing, 'drawing_id') ??
|
|
1294
|
+
readString(candidate.drawing, 'id') ??
|
|
1295
|
+
(isObject(candidate.drawing.item) ? readString(candidate.drawing.item, 'id') : null)
|
|
1296
|
+
if (drawingId) {
|
|
1297
|
+
return drawingId
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (isObject(candidate.version)) {
|
|
1302
|
+
const drawingId = readString(candidate.version, 'drawingId') ?? readString(candidate.version, 'drawing_id')
|
|
1303
|
+
if (drawingId) {
|
|
1304
|
+
return drawingId
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (isObject(candidate.log)) {
|
|
1309
|
+
const drawingId = readString(candidate.log, 'drawingId') ?? readString(candidate.log, 'drawing_id')
|
|
1310
|
+
if (drawingId) {
|
|
1311
|
+
return drawingId
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return null
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function expandHostEventCandidates(event: unknown) {
|
|
1319
|
+
const candidates: unknown[] = []
|
|
1320
|
+
collectHostEventCandidates(event, candidates, 0, new WeakSet<object>())
|
|
1321
|
+
return candidates
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function collectHostEventCandidates(value: unknown, candidates: unknown[], depth: number, seen: WeakSet<object>) {
|
|
1325
|
+
if (depth > 5 || value == null) {
|
|
1326
|
+
return
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const normalized = parseJsonLike(value)
|
|
1330
|
+
if ((isObject(normalized) || Array.isArray(normalized)) && seen.has(normalized)) {
|
|
1331
|
+
return
|
|
1332
|
+
}
|
|
1333
|
+
if (isObject(normalized) || Array.isArray(normalized)) {
|
|
1334
|
+
seen.add(normalized)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
candidates.push(normalized)
|
|
1338
|
+
|
|
1339
|
+
if (Array.isArray(normalized)) {
|
|
1340
|
+
normalized.forEach((item) => collectHostEventCandidates(item, candidates, depth + 1, seen))
|
|
1341
|
+
return
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (!isObject(normalized)) {
|
|
1345
|
+
return
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
;[
|
|
1349
|
+
'payload',
|
|
1350
|
+
'metadata',
|
|
1351
|
+
'data',
|
|
1352
|
+
'result',
|
|
1353
|
+
'output',
|
|
1354
|
+
'outputs',
|
|
1355
|
+
'content',
|
|
1356
|
+
'text',
|
|
1357
|
+
'message',
|
|
1358
|
+
'detail',
|
|
1359
|
+
'response',
|
|
1360
|
+
'toolResult',
|
|
1361
|
+
'returnValue',
|
|
1362
|
+
'artifact',
|
|
1363
|
+
'tool',
|
|
1364
|
+
'toolCall',
|
|
1365
|
+
'tool_call',
|
|
1366
|
+
'function',
|
|
1367
|
+
'arguments',
|
|
1368
|
+
'args',
|
|
1369
|
+
'input'
|
|
1370
|
+
].forEach((key) => collectHostEventCandidates(normalized[key], candidates, depth + 1, seen))
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function parseJsonLike(value: unknown) {
|
|
1374
|
+
if (typeof value !== 'string') {
|
|
1375
|
+
return value
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const trimmed = value.trim()
|
|
1379
|
+
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
|
|
1380
|
+
return value
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
try {
|
|
1384
|
+
return JSON.parse(trimmed)
|
|
1385
|
+
} catch {
|
|
1386
|
+
return value
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function readString(record: Record<string, unknown>, key: string) {
|
|
1391
|
+
const value = record[key]
|
|
1392
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function removeExcalidrawExtension(name: string) {
|
|
1396
|
+
return name.replace(/\.excalidraw(?:\.json)?$/i, '').replace(/\.json$/i, '') || name
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function downloadBlob(blob: Blob, fileName: string) {
|
|
1400
|
+
const url = URL.createObjectURL(blob)
|
|
1401
|
+
const anchor = document.createElement('a')
|
|
1402
|
+
anchor.href = url
|
|
1403
|
+
anchor.download = fileName
|
|
1404
|
+
document.body.appendChild(anchor)
|
|
1405
|
+
anchor.click()
|
|
1406
|
+
anchor.remove()
|
|
1407
|
+
URL.revokeObjectURL(url)
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const root = ReactDOM.createRoot(document.getElementById('root'))
|
|
1411
|
+
root.render(<App />)
|