@getdom/studio 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 (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,620 @@
1
+ let idCounter = 1000;
2
+
3
+ /**
4
+ * Creates an isolated in-memory API for the agent chat demos.
5
+ *
6
+ * @returns {Record<string, Function>} API methods consumed by the demo store.
7
+ */
8
+ export function createAgentChatMockApi() {
9
+ const state = createInitialState();
10
+
11
+ /**
12
+ * Returns the conversation index and available agents.
13
+ *
14
+ * @returns {Promise<Record<string, unknown>>} Bootstrap response.
15
+ */
16
+ async function bootstrap() {
17
+ await wait(180);
18
+ return {
19
+ agents: clone(state.agents),
20
+ conversations: clone(sortConversations(state.conversations)),
21
+ meta: responseMeta('bootstrap'),
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Returns a single conversation and its messages.
27
+ *
28
+ * @param {{ conversationId: string }} request Conversation request.
29
+ * @returns {Promise<Record<string, unknown>>} Conversation response.
30
+ */
31
+ async function getConversation(request) {
32
+ await wait(220);
33
+ const conversation = findConversation(state, request.conversationId);
34
+ if (!conversation) throw new Error('Conversation not found.');
35
+ return {
36
+ conversation: clone(conversation),
37
+ messages: clone(state.messagesByConversationId[conversation.id] || []),
38
+ meta: responseMeta('get-conversation'),
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Creates a new conversation with an empty message list.
44
+ *
45
+ * @param {{ title?: string, agentId?: string }} request Create request.
46
+ * @returns {Promise<Record<string, unknown>>} Created conversation response.
47
+ */
48
+ async function createConversation(request = {}) {
49
+ await wait(260);
50
+ const agent = state.agents.find((item) => item.id === request.agentId) || state.agents[0];
51
+ const conversation = {
52
+ id: nextId('conv'),
53
+ title: request.title || 'New agent thread',
54
+ summary: 'Fresh workspace for a focused agent exchange.',
55
+ agentId: agent.id,
56
+ status: 'idle',
57
+ mode: 'text',
58
+ labels: ['new'],
59
+ unreadCount: 0,
60
+ updatedAt: nowIso(),
61
+ };
62
+ state.conversations.unshift(conversation);
63
+ state.messagesByConversationId[conversation.id] = [];
64
+ return {
65
+ conversation: clone(conversation),
66
+ messages: [],
67
+ meta: responseMeta('create-conversation'),
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Streams an assistant response as reasoning, tool calls, and text deltas.
73
+ *
74
+ * @param {Record<string, unknown>} request Stream request.
75
+ * @param {Record<string, Function>} handlers Stream callbacks.
76
+ * @returns {Promise<Record<string, unknown>>} Final stream response.
77
+ */
78
+ async function streamMessage(request, handlers = {}) {
79
+ const conversation = findConversation(state, request.conversationId);
80
+ if (!conversation) throw new Error('Conversation not found.');
81
+
82
+ conversation.status = 'streaming';
83
+ conversation.mode = request.mode === 'audio' ? 'audio' : 'text';
84
+ conversation.updatedAt = nowIso();
85
+
86
+ const userMessage = request.userMessage || createUserMessage(request.content, request.mode);
87
+ const assistantMessage = createAssistantMessage(conversation);
88
+ appendMessage(state, conversation.id, userMessage);
89
+ appendMessage(state, conversation.id, assistantMessage);
90
+
91
+ handlers.onAssistantStart?.(clone(assistantMessage));
92
+ await waitAndCheck(240, request.signal);
93
+
94
+ const reasoningPart = {
95
+ id: nextId('part'),
96
+ type: 'reasoning',
97
+ status: 'complete',
98
+ text: 'I will inspect the current SEO context, run a lightweight tool lookup, then turn the result into a plan the user can edit.',
99
+ };
100
+ assistantMessage.parts.push(reasoningPart);
101
+ handlers.onPart?.(assistantMessage.id, clone(reasoningPart));
102
+ await waitAndCheck(360, request.signal);
103
+
104
+ const keywordTool = {
105
+ id: nextId('tool'),
106
+ type: 'tool-call',
107
+ name: 'keyword_research',
108
+ label: 'Keyword research',
109
+ status: 'running',
110
+ args: {
111
+ websiteId: 'site_flexai_demo',
112
+ seed: request.content,
113
+ limit: 4,
114
+ },
115
+ result: null,
116
+ };
117
+ assistantMessage.parts.push(keywordTool);
118
+ handlers.onPart?.(assistantMessage.id, clone(keywordTool));
119
+ await waitAndCheck(520, request.signal);
120
+
121
+ keywordTool.status = 'complete';
122
+ keywordTool.result = {
123
+ keywords: [
124
+ { phrase: 'ai seo planner', volume: 1300, difficulty: 31 },
125
+ { phrase: 'agentic content workflow', volume: 260, difficulty: 24 },
126
+ { phrase: 'seo automation dashboard', volume: 590, difficulty: 38 },
127
+ ],
128
+ recommendedIntent: 'commercial',
129
+ };
130
+ handlers.onPart?.(assistantMessage.id, clone(keywordTool));
131
+ await waitAndCheck(320, request.signal);
132
+
133
+ const weatherTool = {
134
+ id: nextId('tool'),
135
+ type: 'tool-call',
136
+ name: 'get_weather',
137
+ rendererKey: 'weather',
138
+ label: 'Launch-day weather',
139
+ status: 'complete',
140
+ args: {
141
+ location: 'London',
142
+ date: '2026-07-03',
143
+ },
144
+ result: {
145
+ location: 'London',
146
+ condition: 'Bright intervals',
147
+ temperatureC: 21,
148
+ windMph: 8,
149
+ confidence: 0.84,
150
+ note: 'Use this only as demo tool data.',
151
+ },
152
+ };
153
+ assistantMessage.parts.push(weatherTool);
154
+ handlers.onPart?.(assistantMessage.id, clone(weatherTool));
155
+ await waitAndCheck(260, request.signal);
156
+
157
+ const textPart = {
158
+ id: nextId('part'),
159
+ type: 'text',
160
+ status: 'streaming',
161
+ text: '',
162
+ };
163
+ assistantMessage.parts.push(textPart);
164
+ handlers.onPart?.(assistantMessage.id, clone(textPart));
165
+
166
+ for (const chunk of responseChunks(request.content, request.mode)) {
167
+ await waitAndCheck(130, request.signal);
168
+ textPart.text += chunk;
169
+ handlers.onTextDelta?.(assistantMessage.id, textPart.id, chunk);
170
+ }
171
+
172
+ textPart.status = 'complete';
173
+ assistantMessage.status = 'complete';
174
+ conversation.status = 'complete';
175
+ conversation.summary = summaryFromPrompt(request.content);
176
+ conversation.updatedAt = nowIso();
177
+ handlers.onAssistantDone?.(clone(assistantMessage));
178
+
179
+ return {
180
+ conversation: clone(conversation),
181
+ message: clone(assistantMessage),
182
+ meta: responseMeta('stream-message'),
183
+ };
184
+ }
185
+
186
+ return {
187
+ bootstrap,
188
+ getConversation,
189
+ createConversation,
190
+ streamMessage,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Creates the initial agent, conversation, and message records.
196
+ *
197
+ * @returns {Record<string, unknown>} Mock database state.
198
+ */
199
+ function createInitialState() {
200
+ const agents = [
201
+ {
202
+ id: 'agent_seo',
203
+ name: 'FlexAI SEO Agent',
204
+ role: 'Plans SEO work from crawl data, keyword facts, and content opportunities.',
205
+ status: 'online',
206
+ model: 'gpt-4.1-mini',
207
+ tools: ['keyword_research', 'crawl_summary', 'content_plan', 'get_weather'],
208
+ },
209
+ {
210
+ id: 'agent_image',
211
+ name: 'Image Brief Agent',
212
+ role: 'Turns article context into structured creative prompts and stored image results.',
213
+ status: 'ready',
214
+ model: 'gpt-image-1',
215
+ tools: ['image_brief', 'brand_assets', 'storage_lookup'],
216
+ },
217
+ {
218
+ id: 'agent_support',
219
+ name: 'Support Copilot',
220
+ role: 'Summarizes customer threads and prepares operator handoffs.',
221
+ status: 'review',
222
+ model: 'gpt-4.1',
223
+ tools: ['customer_lookup', 'ticket_history', 'handoff_note'],
224
+ },
225
+ ];
226
+ const conversations = [
227
+ {
228
+ id: 'conv_homepage_plan',
229
+ title: 'Homepage ranking plan',
230
+ summary: 'Keyword opportunity, weather tool demo, and a focused content-plan answer.',
231
+ agentId: 'agent_seo',
232
+ status: 'complete',
233
+ mode: 'text',
234
+ labels: ['seo', 'planning'],
235
+ unreadCount: 0,
236
+ updatedAt: '2026-07-03T09:15:00.000Z',
237
+ },
238
+ {
239
+ id: 'conv_image_brief',
240
+ title: 'Article image brief',
241
+ summary: 'Draft a reusable article hero image prompt from stored brand assets.',
242
+ agentId: 'agent_image',
243
+ status: 'idle',
244
+ mode: 'text',
245
+ labels: ['images'],
246
+ unreadCount: 2,
247
+ updatedAt: '2026-07-02T16:35:00.000Z',
248
+ },
249
+ {
250
+ id: 'conv_audio_handoff',
251
+ title: 'Audio support handoff',
252
+ summary: 'Switch between voice and text while preparing a support summary.',
253
+ agentId: 'agent_support',
254
+ status: 'waiting',
255
+ mode: 'audio',
256
+ labels: ['audio', 'support'],
257
+ unreadCount: 0,
258
+ updatedAt: '2026-07-01T11:20:00.000Z',
259
+ },
260
+ ];
261
+ const messagesByConversationId = {
262
+ conv_homepage_plan: seedHomepageMessages(),
263
+ conv_image_brief: seedImageMessages(),
264
+ conv_audio_handoff: seedAudioMessages(),
265
+ };
266
+ return { agents, conversations, messagesByConversationId };
267
+ }
268
+
269
+ /**
270
+ * Creates seed messages for the active SEO conversation.
271
+ *
272
+ * @returns {Array<Record<string, unknown>>} Message records.
273
+ */
274
+ function seedHomepageMessages() {
275
+ return [
276
+ {
277
+ id: 'msg_home_user_1',
278
+ role: 'user',
279
+ content: 'Can you turn the latest crawl and keyword context into a homepage ranking plan?',
280
+ status: 'complete',
281
+ mode: 'text',
282
+ createdAt: '2026-07-03T09:10:00.000Z',
283
+ },
284
+ {
285
+ id: 'msg_home_assistant_1',
286
+ role: 'assistant',
287
+ agentId: 'agent_seo',
288
+ status: 'complete',
289
+ mode: 'text',
290
+ createdAt: '2026-07-03T09:10:12.000Z',
291
+ parts: [
292
+ {
293
+ id: 'part_reasoning_home',
294
+ type: 'reasoning',
295
+ status: 'complete',
296
+ text: 'The answer needs ranked actions, not a raw audit dump. I will combine crawl issues, keyword opportunity, and one timely launch signal.',
297
+ },
298
+ {
299
+ id: 'tool_keyword_seed',
300
+ type: 'tool-call',
301
+ name: 'keyword_research',
302
+ label: 'Keyword research',
303
+ status: 'complete',
304
+ args: { websiteId: 'site_flexai_demo', limit: 3 },
305
+ result: {
306
+ keywords: [
307
+ { phrase: 'ai seo planner', volume: 1300, difficulty: 31 },
308
+ { phrase: 'automated seo workflow', volume: 720, difficulty: 35 },
309
+ { phrase: 'content plan generator', volume: 2100, difficulty: 42 },
310
+ ],
311
+ },
312
+ },
313
+ {
314
+ id: 'tool_weather_seed',
315
+ type: 'tool-call',
316
+ name: 'get_weather',
317
+ rendererKey: 'weather',
318
+ label: 'Launch-day weather',
319
+ status: 'complete',
320
+ args: { location: 'London', date: '2026-07-03' },
321
+ result: {
322
+ location: 'London',
323
+ condition: 'Bright intervals',
324
+ temperatureC: 21,
325
+ windMph: 8,
326
+ confidence: 0.84,
327
+ note: 'Demo renderer for a weather tool result.',
328
+ },
329
+ },
330
+ {
331
+ id: 'part_home_answer',
332
+ type: 'text',
333
+ status: 'complete',
334
+ text: 'Yes. I would lead with the "AI SEO planner" angle, then make the homepage prove three things quickly: crawl-backed recommendations, editable content plans, and safe agent actions.\n\nPriority one is a tighter first viewport claim. Priority two is a short workflow strip showing crawl, keyword, plan, and publish review. Priority three is a proof panel that shows stored tool calls instead of vague AI copy.',
335
+ },
336
+ ],
337
+ },
338
+ ];
339
+ }
340
+
341
+ /**
342
+ * Creates seed messages for the image-brief conversation.
343
+ *
344
+ * @returns {Array<Record<string, unknown>>} Message records.
345
+ */
346
+ function seedImageMessages() {
347
+ return [
348
+ {
349
+ id: 'msg_image_user_1',
350
+ role: 'user',
351
+ content: 'Use the stored brand assets and make me an image direction for the AI SEO article.',
352
+ status: 'complete',
353
+ mode: 'text',
354
+ createdAt: '2026-07-02T16:32:00.000Z',
355
+ },
356
+ {
357
+ id: 'msg_image_assistant_1',
358
+ role: 'assistant',
359
+ agentId: 'agent_image',
360
+ status: 'complete',
361
+ mode: 'text',
362
+ createdAt: '2026-07-02T16:32:18.000Z',
363
+ parts: [
364
+ {
365
+ id: 'tool_assets_seed',
366
+ type: 'tool-call',
367
+ name: 'brand_assets',
368
+ label: 'Brand asset lookup',
369
+ status: 'complete',
370
+ args: { websiteId: 'site_flexai_demo', kind: 'article-hero' },
371
+ result: { images: 4, palette: ['canvas', 'primary', 'success'], preferredAspectRatio: '16:9' },
372
+ },
373
+ {
374
+ id: 'part_image_answer',
375
+ type: 'text',
376
+ status: 'complete',
377
+ text: 'Create a quiet operations-style hero: product dashboard on a laptop, keyword clusters visible as cards, and a small review queue panel. Keep it editorial, not sci-fi. Store the generated image result against the article so the browser never depends on a temporary blob URL.',
378
+ },
379
+ ],
380
+ },
381
+ ];
382
+ }
383
+
384
+ /**
385
+ * Creates seed messages for the audio handoff conversation.
386
+ *
387
+ * @returns {Array<Record<string, unknown>>} Message records.
388
+ */
389
+ function seedAudioMessages() {
390
+ return [
391
+ {
392
+ id: 'msg_audio_user_1',
393
+ role: 'user',
394
+ content: 'I am switching to voice for this customer summary.',
395
+ status: 'complete',
396
+ mode: 'audio',
397
+ createdAt: '2026-07-01T11:16:00.000Z',
398
+ parts: [
399
+ {
400
+ id: 'part_audio_user',
401
+ type: 'audio',
402
+ label: 'Voice note',
403
+ status: 'complete',
404
+ text: 'Transcript: Customer needs a concise explanation of why their crawl report changed after deployment.',
405
+ },
406
+ ],
407
+ },
408
+ {
409
+ id: 'msg_audio_assistant_1',
410
+ role: 'assistant',
411
+ agentId: 'agent_support',
412
+ status: 'complete',
413
+ mode: 'text',
414
+ createdAt: '2026-07-01T11:17:00.000Z',
415
+ parts: [
416
+ {
417
+ id: 'part_handoff_seed',
418
+ type: 'handoff',
419
+ label: 'Text handoff ready',
420
+ status: 'complete',
421
+ text: 'I can keep the transcript attached and continue in text for editing.',
422
+ },
423
+ {
424
+ id: 'part_audio_answer',
425
+ type: 'text',
426
+ status: 'complete',
427
+ text: 'Draft: The crawl report changed because the deployment altered page metadata and internal links. The updated report reflects the live version now seen by search crawlers.',
428
+ },
429
+ ],
430
+ },
431
+ ];
432
+ }
433
+
434
+ /**
435
+ * Creates a user message from submitted composer content.
436
+ *
437
+ * @param {string} content User content.
438
+ * @param {'text'|'audio'} mode Message mode.
439
+ * @returns {Record<string, unknown>} User message.
440
+ */
441
+ function createUserMessage(content, mode = 'text') {
442
+ const message = {
443
+ id: nextId('msg_user'),
444
+ role: 'user',
445
+ content,
446
+ status: 'complete',
447
+ mode,
448
+ createdAt: nowIso(),
449
+ };
450
+ if (mode === 'audio') {
451
+ message.parts = [
452
+ {
453
+ id: nextId('part_audio'),
454
+ type: 'audio',
455
+ label: 'Audio message',
456
+ status: 'complete',
457
+ text: `Transcript: ${content}`,
458
+ },
459
+ ];
460
+ }
461
+ return message;
462
+ }
463
+
464
+ /**
465
+ * Creates an empty assistant message for a streaming response.
466
+ *
467
+ * @param {Record<string, unknown>} conversation Conversation record.
468
+ * @returns {Record<string, unknown>} Assistant message.
469
+ */
470
+ function createAssistantMessage(conversation) {
471
+ return {
472
+ id: nextId('msg_assistant'),
473
+ role: 'assistant',
474
+ agentId: conversation.agentId,
475
+ status: 'streaming',
476
+ mode: 'text',
477
+ createdAt: nowIso(),
478
+ parts: [],
479
+ };
480
+ }
481
+
482
+ /**
483
+ * Builds a streaming response chunk list.
484
+ *
485
+ * @param {string} prompt User prompt.
486
+ * @param {'text'|'audio'} mode Composer mode.
487
+ * @returns {Array<string>} Text chunks.
488
+ */
489
+ function responseChunks(prompt, mode) {
490
+ const prefix = mode === 'audio'
491
+ ? 'I kept the voice note as source context and switched the editable answer back to text. '
492
+ : '';
493
+ const text = `${prefix}Here is the practical next step: keep the agent answer grounded in stored website facts, show each tool call as inspectable UI, and let the operator approve the final plan before it changes production content.\n\nFor "${prompt}", I would save the conversation, the tool payloads, and the final recommendation as separate records so FlexAI can replay, audit, or render each part differently later.`;
494
+ return text.match(/.{1,42}(\s|$)/g) || [text];
495
+ }
496
+
497
+ /**
498
+ * Finds a conversation by id.
499
+ *
500
+ * @param {Record<string, unknown>} state Mock state.
501
+ * @param {string} conversationId Conversation id.
502
+ * @returns {Record<string, unknown>|undefined} Matching conversation.
503
+ */
504
+ function findConversation(state, conversationId) {
505
+ return state.conversations.find((conversation) => conversation.id === conversationId);
506
+ }
507
+
508
+ /**
509
+ * Appends a message to a conversation if it is not already present.
510
+ *
511
+ * @param {Record<string, unknown>} state Mock state.
512
+ * @param {string} conversationId Conversation id.
513
+ * @param {Record<string, unknown>} message Message record.
514
+ * @returns {void}
515
+ */
516
+ function appendMessage(state, conversationId, message) {
517
+ const messages = state.messagesByConversationId[conversationId] || [];
518
+ if (!messages.some((item) => item.id === message.id)) messages.push(message);
519
+ state.messagesByConversationId[conversationId] = messages;
520
+ }
521
+
522
+ /**
523
+ * Sorts conversations with the newest updated item first.
524
+ *
525
+ * @param {Array<Record<string, unknown>>} conversations Conversation records.
526
+ * @returns {Array<Record<string, unknown>>} Sorted records.
527
+ */
528
+ function sortConversations(conversations) {
529
+ return [...conversations].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
530
+ }
531
+
532
+ /**
533
+ * Creates response metadata for an API action.
534
+ *
535
+ * @param {string} action Action label.
536
+ * @returns {Record<string, unknown>} Metadata object.
537
+ */
538
+ function responseMeta(action) {
539
+ return {
540
+ action,
541
+ requestedAt: nowIso(),
542
+ };
543
+ }
544
+
545
+ /**
546
+ * Creates a short conversation summary from a prompt.
547
+ *
548
+ * @param {string} prompt User prompt.
549
+ * @returns {string} Summary text.
550
+ */
551
+ function summaryFromPrompt(prompt) {
552
+ const trimmed = String(prompt || '').trim();
553
+ if (!trimmed) return 'Agent response completed.';
554
+ return trimmed.length > 86 ? `${trimmed.slice(0, 83)}...` : trimmed;
555
+ }
556
+
557
+ /**
558
+ * Waits for a duration and aborts if the signal has been cancelled.
559
+ *
560
+ * @param {number} ms Delay in milliseconds.
561
+ * @param {AbortSignal} signal Abort signal.
562
+ * @returns {Promise<void>} Resolves after the delay.
563
+ */
564
+ async function waitAndCheck(ms, signal) {
565
+ await wait(ms);
566
+ if (signal?.aborted) throw abortError();
567
+ }
568
+
569
+ /**
570
+ * Waits for a duration.
571
+ *
572
+ * @param {number} ms Delay in milliseconds.
573
+ * @returns {Promise<void>} Resolves after the delay.
574
+ */
575
+ function wait(ms) {
576
+ return new Promise((resolve) => {
577
+ setTimeout(resolve, ms);
578
+ });
579
+ }
580
+
581
+ /**
582
+ * Creates a DOM-style abort error.
583
+ *
584
+ * @returns {Error} Abort error.
585
+ */
586
+ function abortError() {
587
+ const error = new Error('The agent response was stopped.');
588
+ error.name = 'AbortError';
589
+ return error;
590
+ }
591
+
592
+ /**
593
+ * Returns an ISO timestamp for the current time.
594
+ *
595
+ * @returns {string} ISO timestamp.
596
+ */
597
+ function nowIso() {
598
+ return new Date().toISOString();
599
+ }
600
+
601
+ /**
602
+ * Creates a stable demo id.
603
+ *
604
+ * @param {string} prefix ID prefix.
605
+ * @returns {string} Stable id.
606
+ */
607
+ function nextId(prefix) {
608
+ idCounter += 1;
609
+ return `${prefix}_${idCounter}`;
610
+ }
611
+
612
+ /**
613
+ * Clones JSON-safe mock data.
614
+ *
615
+ * @param {unknown} value Source value.
616
+ * @returns {unknown} Cloned value.
617
+ */
618
+ function clone(value) {
619
+ return JSON.parse(JSON.stringify(value));
620
+ }