@xpert-ai/plugin-lucidchart 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 +10 -0
  5. package/dist/docs/lucidchart-agent-skill.md +46 -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 +25 -0
  11. package/dist/lib/constants.d.ts.map +1 -0
  12. package/dist/lib/constants.js +44 -0
  13. package/dist/lib/constants.js.map +1 -0
  14. package/dist/lib/entities/index.d.ts +4 -0
  15. package/dist/lib/entities/index.d.ts.map +1 -0
  16. package/dist/lib/entities/index.js +4 -0
  17. package/dist/lib/entities/index.js.map +1 -0
  18. package/dist/lib/entities/lucidchart-action-log.entity.d.ts +18 -0
  19. package/dist/lib/entities/lucidchart-action-log.entity.d.ts.map +1 -0
  20. package/dist/lib/entities/lucidchart-action-log.entity.js +69 -0
  21. package/dist/lib/entities/lucidchart-action-log.entity.js.map +1 -0
  22. package/dist/lib/entities/lucidchart-document-version.entity.d.ts +26 -0
  23. package/dist/lib/entities/lucidchart-document-version.entity.d.ts.map +1 -0
  24. package/dist/lib/entities/lucidchart-document-version.entity.js +102 -0
  25. package/dist/lib/entities/lucidchart-document-version.entity.js.map +1 -0
  26. package/dist/lib/entities/lucidchart-document.entity.d.ts +30 -0
  27. package/dist/lib/entities/lucidchart-document.entity.d.ts.map +1 -0
  28. package/dist/lib/entities/lucidchart-document.entity.js +118 -0
  29. package/dist/lib/entities/lucidchart-document.entity.js.map +1 -0
  30. package/dist/lib/lucidchart-view.provider.d.ts +14 -0
  31. package/dist/lib/lucidchart-view.provider.d.ts.map +1 -0
  32. package/dist/lib/lucidchart-view.provider.js +460 -0
  33. package/dist/lib/lucidchart-view.provider.js.map +1 -0
  34. package/dist/lib/lucidchart.middleware.d.ts +10 -0
  35. package/dist/lib/lucidchart.middleware.d.ts.map +1 -0
  36. package/dist/lib/lucidchart.middleware.js +193 -0
  37. package/dist/lib/lucidchart.middleware.js.map +1 -0
  38. package/dist/lib/lucidchart.plugin.d.ts +8 -0
  39. package/dist/lib/lucidchart.plugin.d.ts.map +1 -0
  40. package/dist/lib/lucidchart.plugin.js +27 -0
  41. package/dist/lib/lucidchart.plugin.js.map +1 -0
  42. package/dist/lib/lucidchart.service.d.ts +208 -0
  43. package/dist/lib/lucidchart.service.d.ts.map +1 -0
  44. package/dist/lib/lucidchart.service.js +548 -0
  45. package/dist/lib/lucidchart.service.js.map +1 -0
  46. package/dist/lib/lucidchart.templates.d.ts +3 -0
  47. package/dist/lib/lucidchart.templates.d.ts.map +1 -0
  48. package/dist/lib/lucidchart.templates.js +78 -0
  49. package/dist/lib/lucidchart.templates.js.map +1 -0
  50. package/dist/lib/remote-components/lucidchart-workbench/app.css +0 -0
  51. package/dist/lib/remote-components/lucidchart-workbench/app.js +1245 -0
  52. package/dist/lib/remote-components/lucidchart-workbench/src/i18n.d.ts +3 -0
  53. package/dist/lib/remote-components/lucidchart-workbench/src/i18n.d.ts.map +1 -0
  54. package/dist/lib/remote-components/lucidchart-workbench/src/i18n.js +115 -0
  55. package/dist/lib/remote-components/lucidchart-workbench/src/i18n.js.map +1 -0
  56. package/dist/lib/remote-components/lucidchart-workbench/src/i18n.ts +169 -0
  57. package/dist/lib/remote-components/lucidchart-workbench/src/main.tsx +1514 -0
  58. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.d.ts +3 -0
  59. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.d.ts.map +1 -0
  60. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.js +4 -0
  61. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.js.map +1 -0
  62. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.ts +4 -0
  63. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.d.ts +11 -0
  64. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.d.ts.map +1 -0
  65. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.js +11 -0
  66. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.js.map +1 -0
  67. package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.ts +11 -0
  68. package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.d.ts +5 -0
  69. package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.d.ts.map +1 -0
  70. package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.js +8 -0
  71. package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.js.map +1 -0
  72. package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.ts +8 -0
  73. package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.d.ts +36 -0
  74. package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.d.ts.map +1 -0
  75. package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.js +36 -0
  76. package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.js.map +1 -0
  77. package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.ts +36 -0
  78. package/dist/lib/remote-components/lucidchart-workbench/src/runtime.d.ts +21 -0
  79. package/dist/lib/remote-components/lucidchart-workbench/src/runtime.d.ts.map +1 -0
  80. package/dist/lib/remote-components/lucidchart-workbench/src/runtime.js +198 -0
  81. package/dist/lib/remote-components/lucidchart-workbench/src/runtime.js.map +1 -0
  82. package/dist/lib/remote-components/lucidchart-workbench/src/runtime.ts +228 -0
  83. package/dist/lib/remote-components/lucidchart-workbench/src/styles.d.ts +2 -0
  84. package/dist/lib/remote-components/lucidchart-workbench/src/styles.d.ts.map +1 -0
  85. package/dist/lib/remote-components/lucidchart-workbench/src/styles.js +383 -0
  86. package/dist/lib/remote-components/lucidchart-workbench/src/styles.js.map +1 -0
  87. package/dist/lib/remote-components/lucidchart-workbench/src/styles.ts +382 -0
  88. package/dist/lib/remote-components/lucidchart-workbench/src/vendor.d.ts +4 -0
  89. package/dist/lib/remote-components/lucidchart-workbench/src/vendor.d.ts.map +1 -0
  90. package/dist/lib/remote-components/lucidchart-workbench/src/vendor.js +4 -0
  91. package/dist/lib/remote-components/lucidchart-workbench/src/vendor.js.map +1 -0
  92. package/dist/lib/remote-components/lucidchart-workbench/src/vendor.ts +3 -0
  93. package/dist/lib/types.d.ts +87 -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-lucidchart-assistant.yaml +137 -0
  98. package/package.json +85 -0
  99. package/skills/index/SKILL.md +46 -0
  100. package/skills/index/agents/xpertai.yaml +6 -0
@@ -0,0 +1,1514 @@
1
+ import {
2
+ Archive,
3
+ Badge,
4
+ Button,
5
+ Check,
6
+ Download,
7
+ FileJson,
8
+ Input,
9
+ PanelLeftClose,
10
+ PanelLeftOpen,
11
+ PanelRightClose,
12
+ PanelRightOpen,
13
+ Plus,
14
+ RotateCcw,
15
+ Save,
16
+ ScrollArea,
17
+ Select,
18
+ SelectContent,
19
+ SelectItem,
20
+ SelectTrigger,
21
+ SelectValue,
22
+ Send,
23
+ Sidebar,
24
+ SidebarContent,
25
+ SidebarHeader,
26
+ SidebarMenu,
27
+ SidebarMenuButton,
28
+ SidebarMenuItem,
29
+ SidebarRail,
30
+ SidebarTitle,
31
+ SidebarTrigger,
32
+ Textarea,
33
+ Upload,
34
+ installShadcnThemeVars
35
+ } from '@xpert-ai/plugin-shadcn-ui'
36
+ import { React, ReactDOM, h } from './vendor'
37
+ import { createTranslator, TranslationKey } from './i18n'
38
+ import { injectStyles } from './styles'
39
+ import {
40
+ executeAction,
41
+ executeFileAction,
42
+ getErrorMessage,
43
+ getResponsePayload,
44
+ invokeClientCommand,
45
+ notify,
46
+ post,
47
+ reportResize,
48
+ requestData,
49
+ resolveMessage,
50
+ setRuntimeText,
51
+ startRemoteBridge
52
+ } from './runtime'
53
+
54
+ type StatusFilter = '' | 'draft' | 'reviewed' | 'archived'
55
+ type DocumentRecord = Record<string, any>
56
+ type DocumentVersion = Record<string, any>
57
+ type DetailPayload = {
58
+ item?: DocumentRecord
59
+ currentVersion?: DocumentVersion | null
60
+ versions?: DocumentVersion[]
61
+ logs?: any[]
62
+ }
63
+ type PreviewShape = {
64
+ id: string
65
+ x: number
66
+ y: number
67
+ w: number
68
+ h: number
69
+ text: string
70
+ type: string
71
+ fillColor: string
72
+ strokeColor: string
73
+ strokeWidth: number
74
+ cornerRadius: number
75
+ }
76
+ type PreviewLine = {
77
+ id: string
78
+ x1: number
79
+ y1: number
80
+ x2: number
81
+ y2: number
82
+ text: string
83
+ strokeColor: string
84
+ strokeWidth: number
85
+ }
86
+ type StandardImportPreviewModel = {
87
+ shapes: PreviewShape[]
88
+ lines: PreviewLine[]
89
+ viewBox: string
90
+ }
91
+
92
+ const DEFAULT_MERMAID = `flowchart TD
93
+ A[User Request] --> B[Agent Plans Lucidchart Draft]
94
+ B --> C{Best Path?}
95
+ C -->|Structured import| D[Save Standard Import]
96
+ C -->|Still exploring| E[Save Mermaid Draft]
97
+ C -->|Already in Lucid| F[Register Lucid URL]`
98
+
99
+ const LUCIDCHART_TOOL_NAMES = new Set([
100
+ 'lucidchart_create_document',
101
+ 'lucidchart_save_standard_import_version',
102
+ 'lucidchart_patch_standard_import',
103
+ 'lucidchart_save_mermaid_draft',
104
+ 'lucidchart_register_external_document',
105
+ 'lucidchart_search_documents',
106
+ 'lucidchart_get_document',
107
+ 'lucidchart_update_document_status',
108
+ 'lucidchart_report_failure'
109
+ ])
110
+
111
+ const LUCIDCHART_MUTATION_TOOL_NAMES = new Set([
112
+ 'lucidchart_create_document',
113
+ 'lucidchart_save_standard_import_version',
114
+ 'lucidchart_patch_standard_import',
115
+ 'lucidchart_save_mermaid_draft',
116
+ 'lucidchart_register_external_document',
117
+ 'lucidchart_update_document_status',
118
+ 'lucidchart_report_failure'
119
+ ])
120
+
121
+ installShadcnThemeVars({ styleId: 'lucidchart-workbench-shadcn-ui-vars' })
122
+ injectStyles()
123
+
124
+ function App() {
125
+ const [context, setContext] = React.useState<any>(null)
126
+ const [documents, setDocuments] = React.useState<DocumentRecord[]>([])
127
+ const [detail, setDetail] = React.useState<DetailPayload | null>(null)
128
+ const [selectedId, setSelectedId] = React.useState('')
129
+ const [search, setSearch] = React.useState('')
130
+ const [status, setStatus] = React.useState<StatusFilter>('')
131
+ const [busy, setBusy] = React.useState(false)
132
+ const [dirty, setDirty] = React.useState(false)
133
+ const [newTitle, setNewTitle] = React.useState('')
134
+ const [newDescription, setNewDescription] = React.useState('')
135
+ const [changeSummary, setChangeSummary] = React.useState('')
136
+ const [assistantPrompt, setAssistantPrompt] = React.useState('')
137
+ const [standardImportText, setStandardImportText] = React.useState(() => stringifyJson(createDefaultStandardImport('Untitled')))
138
+ const [mermaidSource, setMermaidSource] = React.useState(DEFAULT_MERMAID)
139
+ const [lucidDocumentId, setLucidDocumentId] = React.useState('')
140
+ const [lucidDocumentUrl, setLucidDocumentUrl] = React.useState('')
141
+ const [embedUrl, setEmbedUrl] = React.useState('')
142
+ const [previewUrl, setPreviewUrl] = React.useState('')
143
+ const [leftPanelCollapsed, setLeftPanelCollapsed] = React.useState(true)
144
+ const [rightPanelCollapsed, setRightPanelCollapsed] = React.useState(true)
145
+ const fileInputRef = React.useRef<HTMLInputElement | null>(null)
146
+ const contextRef = React.useRef<any>(null)
147
+ const selectedIdRef = React.useRef('')
148
+ const searchRef = React.useRef('')
149
+ const statusRef = React.useRef<StatusFilter>('')
150
+ const hostEventSequenceRef = React.useRef(0)
151
+ const savedSignatureRef = React.useRef('')
152
+ const t = createTranslator(context?.locale)
153
+
154
+ React.useEffect(() => {
155
+ setRuntimeText({
156
+ requestTimeout: t('requestTimeout'),
157
+ remoteRequestFailed: t('remoteRequestFailed'),
158
+ unknownError: t('unknownError')
159
+ })
160
+ }, [context?.locale])
161
+
162
+ React.useEffect(() => {
163
+ selectedIdRef.current = selectedId
164
+ }, [selectedId])
165
+
166
+ React.useEffect(() => {
167
+ searchRef.current = search
168
+ }, [search])
169
+
170
+ React.useEffect(() => {
171
+ statusRef.current = status
172
+ }, [status])
173
+
174
+ React.useEffect(() => {
175
+ startRemoteBridge(
176
+ (nextContext) => {
177
+ contextRef.current = nextContext
178
+ setContext(nextContext)
179
+ hydratePayload(nextContext.payload || null)
180
+ setTimeout(() => reloadList(), 0)
181
+ },
182
+ (event) => {
183
+ void reloadAfterHostEvent(event)
184
+ }
185
+ )
186
+ post('ready')
187
+ }, [])
188
+
189
+ React.useEffect(reportResize, [documents, detail, busy, dirty, leftPanelCollapsed, rightPanelCollapsed])
190
+
191
+ function hydratePayload(payload: any) {
192
+ if (!payload) {
193
+ return
194
+ }
195
+ if (Array.isArray(payload.items)) {
196
+ setDocuments(payload.items)
197
+ if (!selectedIdRef.current && payload.items[0]?.id) {
198
+ selectDocument(payload.items[0].id)
199
+ }
200
+ return
201
+ }
202
+ if (payload.item) {
203
+ applyDetailPayload(payload)
204
+ }
205
+ }
206
+
207
+ function applyDetailPayload(payload: DetailPayload) {
208
+ setDetail(payload)
209
+ const documentId = payload.item?.id || ''
210
+ selectedIdRef.current = documentId
211
+ setSelectedId(documentId)
212
+ setChangeSummary('')
213
+ const version = payload.currentVersion || null
214
+ const title = payload.item?.title || t('untitled')
215
+ const standardImport = isObject(version?.standardImport) ? version?.standardImport : createDefaultStandardImport(title)
216
+ const nextText = stringifyJson(standardImport)
217
+ setStandardImportText(nextText)
218
+ const nextMermaidSource = typeof version?.mermaidSource === 'string' ? version.mermaidSource : ''
219
+ const nextLucidDocumentId = firstString(version?.lucidDocumentId, payload.item?.lucidDocumentId)
220
+ const nextLucidDocumentUrl = firstString(version?.lucidDocumentUrl, payload.item?.lucidDocumentUrl)
221
+ const nextEmbedUrl = firstString(version?.embedUrl, payload.item?.embedUrl)
222
+ const nextPreviewUrl = firstString(version?.previewUrl, payload.item?.previewUrl)
223
+ setMermaidSource(nextMermaidSource)
224
+ setLucidDocumentId(nextLucidDocumentId)
225
+ setLucidDocumentUrl(nextLucidDocumentUrl)
226
+ setEmbedUrl(nextEmbedUrl)
227
+ setPreviewUrl(nextPreviewUrl)
228
+ savedSignatureRef.current = createSignatureFromValues(
229
+ nextText,
230
+ nextMermaidSource,
231
+ nextLucidDocumentId,
232
+ nextLucidDocumentUrl,
233
+ nextEmbedUrl,
234
+ nextPreviewUrl
235
+ )
236
+ setDirty(false)
237
+ }
238
+
239
+ async function reloadAfterHostEvent(event: unknown) {
240
+ const toolName = extractToolNameFromHostEvent(event)
241
+ if (toolName && !LUCIDCHART_TOOL_NAMES.has(toolName)) {
242
+ return
243
+ }
244
+
245
+ const sequence = ++hostEventSequenceRef.current
246
+ const eventDocumentId = extractDocumentIdFromHostEvent(event)
247
+ const items = await reloadList()
248
+ if (sequence !== hostEventSequenceRef.current) {
249
+ return
250
+ }
251
+
252
+ const shouldPreferNewest =
253
+ !eventDocumentId &&
254
+ (toolName === 'lucidchart_create_document' || (toolName === 'lucidchart_save_mermaid_draft' && !selectedIdRef.current))
255
+ const nextDocumentId = eventDocumentId ?? (shouldPreferNewest ? items[0]?.id : selectedIdRef.current) ?? items[0]?.id
256
+ if (nextDocumentId) {
257
+ await selectDocument(nextDocumentId)
258
+ }
259
+
260
+ if (!toolName || LUCIDCHART_MUTATION_TOOL_NAMES.has(toolName)) {
261
+ notify('info', createTranslator(contextRef.current?.locale)('agentDocumentUpdated'))
262
+ }
263
+ }
264
+
265
+ async function reloadList(overrides: Partial<{ search: string; status: StatusFilter }> = {}) {
266
+ const nextSearch = overrides.search ?? searchRef.current
267
+ const nextStatus = overrides.status ?? statusRef.current
268
+ setBusy(true)
269
+ try {
270
+ const response = await requestData({
271
+ page: 1,
272
+ pageSize: 50,
273
+ search: nextSearch,
274
+ parameters: {
275
+ ...(nextStatus ? { status: nextStatus } : {})
276
+ }
277
+ })
278
+ const payload = getResponsePayload(response) || {}
279
+ const items = Array.isArray(payload.items) ? payload.items : []
280
+ setDocuments(items)
281
+ if (!selectedIdRef.current && items[0]?.id) {
282
+ await selectDocument(items[0].id)
283
+ }
284
+ return items
285
+ } catch (error) {
286
+ notify('error', getErrorMessage(error))
287
+ return []
288
+ } finally {
289
+ setBusy(false)
290
+ }
291
+ }
292
+
293
+ async function selectDocument(documentId: string): Promise<DetailPayload | null> {
294
+ if (!documentId) {
295
+ return null
296
+ }
297
+ setBusy(true)
298
+ try {
299
+ const response = await requestData({ parameters: { documentId } })
300
+ const payload = getResponsePayload(response) || {}
301
+ applyDetailPayload(payload)
302
+ return payload
303
+ } catch (error) {
304
+ notify('error', getErrorMessage(error))
305
+ return null
306
+ } finally {
307
+ setBusy(false)
308
+ }
309
+ }
310
+
311
+ async function createDocument() {
312
+ const title = newTitle.trim() || t('untitled')
313
+ setBusy(true)
314
+ try {
315
+ const response = await executeAction('create_document', null, {
316
+ title,
317
+ description: newDescription
318
+ })
319
+ const result = getResponsePayload(response)
320
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('documentCreated'))
321
+ const documentId = result?.item?.id || result?.data?.item?.id
322
+ setNewTitle('')
323
+ setNewDescription('')
324
+ setChangeSummary('')
325
+ if (documentId) {
326
+ await reloadList()
327
+ await selectDocument(documentId)
328
+ } else {
329
+ await reloadList()
330
+ }
331
+ } catch (error) {
332
+ notify('error', getErrorMessage(error))
333
+ } finally {
334
+ setBusy(false)
335
+ }
336
+ }
337
+
338
+ async function saveStandardImport() {
339
+ if (!selectedId) {
340
+ notify('warning', t('noDocument'))
341
+ return
342
+ }
343
+ let standardImport: Record<string, unknown>
344
+ try {
345
+ standardImport = JSON.parse(standardImportText)
346
+ if (!isObject(standardImport)) {
347
+ throw new Error(t('invalidJson'))
348
+ }
349
+ } catch (error) {
350
+ notify('error', `${t('invalidJson')}: ${getErrorMessage(error)}`)
351
+ return
352
+ }
353
+ setBusy(true)
354
+ try {
355
+ const response = await executeAction('save_standard_import_version', selectedId, {
356
+ documentId: selectedId,
357
+ standardImport,
358
+ mermaidSource: mermaidSource.trim() || undefined,
359
+ lucidDocumentId: lucidDocumentId.trim() || undefined,
360
+ lucidDocumentUrl: lucidDocumentUrl.trim() || undefined,
361
+ embedUrl: embedUrl.trim() || undefined,
362
+ previewUrl: previewUrl.trim() || undefined,
363
+ product: 'lucidchart',
364
+ importFileName: `${detail?.item?.title || 'document'}.json`,
365
+ changeSummary: changeSummary.trim() || undefined
366
+ })
367
+ const result = getResponsePayload(response)
368
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
369
+ setChangeSummary('')
370
+ await selectDocument(selectedId)
371
+ await reloadList()
372
+ } catch (error) {
373
+ notify('error', getErrorMessage(error))
374
+ } finally {
375
+ setBusy(false)
376
+ }
377
+ }
378
+
379
+ async function saveMermaidDraft() {
380
+ const source = mermaidSource.trim()
381
+ if (!source) {
382
+ return
383
+ }
384
+ setBusy(true)
385
+ try {
386
+ const response = await executeAction('save_mermaid_draft', selectedId || null, {
387
+ documentId: selectedId || undefined,
388
+ title: newTitle.trim() || detail?.item?.title || t('untitled'),
389
+ description: newDescription,
390
+ mermaidSource: source,
391
+ changeSummary: changeSummary.trim() || undefined
392
+ })
393
+ const result = getResponsePayload(response)
394
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
395
+ const documentId = result?.document?.item?.id || result?.data?.document?.item?.id || selectedId
396
+ setChangeSummary('')
397
+ await reloadList()
398
+ if (documentId) {
399
+ await selectDocument(documentId)
400
+ }
401
+ } catch (error) {
402
+ notify('error', getErrorMessage(error))
403
+ } finally {
404
+ setBusy(false)
405
+ }
406
+ }
407
+
408
+ async function registerExternalDocument() {
409
+ if (!selectedId && !newTitle.trim()) {
410
+ notify('warning', t('noDocument'))
411
+ return
412
+ }
413
+ setBusy(true)
414
+ try {
415
+ const response = await executeAction('register_external_document', selectedId || null, {
416
+ documentId: selectedId || undefined,
417
+ title: newTitle.trim() || detail?.item?.title || t('untitled'),
418
+ description: newDescription,
419
+ lucidDocumentId: lucidDocumentId.trim() || undefined,
420
+ lucidDocumentUrl: lucidDocumentUrl.trim() || undefined,
421
+ embedUrl: embedUrl.trim() || undefined,
422
+ previewUrl: previewUrl.trim() || undefined,
423
+ product: 'lucidchart',
424
+ changeSummary: changeSummary.trim() || undefined
425
+ })
426
+ const result = getResponsePayload(response)
427
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
428
+ const documentId = result?.document?.item?.id || result?.data?.document?.item?.id || selectedId
429
+ setChangeSummary('')
430
+ await reloadList()
431
+ if (documentId) {
432
+ await selectDocument(documentId)
433
+ }
434
+ } catch (error) {
435
+ notify('error', getErrorMessage(error))
436
+ } finally {
437
+ setBusy(false)
438
+ }
439
+ }
440
+
441
+ async function restoreVersion(versionId: string) {
442
+ if (!selectedId || !versionId) {
443
+ return
444
+ }
445
+ setBusy(true)
446
+ try {
447
+ const response = await executeAction('restore_version', selectedId, {
448
+ documentId: selectedId,
449
+ versionId,
450
+ changeSummary: changeSummary.trim() || undefined
451
+ })
452
+ const result = getResponsePayload(response)
453
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
454
+ await selectDocument(selectedId)
455
+ await reloadList()
456
+ } catch (error) {
457
+ notify('error', getErrorMessage(error))
458
+ } finally {
459
+ setBusy(false)
460
+ }
461
+ }
462
+
463
+ async function archiveDocument() {
464
+ if (!selectedId) {
465
+ return
466
+ }
467
+ setBusy(true)
468
+ try {
469
+ await executeAction('archive_document', selectedId, { documentId: selectedId })
470
+ notify('success', t('operationCompleted'))
471
+ setDetail(null)
472
+ setSelectedId('')
473
+ await reloadList()
474
+ } catch (error) {
475
+ notify('error', getErrorMessage(error))
476
+ } finally {
477
+ setBusy(false)
478
+ }
479
+ }
480
+
481
+ async function setDocumentReviewStatus(nextStatus: 'draft' | 'reviewed') {
482
+ if (!selectedId) {
483
+ return
484
+ }
485
+ setBusy(true)
486
+ try {
487
+ const response = await executeAction(nextStatus === 'reviewed' ? 'mark_reviewed' : 'mark_draft', selectedId, {
488
+ documentId: selectedId,
489
+ reason: changeSummary.trim() || undefined
490
+ })
491
+ const result = getResponsePayload(response)
492
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
493
+ setChangeSummary('')
494
+ await selectDocument(selectedId)
495
+ await reloadList(statusRef.current && statusRef.current !== nextStatus ? { status: '' } : {})
496
+ if (statusRef.current && statusRef.current !== nextStatus) {
497
+ statusRef.current = ''
498
+ setStatus('')
499
+ }
500
+ } catch (error) {
501
+ notify('error', getErrorMessage(error))
502
+ } finally {
503
+ setBusy(false)
504
+ }
505
+ }
506
+
507
+ async function sendAssistantPrompt() {
508
+ const prompt = assistantPrompt.trim()
509
+ if (!prompt) {
510
+ return
511
+ }
512
+ setBusy(true)
513
+ try {
514
+ const response = await executeAction('prepare_agent_draw_message', selectedId || null, {
515
+ documentId: selectedId || undefined,
516
+ prompt
517
+ })
518
+ const result = getResponsePayload(response)
519
+ const commandKey = result?.data?.commandKey || result?.commandKey
520
+ const payload = result?.data?.payload || result?.payload
521
+ if (commandKey && payload) {
522
+ await invokeClientCommand(commandKey, payload)
523
+ }
524
+ setAssistantPrompt('')
525
+ notify('success', t('operationCompleted'))
526
+ } catch (error) {
527
+ notify('error', getErrorMessage(error))
528
+ } finally {
529
+ setBusy(false)
530
+ }
531
+ }
532
+
533
+ async function importFile(file: File | null) {
534
+ if (!file) {
535
+ return
536
+ }
537
+ setBusy(true)
538
+ try {
539
+ const response = await executeFileAction(
540
+ 'import_standard_import_file',
541
+ selectedId || null,
542
+ {
543
+ documentId: selectedId || undefined,
544
+ title: removeLucidchartExtension(file.name)
545
+ },
546
+ { documentId: selectedId || undefined },
547
+ file
548
+ )
549
+ const result = getResponsePayload(response)
550
+ notify('success', resolveMessage(result?.message, contextRef.current?.locale) || t('operationCompleted'))
551
+ const documentId = result?.data?.item?.id || result?.item?.id || selectedId
552
+ await reloadList()
553
+ if (documentId) {
554
+ await selectDocument(documentId)
555
+ }
556
+ } catch (error) {
557
+ notify('error', getErrorMessage(error))
558
+ } finally {
559
+ setBusy(false)
560
+ if (fileInputRef.current) {
561
+ fileInputRef.current.value = ''
562
+ }
563
+ }
564
+ }
565
+
566
+ function updateStandardImportText(nextText: string) {
567
+ setStandardImportText(nextText)
568
+ updateDirtyState({ standardImportText: nextText })
569
+ }
570
+
571
+ function updateMermaidSource(nextSource: string) {
572
+ setMermaidSource(nextSource)
573
+ updateDirtyState({ mermaidSource: nextSource })
574
+ }
575
+
576
+ function updateLucidDocumentUrl(nextUrl: string) {
577
+ setLucidDocumentUrl(nextUrl)
578
+ updateDirtyState({ lucidDocumentUrl: nextUrl })
579
+ }
580
+
581
+ function updateEmbedUrl(nextUrl: string) {
582
+ setEmbedUrl(nextUrl)
583
+ updateDirtyState({ embedUrl: nextUrl })
584
+ }
585
+
586
+ function updateLucidDocumentId(nextId: string) {
587
+ setLucidDocumentId(nextId)
588
+ updateDirtyState({ lucidDocumentId: nextId })
589
+ }
590
+
591
+ function updatePreviewUrl(nextUrl: string) {
592
+ setPreviewUrl(nextUrl)
593
+ updateDirtyState({ previewUrl: nextUrl })
594
+ }
595
+
596
+ function updateDirtyState(overrides: Partial<{
597
+ standardImportText: string
598
+ mermaidSource: string
599
+ lucidDocumentId: string
600
+ lucidDocumentUrl: string
601
+ embedUrl: string
602
+ previewUrl: string
603
+ }> = {}) {
604
+ if (!selectedIdRef.current) {
605
+ setDirty(false)
606
+ return
607
+ }
608
+ const nextSignature = createSignatureFromValues(
609
+ overrides.standardImportText ?? standardImportText,
610
+ overrides.mermaidSource ?? mermaidSource,
611
+ overrides.lucidDocumentId ?? lucidDocumentId,
612
+ overrides.lucidDocumentUrl ?? lucidDocumentUrl,
613
+ overrides.embedUrl ?? embedUrl,
614
+ overrides.previewUrl ?? previewUrl
615
+ )
616
+ setDirty(nextSignature !== savedSignatureRef.current)
617
+ }
618
+
619
+ function exportJson() {
620
+ try {
621
+ const parsed = JSON.parse(standardImportText)
622
+ downloadBlob(
623
+ new Blob([JSON.stringify(parsed, null, 2)], { type: 'application/json' }),
624
+ `${detail?.item?.title || 'document'}.json`
625
+ )
626
+ } catch (error) {
627
+ notify('error', `${t('invalidJson')}: ${getErrorMessage(error)}`)
628
+ }
629
+ }
630
+
631
+ const currentVersion = detail?.currentVersion || null
632
+ const documentStatus = (detail?.item?.status || 'draft') as StatusFilter
633
+ const embeddableUrl = embedUrl.trim()
634
+ const imagePreviewUrl = previewUrl.trim()
635
+ const lucidOpenUrl = embeddableUrl || lucidDocumentUrl.trim()
636
+ const standardImportPreview = React.useMemo(() => createStandardImportPreview(standardImportText), [standardImportText])
637
+ const canSave = Boolean(selectedId && dirty && !busy)
638
+ const shellClassName = `lw-shell ${leftPanelCollapsed ? 'left-collapsed' : ''} ${rightPanelCollapsed ? 'right-collapsed' : ''}`
639
+ const previewBadge = embeddableUrl
640
+ ? t('embedPreview')
641
+ : imagePreviewUrl
642
+ ? t('imagePreview')
643
+ : standardImportPreview
644
+ ? t('standardImportPreview')
645
+ : t('saved')
646
+
647
+ return (
648
+ <div className={shellClassName}>
649
+ <Sidebar className="lw-sidebar" side="left" collapsed={leftPanelCollapsed}>
650
+ <SidebarHeader>
651
+ <SidebarTrigger
652
+ variant="ghost"
653
+ size="icon"
654
+ aria-label={leftPanelCollapsed ? t('expandDocuments') : t('collapseDocuments')}
655
+ title={leftPanelCollapsed ? t('expandDocuments') : t('collapseDocuments')}
656
+ onClick={() => setLeftPanelCollapsed((value) => !value)}
657
+ >
658
+ {leftPanelCollapsed ? <PanelLeftOpen className="lw-button-icon" aria-hidden="true" /> : <PanelLeftClose className="lw-button-icon" aria-hidden="true" />}
659
+ </SidebarTrigger>
660
+ {!leftPanelCollapsed ? (
661
+ <>
662
+ <SidebarTitle>{t('documents')}</SidebarTitle>
663
+ <Badge variant="secondary">{documents.length}</Badge>
664
+ </>
665
+ ) : null}
666
+ </SidebarHeader>
667
+ {leftPanelCollapsed ? (
668
+ <SidebarRail><span>{t('documents')}</span></SidebarRail>
669
+ ) : (
670
+ <SidebarContent>
671
+ <div className="lw-sidebar-controls">
672
+ <Input
673
+ value={search}
674
+ placeholder={t('search')}
675
+ onChange={(event: any) => {
676
+ const next = event.target.value
677
+ searchRef.current = next
678
+ setSearch(next)
679
+ reloadList({ search: next })
680
+ }}
681
+ />
682
+ <Select
683
+ value={status || 'all'}
684
+ onValueChange={(value: string) => {
685
+ const next = value === 'all' ? '' : (value as StatusFilter)
686
+ statusRef.current = next
687
+ setStatus(next)
688
+ reloadList({ status: next })
689
+ }}
690
+ >
691
+ <SelectTrigger aria-label={t('allStatuses')}>
692
+ <SelectValue placeholder={t('allStatuses')} />
693
+ </SelectTrigger>
694
+ <SelectContent>
695
+ <SelectItem value="all">{t('allStatuses')}</SelectItem>
696
+ <SelectItem value="draft">{t('draft')}</SelectItem>
697
+ <SelectItem value="reviewed">{t('reviewed')}</SelectItem>
698
+ <SelectItem value="archived">{t('archived')}</SelectItem>
699
+ </SelectContent>
700
+ </Select>
701
+ </div>
702
+ <ScrollArea className="lw-list">
703
+ <SidebarMenu>
704
+ {documents.map((document) => (
705
+ <SidebarMenuItem key={document.id}>
706
+ <SidebarMenuButton type="button" active={document.id === selectedId} onClick={() => selectDocument(document.id)}>
707
+ <span className="lw-item-title">{document.title || t('untitled')}</span>
708
+ <span className="lw-item-meta">
709
+ v{document.currentVersionNumber || 0} · {t((document.status || 'draft') as TranslationKey)}
710
+ </span>
711
+ </SidebarMenuButton>
712
+ </SidebarMenuItem>
713
+ ))}
714
+ </SidebarMenu>
715
+ </ScrollArea>
716
+ </SidebarContent>
717
+ )}
718
+ </Sidebar>
719
+
720
+ <main className="lw-main">
721
+ <div className="lw-toolbar">
722
+ <div className="lw-toolbar-title">
723
+ <Input className="lw-title-input" value={newTitle} placeholder={t('title')} onChange={(event: any) => setNewTitle(event.target.value)} />
724
+ </div>
725
+ <div className="lw-toolbar-actions">
726
+ <Button type="button" variant="outline" size="sm" disabled={busy} onClick={createDocument}>
727
+ <Plus className="lw-button-icon" aria-hidden="true" />
728
+ {t('newDocument')}
729
+ </Button>
730
+ <Button type="button" size="sm" disabled={!canSave} onClick={saveStandardImport}>
731
+ <Save className="lw-button-icon" aria-hidden="true" />
732
+ {t('save')}
733
+ </Button>
734
+ <Button type="button" variant="outline" size="sm" disabled={busy} onClick={() => fileInputRef.current?.click()}>
735
+ <Upload className="lw-button-icon" aria-hidden="true" />
736
+ {t('import')}
737
+ </Button>
738
+ <Button type="button" variant="outline" size="sm" disabled={!selectedId} onClick={exportJson}>
739
+ <Download className="lw-button-icon" aria-hidden="true" />
740
+ {t('exportJson')}
741
+ </Button>
742
+ <Button type="button" variant="outline" size="sm" disabled={!lucidOpenUrl} onClick={() => window.open(lucidOpenUrl, '_blank', 'noopener,noreferrer')}>
743
+ <FileJson className="lw-button-icon" aria-hidden="true" />
744
+ {t('openLucid')}
745
+ </Button>
746
+ <Badge className="lw-status" variant={dirty ? 'warning' : 'secondary'}>
747
+ {dirty ? t('dirty') : t('saved')}
748
+ </Badge>
749
+ </div>
750
+ <input
751
+ ref={fileInputRef}
752
+ className="lw-hidden-file"
753
+ type="file"
754
+ accept=".json,application/json"
755
+ onChange={(event: any) => importFile(event.target.files?.[0] || null)}
756
+ />
757
+ </div>
758
+ <div className="lw-stage">
759
+ {selectedId || detail?.item ? (
760
+ <div className="lw-editor-pane">
761
+ <div className="lw-editor-header">
762
+ <Badge variant="secondary">{t('standardImport')}</Badge>
763
+ {currentVersion?.sourceType ? <Badge variant="secondary">{currentVersion.sourceType}</Badge> : null}
764
+ <Badge variant={embeddableUrl || imagePreviewUrl || standardImportPreview ? 'success' : 'secondary'}>{previewBadge}</Badge>
765
+ </div>
766
+ <div className="lw-visual-frame">
767
+ {embeddableUrl ? (
768
+ <iframe title="Lucidchart embed" src={embeddableUrl} />
769
+ ) : imagePreviewUrl ? (
770
+ <img src={imagePreviewUrl} alt={t('imagePreview')} />
771
+ ) : standardImportPreview ? (
772
+ <StandardImportPreview model={standardImportPreview} />
773
+ ) : (
774
+ <div className="lw-embed-empty">{t('previewUnavailable')}</div>
775
+ )}
776
+ </div>
777
+ <Textarea
778
+ className="lw-json-editor"
779
+ value={standardImportText}
780
+ onChange={(event: any) => updateStandardImportText(event.target.value)}
781
+ />
782
+ </div>
783
+ ) : (
784
+ <div className="lw-empty">{t('noDocument')}</div>
785
+ )}
786
+ </div>
787
+ </main>
788
+
789
+ <Sidebar className="lw-inspector" side="right" collapsed={rightPanelCollapsed}>
790
+ <SidebarHeader>
791
+ {!rightPanelCollapsed ? (
792
+ <>
793
+ <div className="lw-inspector-actions">
794
+ {documentStatus === 'archived' ? (
795
+ <Badge variant="secondary">{t('archived')}</Badge>
796
+ ) : documentStatus === 'reviewed' ? (
797
+ <Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDocumentReviewStatus('draft')}>
798
+ <RotateCcw className="lw-button-icon" aria-hidden="true" />
799
+ {t('backToDraft')}
800
+ </Button>
801
+ ) : (
802
+ <Button type="button" variant="outline" size="sm" disabled={busy || !selectedId} onClick={() => setDocumentReviewStatus('reviewed')}>
803
+ <Check className="lw-button-icon" aria-hidden="true" />
804
+ {t('markReviewed')}
805
+ </Button>
806
+ )}
807
+ <Button type="button" variant="destructiveOutline" size="sm" disabled={busy || !selectedId || documentStatus === 'archived'} onClick={archiveDocument}>
808
+ <Archive className="lw-button-icon" aria-hidden="true" />
809
+ {t('archive')}
810
+ </Button>
811
+ </div>
812
+ <SidebarTitle className="lw-sidebar-title-truncate">{detail?.item?.title || t('inspector')}</SidebarTitle>
813
+ </>
814
+ ) : null}
815
+ <SidebarTrigger
816
+ className="lw-sidebar-trigger-right"
817
+ variant="ghost"
818
+ size="icon"
819
+ aria-label={rightPanelCollapsed ? t('expandInspector') : t('collapseInspector')}
820
+ title={rightPanelCollapsed ? t('expandInspector') : t('collapseInspector')}
821
+ onClick={() => setRightPanelCollapsed((value) => !value)}
822
+ >
823
+ {rightPanelCollapsed ? <PanelRightOpen className="lw-button-icon" aria-hidden="true" /> : <PanelRightClose className="lw-button-icon" aria-hidden="true" />}
824
+ </SidebarTrigger>
825
+ </SidebarHeader>
826
+ {rightPanelCollapsed ? (
827
+ <SidebarRail><span>{t('inspector')}</span></SidebarRail>
828
+ ) : (
829
+ <SidebarContent>
830
+ <ScrollArea className="lw-inspector-scroll">
831
+ <div className="lw-inspector-stack">
832
+ <section className="lw-section">
833
+ <div className="lw-section-title">{t('changeSummary')}</div>
834
+ <Input value={changeSummary} placeholder={t('changeSummary')} onChange={(event: any) => setChangeSummary(event.target.value)} />
835
+ </section>
836
+
837
+ <section className="lw-section">
838
+ <div className="lw-section-title">{t('versions')}</div>
839
+ {(detail?.versions || []).map((version) => (
840
+ <div className="lw-version" key={version.id}>
841
+ <div>
842
+ <div>v{version.versionNumber}</div>
843
+ <div className="lw-muted">{version.sourceType || 'workbench'}</div>
844
+ </div>
845
+ <Button
846
+ className="lw-version-action"
847
+ type="button"
848
+ variant="outline"
849
+ size="icon"
850
+ title={t('restore')}
851
+ aria-label={`${t('restore')} v${version.versionNumber}`}
852
+ disabled={busy}
853
+ onClick={() => restoreVersion(version.id)}
854
+ >
855
+ <RotateCcw className="lw-button-icon" aria-hidden="true" />
856
+ </Button>
857
+ </div>
858
+ ))}
859
+ </section>
860
+
861
+ <section className="lw-section">
862
+ <div className="lw-section-title">{t('mermaid')}</div>
863
+ <Textarea value={mermaidSource} onChange={(event: any) => updateMermaidSource(event.target.value)} />
864
+ <div className="lw-muted">{t('standardImportNotice')}</div>
865
+ <div className="lw-inline-actions">
866
+ <Button type="button" size="sm" disabled={busy || !mermaidSource.trim()} onClick={saveMermaidDraft}>
867
+ {t('saveMermaid')}
868
+ </Button>
869
+ </div>
870
+ </section>
871
+
872
+ <section className="lw-section">
873
+ <div className="lw-section-title">{t('externalDocument')}</div>
874
+ <Input value={lucidDocumentUrl} placeholder={t('lucidDocumentUrl')} onChange={(event: any) => updateLucidDocumentUrl(event.target.value)} />
875
+ <Input value={embedUrl} placeholder={t('embedUrl')} onChange={(event: any) => updateEmbedUrl(event.target.value)} />
876
+ <Input value={lucidDocumentId} placeholder={t('lucidDocumentId')} onChange={(event: any) => updateLucidDocumentId(event.target.value)} />
877
+ <Input value={previewUrl} placeholder={t('previewUrl')} onChange={(event: any) => updatePreviewUrl(event.target.value)} />
878
+ <Button type="button" size="sm" disabled={busy || (!lucidDocumentId.trim() && !lucidDocumentUrl.trim() && !embedUrl.trim())} onClick={registerExternalDocument}>
879
+ {t('registerExternal')}
880
+ </Button>
881
+ </section>
882
+
883
+ <section className="lw-section">
884
+ <div className="lw-section-title">{t('drawingRequest')}</div>
885
+ <Textarea value={assistantPrompt} placeholder={t('drawingRequest')} onChange={(event: any) => setAssistantPrompt(event.target.value)} />
886
+ <Button type="button" disabled={busy || !assistantPrompt.trim()} onClick={sendAssistantPrompt}>
887
+ <Send className="lw-button-icon" aria-hidden="true" />
888
+ {t('askAssistant')}
889
+ </Button>
890
+ </section>
891
+
892
+ <section className="lw-section">
893
+ <div className="lw-section-title">{t('description')}</div>
894
+ <Textarea value={newDescription} placeholder={t('description')} onChange={(event: any) => setNewDescription(event.target.value)} />
895
+ </section>
896
+ </div>
897
+ </ScrollArea>
898
+ </SidebarContent>
899
+ )}
900
+ </Sidebar>
901
+ </div>
902
+ )
903
+ }
904
+
905
+ function createDefaultStandardImport(title: string) {
906
+ return {
907
+ title,
908
+ product: 'lucidchart',
909
+ pages: [
910
+ {
911
+ id: 'page-1',
912
+ title: 'Page 1',
913
+ shapes: [],
914
+ lines: []
915
+ }
916
+ ]
917
+ }
918
+ }
919
+
920
+ function stringifyJson(value: unknown) {
921
+ return JSON.stringify(value, null, 2)
922
+ }
923
+
924
+ function createSignatureFromValues(
925
+ standardImportText: string,
926
+ mermaidSource: string,
927
+ lucidDocumentId: string,
928
+ lucidDocumentUrl: string,
929
+ embedUrl: string,
930
+ previewUrl: string
931
+ ) {
932
+ return JSON.stringify({
933
+ standardImportText: normalizeJsonText(standardImportText),
934
+ mermaidSource: mermaidSource.replace(/\r\n/g, '\n'),
935
+ lucidDocumentId,
936
+ lucidDocumentUrl,
937
+ embedUrl,
938
+ previewUrl
939
+ })
940
+ }
941
+
942
+ function normalizeJsonText(value: string) {
943
+ try {
944
+ return JSON.stringify(JSON.parse(value))
945
+ } catch {
946
+ return value
947
+ }
948
+ }
949
+
950
+ function firstString(...values: unknown[]) {
951
+ for (const value of values) {
952
+ if (typeof value === 'string' && value.trim()) {
953
+ return value.trim()
954
+ }
955
+ }
956
+ return ''
957
+ }
958
+
959
+ function isObject(value: unknown): value is Record<string, unknown> {
960
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value))
961
+ }
962
+
963
+ function StandardImportPreview({ model }: { model: StandardImportPreviewModel }) {
964
+ return (
965
+ <div className="lw-standard-preview">
966
+ <svg viewBox={model.viewBox} role="img" aria-label="Lucidchart Standard Import preview">
967
+ <defs>
968
+ <marker id="lw-standard-preview-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto" markerUnits="strokeWidth">
969
+ <path d="M 0 0 L 8 4 L 0 8 z" fill="var(--xps-muted-foreground)" />
970
+ </marker>
971
+ </defs>
972
+ {model.lines.map((line) => (
973
+ <g key={line.id}>
974
+ <line
975
+ x1={line.x1}
976
+ y1={line.y1}
977
+ x2={line.x2}
978
+ y2={line.y2}
979
+ stroke={line.strokeColor}
980
+ strokeWidth={line.strokeWidth}
981
+ strokeLinecap="round"
982
+ markerEnd="url(#lw-standard-preview-arrow)"
983
+ />
984
+ {line.text ? (
985
+ <text className="lw-preview-line-label" x={(line.x1 + line.x2) / 2} y={(line.y1 + line.y2) / 2 - 8} textAnchor="middle">
986
+ {truncatePreviewText(line.text, 32)}
987
+ </text>
988
+ ) : null}
989
+ </g>
990
+ ))}
991
+ {model.shapes.map((shape) => {
992
+ const lines = splitPreviewLabel(shape.text || shape.id)
993
+ const labelStartY = shape.y + shape.h / 2 - ((lines.length - 1) * 15) / 2
994
+ return (
995
+ <g key={shape.id}>
996
+ {renderPreviewShape(shape)}
997
+ <text className="lw-preview-label" textAnchor="middle" dominantBaseline="middle">
998
+ {lines.map((line, index) => (
999
+ <tspan key={`${shape.id}-${index}`} x={shape.x + shape.w / 2} y={labelStartY + index * 15}>
1000
+ {line}
1001
+ </tspan>
1002
+ ))}
1003
+ </text>
1004
+ </g>
1005
+ )
1006
+ })}
1007
+ </svg>
1008
+ </div>
1009
+ )
1010
+ }
1011
+
1012
+ function renderPreviewShape(shape: PreviewShape) {
1013
+ const type = shape.type.toLowerCase()
1014
+ if (type.includes('diamond') || type.includes('rhombus') || type.includes('decision')) {
1015
+ const points = [
1016
+ `${shape.x + shape.w / 2},${shape.y}`,
1017
+ `${shape.x + shape.w},${shape.y + shape.h / 2}`,
1018
+ `${shape.x + shape.w / 2},${shape.y + shape.h}`,
1019
+ `${shape.x},${shape.y + shape.h / 2}`
1020
+ ].join(' ')
1021
+ return (
1022
+ <polygon
1023
+ className="lw-preview-shape"
1024
+ points={points}
1025
+ fill={shape.fillColor}
1026
+ stroke={shape.strokeColor}
1027
+ strokeWidth={shape.strokeWidth}
1028
+ />
1029
+ )
1030
+ }
1031
+ if (type.includes('circle') || type.includes('ellipse') || type.includes('terminator')) {
1032
+ return (
1033
+ <ellipse
1034
+ className="lw-preview-shape"
1035
+ cx={shape.x + shape.w / 2}
1036
+ cy={shape.y + shape.h / 2}
1037
+ rx={shape.w / 2}
1038
+ ry={shape.h / 2}
1039
+ fill={shape.fillColor}
1040
+ stroke={shape.strokeColor}
1041
+ strokeWidth={shape.strokeWidth}
1042
+ />
1043
+ )
1044
+ }
1045
+ return (
1046
+ <rect
1047
+ className="lw-preview-shape"
1048
+ x={shape.x}
1049
+ y={shape.y}
1050
+ width={shape.w}
1051
+ height={shape.h}
1052
+ rx={shape.cornerRadius}
1053
+ fill={shape.fillColor}
1054
+ stroke={shape.strokeColor}
1055
+ strokeWidth={shape.strokeWidth}
1056
+ />
1057
+ )
1058
+ }
1059
+
1060
+ function createStandardImportPreview(source: string): StandardImportPreviewModel | null {
1061
+ const parsed = parseJsonLike(source)
1062
+ if (!isObject(parsed)) {
1063
+ return null
1064
+ }
1065
+ const root = isObject(parsed.standardImport) ? parsed.standardImport : parsed
1066
+ const rawShapes: Record<string, unknown>[] = []
1067
+ const rawLines: Record<string, unknown>[] = []
1068
+ collectPreviewItems(root, rawShapes, rawLines, 0, new WeakSet<object>())
1069
+
1070
+ const shapes = rawShapes
1071
+ .map((shape, index) => normalizePreviewShape(shape, index))
1072
+ .filter((shape): shape is PreviewShape => Boolean(shape))
1073
+ const shapeMap = new Map(shapes.map((shape) => [shape.id, shape]))
1074
+ const lines = rawLines
1075
+ .map((line, index) => normalizePreviewLine(line, index, shapeMap))
1076
+ .filter((line): line is PreviewLine => Boolean(line))
1077
+
1078
+ if (!shapes.length && !lines.length) {
1079
+ return null
1080
+ }
1081
+ const bounds = computePreviewBounds(shapes, lines)
1082
+ return {
1083
+ shapes,
1084
+ lines,
1085
+ viewBox: `${bounds.x} ${bounds.y} ${bounds.w} ${bounds.h}`
1086
+ }
1087
+ }
1088
+
1089
+ function collectPreviewItems(
1090
+ value: unknown,
1091
+ shapes: Record<string, unknown>[],
1092
+ lines: Record<string, unknown>[],
1093
+ depth: number,
1094
+ seen: WeakSet<object>
1095
+ ) {
1096
+ if (depth > 7 || value == null) {
1097
+ return
1098
+ }
1099
+ if (Array.isArray(value)) {
1100
+ value.forEach((item) => collectPreviewItems(item, shapes, lines, depth + 1, seen))
1101
+ return
1102
+ }
1103
+ if (!isObject(value)) {
1104
+ return
1105
+ }
1106
+ if (seen.has(value)) {
1107
+ return
1108
+ }
1109
+ seen.add(value)
1110
+
1111
+ if (hasPreviewLineGeometry(value)) {
1112
+ lines.push(value)
1113
+ return
1114
+ }
1115
+ if (readPreviewBounds(value)) {
1116
+ shapes.push(value)
1117
+ return
1118
+ }
1119
+
1120
+ ;['pages', 'layers', 'groups', 'children', 'items', 'objects', 'blocks', 'shapes', 'lines', 'connectors'].forEach((key) =>
1121
+ collectPreviewItems(value[key], shapes, lines, depth + 1, seen)
1122
+ )
1123
+ }
1124
+
1125
+ function normalizePreviewShape(input: Record<string, unknown>, index: number): PreviewShape | null {
1126
+ const bounds = readPreviewBounds(input)
1127
+ if (!bounds) {
1128
+ return null
1129
+ }
1130
+ const format = firstRecord(input.format, input.style, input.styles, input.properties)
1131
+ const id = firstPreviewString(input.id, input.uuid, input.shapeId, input.name) || `shape-${index + 1}`
1132
+ const text = firstPreviewString(input.text, input.label, input.name, input.title) || id
1133
+ return {
1134
+ id,
1135
+ x: bounds.x,
1136
+ y: bounds.y,
1137
+ w: bounds.w,
1138
+ h: bounds.h,
1139
+ text,
1140
+ type: firstPreviewString(input.type, input.shape, input.shapeType, input.class, input.name) || 'rect',
1141
+ fillColor:
1142
+ firstPreviewString(input.fillColor, format?.fillColor, format?.fill, format?.backgroundColor, input.backgroundColor) || '#eff6ff',
1143
+ strokeColor:
1144
+ firstPreviewString(input.strokeColor, format?.strokeColor, format?.stroke, format?.borderColor, input.borderColor) || '#2563eb',
1145
+ strokeWidth: firstFiniteNumber(input.strokeWidth, format?.strokeWidth, format?.borderWidth) ?? 1.5,
1146
+ cornerRadius: firstFiniteNumber(input.cornerRadius, format?.cornerRadius, input.radius, format?.radius) ?? 8
1147
+ }
1148
+ }
1149
+
1150
+ function normalizePreviewLine(
1151
+ input: Record<string, unknown>,
1152
+ index: number,
1153
+ shapeMap: Map<string, PreviewShape>
1154
+ ): PreviewLine | null {
1155
+ const fromId = readEndpointId(input, ['fromId', 'sourceId', 'startShapeId', 'startId', 'from', 'source', 'start'])
1156
+ const toId = readEndpointId(input, ['toId', 'targetId', 'endShapeId', 'endId', 'to', 'target', 'end'])
1157
+ const fromShape = fromId ? shapeMap.get(fromId) : null
1158
+ const toShape = toId ? shapeMap.get(toId) : null
1159
+ const startPoint = fromShape ? centerOfShape(fromShape) : readPreviewPoint(input, ['start', 'fromPoint', 'sourcePoint', 'p1', 'endpoint1'])
1160
+ const endPoint = toShape ? centerOfShape(toShape) : readPreviewPoint(input, ['end', 'toPoint', 'targetPoint', 'p2', 'endpoint2'])
1161
+ const bounds = readPreviewBounds(input)
1162
+ const x1 = startPoint?.x ?? firstFiniteNumber(input.x1, input.startX, input.fromX) ?? bounds?.x
1163
+ const y1 = startPoint?.y ?? firstFiniteNumber(input.y1, input.startY, input.fromY) ?? bounds?.y
1164
+ const x2 = endPoint?.x ?? firstFiniteNumber(input.x2, input.endX, input.toX) ?? (bounds ? bounds.x + bounds.w : null)
1165
+ const y2 = endPoint?.y ?? firstFiniteNumber(input.y2, input.endY, input.toY) ?? (bounds ? bounds.y + bounds.h : null)
1166
+ if (![x1, y1, x2, y2].every((value) => typeof value === 'number' && Number.isFinite(value))) {
1167
+ return null
1168
+ }
1169
+ const format = firstRecord(input.format, input.style, input.styles, input.properties)
1170
+ return {
1171
+ id: firstPreviewString(input.id, input.uuid, input.lineId, input.name) || `line-${index + 1}`,
1172
+ x1,
1173
+ y1,
1174
+ x2,
1175
+ y2,
1176
+ text: firstPreviewString(input.text, input.label, input.name, input.title) || '',
1177
+ strokeColor: firstPreviewString(input.strokeColor, format?.strokeColor, format?.stroke, input.color) || '#64748b',
1178
+ strokeWidth: firstFiniteNumber(input.strokeWidth, format?.strokeWidth, input.width) ?? 1.5
1179
+ }
1180
+ }
1181
+
1182
+ function hasPreviewLineGeometry(input: Record<string, unknown>) {
1183
+ const type = (firstPreviewString(input.type, input.shape, input.shapeType, input.class) || '').toLowerCase()
1184
+ const isLineType =
1185
+ ['line', 'arrow', 'connector', 'straightline', 'elbowline'].includes(type) ||
1186
+ type.includes('connector') ||
1187
+ type.includes('arrow') ||
1188
+ type.includes('straight_line') ||
1189
+ type.includes('elbow_line')
1190
+ const hasEndpointIds =
1191
+ Boolean(readEndpointId(input, ['fromId', 'sourceId', 'startShapeId', 'startId', 'from', 'source', 'start'])) &&
1192
+ Boolean(readEndpointId(input, ['toId', 'targetId', 'endShapeId', 'endId', 'to', 'target', 'end']))
1193
+ const hasCoordinates =
1194
+ firstFiniteNumber(input.x1, input.startX, input.fromX) != null &&
1195
+ firstFiniteNumber(input.y1, input.startY, input.fromY) != null &&
1196
+ firstFiniteNumber(input.x2, input.endX, input.toX) != null &&
1197
+ firstFiniteNumber(input.y2, input.endY, input.toY) != null
1198
+ const hasPoints =
1199
+ Boolean(readPreviewPoint(input, ['start', 'fromPoint', 'sourcePoint', 'p1', 'endpoint1'])) &&
1200
+ Boolean(readPreviewPoint(input, ['end', 'toPoint', 'targetPoint', 'p2', 'endpoint2']))
1201
+ return hasEndpointIds || hasCoordinates || hasPoints || isLineType
1202
+ }
1203
+
1204
+ function readPreviewBounds(input: Record<string, unknown>) {
1205
+ const bounds = firstRecord(input.bounds, input.boundingBox, input.box, input.geometry, input.position)
1206
+ const x = firstFiniteNumber(input.x, input.left, bounds?.x, bounds?.left)
1207
+ const y = firstFiniteNumber(input.y, input.top, bounds?.y, bounds?.top)
1208
+ const w = firstFiniteNumber(input.w, input.width, bounds?.w, bounds?.width)
1209
+ const h = firstFiniteNumber(input.h, input.height, bounds?.h, bounds?.height)
1210
+ if ([x, y, w, h].every((value) => typeof value === 'number' && Number.isFinite(value)) && w > 0 && h > 0) {
1211
+ return { x, y, w, h }
1212
+ }
1213
+ return null
1214
+ }
1215
+
1216
+ function readPreviewPoint(input: Record<string, unknown>, keys: string[]) {
1217
+ for (const key of keys) {
1218
+ const point = input[key]
1219
+ if (isObject(point)) {
1220
+ const x = firstFiniteNumber(point.x, point.left)
1221
+ const y = firstFiniteNumber(point.y, point.top)
1222
+ if (typeof x === 'number' && typeof y === 'number') {
1223
+ return { x, y }
1224
+ }
1225
+ }
1226
+ }
1227
+ return null
1228
+ }
1229
+
1230
+ function readEndpointId(input: Record<string, unknown>, keys: string[]) {
1231
+ for (const key of keys) {
1232
+ const value = input[key]
1233
+ const direct = firstPreviewString(value)
1234
+ if (direct) {
1235
+ return direct
1236
+ }
1237
+ if (isObject(value)) {
1238
+ const nested = firstPreviewString(value.id, value.shapeId, value.nodeId, value.ref, value.reference)
1239
+ if (nested) {
1240
+ return nested
1241
+ }
1242
+ }
1243
+ }
1244
+ return null
1245
+ }
1246
+
1247
+ function centerOfShape(shape: PreviewShape) {
1248
+ return { x: shape.x + shape.w / 2, y: shape.y + shape.h / 2 }
1249
+ }
1250
+
1251
+ function computePreviewBounds(shapes: PreviewShape[], lines: PreviewLine[]) {
1252
+ let minX = Number.POSITIVE_INFINITY
1253
+ let minY = Number.POSITIVE_INFINITY
1254
+ let maxX = Number.NEGATIVE_INFINITY
1255
+ let maxY = Number.NEGATIVE_INFINITY
1256
+ shapes.forEach((shape) => {
1257
+ minX = Math.min(minX, shape.x)
1258
+ minY = Math.min(minY, shape.y)
1259
+ maxX = Math.max(maxX, shape.x + shape.w)
1260
+ maxY = Math.max(maxY, shape.y + shape.h)
1261
+ })
1262
+ lines.forEach((line) => {
1263
+ minX = Math.min(minX, line.x1, line.x2)
1264
+ minY = Math.min(minY, line.y1, line.y2)
1265
+ maxX = Math.max(maxX, line.x1, line.x2)
1266
+ maxY = Math.max(maxY, line.y1, line.y2)
1267
+ })
1268
+ if (![minX, minY, maxX, maxY].every(Number.isFinite)) {
1269
+ return { x: 0, y: 0, w: 800, h: 360 }
1270
+ }
1271
+ const margin = 48
1272
+ return {
1273
+ x: minX - margin,
1274
+ y: minY - margin,
1275
+ w: Math.max(360, maxX - minX + margin * 2),
1276
+ h: Math.max(220, maxY - minY + margin * 2)
1277
+ }
1278
+ }
1279
+
1280
+ function firstRecord(...values: unknown[]) {
1281
+ return values.find(isObject) || null
1282
+ }
1283
+
1284
+ function firstPreviewString(...values: unknown[]) {
1285
+ for (const value of values) {
1286
+ if (typeof value === 'string' && value.trim()) {
1287
+ return value.trim()
1288
+ }
1289
+ if (typeof value === 'number' && Number.isFinite(value)) {
1290
+ return String(value)
1291
+ }
1292
+ }
1293
+ return ''
1294
+ }
1295
+
1296
+ function firstFiniteNumber(...values: unknown[]) {
1297
+ for (const value of values) {
1298
+ if (typeof value === 'number' && Number.isFinite(value)) {
1299
+ return value
1300
+ }
1301
+ if (typeof value === 'string' && value.trim()) {
1302
+ const parsed = Number(value)
1303
+ if (Number.isFinite(parsed)) {
1304
+ return parsed
1305
+ }
1306
+ }
1307
+ }
1308
+ return null
1309
+ }
1310
+
1311
+ function splitPreviewLabel(value: string) {
1312
+ return value
1313
+ .replace(/\r\n/g, '\n')
1314
+ .split('\n')
1315
+ .flatMap((line) => chunkPreviewText(line.trim(), 18))
1316
+ .filter(Boolean)
1317
+ .slice(0, 5)
1318
+ }
1319
+
1320
+ function chunkPreviewText(value: string, size: number) {
1321
+ if (!value) {
1322
+ return []
1323
+ }
1324
+ const chunks: string[] = []
1325
+ for (let index = 0; index < value.length; index += size) {
1326
+ chunks.push(value.slice(index, index + size))
1327
+ }
1328
+ return chunks
1329
+ }
1330
+
1331
+ function truncatePreviewText(value: string, maxLength: number) {
1332
+ return value.length > maxLength ? `${value.slice(0, maxLength - 1)}...` : value
1333
+ }
1334
+
1335
+ function extractToolNameFromHostEvent(event: unknown) {
1336
+ for (const candidate of expandHostEventCandidates(event)) {
1337
+ if (!isObject(candidate)) {
1338
+ continue
1339
+ }
1340
+ const direct = readString(candidate, 'toolName') ?? readString(candidate, 'tool_name') ?? readString(candidate, 'name')
1341
+ if (direct && LUCIDCHART_TOOL_NAMES.has(direct)) {
1342
+ return direct
1343
+ }
1344
+ const tool = candidate.tool
1345
+ if (isObject(tool)) {
1346
+ const toolName = readString(tool, 'name') ?? readString(tool, 'toolName') ?? readString(tool, 'tool_name')
1347
+ if (toolName && LUCIDCHART_TOOL_NAMES.has(toolName)) {
1348
+ return toolName
1349
+ }
1350
+ }
1351
+ if (isObject(candidate.function)) {
1352
+ const toolName = readString(candidate.function, 'name') ?? readString(candidate.function, 'toolName') ?? readString(candidate.function, 'tool_name')
1353
+ if (toolName && LUCIDCHART_TOOL_NAMES.has(toolName)) {
1354
+ return toolName
1355
+ }
1356
+ }
1357
+ const toolCall = candidate.toolCall ?? candidate.tool_call
1358
+ if (isObject(toolCall)) {
1359
+ const toolName =
1360
+ readString(toolCall, 'name') ??
1361
+ readString(toolCall, 'toolName') ??
1362
+ readString(toolCall, 'tool_name') ??
1363
+ (isObject(toolCall.function) ? readString(toolCall.function, 'name') : null)
1364
+ if (toolName && LUCIDCHART_TOOL_NAMES.has(toolName)) {
1365
+ return toolName
1366
+ }
1367
+ }
1368
+ }
1369
+ return null
1370
+ }
1371
+
1372
+ function extractDocumentIdFromHostEvent(event: unknown) {
1373
+ for (const candidate of expandHostEventCandidates(event)) {
1374
+ if (!isObject(candidate)) {
1375
+ continue
1376
+ }
1377
+ const direct =
1378
+ readString(candidate, 'documentId') ??
1379
+ readString(candidate, 'document_id') ??
1380
+ readString(candidate, 'lucidchartDocumentId') ??
1381
+ readString(candidate, 'lucidchart_document_id') ??
1382
+ readString(candidate, 'drawingId')
1383
+ if (direct) {
1384
+ return direct
1385
+ }
1386
+ if (isObject(candidate.item)) {
1387
+ const itemId = readString(candidate.item, 'id')
1388
+ if (itemId) {
1389
+ return itemId
1390
+ }
1391
+ }
1392
+ if (isObject(candidate.document)) {
1393
+ const documentId =
1394
+ readString(candidate.document, 'documentId') ??
1395
+ readString(candidate.document, 'document_id') ??
1396
+ readString(candidate.document, 'id') ??
1397
+ (isObject(candidate.document.item) ? readString(candidate.document.item, 'id') : null)
1398
+ if (documentId) {
1399
+ return documentId
1400
+ }
1401
+ }
1402
+ if (isObject(candidate.version)) {
1403
+ const documentId = readString(candidate.version, 'documentId') ?? readString(candidate.version, 'document_id')
1404
+ if (documentId) {
1405
+ return documentId
1406
+ }
1407
+ }
1408
+ if (Array.isArray(candidate.items)) {
1409
+ const firstItem = candidate.items.find(isObject)
1410
+ if (firstItem) {
1411
+ const itemId = readString(firstItem, 'id') ?? readString(firstItem, 'documentId') ?? readString(firstItem, 'document_id')
1412
+ if (itemId) {
1413
+ return itemId
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ return null
1419
+ }
1420
+
1421
+ function expandHostEventCandidates(event: unknown) {
1422
+ const candidates: unknown[] = []
1423
+ collectHostEventCandidates(event, candidates, 0, new WeakSet<object>())
1424
+ return candidates
1425
+ }
1426
+
1427
+ function collectHostEventCandidates(value: unknown, candidates: unknown[], depth: number, seen: WeakSet<object>) {
1428
+ if (depth > 5 || value == null) {
1429
+ return
1430
+ }
1431
+ const normalized = parseJsonLike(value)
1432
+ if ((isObject(normalized) || Array.isArray(normalized)) && seen.has(normalized)) {
1433
+ return
1434
+ }
1435
+ if (isObject(normalized) || Array.isArray(normalized)) {
1436
+ seen.add(normalized)
1437
+ }
1438
+ candidates.push(normalized)
1439
+ if (Array.isArray(normalized)) {
1440
+ normalized.forEach((item) => collectHostEventCandidates(item, candidates, depth + 1, seen))
1441
+ return
1442
+ }
1443
+ if (!isObject(normalized)) {
1444
+ return
1445
+ }
1446
+ ;[
1447
+ 'payload',
1448
+ 'metadata',
1449
+ 'data',
1450
+ 'result',
1451
+ 'output',
1452
+ 'content',
1453
+ 'message',
1454
+ 'detail',
1455
+ 'response',
1456
+ 'document',
1457
+ 'documents',
1458
+ 'item',
1459
+ 'items',
1460
+ 'version',
1461
+ 'versions',
1462
+ 'toolResult',
1463
+ 'tool_result',
1464
+ 'toolResponse',
1465
+ 'tool_response',
1466
+ 'resultText',
1467
+ 'text',
1468
+ 'tool',
1469
+ 'toolCall',
1470
+ 'tool_call',
1471
+ 'function',
1472
+ 'arguments',
1473
+ 'args',
1474
+ 'input'
1475
+ ].forEach((key) => collectHostEventCandidates(normalized[key], candidates, depth + 1, seen))
1476
+ }
1477
+
1478
+ function parseJsonLike(value: unknown) {
1479
+ if (typeof value !== 'string') {
1480
+ return value
1481
+ }
1482
+ const trimmed = value.trim()
1483
+ if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
1484
+ return value
1485
+ }
1486
+ try {
1487
+ return JSON.parse(trimmed)
1488
+ } catch {
1489
+ return value
1490
+ }
1491
+ }
1492
+
1493
+ function readString(value: Record<string, unknown>, key: string) {
1494
+ const raw = value[key]
1495
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : null
1496
+ }
1497
+
1498
+ function removeLucidchartExtension(name: string) {
1499
+ return name.replace(/\.(lucid|lucidchart|json)(?:\.json)?$/i, '').replace(/document$/i, 'Lucidchart Document') || name
1500
+ }
1501
+
1502
+ function downloadBlob(blob: Blob, filename: string) {
1503
+ const url = URL.createObjectURL(blob)
1504
+ const link = document.createElement('a')
1505
+ link.href = url
1506
+ link.download = filename
1507
+ document.body.appendChild(link)
1508
+ link.click()
1509
+ link.remove()
1510
+ URL.revokeObjectURL(url)
1511
+ }
1512
+
1513
+ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
1514
+ root.render(<App />)