@gntik-ai/templates 0.0.0-stage → 0.6.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,198 @@
1
+ import { ModelKeysList, type ModelKey } from '@gntik-ai/blocks';
2
+ import { Plus } from '@gntik-ai/icons';
3
+ import {
4
+ Button,
5
+ Dialog,
6
+ DialogBody,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ Field,
13
+ FieldDescription,
14
+ FieldLabel,
15
+ Input,
16
+ Meter,
17
+ PasswordInput,
18
+ Section,
19
+ SimpleSelect,
20
+ Stack,
21
+ } from '@gntik-ai/ui';
22
+ import { useState, type FormEvent } from 'react';
23
+ import { SettingsFrame, type SettingsFrameOptions } from '../settings-profile/SettingsFrame';
24
+ import {
25
+ maskSecret,
26
+ modelProvidersNavItem,
27
+ providerKeys,
28
+ providerOptions,
29
+ providerUsageLimits,
30
+ type NewModelKeyInput,
31
+ type ProviderUsageLimit,
32
+ } from './data';
33
+
34
+ export interface ModelProvidersProps extends SettingsFrameOptions {
35
+ keys: ModelKey[];
36
+ limits: ProviderUsageLimit[];
37
+ providers: Array<{ value: string; label: string }>;
38
+ /** Tests a key (the "Test" button); resolve true when the provider answered. */
39
+ onTest: (key: ModelKey) => Promise<boolean> | boolean;
40
+ onRevoke: (key: ModelKey) => void;
41
+ /** Stores a new key and returns it masked. Defaults to a local, untested entry. */
42
+ onAddKey: (input: NewModelKeyInput) => ModelKey | Promise<ModelKey>;
43
+ }
44
+
45
+ const EMPTY: NewModelKeyInput = { name: '', provider: '', endpoint: '', secret: '' };
46
+
47
+ /**
48
+ * Model provider settings: SettingsFrame (shared settings nav + a "Model providers" item) with
49
+ * the ModelKeysList (masked keys, test connection, revoke), an add-key dialog and the usage
50
+ * limits as Meters.
51
+ */
52
+ export default function ModelProvidersPage({
53
+ keys: initialKeys = providerKeys,
54
+ limits = providerUsageLimits,
55
+ providers = providerOptions,
56
+ onTest,
57
+ onRevoke,
58
+ onAddKey,
59
+ extraNavItems = [],
60
+ ...frame
61
+ }: Partial<ModelProvidersProps>) {
62
+ const [keys, setKeys] = useState(initialKeys);
63
+ const [open, setOpen] = useState(false);
64
+ const [draft, setDraft] = useState(EMPTY);
65
+ const [submitted, setSubmitted] = useState(false);
66
+ const [saving, setSaving] = useState(false);
67
+ const needsEndpoint = draft.provider === 'openai-compatible' || draft.provider === 'self-hosted';
68
+ const invalid = {
69
+ name: !draft.name.trim(),
70
+ provider: !draft.provider,
71
+ endpoint: needsEndpoint && !/^https?:\/\/\S+$/.test(draft.endpoint.trim()),
72
+ secret: draft.secret.trim().length < 8,
73
+ };
74
+ const set = (key: keyof NewModelKeyInput) => (value: string | null) =>
75
+ setDraft((d) => ({ ...d, [key]: value ?? '' }));
76
+
77
+ const openDialog = () => {
78
+ setDraft(EMPTY);
79
+ setSubmitted(false);
80
+ setOpen(true);
81
+ };
82
+
83
+ const submit = async (event: FormEvent) => {
84
+ event.preventDefault();
85
+ setSubmitted(true);
86
+ if (Object.values(invalid).some(Boolean)) return;
87
+ setSaving(true);
88
+ try {
89
+ const input = { ...draft, endpoint: needsEndpoint ? draft.endpoint.trim() : '' };
90
+ const created = onAddKey
91
+ ? await onAddKey(input)
92
+ : {
93
+ id: `key-${keys.length + 1}`,
94
+ name: input.name.trim(),
95
+ provider: input.provider,
96
+ endpoint: input.endpoint || undefined,
97
+ maskedKey: maskSecret(input.secret),
98
+ status: 'untested' as const,
99
+ };
100
+ setKeys((list) => [...list, created]);
101
+ setOpen(false);
102
+ } finally {
103
+ setSaving(false);
104
+ }
105
+ };
106
+
107
+ return (
108
+ <SettingsFrame
109
+ page="model-providers"
110
+ extraNavItems={[modelProvidersNavItem, ...extraNavItems.filter((i) => i.id !== modelProvidersNavItem.id)]}
111
+ description="Connect the model providers your assistants call. Keys are encrypted at rest and only shown masked."
112
+ actions={[{ label: 'Add provider key', icon: Plus, variant: 'primary', onClick: openDialog }]}
113
+ {...frame}
114
+ >
115
+ <Stack gap={6}>
116
+ <ModelKeysList titleAs="h2" title="Provider keys" keys={keys} onTest={onTest} onRevoke={onRevoke} />
117
+ <Section
118
+ variant="card"
119
+ headingLevel="h2"
120
+ title="Usage limits"
121
+ description="Workspace-wide caps across every provider key."
122
+ >
123
+ <Stack gap={5}>
124
+ {limits.map((l) => (
125
+ <Meter
126
+ key={l.id}
127
+ label={l.label}
128
+ value={l.value}
129
+ max={l.max}
130
+ valueLabel={l.valueLabel}
131
+ aria-valuetext={l.valueLabel}
132
+ note={l.note}
133
+ />
134
+ ))}
135
+ </Stack>
136
+ </Section>
137
+ </Stack>
138
+ <Dialog open={open} onOpenChange={setOpen}>
139
+ <DialogContent>
140
+ <form onSubmit={(e) => void submit(e)} noValidate>
141
+ <DialogHeader>
142
+ <DialogTitle>Add provider key</DialogTitle>
143
+ <DialogDescription>The key is stored encrypted; you will not be able to see it again.</DialogDescription>
144
+ </DialogHeader>
145
+ <DialogBody>
146
+ <Stack gap={4}>
147
+ <Field invalid={submitted && invalid.name}>
148
+ <FieldLabel required>Name</FieldLabel>
149
+ <Input value={draft.name} onValueChange={set('name')} placeholder="Production" />
150
+ </Field>
151
+ <SimpleSelect
152
+ label="Provider"
153
+ items={providers}
154
+ value={draft.provider || null}
155
+ onValueChange={set('provider')}
156
+ placeholder="Choose a provider…"
157
+ />
158
+ {needsEndpoint && (
159
+ <Field invalid={submitted && invalid.endpoint}>
160
+ <FieldLabel required>Endpoint URL</FieldLabel>
161
+ <Input
162
+ type="url"
163
+ value={draft.endpoint}
164
+ onValueChange={set('endpoint')}
165
+ placeholder="https://llm-gateway.internal/v1"
166
+ className="font-mono"
167
+ />
168
+ </Field>
169
+ )}
170
+ <Field invalid={submitted && invalid.secret}>
171
+ <FieldLabel required>API key</FieldLabel>
172
+ <PasswordInput
173
+ value={draft.secret}
174
+ onValueChange={set('secret')}
175
+ autoComplete="off"
176
+ className="font-mono"
177
+ />
178
+ <FieldDescription>At least 8 characters. Test the connection after adding it.</FieldDescription>
179
+ </Field>
180
+ {submitted && invalid.provider && (
181
+ <p className="text-[12.5px] text-destructive-text">Choose a provider.</p>
182
+ )}
183
+ </Stack>
184
+ </DialogBody>
185
+ <DialogFooter>
186
+ <Button type="button" variant="secondary" onClick={() => setOpen(false)}>
187
+ Cancel
188
+ </Button>
189
+ <Button type="submit" loading={saving}>
190
+ Add key
191
+ </Button>
192
+ </DialogFooter>
193
+ </form>
194
+ </DialogContent>
195
+ </Dialog>
196
+ </SettingsFrame>
197
+ );
198
+ }
@@ -0,0 +1,101 @@
1
+ import type { ModelKey } from '@gntik-ai/blocks';
2
+ import { Cpu } from '@gntik-ai/icons';
3
+ import type { SettingsNavExtraItem } from '../settings-profile/settings-nav';
4
+
5
+ /** Nav entry this page adds to the shared settings nav (id `model-providers`). */
6
+ export const modelProvidersNavItem: SettingsNavExtraItem = {
7
+ id: 'model-providers',
8
+ label: 'Model providers',
9
+ icon: Cpu,
10
+ group: 'Workspace',
11
+ };
12
+
13
+ /** A usage limit shown as a Meter (monthly tokens, spend, rate). */
14
+ export interface ProviderUsageLimit {
15
+ id: string;
16
+ label: string;
17
+ value: number;
18
+ max: number;
19
+ /** Visible value text, e.g. "31.2M / 50M tokens". */
20
+ valueLabel: string;
21
+ /** Helper line under the bar. */
22
+ note?: string;
23
+ }
24
+
25
+ /** What the add-key dialog collects; the secret never comes back to the UI. */
26
+ export interface NewModelKeyInput {
27
+ name: string;
28
+ provider: string;
29
+ endpoint: string;
30
+ secret: string;
31
+ }
32
+
33
+ export const providerOptions = [
34
+ { value: 'Provider A', label: 'Provider A' },
35
+ { value: 'Provider B', label: 'Provider B' },
36
+ { value: 'openai-compatible', label: 'OpenAI-compatible endpoint' },
37
+ { value: 'self-hosted', label: 'Self-hosted' },
38
+ ];
39
+
40
+ export const providerKeys: ModelKey[] = [
41
+ {
42
+ id: 'k1',
43
+ name: 'Production',
44
+ provider: 'Provider A',
45
+ maskedKey: 'pa-…4f2a',
46
+ status: 'connected',
47
+ lastUsed: '3 min ago',
48
+ },
49
+ {
50
+ id: 'k2',
51
+ name: 'Evaluations',
52
+ provider: 'Provider B',
53
+ maskedKey: 'pb-…a913',
54
+ status: 'connected',
55
+ lastUsed: '1 h ago',
56
+ },
57
+ {
58
+ id: 'k3',
59
+ name: 'Gateway',
60
+ provider: 'openai-compatible',
61
+ endpoint: 'https://llm-gateway.internal/v1',
62
+ maskedKey: 'sk-…9c01',
63
+ status: 'untested',
64
+ },
65
+ {
66
+ id: 'k4',
67
+ name: 'On-prem cluster',
68
+ provider: 'self-hosted',
69
+ endpoint: 'http://10.0.4.12:8000/v1',
70
+ maskedKey: 'tok-…77de',
71
+ status: 'failing',
72
+ lastUsed: '2 days ago',
73
+ },
74
+ ];
75
+
76
+ export const providerUsageLimits: ProviderUsageLimit[] = [
77
+ {
78
+ id: 'tokens',
79
+ label: 'Tokens this month',
80
+ value: 31_200_000,
81
+ max: 50_000_000,
82
+ valueLabel: '31.2M / 50M',
83
+ note: 'Resets on the 1st of each month.',
84
+ },
85
+ {
86
+ id: 'spend',
87
+ label: 'Spend this month',
88
+ value: 412,
89
+ max: 500,
90
+ valueLabel: '$412 / $500',
91
+ note: 'Requests are blocked when the cap is reached.',
92
+ },
93
+ { id: 'rate', label: 'Peak requests per minute', value: 540, max: 600, valueLabel: '540 / 600 rpm' },
94
+ ];
95
+
96
+ /** Masks a secret for display: provider prefix + last four characters. */
97
+ export function maskSecret(secret: string) {
98
+ const trimmed = secret.trim();
99
+ const prefix = trimmed.match(/^[a-z]+-/i)?.[0] ?? '';
100
+ return `${prefix}…${trimmed.slice(-4)}`;
101
+ }
@@ -0,0 +1,12 @@
1
+ import type { TemplateMeta } from '../meta';
2
+
3
+ export const meta: TemplateMeta = {
4
+ name: 'Model providers settings',
5
+ family: 'AI',
6
+ priority: 'P3',
7
+ status: 'experimental',
8
+ description:
9
+ 'Settings page (shared settings nav plus a "Model providers" item) with the ModelKeysList — masked keys, test connection, confirmed revoke — an add-key dialog (provider, endpoint for compatible / self-hosted, secret) and workspace usage limits as Meters.',
10
+ layout: 'SettingsLayout',
11
+ blocks: ['app-sidebar', 'app-topbar', 'page-header', 'model-keys-list'],
12
+ };
@@ -0,0 +1,49 @@
1
+ import { render, screen, within } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { expectNoAxeViolations } from '../test/a11y';
4
+ import { LinkProvider } from '@gntik-ai/ui';
5
+ import type { ComponentPropsWithRef } from 'react';
6
+ import NotificationInboxPage from './Page';
7
+
8
+ describe('NotificationInboxPage', () => {
9
+ it('renders the console, the inbox list and the empty detail', { timeout: 15000 }, async () => {
10
+ render(<NotificationInboxPage />);
11
+ expect(screen.getByRole('main')).toBeInTheDocument();
12
+ expect(screen.getByRole('heading', { level: 1, name: 'Notifications' })).toBeInTheDocument();
13
+ const list = screen.getByRole('list', { name: 'Notifications' });
14
+ expect(within(list).getAllByRole('button')).toHaveLength(6);
15
+ expect(screen.getByRole('heading', { name: 'Select a notification' })).toBeInTheDocument();
16
+ await expectNoAxeViolations();
17
+ });
18
+
19
+ it('opens a notification, marks it read and filters by unread', { timeout: 15000 }, async () => {
20
+ const onReadChange = vi.fn();
21
+ render(<NotificationInboxPage onReadChange={onReadChange} />);
22
+ const list = screen.getByRole('list', { name: 'Notifications' });
23
+ await userEvent.click(within(list).getByRole('button', { name: /Budget alert/ }));
24
+ expect(screen.getByRole('heading', { level: 2, name: 'Budget alert: api-gateway at 97%' })).toBeInTheDocument();
25
+ expect(onReadChange).toHaveBeenCalledWith('ntf_01', true);
26
+
27
+ await userEvent.click(screen.getByRole('button', { name: /^Unread/ }));
28
+ const unread = screen.getByRole('list', { name: 'Notifications' });
29
+ expect(within(unread).getAllByRole('button')).toHaveLength(2);
30
+
31
+ await userEvent.click(screen.getAllByRole('button', { name: 'Mark all as read' })[0]!);
32
+ expect(screen.getByRole('heading', { name: 'You’re all caught up' })).toBeInTheDocument();
33
+ });
34
+
35
+ it('Open goes through the LinkProvider router link', { timeout: 15000 }, async () => {
36
+ const RouterLink = ({ href, ...props }: ComponentPropsWithRef<'a'>) => <a data-router="" href={href} {...props} />;
37
+ render(
38
+ <LinkProvider component={RouterLink}>
39
+ <NotificationInboxPage defaultSelectedId="ntf_01" />
40
+ </LinkProvider>,
41
+ );
42
+ const list = screen.getByRole('list', { name: 'Notifications' });
43
+ await userEvent.click(within(list).getByRole('button', { name: /Budget alert/ }));
44
+ const open = screen.getByRole('button', { name: 'Open' });
45
+ expect(open.tagName).toBe('A');
46
+ expect(open).toHaveAttribute('data-router');
47
+ expect(open.getAttribute('href')).toBeTruthy();
48
+ });
49
+ });
@@ -0,0 +1,237 @@
1
+ import { PageHeader } from '@gntik-ai/blocks';
2
+ import { Archive, BellOff, CheckCheck, ExternalLink, MailOpen } from '@gntik-ai/icons';
3
+ import {
4
+ Badge,
5
+ Button,
6
+ EmptyState,
7
+ Link,
8
+ Page,
9
+ SplitLayout,
10
+ StatusDot,
11
+ Timestamp,
12
+ Toggle,
13
+ ToggleGroup,
14
+ cn,
15
+ type BreadcrumbItem,
16
+ useI18n,
17
+ } from '@gntik-ai/ui';
18
+ import { useId, useState } from 'react';
19
+ import { ConsoleShell, type ConsoleShellProps } from '../shared/ConsoleShell';
20
+ import { inboxBreadcrumbs, inboxCategoryLabels, inboxNotifications, type InboxNotification } from './data';
21
+
22
+ export type InboxFilter = 'all' | 'unread' | 'mention';
23
+
24
+ export interface NotificationInboxProps {
25
+ /** Initial notifications, newest first. The page keeps read/archived state locally. */
26
+ notifications: InboxNotification[];
27
+ defaultFilter: InboxFilter;
28
+ /** Initially selected notification (defaults to the first one). */
29
+ defaultSelectedId: string | null;
30
+ onReadChange: (id: string, read: boolean) => void;
31
+ onArchive: (id: string) => void;
32
+ onMarkAllRead: () => void;
33
+ breadcrumbs: BreadcrumbItem[];
34
+ currentHref: string;
35
+ /** ConsoleShell props (app nav, user, workspaces, sidebar footer, topbar…). */
36
+ shell: Omit<ConsoleShellProps, 'children'>;
37
+ }
38
+
39
+ const FILTERS: Array<{ value: InboxFilter; label: string }> = [
40
+ { value: 'all', label: 'All' },
41
+ { value: 'unread', label: 'Unread' },
42
+ { value: 'mention', label: 'Mentions' },
43
+ ];
44
+
45
+ const matches = (n: InboxNotification, filter: InboxFilter) =>
46
+ filter === 'all' ? true : filter === 'unread' ? !n.read : n.category === filter;
47
+
48
+ /** Notification inbox: ConsoleShell + Page + SplitLayout (filtered list → detail, Back below lg). */
49
+ export default function NotificationInboxPage(props: Partial<NotificationInboxProps>) {
50
+ const { t } = useI18n();
51
+ const {
52
+ notifications: initial = inboxNotifications,
53
+ defaultFilter = 'all',
54
+ defaultSelectedId,
55
+ onReadChange,
56
+ onArchive,
57
+ onMarkAllRead,
58
+ breadcrumbs = inboxBreadcrumbs,
59
+ currentHref = '/notifications',
60
+ shell,
61
+ } = props;
62
+ const [items, setItems] = useState(initial);
63
+ const [filter, setFilter] = useState<InboxFilter>(defaultFilter);
64
+ const [selectedId, setSelectedId] = useState<string | null>(defaultSelectedId ?? null);
65
+ const [showDetail, setShowDetail] = useState(false);
66
+ const titleId = useId();
67
+
68
+ const visible = items.filter((n) => matches(n, filter));
69
+ const selected = items.find((n) => n.id === selectedId) ?? null;
70
+ const unread = items.filter((n) => !n.read).length;
71
+
72
+ const setRead = (id: string, read: boolean) => {
73
+ setItems((list) => list.map((n) => (n.id === id ? { ...n, read } : n)));
74
+ onReadChange?.(id, read);
75
+ };
76
+ const open = (n: InboxNotification) => {
77
+ setSelectedId(n.id);
78
+ setShowDetail(true);
79
+ if (!n.read) setRead(n.id, true);
80
+ };
81
+ const archive = (id: string) => {
82
+ setItems((list) => list.filter((n) => n.id !== id));
83
+ setSelectedId(null);
84
+ setShowDetail(false);
85
+ onArchive?.(id);
86
+ };
87
+ const markAllRead = () => {
88
+ setItems((list) => list.map((n) => ({ ...n, read: true })));
89
+ onMarkAllRead?.();
90
+ };
91
+
92
+ const list =
93
+ visible.length === 0 ? (
94
+ <EmptyState
95
+ icon={BellOff}
96
+ size="sm"
97
+ className="mx-auto mt-8"
98
+ title={filter === 'unread' ? 'You’re all caught up' : 'Nothing here yet'}
99
+ description={filter === 'unread' ? t('notifications.empty') : 'Try another filter.'}
100
+ />
101
+ ) : (
102
+ <ul aria-label={t('common.notifications')} className="flex flex-col gap-0.5 p-2">
103
+ {visible.map((n) => (
104
+ <li key={n.id}>
105
+ <button
106
+ type="button"
107
+ aria-current={n.id === selectedId ? 'true' : undefined}
108
+ onClick={() => open(n)}
109
+ className="flex w-full items-start gap-3 rounded-lg px-3 py-2.5 text-start hover:bg-accent/55 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus-ring aria-[current=true]:bg-accent"
110
+ >
111
+ <span className="mt-1.5 flex w-2 shrink-0 justify-center">
112
+ {!n.read && <StatusDot tone={n.tone} aria-hidden />}
113
+ </span>
114
+ <span className="min-w-0 flex-1">
115
+ <span className="flex items-baseline gap-2">
116
+ <span
117
+ className={cn('min-w-0 flex-1 truncate text-[13px] text-foreground', !n.read && 'font-semibold')}
118
+ >
119
+ {n.title}
120
+ </span>
121
+ <Timestamp value={n.time} tooltip={false} className="shrink-0 font-mono text-[11px]" />
122
+ </span>
123
+ <span className="mt-0.5 block truncate text-[12px] text-muted-foreground">{n.preview}</span>
124
+ {!n.read && <span className="sr-only">Unread</span>}
125
+ </span>
126
+ </button>
127
+ </li>
128
+ ))}
129
+ </ul>
130
+ );
131
+
132
+ const detail = selected ? (
133
+ <article aria-labelledby={titleId} className="flex flex-col gap-5 p-5 sm:p-6">
134
+ <div className="flex flex-wrap items-center gap-2">
135
+ <Badge tone="neutral" variant="outline">
136
+ {inboxCategoryLabels[selected.category]}
137
+ </Badge>
138
+ <span className="text-[12.5px] text-muted-foreground">
139
+ From <span className="font-medium text-foreground">{selected.from}</span> ·{' '}
140
+ <Timestamp value={selected.time} format="absolute" className="font-mono text-[12px]" />
141
+ </span>
142
+ </div>
143
+ <h2 id={titleId} className="text-[18px] font-semibold tracking-tight text-foreground">
144
+ {selected.title}
145
+ </h2>
146
+ <div className="flex max-w-2xl flex-col gap-3 text-[13.5px] leading-6 text-foreground">
147
+ {selected.body.map((p) => (
148
+ <p key={p}>{p}</p>
149
+ ))}
150
+ </div>
151
+ <div className="flex flex-wrap items-center gap-2">
152
+ {selected.href && (
153
+ <Button
154
+ trailingIcon={ExternalLink}
155
+ render={<Link href={selected.href} tone="inherit" underline="none" />}
156
+ nativeButton={false}
157
+ >
158
+ Open
159
+ </Button>
160
+ )}
161
+ <Button variant="secondary" icon={MailOpen} onClick={() => setRead(selected.id, false)}>
162
+ Mark as unread
163
+ </Button>
164
+ <Button variant="ghost" icon={Archive} onClick={() => archive(selected.id)}>
165
+ Archive
166
+ </Button>
167
+ </div>
168
+ </article>
169
+ ) : (
170
+ <EmptyState
171
+ icon={MailOpen}
172
+ size="sm"
173
+ titleAs="h2"
174
+ className="mx-auto mt-16"
175
+ title="Select a notification"
176
+ description="Its details show up here."
177
+ />
178
+ );
179
+
180
+ return (
181
+ <ConsoleShell breadcrumbs={breadcrumbs} currentHref={currentHref} {...shell}>
182
+ <Page
183
+ width="full"
184
+ header={
185
+ <PageHeader
186
+ breadcrumbs={null}
187
+ title="Notifications"
188
+ description="Mentions, deployments, billing and access requests across your workspace."
189
+ status=""
190
+ meta={[]}
191
+ tabs={null}
192
+ actions={[
193
+ {
194
+ label: 'Mark all as read',
195
+ icon: CheckCheck,
196
+ variant: 'secondary',
197
+ disabled: unread === 0,
198
+ onClick: markAllRead,
199
+ },
200
+ ]}
201
+ />
202
+ }
203
+ >
204
+ <div className="h-[640px] min-h-0 overflow-hidden rounded-xl border border-border">
205
+ <SplitLayout
206
+ listLabel="Inbox"
207
+ detailLabel="Notification"
208
+ showDetail={showDetail}
209
+ onShowDetailChange={setShowDetail}
210
+ listHeader={
211
+ <ToggleGroup
212
+ aria-label="Filter notifications"
213
+ size="sm"
214
+ value={[filter]}
215
+ onValueChange={(v) => {
216
+ const next = v[0];
217
+ if (next === 'all' || next === 'unread' || next === 'mention') setFilter(next);
218
+ }}
219
+ >
220
+ {FILTERS.map((f) => (
221
+ <Toggle key={f.value} value={f.value}>
222
+ {f.label}
223
+ {f.value === 'unread' && unread > 0 && (
224
+ <span className="font-mono text-[10.5px] text-muted-foreground">{unread}</span>
225
+ )}
226
+ </Toggle>
227
+ ))}
228
+ </ToggleGroup>
229
+ }
230
+ list={list}
231
+ detail={detail}
232
+ />
233
+ </div>
234
+ </Page>
235
+ </ConsoleShell>
236
+ );
237
+ }