feat: render presentation chat with assistant ui
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||||
|
import { AssistantOperatorThread } from "./AssistantOperatorThread.js";
|
||||||
|
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
describe("AssistantOperatorThread", () => {
|
||||||
|
it("renders text interleaved with a collapsed tool call", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
|
{
|
||||||
|
id: "assistant-1",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [
|
||||||
|
{ type: "text", text: "I will inspect the run." },
|
||||||
|
{
|
||||||
|
type: "tool-call",
|
||||||
|
call: { id: "call-1", name: "readRunTrace", input: { run_id: "run_1" } },
|
||||||
|
},
|
||||||
|
{ type: "text", text: "The trace is inspectable." },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("log", { name: /operator conversation/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("I will inspect the run.")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("The trace is inspectable.")).toBeInTheDocument();
|
||||||
|
const tool = screen.getByRole("button", { name: /readRunTrace/i });
|
||||||
|
expect(screen.queryByText(/run_1/)).not.toBeInTheDocument();
|
||||||
|
await user.click(tool);
|
||||||
|
expect(screen.getByText(/run_1/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders grouped consecutive tool calls", () => {
|
||||||
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
|
{
|
||||||
|
id: "assistant-tools",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [
|
||||||
|
{ type: "tool-call", call: { id: "call-1", name: "readRunTrace", input: {} } },
|
||||||
|
{ type: "tool-call", call: { id: "call-2", name: "inspectDeployment", input: { deployment_id: "demo" } } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(<AssistantOperatorThread mode="dock" messages={messages} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/2 tools/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /readRunTrace/i })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /inspectDeployment/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders schema approval through the existing approval surface", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const submit = vi.fn();
|
||||||
|
const cancel = vi.fn();
|
||||||
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
|
{
|
||||||
|
id: "approval",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
type: "approval-request",
|
||||||
|
callId: "call-approval",
|
||||||
|
name: "resumeIssueReview",
|
||||||
|
prompt: "Submit resume request?",
|
||||||
|
contract: {
|
||||||
|
kind: "issue_review",
|
||||||
|
outcomes: ["submitted", "cancelled"],
|
||||||
|
resumeSchema: { type: "object" },
|
||||||
|
resumePayloadPreview: { selected_issue_ids: ["risk-1"] },
|
||||||
|
runId: "run_recorded_lda_report",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AssistantOperatorThread
|
||||||
|
mode="dock"
|
||||||
|
messages={messages}
|
||||||
|
submitApproval={submit}
|
||||||
|
cancelApproval={cancel}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("group", { name: /issue review resume/i })).toBeInTheDocument();
|
||||||
|
await user.click(screen.getByRole("button", { name: "Submit" }));
|
||||||
|
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||||
|
expect(submit).toHaveBeenCalledOnce();
|
||||||
|
expect(cancel).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders a chat-owned run action", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const run = vi.fn();
|
||||||
|
render(
|
||||||
|
<AssistantOperatorThread
|
||||||
|
mode="dock"
|
||||||
|
messages={[]}
|
||||||
|
runAction={{ label: "Run prepared workflow", disabled: false, run }}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: /run prepared workflow/i }));
|
||||||
|
expect(run).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
AssistantRuntimeProvider,
|
||||||
|
MessagePrimitive,
|
||||||
|
ThreadPrimitive,
|
||||||
|
useExternalStoreRuntime,
|
||||||
|
type AppendMessage,
|
||||||
|
} from "@assistant-ui/react";
|
||||||
|
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||||
|
import { SchemaApprovalSurface } from "../approval/SchemaApprovalSurface.js";
|
||||||
|
import { projectAgentMessagesForAssistant, type AssistantProjectedMessage } from "./assistantRuntimeProjection.js";
|
||||||
|
|
||||||
|
type AssistantOperatorThreadProps = {
|
||||||
|
readonly mode: "hidden" | "full" | "rail" | "dock";
|
||||||
|
readonly messages: ReadonlyArray<AgentMessage>;
|
||||||
|
readonly runAction?: { readonly label: string; readonly disabled: boolean; readonly run: () => void } | undefined;
|
||||||
|
readonly submitApproval?: (() => void) | undefined;
|
||||||
|
readonly cancelApproval?: (() => void) | undefined;
|
||||||
|
readonly ariaLabel?: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatJson = (value: unknown): string => {
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolCard = ({
|
||||||
|
toolName,
|
||||||
|
args,
|
||||||
|
result,
|
||||||
|
isError,
|
||||||
|
}: {
|
||||||
|
readonly toolName: string;
|
||||||
|
readonly args?: unknown;
|
||||||
|
readonly result?: unknown;
|
||||||
|
readonly isError?: boolean;
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className="assistant-tool-card" data-open={open ? "true" : "false"}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="assistant-tool-card__trigger"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen((c) => !c)}
|
||||||
|
>
|
||||||
|
<span className="assistant-tool-card__label">Used tool: <b>{toolName}</b></span>
|
||||||
|
</button>
|
||||||
|
{open ? (
|
||||||
|
<div className="assistant-tool-card__body">
|
||||||
|
{args !== undefined ? (
|
||||||
|
<pre className="assistant-tool-card__io" aria-label="tool input">{formatJson(args)}</pre>
|
||||||
|
) : null}
|
||||||
|
{result !== undefined ? (
|
||||||
|
<div className="assistant-tool-card__result" data-error={isError ? "true" : undefined}>
|
||||||
|
<span>{isError ? "error" : "success"}</span>
|
||||||
|
<pre className="assistant-tool-card__io" aria-label="tool output">{formatJson(result)}</pre>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolGroupCard = ({
|
||||||
|
toolCount,
|
||||||
|
defaultOpen = true,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
readonly toolCount: number;
|
||||||
|
readonly defaultOpen?: boolean;
|
||||||
|
readonly children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(defaultOpen);
|
||||||
|
return (
|
||||||
|
<div className="assistant-tool-group" data-open={open ? "true" : "false"}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="assistant-tool-group__trigger"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen((c) => !c)}
|
||||||
|
>
|
||||||
|
<span>{toolCount} tools</span>
|
||||||
|
</button>
|
||||||
|
{open ? <div className="assistant-tool-group__body">{children}</div> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentPart =
|
||||||
|
| { readonly type: "text"; readonly text: string }
|
||||||
|
| { readonly type: "tool-call"; readonly toolName: string; readonly args?: unknown; readonly result?: unknown; readonly isError?: boolean };
|
||||||
|
|
||||||
|
const renderContentPart = (
|
||||||
|
part: ContentPart,
|
||||||
|
submitApproval?: (() => void) | undefined,
|
||||||
|
cancelApproval?: (() => void) | undefined,
|
||||||
|
): React.ReactNode => {
|
||||||
|
if (part.type === "text") {
|
||||||
|
return <MessagePrimitive.Content />;
|
||||||
|
}
|
||||||
|
const toolName = part.toolName;
|
||||||
|
const args = part.args as Record<string, unknown> | undefined;
|
||||||
|
const contract = args?.contract as
|
||||||
|
| {
|
||||||
|
readonly kind: string;
|
||||||
|
readonly outcomes: readonly string[];
|
||||||
|
readonly resumeSchema: unknown;
|
||||||
|
readonly resumePayloadPreview: unknown;
|
||||||
|
readonly runId: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
if (toolName === "resumeIssueReview" && contract) {
|
||||||
|
return (
|
||||||
|
<div className="assistant-tool-approval">
|
||||||
|
<SchemaApprovalSurface
|
||||||
|
title={`${contract.kind.replaceAll("_", " ")} resume`}
|
||||||
|
schema={contract.resumeSchema}
|
||||||
|
payload={contract.resumePayloadPreview}
|
||||||
|
outcomes={contract.outcomes}
|
||||||
|
runId={contract.runId}
|
||||||
|
onSubmit={submitApproval}
|
||||||
|
onCancel={cancelApproval}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <ToolCard toolName={toolName} args={part.args} result={part.result} isError={part.isError ?? false} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssistantMessageBody = ({
|
||||||
|
parts,
|
||||||
|
submitApproval,
|
||||||
|
cancelApproval,
|
||||||
|
}: {
|
||||||
|
readonly parts: readonly ContentPart[];
|
||||||
|
readonly submitApproval?: (() => void) | undefined;
|
||||||
|
readonly cancelApproval?: (() => void) | undefined;
|
||||||
|
}) => {
|
||||||
|
const textParts = parts.filter((p) => p.type === "text");
|
||||||
|
const toolParts = parts.filter((p) => p.type === "tool-call");
|
||||||
|
|
||||||
|
if (toolParts.length === 0) {
|
||||||
|
return <MessagePrimitive.Content />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolParts.length === 1) {
|
||||||
|
const firstToolIndex = parts.findIndex((p) => p.type === "tool-call");
|
||||||
|
const beforeText = parts.slice(0, firstToolIndex).filter((p) => p.type === "text");
|
||||||
|
const afterText = parts.slice(firstToolIndex + 1).filter((p) => p.type === "text");
|
||||||
|
const tool = toolParts[0]!;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{beforeText.map((p, i) => <p key={`before-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
||||||
|
{renderContentPart(tool, submitApproval, cancelApproval)}
|
||||||
|
{afterText.map((p, i) => <p key={`after-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstToolIndex = parts.findIndex((p) => p.type === "tool-call");
|
||||||
|
const beforeText = parts.slice(0, firstToolIndex).filter((p) => p.type === "text");
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{beforeText.map((p, i) => <p key={`before-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
|
||||||
|
<ToolGroupCard toolCount={toolParts.length}>
|
||||||
|
{toolParts.map((tool, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
{renderContentPart(tool, submitApproval, cancelApproval)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ToolGroupCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThreadBody = ({
|
||||||
|
submitApproval,
|
||||||
|
cancelApproval,
|
||||||
|
}: {
|
||||||
|
readonly submitApproval?: (() => void) | undefined;
|
||||||
|
readonly cancelApproval?: (() => void) | undefined;
|
||||||
|
}) => (
|
||||||
|
<ThreadPrimitive.Root className="assistant-thread">
|
||||||
|
<ThreadPrimitive.Viewport className="assistant-thread__viewport">
|
||||||
|
<ThreadPrimitive.Messages>
|
||||||
|
{({ message }) => {
|
||||||
|
const content = (message as unknown as { content?: unknown }).content;
|
||||||
|
const parts: ContentPart[] = Array.isArray(content)
|
||||||
|
? (content as ContentPart[])
|
||||||
|
: typeof content === "string"
|
||||||
|
? [{ type: "text" as const, text: content }]
|
||||||
|
: [];
|
||||||
|
return (
|
||||||
|
<MessagePrimitive.Root
|
||||||
|
className="assistant-message"
|
||||||
|
data-role={message.role}
|
||||||
|
>
|
||||||
|
<AssistantMessageBody
|
||||||
|
parts={parts}
|
||||||
|
submitApproval={submitApproval}
|
||||||
|
cancelApproval={cancelApproval}
|
||||||
|
/>
|
||||||
|
</MessagePrimitive.Root>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</ThreadPrimitive.Messages>
|
||||||
|
</ThreadPrimitive.Viewport>
|
||||||
|
</ThreadPrimitive.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AssistantOperatorThread = ({
|
||||||
|
mode,
|
||||||
|
messages,
|
||||||
|
runAction,
|
||||||
|
submitApproval,
|
||||||
|
cancelApproval,
|
||||||
|
ariaLabel = "operator conversation",
|
||||||
|
}: AssistantOperatorThreadProps) => {
|
||||||
|
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
|
||||||
|
const [localMessages, setLocalMessages] = useState<AssistantProjectedMessage[]>([]);
|
||||||
|
const runtimeMessages = projected.length > 0 ? projected : localMessages;
|
||||||
|
|
||||||
|
const onNew = useCallback(async (message: AppendMessage) => {
|
||||||
|
const text = message.content.find((part) => part.type === "text")?.text ?? "";
|
||||||
|
setLocalMessages((current) => [
|
||||||
|
...current,
|
||||||
|
{
|
||||||
|
id: `local-${current.length + 1}`,
|
||||||
|
role: "user" as const,
|
||||||
|
content: [{ type: "text" as const, text }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const runtime = useExternalStoreRuntime({
|
||||||
|
messages: runtimeMessages,
|
||||||
|
setMessages: (msgs) => setLocalMessages([...msgs]),
|
||||||
|
onNew,
|
||||||
|
convertMessage: (message) => message,
|
||||||
|
isRunning: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="assistant-operator-thread" data-mode={mode} role="log" aria-label={ariaLabel}>
|
||||||
|
{runAction ? (
|
||||||
|
<div className="assistant-operator-thread__action">
|
||||||
|
<button type="button" disabled={runAction.disabled} onClick={runAction.run}>
|
||||||
|
{runAction.label}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<AssistantRuntimeProvider runtime={runtime}>
|
||||||
|
<ThreadBody submitApproval={submitApproval} cancelApproval={cancelApproval} />
|
||||||
|
</AssistantRuntimeProvider>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -36,7 +36,7 @@ describe("assistantRuntimeProjection", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("projects tool results as tool-role messages", () => {
|
it("projects tool results as text summary", () => {
|
||||||
const messages: ReadonlyArray<AgentMessage> = [
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
{
|
{
|
||||||
id: "tool-result-message",
|
id: "tool-result-message",
|
||||||
@@ -59,15 +59,9 @@ describe("assistantRuntimeProjection", () => {
|
|||||||
|
|
||||||
expect(projected[0]).toMatchObject({
|
expect(projected[0]).toMatchObject({
|
||||||
id: "tool-result-message",
|
id: "tool-result-message",
|
||||||
role: "tool",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
{
|
{ type: "text", text: "Result for readRunTrace: success" },
|
||||||
type: "tool-result",
|
|
||||||
toolCallId: "call-1",
|
|
||||||
toolName: "readRunTrace",
|
|
||||||
result: { frames: 3 },
|
|
||||||
isError: false,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,27 +8,22 @@ export type AssistantToolRenderPayload = {
|
|||||||
readonly isError?: boolean;
|
readonly isError?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JSON-serializable args for assistant-ui compatibility
|
||||||
|
type JsonArgs = Record<string, any>;
|
||||||
|
|
||||||
type AssistantContentPart =
|
type AssistantContentPart =
|
||||||
| { readonly type: "text"; readonly text: string }
|
| { readonly type: "text"; readonly text: string }
|
||||||
| {
|
| {
|
||||||
readonly type: "tool-call";
|
readonly type: "tool-call";
|
||||||
readonly toolCallId: string;
|
readonly toolCallId?: string;
|
||||||
readonly toolName: string;
|
readonly toolName: string;
|
||||||
readonly args: unknown;
|
readonly args?: JsonArgs;
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly type: "tool-result";
|
|
||||||
readonly toolCallId: string;
|
|
||||||
readonly toolName: string;
|
|
||||||
readonly result: unknown;
|
|
||||||
readonly isError: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AssistantProjectedMessage = {
|
export type AssistantProjectedMessage = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly role: "user" | "assistant" | "tool";
|
readonly role: "user" | "assistant";
|
||||||
readonly content: ReadonlyArray<AssistantContentPart>;
|
readonly content: ReadonlyArray<AssistantContentPart>;
|
||||||
readonly metadata?: { readonly unstable_state?: string | undefined };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
||||||
@@ -40,22 +35,19 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
|||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
toolCallId: part.call.id,
|
toolCallId: part.call.id,
|
||||||
toolName: part.call.name,
|
toolName: part.call.name,
|
||||||
args: part.call.input,
|
args: part.call.input as JsonArgs,
|
||||||
}];
|
}];
|
||||||
case "tool-result":
|
case "tool-result":
|
||||||
return [{
|
return [{
|
||||||
type: "tool-result",
|
type: "text",
|
||||||
toolCallId: part.result.callId,
|
text: `Result for ${part.result.name}: ${part.result.status === "failure" ? "error" : "success"}`,
|
||||||
toolName: part.result.name,
|
|
||||||
result: part.result.output,
|
|
||||||
isError: part.result.status === "failure",
|
|
||||||
}];
|
}];
|
||||||
case "presentation-action":
|
case "presentation-action":
|
||||||
return [{
|
return [{
|
||||||
type: "tool-call",
|
type: "tool-call",
|
||||||
toolCallId: `presentation-${part.action.type}`,
|
toolCallId: `presentation-${part.action.type}`,
|
||||||
toolName: `presentation.${part.action.type}`,
|
toolName: `presentation.${part.action.type}`,
|
||||||
args: part.action,
|
args: part.action as JsonArgs,
|
||||||
}];
|
}];
|
||||||
case "approval-request":
|
case "approval-request":
|
||||||
return [
|
return [
|
||||||
@@ -67,7 +59,7 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
|||||||
args: {
|
args: {
|
||||||
prompt: part.prompt,
|
prompt: part.prompt,
|
||||||
contract: part.contract,
|
contract: part.contract,
|
||||||
},
|
} as JsonArgs,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
case "error":
|
case "error":
|
||||||
@@ -76,9 +68,6 @@ const projectPart = (part: AgentMessagePart): AssistantContentPart[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const messageRoleFor = (message: AgentMessage): AssistantProjectedMessage["role"] => {
|
const messageRoleFor = (message: AgentMessage): AssistantProjectedMessage["role"] => {
|
||||||
const onlyToolResults = message.parts.length > 0
|
|
||||||
&& message.parts.every((part) => part.type === "tool-result");
|
|
||||||
if (onlyToolResults) return "tool";
|
|
||||||
return message.role === "user" ? "user" : "assistant";
|
return message.role === "user" ? "user" : "assistant";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1638,6 +1638,145 @@
|
|||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assistant-operator-thread {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 0.55rem;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-operator-thread[data-mode="hidden"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-operator-thread__action button {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--accent-cyan);
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
background: color-mix(in oklch, var(--accent-cyan) 13%, var(--stage-surface));
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
font: 700 0.8rem/1 var(--font-interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-thread,
|
||||||
|
.assistant-thread__viewport {
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-thread__viewport {
|
||||||
|
overflow: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-thread__viewport::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card {
|
||||||
|
border: 1px solid var(--stage-line);
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
background: var(--stage-inset);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card__trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: 600 0.78rem/1 var(--font-interface);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card__trigger:hover {
|
||||||
|
background: color-mix(in oklch, var(--accent-cyan) 8%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card__body {
|
||||||
|
padding: 0 0.65rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card__io {
|
||||||
|
background: var(--stage-surface);
|
||||||
|
border: 1px solid var(--stage-line);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
|
font: 0.72rem/1.45 var(--font-mono);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card__result {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
font: 600 0.7rem/1 var(--font-interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card__result span {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-card__result[data-error="true"] span {
|
||||||
|
color: var(--accent-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-group {
|
||||||
|
border: 1px solid var(--stage-line);
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
background: var(--stage-inset);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-group__trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font: 600 0.78rem/1 var(--font-interface);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-group__trigger:hover {
|
||||||
|
background: color-mix(in oklch, var(--accent-cyan) 8%, transparent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-group__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0 0.65rem 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-group__body .assistant-tool-card {
|
||||||
|
border-color: var(--stage-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-tool-approval {
|
||||||
|
border: 1px solid color-mix(in oklch, var(--accent-cyan) 50%, var(--stage-line));
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--stage-inset);
|
||||||
|
padding: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
.operator-chat .ai-chat-conversation {
|
.operator-chat .ai-chat-conversation {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -28,3 +28,7 @@ if (!globalThis.DOMRect) {
|
|||||||
}),
|
}),
|
||||||
} as unknown as typeof DOMRect;
|
} as unknown as typeof DOMRect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!HTMLDivElement.prototype.scrollTo) {
|
||||||
|
HTMLDivElement.prototype.scrollTo = () => {};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user