@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.
Files changed (100) hide show
  1. package/.xpertai-plugin/plugin.json +118 -0
  2. package/README.md +5 -0
  3. package/assets/composerIcon.svg +5 -0
  4. package/assets/logo.svg +8 -0
  5. package/dist/docs/excalidraw-agent-skill.md +32 -0
  6. package/dist/index.d.ts +14 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +153 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/constants.d.ts +24 -0
  11. package/dist/lib/constants.d.ts.map +1 -0
  12. package/dist/lib/constants.js +42 -0
  13. package/dist/lib/constants.js.map +1 -0
  14. package/dist/lib/entities/excalidraw-action-log.entity.d.ts +18 -0
  15. package/dist/lib/entities/excalidraw-action-log.entity.d.ts.map +1 -0
  16. package/dist/lib/entities/excalidraw-action-log.entity.js +69 -0
  17. package/dist/lib/entities/excalidraw-action-log.entity.js.map +1 -0
  18. package/dist/lib/entities/excalidraw-drawing-version.entity.d.ts +21 -0
  19. package/dist/lib/entities/excalidraw-drawing-version.entity.d.ts.map +1 -0
  20. package/dist/lib/entities/excalidraw-drawing-version.entity.js +82 -0
  21. package/dist/lib/entities/excalidraw-drawing-version.entity.js.map +1 -0
  22. package/dist/lib/entities/excalidraw-drawing.entity.d.ts +24 -0
  23. package/dist/lib/entities/excalidraw-drawing.entity.d.ts.map +1 -0
  24. package/dist/lib/entities/excalidraw-drawing.entity.js +94 -0
  25. package/dist/lib/entities/excalidraw-drawing.entity.js.map +1 -0
  26. package/dist/lib/entities/index.d.ts +4 -0
  27. package/dist/lib/entities/index.d.ts.map +1 -0
  28. package/dist/lib/entities/index.js +4 -0
  29. package/dist/lib/entities/index.js.map +1 -0
  30. package/dist/lib/excalidraw-view.provider.d.ts +14 -0
  31. package/dist/lib/excalidraw-view.provider.d.ts.map +1 -0
  32. package/dist/lib/excalidraw-view.provider.js +423 -0
  33. package/dist/lib/excalidraw-view.provider.js.map +1 -0
  34. package/dist/lib/excalidraw.middleware.d.ts +10 -0
  35. package/dist/lib/excalidraw.middleware.d.ts.map +1 -0
  36. package/dist/lib/excalidraw.middleware.js +173 -0
  37. package/dist/lib/excalidraw.middleware.js.map +1 -0
  38. package/dist/lib/excalidraw.plugin.d.ts +8 -0
  39. package/dist/lib/excalidraw.plugin.d.ts.map +1 -0
  40. package/dist/lib/excalidraw.plugin.js +27 -0
  41. package/dist/lib/excalidraw.plugin.js.map +1 -0
  42. package/dist/lib/excalidraw.service.d.ts +169 -0
  43. package/dist/lib/excalidraw.service.d.ts.map +1 -0
  44. package/dist/lib/excalidraw.service.js +441 -0
  45. package/dist/lib/excalidraw.service.js.map +1 -0
  46. package/dist/lib/excalidraw.templates.d.ts +3 -0
  47. package/dist/lib/excalidraw.templates.d.ts.map +1 -0
  48. package/dist/lib/excalidraw.templates.js +78 -0
  49. package/dist/lib/excalidraw.templates.js.map +1 -0
  50. package/dist/lib/remote-components/excalidraw-workbench/app.css +1 -0
  51. package/dist/lib/remote-components/excalidraw-workbench/app.js +5105 -0
  52. package/dist/lib/remote-components/excalidraw-workbench/src/i18n.d.ts +3 -0
  53. package/dist/lib/remote-components/excalidraw-workbench/src/i18n.d.ts.map +1 -0
  54. package/dist/lib/remote-components/excalidraw-workbench/src/i18n.js +103 -0
  55. package/dist/lib/remote-components/excalidraw-workbench/src/i18n.js.map +1 -0
  56. package/dist/lib/remote-components/excalidraw-workbench/src/i18n.ts +151 -0
  57. package/dist/lib/remote-components/excalidraw-workbench/src/main.tsx +1411 -0
  58. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.d.ts +3 -0
  59. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.d.ts.map +1 -0
  60. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.js +4 -0
  61. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.js.map +1 -0
  62. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-client-shim.ts +4 -0
  63. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.d.ts +11 -0
  64. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.d.ts.map +1 -0
  65. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.js +11 -0
  66. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.js.map +1 -0
  67. package/dist/lib/remote-components/excalidraw-workbench/src/react-dom-shim.ts +11 -0
  68. package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.d.ts +5 -0
  69. package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.d.ts.map +1 -0
  70. package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.js +8 -0
  71. package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.js.map +1 -0
  72. package/dist/lib/remote-components/excalidraw-workbench/src/react-jsx-runtime-shim.ts +8 -0
  73. package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.d.ts +36 -0
  74. package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.d.ts.map +1 -0
  75. package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.js +36 -0
  76. package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.js.map +1 -0
  77. package/dist/lib/remote-components/excalidraw-workbench/src/react-shim.ts +36 -0
  78. package/dist/lib/remote-components/excalidraw-workbench/src/runtime.d.ts +21 -0
  79. package/dist/lib/remote-components/excalidraw-workbench/src/runtime.d.ts.map +1 -0
  80. package/dist/lib/remote-components/excalidraw-workbench/src/runtime.js +198 -0
  81. package/dist/lib/remote-components/excalidraw-workbench/src/runtime.js.map +1 -0
  82. package/dist/lib/remote-components/excalidraw-workbench/src/runtime.ts +228 -0
  83. package/dist/lib/remote-components/excalidraw-workbench/src/styles.d.ts +2 -0
  84. package/dist/lib/remote-components/excalidraw-workbench/src/styles.d.ts.map +1 -0
  85. package/dist/lib/remote-components/excalidraw-workbench/src/styles.js +324 -0
  86. package/dist/lib/remote-components/excalidraw-workbench/src/styles.js.map +1 -0
  87. package/dist/lib/remote-components/excalidraw-workbench/src/styles.ts +323 -0
  88. package/dist/lib/remote-components/excalidraw-workbench/src/vendor.d.ts +4 -0
  89. package/dist/lib/remote-components/excalidraw-workbench/src/vendor.d.ts.map +1 -0
  90. package/dist/lib/remote-components/excalidraw-workbench/src/vendor.js +4 -0
  91. package/dist/lib/remote-components/excalidraw-workbench/src/vendor.js.map +1 -0
  92. package/dist/lib/remote-components/excalidraw-workbench/src/vendor.ts +3 -0
  93. package/dist/lib/types.d.ts +74 -0
  94. package/dist/lib/types.d.ts.map +1 -0
  95. package/dist/lib/types.js +2 -0
  96. package/dist/lib/types.js.map +1 -0
  97. package/dist/xpert-excalidraw-assistant.yaml +129 -0
  98. package/package.json +87 -0
  99. package/skills/index/SKILL.md +46 -0
  100. 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 />)