@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.
- package/GENERATIVE.md +101 -0
- package/INTERACTIONS.md +51 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/REUSE.md +170 -0
- package/REVIEW.md +68 -0
- package/THIRD-PARTY-NOTICES.md +28 -0
- package/ai-sdk/index.js +96 -0
- package/assets/ibm-plex-mono-cyrillic-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-cyrillic-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-cyrillic-ext-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-cyrillic-ext-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-vietnamese-400-normal.woff2 +0 -0
- package/assets/ibm-plex-mono-vietnamese-500-normal.woff2 +0 -0
- package/assets/instrument-sans-latin-ext-wght-normal.woff2 +0 -0
- package/assets/instrument-sans-latin-wght-normal.woff2 +0 -0
- package/assistant-ui/index.js +139 -0
- package/chunks/callout-DKMHSnjc.js +342 -0
- package/chunks/canvas-BJ15-XxE.js +598 -0
- package/chunks/controller-Cfpg0C7v.js +180 -0
- package/chunks/core-BCqVRLUy.js +312 -0
- package/chunks/text-field-BLtoaZFn.js +41 -0
- package/chunks/types-BGF7r1my.js +115 -0
- package/chunks/ui-9DyNsYXY.js +967 -0
- package/core/index.js +4 -0
- package/custom/index.js +64 -0
- package/examples/ai-sdk-backend/package.json +11 -0
- package/examples/ai-sdk-backend/server.mjs +316 -0
- package/examples/custom-backend/copy.mjs +7 -0
- package/examples/custom-backend/server.mjs +118 -0
- package/examples/generative/specs.json +2094 -0
- package/examples/review-backend/copy.mjs +27 -0
- package/examples/review-backend/package.json +11 -0
- package/examples/review-backend/server.mjs +276 -0
- package/examples/web-ai-sdk/index.html +12 -0
- package/examples/web-ai-sdk/package.json +21 -0
- package/examples/web-ai-sdk/src/copy.ts +1 -0
- package/examples/web-ai-sdk/src/main.tsx +64 -0
- package/examples/web-ai-sdk/tsconfig.json +13 -0
- package/examples/web-generative/index.html +12 -0
- package/examples/web-generative/package.json +23 -0
- package/examples/web-generative/src/copy.ts +42 -0
- package/examples/web-generative/src/main.tsx +74 -0
- package/examples/web-generative/tsconfig.json +14 -0
- package/examples/web-independent/index.html +12 -0
- package/examples/web-independent/package.json +20 -0
- package/examples/web-independent/src/main.tsx +32 -0
- package/examples/web-independent/tsconfig.json +14 -0
- package/examples/web-interactions/index.html +12 -0
- package/examples/web-interactions/package.json +21 -0
- package/examples/web-interactions/src/copy.ts +215 -0
- package/examples/web-interactions/src/examples.tsx +628 -0
- package/examples/web-interactions/src/main.tsx +116 -0
- package/examples/web-interactions/tsconfig.json +14 -0
- package/examples/web-review/index.html +12 -0
- package/examples/web-review/package.json +20 -0
- package/examples/web-review/src/main.tsx +59 -0
- package/examples/web-review/tsconfig.json +14 -0
- package/fonts.css +117 -0
- package/generative/index.js +1560 -0
- package/interactions/index.js +1591 -0
- package/langgraph-review/index.js +36 -0
- package/licenses/ai-sdk.txt +13 -0
- package/licenses/assistant-ui-generative.txt +21 -0
- package/licenses/assistant-ui.txt +23 -0
- package/licenses/ibm-plex-mono.txt +93 -0
- package/licenses/instrument-sans.txt +93 -0
- package/licenses/internationalized-date.txt +201 -0
- package/licenses/langgraph.txt +21 -0
- package/licenses/lucide.txt +43 -0
- package/licenses/react-markdown.txt +21 -0
- package/licenses/remark-gfm.txt +22 -0
- package/licenses/zod.txt +21 -0
- package/package.json +138 -0
- package/review/index.js +31 -0
- package/source/components/avatar.tsx +50 -0
- package/source/components/button.tsx +155 -0
- package/source/components/callout.tsx +65 -0
- package/source/components/canvas/connector.tsx +14 -0
- package/source/components/canvas/copy.ts +34 -0
- package/source/components/canvas/index.tsx +257 -0
- package/source/components/canvas/recorder.ts +36 -0
- package/source/components/date-time-field.tsx +77 -0
- package/source/components/flow-motion.tsx +109 -0
- package/source/components/icons.tsx +275 -0
- package/source/components/responsive-overlay.tsx +91 -0
- package/source/components/scenario-picker/copy.ts +5 -0
- package/source/components/scenario-picker/index.tsx +142 -0
- package/source/components/scroll-area.tsx +59 -0
- package/source/components/search-picker.tsx +127 -0
- package/source/components/segmented-control.tsx +123 -0
- package/source/components/skeleton.tsx +11 -0
- package/source/components/spinner.tsx +19 -0
- package/source/components/superxd-arrow.tsx +22 -0
- package/source/components/switch.tsx +45 -0
- package/source/components/text-field.tsx +81 -0
- package/source/components/thinking.tsx +16 -0
- package/source/components/ui/copy.ts +12 -0
- package/source/components/ui/display.tsx +276 -0
- package/source/components/ui/navigation.tsx +346 -0
- package/source/components/ui/portal-theme.tsx +28 -0
- package/source/components/ui/selection.tsx +336 -0
- package/source/lib/cn.ts +23 -0
- package/source/reusable/ai-sdk/index.ts +114 -0
- package/source/reusable/assistant-ui/index.tsx +266 -0
- package/source/reusable/core/attachments.ts +163 -0
- package/source/reusable/core/index.ts +417 -0
- package/source/reusable/custom/index.ts +90 -0
- package/source/reusable/generative/actions.ts +188 -0
- package/source/reusable/generative/copy.ts +47 -0
- package/source/reusable/generative/data.tsx +126 -0
- package/source/reusable/generative/index.tsx +517 -0
- package/source/reusable/generative/schema.ts +327 -0
- package/source/reusable/generative/timeline.tsx +214 -0
- package/source/reusable/generative/vocabulary.tsx +847 -0
- package/source/reusable/interactions/agent.ts +259 -0
- package/source/reusable/interactions/artifact.ts +236 -0
- package/source/reusable/interactions/context.tsx +38 -0
- package/source/reusable/interactions/copy.ts +117 -0
- package/source/reusable/interactions/feedback.ts +73 -0
- package/source/reusable/interactions/index.ts +11 -0
- package/source/reusable/interactions/presentation.tsx +483 -0
- package/source/reusable/interactions/store.ts +74 -0
- package/source/reusable/interactions/threads.ts +126 -0
- package/source/reusable/interactions/web.tsx +574 -0
- package/source/reusable/langgraph-review/index.ts +73 -0
- package/source/reusable/review/controller.ts +263 -0
- package/source/reusable/review/index.ts +47 -0
- package/source/reusable/review/types.ts +189 -0
- package/source/reusable/ui/index.ts +25 -0
- package/source/reusable/uploads/index.ts +48 -0
- package/source/reusable/web/attachments/copy.ts +23 -0
- package/source/reusable/web/attachments/index.tsx +233 -0
- package/source/reusable/web/copy.ts +22 -0
- package/source/reusable/web/index.tsx +470 -0
- package/source/reusable/web/message/copy.ts +26 -0
- package/source/reusable/web/message/index.tsx +388 -0
- package/source/reusable/web/message/streaming.ts +64 -0
- package/source/reusable/web/message/styles.css +21 -0
- package/source/reusable/web/review/copy.ts +78 -0
- package/source/reusable/web/review/flow.ts +24 -0
- package/source/reusable/web/review/index.tsx +506 -0
- package/source/reusable/web/styles.ts +2 -0
- package/source/styles/library.css +612 -0
- package/source/styles/theme.css +228 -0
- package/styles.css +3 -0
- package/styles.d.ts +1 -0
- package/types/components/avatar.d.ts +14 -0
- package/types/components/button.d.ts +36 -0
- package/types/components/callout.d.ts +19 -0
- package/types/components/canvas/connector.d.ts +2 -0
- package/types/components/canvas/copy.d.ts +34 -0
- package/types/components/canvas/index.d.ts +33 -0
- package/types/components/canvas/recorder.d.ts +10 -0
- package/types/components/date-time-field.d.ts +11 -0
- package/types/components/flow-motion.d.ts +38 -0
- package/types/components/icons.d.ts +138 -0
- package/types/components/responsive-overlay.d.ts +15 -0
- package/types/components/scenario-picker/copy.d.ts +5 -0
- package/types/components/scenario-picker/index.d.ts +14 -0
- package/types/components/scroll-area.d.ts +14 -0
- package/types/components/search-picker.d.ts +27 -0
- package/types/components/segmented-control.d.ts +23 -0
- package/types/components/skeleton.d.ts +4 -0
- package/types/components/spinner.d.ts +4 -0
- package/types/components/superxd-arrow.d.ts +5 -0
- package/types/components/switch.d.ts +8 -0
- package/types/components/text-field.d.ts +19 -0
- package/types/components/thinking.d.ts +8 -0
- package/types/components/ui/copy.d.ts +12 -0
- package/types/components/ui/display.d.ts +50 -0
- package/types/components/ui/navigation.d.ts +63 -0
- package/types/components/ui/portal-theme.d.ts +8 -0
- package/types/components/ui/selection.d.ts +37 -0
- package/types/lib/cn.d.ts +3 -0
- package/types/reusable/ai-sdk/index.d.ts +15 -0
- package/types/reusable/assistant-ui/index.d.ts +12 -0
- package/types/reusable/core/attachments.d.ts +54 -0
- package/types/reusable/core/index.d.ts +107 -0
- package/types/reusable/custom/index.d.ts +14 -0
- package/types/reusable/generative/actions.d.ts +45 -0
- package/types/reusable/generative/copy.d.ts +48 -0
- package/types/reusable/generative/data.d.ts +4 -0
- package/types/reusable/generative/index.d.ts +30 -0
- package/types/reusable/generative/schema.d.ts +303 -0
- package/types/reusable/generative/timeline.d.ts +21 -0
- package/types/reusable/generative/vocabulary.d.ts +34 -0
- package/types/reusable/interactions/agent.d.ts +98 -0
- package/types/reusable/interactions/artifact.d.ts +118 -0
- package/types/reusable/interactions/context.d.ts +130 -0
- package/types/reusable/interactions/copy.d.ts +115 -0
- package/types/reusable/interactions/feedback.d.ts +34 -0
- package/types/reusable/interactions/index.d.ts +10 -0
- package/types/reusable/interactions/presentation.d.ts +97 -0
- package/types/reusable/interactions/store.d.ts +29 -0
- package/types/reusable/interactions/threads.d.ts +38 -0
- package/types/reusable/interactions/web.d.ts +22 -0
- package/types/reusable/langgraph-review/index.d.ts +5 -0
- package/types/reusable/review/controller.d.ts +52 -0
- package/types/reusable/review/index.d.ts +12 -0
- package/types/reusable/review/types.d.ts +61 -0
- package/types/reusable/ui/index.d.ts +19 -0
- package/types/reusable/uploads/index.d.ts +11 -0
- package/types/reusable/web/attachments/copy.d.ts +25 -0
- package/types/reusable/web/attachments/index.d.ts +24 -0
- package/types/reusable/web/copy.d.ts +24 -0
- package/types/reusable/web/index.d.ts +41 -0
- package/types/reusable/web/message/copy.d.ts +27 -0
- package/types/reusable/web/message/index.d.ts +56 -0
- package/types/reusable/web/message/streaming.d.ts +27 -0
- package/types/reusable/web/review/copy.d.ts +72 -0
- package/types/reusable/web/review/flow.d.ts +3 -0
- package/types/reusable/web/review/index.d.ts +38 -0
- package/types/reusable/web/styles.d.ts +2 -0
- package/ui/index.js +4 -0
- package/uploads/index.js +34 -0
- package/web/index.js +931 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { Children, isValidElement, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
2
|
+
import Markdown, { type Components } from "react-markdown";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
4
|
+
import { Link, Label, TextArea, TextField } from "react-aria-components";
|
|
5
|
+
import { Button } from "../../../components/button.js";
|
|
6
|
+
import {
|
|
7
|
+
IconCheck,
|
|
8
|
+
IconCopy,
|
|
9
|
+
IconChevronLeft,
|
|
10
|
+
IconChevronRight,
|
|
11
|
+
} from "../../../components/icons.js";
|
|
12
|
+
import { cn } from "../../../lib/cn.js";
|
|
13
|
+
import type { ChatBranch } from "../../core/index.js";
|
|
14
|
+
import { messageCopy, type MessageCopy } from "./copy.js";
|
|
15
|
+
import { rehypeStreamingTail } from "./streaming.js";
|
|
16
|
+
export { messageCopy } from "./copy.js";
|
|
17
|
+
export type { MessageCopy } from "./copy.js";
|
|
18
|
+
|
|
19
|
+
export interface CopyButtonProps {
|
|
20
|
+
text: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
iconOnly?: boolean;
|
|
23
|
+
isDisabled?: boolean;
|
|
24
|
+
copy?: MessageCopy;
|
|
25
|
+
/** Consumer-owned clipboard I/O. Rejections never report success. */
|
|
26
|
+
writeText?: (text: string) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export function CopyButton({
|
|
29
|
+
text,
|
|
30
|
+
label,
|
|
31
|
+
iconOnly = false,
|
|
32
|
+
isDisabled,
|
|
33
|
+
copy = messageCopy,
|
|
34
|
+
writeText,
|
|
35
|
+
}: CopyButtonProps) {
|
|
36
|
+
const [result, setResult] = useState<{
|
|
37
|
+
text: string;
|
|
38
|
+
phase: "idle" | "pending" | "copied" | "error";
|
|
39
|
+
}>({ text, phase: "idle" });
|
|
40
|
+
const generation = useRef(0);
|
|
41
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
42
|
+
const phase = result.text === text ? result.phase : "idle";
|
|
43
|
+
useEffect(
|
|
44
|
+
() => () => {
|
|
45
|
+
++generation.current;
|
|
46
|
+
clearTimeout(timer.current);
|
|
47
|
+
},
|
|
48
|
+
[text],
|
|
49
|
+
);
|
|
50
|
+
const onCopy = async () => {
|
|
51
|
+
const token = ++generation.current;
|
|
52
|
+
clearTimeout(timer.current);
|
|
53
|
+
setResult({ text, phase: "pending" });
|
|
54
|
+
try {
|
|
55
|
+
if (writeText) await writeText(text);
|
|
56
|
+
else {
|
|
57
|
+
if (!navigator.clipboard) throw new Error("Clipboard unavailable");
|
|
58
|
+
await navigator.clipboard.writeText(text);
|
|
59
|
+
}
|
|
60
|
+
if (token !== generation.current) return;
|
|
61
|
+
setResult({ text, phase: "copied" });
|
|
62
|
+
timer.current = setTimeout(() => {
|
|
63
|
+
if (token === generation.current) setResult({ text, phase: "idle" });
|
|
64
|
+
}, 2500);
|
|
65
|
+
} catch {
|
|
66
|
+
if (token === generation.current) setResult({ text, phase: "error" });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const word =
|
|
70
|
+
phase === "copied" ? copy.copied : phase === "pending" ? copy.copying : (label ?? copy.copy);
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
73
|
+
<Button
|
|
74
|
+
variant="ghost"
|
|
75
|
+
size={iconOnly ? "icon-sm" : "sm"}
|
|
76
|
+
aria-label={word}
|
|
77
|
+
isDisabled={isDisabled || !text || phase === "pending"}
|
|
78
|
+
onPress={onCopy}
|
|
79
|
+
>
|
|
80
|
+
{phase === "copied" ? <IconCheck /> : <IconCopy />}
|
|
81
|
+
{!iconOnly ? word : null}
|
|
82
|
+
</Button>
|
|
83
|
+
<span
|
|
84
|
+
role="status"
|
|
85
|
+
aria-live="polite"
|
|
86
|
+
className={phase === "error" ? "type-caption text-fg-subtle" : "sr-only"}
|
|
87
|
+
>
|
|
88
|
+
{phase === "error" ? copy.copyFailed : phase === "copied" ? copy.copied : ""}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
export interface CodeBlockProps extends Pick<CopyButtonProps, "writeText" | "copy"> {
|
|
94
|
+
code: string;
|
|
95
|
+
language?: string;
|
|
96
|
+
className?: string;
|
|
97
|
+
isStreaming?: boolean;
|
|
98
|
+
}
|
|
99
|
+
/** Code is readable/copyable text, never executed or embedded. */
|
|
100
|
+
export function CodeBlock({
|
|
101
|
+
code,
|
|
102
|
+
language,
|
|
103
|
+
className,
|
|
104
|
+
isStreaming,
|
|
105
|
+
writeText,
|
|
106
|
+
copy = messageCopy,
|
|
107
|
+
}: CodeBlockProps) {
|
|
108
|
+
const title = language?.trim().slice(0, 50) || copy.code;
|
|
109
|
+
return (
|
|
110
|
+
<div
|
|
111
|
+
className={cn(
|
|
112
|
+
"reusable-code min-w-0 overflow-hidden rounded-control border border-line-subtle bg-surface-muted",
|
|
113
|
+
className,
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
116
|
+
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-line-subtle px-4 py-2">
|
|
117
|
+
<span className="font-mono type-caption text-fg-subtle">{title}</span>
|
|
118
|
+
<CopyButton
|
|
119
|
+
text={code}
|
|
120
|
+
label={copy.copyCode}
|
|
121
|
+
isDisabled={isStreaming}
|
|
122
|
+
writeText={writeText}
|
|
123
|
+
copy={copy}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
<pre
|
|
127
|
+
tabIndex={0}
|
|
128
|
+
role="group"
|
|
129
|
+
aria-label={`${copy.code}: ${title}`}
|
|
130
|
+
className="m-0 max-w-full overflow-x-auto rounded-cell px-4 py-control-y font-mono type-caption text-fg-emphasis outline-none focus-visible:focus-ring"
|
|
131
|
+
>
|
|
132
|
+
<code>{code}</code>
|
|
133
|
+
</pre>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
export function safeMarkdownUrl(url: string): string {
|
|
138
|
+
if (
|
|
139
|
+
/^[#]/.test(url) ||
|
|
140
|
+
(/^\/(?!\/)/.test(url) &&
|
|
141
|
+
!url.includes("\\") &&
|
|
142
|
+
!Array.from(url).some((char) => char.charCodeAt(0) <= 32))
|
|
143
|
+
)
|
|
144
|
+
return url;
|
|
145
|
+
try {
|
|
146
|
+
const parsed = new URL(url);
|
|
147
|
+
if (
|
|
148
|
+
(["https:", "http:"].includes(parsed.protocol) && !parsed.username && !parsed.password) ||
|
|
149
|
+
parsed.protocol === "mailto:"
|
|
150
|
+
)
|
|
151
|
+
return url;
|
|
152
|
+
} catch {
|
|
153
|
+
/* Untrusted or incomplete streaming URL stays plain text. */
|
|
154
|
+
}
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
export interface MessageMarkdownProps extends Pick<
|
|
158
|
+
CodeBlockProps,
|
|
159
|
+
"copy" | "writeText" | "isStreaming"
|
|
160
|
+
> {
|
|
161
|
+
text: string;
|
|
162
|
+
className?: string;
|
|
163
|
+
/** The answer is arriving now: tint the newest words and show the cursor. */
|
|
164
|
+
streamingTail?: boolean;
|
|
165
|
+
}
|
|
166
|
+
/** Established CommonMark/GFM parser, without raw HTML, remote images or executable previews. */
|
|
167
|
+
export function MessageMarkdown({
|
|
168
|
+
text,
|
|
169
|
+
className,
|
|
170
|
+
copy = messageCopy,
|
|
171
|
+
writeText,
|
|
172
|
+
isStreaming,
|
|
173
|
+
streamingTail = false,
|
|
174
|
+
}: MessageMarkdownProps) {
|
|
175
|
+
const prefix = useId().replace(/[^\w-]/g, "");
|
|
176
|
+
// Stable renderers preserve code-copy feedback when the host scrolls or rerenders.
|
|
177
|
+
const components = useMemo<Components>(
|
|
178
|
+
() => ({
|
|
179
|
+
a: ({ href, children, id, ...props }) =>
|
|
180
|
+
href ? (
|
|
181
|
+
<Link
|
|
182
|
+
href={href}
|
|
183
|
+
id={id}
|
|
184
|
+
aria-label={props["aria-label"]}
|
|
185
|
+
aria-describedby={
|
|
186
|
+
props["aria-describedby"] === "footnote-label"
|
|
187
|
+
? `sxd-${prefix}-notes`
|
|
188
|
+
: props["aria-describedby"]
|
|
189
|
+
}
|
|
190
|
+
className="rounded-cell text-fg-emphasis underline decoration-line-emphasis underline-offset-4 outline-none data-focus-visible:focus-ring"
|
|
191
|
+
>
|
|
192
|
+
{children}
|
|
193
|
+
</Link>
|
|
194
|
+
) : (
|
|
195
|
+
<span>{children}</span>
|
|
196
|
+
),
|
|
197
|
+
img: ({ alt }) => <span>{alt}</span>,
|
|
198
|
+
h2: ({ id, children, className }) => (
|
|
199
|
+
<h2 id={id === "footnote-label" ? `sxd-${prefix}-notes` : id} className={className}>
|
|
200
|
+
{children}
|
|
201
|
+
</h2>
|
|
202
|
+
),
|
|
203
|
+
pre: ({ children }) => {
|
|
204
|
+
const child = Children.toArray(children).find(isValidElement);
|
|
205
|
+
const props = child?.props as { children?: string; className?: string } | undefined;
|
|
206
|
+
return (
|
|
207
|
+
<CodeBlock
|
|
208
|
+
code={String(props?.children ?? "").replace(/\n$/, "")}
|
|
209
|
+
language={props?.className?.replace(/^language-/, "")}
|
|
210
|
+
copy={copy}
|
|
211
|
+
writeText={writeText}
|
|
212
|
+
isStreaming={isStreaming}
|
|
213
|
+
/>
|
|
214
|
+
);
|
|
215
|
+
},
|
|
216
|
+
table: ({ children }) => (
|
|
217
|
+
<div
|
|
218
|
+
role="group"
|
|
219
|
+
tabIndex={0}
|
|
220
|
+
aria-label={copy.table}
|
|
221
|
+
className="max-w-full overflow-x-auto rounded-control border border-line-subtle outline-none focus-visible:focus-ring"
|
|
222
|
+
>
|
|
223
|
+
<table className="w-full border-collapse type-caption">{children}</table>
|
|
224
|
+
</div>
|
|
225
|
+
),
|
|
226
|
+
}),
|
|
227
|
+
[copy, prefix, writeText, isStreaming],
|
|
228
|
+
);
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
className={cn(
|
|
232
|
+
"conversation-markdown reusable-markdown min-w-0 type-body whitespace-normal text-fg-emphasis",
|
|
233
|
+
className,
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
<Markdown
|
|
237
|
+
skipHtml
|
|
238
|
+
remarkPlugins={[remarkGfm]}
|
|
239
|
+
rehypePlugins={streamingTail ? [rehypeStreamingTail] : []}
|
|
240
|
+
urlTransform={safeMarkdownUrl}
|
|
241
|
+
remarkRehypeOptions={{
|
|
242
|
+
clobberPrefix: `sxd-${prefix}-`,
|
|
243
|
+
footnoteLabel: copy.footnotes,
|
|
244
|
+
footnoteBackLabel: copy.back,
|
|
245
|
+
}}
|
|
246
|
+
components={components}
|
|
247
|
+
>
|
|
248
|
+
{text}
|
|
249
|
+
</Markdown>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* A streaming answer, like assistant-ui's streaming text: the newest words arrive tinted in
|
|
255
|
+
* the info colour and fade back to plain text as more arrive; the cursor follows the
|
|
256
|
+
* last word. When the stream ends, it's ordinary text.
|
|
257
|
+
*/
|
|
258
|
+
export function StreamingMessage(
|
|
259
|
+
props: Omit<MessageMarkdownProps, "isStreaming" | "streamingTail">,
|
|
260
|
+
) {
|
|
261
|
+
return <MessageMarkdown {...props} isStreaming streamingTail />;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface BranchPickerProps {
|
|
265
|
+
branch: ChatBranch;
|
|
266
|
+
isDisabled?: boolean;
|
|
267
|
+
onPrevious?: () => void;
|
|
268
|
+
onNext?: () => void;
|
|
269
|
+
copy?: MessageCopy;
|
|
270
|
+
}
|
|
271
|
+
export function BranchPicker({
|
|
272
|
+
branch,
|
|
273
|
+
isDisabled,
|
|
274
|
+
onPrevious,
|
|
275
|
+
onNext,
|
|
276
|
+
copy = messageCopy,
|
|
277
|
+
}: BranchPickerProps) {
|
|
278
|
+
if (branch.count < 2) return null;
|
|
279
|
+
return (
|
|
280
|
+
<div role="group" aria-label={copy.versions} className="flex items-center gap-1">
|
|
281
|
+
{onPrevious ? (
|
|
282
|
+
<Button
|
|
283
|
+
size="icon-sm"
|
|
284
|
+
variant="ghost"
|
|
285
|
+
aria-label={copy.previous}
|
|
286
|
+
isDisabled={isDisabled || branch.index < 1}
|
|
287
|
+
onPress={onPrevious}
|
|
288
|
+
>
|
|
289
|
+
<IconChevronLeft />
|
|
290
|
+
</Button>
|
|
291
|
+
) : null}
|
|
292
|
+
<span
|
|
293
|
+
aria-label={`${copy.version} ${branch.index + 1} / ${branch.count}`}
|
|
294
|
+
className="font-mono type-caption text-fg-subtle numeric"
|
|
295
|
+
>
|
|
296
|
+
{branch.index + 1} / {branch.count}
|
|
297
|
+
</span>
|
|
298
|
+
{onNext ? (
|
|
299
|
+
<Button
|
|
300
|
+
size="icon-sm"
|
|
301
|
+
variant="ghost"
|
|
302
|
+
aria-label={copy.next}
|
|
303
|
+
isDisabled={isDisabled || branch.index >= branch.count - 1}
|
|
304
|
+
onPress={onNext}
|
|
305
|
+
>
|
|
306
|
+
<IconChevronRight />
|
|
307
|
+
</Button>
|
|
308
|
+
) : null}
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
export interface MessageEditorProps {
|
|
313
|
+
text: string;
|
|
314
|
+
/** Enable only for an editor opened by the visitor. Specimens do not move focus. */
|
|
315
|
+
autoFocus?: boolean;
|
|
316
|
+
hasFiles?: boolean;
|
|
317
|
+
isDisabled?: boolean;
|
|
318
|
+
onSave: (text: string) => boolean;
|
|
319
|
+
onCancel: () => void;
|
|
320
|
+
copy?: MessageCopy;
|
|
321
|
+
}
|
|
322
|
+
export function MessageEditor({
|
|
323
|
+
text,
|
|
324
|
+
autoFocus = false,
|
|
325
|
+
hasFiles,
|
|
326
|
+
isDisabled,
|
|
327
|
+
onSave,
|
|
328
|
+
onCancel,
|
|
329
|
+
copy = messageCopy,
|
|
330
|
+
}: MessageEditorProps) {
|
|
331
|
+
const [draft, setDraft] = useState(text);
|
|
332
|
+
const [failed, setFailed] = useState(false);
|
|
333
|
+
const input = useRef<HTMLTextAreaElement>(null);
|
|
334
|
+
const hint = useId();
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (autoFocus) input.current?.focus({ preventScroll: true });
|
|
337
|
+
}, [autoFocus]);
|
|
338
|
+
const save = () => {
|
|
339
|
+
if (!onSave(draft)) setFailed(true);
|
|
340
|
+
};
|
|
341
|
+
return (
|
|
342
|
+
<div className="flex w-full min-w-0 flex-col gap-3 rounded-surface border border-line-input bg-surface p-4 shadow-control">
|
|
343
|
+
<TextField
|
|
344
|
+
value={draft}
|
|
345
|
+
onChange={(value) => {
|
|
346
|
+
setDraft(value);
|
|
347
|
+
setFailed(false);
|
|
348
|
+
}}
|
|
349
|
+
isDisabled={isDisabled}
|
|
350
|
+
>
|
|
351
|
+
<Label className="sr-only">{copy.editInput}</Label>
|
|
352
|
+
<TextArea
|
|
353
|
+
ref={input}
|
|
354
|
+
rows={3}
|
|
355
|
+
maxLength={20_000}
|
|
356
|
+
aria-describedby={hint}
|
|
357
|
+
onKeyDown={(event) => {
|
|
358
|
+
if (event.key === "Escape" && !event.nativeEvent.isComposing) {
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
event.stopPropagation();
|
|
361
|
+
onCancel();
|
|
362
|
+
}
|
|
363
|
+
}}
|
|
364
|
+
className="max-h-48 min-h-24 w-full resize-y rounded-control border border-line-input bg-surface px-control-md-x py-control-y type-body text-fg-emphasis outline-none focus-visible:focus-ring"
|
|
365
|
+
/>
|
|
366
|
+
</TextField>
|
|
367
|
+
<p id={hint} className="type-caption text-fg-subtle">
|
|
368
|
+
{copy.editNote}
|
|
369
|
+
</p>
|
|
370
|
+
<p role="status" className={failed ? "type-caption text-error" : "sr-only"}>
|
|
371
|
+
{failed ? copy.editFailed : ""}
|
|
372
|
+
</p>
|
|
373
|
+
<div className="flex flex-wrap justify-end gap-2">
|
|
374
|
+
<Button size="sm" onPress={onCancel}>
|
|
375
|
+
{copy.cancel}
|
|
376
|
+
</Button>
|
|
377
|
+
<Button
|
|
378
|
+
size="sm"
|
|
379
|
+
variant="primary"
|
|
380
|
+
isDisabled={isDisabled || draft === text || (!draft.trim() && !hasFiles)}
|
|
381
|
+
onPress={save}
|
|
382
|
+
>
|
|
383
|
+
{copy.save}
|
|
384
|
+
</Button>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* The few syntax-tree shapes this needs, described here so the source recipes need no extra
|
|
2
|
+
package. They match react-markdown's HTML tree (hast). */
|
|
3
|
+
interface Text {
|
|
4
|
+
type: "text";
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
interface Element {
|
|
8
|
+
type: "element";
|
|
9
|
+
tagName: string;
|
|
10
|
+
properties: Record<string, unknown>;
|
|
11
|
+
children: Node[];
|
|
12
|
+
}
|
|
13
|
+
type Node = Text | Element | { type: string; children?: Node[] };
|
|
14
|
+
interface Root {
|
|
15
|
+
type: "root";
|
|
16
|
+
children: Node[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Inside these, text is left exactly as written (code is never split into words). */
|
|
20
|
+
const SKIP = new Set(["code", "pre"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* For an answer that is still arriving: wraps each word so the newest ones can be tinted, and
|
|
24
|
+
* puts the cursor after the last word. The tint fades back to plain text (see library.css) as
|
|
25
|
+
* newer words arrive. Modelled on assistant-ui's streaming text.
|
|
26
|
+
*/
|
|
27
|
+
export function rehypeStreamingTail({ fresh = 2 }: { fresh?: number } = {}) {
|
|
28
|
+
return (tree: Root) => {
|
|
29
|
+
const words: Element[] = [];
|
|
30
|
+
const wrap = (node: Root | Element) => {
|
|
31
|
+
node.children = node.children.flatMap((child): Node[] => {
|
|
32
|
+
if (child.type === "element") {
|
|
33
|
+
const element = child as Element;
|
|
34
|
+
if (!SKIP.has(element.tagName)) wrap(element);
|
|
35
|
+
return [element];
|
|
36
|
+
}
|
|
37
|
+
if (child.type !== "text") return [child];
|
|
38
|
+
return (child as Text).value
|
|
39
|
+
.split(/(\s+)/)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.map((part): Text | Element => {
|
|
42
|
+
if (/^\s+$/.test(part)) return { type: "text", value: part };
|
|
43
|
+
const word: Element = {
|
|
44
|
+
type: "element",
|
|
45
|
+
tagName: "span",
|
|
46
|
+
properties: { className: ["stream-word"] },
|
|
47
|
+
children: [{ type: "text", value: part }],
|
|
48
|
+
};
|
|
49
|
+
words.push(word);
|
|
50
|
+
return word;
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
wrap(tree);
|
|
55
|
+
for (const word of words.slice(-fresh))
|
|
56
|
+
(word.properties.className as string[]).push("stream-word--fresh");
|
|
57
|
+
words.at(-1)?.children.push({
|
|
58
|
+
type: "element",
|
|
59
|
+
tagName: "span",
|
|
60
|
+
properties: { className: ["stream-caret"], ariaHidden: "true" },
|
|
61
|
+
children: [],
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* Rich responses reuse the approved conversation's geometry and semantic tokens. */
|
|
2
|
+
.reusable-markdown .reusable-code pre {
|
|
3
|
+
border: 0;
|
|
4
|
+
border-radius: 0;
|
|
5
|
+
}
|
|
6
|
+
.reusable-markdown .reusable-code pre code {
|
|
7
|
+
padding: 0;
|
|
8
|
+
}
|
|
9
|
+
.reusable-markdown table :is(th, td) {
|
|
10
|
+
padding: calc(var(--spacing) * 3);
|
|
11
|
+
border-bottom: 1px solid var(--color-line-subtle);
|
|
12
|
+
text-align: start;
|
|
13
|
+
vertical-align: top;
|
|
14
|
+
}
|
|
15
|
+
.reusable-markdown table th {
|
|
16
|
+
background: var(--color-surface-muted);
|
|
17
|
+
font-weight: var(--heading-weight);
|
|
18
|
+
}
|
|
19
|
+
.reusable-markdown table tr:last-child td {
|
|
20
|
+
border-bottom: 0;
|
|
21
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const reviewCopy = {
|
|
2
|
+
proposed: "Proposed changes",
|
|
3
|
+
before: "Current",
|
|
4
|
+
after: "Proposed",
|
|
5
|
+
reason: "Why this change",
|
|
6
|
+
applied: "Applied",
|
|
7
|
+
unchanged: "Unchanged",
|
|
8
|
+
viewChange: (title: string) => `View change: ${title}`,
|
|
9
|
+
selectChange: (title: string) => `Select change: ${title}`,
|
|
10
|
+
selected: (n: number, total: number) => `${n} of ${total} selected`,
|
|
11
|
+
review: (n: number) => `Review ${n} change${n === 1 ? "" : "s"}`,
|
|
12
|
+
apply: (n: number) => `Apply ${n} change${n === 1 ? "" : "s"}`,
|
|
13
|
+
complete: (n: number) => `${n} change${n === 1 ? "" : "s"} applied`,
|
|
14
|
+
kept: (n: number) => `${n} change${n === 1 ? "" : "s"} left unchanged.`,
|
|
15
|
+
confirm: "Confirm your decision",
|
|
16
|
+
summary: "Your selection",
|
|
17
|
+
target: "Apply to",
|
|
18
|
+
applying: "Applying",
|
|
19
|
+
checking: "Checking status",
|
|
20
|
+
noSelection: "Choose the changes you want to keep.",
|
|
21
|
+
approvalNote: "Only these changes will be applied. The rest stay as they are.",
|
|
22
|
+
deny: "Decline all",
|
|
23
|
+
declineRemaining: "Decline remaining",
|
|
24
|
+
declineRemainingTitle: "Decline the remaining changes?",
|
|
25
|
+
declineRemainingNote: "Applied changes are kept. The remaining changes will stay as they were.",
|
|
26
|
+
denyConfirm: "Decline these changes?",
|
|
27
|
+
denyAction: "Decline changes",
|
|
28
|
+
denyNote: "The current draft will stay as it is.",
|
|
29
|
+
denied: "Changes declined",
|
|
30
|
+
deniedNote: "The draft is unchanged.",
|
|
31
|
+
back: "Back to review",
|
|
32
|
+
retry: "Try again",
|
|
33
|
+
check: "Check status",
|
|
34
|
+
reload: "Review latest draft",
|
|
35
|
+
loading: "Preparing your review",
|
|
36
|
+
loadingNote: "The proposed changes are being loaded.",
|
|
37
|
+
loadError: "The review couldn’t be loaded.",
|
|
38
|
+
loadErrorNote: "Try again to see the proposed changes.",
|
|
39
|
+
uncertain: "Let’s check what happened.",
|
|
40
|
+
uncertainNote:
|
|
41
|
+
"Your decision may have reached the server. Check its status before trying again. Your choices are kept.",
|
|
42
|
+
notApplied: "No changes recorded yet.",
|
|
43
|
+
notAppliedNote: "Your choices are kept. You can check again or confirm the same decision.",
|
|
44
|
+
conflict: "The draft has changed.",
|
|
45
|
+
conflictNote:
|
|
46
|
+
"Nothing from this decision was applied. Review the latest draft before deciding again.",
|
|
47
|
+
blocked: "You can view this review.",
|
|
48
|
+
blockedNote: "You don’t have permission to apply or decline these changes.",
|
|
49
|
+
partial: (applied: number, left: number) => `${applied} applied · ${left} still to review`,
|
|
50
|
+
partialNote:
|
|
51
|
+
"The applied changes are kept. Review the remaining changes before trying them again.",
|
|
52
|
+
empty: "No changes to review",
|
|
53
|
+
emptyNote: "The current draft is unchanged.",
|
|
54
|
+
unavailable: "Review not available",
|
|
55
|
+
announcement: {
|
|
56
|
+
ready: "Changes ready to review.",
|
|
57
|
+
confirming: "Decision ready to confirm.",
|
|
58
|
+
submitting: "Applying your decision.",
|
|
59
|
+
checking: "Checking your decision’s status.",
|
|
60
|
+
partial: "Some changes were applied. Review the remaining changes.",
|
|
61
|
+
complete: "Selected changes applied.",
|
|
62
|
+
denied: "Changes declined. The draft is unchanged.",
|
|
63
|
+
conflict: "The draft changed. Review it again.",
|
|
64
|
+
blocked: "You do not have permission to decide.",
|
|
65
|
+
uncertain: "Check status before retrying your decision.",
|
|
66
|
+
"load-error": "The review could not be loaded.",
|
|
67
|
+
loading: "Preparing your review.",
|
|
68
|
+
idle: "",
|
|
69
|
+
},
|
|
70
|
+
} as const;
|
|
71
|
+
type CustomWords<T> = {
|
|
72
|
+
[K in keyof T]: T[K] extends string
|
|
73
|
+
? string
|
|
74
|
+
: T[K] extends (...args: infer A) => string
|
|
75
|
+
? (...args: A) => string
|
|
76
|
+
: CustomWords<T[K]>;
|
|
77
|
+
};
|
|
78
|
+
export type ReviewCopy = CustomWords<typeof reviewCopy>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { flowDirection, type FlowType } from "../../../components/flow-motion.js";
|
|
2
|
+
import type { ReviewStatus } from "../../review/types.js";
|
|
3
|
+
|
|
4
|
+
/** Where each status sits in the review, so a change can be tagged forward or back. */
|
|
5
|
+
const STEP: Record<ReviewStatus, number> = {
|
|
6
|
+
idle: 0,
|
|
7
|
+
loading: 0,
|
|
8
|
+
"load-error": 0,
|
|
9
|
+
ready: 1,
|
|
10
|
+
partial: 1,
|
|
11
|
+
conflict: 1,
|
|
12
|
+
blocked: 1,
|
|
13
|
+
confirming: 2,
|
|
14
|
+
submitting: 2,
|
|
15
|
+
checking: 2,
|
|
16
|
+
uncertain: 2,
|
|
17
|
+
complete: 3,
|
|
18
|
+
denied: 3,
|
|
19
|
+
};
|
|
20
|
+
/** Steps worth moving to. Problems and in-between states change in place, without movement. */
|
|
21
|
+
const MOVES_TO = new Set<ReviewStatus>(["ready", "confirming", "complete", "denied"]);
|
|
22
|
+
export function reviewFlow(from: ReviewStatus, to: ReviewStatus): FlowType | null {
|
|
23
|
+
return MOVES_TO.has(to) ? flowDirection(STEP[from], STEP[to]) : null;
|
|
24
|
+
}
|