@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.
- package/README.md +29 -7
- package/dist/A2APeerInspector.d.ts +41 -0
- package/dist/A2APeerInspector.d.ts.map +1 -0
- package/dist/A2ATaskTimeline.d.ts +28 -0
- package/dist/A2ATaskTimeline.d.ts.map +1 -0
- package/dist/AgUiEventStream.d.ts +87 -0
- package/dist/AgUiEventStream.d.ts.map +1 -0
- package/dist/GeminiAccountMenu.d.ts +55 -0
- package/dist/GeminiAccountMenu.d.ts.map +1 -0
- package/dist/GeminiPersonalContextEdit.d.ts +29 -0
- package/dist/GeminiPersonalContextEdit.d.ts.map +1 -0
- package/dist/GoogleSpotlightBar.d.ts +32 -0
- package/dist/GoogleSpotlightBar.d.ts.map +1 -0
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.d.ts.map +1 -1
- package/dist/McpAppCard.d.ts +24 -0
- package/dist/McpAppCard.d.ts.map +1 -0
- package/dist/ModelSelector.d.ts.map +1 -1
- package/dist/TaskRow.d.ts.map +1 -1
- package/dist/ToolCall.d.ts.map +1 -1
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3629 -1950
- package/dist/index.js.map +32 -1
- package/package.json +17 -20
- package/src/A2APeerInspector.tsx +370 -0
- package/src/A2ATaskTimeline.tsx +218 -0
- package/src/AgUiEventStream.ts +492 -0
- package/src/ChatComposer.tsx +261 -0
- package/src/ConversationList.tsx +207 -0
- package/src/GeminiAccountMenu.tsx +212 -0
- package/src/GeminiPersonalContextEdit.tsx +147 -0
- package/src/GoogleSpotlightBar.tsx +165 -0
- package/src/InlineBranchCard.tsx +97 -0
- package/src/InlineWorkerCard.tsx +182 -0
- package/src/Markdown.tsx +769 -0
- package/src/McpAppCard.tsx +161 -0
- package/src/MemoryPanel.tsx +176 -0
- package/src/MessageBubble.tsx +143 -0
- package/src/ModelSelector.tsx +319 -0
- package/src/ModelUnavailable.tsx +116 -0
- package/src/TaskCreateForm.tsx +123 -0
- package/src/TaskDetail.tsx +224 -0
- package/src/TaskList.tsx +136 -0
- package/src/TaskPriorityIcon.tsx +39 -0
- package/src/TaskRow.tsx +177 -0
- package/src/TaskStatusIcon.tsx +39 -0
- package/src/ToolCall.tsx +644 -0
- package/src/chatIcons.tsx +105 -0
- package/src/chatLabels.ts +243 -0
- package/src/citations.tsx +286 -0
- package/src/index.ts +173 -0
- package/src/internal.tsx +107 -0
- package/src/openaiChatStream.ts +400 -0
- package/src/types.ts +212 -0
- package/src/useOpenAIChat.ts +126 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* @aphrody/m3-ai - McpAppCard
|
|
4
|
+
*
|
|
5
|
+
* Material Design 3 Interactive MCP App Container:
|
|
6
|
+
* - Renders generative UI resources produced by MCP tools (SEP-1724 / mcp-ui).
|
|
7
|
+
* - Hosts interactive guest web applications in an isolated sandbox iframe.
|
|
8
|
+
* - Passes reactive M3 theme and color scheme context to the guest app.
|
|
9
|
+
* - Supports expandable full-bleed or embedded card layouts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { useState, useRef, useMemo } from "react";
|
|
13
|
+
import { clsx } from "clsx";
|
|
14
|
+
import { AppRenderer, type SandboxConfig, type McpUiHostContext } from "@aphrody/mcp-ui";
|
|
15
|
+
|
|
16
|
+
export interface McpAppCardProps {
|
|
17
|
+
toolName: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
html?: string;
|
|
20
|
+
toolResourceUri?: string;
|
|
21
|
+
toolInput?: Record<string, unknown>;
|
|
22
|
+
toolResult?: any;
|
|
23
|
+
sandboxUrl?: string;
|
|
24
|
+
isExpandedDefault?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
onAction?: (action: string, payload?: unknown) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const McpAppCard: React.FC<McpAppCardProps> = ({
|
|
30
|
+
toolName,
|
|
31
|
+
title,
|
|
32
|
+
html,
|
|
33
|
+
toolResourceUri,
|
|
34
|
+
toolInput,
|
|
35
|
+
toolResult,
|
|
36
|
+
sandboxUrl,
|
|
37
|
+
isExpandedDefault = false,
|
|
38
|
+
className,
|
|
39
|
+
onAction: _onAction,
|
|
40
|
+
}) => {
|
|
41
|
+
const [isExpanded, setIsExpanded] = useState(isExpandedDefault);
|
|
42
|
+
const [hasError, setHasError] = useState<string | null>(null);
|
|
43
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
|
|
45
|
+
// Host context with Material Design 3 tokens
|
|
46
|
+
const hostContext: McpUiHostContext = useMemo(() => {
|
|
47
|
+
const isDark =
|
|
48
|
+
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
49
|
+
return {
|
|
50
|
+
theme: isDark ? "dark" : "light",
|
|
51
|
+
locale: typeof navigator !== "undefined" ? navigator.language : "en-US",
|
|
52
|
+
displayMode: isExpanded ? "fullscreen" : "inline",
|
|
53
|
+
};
|
|
54
|
+
}, [isExpanded]);
|
|
55
|
+
|
|
56
|
+
// Fallback default sandbox proxy URL
|
|
57
|
+
const resolvedSandbox: SandboxConfig = useMemo(() => {
|
|
58
|
+
const url = sandboxUrl ? new URL(sandboxUrl) : new URL("https://mcpui.dev/sandbox_proxy.html");
|
|
59
|
+
return {
|
|
60
|
+
url,
|
|
61
|
+
permissions: "allow-scripts allow-forms allow-same-origin",
|
|
62
|
+
};
|
|
63
|
+
}, [sandboxUrl]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
ref={containerRef}
|
|
68
|
+
className={clsx(
|
|
69
|
+
"flex flex-col rounded-3xl border transition-all duration-300 overflow-hidden",
|
|
70
|
+
"bg-[var(--md-sys-color-surface-container-low,#f7f2fa)]",
|
|
71
|
+
"border-[var(--md-sys-color-outline-variant,#cac4d0)]",
|
|
72
|
+
"text-[var(--md-sys-color-on-surface,#1d1b20)]",
|
|
73
|
+
isExpanded ? "fixed inset-4 z-50 shadow-2xl" : "relative w-full shadow-sm",
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{/* App Card Header */}
|
|
78
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--md-sys-color-outline-variant,#cac4d0)]/40 bg-[var(--md-sys-color-surface-container,#f3edf7)]/60">
|
|
79
|
+
<div className="flex items-center gap-2.5">
|
|
80
|
+
<div className="flex h-7 w-7 items-center justify-center rounded-xl bg-[var(--md-sys-color-primary-container,#eaddff)] text-[var(--md-sys-color-on-primary-container,#21005d)]">
|
|
81
|
+
<span className="material-symbols-outlined text-base">widgets</span>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div className="flex flex-col">
|
|
85
|
+
<div className="flex items-center gap-2">
|
|
86
|
+
<span className="text-xs font-semibold tracking-tight">{title || toolName}</span>
|
|
87
|
+
<span className="rounded-full bg-[var(--md-sys-color-surface-container-high,#ece6f0)] px-2 py-0.2 text-[10px] font-mono text-[var(--md-sys-color-on-surface-variant,#49454f)]">
|
|
88
|
+
mcp-ui
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
{toolResourceUri && (
|
|
92
|
+
<span className="text-[10px] font-mono text-[var(--md-sys-color-outline,#79747e)]">
|
|
93
|
+
{toolResourceUri}
|
|
94
|
+
</span>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Action icons */}
|
|
100
|
+
<div className="flex items-center gap-1">
|
|
101
|
+
<div className="flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-[10px] font-medium text-emerald-700 dark:text-emerald-400">
|
|
102
|
+
<span className="material-symbols-outlined text-xs">verified_user</span>
|
|
103
|
+
<span>Sandboxed</span>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
109
|
+
title={isExpanded ? "Collapse" : "Expand Fullscreen"}
|
|
110
|
+
className="flex h-7 w-7 items-center justify-center rounded-lg text-[var(--md-sys-color-on-surface-variant,#49454f)] hover:bg-[var(--md-sys-color-surface-container-high,#ece6f0)] transition-colors"
|
|
111
|
+
>
|
|
112
|
+
<span className="material-symbols-outlined text-base">
|
|
113
|
+
{isExpanded ? "close_fullscreen" : "open_in_full"}
|
|
114
|
+
</span>
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* App Frame Content */}
|
|
120
|
+
<div
|
|
121
|
+
className={clsx(
|
|
122
|
+
"relative w-full overflow-hidden transition-all",
|
|
123
|
+
isExpanded ? "flex-1 min-h-[500px]" : "h-96",
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{hasError ? (
|
|
127
|
+
<div className="flex h-full flex-col items-center justify-center p-6 text-center text-xs text-[var(--md-sys-color-error,#b3261e)]">
|
|
128
|
+
<span className="material-symbols-outlined text-3xl mb-1">error</span>
|
|
129
|
+
<span className="font-semibold">MCP App Render Error</span>
|
|
130
|
+
<p className="mt-1 font-mono text-[11px] opacity-80">{hasError}</p>
|
|
131
|
+
</div>
|
|
132
|
+
) : html ? (
|
|
133
|
+
<AppRenderer
|
|
134
|
+
toolName={toolName}
|
|
135
|
+
html={html}
|
|
136
|
+
sandbox={resolvedSandbox}
|
|
137
|
+
toolInput={toolInput}
|
|
138
|
+
toolResult={toolResult}
|
|
139
|
+
hostContext={hostContext}
|
|
140
|
+
onError={(err) => setHasError(err.message)}
|
|
141
|
+
/>
|
|
142
|
+
) : (
|
|
143
|
+
<div className="flex h-full flex-col items-center justify-center p-6 text-xs text-[var(--md-sys-color-outline,#79747e)]">
|
|
144
|
+
<span className="material-symbols-outlined text-2xl animate-spin mb-1 text-[var(--md-sys-color-primary,#6750a4)]">
|
|
145
|
+
progress_activity
|
|
146
|
+
</span>
|
|
147
|
+
<span>Awaiting MCP UI Resource ({toolName})...</span>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Footer info */}
|
|
153
|
+
{toolInput && (
|
|
154
|
+
<div className="flex items-center justify-between px-4 py-2 border-t border-[var(--md-sys-color-outline-variant,#cac4d0)]/30 bg-[var(--md-sys-color-surface-container-lowest,#ffffff)] text-[10px] text-[var(--md-sys-color-outline,#79747e)] font-mono">
|
|
155
|
+
<span className="truncate">input: {JSON.stringify(toolInput)}</span>
|
|
156
|
+
<span className="shrink-0 ml-2">M3 UI Bridge</span>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Memory panel: the agent's saved memories as an M3 list, with per-item deletion and a
|
|
3
|
+
// "clear all" action guarded by an inline confirmation (or an injected one).
|
|
4
|
+
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { CircleButton } from "@aphrody/m3-primitives";
|
|
8
|
+
import { clsx } from "clsx";
|
|
9
|
+
import { useMemo, useState } from "react";
|
|
10
|
+
|
|
11
|
+
import { type ChatLabels, resolveChatLabels } from "./chatLabels";
|
|
12
|
+
import { ChatIcon } from "./chatIcons";
|
|
13
|
+
import { CircularIndicator, focusRing } from "./internal";
|
|
14
|
+
|
|
15
|
+
export interface MemoryItem {
|
|
16
|
+
id: string;
|
|
17
|
+
content: string;
|
|
18
|
+
tags?: string[];
|
|
19
|
+
/** ISO string, epoch milliseconds or Date. */
|
|
20
|
+
createdAt: string | number | Date;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MemoryPanelProps {
|
|
24
|
+
memories: readonly MemoryItem[];
|
|
25
|
+
loading?: boolean;
|
|
26
|
+
onDelete?: (id: string) => void;
|
|
27
|
+
/** Shows "clear all". */
|
|
28
|
+
onClear?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Replaces the built-in inline confirmation of "clear all" (e.g. an app dialog):
|
|
31
|
+
* resolve `true` to clear.
|
|
32
|
+
*/
|
|
33
|
+
confirmClear?: () => boolean | Promise<boolean>;
|
|
34
|
+
labels?: Partial<ChatLabels>;
|
|
35
|
+
locale?: string;
|
|
36
|
+
formatDate?: (createdAt: Date) => string;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const textButton = clsx(
|
|
41
|
+
"inline-flex h-10 items-center rounded-full px-3 text-label-large",
|
|
42
|
+
"transition-colors duration-200 ease-standard",
|
|
43
|
+
focusRing,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export function MemoryPanel({
|
|
47
|
+
memories,
|
|
48
|
+
loading = false,
|
|
49
|
+
onDelete,
|
|
50
|
+
onClear,
|
|
51
|
+
confirmClear,
|
|
52
|
+
labels,
|
|
53
|
+
locale,
|
|
54
|
+
formatDate,
|
|
55
|
+
className,
|
|
56
|
+
}: MemoryPanelProps) {
|
|
57
|
+
const l = resolveChatLabels(labels);
|
|
58
|
+
const [confirming, setConfirming] = useState(false);
|
|
59
|
+
const dateFormat = useMemo(
|
|
60
|
+
() => new Intl.DateTimeFormat(locale, { dateStyle: "medium" }),
|
|
61
|
+
[locale],
|
|
62
|
+
);
|
|
63
|
+
const format = (value: string | number | Date) => {
|
|
64
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
65
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
66
|
+
return formatDate ? formatDate(date) : dateFormat.format(date);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const requestClear = async () => {
|
|
70
|
+
if (!onClear) return;
|
|
71
|
+
if (confirmClear) {
|
|
72
|
+
if (await confirmClear()) onClear();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setConfirming(true);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<section
|
|
80
|
+
aria-label={l.memory}
|
|
81
|
+
aria-busy={loading || undefined}
|
|
82
|
+
className={clsx(
|
|
83
|
+
"m3-memory-panel flex min-h-0 flex-col gap-2 rounded-large bg-surface-container-low py-3 text-on-surface",
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<header className="flex items-center justify-between gap-2 px-4">
|
|
88
|
+
<h2 className="m-0 text-title-medium">{l.memory}</h2>
|
|
89
|
+
{onClear && memories.length > 0 && !confirming ? (
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={() => void requestClear()}
|
|
93
|
+
className={clsx(textButton, "text-error hover:bg-error/8")}
|
|
94
|
+
>
|
|
95
|
+
{l.clearMemory}
|
|
96
|
+
</button>
|
|
97
|
+
) : null}
|
|
98
|
+
</header>
|
|
99
|
+
{confirming ? (
|
|
100
|
+
<div
|
|
101
|
+
role="alertdialog"
|
|
102
|
+
aria-label={l.clearMemory}
|
|
103
|
+
className="mx-4 flex flex-col gap-2 rounded-medium bg-error-container px-4 py-3 text-on-error-container"
|
|
104
|
+
>
|
|
105
|
+
<p className="m-0 text-body-medium">{l.clearMemoryConfirm}</p>
|
|
106
|
+
<div className="flex justify-end gap-2">
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setConfirming(false)}
|
|
110
|
+
className={clsx(textButton, "hover:bg-on-error-container/8")}
|
|
111
|
+
>
|
|
112
|
+
{l.cancel}
|
|
113
|
+
</button>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={() => {
|
|
117
|
+
setConfirming(false);
|
|
118
|
+
onClear?.();
|
|
119
|
+
}}
|
|
120
|
+
className={clsx(textButton, "bg-error px-4 text-on-error hover:bg-error/92")}
|
|
121
|
+
>
|
|
122
|
+
{l.confirm}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
) : null}
|
|
127
|
+
{loading ? (
|
|
128
|
+
<div className="flex justify-center py-6">
|
|
129
|
+
<CircularIndicator size={24} label={l.memoryLoading} />
|
|
130
|
+
</div>
|
|
131
|
+
) : memories.length === 0 ? (
|
|
132
|
+
<p className="m-0 px-4 py-6 text-center text-body-medium text-on-surface-variant">
|
|
133
|
+
{l.memoryEmpty}
|
|
134
|
+
</p>
|
|
135
|
+
) : (
|
|
136
|
+
<ul className="m-0 flex min-h-0 list-none flex-col overflow-y-auto p-0">
|
|
137
|
+
{memories.map((memory) => (
|
|
138
|
+
<li
|
|
139
|
+
key={memory.id}
|
|
140
|
+
className="group flex items-start gap-3 px-4 py-2 transition-colors duration-200 ease-standard hover:bg-on-surface/8"
|
|
141
|
+
>
|
|
142
|
+
<div className="min-w-0 flex-1">
|
|
143
|
+
<p className="m-0 text-body-medium break-words whitespace-pre-wrap text-on-surface">
|
|
144
|
+
{memory.content}
|
|
145
|
+
</p>
|
|
146
|
+
<div className="mt-1 flex flex-wrap items-center gap-1.5">
|
|
147
|
+
<time className="text-label-small text-on-surface-variant">
|
|
148
|
+
{format(memory.createdAt)}
|
|
149
|
+
</time>
|
|
150
|
+
{memory.tags?.map((tag) => (
|
|
151
|
+
<span
|
|
152
|
+
key={tag}
|
|
153
|
+
className="inline-flex h-6 items-center rounded-small border border-outline-variant px-2 text-label-small text-on-surface-variant"
|
|
154
|
+
>
|
|
155
|
+
{tag}
|
|
156
|
+
</span>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
{onDelete ? (
|
|
161
|
+
<CircleButton
|
|
162
|
+
icon={<ChatIcon name="delete" size={18} />}
|
|
163
|
+
variant="standard"
|
|
164
|
+
size="sm"
|
|
165
|
+
onClick={() => onDelete(memory.id)}
|
|
166
|
+
aria-label={l.deleteMemory}
|
|
167
|
+
title={l.deleteMemory}
|
|
168
|
+
/>
|
|
169
|
+
) : null}
|
|
170
|
+
</li>
|
|
171
|
+
))}
|
|
172
|
+
</ul>
|
|
173
|
+
)}
|
|
174
|
+
</section>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0 AND MIT
|
|
2
|
+
// Derived from SpaceUI (packages/aphrody-spaceui/packages/ai/src/MessageBubble.tsx), MIT, rebuilt on Material Design 3.
|
|
3
|
+
|
|
4
|
+
"use client";
|
|
5
|
+
|
|
6
|
+
import { CircleButton } from "@aphrody/m3-primitives";
|
|
7
|
+
import { clsx } from "clsx";
|
|
8
|
+
|
|
9
|
+
import { type ChatLabels, resolveChatLabels } from "./chatLabels";
|
|
10
|
+
import { type Citation, CitationList, CitedMarkdown, RefusalNotice } from "./citations";
|
|
11
|
+
import { ChatIcon } from "./chatIcons";
|
|
12
|
+
import { Markdown, type MarkdownLinkHandler } from "./Markdown";
|
|
13
|
+
|
|
14
|
+
export interface MessageBubbleProps {
|
|
15
|
+
content: string;
|
|
16
|
+
isUser: boolean;
|
|
17
|
+
isStreaming?: boolean;
|
|
18
|
+
onCopy?: (content: string) => void;
|
|
19
|
+
onDislike?: (content: string) => void;
|
|
20
|
+
onFork?: () => void;
|
|
21
|
+
variant?: "default" | "codex";
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Visible strings (aria-labels, titles); English defaults. */
|
|
24
|
+
labels?: Partial<ChatLabels>;
|
|
25
|
+
/** Grounded answer: `[n]` markers become citation chips and the sources are listed below. */
|
|
26
|
+
citations?: readonly Citation[];
|
|
27
|
+
/** The backend refused to answer: a refusal notice replaces an empty answer. */
|
|
28
|
+
refusal?: boolean;
|
|
29
|
+
onCitationSelect?: (citation: Citation) => void;
|
|
30
|
+
/** Link activation in the answer (e.g. open the system browser from Tauri). */
|
|
31
|
+
onLinkClick?: MarkdownLinkHandler;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One chat turn. The user turn is an M3 bubble, the assistant turn is plain markdown
|
|
36
|
+
* on the surface, as in M3 / Codex conversations.
|
|
37
|
+
*/
|
|
38
|
+
export function MessageBubble({
|
|
39
|
+
content,
|
|
40
|
+
isUser,
|
|
41
|
+
isStreaming = false,
|
|
42
|
+
onCopy,
|
|
43
|
+
onDislike,
|
|
44
|
+
onFork,
|
|
45
|
+
variant = "default",
|
|
46
|
+
className,
|
|
47
|
+
labels,
|
|
48
|
+
citations,
|
|
49
|
+
refusal = false,
|
|
50
|
+
onCitationSelect,
|
|
51
|
+
onLinkClick,
|
|
52
|
+
}: MessageBubbleProps) {
|
|
53
|
+
const isCodex = variant === "codex";
|
|
54
|
+
const l = resolveChatLabels(labels);
|
|
55
|
+
const cited = !isUser && citations && citations.length > 0 ? citations : null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={clsx(
|
|
60
|
+
"group flex flex-col py-1.5 w-full",
|
|
61
|
+
isUser ? "items-end" : "items-start",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className={clsx(
|
|
67
|
+
"min-w-0",
|
|
68
|
+
isUser
|
|
69
|
+
? isCodex
|
|
70
|
+
? "max-w-[75%] rounded-[28px] bg-surface-container-high px-5 py-3 text-body-large text-on-surface shadow-xs"
|
|
71
|
+
: "max-w-[80%] rounded-large rounded-br-extra-small bg-secondary-container px-4 py-2.5 text-body-large text-on-secondary-container"
|
|
72
|
+
: "w-full max-w-full bg-transparent text-body-large text-on-surface",
|
|
73
|
+
)}
|
|
74
|
+
aria-busy={isStreaming || undefined}
|
|
75
|
+
>
|
|
76
|
+
{isUser ? (
|
|
77
|
+
<div className="whitespace-pre-wrap break-words">{content}</div>
|
|
78
|
+
) : (
|
|
79
|
+
<>
|
|
80
|
+
{refusal && !content.trim() ? (
|
|
81
|
+
<RefusalNotice labels={labels} />
|
|
82
|
+
) : cited ? (
|
|
83
|
+
<CitedMarkdown
|
|
84
|
+
content={content}
|
|
85
|
+
citations={cited}
|
|
86
|
+
labels={labels}
|
|
87
|
+
onCitationSelect={onCitationSelect}
|
|
88
|
+
onLinkClick={onLinkClick}
|
|
89
|
+
className="break-words font-sans"
|
|
90
|
+
/>
|
|
91
|
+
) : (
|
|
92
|
+
<Markdown
|
|
93
|
+
content={content}
|
|
94
|
+
size="large"
|
|
95
|
+
labels={labels}
|
|
96
|
+
onLinkClick={onLinkClick}
|
|
97
|
+
className="break-words font-sans"
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
{refusal && content.trim() ? <RefusalNotice labels={labels} className="mt-2" /> : null}
|
|
101
|
+
{cited && !isStreaming ? (
|
|
102
|
+
<CitationList citations={cited} labels={labels} onSelect={onCitationSelect} />
|
|
103
|
+
) : null}
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
{!isUser && (onCopy || onDislike || onFork) ? (
|
|
108
|
+
<div className="mt-1.5 flex items-center gap-1 opacity-0 transition-opacity duration-200 ease-standard group-focus-within:opacity-100 group-hover:opacity-100">
|
|
109
|
+
{onCopy && (
|
|
110
|
+
<CircleButton
|
|
111
|
+
icon={<ChatIcon name="content_copy" size={18} />}
|
|
112
|
+
variant="standard"
|
|
113
|
+
size="sm"
|
|
114
|
+
onClick={() => onCopy(content)}
|
|
115
|
+
aria-label={isStreaming ? l.copyStreamingMessage : l.copyMessage}
|
|
116
|
+
title={isStreaming ? l.copyStreamingMessage : l.copyMessage}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
{onDislike && (
|
|
120
|
+
<CircleButton
|
|
121
|
+
icon={<ChatIcon name="thumb_down" size={18} />}
|
|
122
|
+
variant="standard"
|
|
123
|
+
size="sm"
|
|
124
|
+
onClick={() => onDislike(content)}
|
|
125
|
+
aria-label={l.dislike}
|
|
126
|
+
title={l.dislike}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
{onFork && (
|
|
130
|
+
<CircleButton
|
|
131
|
+
icon={<ChatIcon name="call_split" size={18} />}
|
|
132
|
+
variant="standard"
|
|
133
|
+
size="sm"
|
|
134
|
+
onClick={onFork}
|
|
135
|
+
aria-label={l.fork}
|
|
136
|
+
title={l.fork}
|
|
137
|
+
/>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
) : null}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|