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;
|
||||
}
|
||||
|
||||
/* Chat message area — use full width instead of max-w-3xl */
|
||||
.chat-content .max-w-3xl {
|
||||
max-width: 100%;
|
||||
/* Chat message area — use full width instead of CopilotKit's max-width constraints.
|
||||
Scoped to scroll area; the input area inherits CopilotKit's natural sizing. */
|
||||
.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 */
|
||||
|
|
@ -57,7 +86,7 @@ a {
|
|||
|
||||
.chat-input-area {
|
||||
position: relative;
|
||||
padding: 0 0.75rem 0.5rem;
|
||||
padding: 0 1rem 1rem;
|
||||
}
|
||||
|
||||
/* Override CopilotKit's absolute positioning on the input container
|
||||
|
|
@ -69,6 +98,11 @@ a {
|
|||
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 */
|
||||
[data-message-id].items-end {
|
||||
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
|
||||
const ChatStateContext = createContext<RAGState | null>(null);
|
||||
|
||||
|
|
@ -261,6 +294,21 @@ function MessageViewWithCitations({
|
|||
const ragState = useContext(ChatStateContext);
|
||||
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 ? (
|
||||
<div key="cursor" className="streaming-cursor">
|
||||
<span className="dot" />
|
||||
|
|
@ -273,17 +321,63 @@ function MessageViewWithCitations({
|
|||
<CopilotChatMessageView messages={messages} isRunning={isRunning}>
|
||||
{({ messageElements }) => {
|
||||
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 (
|
||||
<>
|
||||
{messageElements}
|
||||
{result}
|
||||
{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
|
||||
// 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,
|
||||
// so after each assistant text response that followed tool calls,
|
||||
|
|
@ -306,10 +400,28 @@ function MessageViewWithCitations({
|
|||
seenToolCalls = true;
|
||||
}
|
||||
|
||||
const isRendered =
|
||||
msg.role === "user" ||
|
||||
msg.role === "assistant" ||
|
||||
msg.role === "activity";
|
||||
// Activity messages are not rendered by CopilotKit —
|
||||
// render them ourselves without consuming messageElements
|
||||
if (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 (elemIdx < messageElements.length) {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@
|
|||
"format": "biome check --write app components lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-ui/client": "^0.0.43",
|
||||
"@copilotkit/react-core": "^1.51.4",
|
||||
"@copilotkit/runtime": "^1.51.4",
|
||||
"@ag-ui/client": "^0.0.47",
|
||||
"@copilotkit/react-core": "^1.54.0",
|
||||
"@copilotkit/runtime": "^1.54.0",
|
||||
"zod": "^3.24.0",
|
||||
"next": "^16.1.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue