@superxd-studio/adptr 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 (221) hide show
  1. package/GENERATIVE.md +101 -0
  2. package/INTERACTIONS.md +51 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -0
  5. package/REUSE.md +170 -0
  6. package/REVIEW.md +68 -0
  7. package/THIRD-PARTY-NOTICES.md +28 -0
  8. package/ai-sdk/index.js +96 -0
  9. package/assets/ibm-plex-mono-cyrillic-400-normal.woff2 +0 -0
  10. package/assets/ibm-plex-mono-cyrillic-500-normal.woff2 +0 -0
  11. package/assets/ibm-plex-mono-cyrillic-ext-400-normal.woff2 +0 -0
  12. package/assets/ibm-plex-mono-cyrillic-ext-500-normal.woff2 +0 -0
  13. package/assets/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  14. package/assets/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  15. package/assets/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  16. package/assets/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  17. package/assets/ibm-plex-mono-vietnamese-400-normal.woff2 +0 -0
  18. package/assets/ibm-plex-mono-vietnamese-500-normal.woff2 +0 -0
  19. package/assets/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
  20. package/assets/instrument-sans-latin-wght-normal.woff2 +0 -0
  21. package/assistant-ui/index.js +139 -0
  22. package/chunks/callout-DKMHSnjc.js +342 -0
  23. package/chunks/canvas-BJ15-XxE.js +598 -0
  24. package/chunks/controller-Cfpg0C7v.js +180 -0
  25. package/chunks/core-BCqVRLUy.js +312 -0
  26. package/chunks/text-field-BLtoaZFn.js +41 -0
  27. package/chunks/types-BGF7r1my.js +115 -0
  28. package/chunks/ui-9DyNsYXY.js +967 -0
  29. package/core/index.js +4 -0
  30. package/custom/index.js +64 -0
  31. package/examples/ai-sdk-backend/package.json +11 -0
  32. package/examples/ai-sdk-backend/server.mjs +316 -0
  33. package/examples/custom-backend/copy.mjs +7 -0
  34. package/examples/custom-backend/server.mjs +118 -0
  35. package/examples/generative/specs.json +2094 -0
  36. package/examples/review-backend/copy.mjs +27 -0
  37. package/examples/review-backend/package.json +11 -0
  38. package/examples/review-backend/server.mjs +276 -0
  39. package/examples/web-ai-sdk/index.html +12 -0
  40. package/examples/web-ai-sdk/package.json +21 -0
  41. package/examples/web-ai-sdk/src/copy.ts +1 -0
  42. package/examples/web-ai-sdk/src/main.tsx +64 -0
  43. package/examples/web-ai-sdk/tsconfig.json +13 -0
  44. package/examples/web-generative/index.html +12 -0
  45. package/examples/web-generative/package.json +23 -0
  46. package/examples/web-generative/src/copy.ts +42 -0
  47. package/examples/web-generative/src/main.tsx +74 -0
  48. package/examples/web-generative/tsconfig.json +14 -0
  49. package/examples/web-independent/index.html +12 -0
  50. package/examples/web-independent/package.json +20 -0
  51. package/examples/web-independent/src/main.tsx +32 -0
  52. package/examples/web-independent/tsconfig.json +14 -0
  53. package/examples/web-interactions/index.html +12 -0
  54. package/examples/web-interactions/package.json +21 -0
  55. package/examples/web-interactions/src/copy.ts +215 -0
  56. package/examples/web-interactions/src/examples.tsx +628 -0
  57. package/examples/web-interactions/src/main.tsx +116 -0
  58. package/examples/web-interactions/tsconfig.json +14 -0
  59. package/examples/web-review/index.html +12 -0
  60. package/examples/web-review/package.json +20 -0
  61. package/examples/web-review/src/main.tsx +59 -0
  62. package/examples/web-review/tsconfig.json +14 -0
  63. package/fonts.css +117 -0
  64. package/generative/index.js +1560 -0
  65. package/interactions/index.js +1591 -0
  66. package/langgraph-review/index.js +36 -0
  67. package/licenses/ai-sdk.txt +13 -0
  68. package/licenses/assistant-ui-generative.txt +21 -0
  69. package/licenses/assistant-ui.txt +23 -0
  70. package/licenses/ibm-plex-mono.txt +93 -0
  71. package/licenses/instrument-sans.txt +93 -0
  72. package/licenses/internationalized-date.txt +201 -0
  73. package/licenses/langgraph.txt +21 -0
  74. package/licenses/lucide.txt +43 -0
  75. package/licenses/react-markdown.txt +21 -0
  76. package/licenses/remark-gfm.txt +22 -0
  77. package/licenses/zod.txt +21 -0
  78. package/package.json +138 -0
  79. package/review/index.js +31 -0
  80. package/source/components/avatar.tsx +50 -0
  81. package/source/components/button.tsx +155 -0
  82. package/source/components/callout.tsx +65 -0
  83. package/source/components/canvas/connector.tsx +14 -0
  84. package/source/components/canvas/copy.ts +34 -0
  85. package/source/components/canvas/index.tsx +257 -0
  86. package/source/components/canvas/recorder.ts +36 -0
  87. package/source/components/date-time-field.tsx +77 -0
  88. package/source/components/flow-motion.tsx +109 -0
  89. package/source/components/icons.tsx +275 -0
  90. package/source/components/responsive-overlay.tsx +91 -0
  91. package/source/components/scenario-picker/copy.ts +5 -0
  92. package/source/components/scenario-picker/index.tsx +142 -0
  93. package/source/components/scroll-area.tsx +59 -0
  94. package/source/components/search-picker.tsx +127 -0
  95. package/source/components/segmented-control.tsx +123 -0
  96. package/source/components/skeleton.tsx +11 -0
  97. package/source/components/spinner.tsx +19 -0
  98. package/source/components/superxd-arrow.tsx +22 -0
  99. package/source/components/switch.tsx +45 -0
  100. package/source/components/text-field.tsx +81 -0
  101. package/source/components/thinking.tsx +16 -0
  102. package/source/components/ui/copy.ts +12 -0
  103. package/source/components/ui/display.tsx +276 -0
  104. package/source/components/ui/navigation.tsx +346 -0
  105. package/source/components/ui/portal-theme.tsx +28 -0
  106. package/source/components/ui/selection.tsx +336 -0
  107. package/source/lib/cn.ts +23 -0
  108. package/source/reusable/ai-sdk/index.ts +114 -0
  109. package/source/reusable/assistant-ui/index.tsx +266 -0
  110. package/source/reusable/core/attachments.ts +163 -0
  111. package/source/reusable/core/index.ts +417 -0
  112. package/source/reusable/custom/index.ts +90 -0
  113. package/source/reusable/generative/actions.ts +188 -0
  114. package/source/reusable/generative/copy.ts +47 -0
  115. package/source/reusable/generative/data.tsx +126 -0
  116. package/source/reusable/generative/index.tsx +517 -0
  117. package/source/reusable/generative/schema.ts +327 -0
  118. package/source/reusable/generative/timeline.tsx +214 -0
  119. package/source/reusable/generative/vocabulary.tsx +847 -0
  120. package/source/reusable/interactions/agent.ts +259 -0
  121. package/source/reusable/interactions/artifact.ts +236 -0
  122. package/source/reusable/interactions/context.tsx +38 -0
  123. package/source/reusable/interactions/copy.ts +117 -0
  124. package/source/reusable/interactions/feedback.ts +73 -0
  125. package/source/reusable/interactions/index.ts +11 -0
  126. package/source/reusable/interactions/presentation.tsx +483 -0
  127. package/source/reusable/interactions/store.ts +74 -0
  128. package/source/reusable/interactions/threads.ts +126 -0
  129. package/source/reusable/interactions/web.tsx +574 -0
  130. package/source/reusable/langgraph-review/index.ts +73 -0
  131. package/source/reusable/review/controller.ts +263 -0
  132. package/source/reusable/review/index.ts +47 -0
  133. package/source/reusable/review/types.ts +189 -0
  134. package/source/reusable/ui/index.ts +25 -0
  135. package/source/reusable/uploads/index.ts +48 -0
  136. package/source/reusable/web/attachments/copy.ts +23 -0
  137. package/source/reusable/web/attachments/index.tsx +233 -0
  138. package/source/reusable/web/copy.ts +22 -0
  139. package/source/reusable/web/index.tsx +470 -0
  140. package/source/reusable/web/message/copy.ts +26 -0
  141. package/source/reusable/web/message/index.tsx +388 -0
  142. package/source/reusable/web/message/streaming.ts +64 -0
  143. package/source/reusable/web/message/styles.css +21 -0
  144. package/source/reusable/web/review/copy.ts +78 -0
  145. package/source/reusable/web/review/flow.ts +24 -0
  146. package/source/reusable/web/review/index.tsx +506 -0
  147. package/source/reusable/web/styles.ts +2 -0
  148. package/source/styles/library.css +612 -0
  149. package/source/styles/theme.css +228 -0
  150. package/styles.css +3 -0
  151. package/styles.d.ts +1 -0
  152. package/types/components/avatar.d.ts +14 -0
  153. package/types/components/button.d.ts +36 -0
  154. package/types/components/callout.d.ts +19 -0
  155. package/types/components/canvas/connector.d.ts +2 -0
  156. package/types/components/canvas/copy.d.ts +34 -0
  157. package/types/components/canvas/index.d.ts +33 -0
  158. package/types/components/canvas/recorder.d.ts +10 -0
  159. package/types/components/date-time-field.d.ts +11 -0
  160. package/types/components/flow-motion.d.ts +38 -0
  161. package/types/components/icons.d.ts +138 -0
  162. package/types/components/responsive-overlay.d.ts +15 -0
  163. package/types/components/scenario-picker/copy.d.ts +5 -0
  164. package/types/components/scenario-picker/index.d.ts +14 -0
  165. package/types/components/scroll-area.d.ts +14 -0
  166. package/types/components/search-picker.d.ts +27 -0
  167. package/types/components/segmented-control.d.ts +23 -0
  168. package/types/components/skeleton.d.ts +4 -0
  169. package/types/components/spinner.d.ts +4 -0
  170. package/types/components/superxd-arrow.d.ts +5 -0
  171. package/types/components/switch.d.ts +8 -0
  172. package/types/components/text-field.d.ts +19 -0
  173. package/types/components/thinking.d.ts +8 -0
  174. package/types/components/ui/copy.d.ts +12 -0
  175. package/types/components/ui/display.d.ts +50 -0
  176. package/types/components/ui/navigation.d.ts +63 -0
  177. package/types/components/ui/portal-theme.d.ts +8 -0
  178. package/types/components/ui/selection.d.ts +37 -0
  179. package/types/lib/cn.d.ts +3 -0
  180. package/types/reusable/ai-sdk/index.d.ts +15 -0
  181. package/types/reusable/assistant-ui/index.d.ts +12 -0
  182. package/types/reusable/core/attachments.d.ts +54 -0
  183. package/types/reusable/core/index.d.ts +107 -0
  184. package/types/reusable/custom/index.d.ts +14 -0
  185. package/types/reusable/generative/actions.d.ts +45 -0
  186. package/types/reusable/generative/copy.d.ts +48 -0
  187. package/types/reusable/generative/data.d.ts +4 -0
  188. package/types/reusable/generative/index.d.ts +30 -0
  189. package/types/reusable/generative/schema.d.ts +303 -0
  190. package/types/reusable/generative/timeline.d.ts +21 -0
  191. package/types/reusable/generative/vocabulary.d.ts +34 -0
  192. package/types/reusable/interactions/agent.d.ts +98 -0
  193. package/types/reusable/interactions/artifact.d.ts +118 -0
  194. package/types/reusable/interactions/context.d.ts +130 -0
  195. package/types/reusable/interactions/copy.d.ts +115 -0
  196. package/types/reusable/interactions/feedback.d.ts +34 -0
  197. package/types/reusable/interactions/index.d.ts +10 -0
  198. package/types/reusable/interactions/presentation.d.ts +97 -0
  199. package/types/reusable/interactions/store.d.ts +29 -0
  200. package/types/reusable/interactions/threads.d.ts +38 -0
  201. package/types/reusable/interactions/web.d.ts +22 -0
  202. package/types/reusable/langgraph-review/index.d.ts +5 -0
  203. package/types/reusable/review/controller.d.ts +52 -0
  204. package/types/reusable/review/index.d.ts +12 -0
  205. package/types/reusable/review/types.d.ts +61 -0
  206. package/types/reusable/ui/index.d.ts +19 -0
  207. package/types/reusable/uploads/index.d.ts +11 -0
  208. package/types/reusable/web/attachments/copy.d.ts +25 -0
  209. package/types/reusable/web/attachments/index.d.ts +24 -0
  210. package/types/reusable/web/copy.d.ts +24 -0
  211. package/types/reusable/web/index.d.ts +41 -0
  212. package/types/reusable/web/message/copy.d.ts +27 -0
  213. package/types/reusable/web/message/index.d.ts +56 -0
  214. package/types/reusable/web/message/streaming.d.ts +27 -0
  215. package/types/reusable/web/review/copy.d.ts +72 -0
  216. package/types/reusable/web/review/flow.d.ts +3 -0
  217. package/types/reusable/web/review/index.d.ts +38 -0
  218. package/types/reusable/web/styles.d.ts +2 -0
  219. package/ui/index.js +4 -0
  220. package/uploads/index.js +34 -0
  221. package/web/index.js +931 -0
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+ import { InteractionStore, identity, shortText, jsonSchema } from "./store.js";
3
+ export const threadSummarySchema = z.strictObject({
4
+ id: identity,
5
+ title: shortText,
6
+ archived: z.boolean(),
7
+ });
8
+ export const threadDocumentSchema = threadSummarySchema.extend({ payload: jsonSchema });
9
+ export type ThreadSummary = z.infer<typeof threadSummarySchema>;
10
+ export type ThreadDocument = z.infer<typeof threadDocumentSchema>;
11
+ export interface ThreadAdapter {
12
+ list(signal: AbortSignal): Promise<readonly ThreadSummary[]>;
13
+ load(id: string, signal: AbortSignal): Promise<ThreadDocument>;
14
+ rename(id: string, title: string, signal: AbortSignal): Promise<ThreadSummary>;
15
+ archive(id: string, archived: boolean, signal: AbortSignal): Promise<ThreadSummary>;
16
+ }
17
+ export interface ThreadSnapshot {
18
+ phase: "idle" | "listing" | "opening" | "mutating" | "ready" | "error";
19
+ items: readonly ThreadSummary[];
20
+ selected?: ThreadDocument;
21
+ pendingId?: string;
22
+ operation?: "list" | "open" | "rename" | "archive";
23
+ }
24
+ export class ThreadController extends InteractionStore<ThreadSnapshot> {
25
+ constructor(
26
+ private adapter: ThreadAdapter,
27
+ private canLeave: () => boolean = () => true,
28
+ ) {
29
+ super({ phase: "idle", items: [] });
30
+ }
31
+ refresh() {
32
+ if (this.disposed || ["listing", "mutating", "opening"].includes(this.state.phase))
33
+ return false;
34
+ const { token, signal } = this.request();
35
+ this.publish({ ...this.state, phase: "listing", operation: "list" });
36
+ void (async () => {
37
+ try {
38
+ const items = z
39
+ .array(threadSummarySchema)
40
+ .max(1000)
41
+ .parse(await this.adapter.list(signal));
42
+ if (new Set(items.map((item) => item.id)).size !== items.length)
43
+ throw new Error("Duplicate thread");
44
+ if (this.current(token))
45
+ this.publish({ ...this.state, items, phase: "ready", operation: undefined });
46
+ } catch {
47
+ if (this.current(token)) this.publish({ ...this.state, phase: "error" });
48
+ }
49
+ })();
50
+ return true;
51
+ }
52
+ open(id: string) {
53
+ if (
54
+ this.disposed ||
55
+ ["listing", "mutating"].includes(this.state.phase) ||
56
+ !this.state.items.some((item) => item.id === id) ||
57
+ !this.canLeave()
58
+ )
59
+ return false;
60
+ const { token, signal } = this.request();
61
+ this.publish({ ...this.state, phase: "opening", pendingId: id, operation: "open" });
62
+ void (async () => {
63
+ try {
64
+ const selected = threadDocumentSchema.parse(await this.adapter.load(id, signal));
65
+ if (selected.id !== id) throw new Error("Wrong thread");
66
+ if (this.current(token))
67
+ this.publish({
68
+ ...this.state,
69
+ selected,
70
+ phase: "ready",
71
+ pendingId: undefined,
72
+ operation: undefined,
73
+ });
74
+ } catch {
75
+ if (this.current(token))
76
+ this.publish({ ...this.state, phase: "error", pendingId: undefined });
77
+ }
78
+ })();
79
+ return true;
80
+ }
81
+ rename(id: string, title: string) {
82
+ const parsed = shortText.safeParse(title);
83
+ if (!parsed.success) return false;
84
+ return this.mutate(id, "rename", (signal) => this.adapter.rename(id, parsed.data, signal));
85
+ }
86
+ archive(id: string, archived: boolean) {
87
+ if (this.state.selected?.id === id && !this.canLeave()) return false;
88
+ return this.mutate(id, "archive", (signal) => this.adapter.archive(id, archived, signal));
89
+ }
90
+ private mutate(
91
+ id: string,
92
+ operation: "rename" | "archive",
93
+ run: (signal: AbortSignal) => Promise<ThreadSummary>,
94
+ ) {
95
+ if (
96
+ this.disposed ||
97
+ !["ready", "error"].includes(this.state.phase) ||
98
+ !this.state.items.some((item) => item.id === id)
99
+ )
100
+ return false;
101
+ const { token, signal } = this.request();
102
+ this.publish({ ...this.state, phase: "mutating", pendingId: id, operation });
103
+ void (async () => {
104
+ try {
105
+ const item = threadSummarySchema.parse(await run(signal));
106
+ if (item.id !== id) throw new Error("Wrong thread");
107
+ if (this.current(token))
108
+ this.publish({
109
+ ...this.state,
110
+ phase: "ready",
111
+ pendingId: undefined,
112
+ operation: undefined,
113
+ items: this.state.items.map((old) => (old.id === id ? item : old)),
114
+ selected:
115
+ this.state.selected?.id === id
116
+ ? { ...this.state.selected, ...item }
117
+ : this.state.selected,
118
+ });
119
+ } catch {
120
+ if (this.current(token))
121
+ this.publish({ ...this.state, phase: "error", pendingId: undefined });
122
+ }
123
+ })();
124
+ return true;
125
+ }
126
+ }
@@ -0,0 +1,574 @@
1
+ import { useInteractionCopy } from "./context.js";
2
+ import { useLayoutEffect, useRef, useState, useSyncExternalStore, type ReactNode } from "react";
3
+ import { Button } from "../../components/button.js";
4
+ import { TextField } from "../../components/text-field.js";
5
+ import { Callout } from "../../components/callout.js";
6
+ import { Spinner } from "../../components/spinner.js";
7
+ import { Card, Badge, RadioGroup, Select, Tabs } from "../ui/index.js";
8
+ import { CanvasSequence, CanvasStage } from "../../components/canvas/index.js";
9
+ import { ToolCall } from "./presentation.js";
10
+ import type { AgentController, AgentSnapshot } from "./agent.js";
11
+ import type { ArtifactController, ArtifactSnapshot } from "./artifact.js";
12
+ import type { FeedbackController, FeedbackSnapshot } from "./feedback.js";
13
+ import type { ThreadController, ThreadSnapshot, ThreadDocument } from "./threads.js";
14
+
15
+ /** Presentations receive immutable snapshots; retained canvas frames never mount an owner. */
16
+ function Observed({
17
+ phase,
18
+ label,
19
+ children,
20
+ terminal = false,
21
+ }: {
22
+ phase: string;
23
+ label: string;
24
+ children: ReactNode;
25
+ terminal?: boolean;
26
+ }) {
27
+ const root = useRef<HTMLDivElement>(null);
28
+ const initiated = useRef(false);
29
+ useLayoutEffect(() => {
30
+ if (initiated.current && document.activeElement === document.body)
31
+ root.current?.focus({ preventScroll: true });
32
+ initiated.current = false;
33
+ }, [phase]);
34
+ return (
35
+ <CanvasStage phase={phase} label={label} terminal={terminal} frameClassName="max-w-2xl">
36
+ <div
37
+ ref={root}
38
+ tabIndex={-1}
39
+ role="group"
40
+ aria-label={label}
41
+ onPointerDownCapture={() => {
42
+ initiated.current = true;
43
+ }}
44
+ onKeyDownCapture={() => {
45
+ initiated.current = true;
46
+ }}
47
+ onClickCapture={() => {
48
+ initiated.current = true;
49
+ }}
50
+ className="mx-auto w-full max-w-2xl min-w-0 outline-none"
51
+ >
52
+ {children}
53
+ </div>
54
+ </CanvasStage>
55
+ );
56
+ }
57
+ export function AgentSupervision({
58
+ controller,
59
+ label,
60
+ }: {
61
+ controller: AgentController;
62
+ label?: string;
63
+ }) {
64
+ const copy = useInteractionCopy();
65
+ label ??= copy.agent;
66
+ return (
67
+ <CanvasSequence>
68
+ <AgentLive controller={controller} label={label} />
69
+ </CanvasSequence>
70
+ );
71
+ }
72
+ function AgentLive({ controller, label }: { controller: AgentController; label: string }) {
73
+ const copy = useInteractionCopy();
74
+
75
+ const state = useSyncExternalStore(
76
+ controller.subscribe,
77
+ controller.getSnapshot,
78
+ controller.getSnapshot,
79
+ );
80
+ return (
81
+ <Observed
82
+ phase={`agent-${state.completed.length}-${state.phase}`}
83
+ label={copy.agentStates[state.phase]}
84
+ terminal={state.phase === "complete" || state.phase === "denied"}
85
+ >
86
+ <AgentPanel state={state} label={label} controller={controller} />
87
+ </Observed>
88
+ );
89
+ }
90
+ function AgentPanel({
91
+ state,
92
+ label,
93
+ controller,
94
+ }: {
95
+ state: AgentSnapshot;
96
+ label: string;
97
+ controller: AgentController;
98
+ }) {
99
+ const copy = useInteractionCopy();
100
+
101
+ return (
102
+ <Card aria-label={label}>
103
+ <div className="flex flex-wrap items-center justify-between gap-3">
104
+ <h2 className="type-heading text-fg-emphasis">{label}</h2>
105
+ <span role="status" className="type-caption text-fg-subtle">
106
+ {copy.agentStates[state.phase]}
107
+ </span>
108
+ </div>
109
+ <ol aria-label={copy.plan} className="mt-4 flex flex-col gap-3">
110
+ {state.steps.map((step) => (
111
+ <li key={step.id} className="flex flex-wrap items-center justify-between gap-3 type-body">
112
+ <span>{step.title}</span>
113
+ <Badge
114
+ tone={
115
+ state.completed.some((result) => result.stepId === step.id) ? "success" : "neutral"
116
+ }
117
+ >
118
+ {state.completed.some((result) => result.stepId === step.id)
119
+ ? copy.toolStates.completed
120
+ : copy.toolStates.pending}
121
+ </Badge>
122
+ </li>
123
+ ))}
124
+ </ol>
125
+ {state.completed.map((result) => (
126
+ <ToolCall
127
+ key={result.requestId}
128
+ title={state.steps.find((step) => step.id === result.stepId)?.title ?? copy.tool}
129
+ status="completed"
130
+ input={state.steps.find((step) => step.id === result.stepId)?.input ?? null}
131
+ output={result.output}
132
+ />
133
+ ))}
134
+ {state.request && (
135
+ <div className="mt-6 flex flex-col gap-4">
136
+ <ToolCall
137
+ title={state.request.step.title}
138
+ status={
139
+ state.phase === "approval"
140
+ ? "approval"
141
+ : state.phase === "running"
142
+ ? "running"
143
+ : state.phase === "failed"
144
+ ? "rejected"
145
+ : "unknown"
146
+ }
147
+ input={state.request.step.input}
148
+ />
149
+ <p className="type-body">
150
+ <span className="type-label">{copy.scope}: </span>
151
+ {state.request.step.scope}
152
+ </p>
153
+ {state.phase === "approval" && (
154
+ <p className="type-caption text-fg-subtle">{copy.approvalHint}</p>
155
+ )}
156
+ </div>
157
+ )}
158
+ {state.progress && (
159
+ <p role="status" className="mt-4 type-body">
160
+ {state.progress}
161
+ </p>
162
+ )}
163
+ {state.phase === "uncertain" && (
164
+ <Callout tone="attention" className="mt-4">
165
+ {copy.unknown}
166
+ </Callout>
167
+ )}
168
+ {state.reason && (
169
+ <Callout tone="error" className="mt-4">
170
+ {state.reason}
171
+ </Callout>
172
+ )}
173
+ {state.phase === "expired" && (
174
+ <Callout tone="attention" className="mt-4">
175
+ {copy.expires}
176
+ </Callout>
177
+ )}
178
+ {state.phase === "denied" && <p className="mt-4 type-body">{copy.denied}</p>}
179
+ {state.phase === "complete" && <p className="mt-4 type-body">{copy.complete}</p>}
180
+ <div className="mt-6 flex flex-wrap gap-3">
181
+ {state.phase === "ready" && (
182
+ <Button onPress={() => controller.next()}>{copy.request}</Button>
183
+ )}
184
+ {state.phase === "approval" && (
185
+ <>
186
+ <Button variant="primary" onPress={() => controller.approve()}>
187
+ {copy.allow}
188
+ </Button>
189
+ <Button onPress={() => controller.deny()}>{copy.deny}</Button>
190
+ </>
191
+ )}
192
+ {state.phase === "running" && (
193
+ <Button onPress={() => controller.stop()}>{copy.stop}</Button>
194
+ )}
195
+ {state.phase === "uncertain" && (
196
+ <Button onPress={() => controller.check()}>{copy.check}</Button>
197
+ )}
198
+ {(state.phase === "failed" || state.phase === "expired") && (
199
+ <Button onPress={() => controller.renew()}>{copy.renew}</Button>
200
+ )}
201
+ {state.phase === "checking" && <Spinner />}
202
+ </div>
203
+ </Card>
204
+ );
205
+ }
206
+ export function ArtifactWorkspace({
207
+ controller,
208
+ label,
209
+ }: {
210
+ controller: ArtifactController;
211
+ label?: string;
212
+ }) {
213
+ const copy = useInteractionCopy();
214
+ label ??= copy.artifact;
215
+ return (
216
+ <CanvasSequence>
217
+ <ArtifactLive controller={controller} label={label} />
218
+ </CanvasSequence>
219
+ );
220
+ }
221
+ function ArtifactLive({ controller, label }: { controller: ArtifactController; label: string }) {
222
+ const copy = useInteractionCopy();
223
+
224
+ const state = useSyncExternalStore(
225
+ controller.subscribe,
226
+ controller.getSnapshot,
227
+ controller.getSnapshot,
228
+ );
229
+ return (
230
+ <Observed
231
+ phase={`artifact-${state.record?.revision ?? "idle"}-${state.phase}`}
232
+ label={copy.artifactStates[state.phase]}
233
+ >
234
+ <ArtifactPanel state={state} label={label} controller={controller} />
235
+ </Observed>
236
+ );
237
+ }
238
+ function ArtifactPanel({
239
+ state,
240
+ label,
241
+ controller,
242
+ }: {
243
+ state: ArtifactSnapshot;
244
+ label: string;
245
+ controller: ArtifactController;
246
+ }) {
247
+ const copy = useInteractionCopy();
248
+
249
+ const [revision, setRevision] = useState<string>();
250
+ const record = state.record;
251
+ const historical = record?.history.find((item) => item.revision === revision);
252
+ return (
253
+ <Card aria-label={label}>
254
+ <div className="flex flex-wrap items-center justify-between gap-3">
255
+ <h2 className="type-heading text-fg-emphasis">{record?.title ?? label}</h2>
256
+ <span role="status" className="type-caption text-fg-subtle">
257
+ {copy.artifactStates[state.phase]}
258
+ </span>
259
+ </div>
260
+ {(state.phase === "loading" || state.phase === "checking") && <Spinner />}
261
+ {state.phase === "idle" && <p className="mt-4 type-body">{copy.idleArtifact}</p>}
262
+ {state.phase === "load-error" && (
263
+ <Callout
264
+ tone="error"
265
+ className="mt-4"
266
+ actions={
267
+ <Button size="sm" onPress={() => state.loadId && controller.open(state.loadId)}>
268
+ {copy.retry}
269
+ </Button>
270
+ }
271
+ >
272
+ {copy.loadError}
273
+ </Callout>
274
+ )}
275
+ {record && (
276
+ <div className="mt-4 flex flex-col gap-4">
277
+ <Badge>
278
+ {copy.revision}: {record.revision}
279
+ </Badge>
280
+ {state.phase === "viewing" && record.history.length > 0 && (
281
+ <Select
282
+ label={copy.revision}
283
+ options={[
284
+ { id: record.revision, label: record.revision },
285
+ ...record.history
286
+ .filter((item) => item.revision !== record.revision)
287
+ .map((item) => ({ id: item.revision, label: item.revision })),
288
+ ]}
289
+ selectedKey={historical?.revision ?? record.revision}
290
+ onSelectionChange={(key) => setRevision(key === null ? undefined : String(key))}
291
+ />
292
+ )}
293
+ {state.phase === "conflict" && state.latest ? (
294
+ <>
295
+ <Callout tone="attention">{copy.conflict}</Callout>
296
+ <Tabs
297
+ label={copy.revision}
298
+ items={[
299
+ {
300
+ id: "draft",
301
+ label: copy.localDraft,
302
+ content: (
303
+ <pre className="type-body break-words whitespace-pre-wrap">{state.draft}</pre>
304
+ ),
305
+ },
306
+ {
307
+ id: "latest",
308
+ label: copy.latest,
309
+ content: (
310
+ <pre className="type-body break-words whitespace-pre-wrap">
311
+ {state.latest.content}
312
+ </pre>
313
+ ),
314
+ },
315
+ ]}
316
+ />
317
+ <div className="flex flex-wrap gap-3">
318
+ <Button onPress={() => controller.adoptLatest(false)}>{copy.useLatest}</Button>
319
+ <Button onPress={() => controller.adoptLatest(true)}>{copy.keepDraft}</Button>
320
+ </div>
321
+ </>
322
+ ) : state.phase === "viewing" ? (
323
+ <>
324
+ <pre className="type-body break-words whitespace-pre-wrap">
325
+ {historical?.content ?? record.content}
326
+ </pre>
327
+ {record.canEdit && !historical && (
328
+ <Button onPress={() => controller.edit(record.content)}>{copy.edit}</Button>
329
+ )}
330
+ </>
331
+ ) : (
332
+ <TextField
333
+ label={copy.artifactContent}
334
+ multiline
335
+ value={state.draft}
336
+ onChange={(value) => controller.edit(value)}
337
+ isDisabled={state.phase !== "editing"}
338
+ />
339
+ )}
340
+ {state.reason && <Callout tone="error">{state.reason}</Callout>}
341
+ {state.phase === "editing" && (
342
+ <div className="flex flex-wrap gap-3">
343
+ <Button
344
+ variant="primary"
345
+ isDisabled={state.draft === record.content}
346
+ onPress={() => controller.save()}
347
+ >
348
+ {copy.save}
349
+ </Button>
350
+ <Button onPress={() => controller.discard()}>{copy.discard}</Button>
351
+ </div>
352
+ )}
353
+ {state.phase === "unknown" && (
354
+ <Callout
355
+ tone="attention"
356
+ actions={<Button onPress={() => controller.check()}>{copy.check}</Button>}
357
+ >
358
+ {copy.unknown}
359
+ </Callout>
360
+ )}
361
+ </div>
362
+ )}
363
+ </Card>
364
+ );
365
+ }
366
+ export function ResponseFeedback({
367
+ controller,
368
+ label,
369
+ }: {
370
+ controller: FeedbackController;
371
+ label?: string;
372
+ }) {
373
+ const copy = useInteractionCopy();
374
+ label ??= copy.feedback;
375
+ const state = useSyncExternalStore(
376
+ controller.subscribe,
377
+ controller.getSnapshot,
378
+ controller.getSnapshot,
379
+ );
380
+ return <FeedbackPanel state={state} label={label} controller={controller} />;
381
+ }
382
+ function FeedbackPanel({
383
+ state,
384
+ label,
385
+ controller,
386
+ }: {
387
+ state: FeedbackSnapshot;
388
+ label: string;
389
+ controller: FeedbackController;
390
+ }) {
391
+ const copy = useInteractionCopy();
392
+
393
+ const locked = state.phase === "saving" || state.phase === "error";
394
+ return (
395
+ <Card aria-label={label}>
396
+ <RadioGroup
397
+ label={label}
398
+ options={[
399
+ { id: "helpful", label: copy.helpful },
400
+ { id: "unhelpful", label: copy.unhelpful },
401
+ ]}
402
+ value={state.rating ?? ""}
403
+ onChange={(value) => controller.edit(value as "helpful" | "unhelpful")}
404
+ isDisabled={locked}
405
+ />
406
+ <TextField
407
+ className="mt-4"
408
+ label={copy.comment}
409
+ multiline
410
+ value={state.comment}
411
+ onChange={(value) => controller.edit(state.rating, value)}
412
+ isDisabled={locked}
413
+ />
414
+ <div className="mt-4">
415
+ <Button
416
+ isDisabled={!state.rating || state.phase === "saving" || state.phase === "saved"}
417
+ onPress={() => controller.save()}
418
+ >
419
+ {state.phase === "error" ? copy.retry : copy.sendFeedback}
420
+ </Button>
421
+ </div>
422
+ <p role="status" className="mt-3 type-body text-fg-subtle">
423
+ {state.phase === "saved"
424
+ ? copy.savedFeedback
425
+ : state.phase === "error"
426
+ ? copy.failedFeedback
427
+ : state.phase === "saving"
428
+ ? copy.saving
429
+ : ""}
430
+ </p>
431
+ </Card>
432
+ );
433
+ }
434
+ export function ConversationHistory({
435
+ controller,
436
+ renderThread,
437
+ label,
438
+ }: {
439
+ controller: ThreadController;
440
+ renderThread: (thread: ThreadDocument) => ReactNode;
441
+ label?: string;
442
+ }) {
443
+ const copy = useInteractionCopy();
444
+ label ??= copy.threads;
445
+ const state = useSyncExternalStore(
446
+ controller.subscribe,
447
+ controller.getSnapshot,
448
+ controller.getSnapshot,
449
+ );
450
+ return (
451
+ <HistoryPanel state={state} controller={controller} renderThread={renderThread} label={label} />
452
+ );
453
+ }
454
+ function HistoryPanel({
455
+ state,
456
+ controller,
457
+ renderThread,
458
+ label,
459
+ }: {
460
+ state: ThreadSnapshot;
461
+ controller: ThreadController;
462
+ renderThread: (thread: ThreadDocument) => ReactNode;
463
+ label: string;
464
+ }) {
465
+ const copy = useInteractionCopy();
466
+
467
+ const [view, setView] = useState("active");
468
+ const [editing, setEditing] = useState<string>();
469
+ const [title, setTitle] = useState("");
470
+ const root = useRef<HTMLDivElement>(null);
471
+ const busy = !["ready", "error"].includes(state.phase);
472
+ const items = state.items.filter((item) => item.archived === (view === "archived"));
473
+ return (
474
+ <div ref={root} role="group" aria-label={label} tabIndex={-1} className="min-w-0 outline-none">
475
+ <Card aria-label={label}>
476
+ <div className="flex flex-wrap items-center justify-between gap-3">
477
+ <h2 className="type-heading text-fg-emphasis">{label}</h2>
478
+ <Button
479
+ size="sm"
480
+ isDisabled={busy && state.phase !== "idle"}
481
+ onPress={() => controller.refresh()}
482
+ >
483
+ {copy.refresh}
484
+ </Button>
485
+ </div>
486
+ <Select
487
+ className="mt-4"
488
+ label={label}
489
+ selectedKey={view}
490
+ onSelectionChange={(key) => setView(String(key))}
491
+ options={[
492
+ { id: "active", label: copy.active },
493
+ { id: "archived", label: copy.archived },
494
+ ]}
495
+ />
496
+ {state.phase === "error" && (
497
+ <Callout tone="error" className="mt-4">
498
+ {copy.threadError}
499
+ </Callout>
500
+ )}
501
+ <ul className="mt-4 flex flex-col gap-4">
502
+ {items.map((item) => (
503
+ <li
504
+ key={item.id}
505
+ className="flex flex-col gap-3 rounded-control border border-line-subtle p-3"
506
+ >
507
+ <Button
508
+ className="justify-start text-left whitespace-normal"
509
+ isDisabled={["listing", "mutating"].includes(state.phase)}
510
+ onPress={() => {
511
+ if (controller.open(item.id)) setEditing(undefined);
512
+ }}
513
+ >
514
+ {item.title}
515
+ </Button>
516
+ <div className="flex flex-wrap gap-3">
517
+ <Button
518
+ size="sm"
519
+ isDisabled={busy}
520
+ onPress={() => {
521
+ setEditing(item.id);
522
+ setTitle(item.title);
523
+ }}
524
+ >
525
+ {copy.rename}
526
+ </Button>
527
+ <Button
528
+ size="sm"
529
+ isDisabled={busy}
530
+ onPress={() => {
531
+ // Archiving removes this row after confirmation; keep the visitor in the list.
532
+ if (controller.archive(item.id, !item.archived))
533
+ root.current?.focus({ preventScroll: true });
534
+ }}
535
+ >
536
+ {item.archived ? copy.restore : copy.archive}
537
+ </Button>
538
+ </div>
539
+ {editing === item.id && (
540
+ <div className="flex flex-col gap-3">
541
+ <TextField
542
+ label={copy.title}
543
+ value={title}
544
+ onChange={setTitle}
545
+ isDisabled={busy}
546
+ />
547
+ <Button
548
+ isDisabled={busy || !title.trim()}
549
+ onPress={() => controller.rename(item.id, title)}
550
+ >
551
+ {copy.saveTitle}
552
+ </Button>
553
+ </div>
554
+ )}
555
+ </li>
556
+ ))}
557
+ </ul>
558
+ {!items.length && state.phase !== "idle" && (
559
+ <p className="mt-4 type-body text-fg-subtle">{copy.noThreads}</p>
560
+ )}
561
+ <div role="status" className="mt-4 type-caption text-fg-subtle">
562
+ {busy && state.phase !== "idle" ? copy.loading : ""}
563
+ </div>
564
+ <div className="mt-6 min-w-0">
565
+ {state.selected ? (
566
+ renderThread(state.selected)
567
+ ) : (
568
+ <p className="type-body text-fg-subtle">{copy.chooseThread}</p>
569
+ )}
570
+ </div>
571
+ </Card>
572
+ </div>
573
+ );
574
+ }