@twoux/ui 0.0.0-stage → 0.1.1

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 (325) hide show
  1. package/LICENSE.md +61 -0
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/NOTICE.md +64 -0
  4. package/README.md +211 -2
  5. package/SHADCN-DIFF.md +228 -0
  6. package/agent/AGENTS.md +358 -0
  7. package/agent/docs-md/accessibility.md +49 -0
  8. package/agent/docs-md/action-catalogue.md +81 -0
  9. package/agent/docs-md/actions.md +74 -0
  10. package/agent/docs-md/agent-activity-log.md +70 -0
  11. package/agent/docs-md/agent-permissions.md +82 -0
  12. package/agent/docs-md/agent-run.md +90 -0
  13. package/agent/docs-md/agents-on-behalf.md +74 -0
  14. package/agent/docs-md/ai-column.md +85 -0
  15. package/agent/docs-md/ai-origin-label.md +70 -0
  16. package/agent/docs-md/announcing-features.md +73 -0
  17. package/agent/docs-md/answer-actions.md +81 -0
  18. package/agent/docs-md/app-shell.md +114 -0
  19. package/agent/docs-md/artifact-card.md +89 -0
  20. package/agent/docs-md/ask-on-screen.md +72 -0
  21. package/agent/docs-md/assistant-page.md +100 -0
  22. package/agent/docs-md/assistant-panel.md +142 -0
  23. package/agent/docs-md/avatar.md +82 -0
  24. package/agent/docs-md/banner.md +106 -0
  25. package/agent/docs-md/board.md +163 -0
  26. package/agent/docs-md/breadcrumb.md +89 -0
  27. package/agent/docs-md/breakdown-table.md +97 -0
  28. package/agent/docs-md/bulk-action-bar.md +112 -0
  29. package/agent/docs-md/button.md +293 -0
  30. package/agent/docs-md/card.md +115 -0
  31. package/agent/docs-md/change-list.md +99 -0
  32. package/agent/docs-md/change-request.md +166 -0
  33. package/agent/docs-md/chart-area.md +87 -0
  34. package/agent/docs-md/chart-bar.md +100 -0
  35. package/agent/docs-md/chart-box-plot.md +88 -0
  36. package/agent/docs-md/chart-bullet.md +89 -0
  37. package/agent/docs-md/chart-candlestick.md +112 -0
  38. package/agent/docs-md/chart-card-line.md +120 -0
  39. package/agent/docs-md/chart-donut.md +90 -0
  40. package/agent/docs-md/chart-drill-down.md +72 -0
  41. package/agent/docs-md/chart-funnel.md +71 -0
  42. package/agent/docs-md/chart-heatmap.md +76 -0
  43. package/agent/docs-md/chart-histogram.md +81 -0
  44. package/agent/docs-md/chart-map.md +165 -0
  45. package/agent/docs-md/chart-mixed.md +88 -0
  46. package/agent/docs-md/chart-price.md +92 -0
  47. package/agent/docs-md/chart-radial-bars.md +109 -0
  48. package/agent/docs-md/chart-radial-lines.md +111 -0
  49. package/agent/docs-md/chart-sankey.md +104 -0
  50. package/agent/docs-md/chart-scatter.md +87 -0
  51. package/agent/docs-md/chart-share-bar.md +89 -0
  52. package/agent/docs-md/chart-treemap.md +98 -0
  53. package/agent/docs-md/chart-waterfall.md +93 -0
  54. package/agent/docs-md/checkbox.md +131 -0
  55. package/agent/docs-md/choosing-selection-control.md +71 -0
  56. package/agent/docs-md/citations-sources.md +79 -0
  57. package/agent/docs-md/citing-sources.md +63 -0
  58. package/agent/docs-md/clarify-before-acting.md +69 -0
  59. package/agent/docs-md/clarifying-question.md +92 -0
  60. package/agent/docs-md/code-block.md +174 -0
  61. package/agent/docs-md/colours.md +87 -0
  62. package/agent/docs-md/columns-menu.md +140 -0
  63. package/agent/docs-md/combobox.md +161 -0
  64. package/agent/docs-md/command-palette.md +126 -0
  65. package/agent/docs-md/confidence-note.md +87 -0
  66. package/agent/docs-md/configurable-dashboard.md +76 -0
  67. package/agent/docs-md/confirmation-dialog.md +93 -0
  68. package/agent/docs-md/connected-tools.md +80 -0
  69. package/agent/docs-md/connector-picker.md +127 -0
  70. package/agent/docs-md/consistency.md +53 -0
  71. package/agent/docs-md/content-density.md +61 -0
  72. package/agent/docs-md/conversation-history.md +78 -0
  73. package/agent/docs-md/create-record.md +80 -0
  74. package/agent/docs-md/csv-import.md +88 -0
  75. package/agent/docs-md/dashboard-items.md +78 -0
  76. package/agent/docs-md/data-export.md +61 -0
  77. package/agent/docs-md/data-grid.md +137 -0
  78. package/agent/docs-md/data-vis-colours.md +76 -0
  79. package/agent/docs-md/data-visualisation.md +76 -0
  80. package/agent/docs-md/date-picker.md +178 -0
  81. package/agent/docs-md/delete-record.md +75 -0
  82. package/agent/docs-md/dense-table.md +495 -0
  83. package/agent/docs-md/design-tokens.md +53 -0
  84. package/agent/docs-md/disabled-read-only.md +75 -0
  85. package/agent/docs-md/drag-and-drop.md +74 -0
  86. package/agent/docs-md/dropdown-menu.md +137 -0
  87. package/agent/docs-md/edit-record.md +73 -0
  88. package/agent/docs-md/empty-loading-error.md +78 -0
  89. package/agent/docs-md/empty-state.md +128 -0
  90. package/agent/docs-md/error-messages.md +75 -0
  91. package/agent/docs-md/error-state.md +123 -0
  92. package/agent/docs-md/export-menu.md +86 -0
  93. package/agent/docs-md/feedback-prompt.md +88 -0
  94. package/agent/docs-md/field-mapping.md +78 -0
  95. package/agent/docs-md/file-drop.md +110 -0
  96. package/agent/docs-md/filter-bar.md +138 -0
  97. package/agent/docs-md/filter-panel.md +103 -0
  98. package/agent/docs-md/filtering-search.md +79 -0
  99. package/agent/docs-md/follow-ups-starters.md +79 -0
  100. package/agent/docs-md/form-validation.md +95 -0
  101. package/agent/docs-md/generated-columns.md +65 -0
  102. package/agent/docs-md/generated-drafts-files.md +71 -0
  103. package/agent/docs-md/grouped-list.md +89 -0
  104. package/agent/docs-md/guide-cue.md +170 -0
  105. package/agent/docs-md/help-panel.md +103 -0
  106. package/agent/docs-md/help-system.md +74 -0
  107. package/agent/docs-md/iconography.md +49 -0
  108. package/agent/docs-md/inbox.md +108 -0
  109. package/agent/docs-md/inline-editing-validation.md +80 -0
  110. package/agent/docs-md/inline-input-in-answer.md +70 -0
  111. package/agent/docs-md/items-palette.md +116 -0
  112. package/agent/docs-md/keyboard-model.md +47 -0
  113. package/agent/docs-md/keyboard-navigation.md +68 -0
  114. package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
  115. package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
  116. package/agent/docs-md/labelling-ai-output.md +66 -0
  117. package/agent/docs-md/layout.md +47 -0
  118. package/agent/docs-md/list-table.md +254 -0
  119. package/agent/docs-md/loading-skeleton.md +91 -0
  120. package/agent/docs-md/managing-agents.md +71 -0
  121. package/agent/docs-md/message-composer.md +105 -0
  122. package/agent/docs-md/migrating.md +49 -0
  123. package/agent/docs-md/motion.md +93 -0
  124. package/agent/docs-md/navigation.md +76 -0
  125. package/agent/docs-md/notification-channels.md +65 -0
  126. package/agent/docs-md/notifications-decision.md +100 -0
  127. package/agent/docs-md/onboarding-step.md +114 -0
  128. package/agent/docs-md/order-book.md +108 -0
  129. package/agent/docs-md/page-header.md +96 -0
  130. package/agent/docs-md/pagination.md +88 -0
  131. package/agent/docs-md/pane-layout.md +105 -0
  132. package/agent/docs-md/period-comparison.md +66 -0
  133. package/agent/docs-md/plan-card.md +120 -0
  134. package/agent/docs-md/popover-rules.md +88 -0
  135. package/agent/docs-md/prompt-editor.md +90 -0
  136. package/agent/docs-md/propose-apply.md +65 -0
  137. package/agent/docs-md/provider-logo.md +97 -0
  138. package/agent/docs-md/radio.md +83 -0
  139. package/agent/docs-md/ranked-list.md +102 -0
  140. package/agent/docs-md/reasoning-progress.md +72 -0
  141. package/agent/docs-md/record-detail.md +71 -0
  142. package/agent/docs-md/regenerating-answers.md +70 -0
  143. package/agent/docs-md/run-status-bar.md +94 -0
  144. package/agent/docs-md/saved-tasks-schedules.md +63 -0
  145. package/agent/docs-md/saved-views.md +149 -0
  146. package/agent/docs-md/saving-views.md +68 -0
  147. package/agent/docs-md/scheduler.md +258 -0
  148. package/agent/docs-md/secondary-panels.md +72 -0
  149. package/agent/docs-md/segmented-control.md +88 -0
  150. package/agent/docs-md/selection-bulk-actions.md +70 -0
  151. package/agent/docs-md/separator.md +75 -0
  152. package/agent/docs-md/setup-checklist.md +92 -0
  153. package/agent/docs-md/shape-elevation.md +55 -0
  154. package/agent/docs-md/sheet-grid.md +211 -0
  155. package/agent/docs-md/side-panel.md +134 -0
  156. package/agent/docs-md/sign-in.md +114 -0
  157. package/agent/docs-md/spacing.md +55 -0
  158. package/agent/docs-md/split-button.md +100 -0
  159. package/agent/docs-md/stat-compare.md +82 -0
  160. package/agent/docs-md/static-dashboard.md +75 -0
  161. package/agent/docs-md/status-badge.md +140 -0
  162. package/agent/docs-md/step-card.md +75 -0
  163. package/agent/docs-md/stepper.md +92 -0
  164. package/agent/docs-md/suggested-changes.md +91 -0
  165. package/agent/docs-md/switch.md +78 -0
  166. package/agent/docs-md/table-settings.md +81 -0
  167. package/agent/docs-md/tabs.md +85 -0
  168. package/agent/docs-md/tag-input.md +108 -0
  169. package/agent/docs-md/tag.md +79 -0
  170. package/agent/docs-md/task-builder.md +87 -0
  171. package/agent/docs-md/team-instructions.md +72 -0
  172. package/agent/docs-md/templates-gallery.md +85 -0
  173. package/agent/docs-md/text-input.md +160 -0
  174. package/agent/docs-md/thinking-steps.md +89 -0
  175. package/agent/docs-md/timeline.md +189 -0
  176. package/agent/docs-md/timestamp.md +84 -0
  177. package/agent/docs-md/timestamps.md +70 -0
  178. package/agent/docs-md/toast.md +117 -0
  179. package/agent/docs-md/tooltip.md +90 -0
  180. package/agent/docs-md/typography.md +62 -0
  181. package/agent/docs-md/undo-destructive.md +66 -0
  182. package/agent/docs-md/unsaved-changes.md +71 -0
  183. package/agent/docs-md/usage-meter.md +80 -0
  184. package/agent/docs-md/user-feedback.md +71 -0
  185. package/agent/docs-md/voice-and-tone.md +60 -0
  186. package/agent/docs-md/why-choice.md +93 -0
  187. package/agent/docs-md/workspace-onboarding.md +74 -0
  188. package/agent/docs-md/writing.md +48 -0
  189. package/agent/llms-full.txt +18369 -0
  190. package/agent/llms.txt +198 -0
  191. package/agent/skills/twoux-audit/SKILL.md +49 -0
  192. package/agent/skills/twoux-build/SKILL.md +39 -0
  193. package/agent/skills/twoux-design/SKILL.md +54 -0
  194. package/agent/skills/twoux-write/SKILL.md +32 -0
  195. package/bin/check.mjs +123 -0
  196. package/bin/docs.mjs +24 -0
  197. package/bin/guard.mjs +27 -0
  198. package/bin/init.mjs +113 -0
  199. package/bin/knowledge.mjs +87 -0
  200. package/bin/mcp.mjs +116 -0
  201. package/bin/theme.mjs +168 -0
  202. package/bin/twoux.mjs +25 -0
  203. package/package.json +80 -3
  204. package/registry/jobs.json +1028 -0
  205. package/registry/palette.json +770 -0
  206. package/registry/rules.json +913 -0
  207. package/src/components/agent-review.tsx +364 -0
  208. package/src/components/ai-basics.tsx +212 -0
  209. package/src/components/ai-work.tsx +304 -0
  210. package/src/components/ai.tsx +227 -0
  211. package/src/components/alert-dialog.tsx +203 -0
  212. package/src/components/alert.tsx +86 -0
  213. package/src/components/avatar.tsx +114 -0
  214. package/src/components/badge.tsx +59 -0
  215. package/src/components/board.tsx +397 -0
  216. package/src/components/breadcrumb.tsx +114 -0
  217. package/src/components/button-group.tsx +90 -0
  218. package/src/components/button.tsx +109 -0
  219. package/src/components/calendar.tsx +247 -0
  220. package/src/components/card.tsx +102 -0
  221. package/src/components/chart-map.tsx +336 -0
  222. package/src/components/charts-more.tsx +1331 -0
  223. package/src/components/charts-radial.tsx +328 -0
  224. package/src/components/charts.tsx +390 -0
  225. package/src/components/checkbox.tsx +40 -0
  226. package/src/components/code.tsx +139 -0
  227. package/src/components/collapsible.tsx +37 -0
  228. package/src/components/combobox.tsx +339 -0
  229. package/src/components/command-extras.tsx +11 -0
  230. package/src/components/command.tsx +196 -0
  231. package/src/components/data-grid.tsx +213 -0
  232. package/src/components/data-table.tsx +334 -0
  233. package/src/components/data-views.tsx +740 -0
  234. package/src/components/date-picker.tsx +686 -0
  235. package/src/components/dense-table.tsx +207 -0
  236. package/src/components/dialog.tsx +162 -0
  237. package/src/components/dropdown-menu.tsx +264 -0
  238. package/src/components/empty.tsx +114 -0
  239. package/src/components/feedback-prompt.tsx +80 -0
  240. package/src/components/field.tsx +254 -0
  241. package/src/components/fields.tsx +112 -0
  242. package/src/components/flow.tsx +840 -0
  243. package/src/components/guide-cue.tsx +273 -0
  244. package/src/components/inbox.tsx +113 -0
  245. package/src/components/input-group.tsx +176 -0
  246. package/src/components/input.tsx +26 -0
  247. package/src/components/kbd.tsx +34 -0
  248. package/src/components/label.tsx +27 -0
  249. package/src/components/labels.tsx +176 -0
  250. package/src/components/motion.tsx +136 -0
  251. package/src/components/onboarding.tsx +454 -0
  252. package/src/components/order-book.tsx +185 -0
  253. package/src/components/pane-layout.tsx +417 -0
  254. package/src/components/popover.tsx +96 -0
  255. package/src/components/provider-logos.data.ts +49 -0
  256. package/src/components/radio-group.tsx +52 -0
  257. package/src/components/scheduler.tsx +997 -0
  258. package/src/components/segmented-control.tsx +28 -0
  259. package/src/components/select.tsx +195 -0
  260. package/src/components/separator.tsx +31 -0
  261. package/src/components/sheet.tsx +148 -0
  262. package/src/components/shell.tsx +426 -0
  263. package/src/components/shortcut-sheet.tsx +83 -0
  264. package/src/components/skeleton.tsx +18 -0
  265. package/src/components/sonner.tsx +107 -0
  266. package/src/components/spinner.tsx +13 -0
  267. package/src/components/split-button.tsx +58 -0
  268. package/src/components/stepper.tsx +78 -0
  269. package/src/components/switch.tsx +43 -0
  270. package/src/components/table.tsx +126 -0
  271. package/src/components/tabs.tsx +118 -0
  272. package/src/components/tag-input.tsx +253 -0
  273. package/src/components/textarea.tsx +21 -0
  274. package/src/components/theme.tsx +80 -0
  275. package/src/components/timeline.tsx +408 -0
  276. package/src/components/toggle-group.tsx +87 -0
  277. package/src/components/toggle.tsx +54 -0
  278. package/src/components/tooltip.tsx +65 -0
  279. package/src/components/workspace-actions.tsx +294 -0
  280. package/src/components/workspace-import.tsx +465 -0
  281. package/src/components/workspace.tsx +1884 -0
  282. package/src/index.ts +80 -0
  283. package/src/lib/active-indicator.tsx +53 -0
  284. package/src/lib/chart-kit.tsx +72 -0
  285. package/src/lib/cn.ts +6 -0
  286. package/src/lib/shortcuts.tsx +61 -0
  287. package/src/lib/workspace.tsx +10 -0
  288. package/src/shadcn/alert-dialog.tsx +196 -0
  289. package/src/shadcn/alert.tsx +65 -0
  290. package/src/shadcn/avatar.tsx +108 -0
  291. package/src/shadcn/badge.tsx +47 -0
  292. package/src/shadcn/breadcrumb.tsx +108 -0
  293. package/src/shadcn/button-group.tsx +83 -0
  294. package/src/shadcn/button.tsx +63 -0
  295. package/src/shadcn/calendar.tsx +220 -0
  296. package/src/shadcn/card.tsx +91 -0
  297. package/src/shadcn/checkbox.tsx +31 -0
  298. package/src/shadcn/collapsible.tsx +33 -0
  299. package/src/shadcn/command.tsx +184 -0
  300. package/src/shadcn/dialog.tsx +158 -0
  301. package/src/shadcn/dropdown-menu.tsx +256 -0
  302. package/src/shadcn/empty.tsx +103 -0
  303. package/src/shadcn/field.tsx +248 -0
  304. package/src/shadcn/input-group.tsx +170 -0
  305. package/src/shadcn/input.tsx +20 -0
  306. package/src/shadcn/kbd.tsx +28 -0
  307. package/src/shadcn/label.tsx +23 -0
  308. package/src/shadcn/popover.tsx +88 -0
  309. package/src/shadcn/radio-group.tsx +44 -0
  310. package/src/shadcn/select.tsx +189 -0
  311. package/src/shadcn/separator.tsx +27 -0
  312. package/src/shadcn/sheet.tsx +142 -0
  313. package/src/shadcn/skeleton.tsx +13 -0
  314. package/src/shadcn/sonner.tsx +40 -0
  315. package/src/shadcn/switch.tsx +34 -0
  316. package/src/shadcn/table.tsx +115 -0
  317. package/src/shadcn/tabs.tsx +90 -0
  318. package/src/shadcn/textarea.tsx +17 -0
  319. package/src/shadcn/toggle-group.tsx +83 -0
  320. package/src/shadcn/toggle.tsx +46 -0
  321. package/src/shadcn/tooltip.tsx +56 -0
  322. package/src/styles/palette.css +127 -0
  323. package/src/styles/theme.css +35 -0
  324. package/src/styles/twoux.css +236 -0
  325. package/theme.json +14 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * TwoUX compositions for agents, saved tasks and the assistant's set-up.
3
+ * Every permission is Off, Ask first or Allow; candidate decisions are never Allow (UK GDPR Art. 22).
4
+ * Shared context (team instructions, connected tools) is visible and editable, never hidden in a prompt.
5
+ */
6
+ import { Check, CircleAlert, CircleDashed, Clock, ExternalLink, FileText, Loader2, Pause, Plug, RefreshCw, Search, X } from "lucide-react"
7
+ import * as React from "react"
8
+ import { cn } from "../lib/cn"
9
+ import { AiMark, AiOrigin } from "./ai-basics"
10
+ import { Badge } from "./badge"
11
+ import { Button } from "./button"
12
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./dialog"
13
+ import { Input } from "./input"
14
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover"
15
+ import { Segment, SegmentedControl } from "./segmented-control"
16
+ import { Textarea } from "./textarea"
17
+
18
+ /* ---------------------------------------------------------------- AI column */
19
+
20
+ export function AiColumnHeader({ name, written, onRefresh }: { name: string; written: string; onRefresh?: () => void }) {
21
+ return (
22
+ <span className="inline-flex items-center gap-control">
23
+ <AiMark />
24
+ <span>{name}</span>
25
+ <span className="font-normal text-ink-3">· {written}</span>
26
+ {onRefresh && <Button variant="ghost" size="icon-xs" aria-label={`Refresh ${name}`} onClick={onRefresh}><RefreshCw /></Button>}
27
+ </span>
28
+ )
29
+ }
30
+
31
+ export type AiCellState = { state: "writing" } | { state: "done"; value: string; source?: string } | { state: "failed"; reason: string } | { state: "queued" }
32
+
33
+ /** One generated cell: writing, done (with its citation), or failed with why and Try again. */
34
+ export function AiCell({ cell, onRetry, onOpenSource }: { cell: AiCellState; onRetry?: () => void; onOpenSource?: (id: string) => void }) {
35
+ if (cell.state === "queued") return <span className="text-sm text-ink-3">Waiting</span>
36
+ if (cell.state === "writing") return <span role="status" className="inline-flex items-center gap-control text-sm text-ink-3"><Loader2 className="size-3 animate-spin" aria-hidden="true" />Writing</span>
37
+ if (cell.state === "failed")
38
+ return (
39
+ <span className="inline-flex items-center gap-control text-sm text-bad">
40
+ <CircleAlert className="size-3.5 shrink-0" aria-hidden="true" />
41
+ <span className="truncate" title={cell.reason}>{cell.reason}</span>
42
+ {onRetry && <Button variant="ghost" size="xs" onClick={onRetry}>Try again</Button>}
43
+ </span>
44
+ )
45
+ return (
46
+ <span className="inline-flex min-w-0 items-center gap-control">
47
+ <span className="truncate">{cell.value}</span>
48
+ {cell.source && <button type="button" onClick={() => onOpenSource?.(cell.source!)} className="shrink-0 rounded-xs font-mono text-xs text-ink-3 hover:text-primary-ink">{cell.source}</button>}
49
+ </span>
50
+ )
51
+ }
52
+
53
+ /* ---------------------------------------------------------------- task builder */
54
+
55
+ export type TaskSlot = { id: string; placeholder: string; options: string[]; required?: boolean }
56
+
57
+ /** A repeated task written as a sentence with slots. Empty required slots are dashed and block Run. */
58
+ export function TaskBuilder({ parts, value, onChange, onRun, onSave, onSchedule }: { parts: (string | TaskSlot)[]; value: Record<string, string>; onChange: (v: Record<string, string>) => void; onRun: () => void; onSave?: () => void; onSchedule?: () => void }) {
59
+ const slots = parts.filter((p): p is TaskSlot => typeof p !== "string")
60
+ const missing = slots.find((s) => s.required && !value[s.id])
61
+ const [tried, setTried] = React.useState(false)
62
+ const run = () => { setTried(true); if (!missing) onRun() }
63
+ return (
64
+ <section aria-label="Task" className="grid gap-3 rounded-md border border-line bg-surface p-3" onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); run() } }}>
65
+ <p className="text-md leading-8 text-ink">
66
+ {parts.map((p, i) =>
67
+ typeof p === "string" ? (
68
+ <span key={i}>{p}</span>
69
+ ) : (
70
+ <Popover key={p.id}>
71
+ <PopoverTrigger asChild>
72
+ <button type="button" aria-label={`${p.placeholder}${p.required ? " (required)" : ""}: ${value[p.id] ?? "not set"}`} className={cn("mx-1 inline-flex h-7 items-center rounded-sm border px-2 align-baseline text-base transition-colors", value[p.id] ? "border-line-strong bg-surface-2 font-medium text-ink" : "border-dashed border-line-strong text-ink-3 hover:border-input-hover", tried && missing?.id === p.id && "border-destructive-edge")}>
73
+ {value[p.id] ?? p.placeholder}
74
+ </button>
75
+ </PopoverTrigger>
76
+ <PopoverContent align="start" className="w-56 p-1">
77
+ {p.options.map((o) => <button key={o} type="button" onClick={() => onChange({ ...value, [p.id]: o })} className={cn("flex w-full items-center justify-between rounded-xs px-2 py-2 text-left text-base hover:bg-accent", value[p.id] === o && "font-medium")}>{o}{value[p.id] === o && <Check className="size-3.5" aria-hidden="true" />}</button>)}
78
+ </PopoverContent>
79
+ </Popover>
80
+ ),
81
+ )}
82
+ </p>
83
+ {tried && missing && <p role="alert" className="flex items-center gap-control text-sm text-bad"><CircleAlert className="size-3.5" aria-hidden="true" />Choose {missing.placeholder.toLowerCase()} before running.</p>}
84
+ <div className="flex justify-end gap-2">
85
+ {onSchedule && <Button variant="ghost" size="sm" onClick={onSchedule}><Clock />Schedule</Button>}
86
+ {onSave && <Button variant="outline" size="sm" onClick={onSave}>Save as task</Button>}
87
+ <Button size="sm" shortcut="⌘↵" onClick={run}>Run</Button>
88
+ </div>
89
+ </section>
90
+ )
91
+ }
92
+
93
+ /* ---------------------------------------------------------------- permissions */
94
+
95
+ export type PermissionLevel = "off" | "ask" | "allow"
96
+ export type Permission = { id: string; action: string; covers: string; level: PermissionLevel; group: "Read" | "Change"; neverAllow?: boolean }
97
+
98
+ export function AgentPermissions({ items, onChange }: { items: Permission[]; onChange: (id: string, level: PermissionLevel) => void }) {
99
+ return (
100
+ <div className="grid gap-4">
101
+ {(["Read", "Change"] as const).map((g) => (
102
+ <section key={g} aria-label={g === "Read" ? "What the assistant can read" : "What the assistant can change"} className="grid gap-1">
103
+ <h3 className="text-sm font-medium text-ink-2">{g === "Read" ? "Read" : "Change"}</h3>
104
+ <ul className="divide-y divide-line rounded-sm border border-line bg-surface">
105
+ {items.filter((p) => p.group === g).map((p) => (
106
+ <li key={p.id} className="flex flex-wrap items-center gap-3 px-3 py-2">
107
+ <div className="grid min-w-0 flex-1 gap-0.5">
108
+ <span className="text-base font-medium text-ink">{p.action}</span>
109
+ <span className="text-sm text-ink-2">{p.covers}{p.neverAllow && " Always asks first: decisions about people are never automatic."}</span>
110
+ </div>
111
+ <SegmentedControl value={p.level} onValueChange={(v) => onChange(p.id, v as PermissionLevel)} size="sm" aria-label={`${p.action}: access level`}>
112
+ <Segment value="off">Off</Segment>
113
+ <Segment value="ask">Ask first</Segment>
114
+ <Segment value="allow" disabled={p.neverAllow}>Allow</Segment>
115
+ </SegmentedControl>
116
+ </li>
117
+ ))}
118
+ </ul>
119
+ </section>
120
+ ))}
121
+ </div>
122
+ )
123
+ }
124
+
125
+ /* ---------------------------------------------------------------- agent run */
126
+
127
+ export type RunStep = { title: string; detail?: string; state: "done" | "running" | "waiting" | "skipped" | "failed"; action?: React.ReactNode }
128
+ const RUN_ICON = { done: <Check className="size-3.5 text-ok" />, running: <Loader2 className="size-3.5 animate-spin text-ink-3" />, waiting: <Pause className="size-3.5 text-warn" />, skipped: <CircleDashed className="size-3.5 text-ink-3" />, failed: <X className="size-3.5 text-bad" /> }
129
+ const RUN_WORD = { done: "Done", running: "Running", waiting: "Waiting for you", skipped: "Skipped", failed: "Failed" }
130
+
131
+ export function AgentRun({ task, status, steps, meta, onStop }: { task: string; status: "Running" | "Needs you" | "Done" | "Failed" | "Stopped"; steps: RunStep[]; meta: string; onStop?: () => void }) {
132
+ return (
133
+ <section aria-label={`Run: ${task}`} className="grid rounded-sm border border-line bg-surface">
134
+ <header className="flex items-center gap-2 border-b border-line px-3 py-2">
135
+ <h3 className="min-w-0 flex-1 truncate text-base font-semibold text-ink">{task}</h3>
136
+ <Badge variant={status === "Done" ? "success" : status === "Needs you" ? "warning" : status === "Failed" ? "destructive" : "secondary"} dot>{status}</Badge>
137
+ {onStop && status === "Running" && <Button variant="ghost" size="sm" onClick={onStop}>Stop</Button>}
138
+ </header>
139
+ <ol className="grid">
140
+ {steps.map((s, i) => (
141
+ <li key={i} className={cn("flex items-start gap-2 px-3 py-2", s.state === "waiting" && "bg-warn-bg/60")}>
142
+ <span className="mt-0.5" aria-hidden="true">{RUN_ICON[s.state]}</span>
143
+ <span className="grid min-w-0 flex-1 gap-0.5"><span className="text-base text-ink">{s.title}<span className="sr-only"> ({RUN_WORD[s.state]})</span></span>{s.detail && <span className="text-sm text-ink-2">{s.detail}</span>}</span>
144
+ {s.action}
145
+ </li>
146
+ ))}
147
+ </ol>
148
+ <footer className="border-t border-line px-3 py-2 text-sm text-ink-3">{meta}</footer>
149
+ </section>
150
+ )
151
+ }
152
+
153
+ /* ---------------------------------------------------------------- tools, instructions, history, templates */
154
+
155
+ export type Tool = { id: string; name: string; kind: string; connected: boolean; canRead: string; canChange: string; stored: string; connectedBy?: string; privacyHref?: string }
156
+
157
+ export function ConnectedToolCard({ tool, onConnect, onDisconnect }: { tool: Tool; onConnect?: () => void; onDisconnect?: () => void }) {
158
+ return (
159
+ <article aria-label={tool.name} className="grid gap-3 rounded-sm border border-line bg-surface p-3">
160
+ <header className="flex items-center gap-2">
161
+ <span className="grid size-8 place-items-center rounded-sm bg-surface-2 text-ink-2"><Plug className="size-4" aria-hidden="true" /></span>
162
+ <span className="grid min-w-0 flex-1"><span className="text-base font-semibold text-ink">{tool.name}</span><span className="text-sm text-ink-3">{tool.kind}</span></span>
163
+ <Badge variant={tool.connected ? "success" : "secondary"} dot>{tool.connected ? "Connected" : "Not connected"}</Badge>
164
+ </header>
165
+ <dl className="grid grid-cols-[110px_1fr] gap-x-3 gap-y-2 text-sm">
166
+ <dt className="text-ink-2">Can read</dt><dd className="text-ink">{tool.canRead}</dd>
167
+ <dt className="text-ink-2">Can change</dt><dd className="text-ink">{tool.canChange}</dd>
168
+ <dt className="text-ink-2">Data stored</dt><dd className="text-ink">{tool.stored}</dd>
169
+ {tool.connectedBy && <><dt className="text-ink-2">Connected by</dt><dd className="text-ink">{tool.connectedBy}</dd></>}
170
+ </dl>
171
+ <footer className="flex items-center gap-2">
172
+ {tool.privacyHref && <a href={tool.privacyHref} className="inline-flex items-center gap-1 text-sm font-medium text-primary-ink hover:underline">Privacy policy<ExternalLink className="size-3" aria-hidden="true" /></a>}
173
+ <span className="flex-1" />
174
+ {tool.connected ? <Button variant="outline" size="sm" onClick={onDisconnect}>Disconnect</Button> : <Button size="sm" onClick={onConnect}>Connect</Button>}
175
+ </footer>
176
+ </article>
177
+ )
178
+ }
179
+
180
+ export function TeamInstructions({ text, setBy, files, onSave, onAddFile }: { text: string; setBy: string; files: { name: string; size: string }[]; onSave: (text: string) => void; onAddFile?: () => void }) {
181
+ const [editing, setEditing] = React.useState(false)
182
+ const [draft, setDraft] = React.useState(text)
183
+ return (
184
+ <div className="grid gap-3">
185
+ <section aria-label="Team instructions" className="grid gap-2 rounded-sm border border-line bg-surface p-3">
186
+ <header className="flex items-center gap-2"><h3 className="flex-1 text-base font-semibold text-ink">Instructions</h3>{!editing && <Button variant="ghost" size="sm" onClick={() => { setDraft(text); setEditing(true) }}>Edit</Button>}</header>
187
+ {editing ? (
188
+ <>
189
+ <Textarea value={draft} onChange={(e) => setDraft(e.target.value)} aria-label="Team instructions" className="min-h-28" />
190
+ <div className="flex justify-end gap-2"><Button variant="outline" size="sm" onClick={() => setEditing(false)}>Cancel</Button><Button size="sm" onClick={() => { onSave(draft); setEditing(false) }}>Save</Button></div>
191
+ </>
192
+ ) : (
193
+ <p className="text-base whitespace-pre-line text-ink">{text}</p>
194
+ )}
195
+ <p className="text-sm text-ink-3">Set by {setBy} · Applies to everyone in this workspace</p>
196
+ </section>
197
+ <section aria-label="Team files" className="grid gap-2 rounded-sm border border-line bg-surface p-3">
198
+ <header className="flex items-center gap-2"><h3 className="flex-1 text-base font-semibold text-ink">Files</h3>{onAddFile && <Button variant="ghost" size="sm" onClick={onAddFile}>Add a file</Button>}</header>
199
+ <ul className="grid gap-1">{files.map((f) => <li key={f.name} className="flex items-center gap-2 text-base"><FileText className="size-3.5 text-ink-3" aria-hidden="true" /><span className="flex-1 text-ink">{f.name}</span><span className="tabular text-sm text-ink-3">{f.size}</span></li>)}</ul>
200
+ </section>
201
+ <p className="text-sm text-ink-3">The assistant reads these for every answer. Don't add personal information about candidates.</p>
202
+ </div>
203
+ )
204
+ }
205
+
206
+ export type Conversation = { id: string; title: string; date: string; status?: "new" | "needs-you" }
207
+
208
+ export function ConversationHistory({ scheduled, recent, onOpen }: { scheduled: Conversation[]; recent: Conversation[]; onOpen: (id: string) => void }) {
209
+ const [q, setQ] = React.useState("")
210
+ const f = (l: Conversation[]) => l.filter((c) => c.title.toLowerCase().includes(q.toLowerCase()))
211
+ const row = (c: Conversation) => (
212
+ <li key={c.id}>
213
+ <button type="button" onClick={() => onOpen(c.id)} className="flex w-full items-center gap-2 rounded-xs px-2 py-2 text-left hover:bg-hover">
214
+ {c.status === "new" && <span className="size-1.5 shrink-0 rounded-full bg-primary" aria-label="New answer" />}
215
+ <span className="min-w-0 flex-1 truncate text-base text-ink">{c.title}</span>
216
+ {c.status === "needs-you" && <Badge variant="warning" dot>Needs you</Badge>}
217
+ <span className="tabular shrink-0 text-sm text-ink-3">{c.date}</span>
218
+ </button>
219
+ </li>
220
+ )
221
+ return (
222
+ <div className="grid gap-3">
223
+ <div className="relative"><Search className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-ink-3" aria-hidden="true" /><Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search conversations" aria-label="Search conversations" className="pl-8" /></div>
224
+ {f(scheduled).length > 0 && <section className="grid gap-0.5"><h3 className="px-2 text-sm font-medium text-ink-3">Scheduled tasks</h3><ul>{f(scheduled).map(row)}</ul></section>}
225
+ <section className="grid gap-0.5"><h3 className="px-2 text-sm font-medium text-ink-3">Recent</h3>{f(recent).length ? <ul>{f(recent).map(row)}</ul> : <p className="px-2 text-base text-ink-2">No conversations match.</p>}</section>
226
+ </div>
227
+ )
228
+ }
229
+
230
+ export type Template = { id: string; name: string; does: string; tools: string[]; category: string }
231
+
232
+ /** Picking a template opens it as a draft task; it never runs. */
233
+ export function TemplatesGallery({ open, onOpenChange, templates, onPick }: { open: boolean; onOpenChange: (o: boolean) => void; templates: Template[]; onPick: (t: Template) => void }) {
234
+ const cats = ["All", ...new Set(templates.map((t) => t.category))]
235
+ const [cat, setCat] = React.useState("All")
236
+ const [q, setQ] = React.useState("")
237
+ const shown = templates.filter((t) => (cat === "All" || t.category === cat) && `${t.name} ${t.does}`.toLowerCase().includes(q.toLowerCase()))
238
+ return (
239
+ <Dialog open={open} onOpenChange={onOpenChange}>
240
+ <DialogContent className="gap-0 p-0 sm:max-w-3xl">
241
+ <DialogHeader className="border-b border-line p-4"><DialogTitle>Templates</DialogTitle><DialogDescription>Start from a template. It opens as a draft you can change before it runs.</DialogDescription></DialogHeader>
242
+ <div className="grid min-h-80 grid-cols-[180px_1fr] max-sm:grid-cols-1">
243
+ <nav aria-label="Categories" className="grid content-start gap-0.5 border-r border-line p-2">
244
+ {cats.map((c) => <button key={c} type="button" aria-current={cat === c || undefined} onClick={() => setCat(c)} className={cn("rounded-xs px-2 py-2 text-left text-base", cat === c ? "bg-selected font-medium text-ink" : "text-ink-2 hover:bg-hover")}>{c}</button>)}
245
+ </nav>
246
+ <div className="grid content-start gap-3 p-4">
247
+ <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search templates" aria-label="Search templates" />
248
+ <ul className="grid grid-cols-2 gap-2 max-sm:grid-cols-1">
249
+ {shown.map((t) => (
250
+ <li key={t.id}>
251
+ <button type="button" onClick={() => onPick(t)} className="grid h-full w-full content-start gap-1 rounded-sm border border-line p-3 text-left transition-shadow hover:shadow-raised">
252
+ <span className="text-base font-medium text-ink">{t.name}</span>
253
+ <span className="text-sm text-ink-2">{t.does}</span>
254
+ <span className="text-xs text-ink-3">Uses {t.tools.join(", ")}</span>
255
+ </button>
256
+ </li>
257
+ ))}
258
+ </ul>
259
+ {!shown.length && <p className="text-base text-ink-2">No templates match.</p>}
260
+ </div>
261
+ </div>
262
+ </DialogContent>
263
+ </Dialog>
264
+ )
265
+ }
266
+
267
+ /* ---------------------------------------------------------------- prompt editor */
268
+
269
+ export function PromptEditor({ segments, onRemoveField, inputs, outputs, onTest, testing, results }: { segments: (string | { field: string })[]; onRemoveField: (i: number) => void; inputs: { name: string; type: string }[]; outputs: { name: string; type: string }[]; onTest: () => void; testing?: boolean; results?: { row: string; value: string }[] }) {
270
+ return (
271
+ <div className="grid gap-3">
272
+ <p className="text-sm text-ink-3">Type <kbd className="rounded-xs border border-line px-1 font-mono">{"{{"}</kbd> for a field, <kbd className="rounded-xs border border-line px-1 font-mono">@</kbd> for a team file.</p>
273
+ <div role="textbox" aria-label="Prompt" aria-multiline="true" className="min-h-20 rounded-sm border border-input bg-surface px-2 py-2 text-base leading-7 text-ink shadow-xs">
274
+ {segments.map((s, i) =>
275
+ typeof s === "string" ? (
276
+ <span key={i}>{s}</span>
277
+ ) : (
278
+ <span key={i} className="mx-0.5 inline-flex h-5.5 items-center gap-1 rounded-xs bg-primary-soft pr-0.5 pl-2 align-baseline font-mono text-sm text-primary-ink">
279
+ {s.field}
280
+ <button type="button" aria-label={`Remove field ${s.field}`} onClick={() => onRemoveField(i)} className="relative grid size-4 place-items-center rounded-xs after:absolute after:-inset-1 hover:bg-surface"><X className="size-3" aria-hidden="true" /></button>
281
+ </span>
282
+ ),
283
+ )}
284
+ </div>
285
+ <div className="grid grid-cols-2 gap-3 max-sm:grid-cols-1">
286
+ {[["Inputs", inputs], ["Output", outputs]].map(([title, list]) => (
287
+ <section key={title as string} className="grid gap-1 rounded-sm border border-line p-2">
288
+ <h3 className="text-sm font-medium text-ink-2">{title as string}</h3>
289
+ <ul className="grid gap-1">{(list as { name: string; type: string }[]).map((f) => <li key={f.name} className="flex justify-between text-base"><span className="font-mono text-ink">{f.name}</span><span className="text-sm text-ink-3">{f.type}</span></li>)}</ul>
290
+ </section>
291
+ ))}
292
+ </div>
293
+ <div className="flex items-center gap-2">
294
+ <Button variant="outline" size="sm" loading={testing} onClick={onTest}>Test on 3 rows</Button>
295
+ <span className="text-sm text-ink-3">Check the results before running on the whole table.</span>
296
+ </div>
297
+ {results && (
298
+ <ul aria-label="Test results" className="divide-y divide-line rounded-sm border border-line">
299
+ {results.map((r) => <li key={r.row} className="grid gap-0.5 px-3 py-2"><span className="text-sm text-ink-3">{r.row}</span><span className="text-base text-ink">{r.value}</span><AiOrigin from={r.row} /></li>)}
300
+ </ul>
301
+ )}
302
+ </div>
303
+ )
304
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * TwoUX compositions for working with the assistant, built from generated shadcn parts.
3
+ * The assistant changes nothing on its own: it proposes (SuggestedChanges), the person applies, Undo follows.
4
+ * The basic parts (origin label, citations, thinking steps, composer, starters and follow-ups) live in ai-basics.tsx.
5
+ * AI is marked by one small mark plus words ("Assistant · from …"), never by a separate colour or gradient.
6
+ */
7
+ import { Check, Copy, Ellipsis, FileText, Loader2, RefreshCw, ThumbsDown, ThumbsUp, X } from "lucide-react"
8
+ import * as React from "react"
9
+ import { cn } from "../lib/cn"
10
+ import { ShimmerText, Swap } from "./motion"
11
+ import { Button } from "./button"
12
+ import { Checkbox } from "./checkbox"
13
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "./dropdown-menu"
14
+ import { SidePanel, type PanelMode } from "./shell"
15
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs"
16
+ import { Textarea } from "./textarea"
17
+ import { AiMark, AiOrigin } from "./ai-basics"
18
+
19
+ /* ---------------------------------------------------------------- clarifying question */
20
+
21
+ export type ClarifyOption = { id: string; title: string; detail?: string }
22
+
23
+ /** Up to three questions, one at a time; the recommended option first; Other last; Skip keeps the defaults. */
24
+ export function ClarifyingQuestion({ questions, onDone }: { questions: { question: string; options: ClarifyOption[] }[]; onDone: (answers: string[]) => void }) {
25
+ const [i, setI] = React.useState(0)
26
+ const [answers, setAnswers] = React.useState<string[]>(questions.map((q) => q.options[0]?.id ?? ""))
27
+ const [other, setOther] = React.useState("")
28
+ const q = questions[i]
29
+ const choose = (v: string) => setAnswers((a) => a.map((x, j) => (j === i ? v : x)))
30
+ const next = () => (i < questions.length - 1 ? setI(i + 1) : onDone(answers.map((a) => (a === "__other" ? other : a))))
31
+ const onKey = (e: React.KeyboardEvent) => {
32
+ if ((e.target as HTMLElement).tagName === "TEXTAREA" || (e.target as HTMLElement).tagName === "INPUT") return
33
+ const n = Number(e.key)
34
+ if (n >= 1 && n <= q.options.length) { e.preventDefault(); choose(q.options[n - 1].id) }
35
+ else if (n === q.options.length + 1) { e.preventDefault(); choose("__other") }
36
+ else if (e.key === "Enter") { e.preventDefault(); next() }
37
+ }
38
+ return (
39
+ <section aria-label="Question from the assistant" onKeyDown={onKey} className="grid gap-3 rounded-md border border-line bg-surface p-3 shadow-card">
40
+ <div className="flex items-baseline gap-2"><span className="tabular text-sm text-ink-3">{i + 1} / {questions.length}</span><h3 className="text-base font-medium text-ink">{q.question}</h3></div>
41
+ <div role="radiogroup" aria-label={q.question} className="grid gap-1">
42
+ {[...q.options, { id: "__other", title: "Other", detail: undefined }].map((o, k) => {
43
+ const on = answers[i] === o.id
44
+ return (
45
+ <button key={o.id} type="button" role="radio" aria-checked={on} onClick={() => choose(o.id)} className={cn("flex items-start gap-2 rounded-sm border px-2 py-2 text-left transition-colors", on ? "border-primary bg-primary-soft" : "border-line hover:bg-hover")}>
46
+ <span className="tabular mt-px grid size-4.5 shrink-0 place-items-center rounded-xs border border-line-strong bg-surface text-xs text-ink-2">{k + 1}</span>
47
+ <span className="grid gap-0.5">
48
+ <span className="text-base text-ink">{o.title}{k === 0 && <span className="text-ink-3"> (Recommended)</span>}</span>
49
+ {o.detail && <span className="text-sm text-ink-2">{o.detail}</span>}
50
+ </span>
51
+ </button>
52
+ )
53
+ })}
54
+ {answers[i] === "__other" && <Textarea autoFocus value={other} onChange={(e) => setOther(e.target.value)} placeholder="Tell the assistant what you want" aria-label="Your answer" />}
55
+ </div>
56
+ <div className="flex items-center gap-2">
57
+ <Button variant="ghost" size="sm" disabled={i === 0} onClick={() => setI(i - 1)}>Previous</Button>
58
+ <span className="flex-1" />
59
+ <Button variant="ghost" size="sm" onClick={() => onDone(answers)}>Skip, use defaults</Button>
60
+ <Button size="sm" shortcut="↵" onClick={next}>{i < questions.length - 1 ? "Next" : "Continue"}</Button>
61
+ </div>
62
+ </section>
63
+ )
64
+ }
65
+
66
+ /* ---------------------------------------------------------------- proposals */
67
+
68
+ export type ProposedChange = { id: string; label: string; detail?: string; before?: string; after?: string }
69
+
70
+ /** The proposal: each change with a tick and before/after. Nothing changes until Apply. */
71
+ export function SuggestedChanges({ changes, onApply, onDismiss, onEdit }: { changes: ProposedChange[]; onApply: (ids: string[]) => void; onDismiss: () => void; onEdit?: () => void }) {
72
+ const [on, setOn] = React.useState(changes.map((c) => c.id))
73
+ const n = on.length
74
+ return (
75
+ <section aria-label="Suggested changes" onKeyDown={(e) => { if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && n) { e.preventDefault(); onApply(on) } }} className="grid rounded-md border border-line bg-surface shadow-card">
76
+ <header className="flex items-center gap-2 border-b border-line px-3 py-2"><AiMark /><h3 className="text-base font-semibold text-ink">Suggested changes</h3><span className="tabular text-sm text-ink-3">{changes.length}</span></header>
77
+ <ul className="divide-y divide-line">
78
+ {changes.map((c) => {
79
+ const id = `chg-${c.id}`
80
+ const checked = on.includes(c.id)
81
+ return (
82
+ <li key={c.id} className="flex items-start gap-2 px-3 py-2">
83
+ <Checkbox id={id} checked={checked} onCheckedChange={() => setOn((x) => (checked ? x.filter((y) => y !== c.id) : [...x, c.id]))} className="mt-0.5" />
84
+ <label htmlFor={id} className="grid min-w-0 gap-0.5">
85
+ <span className="text-base font-medium text-ink">{c.label}</span>
86
+ {c.detail && <span className="text-sm text-ink-2">{c.detail}</span>}
87
+ {(c.before || c.after) && (
88
+ <span className="text-sm">
89
+ {c.before && <del className="mr-2 rounded-xs bg-bad-bg px-1 text-bad decoration-bad/60">{c.before}</del>}
90
+ {c.after && <ins className="rounded-xs bg-ok-bg px-1 text-ok no-underline">{c.after}</ins>}
91
+ </span>
92
+ )}
93
+ </label>
94
+ </li>
95
+ )
96
+ })}
97
+ </ul>
98
+ <footer className="flex flex-wrap items-center gap-2 border-t border-line px-3 py-2">
99
+ <span className="flex-1 text-sm text-ink-3">Nothing changes until you apply.</span>
100
+ <Button variant="ghost" size="sm" onClick={onDismiss}>Dismiss</Button>
101
+ {onEdit && <Button variant="ghost" size="sm" onClick={onEdit}>Edit</Button>}
102
+ <Button size="sm" shortcut="⌘↵" disabled={!n} onClick={() => onApply(on)}>Apply {n} {n === 1 ? "change" : "changes"}</Button>
103
+ </footer>
104
+ </section>
105
+ )
106
+ }
107
+
108
+ /** While changes apply: each step pending, done or failed (with why); a counter; then a summary. */
109
+ export function StepCard({ steps, title }: { title: string; steps: { label: string; state: "pending" | "done" | "failed"; reason?: string }[] }) {
110
+ const done = steps.filter((s) => s.state === "done").length
111
+ const finished = steps.every((s) => s.state !== "pending")
112
+ return (
113
+ <section aria-label={title} className="grid rounded-md border border-line bg-surface shadow-card">
114
+ <header className="flex items-center justify-between border-b border-line px-3 py-2"><h3 className="text-base font-semibold text-ink">{title}</h3><span className="tabular text-sm text-ink-3">{done} / {steps.length}</span></header>
115
+ <ul className="grid gap-2 px-3 py-2">
116
+ {steps.map((s, i) => (
117
+ <li key={i} className="flex items-start gap-2 text-base">
118
+ {s.state === "pending" ? <Loader2 className="mt-0.5 size-3.5 shrink-0 animate-spin text-ink-3" aria-hidden="true" /> : s.state === "done" ? <Check className="mt-0.5 size-3.5 shrink-0 text-ok" aria-hidden="true" /> : <X className="mt-0.5 size-3.5 shrink-0 text-bad" aria-hidden="true" />}
119
+ <span className="grid"><span className="text-ink">{s.label}<span className="sr-only"> ({s.state})</span></span>{s.reason && <span className="text-sm text-bad">{s.reason}</span>}</span>
120
+ </li>
121
+ ))}
122
+ </ul>
123
+ <footer role="status" className="border-t border-line px-3 py-2 text-sm text-ink-2">{finished ? `Applied ${done} of ${steps.length}.` : <ShimmerText>Working…</ShimmerText>}</footer>
124
+ </section>
125
+ )
126
+ }
127
+
128
+ /* ---------------------------------------------------------------- answer actions and artifacts */
129
+
130
+ export function AnswerActions({ text, latest = true, onRegenerate, onFeedback, more }: { text: string; latest?: boolean; onRegenerate?: () => void; onFeedback: (v: "up" | "down") => Promise<void>; more?: React.ReactNode }) {
131
+ const [fb, setFb] = React.useState<{ v: "up" | "down"; state: "sending" | "sent" } | null>(null)
132
+ const [copied, setCopied] = React.useState(false)
133
+ const send = async (v: "up" | "down") => { setFb({ v, state: "sending" }); try { await onFeedback(v); setFb({ v, state: "sent" }) } catch { setFb(null) } }
134
+ return (
135
+ <div role="group" aria-label="Answer actions" className="flex items-center gap-0.5">
136
+ <Button variant="ghost" size="icon-xs" aria-label={copied ? "Copied" : "Copy answer"} onClick={() => { void navigator.clipboard?.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1500) }}><Swap kind="icon" swapKey={copied ? "copied" : "copy"}>{copied ? <Check /> : <Copy />}</Swap></Button>
137
+ <Button variant="ghost" size="icon-xs" aria-label="Helpful" aria-pressed={fb?.v === "up"} disabled={fb?.state === "sent" && fb.v !== "up"} loading={fb?.state === "sending" && fb.v === "up"} onClick={() => send("up")} className="aria-pressed:text-primary-ink"><ThumbsUp /></Button>
138
+ <Button variant="ghost" size="icon-xs" aria-label="Not helpful" aria-pressed={fb?.v === "down"} disabled={fb?.state === "sent" && fb.v !== "down"} loading={fb?.state === "sending" && fb.v === "down"} onClick={() => send("down")} className="aria-pressed:text-primary-ink"><ThumbsDown /></Button>
139
+ {latest && onRegenerate && <Button variant="ghost" size="icon-xs" aria-label="Regenerate" onClick={onRegenerate}><RefreshCw /></Button>}
140
+ {more && (
141
+ <DropdownMenu>
142
+ <DropdownMenuTrigger asChild><Button variant="ghost" size="icon-xs" aria-label="More answer actions"><Ellipsis /></Button></DropdownMenuTrigger>
143
+ <DropdownMenuContent align="start">{more}</DropdownMenuContent>
144
+ </DropdownMenu>
145
+ )}
146
+ {fb?.state === "sent" && <span role="status" className="ml-1 text-sm text-ink-3">Thanks for the feedback</span>}
147
+ </div>
148
+ )
149
+ }
150
+
151
+ export function ArtifactCard({ title, meta, from, writing, actions, preview }: { title: string; meta: string; from: string; writing?: boolean; actions?: React.ReactNode; preview?: React.ReactNode }) {
152
+ return (
153
+ <article aria-label={title} className="grid rounded-sm border border-line bg-surface">
154
+ <div className="flex items-start gap-2 p-3">
155
+ <span className="grid size-8 shrink-0 place-items-center rounded-sm bg-surface-2 text-ink-2"><FileText className="size-4" aria-hidden="true" /></span>
156
+ <div className="grid min-w-0 flex-1 gap-0.5">
157
+ <span className="truncate text-base font-medium text-ink">{title}</span>
158
+ {writing ? <span role="status" className="flex items-center gap-control text-sm text-ink-3"><Loader2 className="size-3 animate-spin" aria-hidden="true" />Writing {title.toLowerCase()}</span> : <span className="text-sm text-ink-3">{meta}</span>}
159
+ <AiOrigin from={from} />
160
+ </div>
161
+ {!writing && actions && <div className="flex shrink-0 items-center gap-1">{actions}</div>}
162
+ </div>
163
+ {preview && !writing && <div className="max-h-48 overflow-hidden border-t border-line bg-surface-2 px-3 py-2 text-sm text-ink-2">{preview}</div>}
164
+ </article>
165
+ )
166
+ }
167
+
168
+ /* ---------------------------------------------------------------- thread, panel and page */
169
+
170
+ export function UserMessage({ children }: { children: React.ReactNode }) {
171
+ return <div className="justify-self-end rounded-md bg-surface-2 px-3 py-2 text-base text-ink">{children}</div>
172
+ }
173
+
174
+ export function AssistantMessage({ from, children, actions, followUps }: { from: string; children: React.ReactNode; actions?: React.ReactNode; followUps?: React.ReactNode }) {
175
+ return (
176
+ <div className="grid gap-2">
177
+ <AiOrigin from={from} />
178
+ <div className="grid gap-2 text-base text-ink [&_p]:max-w-prose">{children}</div>
179
+ {actions}
180
+ {followUps}
181
+ </div>
182
+ )
183
+ }
184
+
185
+ export function AssistantPanel({
186
+ open,
187
+ onOpenChange,
188
+ title = "Assistant",
189
+ mode = "push",
190
+ onOpenPage,
191
+ children,
192
+ composer,
193
+ caveat,
194
+ }: {
195
+ open: boolean
196
+ onOpenChange: (o: boolean) => void
197
+ title?: string
198
+ mode?: PanelMode
199
+ onOpenPage?: () => void
200
+ children: React.ReactNode
201
+ composer: React.ReactNode
202
+ /** One specific limit and what to check, e.g. "Reads listings and applications only, not emails. Check closing dates before extending." Never a generic "can make mistakes". */
203
+ caveat?: string
204
+ }) {
205
+ return (
206
+ <SidePanel open={open} onOpenChange={onOpenChange} mode={mode} label="Assistant" title={<span className="flex items-center gap-control"><AiMark className="size-3.5" />{title}</span>} onOpenFull={onOpenPage} footer={<div className="grid w-full gap-2">{composer}{caveat && <p className="text-xs text-ink-3">{caveat}</p>}</div>}>
207
+ <div role="log" aria-label="Conversation" className="grid gap-5">{children}</div>
208
+ </SidePanel>
209
+ )
210
+ }
211
+
212
+ export function AssistantPage({ thread, records, sources, composer }: { thread: React.ReactNode; records: React.ReactNode; sources: React.ReactNode; composer: React.ReactNode }) {
213
+ return (
214
+ <div className="grid min-h-0 grid-cols-[minmax(0,1fr)_280px] max-lg:grid-cols-1">
215
+ <Tabs defaultValue="answer" className="min-h-0 gap-0">
216
+ <div className="border-b border-line px-6"><TabsList aria-label="Assistant views" className="border-b-0"><TabsTrigger value="answer">Answer</TabsTrigger><TabsTrigger value="records">Records</TabsTrigger></TabsList></div>
217
+ <TabsContent value="answer" className="mx-auto grid w-full max-w-2xl content-start gap-5 px-6 py-5"><div role="log" aria-label="Conversation" className="grid gap-5">{thread}</div>{composer}</TabsContent>
218
+ <TabsContent value="records" className="px-6 py-5">{records}</TabsContent>
219
+ </Tabs>
220
+ <aside aria-label="Sources" className="grid content-start gap-2 border-l border-line bg-surface-2 p-4 max-lg:border-t max-lg:border-l-0">
221
+ <h2 className="text-sm font-medium text-ink-2">Sources</h2>
222
+ {sources}
223
+ </aside>
224
+ </div>
225
+ )
226
+ }
227
+