@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.
- package/.xpertai-plugin/plugin.json +118 -0
- package/README.md +5 -0
- package/assets/composerIcon.svg +5 -0
- package/assets/logo.svg +10 -0
- package/dist/docs/lucidchart-agent-skill.md +46 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +153 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/constants.d.ts +25 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +44 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/entities/index.d.ts +4 -0
- package/dist/lib/entities/index.d.ts.map +1 -0
- package/dist/lib/entities/index.js +4 -0
- package/dist/lib/entities/index.js.map +1 -0
- package/dist/lib/entities/lucidchart-action-log.entity.d.ts +18 -0
- package/dist/lib/entities/lucidchart-action-log.entity.d.ts.map +1 -0
- package/dist/lib/entities/lucidchart-action-log.entity.js +69 -0
- package/dist/lib/entities/lucidchart-action-log.entity.js.map +1 -0
- package/dist/lib/entities/lucidchart-document-version.entity.d.ts +26 -0
- package/dist/lib/entities/lucidchart-document-version.entity.d.ts.map +1 -0
- package/dist/lib/entities/lucidchart-document-version.entity.js +102 -0
- package/dist/lib/entities/lucidchart-document-version.entity.js.map +1 -0
- package/dist/lib/entities/lucidchart-document.entity.d.ts +30 -0
- package/dist/lib/entities/lucidchart-document.entity.d.ts.map +1 -0
- package/dist/lib/entities/lucidchart-document.entity.js +118 -0
- package/dist/lib/entities/lucidchart-document.entity.js.map +1 -0
- package/dist/lib/lucidchart-view.provider.d.ts +14 -0
- package/dist/lib/lucidchart-view.provider.d.ts.map +1 -0
- package/dist/lib/lucidchart-view.provider.js +460 -0
- package/dist/lib/lucidchart-view.provider.js.map +1 -0
- package/dist/lib/lucidchart.middleware.d.ts +10 -0
- package/dist/lib/lucidchart.middleware.d.ts.map +1 -0
- package/dist/lib/lucidchart.middleware.js +193 -0
- package/dist/lib/lucidchart.middleware.js.map +1 -0
- package/dist/lib/lucidchart.plugin.d.ts +8 -0
- package/dist/lib/lucidchart.plugin.d.ts.map +1 -0
- package/dist/lib/lucidchart.plugin.js +27 -0
- package/dist/lib/lucidchart.plugin.js.map +1 -0
- package/dist/lib/lucidchart.service.d.ts +208 -0
- package/dist/lib/lucidchart.service.d.ts.map +1 -0
- package/dist/lib/lucidchart.service.js +548 -0
- package/dist/lib/lucidchart.service.js.map +1 -0
- package/dist/lib/lucidchart.templates.d.ts +3 -0
- package/dist/lib/lucidchart.templates.d.ts.map +1 -0
- package/dist/lib/lucidchart.templates.js +78 -0
- package/dist/lib/lucidchart.templates.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/app.css +0 -0
- package/dist/lib/remote-components/lucidchart-workbench/app.js +1245 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.d.ts +3 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.js +115 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/i18n.ts +169 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/main.tsx +1514 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.d.ts +3 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.js +4 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-client-shim.ts +4 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.d.ts +11 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.js +11 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-dom-shim.ts +11 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.d.ts +5 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.js +8 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-jsx-runtime-shim.ts +8 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.d.ts +36 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.js +36 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/react-shim.ts +36 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/runtime.d.ts +21 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/runtime.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/runtime.js +198 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/runtime.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/runtime.ts +228 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.d.ts +2 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.js +383 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/styles.ts +382 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/vendor.d.ts +4 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/vendor.d.ts.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/vendor.js +4 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/vendor.js.map +1 -0
- package/dist/lib/remote-components/lucidchart-workbench/src/vendor.ts +3 -0
- package/dist/lib/types.d.ts +87 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/xpert-lucidchart-assistant.yaml +137 -0
- package/package.json +85 -0
- package/skills/index/SKILL.md +46 -0
- 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 />)
|