handle ActivitySnapshotEvent in web frontend and fix CopilotKit 1.54.0 styling
This commit is contained in:
parent
1d108b42ec
commit
9e29a7e849
4 changed files with 636 additions and 252 deletions
|
|
@ -36,9 +36,38 @@ a {
|
||||||
--ring: #3b82f6;
|
--ring: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chat message area — use full width instead of max-w-3xl */
|
/* Chat message area — use full width instead of CopilotKit's max-width constraints.
|
||||||
.chat-content .max-w-3xl {
|
Scoped to scroll area; the input area inherits CopilotKit's natural sizing. */
|
||||||
max-width: 100%;
|
.chat-scroll-area .max-w-3xl,
|
||||||
|
.chat-scroll-area [class*="cpk:max-w-3xl"],
|
||||||
|
.chat-scroll-area [class*="cpk:max-w-"] {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In the empty state (no chat-scroll-area), widen only the message view container */
|
||||||
|
.copilotKitChat [class*="cpk:max-w-3xl"]:not(:has(.copilotKitInput)) {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state input — stretch to full width with horizontal margin */
|
||||||
|
.copilotKitChat [class*="cpk:max-w-3xl"]:has(.copilotKitInput) {
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input box — breathing room for send button and bottom spacing */
|
||||||
|
.copilotKitInput {
|
||||||
|
padding-right: 0.5rem !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User message bubble — more generous padding and spacing */
|
||||||
|
.copilotKitUserMessage {
|
||||||
|
margin-top: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copilotKitUserMessage [class*="cpk:rounded"] {
|
||||||
|
padding: 12px 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Render-prop layout: flex column so citations sit between messages and input */
|
/* Render-prop layout: flex column so citations sit between messages and input */
|
||||||
|
|
@ -57,7 +86,7 @@ a {
|
||||||
|
|
||||||
.chat-input-area {
|
.chat-input-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 0.75rem 0.5rem;
|
padding: 0 1rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override CopilotKit's absolute positioning on the input container
|
/* Override CopilotKit's absolute positioning on the input container
|
||||||
|
|
@ -69,6 +98,11 @@ a {
|
||||||
right: auto;
|
right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Make the input stretch to full width within its padded container */
|
||||||
|
.chat-input-area [class*="cpk:max-w-"] {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* User message bubble spacing and padding */
|
/* User message bubble spacing and padding */
|
||||||
[data-message-id].items-end {
|
[data-message-id].items-end {
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,39 @@ function ToolCallIndicator({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render an activity message from a skill sub-agent tool call/result
|
||||||
|
function ActivityIndicator({
|
||||||
|
message,
|
||||||
|
isComplete,
|
||||||
|
}: {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: AG-UI activity message shape
|
||||||
|
message: any;
|
||||||
|
isComplete: boolean;
|
||||||
|
}) {
|
||||||
|
const content = message.content ?? {};
|
||||||
|
const toolName = content.tool_name ?? "tool";
|
||||||
|
|
||||||
|
let args: Record<string, unknown> = {};
|
||||||
|
if (content.args) {
|
||||||
|
try {
|
||||||
|
args =
|
||||||
|
typeof content.args === "string"
|
||||||
|
? JSON.parse(content.args)
|
||||||
|
: content.args;
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolCallIndicator
|
||||||
|
toolName={toolName}
|
||||||
|
status={isComplete ? "complete" : "loading"}
|
||||||
|
args={args}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Context for sharing chat state with the message view
|
// Context for sharing chat state with the message view
|
||||||
const ChatStateContext = createContext<RAGState | null>(null);
|
const ChatStateContext = createContext<RAGState | null>(null);
|
||||||
|
|
||||||
|
|
@ -261,6 +294,21 @@ function MessageViewWithCitations({
|
||||||
const ragState = useContext(ChatStateContext);
|
const ragState = useContext(ChatStateContext);
|
||||||
const citationsHistory = ragState ? deriveCitationsHistory(ragState) : [];
|
const citationsHistory = ragState ? deriveCitationsHistory(ragState) : [];
|
||||||
|
|
||||||
|
// Collect completed tool_call_ids from skill_tool_result activity messages
|
||||||
|
const completedToolCallIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (
|
||||||
|
msg.role === "activity" &&
|
||||||
|
msg.activityType === "skill_tool_result" &&
|
||||||
|
msg.content?.tool_call_id
|
||||||
|
) {
|
||||||
|
ids.add(msg.content.tool_call_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
const cursor = isRunning ? (
|
const cursor = isRunning ? (
|
||||||
<div key="cursor" className="streaming-cursor">
|
<div key="cursor" className="streaming-cursor">
|
||||||
<span className="dot" />
|
<span className="dot" />
|
||||||
|
|
@ -273,17 +321,63 @@ function MessageViewWithCitations({
|
||||||
<CopilotChatMessageView messages={messages} isRunning={isRunning}>
|
<CopilotChatMessageView messages={messages} isRunning={isRunning}>
|
||||||
{({ messageElements }) => {
|
{({ messageElements }) => {
|
||||||
if (!citationsHistory.length) {
|
if (!citationsHistory.length) {
|
||||||
|
// Check if there are activity messages to render
|
||||||
|
const hasActivity = messages.some(
|
||||||
|
(msg: { role: string }) => msg.role === "activity",
|
||||||
|
);
|
||||||
|
if (!hasActivity) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{messageElements}
|
||||||
|
{cursor}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interleave activity indicators with CopilotKit elements
|
||||||
|
const result: React.ReactNode[] = [];
|
||||||
|
let eIdx = 0;
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (
|
||||||
|
msg.role === "activity" &&
|
||||||
|
msg.activityType === "skill_tool_call"
|
||||||
|
) {
|
||||||
|
const toolCallId = msg.content?.tool_call_id;
|
||||||
|
result.push(
|
||||||
|
<ActivityIndicator
|
||||||
|
key={msg.id}
|
||||||
|
message={msg}
|
||||||
|
isComplete={
|
||||||
|
toolCallId ? completedToolCallIds.has(toolCallId) : false
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (msg.role === "activity") continue;
|
||||||
|
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
||||||
|
if (eIdx < messageElements.length) {
|
||||||
|
result.push(messageElements[eIdx]);
|
||||||
|
eIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (eIdx < messageElements.length) {
|
||||||
|
result.push(messageElements[eIdx]);
|
||||||
|
eIdx++;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{messageElements}
|
{result}
|
||||||
{cursor}
|
{cursor}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopilotChatMessageView renders one element per user/assistant/activity
|
// CopilotChatMessageView renders one element per user/assistant
|
||||||
// message (tool messages produce nothing). We correlate elements with
|
// message (tool messages produce nothing). We correlate elements with
|
||||||
// messages to inject CitationBlocks after the right assistant responses.
|
// messages to inject CitationBlocks after the right assistant responses.
|
||||||
|
// Activity messages (skill sub-agent tool calls) are rendered by us
|
||||||
|
// directly, not by CopilotKit.
|
||||||
//
|
//
|
||||||
// Both search and ask tools append to citations via qa_history,
|
// Both search and ask tools append to citations via qa_history,
|
||||||
// so after each assistant text response that followed tool calls,
|
// so after each assistant text response that followed tool calls,
|
||||||
|
|
@ -306,10 +400,28 @@ function MessageViewWithCitations({
|
||||||
seenToolCalls = true;
|
seenToolCalls = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRendered =
|
// Activity messages are not rendered by CopilotKit —
|
||||||
msg.role === "user" ||
|
// render them ourselves without consuming messageElements
|
||||||
msg.role === "assistant" ||
|
if (msg.role === "activity") {
|
||||||
msg.role === "activity";
|
if (msg.activityType === "skill_tool_call") {
|
||||||
|
const toolCallId = msg.content?.tool_call_id;
|
||||||
|
const isComplete = toolCallId
|
||||||
|
? completedToolCallIds.has(toolCallId)
|
||||||
|
: false;
|
||||||
|
result.push(
|
||||||
|
<ActivityIndicator
|
||||||
|
key={msg.id}
|
||||||
|
message={msg}
|
||||||
|
isComplete={isComplete}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Skip skill_tool_result — completion is shown
|
||||||
|
// on the corresponding skill_tool_call indicator
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRendered = msg.role === "user" || msg.role === "assistant";
|
||||||
if (!isRendered) continue;
|
if (!isRendered) continue;
|
||||||
|
|
||||||
if (elemIdx < messageElements.length) {
|
if (elemIdx < messageElements.length) {
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@
|
||||||
"format": "biome check --write app components lib"
|
"format": "biome check --write app components lib"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ag-ui/client": "^0.0.43",
|
"@ag-ui/client": "^0.0.47",
|
||||||
"@copilotkit/react-core": "^1.51.4",
|
"@copilotkit/react-core": "^1.54.0",
|
||||||
"@copilotkit/runtime": "^1.51.4",
|
"@copilotkit/runtime": "^1.54.0",
|
||||||
|
"zod": "^3.24.0",
|
||||||
"next": "^16.1.1",
|
"next": "^16.1.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue