@gntik-ai/templates 0.0.0-stage → 0.5.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 (300) hide show
  1. package/dist/index.d.ts +2420 -0
  2. package/dist/index.js +14520 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +64 -3
  5. package/src/accept-invitation/Page.test.tsx +40 -0
  6. package/src/accept-invitation/Page.tsx +153 -0
  7. package/src/accept-invitation/data.ts +22 -0
  8. package/src/accept-invitation/template.meta.ts +12 -0
  9. package/src/assistant-chat/Page.test.tsx +51 -0
  10. package/src/assistant-chat/Page.tsx +273 -0
  11. package/src/assistant-chat/data.ts +71 -0
  12. package/src/assistant-chat/template.meta.ts +12 -0
  13. package/src/audit-log/Page.test.tsx +58 -0
  14. package/src/audit-log/Page.tsx +284 -0
  15. package/src/audit-log/data.ts +133 -0
  16. package/src/audit-log/template.meta.ts +12 -0
  17. package/src/board/Lane.tsx +117 -0
  18. package/src/board/Page.test.tsx +68 -0
  19. package/src/board/Page.tsx +178 -0
  20. package/src/board/data.ts +127 -0
  21. package/src/board/template.meta.ts +12 -0
  22. package/src/board/useBoard.ts +146 -0
  23. package/src/changelog/Page.test.tsx +31 -0
  24. package/src/changelog/Page.tsx +174 -0
  25. package/src/changelog/data.ts +105 -0
  26. package/src/changelog/template.meta.ts +12 -0
  27. package/src/code-editor/Page.test.tsx +83 -0
  28. package/src/code-editor/Page.tsx +207 -0
  29. package/src/code-editor/data.ts +130 -0
  30. package/src/code-editor/template.meta.ts +12 -0
  31. package/src/contact/Page.test.tsx +33 -0
  32. package/src/contact/Page.tsx +272 -0
  33. package/src/contact/PublicFrame.tsx +84 -0
  34. package/src/contact/data.ts +129 -0
  35. package/src/contact/template.meta.ts +12 -0
  36. package/src/create-wizard/Overlay.test.tsx +68 -0
  37. package/src/create-wizard/Page.test.tsx +57 -0
  38. package/src/create-wizard/Page.tsx +246 -0
  39. package/src/create-wizard/data.ts +139 -0
  40. package/src/create-wizard/examples/Overlay.tsx +60 -0
  41. package/src/create-wizard/template.meta.ts +12 -0
  42. package/src/create-workspace/Page.test.tsx +41 -0
  43. package/src/create-workspace/Page.tsx +227 -0
  44. package/src/create-workspace/data.ts +61 -0
  45. package/src/create-workspace/template.meta.ts +12 -0
  46. package/src/data-explorer/Page.test.tsx +45 -0
  47. package/src/data-explorer/Page.tsx +247 -0
  48. package/src/data-explorer/data.ts +94 -0
  49. package/src/data-explorer/template.meta.ts +12 -0
  50. package/src/docs-article/ArticleFeedback.tsx +75 -0
  51. package/src/docs-article/Page.test.tsx +34 -0
  52. package/src/docs-article/Page.tsx +200 -0
  53. package/src/docs-article/data.ts +65 -0
  54. package/src/docs-article/template.meta.ts +12 -0
  55. package/src/evaluations/Page.test.tsx +40 -0
  56. package/src/evaluations/Page.tsx +238 -0
  57. package/src/evaluations/data.ts +194 -0
  58. package/src/evaluations/template.meta.ts +12 -0
  59. package/src/forgot-password/Page.test.tsx +199 -0
  60. package/src/forgot-password/Page.tsx +66 -0
  61. package/src/forgot-password/data.ts +5 -0
  62. package/src/forgot-password/examples/UsernameOrEmail.tsx +25 -0
  63. package/src/forgot-password/template.meta.ts +12 -0
  64. package/src/getting-started/Page.test.tsx +27 -0
  65. package/src/getting-started/Page.tsx +125 -0
  66. package/src/getting-started/data.ts +67 -0
  67. package/src/getting-started/template.meta.ts +12 -0
  68. package/src/help-home/Page.test.tsx +33 -0
  69. package/src/help-home/Page.tsx +159 -0
  70. package/src/help-home/data.ts +151 -0
  71. package/src/help-home/template.meta.ts +12 -0
  72. package/src/home-dashboard/Page.test.tsx +57 -0
  73. package/src/home-dashboard/Page.tsx +152 -0
  74. package/src/home-dashboard/data.ts +66 -0
  75. package/src/home-dashboard/template.meta.ts +12 -0
  76. package/src/incident-board/Page.test.tsx +77 -0
  77. package/src/incident-board/Page.tsx +263 -0
  78. package/src/incident-board/data.ts +220 -0
  79. package/src/incident-board/template.meta.ts +12 -0
  80. package/src/incident-console/Page.test.tsx +49 -0
  81. package/src/incident-console/Page.tsx +274 -0
  82. package/src/incident-console/data.ts +176 -0
  83. package/src/incident-console/template.meta.ts +12 -0
  84. package/src/index.ts +250 -0
  85. package/src/keyboard-shortcuts/Page.test.tsx +45 -0
  86. package/src/keyboard-shortcuts/Page.tsx +94 -0
  87. package/src/keyboard-shortcuts/ShortcutSheet.tsx +80 -0
  88. package/src/keyboard-shortcuts/data.ts +84 -0
  89. package/src/keyboard-shortcuts/template.meta.ts +12 -0
  90. package/src/landing/Page.test.tsx +27 -0
  91. package/src/landing/Page.tsx +110 -0
  92. package/src/landing/data.ts +17 -0
  93. package/src/landing/template.meta.ts +11 -0
  94. package/src/live-event-stream/Page.test.tsx +251 -0
  95. package/src/live-event-stream/Page.tsx +307 -0
  96. package/src/live-event-stream/data.ts +44 -0
  97. package/src/live-event-stream/examples/Connecting.tsx +5 -0
  98. package/src/live-event-stream/examples/Error.tsx +5 -0
  99. package/src/live-event-stream/examples/LiveEmpty.tsx +5 -0
  100. package/src/live-event-stream/examples/LiveWithEvents.tsx +6 -0
  101. package/src/live-event-stream/examples/Stopped.tsx +6 -0
  102. package/src/live-event-stream/template.meta.ts +74 -0
  103. package/src/messaging/Page.test.tsx +34 -0
  104. package/src/messaging/Page.tsx +236 -0
  105. package/src/messaging/data.ts +118 -0
  106. package/src/messaging/template.meta.ts +12 -0
  107. package/src/meta.ts +24 -0
  108. package/src/mfa/Page.test.tsx +22 -0
  109. package/src/mfa/Page.tsx +45 -0
  110. package/src/mfa/data.ts +6 -0
  111. package/src/mfa/template.meta.ts +12 -0
  112. package/src/model-providers/Page.test.tsx +58 -0
  113. package/src/model-providers/Page.tsx +198 -0
  114. package/src/model-providers/data.ts +101 -0
  115. package/src/model-providers/template.meta.ts +12 -0
  116. package/src/notification-inbox/Page.test.tsx +49 -0
  117. package/src/notification-inbox/Page.tsx +237 -0
  118. package/src/notification-inbox/data.ts +129 -0
  119. package/src/notification-inbox/template.meta.ts +12 -0
  120. package/src/pending-activation/Page.test.tsx +390 -0
  121. package/src/pending-activation/Page.tsx +145 -0
  122. package/src/pending-activation/data.ts +25 -0
  123. package/src/pending-activation/template.meta.ts +12 -0
  124. package/src/pricing/ComparisonTable.tsx +75 -0
  125. package/src/pricing/Page.test.tsx +29 -0
  126. package/src/pricing/Page.tsx +83 -0
  127. package/src/pricing/PublicFrame.tsx +84 -0
  128. package/src/pricing/data.ts +104 -0
  129. package/src/pricing/template.meta.ts +12 -0
  130. package/src/prompt-playground/Page.test.tsx +72 -0
  131. package/src/prompt-playground/Page.tsx +254 -0
  132. package/src/prompt-playground/data.ts +68 -0
  133. package/src/prompt-playground/template.meta.ts +12 -0
  134. package/src/public-hub/Page.test.tsx +176 -0
  135. package/src/public-hub/Page.tsx +125 -0
  136. package/src/public-hub/Source.test.ts +54 -0
  137. package/src/public-hub/data.ts +24 -0
  138. package/src/public-hub/examples/Dark.tsx +11 -0
  139. package/src/public-hub/examples/Full.tsx +6 -0
  140. package/src/public-hub/examples/HighContrast.tsx +11 -0
  141. package/src/public-hub/examples/Light.tsx +11 -0
  142. package/src/public-hub/examples/Loading.tsx +6 -0
  143. package/src/public-hub/examples/Minimal.tsx +5 -0
  144. package/src/public-hub/examples/MinimalLoading.tsx +5 -0
  145. package/src/public-hub/template.meta.ts +60 -0
  146. package/src/reset-password/Page.test.tsx +27 -0
  147. package/src/reset-password/Page.tsx +46 -0
  148. package/src/reset-password/data.ts +5 -0
  149. package/src/reset-password/template.meta.ts +12 -0
  150. package/src/resource-detail/Page.test.tsx +260 -0
  151. package/src/resource-detail/Page.tsx +262 -0
  152. package/src/resource-detail/data.ts +55 -0
  153. package/src/resource-detail/examples/postgres-table-detail.tsx +28 -0
  154. package/src/resource-detail/examples/realtime-configuration.tsx +26 -0
  155. package/src/resource-detail/examples/runtime-with-transition.tsx +30 -0
  156. package/src/resource-detail/examples/runtime-without-transition.tsx +33 -0
  157. package/src/resource-detail/examples/shell.ts +12 -0
  158. package/src/resource-detail/template.meta.ts +21 -0
  159. package/src/resource-form/Page.test.tsx +40 -0
  160. package/src/resource-form/Page.tsx +186 -0
  161. package/src/resource-form/data.ts +63 -0
  162. package/src/resource-form/template.meta.ts +12 -0
  163. package/src/resource-gallery/Page.test.tsx +42 -0
  164. package/src/resource-gallery/Page.tsx +125 -0
  165. package/src/resource-gallery/data.ts +44 -0
  166. package/src/resource-gallery/template.meta.ts +20 -0
  167. package/src/resource-index/Page.test.tsx +120 -0
  168. package/src/resource-index/Page.tsx +216 -0
  169. package/src/resource-index/data.ts +84 -0
  170. package/src/resource-index/template.meta.ts +21 -0
  171. package/src/resource-tree/Page.test.tsx +31 -0
  172. package/src/resource-tree/Page.tsx +232 -0
  173. package/src/resource-tree/data.ts +180 -0
  174. package/src/resource-tree/template.meta.ts +12 -0
  175. package/src/run-trace/Page.test.tsx +56 -0
  176. package/src/run-trace/Page.tsx +242 -0
  177. package/src/run-trace/data.ts +171 -0
  178. package/src/run-trace/template.meta.ts +12 -0
  179. package/src/scorecard/Page.test.tsx +38 -0
  180. package/src/scorecard/Page.tsx +160 -0
  181. package/src/scorecard/TargetsTable.tsx +92 -0
  182. package/src/scorecard/data.ts +118 -0
  183. package/src/scorecard/template.meta.ts +12 -0
  184. package/src/search-results/Page.test.tsx +43 -0
  185. package/src/search-results/Page.tsx +243 -0
  186. package/src/search-results/data.ts +130 -0
  187. package/src/search-results/template.meta.ts +12 -0
  188. package/src/settings-api-keys/Page.test.tsx +39 -0
  189. package/src/settings-api-keys/Page.tsx +171 -0
  190. package/src/settings-api-keys/data.ts +58 -0
  191. package/src/settings-api-keys/template.meta.ts +12 -0
  192. package/src/settings-appearance/Page.test.tsx +44 -0
  193. package/src/settings-appearance/Page.tsx +92 -0
  194. package/src/settings-appearance/data.ts +21 -0
  195. package/src/settings-appearance/template.meta.ts +11 -0
  196. package/src/settings-billing/Page.test.tsx +31 -0
  197. package/src/settings-billing/Page.tsx +71 -0
  198. package/src/settings-billing/data.ts +15 -0
  199. package/src/settings-billing/template.meta.ts +19 -0
  200. package/src/settings-integrations/Page.test.tsx +31 -0
  201. package/src/settings-integrations/Page.tsx +185 -0
  202. package/src/settings-integrations/data.ts +116 -0
  203. package/src/settings-integrations/template.meta.ts +11 -0
  204. package/src/settings-members/Page.test.tsx +32 -0
  205. package/src/settings-members/Page.tsx +140 -0
  206. package/src/settings-members/data.ts +28 -0
  207. package/src/settings-members/template.meta.ts +12 -0
  208. package/src/settings-notifications/Page.test.tsx +31 -0
  209. package/src/settings-notifications/Page.tsx +71 -0
  210. package/src/settings-notifications/data.ts +12 -0
  211. package/src/settings-notifications/template.meta.ts +11 -0
  212. package/src/settings-profile/Baseline.test.tsx +9 -0
  213. package/src/settings-profile/Editable.test.tsx +21 -0
  214. package/src/settings-profile/Page.test.tsx +41 -0
  215. package/src/settings-profile/Page.tsx +133 -0
  216. package/src/settings-profile/ReadOnly.test.tsx +143 -0
  217. package/src/settings-profile/ReadOnlyProfile.tsx +85 -0
  218. package/src/settings-profile/SettingsFrame.test.tsx +55 -0
  219. package/src/settings-profile/SettingsFrame.tsx +88 -0
  220. package/src/settings-profile/__tests__/dom.ts +8 -0
  221. package/src/settings-profile/data.ts +41 -0
  222. package/src/settings-profile/examples/ReadOnly.tsx +31 -0
  223. package/src/settings-profile/fixtures/editable.snap +1 -0
  224. package/src/settings-profile/settings-nav.ts +98 -0
  225. package/src/settings-profile/template.meta.ts +21 -0
  226. package/src/settings-security/MfaSetupDialog.tsx +96 -0
  227. package/src/settings-security/Page.test.tsx +44 -0
  228. package/src/settings-security/Page.tsx +158 -0
  229. package/src/settings-security/data.ts +11 -0
  230. package/src/settings-security/template.meta.ts +11 -0
  231. package/src/settings-workspace/Page.test.tsx +44 -0
  232. package/src/settings-workspace/Page.tsx +120 -0
  233. package/src/settings-workspace/data.ts +44 -0
  234. package/src/settings-workspace/template.meta.ts +19 -0
  235. package/src/shared/ConsoleShell.test.tsx +81 -0
  236. package/src/shared/ConsoleShell.tsx +98 -0
  237. package/src/shell-sidebar/Page.test.tsx +21 -0
  238. package/src/shell-sidebar/Page.tsx +85 -0
  239. package/src/shell-sidebar/data.ts +24 -0
  240. package/src/shell-sidebar/template.meta.ts +12 -0
  241. package/src/shell-topnav/Page.test.tsx +25 -0
  242. package/src/shell-topnav/Page.tsx +94 -0
  243. package/src/shell-topnav/data.ts +25 -0
  244. package/src/shell-topnav/template.meta.ts +12 -0
  245. package/src/sign-in/BrandPanel.tsx +34 -0
  246. package/src/sign-in/Page.test.tsx +41 -0
  247. package/src/sign-in/Page.tsx +73 -0
  248. package/src/sign-in/data.ts +11 -0
  249. package/src/sign-in/template.meta.ts +12 -0
  250. package/src/sign-in-card/Page.test.tsx +182 -0
  251. package/src/sign-in-card/Page.tsx +82 -0
  252. package/src/sign-in-card/data.ts +8 -0
  253. package/src/sign-in-card/examples/Username.tsx +28 -0
  254. package/src/sign-in-card/template.meta.ts +12 -0
  255. package/src/sign-up/Page.test.tsx +33 -0
  256. package/src/sign-up/Page.tsx +70 -0
  257. package/src/sign-up/data.ts +15 -0
  258. package/src/sign-up/template.meta.ts +12 -0
  259. package/src/sso-login/Page.test.tsx +41 -0
  260. package/src/sso-login/Page.tsx +151 -0
  261. package/src/sso-login/data.ts +10 -0
  262. package/src/sso-login/template.meta.ts +12 -0
  263. package/src/status-403/Page.test.tsx +23 -0
  264. package/src/status-403/Page.tsx +92 -0
  265. package/src/status-403/data.ts +13 -0
  266. package/src/status-403/template.meta.ts +12 -0
  267. package/src/status-404/Page.test.tsx +23 -0
  268. package/src/status-404/Page.tsx +60 -0
  269. package/src/status-404/PageHeading.test.tsx +52 -0
  270. package/src/status-404/data.ts +12 -0
  271. package/src/status-404/examples/PageHeader.tsx +28 -0
  272. package/src/status-404/template.meta.ts +12 -0
  273. package/src/status-500/Page.test.tsx +24 -0
  274. package/src/status-500/Page.tsx +70 -0
  275. package/src/status-500/data.ts +20 -0
  276. package/src/status-500/template.meta.ts +12 -0
  277. package/src/status-maintenance/Page.test.tsx +24 -0
  278. package/src/status-maintenance/Page.tsx +84 -0
  279. package/src/status-maintenance/data.ts +14 -0
  280. package/src/status-maintenance/template.meta.ts +12 -0
  281. package/src/styles.css +11 -0
  282. package/src/test/a11y.ts +18 -0
  283. package/src/test/setup.ts +36 -0
  284. package/src/upgrade-required/Page.test.tsx +28 -0
  285. package/src/upgrade-required/Page.tsx +79 -0
  286. package/src/upgrade-required/data.ts +19 -0
  287. package/src/upgrade-required/template.meta.ts +12 -0
  288. package/src/usage-analytics/Page.test.tsx +83 -0
  289. package/src/usage-analytics/Page.tsx +184 -0
  290. package/src/usage-analytics/data.ts +161 -0
  291. package/src/usage-analytics/template.meta.ts +21 -0
  292. package/src/verify-email/Page.test.tsx +32 -0
  293. package/src/verify-email/Page.tsx +118 -0
  294. package/src/verify-email/data.ts +7 -0
  295. package/src/verify-email/template.meta.ts +12 -0
  296. package/src/workflow-builder/Page.test.tsx +97 -0
  297. package/src/workflow-builder/Page.tsx +163 -0
  298. package/src/workflow-builder/data.ts +13 -0
  299. package/src/workflow-builder/template.meta.ts +12 -0
  300. package/README.md +0 -3
@@ -0,0 +1,254 @@
1
+ import { PromptEditor, type PromptRunInput } from '@gntik-ai/blocks';
2
+ import { DiffEditor, type MonacoLoader } from '@gntik-ai/editor';
3
+ import { Plus, X } from '@gntik-ai/icons';
4
+ import {
5
+ Badge,
6
+ Button,
7
+ CanvasLayout,
8
+ Grid,
9
+ HStack,
10
+ IconButton,
11
+ ListInput,
12
+ Section,
13
+ SimpleSelect,
14
+ Stack,
15
+ Switch,
16
+ type BreadcrumbItem,
17
+ type ListInputPair,
18
+ type ListInputRow,
19
+ } from '@gntik-ai/ui';
20
+ import { useState } from 'react';
21
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
22
+ import {
23
+ playgroundBreadcrumbs,
24
+ playgroundDefaultColumns,
25
+ playgroundModels,
26
+ playgroundPrompt,
27
+ playgroundVariables,
28
+ sampleOutput,
29
+ simulateGenerate,
30
+ type PlaygroundModel,
31
+ type PlaygroundOutput,
32
+ } from './data';
33
+
34
+ export interface PromptPlaygroundProps {
35
+ title: string;
36
+ /** Initial prompt (system, user, temperature, max tokens). */
37
+ prompt: { system: string; user: string; temperature: number; maxTokens: number };
38
+ /** Initial shared variables (ListInput rows); non-empty values override the editor's own fields. */
39
+ variables: ListInputPair[];
40
+ models: PlaygroundModel[];
41
+ /** Model ids of the initial columns (2–3). */
42
+ defaultColumns: string[];
43
+ /** Runs the prompt on one model. Defaults to a simulated answer. */
44
+ onGenerate: (model: string, input: PromptRunInput) => PlaygroundOutput | Promise<PlaygroundOutput>;
45
+ /** Monaco loader for the diff view (self-hosting, tests). */
46
+ loader: MonacoLoader;
47
+ breadcrumbs: BreadcrumbItem[];
48
+ currentHref: string;
49
+ /** ConsoleShell props (app nav, user, workspaces, sidebar footer, topbar…). */
50
+ shell: Omit<ConsoleShellProps, 'children'>;
51
+ }
52
+
53
+ const MAX_COLUMNS = 3;
54
+
55
+ interface Column {
56
+ id: number;
57
+ model: string;
58
+ output?: PlaygroundOutput;
59
+ error?: string;
60
+ }
61
+
62
+ /**
63
+ * Prompt playground. CanvasLayout `embedded` inside ConsoleShell: the playground is a console
64
+ * tool people jump in and out of (from a trace, an evaluation), so the app nav stays; the
65
+ * canvas layout still gives it the editor chrome (top bar, variables palette). PromptEditor
66
+ * runs the prompt on 2–3 model columns side by side; "Diff" compares each column against the
67
+ * first one in a DiffEditor.
68
+ */
69
+ export default function PromptPlaygroundPage(props: Partial<PromptPlaygroundProps>) {
70
+ const {
71
+ title = playgroundPrompt.title,
72
+ prompt = playgroundPrompt,
73
+ variables = playgroundVariables,
74
+ models = playgroundModels,
75
+ defaultColumns = playgroundDefaultColumns,
76
+ onGenerate = simulateGenerate,
77
+ loader,
78
+ breadcrumbs = playgroundBreadcrumbs,
79
+ currentHref = '/prompts',
80
+ shell,
81
+ } = props;
82
+ const [rows, setRows] = useState<ListInputRow[] | undefined>(undefined);
83
+ const [columns, setColumns] = useState<Column[]>(() =>
84
+ defaultColumns.slice(0, MAX_COLUMNS).map((model, id) => ({ id, model, output: sampleOutput(model) })),
85
+ );
86
+ const [nextId, setNextId] = useState(defaultColumns.length);
87
+ const [diff, setDiff] = useState(false);
88
+ const [status, setStatus] = useState('');
89
+
90
+ const shared = Object.fromEntries(
91
+ (rows ?? variables).filter((r) => r.key.trim() && r.value.trim()).map((r) => [r.key.trim(), r.value]),
92
+ );
93
+ const modelItems = models.map((m) => ({ value: m.id, label: m.label }));
94
+ const labelOf = (id: string) => models.find((m) => m.id === id)?.label ?? id;
95
+ const baseline = columns[0];
96
+
97
+ const run = async (input: PromptRunInput) => {
98
+ const merged: PromptRunInput = { ...input, variables: { ...input.variables, ...shared } };
99
+ setStatus(`Running on ${columns.length} models…`);
100
+ const results = await Promise.all(
101
+ columns.map(async (c) => {
102
+ try {
103
+ return { id: c.id, output: await onGenerate(c.model, merged) };
104
+ } catch (e) {
105
+ return { id: c.id, error: e instanceof Error ? e.message : 'The model did not answer.' };
106
+ }
107
+ }),
108
+ );
109
+ setColumns((cols) => cols.map((c) => ({ ...c, ...results.find((r) => r.id === c.id) })));
110
+ setStatus(`Finished on ${results.length} models.`);
111
+ };
112
+
113
+ const addColumn = () => {
114
+ const model = models.find((m) => !columns.some((c) => c.model === m.id))?.id ?? models[0]?.id ?? '';
115
+ setColumns((cols) => [...cols, { id: nextId, model }]);
116
+ setNextId((n) => n + 1);
117
+ };
118
+
119
+ return (
120
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
121
+ <CanvasLayout
122
+ embedded
123
+ mainId="playground-canvas"
124
+ mainLabel={`${title} playground`}
125
+ smallScreenNotice={null}
126
+ paletteLabel="Variables"
127
+ palette={
128
+ <Stack gap={2}>
129
+ <p className="text-[12px] text-muted-foreground">
130
+ Shared by every model column. Paste KEY=value lines to add several.
131
+ </p>
132
+ <ListInput
133
+ label="Prompt variables"
134
+ defaultValue={variables}
135
+ onValueChange={setRows}
136
+ uniqueKeys
137
+ keyPlaceholder="name"
138
+ valuePlaceholder="value"
139
+ />
140
+ </Stack>
141
+ }
142
+ header={
143
+ <>
144
+ <div className="flex min-w-0 flex-1 items-center gap-3">
145
+ <h1 className="truncate text-[14px] font-semibold text-foreground">{title}</h1>
146
+ <Badge size="sm">Playground</Badge>
147
+ <p aria-live="polite" className="hidden truncate text-[12px] text-muted-foreground sm:block">
148
+ {status}
149
+ </p>
150
+ </div>
151
+ <Switch label="Diff" checked={diff} onCheckedChange={setDiff} disabled={columns.length < 2} />
152
+ </>
153
+ }
154
+ >
155
+ <div className="h-full overflow-y-auto p-3 sm:p-4">
156
+ <Stack gap={4}>
157
+ <PromptEditor
158
+ titleAs="h2"
159
+ title="Prompt"
160
+ defaultSystem={prompt.system}
161
+ defaultUser={prompt.user}
162
+ defaultVariables={Object.fromEntries(variables.map((v) => [v.key, v.value]))}
163
+ defaultTemperature={prompt.temperature}
164
+ defaultMaxTokens={prompt.maxTokens}
165
+ onRun={run}
166
+ />
167
+ <Section
168
+ title="Outputs"
169
+ headingLevel="h2"
170
+ description={
171
+ diff && baseline ? `Changes against ${labelOf(baseline.model)}.` : 'Each column runs the same prompt.'
172
+ }
173
+ actions={
174
+ <Button
175
+ size="sm"
176
+ variant="secondary"
177
+ icon={Plus}
178
+ disabled={columns.length >= MAX_COLUMNS}
179
+ onClick={addColumn}
180
+ >
181
+ Add model
182
+ </Button>
183
+ }
184
+ >
185
+ <Grid cols={{ base: 1, md: columns.length >= 3 ? 3 : 2 }} gap={3}>
186
+ {columns.map((c, i) => (
187
+ <Section
188
+ key={c.id}
189
+ variant="card"
190
+ padding="sm"
191
+ headingLevel="h3"
192
+ title={`Column ${i + 1}: ${labelOf(c.model)}`}
193
+ actions={
194
+ columns.length > 2 && (
195
+ <IconButton
196
+ size="sm"
197
+ icon={X}
198
+ label={`Remove column ${i + 1}`}
199
+ onClick={() => setColumns((cols) => cols.filter((x) => x.id !== c.id))}
200
+ />
201
+ )
202
+ }
203
+ >
204
+ <Stack gap={3}>
205
+ <SimpleSelect
206
+ aria-label={`Model for column ${i + 1}`}
207
+ size="sm"
208
+ items={modelItems}
209
+ value={c.model}
210
+ onValueChange={(model) =>
211
+ model && setColumns((cols) => cols.map((x) => (x.id === c.id ? { id: x.id, model } : x)))
212
+ }
213
+ />
214
+ {c.error ? (
215
+ <p className="text-[12.5px] text-destructive-text">{c.error}</p>
216
+ ) : !c.output ? (
217
+ <p className="text-[12.5px] text-muted-foreground">
218
+ Run the prompt to see this model&apos;s answer.
219
+ </p>
220
+ ) : diff && i > 0 && baseline?.output ? (
221
+ <DiffEditor
222
+ original={baseline.output.text}
223
+ modified={c.output.text}
224
+ sideBySide={false}
225
+ height={220}
226
+ aria-label={`${labelOf(c.model)} compared with ${labelOf(baseline.model)}`}
227
+ loader={loader}
228
+ />
229
+ ) : (
230
+ <p className="text-[13px] leading-relaxed whitespace-pre-wrap text-foreground">
231
+ {c.output.text}
232
+ </p>
233
+ )}
234
+ {c.output && (
235
+ <HStack gap={2} wrap>
236
+ <Badge size="sm" className="font-mono">
237
+ {c.output.latencyMs} ms
238
+ </Badge>
239
+ <Badge size="sm" className="font-mono">
240
+ {c.output.outputTokens} tokens
241
+ </Badge>
242
+ </HStack>
243
+ )}
244
+ </Stack>
245
+ </Section>
246
+ ))}
247
+ </Grid>
248
+ </Section>
249
+ </Stack>
250
+ </div>
251
+ </CanvasLayout>
252
+ </ConsoleShell>
253
+ );
254
+ }
@@ -0,0 +1,68 @@
1
+ import type { PromptRunInput } from '@gntik-ai/blocks';
2
+ import type { BreadcrumbItem, ListInputPair } from '@gntik-ai/ui';
3
+
4
+ /** A model the playground can compare. */
5
+ export interface PlaygroundModel {
6
+ id: string;
7
+ label: string;
8
+ }
9
+
10
+ /** One model's answer to a run. */
11
+ export interface PlaygroundOutput {
12
+ text: string;
13
+ latencyMs: number;
14
+ outputTokens: number;
15
+ }
16
+
17
+ export const playgroundModels: PlaygroundModel[] = [
18
+ { id: 'general-large', label: 'general-large' },
19
+ { id: 'general-small', label: 'general-small' },
20
+ { id: 'reasoning-medium', label: 'reasoning-medium' },
21
+ { id: 'self-hosted-8b', label: 'self-hosted-8b' },
22
+ ];
23
+
24
+ export const playgroundDefaultColumns = ['general-large', 'general-small'];
25
+
26
+ export const playgroundPrompt = {
27
+ title: 'Ticket summary',
28
+ system: 'You are a concise support assistant for {{company}}. Answer in {{language}}.',
29
+ user: 'Summarise the ticket below in three bullet points and suggest a next step.\n\n{{ticket}}',
30
+ temperature: 0.4,
31
+ maxTokens: 512,
32
+ };
33
+
34
+ /** Shared variable values applied to every column (they override the editor's own fields). */
35
+ export const playgroundVariables: ListInputPair[] = [
36
+ { key: 'company', value: 'Northwind' },
37
+ { key: 'language', value: 'English' },
38
+ { key: 'ticket', value: 'Customer cannot export invoices as CSV since the last release; the button spins forever.' },
39
+ ];
40
+
41
+ export const playgroundBreadcrumbs: BreadcrumbItem[] = [
42
+ { label: 'Prompts', href: '/prompts' },
43
+ { label: 'ticket-summary', mono: true },
44
+ ];
45
+
46
+ const SAMPLE_OUTPUTS: Record<string, string> = {
47
+ 'general-large':
48
+ '- Invoice CSV export hangs since the latest release.\n- The export button keeps spinning and no file is produced.\n- Affects the billing area for at least one customer.\nNext step: reproduce on staging and check the export job logs.',
49
+ 'general-small':
50
+ '- CSV export of invoices is broken.\n- The export button keeps spinning and no file is produced.\n- Started after the last release.\nNext step: roll back the export change.',
51
+ 'reasoning-medium':
52
+ '- Invoice CSV export hangs since the latest release.\n- The spinner suggests the export request never completes.\n- Likely a regression in the export job.\nNext step: compare export job logs before and after the release.',
53
+ 'self-hosted-8b': '- Export to CSV not working.\n- Button spins.\n- Since release.\nNext step: investigate.',
54
+ };
55
+
56
+ /** Sample output of a model before the first run (or for a model without a sample). */
57
+ export function sampleOutput(model: string): PlaygroundOutput {
58
+ const text = SAMPLE_OUTPUTS[model] ?? SAMPLE_OUTPUTS['general-small'] ?? '';
59
+ return { text, latencyMs: 600 + ((model.length * 97) % 900), outputTokens: Math.round(text.length / 4) };
60
+ }
61
+
62
+ /** Simulated generation: the sample output, prefixed with the resolved variables. */
63
+ export function simulateGenerate(model: string, input: PromptRunInput): Promise<PlaygroundOutput> {
64
+ const out = sampleOutput(model);
65
+ const company = input.variables.company?.trim();
66
+ const text = company ? `${out.text}\n(${company}, temperature ${input.temperature})` : out.text;
67
+ return new Promise((resolve) => setTimeout(() => resolve({ ...out, text }), 400));
68
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Prompt playground',
5
+ family: 'AI',
6
+ priority: 'P3',
7
+ status: 'experimental',
8
+ description:
9
+ 'Prompt playground inside the console (CanvasLayout embedded in ConsoleShell, so the app nav stays): shared variables in a ListInput palette, the PromptEditor, and 2–3 model columns side by side with a Diff switch that compares each column against the first in a DiffEditor.',
10
+ layout: 'CanvasLayout',
11
+ blocks: ['app-sidebar', 'app-topbar', 'prompt-editor'],
12
+ };
@@ -0,0 +1,176 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { ThemeProvider } from '@gntik-ai/ui';
4
+ import { renderToString } from 'react-dom/server';
5
+ import { expectNoAxeViolations } from '../test/a11y';
6
+ import PublicHubPage from './Page';
7
+
8
+ // Public seam: the rendered page's content order and accessible document outline.
9
+ const full = {
10
+ eyebrow: 'Start here',
11
+ title: 'Welcome',
12
+ description: 'Choose how to continue.',
13
+ cards: [
14
+ { title: 'Your workspace', body: 'Keep your work together.' },
15
+ { title: 'Your team', body: 'Collaborate with others.' },
16
+ ],
17
+ primaryAction: { label: 'Sign in', href: '#sign-in' },
18
+ secondaryAction: { label: 'Create an account', href: '#sign-up' },
19
+ tertiaryLink: { label: 'Recover access', href: '#recovery' },
20
+ footer: 'Contact support for help.',
21
+ };
22
+
23
+ describe('PublicHubPage', () => {
24
+ it('names the region from its page heading in server HTML before hydration', () => {
25
+ const container = document.createElement('div');
26
+ container.innerHTML = renderToString(<PublicHubPage {...full} />);
27
+ document.body.append(container);
28
+ try {
29
+ const region = within(container).getByRole('region', { name: full.title });
30
+ const heading = within(region).getByRole('heading', { level: 1, name: full.title });
31
+ const labelId = region.getAttribute('aria-labelledby');
32
+ expect(labelId).toBeTruthy();
33
+ const label = document.getElementById(labelId!);
34
+ expect(label).toHaveTextContent(full.title);
35
+ expect(heading.contains(label)).toBe(true);
36
+ } finally {
37
+ container.remove();
38
+ }
39
+ });
40
+
41
+ it('renders the full hub in reading order with a single labelled page heading', () => {
42
+ const { container } = render(<PublicHubPage {...full} />);
43
+ const heading = screen.getByRole('heading', { level: 1, name: full.title });
44
+ const region = screen.getByRole('region', { name: full.title });
45
+ const titleLabel = within(heading).getByText(full.title);
46
+ expect(titleLabel.id).not.toBe('');
47
+ expect(region).toHaveAttribute('aria-labelledby', titleLabel.id);
48
+ expect(container.querySelectorAll('h1')).toHaveLength(1);
49
+ expect(
50
+ within(region)
51
+ .getAllByRole('heading', { level: 2 })
52
+ .map((h) => h.textContent),
53
+ ).toEqual(['Your workspace', 'Your team']);
54
+ const list = within(region).getByRole('list');
55
+ expect(list.tagName).toBe('UL');
56
+ expect(within(list).getAllByRole('listitem')).toHaveLength(2);
57
+ expect(list).toHaveClass('grid-cols-1', 'sm:grid-cols-2');
58
+ const ordered = [
59
+ screen.getByText(full.eyebrow),
60
+ heading,
61
+ screen.getByText(full.description),
62
+ list,
63
+ screen.getByRole('link', { name: 'Sign in' }),
64
+ screen.getByRole('link', { name: 'Create an account' }),
65
+ screen.getByRole('link', { name: 'Recover access' }),
66
+ screen.getByRole('separator'),
67
+ screen.getByText(full.footer),
68
+ ];
69
+ ordered.slice(1).forEach((element, index) => {
70
+ expect(ordered[index]!.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
71
+ });
72
+ expect(region.querySelectorAll('header')).toHaveLength(1);
73
+ expect(within(region).queryByRole('navigation')).not.toBeInTheDocument();
74
+ expect(within(region).queryByRole('tablist')).not.toBeInTheDocument();
75
+ expect(region.querySelector('dl')).toBeNull();
76
+ for (const card of full.cards) expect(within(list).getByText(card.body)).toBeVisible();
77
+ expect(within(region).queryByRole('button')).not.toBeInTheDocument();
78
+ expect(container.querySelector('img')).toBeNull();
79
+ });
80
+
81
+ it('replaces the full content with decorative kit skeletons in a named busy region', () => {
82
+ const { rerender } = render(<PublicHubPage {...full} loading />);
83
+ const region = screen.getByRole('region', { name: 'Loading' });
84
+ expect(region).toHaveAttribute('aria-busy', 'true');
85
+ expect(region).not.toHaveAttribute('aria-labelledby');
86
+ expect(within(region).queryByRole('heading')).not.toBeInTheDocument();
87
+ expect(within(region).queryByRole('link')).not.toBeInTheDocument();
88
+ expect(within(region).queryByRole('separator')).not.toBeInTheDocument();
89
+ expect(within(region).queryByText(full.eyebrow)).not.toBeInTheDocument();
90
+ expect(within(region).queryByText(full.footer)).not.toBeInTheDocument();
91
+ expect(region.querySelector('[aria-hidden="true"]')).toBeInTheDocument();
92
+ rerender(<PublicHubPage loading loadingLabel="Loading account access" />);
93
+ const minimal = screen.getByRole('region', {
94
+ name: 'Loading account access',
95
+ });
96
+ expect(minimal.querySelector('[aria-hidden="true"]')).toBeInTheDocument();
97
+ expect(within(minimal).queryByRole('list')).not.toBeInTheDocument();
98
+ rerender(<PublicHubPage {...full} />);
99
+ expect(screen.getByRole('region', { name: full.title })).not.toHaveAttribute('aria-busy');
100
+ });
101
+
102
+ for (const optional of ['eyebrow', 'description', 'cards', 'secondaryAction', 'tertiaryLink', 'footer'] as const) {
103
+ it(`omits ${optional} without an empty replacement`, () => {
104
+ render(<PublicHubPage {...full} {...{ [optional]: undefined }} />);
105
+ const region = screen.getByRole('region', { name: full.title });
106
+ if (optional === 'cards') {
107
+ expect(within(region).queryByRole('list')).not.toBeInTheDocument();
108
+ expect(within(region).queryByRole('heading', { level: 2 })).not.toBeInTheDocument();
109
+ } else if (optional === 'secondaryAction' || optional === 'tertiaryLink') {
110
+ expect(within(region).queryByRole('link', { name: full[optional].label })).not.toBeInTheDocument();
111
+ } else {
112
+ expect(within(region).queryByText(full[optional])).not.toBeInTheDocument();
113
+ }
114
+ if (optional === 'footer') expect(within(region).queryByRole('separator')).not.toBeInTheDocument();
115
+ const emptyWrappers = [...region.querySelectorAll('div:empty, ul:empty, li:empty')].filter(
116
+ (element) => element.getAttribute('role') !== 'separator',
117
+ );
118
+ expect(emptyWrappers).toHaveLength(0);
119
+ });
120
+ }
121
+
122
+ it('renders the minimal fixture and no list for an empty card array', () => {
123
+ render(<PublicHubPage cards={[]} logo={<span>Example mark</span>} />);
124
+ const region = screen.getByRole('region', { name: 'Welcome' });
125
+ expect(within(region).getAllByRole('link')).toHaveLength(1);
126
+ expect(within(region).queryByRole('list')).not.toBeInTheDocument();
127
+ expect(within(region).queryByRole('separator')).not.toBeInTheDocument();
128
+ expect(region.querySelectorAll('div:empty, ul:empty, li:empty')).toHaveLength(0);
129
+ expect(screen.getByText('Example mark')).toBeInTheDocument();
130
+ });
131
+
132
+ it('tabs through navigation links in order, activates each with Enter, and leaves the page', async () => {
133
+ const user = userEvent.setup();
134
+ const actions = [full.primaryAction, full.secondaryAction, full.tertiaryLink].map((action) => ({
135
+ ...action,
136
+ onClick: vi.fn((event) => event.preventDefault()),
137
+ }));
138
+ render(
139
+ <>
140
+ <PublicHubPage {...full} primaryAction={actions[0]} secondaryAction={actions[1]} tertiaryLink={actions[2]} />
141
+ <a href="#after">After hub</a>
142
+ </>,
143
+ );
144
+ await user.tab();
145
+ expect(screen.getByRole('link', { name: 'Skip to main content' })).toHaveFocus();
146
+ for (const action of actions) {
147
+ await user.tab();
148
+ const link = screen.getByRole('link', { name: action.label });
149
+ expect(link.tagName).toBe('A');
150
+ expect(link).toHaveAttribute('href', action.href);
151
+ expect(link).toHaveFocus();
152
+ await user.keyboard('{Enter}');
153
+ expect(action.onClick).toHaveBeenCalledOnce();
154
+ }
155
+ await user.tab();
156
+ expect(screen.getByRole('link', { name: 'After hub' })).toHaveFocus();
157
+ await user.tab({ shift: true });
158
+ expect(screen.getByRole('link', { name: 'Recover access' })).toHaveFocus();
159
+ });
160
+ });
161
+
162
+ // Theme × fixture × loading matrix exercises the real composition, with no kit mocks.
163
+ for (const theme of ['dark', 'light', 'high_contrast'] as const) {
164
+ for (const fixture of ['full', 'minimal'] as const) {
165
+ for (const loading of [false, true]) {
166
+ it(`axe: ${theme} / ${fixture} / loading=${loading}`, async () => {
167
+ const { container } = render(
168
+ <ThemeProvider defaultMode={theme} storageKey={null}>
169
+ <PublicHubPage {...(fixture === 'full' ? full : {})} loading={loading} />
170
+ </ThemeProvider>,
171
+ );
172
+ await expectNoAxeViolations(container);
173
+ });
174
+ }
175
+ }
176
+ }
@@ -0,0 +1,125 @@
1
+ import { PageHeader } from '@gntik-ai/blocks';
2
+ import { AuthLayout, Badge, Button, Card, CardTitle, Grid, Link, Separator, Skeleton, Text } from '@gntik-ai/ui';
3
+ import { useId, type MouseEventHandler, type ReactNode } from 'react';
4
+ import { publicHubCopy } from './data';
5
+
6
+ export interface PublicHubAction {
7
+ label: string;
8
+ href: string;
9
+ onClick?: MouseEventHandler<HTMLAnchorElement>;
10
+ }
11
+
12
+ export interface PublicHubPageProps {
13
+ eyebrow?: string;
14
+ title: string;
15
+ description?: string;
16
+ cards?: { title: string; body: string }[];
17
+ primaryAction: PublicHubAction;
18
+ secondaryAction?: PublicHubAction;
19
+ tertiaryLink?: PublicHubAction;
20
+ footer?: string;
21
+ loading?: boolean;
22
+ logo?: ReactNode;
23
+ loadingLabel?: string;
24
+ }
25
+
26
+ /** Unauthenticated entry hub; optional content stays absent unless supplied. */
27
+ export default function PublicHubPage({
28
+ eyebrow,
29
+ title = publicHubCopy.title,
30
+ description,
31
+ cards,
32
+ primaryAction = publicHubCopy.primaryAction,
33
+ secondaryAction,
34
+ tertiaryLink,
35
+ footer,
36
+ logo,
37
+ loading = false,
38
+ loadingLabel = 'Loading',
39
+ }: Partial<PublicHubPageProps>) {
40
+ const titleId = useId();
41
+ return (
42
+ <AuthLayout variant="full-bleed" fullScreen logo={logo} skipLinkLabel="Skip to main content">
43
+ <section
44
+ aria-labelledby={loading ? undefined : titleId}
45
+ aria-label={loading ? loadingLabel : undefined}
46
+ aria-busy={loading || undefined}
47
+ className="flex min-w-0 flex-col gap-6 break-words"
48
+ >
49
+ {eyebrow && (loading ? <Skeleton className="h-5 w-24" /> : <Badge className="self-start">{eyebrow}</Badge>)}
50
+ {loading ? (
51
+ <div className="flex flex-col gap-3">
52
+ <Skeleton className="h-8 w-2/3" />
53
+ {description && <Skeleton shape="text" lines={2} />}
54
+ </div>
55
+ ) : (
56
+ <PageHeader
57
+ breadcrumbs={null}
58
+ status=""
59
+ meta={[]}
60
+ actions={[]}
61
+ tabs={null}
62
+ title={<span id={titleId}>{title}</span>}
63
+ description={description ?? null}
64
+ />
65
+ )}
66
+ {cards && cards.length > 0 && (
67
+ <Grid as="ul" cols={{ base: 1, sm: 2 }} gap={4}>
68
+ {cards.map((card, index) => (
69
+ <li key={index} className="min-w-0">
70
+ {loading ? (
71
+ <Skeleton className="h-32 w-full" />
72
+ ) : (
73
+ <Card className="flex h-full min-w-0 flex-col gap-3 p-4">
74
+ <CardTitle as="h2">{card.title}</CardTitle>
75
+ <Text variant="supporting">{card.body}</Text>
76
+ </Card>
77
+ )}
78
+ </li>
79
+ ))}
80
+ </Grid>
81
+ )}
82
+ <div className="flex min-w-0 flex-col items-start gap-3">
83
+ {loading ? (
84
+ <Skeleton className="h-9 w-32" />
85
+ ) : (
86
+ <Button
87
+ nativeButton={false}
88
+ role="link"
89
+ render={<a href={primaryAction.href} onClick={primaryAction.onClick} />}
90
+ >
91
+ {primaryAction.label}
92
+ </Button>
93
+ )}
94
+ {secondaryAction &&
95
+ (loading ? (
96
+ <Skeleton className="h-9 w-40" />
97
+ ) : (
98
+ <Button
99
+ variant="secondary"
100
+ nativeButton={false}
101
+ role="link"
102
+ render={<a href={secondaryAction.href} onClick={secondaryAction.onClick} />}
103
+ >
104
+ {secondaryAction.label}
105
+ </Button>
106
+ ))}
107
+ {tertiaryLink &&
108
+ (loading ? (
109
+ <Skeleton className="h-5 w-32" />
110
+ ) : (
111
+ <Link href={tertiaryLink.href} onClick={tertiaryLink.onClick}>
112
+ {tertiaryLink.label}
113
+ </Link>
114
+ ))}
115
+ </div>
116
+ {!loading && footer && (
117
+ <>
118
+ <Separator />
119
+ <Text variant="supporting">{footer}</Text>
120
+ </>
121
+ )}
122
+ </section>
123
+ </AuthLayout>
124
+ );
125
+ }