@swiss-ai-hub/web 0.290.11

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 (313) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +479 -0
  3. package/app.config.ts +1 -0
  4. package/app.vue +52 -0
  5. package/assets/css/main.css +4 -0
  6. package/assets/images/logo.png +0 -0
  7. package/components/Agent/Avatar.vue +40 -0
  8. package/components/Agent/Card.vue +139 -0
  9. package/components/Agent/Configuration.vue +20 -0
  10. package/components/Agent/CreateModal.vue +287 -0
  11. package/components/Agent/EmptyCard.vue +35 -0
  12. package/components/Agent/List.vue +85 -0
  13. package/components/Agent/TemplateCard.vue +58 -0
  14. package/components/AppLoader.vue +55 -0
  15. package/components/Chat/Message.vue +90 -0
  16. package/components/Chat/SourceNodes.vue +120 -0
  17. package/components/Chat/Thread.vue +134 -0
  18. package/components/Costs/Table.vue +56 -0
  19. package/components/Dashboard/Component/BarChart.vue +46 -0
  20. package/components/Dashboard/Component/LineChart.vue +138 -0
  21. package/components/Dashboard/Component/Number.vue +31 -0
  22. package/components/Dashboard/Grid.vue +214 -0
  23. package/components/Dashboard/Item.vue +75 -0
  24. package/components/Dashboard/Trend.vue +34 -0
  25. package/components/Display/List/DisplayList.vue +93 -0
  26. package/components/Evaluation/Dataset/Card.vue +70 -0
  27. package/components/Evaluation/Dataset/Create.vue +81 -0
  28. package/components/Evaluation/Dataset/Edit.vue +132 -0
  29. package/components/Event/Display/AddMemoryToChatHistoryEvent.vue +60 -0
  30. package/components/Event/Display/AgentInTheLoopRequestEvent.vue +56 -0
  31. package/components/Event/Display/AgentInTheLoopResponseEvent.vue +17 -0
  32. package/components/Event/Display/Base.vue +125 -0
  33. package/components/Event/Display/BaseRetrieveMemoryEvent.vue +101 -0
  34. package/components/Event/Display/BaseStoreMemoryEvent.vue +182 -0
  35. package/components/Event/Display/ChunkEvent.vue +40 -0
  36. package/components/Event/Display/EmbeddingEvent.vue +25 -0
  37. package/components/Event/Display/ExceptionEvent.vue +21 -0
  38. package/components/Event/Display/GuardAcceptEvent.vue +25 -0
  39. package/components/Event/Display/GuardEvent.vue +17 -0
  40. package/components/Event/Display/GuardRejectionEvent.vue +25 -0
  41. package/components/Event/Display/HumanInTheLoopRequestEvent.vue +53 -0
  42. package/components/Event/Display/HumanInTheLoopResponseEvent.vue +40 -0
  43. package/components/Event/Display/LLMCostEvent.vue +25 -0
  44. package/components/Event/Display/LLMEvent.vue +92 -0
  45. package/components/Event/Display/LimitChatHistoryEvent.vue +60 -0
  46. package/components/Event/Display/RAGFailureStopEvent.vue +28 -0
  47. package/components/Event/Display/RAGStartEvent.vue +77 -0
  48. package/components/Event/Display/RAGSuccessStopEvent.vue +16 -0
  49. package/components/Event/Display/RawDataContent.vue +69 -0
  50. package/components/Event/Display/RerankerEvent.vue +39 -0
  51. package/components/Event/Display/RetrieverEvent.vue +22 -0
  52. package/components/Event/Display/RouterEvent.vue +54 -0
  53. package/components/Event/Display/StandaloneQuestionCondenserEvent.vue +38 -0
  54. package/components/Event/Display/StopEvent.vue +16 -0
  55. package/components/Event/Display/ThoughtEvent.vue +20 -0
  56. package/components/Event/Display/ToolEvent.vue +38 -0
  57. package/components/Event/Display/UnknownEvent.vue +17 -0
  58. package/components/Event/Display/UserMessageEvent.vue +35 -0
  59. package/components/Event/List/EventList.vue +249 -0
  60. package/components/Event/Statistics.vue +49 -0
  61. package/components/Event/Timeseries.vue +224 -0
  62. package/components/FormKit/AgentSelector.vue +307 -0
  63. package/components/FormKit/ChipsInput.vue +62 -0
  64. package/components/FormKit/DynamicConfiguration.vue +155 -0
  65. package/components/FormKit/IconSelector.vue +72 -0
  66. package/components/FormKit/KnowledgeDatabaseSelector.vue +92 -0
  67. package/components/FormKit/LocaleInput.vue +150 -0
  68. package/components/FormKit/ModelSelect.vue +110 -0
  69. package/components/FormKit/Repeater.vue +93 -0
  70. package/components/FormKit/VectorStoreInput.vue +247 -0
  71. package/components/Knowledge/Document/List.vue +140 -0
  72. package/components/Knowledge/Document/Overview.vue +28 -0
  73. package/components/Knowledge/Document/UploadModal.vue +298 -0
  74. package/components/Knowledge/Document/WithNodes.vue +105 -0
  75. package/components/Knowledge/Namespace/Card.vue +108 -0
  76. package/components/Knowledge/Namespace/CreateModal.vue +203 -0
  77. package/components/Knowledge/Namespace/EditModal.vue +134 -0
  78. package/components/Knowledge/Namespace/EmptyCard.vue +35 -0
  79. package/components/Knowledge/Node/Content.vue +71 -0
  80. package/components/Markdown/Renderer.vue +87 -0
  81. package/components/Memory/DetailPage.vue +48 -0
  82. package/components/Memory/Edit.vue +241 -0
  83. package/components/Memory/Graph.vue +318 -0
  84. package/components/Memory/List.vue +155 -0
  85. package/components/Memory/MemoryManagementPage.vue +178 -0
  86. package/components/Memory/OpenWebUIContent.vue +96 -0
  87. package/components/Memory/PageLayout.vue +72 -0
  88. package/components/Models/ModelDetailsPanel.vue +250 -0
  89. package/components/Models/NamespaceCard.vue +79 -0
  90. package/components/Navigation/Left.vue +85 -0
  91. package/components/Navigation/Top.vue +31 -0
  92. package/components/Notification/Item.vue +88 -0
  93. package/components/Notification/NotificationsOverlay.vue +164 -0
  94. package/components/Process/Card.vue +119 -0
  95. package/components/Process/Configuration.vue +20 -0
  96. package/components/Process/CreateModal.vue +276 -0
  97. package/components/Process/EmptyCard.vue +35 -0
  98. package/components/Process/Form.vue +153 -0
  99. package/components/Process/Starts.vue +44 -0
  100. package/components/Process/Walkthrough/List.vue +162 -0
  101. package/components/Role/AccessRulesEditor.vue +132 -0
  102. package/components/Role/Card.vue +68 -0
  103. package/components/Role/Create.vue +55 -0
  104. package/components/Role/Edit.vue +82 -0
  105. package/components/Role/UsageLimitsEditor.vue +225 -0
  106. package/components/Service/Selection.vue +148 -0
  107. package/components/Structural/Column.vue +74 -0
  108. package/components/Structural/Screen.vue +10 -0
  109. package/components/Structural/Substructure.vue +5 -0
  110. package/components/Tenant/Switcher.vue +102 -0
  111. package/components/Thread/Details.vue +135 -0
  112. package/components/Thread/Hierarchy.vue +136 -0
  113. package/components/Thread/Info.vue +41 -0
  114. package/components/Thread/List.vue +136 -0
  115. package/components/User/Bar.vue +74 -0
  116. package/components/User/List.vue +86 -0
  117. package/components/User/RoleChips.vue +83 -0
  118. package/components/User/Settings.vue +79 -0
  119. package/components/Workflow/Modal.vue +39 -0
  120. package/components/Workflow/NodeCard.vue +41 -0
  121. package/components/Workflow/StartNode.vue +24 -0
  122. package/components/Workflow/StepNode.vue +27 -0
  123. package/components/Workflow/StopNode.vue +24 -0
  124. package/components/Workflow/Visualization.vue +265 -0
  125. package/components/mdc/MarkdownFigure.vue +9 -0
  126. package/components/mdc/MarkdownTable.vue +9 -0
  127. package/components/mdc/ResolveImageComponent.vue +58 -0
  128. package/composables/agent/useAgentClass.ts +27 -0
  129. package/composables/agent/useAgentClassInstances.ts +27 -0
  130. package/composables/agent/useAgentClasses.ts +27 -0
  131. package/composables/agent/useAgentIconFromThread.ts +8 -0
  132. package/composables/agent/useAgentInstance.ts +28 -0
  133. package/composables/agent/useAgentInstanceThreads.ts +76 -0
  134. package/composables/agent/useAgentInstances.ts +25 -0
  135. package/composables/agent/useAgentNavigation.ts +35 -0
  136. package/composables/agent/useCreateAgentInstance.ts +33 -0
  137. package/composables/agent/useDeleteAgentInstance.ts +31 -0
  138. package/composables/agent/useUpdateAgentInstance.ts +40 -0
  139. package/composables/auth/useAuth.ts +54 -0
  140. package/composables/auth/useAuthProviders.ts +14 -0
  141. package/composables/chat/useChatCompletions.ts +30 -0
  142. package/composables/dashboard/useAgentNameFromDashboardWidget.ts +27 -0
  143. package/composables/dashboard/useDashboardComponent.ts +27 -0
  144. package/composables/dashboard/useSaveDashboard.ts +21 -0
  145. package/composables/document/useCreateNamespace.ts +26 -0
  146. package/composables/document/useDatabases.ts +23 -0
  147. package/composables/document/useDocument.ts +29 -0
  148. package/composables/document/useDocumentUrl.ts +20 -0
  149. package/composables/document/useDocuments.ts +107 -0
  150. package/composables/document/useNodes.ts +29 -0
  151. package/composables/document/useSummaryNodes.ts +32 -0
  152. package/composables/document/useUpdateNamespace.ts +22 -0
  153. package/composables/evaluation/useCreateDataset.ts +19 -0
  154. package/composables/evaluation/useDataset.ts +26 -0
  155. package/composables/evaluation/useDatasets.ts +25 -0
  156. package/composables/evaluation/useUpdateDataset.ts +23 -0
  157. package/composables/event/useBasicEventStatistics.ts +83 -0
  158. package/composables/event/useEventColor.ts +25 -0
  159. package/composables/event/useEventComponent.ts +87 -0
  160. package/composables/event/useEventTimeseries.ts +39 -0
  161. package/composables/event/useEventTimeseriesStats.ts +26 -0
  162. package/composables/file/useFileUpload.ts +91 -0
  163. package/composables/file/useSupportedFileTypes.ts +22 -0
  164. package/composables/form/useCreateInstanceForm.ts +251 -0
  165. package/composables/form/useFormKitTransform.ts +753 -0
  166. package/composables/memory/useMemoryCRUD.ts +88 -0
  167. package/composables/memory/useMemoryFactory.ts +319 -0
  168. package/composables/memory/useMemorySearchFilter.ts +74 -0
  169. package/composables/models/useModelsList.ts +24 -0
  170. package/composables/models/useSingleModel.ts +30 -0
  171. package/composables/notification/useNotificationPoller.ts +58 -0
  172. package/composables/notification/useNotifications.ts +57 -0
  173. package/composables/notification/useUpdateMultipleNotifications.ts +17 -0
  174. package/composables/notification/useUpdateNotification.ts +17 -0
  175. package/composables/process/useCreateProcessInstance.ts +32 -0
  176. package/composables/process/useDeleteProcessInstance.ts +31 -0
  177. package/composables/process/useProcessClasses.ts +27 -0
  178. package/composables/process/useProcessInstance.ts +28 -0
  179. package/composables/process/useProcessInstances.ts +27 -0
  180. package/composables/process/useProcessWalkthroughs.ts +73 -0
  181. package/composables/process/useSendProcessStartForm.ts +43 -0
  182. package/composables/process/useUpdateProcessInstance.ts +40 -0
  183. package/composables/role/useCreateRole.ts +19 -0
  184. package/composables/role/useDeleteRole.ts +21 -0
  185. package/composables/role/useRole.ts +30 -0
  186. package/composables/role/useRoles.ts +25 -0
  187. package/composables/role/useUpdateRole.ts +22 -0
  188. package/composables/suite/useApps.ts +31 -0
  189. package/composables/suite/useSuite.ts +26 -0
  190. package/composables/tenant/useActiveTenant.ts +27 -0
  191. package/composables/tenant/useSysadminNavigation.ts +19 -0
  192. package/composables/tenant/useTenant.ts +38 -0
  193. package/composables/tenant/useTenantMemberships.ts +15 -0
  194. package/composables/tenant/useTenantPath.ts +20 -0
  195. package/composables/tenant/useTenantPolling.ts +30 -0
  196. package/composables/tenant/useTenantReady.ts +12 -0
  197. package/composables/theme/useDarkMode.ts +5 -0
  198. package/composables/thread/useThread.ts +27 -0
  199. package/composables/thread/useThreadEvents.ts +91 -0
  200. package/composables/thread/useThreadUtils.ts +49 -0
  201. package/composables/thread/useThreads.ts +64 -0
  202. package/composables/thread/useThreadsInfinite.ts +56 -0
  203. package/composables/translation/useTranslate.ts +20 -0
  204. package/composables/useRouteReady.ts +21 -0
  205. package/composables/useTimeAgo.ts +40 -0
  206. package/composables/user/useAssignRoleToUser.ts +22 -0
  207. package/composables/user/useMyUser.ts +25 -0
  208. package/composables/user/useRevokeRoleFromUser.ts +21 -0
  209. package/composables/user/useUser.ts +30 -0
  210. package/composables/user/useUsers.ts +63 -0
  211. package/composables/utils/useJsonTree.ts +138 -0
  212. package/formkit.config.ts +44 -0
  213. package/i18n/locales/de.yaml +815 -0
  214. package/i18n/locales/en.yaml +804 -0
  215. package/i18n/locales/fr.yaml +812 -0
  216. package/i18n/locales/it.yaml +808 -0
  217. package/layouts/anonymous.vue +8 -0
  218. package/layouts/default.vue +116 -0
  219. package/middleware/auth.global.ts +62 -0
  220. package/nuxt.config.ts +145 -0
  221. package/package.json +114 -0
  222. package/pages/[tenant]/index.vue +31 -0
  223. package/pages/[tenant]/notifications/index.vue +235 -0
  224. package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/chat.vue +67 -0
  225. package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/configuration.vue +122 -0
  226. package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/memories/[memory_id].vue +3 -0
  227. package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/memories.vue +20 -0
  228. package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/overview.vue +72 -0
  229. package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/threads.vue +52 -0
  230. package/pages/[tenant]/service/agents/[agent_class]-[agent_id]/workflow.vue +19 -0
  231. package/pages/[tenant]/service/agents/[agent_class]-[agent_id].vue +63 -0
  232. package/pages/[tenant]/service/agents/templates.vue +102 -0
  233. package/pages/[tenant]/service/agents.vue +185 -0
  234. package/pages/[tenant]/service/datasets/[dataset_id].vue +81 -0
  235. package/pages/[tenant]/service/datasets.vue +53 -0
  236. package/pages/[tenant]/service/health/index.vue +3 -0
  237. package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id]/nodes.vue +20 -0
  238. package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id]/overview.vue +40 -0
  239. package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id]/summary.vue +88 -0
  240. package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +48 -0
  241. package/pages/[tenant]/service/knowledge/[db]/[namespace].vue +144 -0
  242. package/pages/[tenant]/service/knowledge.vue +126 -0
  243. package/pages/[tenant]/service/models/[model_name].vue +84 -0
  244. package/pages/[tenant]/service/models.vue +61 -0
  245. package/pages/[tenant]/service/my-account.vue +117 -0
  246. package/pages/[tenant]/service/openai/[thread_id]/[display_id]/memories.vue +66 -0
  247. package/pages/[tenant]/service/openai/[thread_id]/[display_id]/sources.vue +100 -0
  248. package/pages/[tenant]/service/openai/[thread_id]/[display_id]/tracing.vue +49 -0
  249. package/pages/[tenant]/service/openai.vue +101 -0
  250. package/pages/[tenant]/service/organization-memories/graph.vue +97 -0
  251. package/pages/[tenant]/service/organization-memories/list/[memory_id].vue +3 -0
  252. package/pages/[tenant]/service/organization-memories/list.vue +150 -0
  253. package/pages/[tenant]/service/organization-memories.vue +3 -0
  254. package/pages/[tenant]/service/processes/[process_class]-[process_id]/[process_walkthrough_id].vue +7 -0
  255. package/pages/[tenant]/service/processes/[process_class]-[process_id]/configuration.vue +106 -0
  256. package/pages/[tenant]/service/processes/[process_class]-[process_id]/overview.vue +67 -0
  257. package/pages/[tenant]/service/processes/[process_class]-[process_id]/start.vue +26 -0
  258. package/pages/[tenant]/service/processes/[process_class]-[process_id]/walkthroughs/[process_walkthrough_id]/overview.vue +14 -0
  259. package/pages/[tenant]/service/processes/[process_class]-[process_id]/walkthroughs.vue +54 -0
  260. package/pages/[tenant]/service/processes/[process_class]-[process_id].vue +60 -0
  261. package/pages/[tenant]/service/processes.vue +129 -0
  262. package/pages/[tenant]/service/roles/[role_id].vue +54 -0
  263. package/pages/[tenant]/service/roles.vue +84 -0
  264. package/pages/[tenant]/service/threads/[thread_id]/chat.vue +51 -0
  265. package/pages/[tenant]/service/threads/[thread_id]/display/[display_id].vue +21 -0
  266. package/pages/[tenant]/service/threads/[thread_id]/display.vue +29 -0
  267. package/pages/[tenant]/service/threads/[thread_id]/hierarchy.vue +14 -0
  268. package/pages/[tenant]/service/threads/[thread_id]/memories/[memory_id].vue +3 -0
  269. package/pages/[tenant]/service/threads/[thread_id]/memories.vue +19 -0
  270. package/pages/[tenant]/service/threads/[thread_id]/overview.vue +100 -0
  271. package/pages/[tenant]/service/threads/[thread_id].vue +54 -0
  272. package/pages/[tenant]/service/threads.vue +52 -0
  273. package/pages/[tenant]/service/user-memories/graph.vue +97 -0
  274. package/pages/[tenant]/service/user-memories/list/[memory_id].vue +3 -0
  275. package/pages/[tenant]/service/user-memories/list.vue +150 -0
  276. package/pages/[tenant]/service/user-memories.vue +3 -0
  277. package/pages/[tenant]/service/users/[user_id].vue +117 -0
  278. package/pages/[tenant]/service/users.vue +88 -0
  279. package/pages/auth/callback.vue +52 -0
  280. package/pages/auth/login.vue +80 -0
  281. package/pages/auth/renew.vue +24 -0
  282. package/pages/index.vue +59 -0
  283. package/pages/select-tenant.vue +76 -0
  284. package/plugins/0.runtime-config.client.ts +55 -0
  285. package/plugins/apexcharts.client.ts +5 -0
  286. package/plugins/api-client.client.ts +38 -0
  287. package/plugins/dark-mode.client.ts +12 -0
  288. package/plugins/keycloak-client.ts +41 -0
  289. package/plugins/oidc-client.ts +78 -0
  290. package/sdk/client/client/client.gen.ts +237 -0
  291. package/sdk/client/client/index.ts +24 -0
  292. package/sdk/client/client/types.gen.ts +213 -0
  293. package/sdk/client/client/utils.gen.ts +407 -0
  294. package/sdk/client/client.gen.ts +25 -0
  295. package/sdk/client/core/auth.gen.ts +42 -0
  296. package/sdk/client/core/bodySerializer.gen.ts +96 -0
  297. package/sdk/client/core/params.gen.ts +181 -0
  298. package/sdk/client/core/pathSerializer.gen.ts +180 -0
  299. package/sdk/client/core/queryKeySerializer.gen.ts +136 -0
  300. package/sdk/client/core/serverSentEvents.gen.ts +265 -0
  301. package/sdk/client/core/types.gen.ts +118 -0
  302. package/sdk/client/core/utils.gen.ts +143 -0
  303. package/sdk/client/index.ts +1013 -0
  304. package/sdk/client/schemas.gen.ts +35395 -0
  305. package/sdk/client/sdk.gen.ts +3438 -0
  306. package/sdk/client/transformers.gen.ts +143 -0
  307. package/sdk/client/types.gen.ts +27567 -0
  308. package/tailwind.config.mjs +27 -0
  309. package/themes/aihub-theme.ts +125 -0
  310. package/types/DashboardWidget.ts +13 -0
  311. package/types/EventChartInput.ts +7 -0
  312. package/types/NavItem.ts +6 -0
  313. package/types/TimeseriesInput.ts +7 -0
@@ -0,0 +1,753 @@
1
+ import type { FormKitSchemaNode } from '@formkit/core'
2
+
3
+ export interface RepeaterConfig {
4
+ name: string
5
+ path: string // Full path for nested data access (e.g., "few_shot.few_shot_examples")
6
+ label?: string
7
+ addLabel?: string
8
+ childrenSchema: FormKitSchemaNode[]
9
+ min?: number
10
+ max?: number
11
+ }
12
+
13
+ export interface GroupConfig {
14
+ name: string
15
+ label?: string
16
+ schema: FormKitSchemaNode[]
17
+ }
18
+
19
+ export interface CategorizedElements {
20
+ simpleElements: FormElement[]
21
+ groupElements: FormElement[]
22
+ repeaterElements: FormElement[]
23
+ }
24
+
25
+ export type FormElement = Record<string, unknown>
26
+
27
+ /**
28
+ * Gets a nested value from an object using a dot-separated path.
29
+ * Creates intermediate objects if they don't exist.
30
+ */
31
+ export function getNestedValue(
32
+ obj: Record<string, unknown>,
33
+ path: string,
34
+ ): Record<string, unknown>[] {
35
+ const parts = path.split('.')
36
+ let current: Record<string, unknown> = obj
37
+
38
+ for (let i = 0; i < parts.length - 1; i++) {
39
+ const key = parts[i]
40
+ current[key] ??= {}
41
+ current = current[key] as Record<string, unknown>
42
+ }
43
+
44
+ const lastKey = parts[parts.length - 1]
45
+ if (!Array.isArray(current[lastKey])) {
46
+ current[lastKey] = []
47
+ }
48
+
49
+ return current[lastKey] as Record<string, unknown>[]
50
+ }
51
+
52
+ /**
53
+ * Sets a nested array value in an object using a dot-separated path.
54
+ * Creates intermediate objects if they don't exist.
55
+ */
56
+ export function setNestedValue(
57
+ obj: Record<string, unknown>,
58
+ path: string,
59
+ value: Record<string, unknown>[],
60
+ ): void {
61
+ const parts = path.split('.')
62
+ let current: Record<string, unknown> = obj
63
+
64
+ for (let i = 0; i < parts.length - 1; i++) {
65
+ const key = parts[i]
66
+ current[key] ??= {}
67
+ current = current[key] as Record<string, unknown>
68
+ }
69
+
70
+ current[parts[parts.length - 1]] = value
71
+ }
72
+
73
+ /**
74
+ * Gets the FormKit type from an element, checking both 'formkit' and '$formkit' properties.
75
+ */
76
+ export function getFormkitType(element: FormElement): unknown {
77
+ return element.formkit || element.$formkit
78
+ }
79
+
80
+ /**
81
+ * Wraps a FormKit schema node in a fieldset with a legend label.
82
+ * Optionally applies a condition to the fieldset wrapper.
83
+ */
84
+ export function wrapInFieldset(
85
+ label: string,
86
+ node: FormKitSchemaNode,
87
+ condition?: string,
88
+ key?: string,
89
+ ): FormKitSchemaNode[] {
90
+ const fieldset: Record<string, unknown> = {
91
+ $el: 'fieldset',
92
+ attrs: { class: 'formkit-group-fieldset' },
93
+ children: [
94
+ {
95
+ $el: 'legend',
96
+ attrs: { class: 'text-sm font-semibold px-2 text-surface-700 dark:text-surface-300' },
97
+ children: label,
98
+ },
99
+ node,
100
+ ],
101
+ }
102
+
103
+ // Apply condition to fieldset wrapper so the entire section hides
104
+ if (condition) {
105
+ fieldset.if = condition
106
+ }
107
+
108
+ // Add key to prevent Vue from reusing DOM elements between conditional fieldsets
109
+ if (key) {
110
+ fieldset.key = key
111
+ }
112
+
113
+ return [fieldset] as FormKitSchemaNode[]
114
+ }
115
+
116
+ /**
117
+ * Gets a localized string from a value that may be a string or a locale object.
118
+ */
119
+ export function getLocalizedString(value: unknown, locale: string): string | undefined {
120
+ if (!value) return undefined
121
+ if (typeof value === 'string') return value
122
+ if (typeof value === 'object') {
123
+ const localeObj = value as Record<string, string>
124
+ return localeObj[locale] || localeObj.en || Object.values(localeObj)[0]
125
+ }
126
+ return String(value)
127
+ }
128
+
129
+ export interface TransformOptions {
130
+ locale?: string
131
+ labelTransform?: (label: string) => string
132
+ optionsResolver?: (element: FormElement) => unknown[] | undefined
133
+ }
134
+
135
+ function createGroupNode(
136
+ element: FormElement,
137
+ children: FormKitSchemaNode[],
138
+ label: string | undefined,
139
+ ): FormKitSchemaNode | FormKitSchemaNode[] {
140
+ const groupNode: Record<string, unknown> = {
141
+ $formkit: 'group',
142
+ name: element.name as string,
143
+ children,
144
+ // Keep the group's data in the form context if its `if:` condition (or its
145
+ // wrapping fieldset's) flips false — without preserve, an unmounted group
146
+ // drops every nested value.
147
+ preserve: true,
148
+ }
149
+
150
+ // Preserve id for $get() references in conditionals
151
+ if (element.id) groupNode.id = element.id
152
+
153
+ // Add key for Vue to prevent DOM reuse between conditional groups
154
+ // This is critical when sibling groups have fields with the same names
155
+ groupNode.key = element.id || element.name
156
+
157
+ const condition = element.if as string | undefined
158
+ const key = (element.id || element.name) as string | undefined
159
+
160
+ // When wrapped in fieldset, apply condition to fieldset (outer wrapper)
161
+ // Otherwise apply to the group itself
162
+ if (label) {
163
+ return wrapInFieldset(label, groupNode as FormKitSchemaNode, condition, key)
164
+ }
165
+
166
+ if (condition) groupNode.if = condition
167
+ return groupNode as FormKitSchemaNode
168
+ }
169
+
170
+ function resolveElementOptions(
171
+ element: FormElement,
172
+ optionsResolver?: (element: FormElement) => unknown[] | undefined,
173
+ ): unknown[] | undefined {
174
+ if (optionsResolver) {
175
+ return optionsResolver(element)
176
+ }
177
+ return element.options as unknown[] | undefined
178
+ }
179
+
180
+ function buildLocaleInputProperties(
181
+ element: FormElement,
182
+ label: string | undefined,
183
+ locale: string,
184
+ ): Record<string, unknown> {
185
+ // `preserve: true` keeps the input's value in the form context when an `if:`
186
+ // condition (nullable toggle or backend-supplied `condition_if`) unmounts it.
187
+ const cleanNode: Record<string, unknown> = { $formkit: 'localeInput', preserve: true }
188
+
189
+ if (element.name) cleanNode.name = element.name
190
+ if (label) cleanNode.label = label
191
+
192
+ // Preserve id for $get() references in conditionals
193
+ if (element.id) cleanNode.id = element.id
194
+
195
+ // Stable key prevents Vue from reusing this DOM node across sibling schema entries
196
+ // (critical when conditional `if:` siblings mount/unmount around it). `element.id`
197
+ // is auto-assigned per FormkitElement and `element.name` is FormKit-unique within
198
+ // a group by construction, so collisions cannot occur via this fallback chain.
199
+ cleanNode.key = (element.id as string | undefined) ?? (element.name as string)
200
+
201
+ // Preserve conditional visibility (FormKit uses 'if' for schema conditionals)
202
+ if (element.if) cleanNode.if = element.if
203
+
204
+ const help = getLocalizedString(element.help, locale)
205
+ if (help) cleanNode.help = help
206
+
207
+ // For localeInput, pass placeholder as full LocaleString object (not localized)
208
+ if (element.placeholder) cleanNode.placeholder = element.placeholder
209
+
210
+ // Pass inputType and rows props (support both camelCase and snake_case for backwards compatibility)
211
+ const inputType = element.inputType || element.input_type
212
+ if (inputType) cleanNode.inputType = inputType
213
+ if (element.rows !== undefined) cleanNode.rows = element.rows
214
+
215
+ if (element.validation) cleanNode.validation = element.validation
216
+
217
+ return cleanNode
218
+ }
219
+
220
+ // Fields that should be excluded from passthrough (internal markers, already handled, or need transformation)
221
+ const EXCLUDED_FIELDS = new Set([
222
+ 'is_formkit_element', // Internal marker
223
+ 'formkit', // Handled separately as $formkit
224
+ '$formkit', // Handled separately
225
+ 'label', // Transformed via getLocalizedString
226
+ 'help', // Transformed via getLocalizedString
227
+ 'placeholder', // Transformed via getLocalizedString
228
+ 'children', // Handled separately for recursion
229
+ 'nullable', // Wrapper-level signal for the transform; never a FormKit/PrimeVue prop
230
+ // Backend serialises the Pydantic default into element.value (form duality). FormKit pushes
231
+ // schema `value` up to the parent v-model on input registration, which would clobber the
232
+ // loaded data with the backend default. Defaults belong in data, seeded via seedFormDefaults.
233
+ 'value',
234
+ ])
235
+
236
+ function buildNodeProperties(
237
+ element: FormElement,
238
+ formkitType: unknown,
239
+ label: string | undefined,
240
+ locale: string,
241
+ optionsResolver?: (element: FormElement) => unknown[] | undefined,
242
+ ): Record<string, unknown> {
243
+ // Handle localeInput specially
244
+ if (formkitType === 'localeInput') {
245
+ return buildLocaleInputProperties(element, label, locale)
246
+ }
247
+
248
+ // `preserve: true` keeps the input's value in the form context when an `if:`
249
+ // condition (nullable toggle or backend-supplied `condition_if`) unmounts it.
250
+ const cleanNode: Record<string, unknown> = { $formkit: formkitType, preserve: true }
251
+
252
+ // Pass through all properties except excluded ones
253
+ for (const [key, value] of Object.entries(element)) {
254
+ if (EXCLUDED_FIELDS.has(key)) continue
255
+ if (value === undefined || value === null) continue
256
+ cleanNode[key] = value
257
+ }
258
+
259
+ // Apply transformations for localized fields
260
+ if (label) cleanNode.label = label
261
+
262
+ const help = getLocalizedString(element.help, locale)
263
+ if (help) cleanNode.help = help
264
+
265
+ const placeholder = getLocalizedString(element.placeholder, locale)
266
+ if (placeholder) cleanNode.placeholder = placeholder
267
+
268
+ // Resolve options if resolver provided
269
+ const options = resolveElementOptions(element, optionsResolver)
270
+ if (options) cleanNode.options = options
271
+
272
+ // Stable key prevents Vue from reusing this DOM node across sibling schema entries
273
+ // (critical when conditional `if:` siblings mount/unmount around it). `element.id`
274
+ // is auto-assigned per FormkitElement and `element.name` is FormKit-unique within
275
+ // a group by construction, so collisions cannot occur via this fallback chain.
276
+ cleanNode.key = (element.id as string | undefined) ?? (element.name as string)
277
+
278
+ return cleanNode
279
+ }
280
+
281
+ /**
282
+ * Builds the toggle name used to gate a nullable form element.
283
+ */
284
+ export function nullableToggleName(fieldName: string): string {
285
+ return `__${fieldName}__enabled`
286
+ }
287
+
288
+ /**
289
+ * Builds a unique FormKit input id for the synthetic toggle that gates a nullable element.
290
+ * Uses the element's existing id (backend ref) when available so the id is unique across
291
+ * the whole form schema; falls back to the field name. Dots are replaced because FormKit
292
+ * `$get()` lookups expect slug-style ids.
293
+ */
294
+ function nullableToggleId(element: FormElement): string {
295
+ const base = (element.id as string | undefined) ?? (element.name as string)
296
+ return `${base.replace(/\./g, '__')}__enabled_toggle`
297
+ }
298
+
299
+ /**
300
+ * Combines a synthetic toggle condition with any existing condition_if.
301
+ */
302
+ function combineConditions(toggleCondition: string, existing: string | undefined): string {
303
+ if (!existing) return toggleCondition
304
+ if (existing.startsWith('$:')) {
305
+ return `$: ${toggleCondition.slice(1)} && (${existing.slice(2).trim()})`
306
+ }
307
+ return `$: ${toggleCondition.slice(1)} && (${existing.slice(1)})`
308
+ }
309
+
310
+ function buildNullableToggleNode(element: FormElement, label: string | undefined): Record<string, unknown> {
311
+ const fieldName = element.name as string
312
+ const toggleId = nullableToggleId(element)
313
+ return {
314
+ $formkit: 'primeCheckbox',
315
+ name: nullableToggleName(fieldName),
316
+ id: toggleId,
317
+ key: toggleId,
318
+ label: label ? `Enable ${label}` : 'Enable',
319
+ binary: true,
320
+ }
321
+ }
322
+
323
+ function applyNullableToggle(
324
+ element: FormElement,
325
+ baseNode: FormKitSchemaNode | FormKitSchemaNode[],
326
+ label: string | undefined,
327
+ ): FormKitSchemaNode[] {
328
+ const nodeArray = Array.isArray(baseNode) ? baseNode : [baseNode]
329
+ return [buildNullableToggleNode(element, label) as FormKitSchemaNode, ...nodeArray]
330
+ }
331
+
332
+ function gateElement(element: FormElement, toggleCondition: string): FormElement {
333
+ return { ...element, if: combineConditions(toggleCondition, element.if as string | undefined) }
334
+ }
335
+
336
+ /**
337
+ * Transforms a form element to a FormKit schema node.
338
+ * Handles groups specially by wrapping them in fieldsets when they have labels.
339
+ * Skips repeater elements (they are handled separately).
340
+ */
341
+ export function transformElementToSchema(
342
+ element: FormElement,
343
+ options: TransformOptions = {},
344
+ ): FormKitSchemaNode | FormKitSchemaNode[] {
345
+ if (!element) return []
346
+
347
+ const formkitType = getFormkitType(element)
348
+ if (formkitType === 'repeater') return []
349
+
350
+ const { locale = 'en', labelTransform, optionsResolver } = options
351
+
352
+ const children = (element.children as FormElement[] || []).flatMap(
353
+ child => transformElementToSchema(child, options),
354
+ ) as FormKitSchemaNode[]
355
+
356
+ let label = getLocalizedString(element.label, locale)
357
+ if (label && labelTransform) {
358
+ label = labelTransform(label)
359
+ }
360
+
361
+ const isNullable = element.nullable === true
362
+ const toggleCondition = isNullable ? `$get(${nullableToggleId(element)}).value` : undefined
363
+
364
+ if (formkitType === 'group') {
365
+ const gatedElement = isNullable ? gateElement(element, toggleCondition!) : element
366
+ const groupNode = createGroupNode(gatedElement, children, label)
367
+ return isNullable ? applyNullableToggle(element, groupNode, label) : groupNode
368
+ }
369
+
370
+ const cleanNode = buildNodeProperties(element, formkitType, label, locale, optionsResolver)
371
+ if (children.length > 0) cleanNode.children = children
372
+ if (isNullable) {
373
+ cleanNode.if = combineConditions(toggleCondition!, element.if as string | undefined)
374
+ return applyNullableToggle(element, cleanNode as FormKitSchemaNode, label)
375
+ }
376
+
377
+ return cleanNode as FormKitSchemaNode
378
+ }
379
+
380
+ function buildLeafNodeForRepeater(
381
+ element: FormElement,
382
+ formkitType: unknown,
383
+ label: string | undefined,
384
+ locale: string,
385
+ children: FormKitSchemaNode[],
386
+ ): Record<string, unknown> {
387
+ if (formkitType === 'localeInput') {
388
+ return buildLocaleInputProperties(element, label, locale)
389
+ }
390
+
391
+ const cleanNode: Record<string, unknown> = { $formkit: formkitType, preserve: true }
392
+ for (const [key, value] of Object.entries(element)) {
393
+ if (EXCLUDED_FIELDS.has(key)) continue
394
+ if (value === undefined || value === null) continue
395
+ cleanNode[key] = value
396
+ }
397
+ if (label) cleanNode.label = label
398
+ const help = getLocalizedString(element.help, locale)
399
+ if (help) cleanNode.help = help
400
+ if (children.length > 0) cleanNode.children = children
401
+ cleanNode.key = (element.id as string | undefined) ?? (element.name as string)
402
+ return cleanNode
403
+ }
404
+
405
+ /**
406
+ * Transforms a form element for use inside a repeater's children schema.
407
+ */
408
+ export function transformElementForRepeater(
409
+ element: FormElement,
410
+ locale = 'en',
411
+ ): FormKitSchemaNode | FormKitSchemaNode[] {
412
+ if (!element) return []
413
+
414
+ const formkitType = getFormkitType(element)
415
+ const children = (element.children as FormElement[] || []).flatMap(
416
+ child => transformElementForRepeater(child, locale),
417
+ ) as FormKitSchemaNode[]
418
+
419
+ const label = getLocalizedString(element.label, locale)
420
+ const isNullable = element.nullable === true
421
+ const toggleCondition = isNullable ? `$get(${nullableToggleId(element)}).value` : undefined
422
+
423
+ if (formkitType === 'group') {
424
+ const gatedElement = isNullable ? gateElement(element, toggleCondition!) : element
425
+ const groupNode = createGroupNode(gatedElement, children, label)
426
+ return isNullable ? applyNullableToggle(element, groupNode, label) : groupNode
427
+ }
428
+
429
+ const cleanNode = buildLeafNodeForRepeater(element, formkitType, label, locale, children)
430
+ if (isNullable) {
431
+ cleanNode.if = combineConditions(toggleCondition!, element.if as string | undefined)
432
+ return applyNullableToggle(element, cleanNode as FormKitSchemaNode, label)
433
+ }
434
+
435
+ return cleanNode as FormKitSchemaNode
436
+ }
437
+
438
+ /**
439
+ * Extracts repeater configurations from a form definition.
440
+ */
441
+ export function extractRepeaterConfigs(
442
+ formElements: FormElement[],
443
+ locale = 'en',
444
+ parentPath = '',
445
+ ): RepeaterConfig[] {
446
+ if (!formElements || formElements.length === 0) return []
447
+
448
+ const repeaters: RepeaterConfig[] = []
449
+
450
+ for (const element of formElements) {
451
+ const formkitType = getFormkitType(element)
452
+ const elementName = element.name as string
453
+
454
+ if (formkitType === 'repeater') {
455
+ const childrenSchema = (element.children as FormElement[] || []).flatMap(
456
+ child => transformElementForRepeater(child, locale),
457
+ ) as FormKitSchemaNode[]
458
+
459
+ // Build full path for nested data access
460
+ const fullPath = parentPath ? `${parentPath}.${elementName}` : elementName
461
+
462
+ repeaters.push({
463
+ name: elementName,
464
+ path: fullPath,
465
+ label: getLocalizedString(element.label, locale),
466
+ addLabel: getLocalizedString(element.addLabel || element.add_label, locale),
467
+ childrenSchema,
468
+ min: element.min as number | undefined,
469
+ max: element.max as number | undefined,
470
+ })
471
+ }
472
+ else if (formkitType === 'group' && element.children) {
473
+ // Recursively search for repeaters inside groups, passing the current path
474
+ const groupPath = parentPath ? `${parentPath}.${elementName}` : elementName
475
+ const nestedRepeaters = extractRepeaterConfigs(
476
+ element.children as FormElement[],
477
+ locale,
478
+ groupPath,
479
+ )
480
+ repeaters.push(...nestedRepeaters)
481
+ }
482
+ }
483
+
484
+ return repeaters
485
+ }
486
+
487
+ /**
488
+ * Builds a FormKit schema from form elements.
489
+ */
490
+ export function buildFormKitSchema(
491
+ formElements: FormElement[],
492
+ options: TransformOptions = {},
493
+ ): FormKitSchemaNode[] {
494
+ if (!formElements || formElements.length === 0) return []
495
+
496
+ try {
497
+ return formElements.flatMap(el => transformElementToSchema(el, options)) as FormKitSchemaNode[]
498
+ }
499
+ catch (error) {
500
+ console.error('Error transforming schema:', error)
501
+ return []
502
+ }
503
+ }
504
+
505
+ const LOCALE_KEYS = new Set(['de', 'en', 'fr', 'it'])
506
+
507
+ /**
508
+ * Checks if a value is a LocaleString object (has only locale keys: de, en, fr, it).
509
+ */
510
+ function isLocaleStringObject(value: unknown): value is Record<string, unknown> {
511
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
512
+ return false
513
+ }
514
+ const keys = Object.keys(value as object)
515
+ return keys.length > 0 && keys.every(key => LOCALE_KEYS.has(key))
516
+ }
517
+
518
+ /**
519
+ * Normalizes a LocaleString object:
520
+ * - Always returns an object with all locale fields as strings (empty string for unfilled)
521
+ * - Never returns null - backend expects a dict/LocaleString object, not null
522
+ *
523
+ * Backend validation will determine if empty values are acceptable based on field constraints.
524
+ */
525
+ function normalizeLocaleString(localeObj: Record<string, unknown>): Record<string, string> {
526
+ return {
527
+ de: (localeObj.de as string) || '',
528
+ en: (localeObj.en as string) || '',
529
+ fr: (localeObj.fr as string) || '',
530
+ it: (localeObj.it as string) || '',
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Recursively normalizes LocaleString fields in form data before submission.
536
+ * - LocaleString objects → all fields normalized to strings (empty string for unfilled)
537
+ * - Other values are recursively processed
538
+ */
539
+ export function normalizeFormLocaleStrings<T>(data: T): T {
540
+ if (data === null || data === undefined) {
541
+ return data
542
+ }
543
+
544
+ if (Array.isArray(data)) {
545
+ return data.map(item => normalizeFormLocaleStrings(item)) as T
546
+ }
547
+
548
+ if (typeof data === 'object') {
549
+ if (isLocaleStringObject(data)) {
550
+ return normalizeLocaleString(data as Record<string, unknown>) as T
551
+ }
552
+
553
+ const result: Record<string, unknown> = {}
554
+ for (const [key, value] of Object.entries(data)) {
555
+ result[key] = normalizeFormLocaleStrings(value)
556
+ }
557
+ return result as T
558
+ }
559
+
560
+ return data
561
+ }
562
+
563
+ /**
564
+ * Walks the form schema; for every nullable element whose synthetic toggle is off,
565
+ * replaces the actual field value with `null`. Always strips synthetic toggle keys.
566
+ */
567
+ export function coerceNullableToggles(
568
+ data: Record<string, unknown>,
569
+ elements: FormElement[],
570
+ ): Record<string, unknown> {
571
+ const result: Record<string, unknown> = { ...data }
572
+
573
+ for (const element of elements) {
574
+ const name = element.name as string
575
+ if (element.nullable === true) {
576
+ const toggleKey = nullableToggleName(name)
577
+ const enabled = result[toggleKey] === true
578
+ Reflect.deleteProperty(result, toggleKey)
579
+ if (!enabled) {
580
+ result[name] = null
581
+ continue
582
+ }
583
+ }
584
+
585
+ const formkitType = getFormkitType(element)
586
+ const children = (element.children as FormElement[] | undefined) ?? []
587
+ const value = result[name]
588
+
589
+ if (formkitType === 'group' && value && typeof value === 'object' && !Array.isArray(value)) {
590
+ result[name] = coerceNullableToggles(value as Record<string, unknown>, children)
591
+ }
592
+ else if (formkitType === 'repeater' && Array.isArray(value)) {
593
+ result[name] = value.map(item =>
594
+ item && typeof item === 'object' && !Array.isArray(item)
595
+ ? coerceNullableToggles(item as Record<string, unknown>, children)
596
+ : item,
597
+ )
598
+ }
599
+ }
600
+
601
+ for (const key of Object.keys(result)) {
602
+ if (key.startsWith('__') && key.endsWith('__enabled')) {
603
+ Reflect.deleteProperty(result, key)
604
+ }
605
+ }
606
+
607
+ return result
608
+ }
609
+
610
+ /**
611
+ * Recursively fills missing leaf keys with the backend's serialised Pydantic defaults
612
+ * (`element.value`). FormKit no longer receives `value` in the schema (it would clobber
613
+ * the v-model on registration), so defaults must be merged into the form data instead.
614
+ * Existing values — including falsy ones like `false` or `""` — are preserved.
615
+ *
616
+ * NOTE: This helper is load-bearing for edit/clone/template flows but has no direct
617
+ * unit tests yet — Vitest is not configured for packages/web (see packages/web/CLAUDE.md).
618
+ * The Python-side `Form.to_formkit_form()` tests in packages/core lock in what
619
+ * `element.value` looks like; behaviour here is exercised end-to-end on agent and
620
+ * process edit forms.
621
+ */
622
+ export function seedFormDefaults(
623
+ data: Record<string, unknown>,
624
+ elements: FormElement[],
625
+ ): Record<string, unknown> {
626
+ const result: Record<string, unknown> = { ...data }
627
+
628
+ for (const element of elements) {
629
+ const name = element.name as string
630
+ const formkitType = getFormkitType(element)
631
+ const children = (element.children as FormElement[] | undefined) ?? []
632
+ const value = result[name]
633
+
634
+ if (formkitType === 'group') {
635
+ if (value === null) continue // nullable group disabled — leave as null
636
+ // value === undefined (or a non-object): materialise group defaults so children
637
+ // render with backend defaults. For nullable groups whose toggle is off,
638
+ // coerceNullableToggles re-nullifies the whole subtree at submit time, so seeding
639
+ // here is safe even when the toggle will end up disabled.
640
+ const groupValue = (value && typeof value === 'object' && !Array.isArray(value))
641
+ ? value as Record<string, unknown>
642
+ : {}
643
+ result[name] = seedFormDefaults(groupValue, children)
644
+ }
645
+ else if (formkitType === 'repeater' && Array.isArray(value)) {
646
+ result[name] = value.map(item =>
647
+ item && typeof item === 'object' && !Array.isArray(item)
648
+ ? seedFormDefaults(item as Record<string, unknown>, children)
649
+ : item,
650
+ )
651
+ }
652
+ else if (!(name in result) && element.value !== undefined) {
653
+ result[name] = element.value
654
+ }
655
+ }
656
+
657
+ return result
658
+ }
659
+
660
+ /**
661
+ * Recursively seeds synthetic toggle values from initial data: toggle is on iff the
662
+ * matching field was non-null/undefined in the source data.
663
+ */
664
+ export function seedNullableToggles(
665
+ data: Record<string, unknown>,
666
+ elements: FormElement[],
667
+ ): Record<string, unknown> {
668
+ const result: Record<string, unknown> = { ...data }
669
+
670
+ for (const element of elements) {
671
+ const name = element.name as string
672
+ if (element.nullable === true) {
673
+ result[nullableToggleName(name)] = result[name] !== null && result[name] !== undefined
674
+ }
675
+
676
+ const formkitType = getFormkitType(element)
677
+ const children = (element.children as FormElement[] | undefined) ?? []
678
+ const value = result[name]
679
+
680
+ if (formkitType === 'group' && value && typeof value === 'object' && !Array.isArray(value)) {
681
+ result[name] = seedNullableToggles(value as Record<string, unknown>, children)
682
+ }
683
+ else if (formkitType === 'repeater' && Array.isArray(value)) {
684
+ result[name] = value.map(item =>
685
+ item && typeof item === 'object' && !Array.isArray(item)
686
+ ? seedNullableToggles(item as Record<string, unknown>, children)
687
+ : item,
688
+ )
689
+ }
690
+ }
691
+
692
+ return result
693
+ }
694
+
695
+ /**
696
+ * Categorizes form elements into simple inputs, groups, and repeaters.
697
+ * Used for organizing form elements into stepper steps.
698
+ */
699
+ export function categorizeFormElements(formElements: FormElement[]): CategorizedElements {
700
+ if (!formElements || formElements.length === 0) {
701
+ return { simpleElements: [], groupElements: [], repeaterElements: [] }
702
+ }
703
+
704
+ const simpleElements: FormElement[] = []
705
+ const groupElements: FormElement[] = []
706
+ const repeaterElements: FormElement[] = []
707
+
708
+ for (const element of formElements) {
709
+ const formkitType = getFormkitType(element)
710
+
711
+ if (formkitType === 'group') {
712
+ groupElements.push(element)
713
+ }
714
+ else if (formkitType === 'repeater') {
715
+ repeaterElements.push(element)
716
+ }
717
+ else {
718
+ simpleElements.push(element)
719
+ }
720
+ }
721
+
722
+ return { simpleElements, groupElements, repeaterElements }
723
+ }
724
+
725
+ /**
726
+ * Extracts group configurations from form elements.
727
+ * Similar to extractRepeaterConfigs but for group elements.
728
+ */
729
+ export function extractGroupConfigs(
730
+ formElements: FormElement[],
731
+ locale = 'en',
732
+ ): GroupConfig[] {
733
+ if (!formElements || formElements.length === 0) return []
734
+
735
+ const groups: GroupConfig[] = []
736
+
737
+ for (const element of formElements) {
738
+ const formkitType = getFormkitType(element)
739
+
740
+ if (formkitType === 'group') {
741
+ const schema = transformElementToSchema(element, { locale })
742
+ const schemaArray = Array.isArray(schema) ? schema : [schema]
743
+
744
+ groups.push({
745
+ name: element.name as string,
746
+ label: getLocalizedString(element.label, locale),
747
+ schema: schemaArray,
748
+ })
749
+ }
750
+ }
751
+
752
+ return groups
753
+ }