@aphrody/m3-ai 3.3.2 → 3.3.5

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 (56) hide show
  1. package/README.md +29 -7
  2. package/dist/A2APeerInspector.d.ts +41 -0
  3. package/dist/A2APeerInspector.d.ts.map +1 -0
  4. package/dist/A2ATaskTimeline.d.ts +28 -0
  5. package/dist/A2ATaskTimeline.d.ts.map +1 -0
  6. package/dist/AgUiEventStream.d.ts +87 -0
  7. package/dist/AgUiEventStream.d.ts.map +1 -0
  8. package/dist/GeminiAccountMenu.d.ts +55 -0
  9. package/dist/GeminiAccountMenu.d.ts.map +1 -0
  10. package/dist/GeminiPersonalContextEdit.d.ts +29 -0
  11. package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
  12. package/dist/GoogleSpotlightBar.d.ts +32 -0
  13. package/dist/GoogleSpotlightBar.d.ts.map +1 -0
  14. package/dist/Markdown.d.ts +2 -1
  15. package/dist/Markdown.d.ts.map +1 -1
  16. package/dist/McpAppCard.d.ts +24 -0
  17. package/dist/McpAppCard.d.ts.map +1 -0
  18. package/dist/ModelSelector.d.ts.map +1 -1
  19. package/dist/TaskRow.d.ts.map +1 -1
  20. package/dist/ToolCall.d.ts.map +1 -1
  21. package/dist/index.d.ts +15 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3629 -1950
  24. package/dist/index.js.map +32 -1
  25. package/package.json +17 -20
  26. package/src/A2APeerInspector.tsx +370 -0
  27. package/src/A2ATaskTimeline.tsx +218 -0
  28. package/src/AgUiEventStream.ts +492 -0
  29. package/src/ChatComposer.tsx +261 -0
  30. package/src/ConversationList.tsx +207 -0
  31. package/src/GeminiAccountMenu.tsx +212 -0
  32. package/src/GeminiPersonalContextEdit.tsx +147 -0
  33. package/src/GoogleSpotlightBar.tsx +165 -0
  34. package/src/InlineBranchCard.tsx +97 -0
  35. package/src/InlineWorkerCard.tsx +182 -0
  36. package/src/Markdown.tsx +769 -0
  37. package/src/McpAppCard.tsx +161 -0
  38. package/src/MemoryPanel.tsx +176 -0
  39. package/src/MessageBubble.tsx +143 -0
  40. package/src/ModelSelector.tsx +319 -0
  41. package/src/ModelUnavailable.tsx +116 -0
  42. package/src/TaskCreateForm.tsx +123 -0
  43. package/src/TaskDetail.tsx +224 -0
  44. package/src/TaskList.tsx +136 -0
  45. package/src/TaskPriorityIcon.tsx +39 -0
  46. package/src/TaskRow.tsx +177 -0
  47. package/src/TaskStatusIcon.tsx +39 -0
  48. package/src/ToolCall.tsx +644 -0
  49. package/src/chatIcons.tsx +105 -0
  50. package/src/chatLabels.ts +243 -0
  51. package/src/citations.tsx +286 -0
  52. package/src/index.ts +173 -0
  53. package/src/internal.tsx +107 -0
  54. package/src/openaiChatStream.ts +400 -0
  55. package/src/types.ts +212 -0
  56. package/src/useOpenAIChat.ts +126 -0
@@ -0,0 +1,769 @@
1
+ // SPDX-License-Identifier: Apache-2.0 AND MIT
2
+ // Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/Markdown.tsx), MIT, rebuilt on Material Design 3.
3
+
4
+ "use client";
5
+
6
+ import { clsx } from "clsx";
7
+ import { forwardRef, isValidElement, useEffect, useRef, useState, type ReactNode } from "react";
8
+ import ReactMarkdown, { type Components } from "react-markdown";
9
+ import rehypeRaw from "rehype-raw";
10
+ import remarkGfm from "remark-gfm";
11
+
12
+ import { type ChatLabels, resolveChatLabels } from "./chatLabels";
13
+ import { ChatIcon } from "./chatIcons";
14
+
15
+ type MarkdownSize = "small" | "medium" | "large";
16
+
17
+ interface MarkdownProps {
18
+ content: string;
19
+ className?: string;
20
+ /** Body typescale of paragraphs and lists: `text-body-small|medium|large`. Default `medium`. */
21
+ size?: MarkdownSize;
22
+ /**
23
+ * Render raw HTML embedded in the markdown (rehype-raw). Off by default: only enable it for
24
+ * trusted content.
25
+ */
26
+ allowHtml?: boolean;
27
+ /** Element overrides merged over the M3 renderers (e.g. citation links). */
28
+ components?: Components;
29
+ /**
30
+ * Called on every link activation. Call `event.preventDefault()` to handle it yourself (e.g.
31
+ * open the system browser from a Tauri webview).
32
+ */
33
+ onLinkClick?: MarkdownLinkHandler;
34
+ /** Replaces the link element entirely. */
35
+ renderLink?: MarkdownLinkRenderer;
36
+ /** Visible strings of the code blocks; English defaults. */
37
+ labels?: Partial<ChatLabels>;
38
+ }
39
+
40
+ /** Link activation callback of {@link Markdown}. */
41
+ export type MarkdownLinkHandler = (
42
+ href: string,
43
+ event: React.MouseEvent<HTMLAnchorElement>,
44
+ ) => void;
45
+
46
+ /** Custom link renderer of {@link Markdown}. */
47
+ export type MarkdownLinkRenderer = (link: {
48
+ href: string;
49
+ children: React.ReactNode;
50
+ external: boolean;
51
+ }) => React.ReactNode;
52
+
53
+ /** Whether `href` leaves the page: an absolute URL with a scheme, or protocol-relative. */
54
+ export function isExternalHref(href: string | undefined): boolean {
55
+ if (!href) return false;
56
+ return /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("//");
57
+ }
58
+
59
+ /** Safe defaults of an external link: a new browsing context without opener or referrer. */
60
+ export const externalLinkProps = { target: "_blank", rel: "noopener noreferrer" } as const;
61
+
62
+ const bodyClass: Record<MarkdownSize, string> = {
63
+ small: "text-body-small",
64
+ medium: "text-body-medium",
65
+ large: "text-body-large",
66
+ };
67
+
68
+ function extractText(node: ReactNode): string {
69
+ if (typeof node === "string") return node;
70
+ if (typeof node === "number") return String(node);
71
+ if (Array.isArray(node)) return node.map(extractText).join("");
72
+ if (isValidElement(node)) return extractText((node.props as { children?: ReactNode }).children);
73
+ return "";
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Language detection & mapping
78
+ // ---------------------------------------------------------------------------
79
+
80
+ const LANGUAGE_MAP: Record<string, string> = {
81
+ typescript: "TypeScript",
82
+ ts: "TypeScript",
83
+ tsx: "TSX",
84
+ javascript: "JavaScript",
85
+ js: "JavaScript",
86
+ jsx: "JSX",
87
+ python: "Python",
88
+ py: "Python",
89
+ rust: "Rust",
90
+ rs: "Rust",
91
+ bash: "Bash",
92
+ sh: "Bash",
93
+ shell: "Bash",
94
+ zsh: "Bash",
95
+ json: "JSON",
96
+ jsonc: "JSONC",
97
+ sql: "SQL",
98
+ html: "HTML",
99
+ css: "CSS",
100
+ scss: "SCSS",
101
+ sass: "Sass",
102
+ yaml: "YAML",
103
+ yml: "YAML",
104
+ markdown: "Markdown",
105
+ md: "Markdown",
106
+ go: "Go",
107
+ golang: "Go",
108
+ java: "Java",
109
+ c: "C",
110
+ cpp: "C++",
111
+ "c++": "C++",
112
+ csharp: "C#",
113
+ cs: "C#",
114
+ "c#": "C#",
115
+ ruby: "Ruby",
116
+ rb: "Ruby",
117
+ php: "PHP",
118
+ swift: "Swift",
119
+ kotlin: "Kotlin",
120
+ kt: "Kotlin",
121
+ xml: "XML",
122
+ toml: "TOML",
123
+ dockerfile: "Dockerfile",
124
+ docker: "Dockerfile",
125
+ graphql: "GraphQL",
126
+ gql: "GraphQL",
127
+ wasm: "WebAssembly",
128
+ lua: "Lua",
129
+ r: "R",
130
+ diff: "Diff",
131
+ plaintext: "Text",
132
+ text: "Text",
133
+ txt: "Text",
134
+ };
135
+
136
+ export function detectLanguage(raw?: string): { raw: string; label: string } {
137
+ if (!raw) return { raw: "", label: "Code" };
138
+ const cleaned = raw
139
+ .replace(/^language-|^lang-/, "")
140
+ .toLowerCase()
141
+ .trim();
142
+ if (LANGUAGE_MAP[cleaned]) {
143
+ return { raw: cleaned, label: LANGUAGE_MAP[cleaned] };
144
+ }
145
+ const label =
146
+ cleaned.length <= 3
147
+ ? cleaned.toUpperCase()
148
+ : cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
149
+ return { raw: cleaned, label };
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // CodeBlock component
154
+ // ---------------------------------------------------------------------------
155
+
156
+ export interface CodeBlockProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onCopy"> {
157
+ code?: string;
158
+ language?: string;
159
+ onCopy?: (code: string) => void;
160
+ children?: ReactNode;
161
+ /** `copy`, `copyCode`, `copied`; English defaults. */
162
+ labels?: Partial<ChatLabels>;
163
+ }
164
+
165
+ export function CodeBlock({
166
+ code: explicitCode,
167
+ language: explicitLang,
168
+ onCopy,
169
+ className,
170
+ children,
171
+ labels,
172
+ ...restProps
173
+ }: CodeBlockProps) {
174
+ const l = resolveChatLabels(labels);
175
+ // Strip node passed by ReactMarkdown to avoid rendering node="[object Object]" in DOM
176
+ const { node: _node, ...props } = restProps as Record<string, any>;
177
+
178
+ const [copied, setCopied] = useState(false);
179
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
180
+
181
+ useEffect(() => {
182
+ return () => {
183
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
184
+ };
185
+ }, []);
186
+
187
+ const childElement = isValidElement(children) ? children : null;
188
+ const childProps = childElement
189
+ ? (childElement.props as { className?: string; children?: ReactNode })
190
+ : null;
191
+
192
+ const rawLangMatch =
193
+ explicitLang ??
194
+ className?.match(/language-([a-zA-Z0-9_+#-]+)/)?.[1] ??
195
+ childProps?.className?.match(/language-([a-zA-Z0-9_+#-]+)/)?.[1];
196
+
197
+ const { raw: rawLang, label: languageLabel } = detectLanguage(rawLangMatch);
198
+
199
+ const rawCode =
200
+ explicitCode ??
201
+ (childProps?.children ? extractText(childProps.children) : extractText(children));
202
+
203
+ const codeContent = rawCode;
204
+ const codeToCopy = rawCode.replace(/\n$/, "");
205
+
206
+ const handleCopy = async () => {
207
+ try {
208
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
209
+ await navigator.clipboard.writeText(codeToCopy);
210
+ }
211
+ } catch {
212
+ // Ignore clipboard API failures in non-secure or test environments
213
+ }
214
+ onCopy?.(codeToCopy);
215
+ setCopied(true);
216
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
217
+ timeoutRef.current = setTimeout(() => {
218
+ setCopied(false);
219
+ }, 2000);
220
+ };
221
+
222
+ return (
223
+ <div
224
+ className={clsx(
225
+ "my-3 overflow-hidden rounded-medium border border-outline-variant bg-surface-container-highest",
226
+ className,
227
+ )}
228
+ {...props}
229
+ >
230
+ <div className="flex items-center justify-between border-b border-outline-variant bg-surface-container px-3 py-1.5">
231
+ <span className="inline-flex items-center rounded-extra-small bg-surface-container-high px-2 py-0.5 text-label-small font-medium text-on-surface-variant border border-outline-variant/60">
232
+ {languageLabel}
233
+ </span>
234
+ <button
235
+ type="button"
236
+ onClick={handleCopy}
237
+ className={clsx(
238
+ "inline-flex items-center gap-1.5 rounded-small px-2 py-1 text-label-small font-medium transition-colors duration-200",
239
+ copied
240
+ ? "text-success"
241
+ : "text-on-surface-variant hover:bg-on-surface/8 hover:text-on-surface active:bg-on-surface/12",
242
+ "focus-visible:outline-2 focus-visible:outline-primary",
243
+ )}
244
+ aria-label={copied ? l.copied : l.copyCode}
245
+ title={copied ? l.copied : l.copyCode}
246
+ >
247
+ <ChatIcon
248
+ name={copied ? "check" : "content_copy"}
249
+ size={16}
250
+ className={copied ? "text-success" : undefined}
251
+ />
252
+ <span>{copied ? l.copied : l.copy}</span>
253
+ </button>
254
+ </div>
255
+ <pre className="overflow-x-auto p-3 font-code text-body-small text-on-surface bg-surface-container-highest [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-[length:inherit]">
256
+ <code className={rawLang ? `language-${rawLang}` : undefined}>{codeContent}</code>
257
+ </pre>
258
+ </div>
259
+ );
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Math formula rendering & typography
264
+ // ---------------------------------------------------------------------------
265
+
266
+ const GREEK_AND_SYMBOLS: Record<string, string> = {
267
+ // Greek letters (lowercase)
268
+ alpha: "α",
269
+ beta: "β",
270
+ gamma: "γ",
271
+ delta: "δ",
272
+ epsilon: "ε",
273
+ zeta: "ζ",
274
+ eta: "η",
275
+ theta: "θ",
276
+ iota: "ι",
277
+ kappa: "κ",
278
+ lambda: "λ",
279
+ mu: "μ",
280
+ nu: "ν",
281
+ xi: "ξ",
282
+ pi: "π",
283
+ rho: "ρ",
284
+ sigma: "σ",
285
+ tau: "τ",
286
+ upsilon: "υ",
287
+ phi: "φ",
288
+ chi: "χ",
289
+ psi: "ψ",
290
+ omega: "ω",
291
+ // Greek letters (uppercase)
292
+ Gamma: "Γ",
293
+ Delta: "Δ",
294
+ Theta: "Θ",
295
+ Lambda: "Λ",
296
+ Xi: "Ξ",
297
+ Pi: "Π",
298
+ Sigma: "Σ",
299
+ Phi: "Φ",
300
+ Psi: "Ψ",
301
+ Omega: "Ω",
302
+ // Operators & Relations
303
+ times: "×",
304
+ div: "÷",
305
+ pm: "±",
306
+ mp: "∓",
307
+ le: "≤",
308
+ leq: "≤",
309
+ ge: "≥",
310
+ geq: "≥",
311
+ approx: "≈",
312
+ neq: "≠",
313
+ ne: "≠",
314
+ infty: "∞",
315
+ cdot: "·",
316
+ circ: "°",
317
+ partial: "∂",
318
+ nabla: "∇",
319
+ sum: "∑",
320
+ prod: "∏",
321
+ int: "∫",
322
+ sqrt: "√",
323
+ in: "∈",
324
+ notin: "∉",
325
+ subset: "⊂",
326
+ subseteq: "⊆",
327
+ cup: "∪",
328
+ cap: "∩",
329
+ to: "→",
330
+ rightarrow: "→",
331
+ leftarrow: "←",
332
+ leftrightarrow: "↔",
333
+ Rightarrow: "⇒",
334
+ Leftarrow: "⇐",
335
+ Leftrightarrow: "⇔",
336
+ forall: "∀",
337
+ exists: "∃",
338
+ };
339
+
340
+ function formatSubSup(text: string): ReactNode {
341
+ const parts = text.split(
342
+ /(\^\{[^}]+\}|\^[-+]?[a-zA-Z0-9\u0370-\u03FF\u2100-\u214F]|_\{[^}]+\}|_[-+]?[a-zA-Z0-9\u0370-\u03FF\u2100-\u214F])/g,
343
+ );
344
+ if (parts.length === 1) return text;
345
+
346
+ const occurrences = new Map<string, number>();
347
+ return parts.map((part) => {
348
+ const occurrence = occurrences.get(part) ?? 0;
349
+ occurrences.set(part, occurrence + 1);
350
+ const key = `${part}:${occurrence}`;
351
+ if (part.startsWith("^")) {
352
+ const val = part.startsWith("^{") && part.endsWith("}") ? part.slice(2, -1) : part.slice(1);
353
+ return (
354
+ <sup key={key} className="text-[0.75em] leading-none">
355
+ {val}
356
+ </sup>
357
+ );
358
+ }
359
+ if (part.startsWith("_")) {
360
+ const val = part.startsWith("_{") && part.endsWith("}") ? part.slice(2, -1) : part.slice(1);
361
+ return (
362
+ <sub key={key} className="text-[0.75em] leading-none">
363
+ {val}
364
+ </sub>
365
+ );
366
+ }
367
+ return part;
368
+ });
369
+ }
370
+
371
+ export function formatMathContent(math: string): ReactNode {
372
+ // Step 1: Replace LaTeX symbol commands \name (or \\name)
373
+ const text = math.replace(/\\+([a-zA-Z]+)/g, (match, name) => {
374
+ return GREEK_AND_SYMBOLS[name] ?? match;
375
+ });
376
+
377
+ // Step 2: Handle \frac{num}{den}
378
+ const fracRegex = /\\+frac\{([^{}]+)\}\{([^{}]+)\}/g;
379
+ if (fracRegex.test(text)) {
380
+ const parts: ReactNode[] = [];
381
+ let lastIndex = 0;
382
+ text.replace(fracRegex, (match, num, den, offset) => {
383
+ if (offset > lastIndex) {
384
+ parts.push(formatSubSup(text.slice(lastIndex, offset)));
385
+ }
386
+ parts.push(
387
+ <span
388
+ key={`frac-${offset}`}
389
+ className="inline-flex flex-col items-center align-middle mx-1 text-center text-[0.85em] leading-none not-italic"
390
+ >
391
+ <span className="border-b border-current px-0.5 pb-0.5">{formatSubSup(num)}</span>
392
+ <span className="px-0.5 pt-0.5">{formatSubSup(den)}</span>
393
+ </span>,
394
+ );
395
+ lastIndex = offset + match.length;
396
+ return match;
397
+ });
398
+ if (lastIndex < text.length) {
399
+ parts.push(formatSubSup(text.slice(lastIndex)));
400
+ }
401
+ return parts;
402
+ }
403
+
404
+ return formatSubSup(text);
405
+ }
406
+
407
+ export interface InlineMathProps extends React.HTMLAttributes<HTMLSpanElement> {
408
+ math: string;
409
+ }
410
+
411
+ export function InlineMath({ math, className, ...props }: InlineMathProps) {
412
+ return (
413
+ <span
414
+ className={clsx(
415
+ "inline-flex items-center px-1 font-serif italic font-medium text-primary",
416
+ className,
417
+ )}
418
+ {...props}
419
+ >
420
+ {formatMathContent(math)}
421
+ </span>
422
+ );
423
+ }
424
+
425
+ export interface BlockMathProps extends React.HTMLAttributes<HTMLDivElement> {
426
+ math: string;
427
+ }
428
+
429
+ export function BlockMath({ math, className, ...props }: BlockMathProps) {
430
+ return (
431
+ <div
432
+ className={clsx(
433
+ "my-3 flex justify-center overflow-x-auto rounded-medium bg-surface-container-low/40 px-4 py-2 text-title-medium font-serif italic font-medium text-primary",
434
+ className,
435
+ )}
436
+ {...props}
437
+ >
438
+ {formatMathContent(math)}
439
+ </div>
440
+ );
441
+ }
442
+
443
+ // ---------------------------------------------------------------------------
444
+ // Remark plugin for recognizing $...$ and $$...$$ expressions
445
+ // ---------------------------------------------------------------------------
446
+
447
+ function transformBlocks(parent: any) {
448
+ if (!parent.children) return;
449
+ const newChildren: any[] = [];
450
+ for (const child of parent.children) {
451
+ if (child.type === "paragraph" && Array.isArray(child.children)) {
452
+ const hasBlockMath = child.children.some(
453
+ (c: any) => c.type === "text" && /\$\$[\s\S]+?\$\$/.test(c.value),
454
+ );
455
+ if (hasBlockMath) {
456
+ let currentParagraphChildren: any[] = [];
457
+ for (const item of child.children) {
458
+ if (item.type === "text" && /\$\$[\s\S]+?\$\$/.test(item.value)) {
459
+ const parts = item.value.split(/(\$\$[\s\S]+?\$\$)/g);
460
+ for (const part of parts) {
461
+ if (part.startsWith("$$") && part.endsWith("$$") && part.length >= 4) {
462
+ if (currentParagraphChildren.length > 0) {
463
+ newChildren.push({
464
+ type: "paragraph",
465
+ children: currentParagraphChildren,
466
+ });
467
+ currentParagraphChildren = [];
468
+ }
469
+ const mathVal = part.slice(2, -2).trim();
470
+ newChildren.push({
471
+ type: "mathBlock",
472
+ data: {
473
+ hName: "div",
474
+ hProperties: { className: "math-block", "data-math": mathVal },
475
+ hChildren: [{ type: "text", value: mathVal }],
476
+ },
477
+ children: [{ type: "text", value: mathVal }],
478
+ });
479
+ } else if (part.trim().length > 0) {
480
+ currentParagraphChildren.push({ type: "text", value: part });
481
+ }
482
+ }
483
+ } else {
484
+ currentParagraphChildren.push(item);
485
+ }
486
+ }
487
+ if (currentParagraphChildren.length > 0) {
488
+ newChildren.push({
489
+ type: "paragraph",
490
+ children: currentParagraphChildren,
491
+ });
492
+ }
493
+ continue;
494
+ }
495
+ }
496
+ transformBlocks(child);
497
+ newChildren.push(child);
498
+ }
499
+ parent.children = newChildren;
500
+ }
501
+
502
+ function transformInlines(node: any) {
503
+ if (!node.children) return;
504
+ const newChildren: any[] = [];
505
+ for (const child of node.children) {
506
+ if (child.type === "text" && typeof child.value === "string" && child.value.includes("$")) {
507
+ const parts = child.value.split(/((?<!\\)\$(?!\s)[^$\n]+?(?<!\s)\$(?!\d))/g);
508
+ for (const part of parts) {
509
+ if (
510
+ part.startsWith("$") &&
511
+ part.endsWith("$") &&
512
+ !part.startsWith("$$") &&
513
+ part.length > 2
514
+ ) {
515
+ const mathVal = part.slice(1, -1).trim();
516
+ newChildren.push({
517
+ type: "mathInline",
518
+ data: {
519
+ hName: "span",
520
+ hProperties: { className: "math-inline", "data-math": mathVal },
521
+ hChildren: [{ type: "text", value: mathVal }],
522
+ },
523
+ children: [{ type: "text", value: mathVal }],
524
+ });
525
+ } else if (part) {
526
+ newChildren.push({ type: "text", value: part });
527
+ }
528
+ }
529
+ } else {
530
+ transformInlines(child);
531
+ newChildren.push(child);
532
+ }
533
+ }
534
+ node.children = newChildren;
535
+ }
536
+
537
+ function transformMathTree(tree: any) {
538
+ transformBlocks(tree);
539
+ transformInlines(tree);
540
+ }
541
+
542
+ export function remarkMathPlugin() {
543
+ return transformMathTree;
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // Element overrides mapping markdown onto M3 typescale and color roles
548
+ // ---------------------------------------------------------------------------
549
+
550
+ interface LinkOptions {
551
+ onLinkClick?: MarkdownLinkHandler;
552
+ renderLink?: MarkdownLinkRenderer;
553
+ labels?: Partial<ChatLabels>;
554
+ }
555
+
556
+ function m3Components(size: MarkdownSize, link: LinkOptions = {}): Components {
557
+ const body = bodyClass[size];
558
+ return {
559
+ h1: ({ node: _node, className, children, ...props }) => (
560
+ <h1
561
+ className={clsx("mt-4 mb-2 text-headline-small text-on-surface first:mt-0", className)}
562
+ {...props}
563
+ >
564
+ {children}
565
+ </h1>
566
+ ),
567
+ h2: ({ node: _node, className, children, ...props }) => (
568
+ <h2
569
+ className={clsx("mt-4 mb-2 text-title-large text-on-surface first:mt-0", className)}
570
+ {...props}
571
+ >
572
+ {children}
573
+ </h2>
574
+ ),
575
+ h3: ({ node: _node, className, children, ...props }) => (
576
+ <h3
577
+ className={clsx("mt-3 mb-1.5 text-title-medium text-on-surface first:mt-0", className)}
578
+ {...props}
579
+ >
580
+ {children}
581
+ </h3>
582
+ ),
583
+ h4: ({ node: _node, className, children, ...props }) => (
584
+ <h4
585
+ className={clsx("mt-3 mb-1 text-title-small text-on-surface first:mt-0", className)}
586
+ {...props}
587
+ >
588
+ {children}
589
+ </h4>
590
+ ),
591
+ h5: ({ node: _node, className, children, ...props }) => (
592
+ <h5
593
+ className={clsx("mt-3 mb-1 text-title-small text-on-surface-variant first:mt-0", className)}
594
+ {...props}
595
+ >
596
+ {children}
597
+ </h5>
598
+ ),
599
+ h6: ({ node: _node, className, children, ...props }) => (
600
+ <h6
601
+ className={clsx("mt-3 mb-1 text-label-large text-on-surface-variant first:mt-0", className)}
602
+ {...props}
603
+ >
604
+ {children}
605
+ </h6>
606
+ ),
607
+ p: ({ node: _node, className, ...props }) => (
608
+ <p className={clsx("my-2 first:mt-0 last:mb-0", body, className)} {...props} />
609
+ ),
610
+ a: ({ node: _node, className, children, href, onClick, ...props }) => {
611
+ const external = isExternalHref(href);
612
+ if (link.renderLink) return <>{link.renderLink({ href: href ?? "", children, external })}</>;
613
+ return (
614
+ <a
615
+ className={clsx(
616
+ "text-primary underline underline-offset-2 hover:decoration-2",
617
+ "rounded-extra-small outline-none focus-visible:outline-3 focus-visible:outline-secondary focus-visible:outline-offset-2",
618
+ className,
619
+ )}
620
+ href={href}
621
+ {...(external ? externalLinkProps : {})}
622
+ onClick={(event) => {
623
+ onClick?.(event);
624
+ if (href) link.onLinkClick?.(href, event);
625
+ }}
626
+ {...props}
627
+ >
628
+ {children}
629
+ </a>
630
+ );
631
+ },
632
+ strong: ({ node: _node, className, ...props }) => (
633
+ <strong className={clsx("font-semibold text-on-surface", className)} {...props} />
634
+ ),
635
+ ul: ({ node: _node, className, ...props }) => (
636
+ <ul
637
+ className={clsx("my-2 list-disc pl-6 marker:text-on-surface-variant", body, className)}
638
+ {...props}
639
+ />
640
+ ),
641
+ ol: ({ node: _node, className, ...props }) => (
642
+ <ol
643
+ className={clsx("my-2 list-decimal pl-6 marker:text-on-surface-variant", body, className)}
644
+ {...props}
645
+ />
646
+ ),
647
+ li: ({ node: _node, className, ...props }) => (
648
+ <li className={clsx("my-1", className)} {...props} />
649
+ ),
650
+ blockquote: ({ node: _node, className, ...props }) => (
651
+ <blockquote
652
+ className={clsx(
653
+ "my-3 border-l-4 border-outline-variant pl-4 text-on-surface-variant",
654
+ className,
655
+ )}
656
+ {...props}
657
+ />
658
+ ),
659
+ code: ({ node: _node, className, ...props }) => (
660
+ <code
661
+ className={clsx(
662
+ "rounded-extra-small bg-surface-container-highest px-1 py-0.5 font-code text-[0.9em] text-on-surface",
663
+ className,
664
+ )}
665
+ {...props}
666
+ />
667
+ ),
668
+ pre: ({ node: _node, children, className, style }) => (
669
+ <CodeBlock className={className} style={style} labels={link.labels}>
670
+ {children}
671
+ </CodeBlock>
672
+ ),
673
+ hr: ({ node: _node, className, ...props }) => (
674
+ <hr className={clsx("my-4 border-outline-variant", className)} {...props} />
675
+ ),
676
+ table: ({ node: _node, className, ...props }) => (
677
+ <div className="my-3 overflow-x-auto rounded-small border border-outline-variant">
678
+ <table className={clsx("w-full border-collapse", body, className)} {...props} />
679
+ </div>
680
+ ),
681
+ thead: ({ node: _node, className, ...props }) => (
682
+ <thead className={clsx("bg-surface-container", className)} {...props} />
683
+ ),
684
+ th: ({ node: _node, className, ...props }) => (
685
+ <th
686
+ className={clsx(
687
+ "border-b border-outline-variant px-3 py-2 text-left text-title-small text-on-surface",
688
+ className,
689
+ )}
690
+ {...props}
691
+ />
692
+ ),
693
+ td: ({ node: _node, className, ...props }) => (
694
+ <td
695
+ className={clsx("border-t border-outline-variant px-3 py-2 text-on-surface", className)}
696
+ {...props}
697
+ />
698
+ ),
699
+ img: ({ node: _node, className, alt, ...props }) => (
700
+ <img
701
+ className={clsx("my-2 max-w-full rounded-medium", className)}
702
+ alt={alt ?? ""}
703
+ {...props}
704
+ />
705
+ ),
706
+ input: ({ node: _node, className, ...props }) => (
707
+ <input className={clsx("mr-2 accent-primary", className)} {...props} />
708
+ ),
709
+ div: ({ node: _node, className, children, ...props }) => {
710
+ if (className?.includes("math-block")) {
711
+ const math = (props as Record<string, any>)["data-math"] ?? extractText(children);
712
+ return <BlockMath math={math} className={className} />;
713
+ }
714
+ return (
715
+ <div className={className} {...props}>
716
+ {children}
717
+ </div>
718
+ );
719
+ },
720
+ span: ({ node: _node, className, children, ...props }) => {
721
+ if (className?.includes("math-inline")) {
722
+ const math = (props as Record<string, any>)["data-math"] ?? extractText(children);
723
+ return <InlineMath math={math} className={className} />;
724
+ }
725
+ return (
726
+ <span className={className} {...props}>
727
+ {children}
728
+ </span>
729
+ );
730
+ },
731
+ };
732
+ }
733
+
734
+ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
735
+ (
736
+ {
737
+ content,
738
+ className,
739
+ size = "medium",
740
+ allowHtml = false,
741
+ components,
742
+ onLinkClick,
743
+ renderLink,
744
+ labels,
745
+ },
746
+ ref,
747
+ ) => {
748
+ const base = m3Components(size, { onLinkClick, renderLink, labels });
749
+ return (
750
+ <div
751
+ ref={ref}
752
+ className={clsx("markdown min-w-0 text-on-surface", bodyClass[size], className)}
753
+ >
754
+ <ReactMarkdown
755
+ remarkPlugins={[remarkGfm, remarkMathPlugin]}
756
+ rehypePlugins={allowHtml ? [rehypeRaw] : undefined}
757
+ components={components ? { ...base, ...components } : base}
758
+ >
759
+ {content}
760
+ </ReactMarkdown>
761
+ </div>
762
+ );
763
+ },
764
+ );
765
+
766
+ Markdown.displayName = "Markdown";
767
+
768
+ export { Markdown };
769
+ export type { MarkdownProps };