spine-framework 0.3.124 → 0.3.126

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 (319) hide show
  1. package/.framework/cli/commands/agents.ts +35 -37
  2. package/.framework/cli/commands/auth.ts +35 -34
  3. package/.framework/cli/commands/create-app.ts +104 -40
  4. package/.framework/cli/commands/dev.ts +104 -69
  5. package/.framework/cli/commands/doctor.ts +179 -69
  6. package/.framework/cli/commands/generate.ts +83 -143
  7. package/.framework/cli/commands/init.ts +72 -34
  8. package/.framework/cli/commands/install-app.ts +260 -125
  9. package/.framework/cli/commands/items.ts +33 -44
  10. package/.framework/cli/commands/migrate.ts +122 -40
  11. package/.framework/cli/commands/migrations.ts +33 -40
  12. package/.framework/cli/commands/pipelines.ts +31 -39
  13. package/.framework/cli/commands/status.ts +47 -23
  14. package/.framework/cli/commands/system.ts +47 -37
  15. package/.framework/cli/commands/test.ts +112 -68
  16. package/.framework/cli/commands/validate-app.ts +215 -121
  17. package/.framework/cli/context.ts +112 -130
  18. package/.framework/cli/env-loader.ts +47 -18
  19. package/.framework/cli/index.ts +81 -42
  20. package/.framework/docs/runtime-types.md +1024 -0
  21. package/.framework/functions/_shared/agent-runner.ts +1296 -961
  22. package/.framework/functions/_shared/app-manifest.ts +82 -68
  23. package/.framework/functions/_shared/audit.ts +135 -83
  24. package/.framework/functions/_shared/{cil-state.ts → cil.ts} +158 -56
  25. package/.framework/functions/_shared/db.ts +80 -126
  26. package/.framework/functions/_shared/hash-api-key.ts +63 -0
  27. package/.framework/functions/_shared/index.ts +224 -335
  28. package/.framework/functions/_shared/kb.ts +319 -0
  29. package/.framework/functions/_shared/llm.ts +92 -0
  30. package/.framework/functions/_shared/logging.ts +288 -0
  31. package/.framework/functions/_shared/middleware.ts +329 -365
  32. package/.framework/functions/_shared/outcomes.ts +36 -0
  33. package/.framework/functions/_shared/permissions.ts +359 -643
  34. package/.framework/functions/_shared/pipeline-runner.ts +243 -316
  35. package/.framework/functions/_shared/principal.ts +376 -468
  36. package/.framework/functions/_shared/resolve-credentials.ts +102 -0
  37. package/.framework/functions/_shared/resolve-ids.ts +188 -16
  38. package/.framework/functions/_shared/retry-deliveries.ts +104 -0
  39. package/.framework/functions/_shared/schema-utils.ts +611 -373
  40. package/.framework/functions/_shared/testing.ts +207 -137
  41. package/.framework/functions/_shared/tracing.ts +276 -0
  42. package/.framework/functions/_shared/trigger-engine.ts +159 -211
  43. package/.framework/functions/_shared/webhook-registration.ts +95 -81
  44. package/.framework/functions/_shared/webhook-registry.ts +153 -75
  45. package/.framework/functions/account-nodes.ts +31 -53
  46. package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
  47. package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
  48. package/.framework/functions/adapters/openai-outbound.ts +40 -0
  49. package/.framework/functions/admin-data.ts +685 -537
  50. package/.framework/functions/admin-funnel.ts +31 -20
  51. package/.framework/functions/admin-install-funnel.ts +47 -28
  52. package/.framework/functions/admin-signals.ts +47 -29
  53. package/.framework/functions/ai-agents.ts +122 -147
  54. package/.framework/functions/ai-interactions.ts +180 -90
  55. package/.framework/functions/api-keys.ts +250 -192
  56. package/.framework/functions/apps.ts +203 -197
  57. package/.framework/functions/auth.ts +98 -91
  58. package/.framework/functions/cil-agent-ingest.ts +773 -0
  59. package/.framework/functions/cil-agent.ts +592 -0
  60. package/.framework/functions/cil-callmap.ts +1227 -0
  61. package/.framework/functions/cil-config.ts +583 -0
  62. package/.framework/functions/cil-context.ts +445 -0
  63. package/.framework/functions/cil-doc.ts +1752 -0
  64. package/.framework/functions/cil-embeddings.ts +1202 -0
  65. package/.framework/functions/cil-graph.ts +585 -0
  66. package/.framework/functions/cil-ingest.ts +2416 -0
  67. package/.framework/functions/cil-pipeline.ts +318 -0
  68. package/.framework/functions/cil-project.ts +499 -0
  69. package/.framework/functions/cil-validate.ts +469 -0
  70. package/.framework/functions/debug-auth.ts +73 -53
  71. package/.framework/functions/deliveries.ts +117 -0
  72. package/.framework/functions/embeddings.ts +528 -516
  73. package/.framework/functions/eval-harness.ts +158 -106
  74. package/.framework/functions/integration-outbound.ts +311 -0
  75. package/.framework/functions/integration-routes.ts +240 -466
  76. package/.framework/functions/integrations.ts +132 -140
  77. package/.framework/functions/invites.ts +401 -201
  78. package/.framework/functions/item-progress.ts +113 -53
  79. package/.framework/functions/kb.ts +28 -0
  80. package/.framework/functions/logs.ts +239 -222
  81. package/.framework/functions/observability.ts +308 -259
  82. package/.framework/functions/pipeline-executions.ts +179 -235
  83. package/.framework/functions/pipelines.ts +163 -205
  84. package/.framework/functions/prompt-configs.ts +109 -156
  85. package/.framework/functions/roles.ts +97 -147
  86. package/.framework/functions/system-cron.ts +132 -218
  87. package/.framework/functions/system.ts +112 -52
  88. package/.framework/functions/tests.ts +32 -28
  89. package/.framework/functions/timers.ts +162 -210
  90. package/.framework/functions/triggers.ts +282 -346
  91. package/.framework/functions/types.ts +283 -400
  92. package/.framework/migrations/001_core_schema.sql +630 -0
  93. package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
  94. package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
  95. package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
  96. package/.framework/migrations/005_core_observability.sql +598 -0
  97. package/.framework/migrations/006_security_hardening_rls.sql +97 -0
  98. package/.framework/migrations/007_kb_article_list.sql +237 -0
  99. package/.framework/migrations/008_items_target.sql +8 -0
  100. package/.framework/migrations/009_roles_slug_unique.sql +11 -0
  101. package/.framework/public/spine.config.json +91 -0
  102. package/.framework/scripts/app-install-cli.ts +112 -98
  103. package/.framework/src/App.tsx +67 -20
  104. package/.framework/src/apps/admin/index.tsx +97 -27
  105. package/.framework/src/components/AppWrapper.tsx +50 -11
  106. package/.framework/src/components/CustomAppLoader.tsx +131 -22
  107. package/.framework/src/components/admin/AdminListPage.tsx +75 -8
  108. package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
  109. package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
  110. package/.framework/src/components/admin/PageInstructions.tsx +49 -0
  111. package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
  112. package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
  113. package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
  114. package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
  115. package/.framework/src/components/app-sidebar.tsx +50 -3
  116. package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
  117. package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
  118. package/.framework/src/components/kb/KBArticleList.tsx +557 -0
  119. package/.framework/src/components/layout/AppShell.tsx +54 -16
  120. package/.framework/src/components/layout/Header.tsx +50 -38
  121. package/.framework/src/components/layout/Layout.tsx +48 -32
  122. package/.framework/src/components/layout/Sidebar.tsx +167 -16
  123. package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
  124. package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
  125. package/.framework/src/components/runtime/DataFilters.tsx +78 -1
  126. package/.framework/src/components/runtime/DataHeader.tsx +32 -29
  127. package/.framework/src/components/runtime/DataListPage.tsx +161 -2
  128. package/.framework/src/components/runtime/DataStats.tsx +48 -1
  129. package/.framework/src/components/runtime/DataTable.tsx +153 -1
  130. package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
  131. package/.framework/src/components/runtime/index.ts +16 -9
  132. package/.framework/src/components/search-form.tsx +33 -1
  133. package/.framework/src/components/shared/AgentView.tsx +48 -51
  134. package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
  135. package/.framework/src/components/shared/InviteManager.tsx +113 -52
  136. package/.framework/src/components/shared/MemberList.tsx +182 -0
  137. package/.framework/src/components/shared/SchemaFields.tsx +96 -78
  138. package/.framework/src/components/ui/DataTable.tsx +86 -56
  139. package/.framework/src/components/ui/Form.tsx +188 -109
  140. package/.framework/src/components/ui/ItemCard.tsx +86 -58
  141. package/.framework/src/components/ui/ItemListView.tsx +88 -61
  142. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
  143. package/.framework/src/components/ui/Modal.tsx +81 -4
  144. package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
  145. package/.framework/src/components/ui/accordion.tsx +97 -5
  146. package/.framework/src/components/ui/alert-dialog.tsx +225 -13
  147. package/.framework/src/components/ui/alert.tsx +113 -6
  148. package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
  149. package/.framework/src/components/ui/avatar.tsx +129 -7
  150. package/.framework/src/components/ui/badge.tsx +65 -3
  151. package/.framework/src/components/ui/breadcrumb.tsx +145 -8
  152. package/.framework/src/components/ui/button-group.tsx +81 -4
  153. package/.framework/src/components/ui/button.tsx +49 -2
  154. package/.framework/src/components/ui/calendar.tsx +65 -3
  155. package/.framework/src/components/ui/card.tsx +145 -8
  156. package/.framework/src/components/ui/carousel.tsx +129 -7
  157. package/.framework/src/components/ui/chart.tsx +161 -10
  158. package/.framework/src/components/ui/checkbox.tsx +49 -2
  159. package/.framework/src/components/ui/collapsible.tsx +81 -5
  160. package/.framework/src/components/ui/combobox.tsx +305 -20
  161. package/.framework/src/components/ui/command.tsx +177 -11
  162. package/.framework/src/components/ui/context-menu.tsx +273 -17
  163. package/.framework/src/components/ui/dialog.tsx +209 -27
  164. package/.framework/src/components/ui/direction.tsx +64 -4
  165. package/.framework/src/components/ui/drawer.tsx +193 -12
  166. package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
  167. package/.framework/src/components/ui/empty.tsx +145 -9
  168. package/.framework/src/components/ui/field.tsx +209 -14
  169. package/.framework/src/components/ui/hover-card.tsx +81 -4
  170. package/.framework/src/components/ui/input-group.tsx +162 -10
  171. package/.framework/src/components/ui/input-otp.tsx +98 -5
  172. package/.framework/src/components/ui/input.tsx +49 -2
  173. package/.framework/src/components/ui/item.tsx +225 -13
  174. package/.framework/src/components/ui/kbd.tsx +65 -3
  175. package/.framework/src/components/ui/label.tsx +49 -2
  176. package/.framework/src/components/ui/menubar.tsx +289 -17
  177. package/.framework/src/components/ui/native-select.tsx +97 -5
  178. package/.framework/src/components/ui/navigation-menu.tsx +177 -10
  179. package/.framework/src/components/ui/pagination.tsx +161 -9
  180. package/.framework/src/components/ui/popover.tsx +145 -8
  181. package/.framework/src/components/ui/progress.tsx +49 -2
  182. package/.framework/src/components/ui/radio-group.tsx +65 -3
  183. package/.framework/src/components/ui/resizable.tsx +82 -4
  184. package/.framework/src/components/ui/scroll-area.tsx +65 -3
  185. package/.framework/src/components/ui/select.tsx +193 -11
  186. package/.framework/src/components/ui/separator.tsx +49 -2
  187. package/.framework/src/components/ui/sheet.tsx +193 -11
  188. package/.framework/src/components/ui/sidebar.tsx +481 -38
  189. package/.framework/src/components/ui/skeleton.tsx +49 -2
  190. package/.framework/src/components/ui/slider.tsx +49 -2
  191. package/.framework/src/components/ui/sonner.tsx +49 -2
  192. package/.framework/src/components/ui/spinner.tsx +49 -2
  193. package/.framework/src/components/ui/switch.tsx +49 -2
  194. package/.framework/src/components/ui/table-primitives.tsx +160 -33
  195. package/.framework/src/components/ui/table.tsx +161 -9
  196. package/.framework/src/components/ui/tabs.tsx +113 -6
  197. package/.framework/src/components/ui/textarea.tsx +49 -2
  198. package/.framework/src/components/ui/toggle-group.tsx +81 -4
  199. package/.framework/src/components/ui/toggle.tsx +65 -3
  200. package/.framework/src/components/ui/tooltip.tsx +97 -5
  201. package/.framework/src/contexts/AppContext.tsx +129 -45
  202. package/.framework/src/contexts/AuthContext.tsx +187 -144
  203. package/.framework/src/hooks/use-mobile.ts +48 -2
  204. package/.framework/src/hooks/useApi.ts +208 -159
  205. package/.framework/src/hooks/useAppPath.ts +32 -23
  206. package/.framework/src/hooks/useApps.ts +51 -20
  207. package/.framework/src/hooks/useEntityList.ts +32 -78
  208. package/.framework/src/hooks/useEntityRecord.ts +35 -96
  209. package/.framework/src/hooks/useForm.ts +32 -92
  210. package/.framework/src/hooks/useListSchema.ts +49 -78
  211. package/.framework/src/hooks/useSchemaRecord.ts +114 -105
  212. package/.framework/src/lib/api.ts +195 -116
  213. package/.framework/src/lib/supabase.ts +98 -64
  214. package/.framework/src/lib/utils.ts +287 -151
  215. package/.framework/src/main.tsx +49 -3
  216. package/.framework/src/pages/DashboardPage.tsx +52 -29
  217. package/.framework/src/pages/NotFoundPage.tsx +32 -10
  218. package/.framework/src/pages/SetupPage.tsx +32 -12
  219. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
  220. package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
  221. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
  222. package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
  223. package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
  224. package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
  225. package/.framework/src/pages/admin/AppsPage.tsx +49 -2
  226. package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
  227. package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
  228. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
  229. package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
  230. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
  231. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
  232. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
  233. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
  234. package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
  235. package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
  236. package/.framework/src/pages/admin/LogsPage.tsx +422 -75
  237. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
  238. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
  239. package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
  240. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
  241. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
  242. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
  243. package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
  244. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
  245. package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
  246. package/.framework/src/pages/admin/RolesPage.tsx +171 -18
  247. package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
  248. package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
  249. package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
  250. package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
  251. package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
  252. package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
  253. package/.framework/src/pages/admin/TimersPage.tsx +48 -20
  254. package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
  255. package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
  256. package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
  257. package/.framework/src/pages/admin/TypesPage.tsx +48 -20
  258. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
  259. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
  260. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
  261. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
  262. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
  263. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
  264. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
  265. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
  266. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
  267. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  268. package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
  269. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
  270. package/.framework/src/pages/auth/LoginPage.tsx +50 -23
  271. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
  272. package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
  273. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
  274. package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
  275. package/.framework/src/types/auth.ts +128 -71
  276. package/.framework/src/types/types.ts +373 -157
  277. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
  278. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
  279. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
  280. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
  281. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
  282. package/docs/README.md +9 -130
  283. package/netlify.toml +5 -2
  284. package/package.json +13 -25
  285. package/scripts/kb-ingest-docs.ts +203 -0
  286. package/spine.config.json +65 -4
  287. package/.framework/migrations/001_schema.sql +0 -425
  288. package/.framework/migrations/002_rls_and_functions.sql +0 -763
  289. package/.framework/migrations/004_enhancements.sql +0 -43
  290. package/.framework/migrations/005_auth_user_trigger.sql +0 -105
  291. package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
  292. package/.framework/migrations/016_invites.sql +0 -27
  293. package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
  294. package/.framework/migrations/018_apps_required_roles.sql +0 -17
  295. package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
  296. package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
  297. package/.framework/migrations/021_app_domains.sql +0 -112
  298. package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
  299. package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
  300. package/.framework/migrations/024_messages_account_id.sql +0 -9
  301. package/.framework/migrations/025_retype_threads_messages.sql +0 -240
  302. package/.framework/migrations/026_fix_rls_policies.sql +0 -47
  303. package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
  304. package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
  305. package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
  306. package/.framework/migrations/030_messages_updated_by.sql +0 -6
  307. package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
  308. package/docs/APPS.md +0 -232
  309. package/docs/ARCHITECTURE.md +0 -246
  310. package/docs/INSTALL.md +0 -155
  311. package/docs/design-schema-spec.md +0 -1837
  312. package/docs/dev-guides/01-create-an-app.md +0 -183
  313. package/docs/dev-guides/02-create-a-function.md +0 -178
  314. package/docs/dev-guides/03-create-a-component.md +0 -178
  315. package/docs/dev-guides/04-webhook-handlers.md +0 -129
  316. package/docs/dev-guides/05-testing.md +0 -168
  317. package/docs/dev-guides/README.md +0 -40
  318. package/docs/npm-packages-plan.md +0 -288
  319. package/docs/spine-core--api-reference.md +0 -822
@@ -0,0 +1,409 @@
1
+ import { useState, useEffect, useMemo } from 'react'
2
+ import DOMPurify from 'dompurify'
3
+ import { apiFetch } from '../../lib/api'
4
+ import { formatDateTime } from '../../lib/utils'
5
+ import { Button } from '../ui/button'
6
+ import { Input } from '../ui/input'
7
+ import { Label } from '../ui/label'
8
+ import { Badge } from '../ui/badge'
9
+ import { Skeleton } from '../ui/skeleton'
10
+ import { RichTextEditor } from '../ui/RichTextEditor'
11
+ import { SchemaFields } from '../shared/SchemaFields'
12
+ import { LoadingSpinner } from '../ui/LoadingSpinner'
13
+ import { ArrowLeft, Save, Send, Trash, X, Edit } from 'lucide-react'
14
+
15
+ export interface KBArticleEditorProps {
16
+ typeSlug: string
17
+ typeName?: string
18
+ mode?: 'view' | 'edit' | 'new'
19
+ articleId?: string | null
20
+ canEdit?: boolean
21
+ canCreate?: boolean
22
+ canDelete?: boolean
23
+ onBack?: () => void
24
+ onSave?: (record: any) => void
25
+ onEdit?: (id: string) => void
26
+ onCancel?: () => void
27
+ onDelete?: () => void
28
+ }
29
+
30
+ interface KBRecord {
31
+ id: string
32
+ title: string
33
+ status: string
34
+ created_at: string
35
+ data: Record<string, any>
36
+ [key: string]: any
37
+ }
38
+
39
+ function getFieldDefault(def: any): any {
40
+ if (def.data_type === 'multiselect' || (def.data_type === 'array' && def.options)) return []
41
+ if (def.data_type === 'boolean' || def.data_type === 'checkbox' || def.data_type === 'switch') return false
42
+ if (def.data_type === 'number' || def.data_type === 'currency' || def.data_type === 'range') return null
43
+ if (def.data_type === 'select' || def.data_type === 'radio') {
44
+ if (Array.isArray(def.options) && def.options.length) {
45
+ const first = def.options[0]
46
+ return typeof first === 'object' ? first.value : first
47
+ }
48
+ return ''
49
+ }
50
+ if (def.data_type === 'datetime' || def.data_type === 'date') return null
51
+ return ''
52
+ }
53
+
54
+ function initialFormData(schema: any, record: KBRecord | null): Record<string, any> {
55
+ const values: Record<string, any> = {}
56
+ Object.entries(schema?.fields || {}).forEach(([fieldName, fieldDef]: [string, any]) => {
57
+ if (record) {
58
+ values[fieldName] = fieldDef.system
59
+ ? (record[fieldName] !== undefined ? record[fieldName] : getFieldDefault(fieldDef))
60
+ : (record.data?.[fieldName] !== undefined ? record.data[fieldName] : getFieldDefault(fieldDef))
61
+ } else {
62
+ values[fieldName] = getFieldDefault(fieldDef)
63
+ }
64
+ })
65
+ if (!record) {
66
+ values.status = 'draft'
67
+ values.title = ''
68
+ values.content = ''
69
+ values.description = ''
70
+ }
71
+ return values
72
+ }
73
+
74
+ export function KBArticleEditor({
75
+ typeSlug,
76
+ typeName,
77
+ mode = 'view',
78
+ articleId = null,
79
+ canEdit = true,
80
+ canCreate = true,
81
+ canDelete = true,
82
+ onBack,
83
+ onSave,
84
+ onEdit,
85
+ onCancel,
86
+ onDelete,
87
+ }: KBArticleEditorProps) {
88
+ const isCreating = mode === 'new'
89
+ const isEditing = mode === 'edit' || isCreating
90
+
91
+ const [typeRecord, setTypeRecord] = useState<any>(null)
92
+ const [record, setRecord] = useState<KBRecord | null>(null)
93
+ const [loading, setLoading] = useState(true)
94
+ const [formData, setFormData] = useState<Record<string, any>>({})
95
+ const [saving, setSaving] = useState(false)
96
+ const [error, setError] = useState<string | null>(null)
97
+
98
+ useEffect(() => {
99
+ if (!isCreating) {
100
+ setTypeRecord(null)
101
+ return
102
+ }
103
+ let cancelled = false
104
+ setLoading(true)
105
+ setTypeRecord(null)
106
+ setRecord(null)
107
+ setError(null)
108
+
109
+ apiFetch('/api/types?action=list')
110
+ .then(r => r.json())
111
+ .then(j => {
112
+ if (cancelled) return
113
+ const found = (j.data || []).find((t: any) => t.slug === typeSlug)
114
+ setTypeRecord(found || null)
115
+ setLoading(false)
116
+ })
117
+ .catch(() => {
118
+ setTypeRecord(null)
119
+ setLoading(false)
120
+ })
121
+
122
+ return () => { cancelled = true }
123
+ }, [isCreating, typeSlug])
124
+
125
+ useEffect(() => {
126
+ if (isCreating) {
127
+ setRecord(null)
128
+ return
129
+ }
130
+ if (!articleId) {
131
+ setRecord(null)
132
+ return
133
+ }
134
+ let cancelled = false
135
+ setLoading(true)
136
+ setError(null)
137
+ apiFetch(`/api/admin-data?action=get&entity=items&id=${articleId}`)
138
+ .then(r => r.json())
139
+ .then(j => {
140
+ if (cancelled) return
141
+ const rec = j.data ?? j
142
+ setRecord(rec)
143
+ setLoading(false)
144
+ })
145
+ .catch((e: any) => {
146
+ setError(e.message || 'Failed to load article')
147
+ setRecord(null)
148
+ setLoading(false)
149
+ })
150
+ return () => { cancelled = true }
151
+ }, [articleId, isCreating])
152
+
153
+ const schema = record?.design_schema || typeRecord?.design_schema
154
+
155
+ useEffect(() => {
156
+ if (schema) {
157
+ setFormData(initialFormData(schema, isCreating ? null : record))
158
+ }
159
+ }, [schema, isCreating, record])
160
+
161
+ const allFields = useMemo(() => {
162
+ return Object.entries(schema?.fields || {}).map(([name, def]: [string, any]) => ({
163
+ name,
164
+ label: def.label,
165
+ data_type: def.data_type,
166
+ required: def.required,
167
+ options: def.options,
168
+ validation: def.validation,
169
+ permissions: def.permissions,
170
+ system: def.system,
171
+ placeholder: def.placeholder,
172
+ }))
173
+ }, [schema])
174
+
175
+ const mainFieldNames = ['title', 'content']
176
+ const mainFields = useMemo(() => allFields.filter(f => mainFieldNames.includes(f.name)), [allFields])
177
+ const sidebarFields = useMemo(() => allFields.filter(f => !mainFieldNames.includes(f.name)), [allFields])
178
+ const titleField = mainFields.find(f => f.name === 'title')
179
+ const contentField = mainFields.find(f => f.name === 'content')
180
+
181
+ const titleValue = isEditing ? formData.title : record?.title
182
+ const contentValue = isEditing
183
+ ? (formData.content || '')
184
+ : (record?.data?.content || record?.content || '')
185
+ const descriptionValue = isEditing
186
+ ? (formData.description || '')
187
+ : (record?.description || '')
188
+
189
+ const buildPayload = () => {
190
+ const system: Record<string, any> = {}
191
+ const data: Record<string, any> = {}
192
+ Object.entries(schema?.fields || {}).forEach(([fieldName, fieldDef]: [string, any]) => {
193
+ const value = formData[fieldName]
194
+ if (fieldDef.system) {
195
+ system[fieldName] = value
196
+ } else {
197
+ data[fieldName] = value
198
+ }
199
+ })
200
+ return { entity: 'items', type_slug: record?.type_slug || typeRecord?.slug || typeSlug, ...system, data }
201
+ }
202
+
203
+ const handleSave = async (publish = false) => {
204
+ if (!schema) return
205
+ setSaving(true)
206
+ setError(null)
207
+ try {
208
+ const payload = buildPayload()
209
+ if (publish) payload.status = 'published'
210
+ if (!payload.title?.trim()) throw new Error('Title is required')
211
+ if (!payload.data?.content?.trim()) throw new Error('Content is required')
212
+
213
+ let res: Response
214
+ if (isCreating) {
215
+ res = await apiFetch('/api/admin-data?action=create&entity=items', {
216
+ method: 'POST',
217
+ headers: { 'Content-Type': 'application/json' },
218
+ body: JSON.stringify(payload),
219
+ })
220
+ } else {
221
+ res = await apiFetch(`/api/admin-data?action=update&entity=items&id=${articleId}`, {
222
+ method: 'PATCH',
223
+ headers: { 'Content-Type': 'application/json' },
224
+ body: JSON.stringify(payload),
225
+ })
226
+ }
227
+
228
+ if (!res.ok) {
229
+ const err = await res.json().catch(() => ({}))
230
+ throw new Error(err.error || 'Save failed')
231
+ }
232
+
233
+ const json = await res.json().catch(() => null)
234
+ const saved = json?.data ?? json
235
+ const recordId = saved?.id || (isCreating ? null : articleId)
236
+
237
+ if (publish && recordId) {
238
+ await apiFetch('/api/kb?action=publish', {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json' },
241
+ body: JSON.stringify({ id: recordId }),
242
+ })
243
+ }
244
+
245
+ onSave?.(saved)
246
+ } catch (e: any) {
247
+ setError(e.message || 'Save failed')
248
+ } finally {
249
+ setSaving(false)
250
+ }
251
+ }
252
+
253
+ const handleDelete = async () => {
254
+ if (isCreating || !articleId) return
255
+ if (!confirm('Delete this article? This cannot be undone.')) return
256
+ setSaving(true)
257
+ try {
258
+ const res = await apiFetch(`/api/admin-data?action=remove&entity=items&id=${articleId}`, { method: 'DELETE' })
259
+ if (!res.ok) throw new Error('Delete failed')
260
+ onDelete?.()
261
+ } catch (e: any) {
262
+ setError(e.message || 'Delete failed')
263
+ } finally {
264
+ setSaving(false)
265
+ }
266
+ }
267
+
268
+ const handleFieldChange = (name: string, value: any) => {
269
+ setFormData(prev => ({ ...prev, [name]: value }))
270
+ }
271
+
272
+ if (loading && !record && !isCreating) {
273
+ return (
274
+ <div className="p-6 space-y-4 h-full">
275
+ <Skeleton className="h-8 w-2/3" />
276
+ <Skeleton className="h-64 w-full" />
277
+ </div>
278
+ )
279
+ }
280
+
281
+ if (!schema) {
282
+ return (
283
+ <div className="p-6 text-center text-muted-foreground h-full flex items-center justify-center">
284
+ <p>Could not load type schema for {typeSlug}.</p>
285
+ </div>
286
+ )
287
+ }
288
+
289
+ return (
290
+ <div className="flex flex-col h-full">
291
+ <div className="px-6 py-3 border-b border-border shrink-0 flex items-center justify-between gap-4">
292
+ <div className="flex items-center gap-3 min-w-0">
293
+ {onBack && (
294
+ <Button variant="ghost" size="sm" onClick={onBack} className="gap-1 text-muted-foreground">
295
+ <ArrowLeft className="h-3.5 w-3.5" /> KB
296
+ </Button>
297
+ )}
298
+ <h1 className="text-base font-semibold truncate">
299
+ {isCreating ? `New ${typeName || typeSlug}` : (record?.title || 'Edit Article')}
300
+ </h1>
301
+ {!isCreating && record && (
302
+ <div className="hidden sm:flex items-center gap-2">
303
+ <Badge variant={record.status === 'published' ? 'default' : 'outline'} className="text-xs">
304
+ {record.status || 'draft'}
305
+ </Badge>
306
+ <span className="text-xs text-muted-foreground">{formatDateTime(record.created_at)}</span>
307
+ </div>
308
+ )}
309
+ </div>
310
+
311
+ <div className="flex items-center gap-2 shrink-0">
312
+ {mode === 'view' && articleId && canEdit && (
313
+ <Button size="sm" variant="outline" onClick={() => articleId && onEdit?.(articleId)} className="gap-1">
314
+ <Edit className="h-3.5 w-3.5" /> Edit
315
+ </Button>
316
+ )}
317
+ {isEditing && (
318
+ <>
319
+ <Button size="sm" variant="outline" onClick={() => onCancel?.()} className="gap-1">
320
+ <X className="h-3.5 w-3.5" /> Cancel
321
+ </Button>
322
+ {canDelete && !isCreating && (
323
+ <Button size="sm" variant="outline" onClick={handleDelete} disabled={saving} className="gap-1 text-destructive hover:text-destructive">
324
+ <Trash className="h-3.5 w-3.5" /> Delete
325
+ </Button>
326
+ )}
327
+ <Button size="sm" variant="outline" onClick={() => handleSave(false)} disabled={saving} className="gap-1">
328
+ <Save className="h-3.5 w-3.5" /> Save Draft
329
+ </Button>
330
+ <Button size="sm" onClick={() => handleSave(true)} disabled={saving} className="gap-1">
331
+ <Send className="h-3.5 w-3.5" /> Publish
332
+ </Button>
333
+ </>
334
+ )}
335
+ </div>
336
+ </div>
337
+
338
+ {error && (
339
+ <div className="mx-6 mt-4 px-4 py-3 bg-destructive/10 border border-destructive/20 text-destructive rounded text-sm">
340
+ {error}
341
+ </div>
342
+ )}
343
+
344
+ <div className="flex flex-1 min-h-0">
345
+ <div className="flex-1 p-6 space-y-4 overflow-y-auto">
346
+ <div className="space-y-1.5">
347
+ <Label>{titleField?.label || 'Title'} *</Label>
348
+ {isEditing ? (
349
+ <Input
350
+ value={formData.title || ''}
351
+ onChange={e => handleFieldChange('title', e.target.value)}
352
+ placeholder="Article title"
353
+ className="text-lg font-medium"
354
+ />
355
+ ) : (
356
+ <h2 className="text-xl font-semibold">{record?.title || 'Untitled'}</h2>
357
+ )}
358
+ </div>
359
+
360
+ <div className="space-y-1.5">
361
+ <Label>{contentField?.label || 'Content'} *</Label>
362
+ {isEditing ? (
363
+ <RichTextEditor
364
+ value={formData.content || ''}
365
+ onChange={html => handleFieldChange('content', html)}
366
+ placeholder="Write the article content here…"
367
+ minHeight="400px"
368
+ />
369
+ ) : (
370
+ <div
371
+ className="prose prose-sm dark:prose-invert max-w-none min-h-[200px] p-3 border rounded-md bg-background"
372
+ // Sanitize before injecting: content is rich-text HTML from a stored
373
+ // record and must never execute scripts/handlers (stored-XSS defense).
374
+ dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(record?.data?.content || record?.content || '<p class="text-muted-foreground italic">No content available.</p>') }}
375
+ />
376
+ )}
377
+ </div>
378
+
379
+ {schema.fields?.description && (
380
+ <div className="space-y-1.5">
381
+ <Label>Description</Label>
382
+ {isEditing ? (
383
+ <textarea
384
+ value={formData.description || ''}
385
+ onChange={e => handleFieldChange('description', e.target.value)}
386
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm min-h-[80px]"
387
+ placeholder="Short description or summary"
388
+ />
389
+ ) : (
390
+ <p className="text-sm text-slate-600 whitespace-pre-wrap">{record?.description || <span className="text-muted-foreground italic">No description.</span>}</p>
391
+ )}
392
+ </div>
393
+ )}
394
+ </div>
395
+
396
+ <div className="w-72 shrink-0 border-l border-border p-4 space-y-4 overflow-y-auto">
397
+ <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">Metadata</h3>
398
+ <SchemaFields
399
+ fields={sidebarFields}
400
+ data={isEditing ? { ...formData, data: formData } : { ...record, data: record?.data }}
401
+ onChange={isEditing ? handleFieldChange : undefined}
402
+ readonly={!isEditing}
403
+ twoColumn={false}
404
+ />
405
+ </div>
406
+ </div>
407
+ </div>
408
+ )
409
+ }