cb15c5e0d8
Release / Check for new version (push) Has been cancelled
Release / Build macOS ARM64 (push) Has been cancelled
Release / Build macOS x64 (push) Has been cancelled
Release / Build Linux ARM64 (push) Has been cancelled
Release / Build Linux musl ARM64 (push) Has been cancelled
Release / Build Linux musl x64 (push) Has been cancelled
Release / Build Linux x64 (push) Has been cancelled
Release / Build Windows x64 (push) Has been cancelled
Release / Publish to npm (push) Has been cancelled
Release / Publish sandbox package to npm (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
CI / Rust (windows-latest - x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Native E2E Tests (push) Has been cancelled
CI / Windows Integration Test (push) Has been cancelled
CI / Global Install (macos-latest) (push) Has been cancelled
CI / Global Install (ubuntu-latest) (push) Has been cancelled
CI / Version Sync Check (push) Has been cancelled
CI / Rust (push) Has been cancelled
CI / Dashboard (push) Has been cancelled
CI / Sandbox Package (push) Has been cancelled
CI / Rust (macos-latest - aarch64-apple-darwin) (push) Has been cancelled
CI / Rust (macos-latest - x86_64-apple-darwin) (push) Has been cancelled
CI / Global Install (windows-latest) (push) Has been cancelled
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
"use client";
|
|
|
|
import type { EveDynamicToolPart, EveMessage, EveMessagePart } from "eve/react";
|
|
import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message";
|
|
import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning";
|
|
import {
|
|
Tool,
|
|
ToolContent,
|
|
ToolHeader,
|
|
ToolInput,
|
|
ToolOutput,
|
|
} from "@/components/ai-elements/tool";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export type AgentInputResponse = {
|
|
readonly optionId?: string;
|
|
readonly requestId: string;
|
|
readonly text?: string;
|
|
};
|
|
|
|
export function AgentMessage({
|
|
canRespond,
|
|
isStreaming,
|
|
message,
|
|
onInputResponses,
|
|
}: {
|
|
readonly canRespond: boolean;
|
|
readonly isStreaming: boolean;
|
|
readonly message: EveMessage;
|
|
readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;
|
|
}) {
|
|
const lastTextIndex = message.parts.reduce(
|
|
(last, part, index) => (part.type === "text" ? index : last),
|
|
-1,
|
|
);
|
|
|
|
return (
|
|
<Message
|
|
data-optimistic={message.metadata?.optimistic ? "true" : undefined}
|
|
from={message.role}
|
|
>
|
|
<MessageContent>
|
|
{message.parts.map((part, index) => (
|
|
<AgentMessagePart
|
|
canRespond={canRespond}
|
|
key={partKey(part, index)}
|
|
onInputResponses={onInputResponses}
|
|
part={part}
|
|
showCaret={isStreaming && message.role === "assistant" && index === lastTextIndex}
|
|
/>
|
|
))}
|
|
</MessageContent>
|
|
</Message>
|
|
);
|
|
}
|
|
|
|
function AgentMessagePart({
|
|
canRespond,
|
|
onInputResponses,
|
|
part,
|
|
showCaret,
|
|
}: {
|
|
readonly canRespond: boolean;
|
|
readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;
|
|
readonly part: EveMessagePart;
|
|
readonly showCaret: boolean;
|
|
}) {
|
|
switch (part.type) {
|
|
case "step-start":
|
|
return null;
|
|
case "text":
|
|
return (
|
|
<MessageResponse caret="block" isAnimating={showCaret}>
|
|
{part.text}
|
|
</MessageResponse>
|
|
);
|
|
case "reasoning":
|
|
return (
|
|
<Reasoning defaultOpen isStreaming={part.state === "streaming"}>
|
|
<ReasoningTrigger />
|
|
<ReasoningContent>{part.text}</ReasoningContent>
|
|
</Reasoning>
|
|
);
|
|
case "dynamic-tool":
|
|
return (
|
|
<Tool
|
|
defaultOpen={part.state === "approval-requested" || part.state === "approval-responded"}
|
|
>
|
|
<ToolHeader
|
|
state={part.state}
|
|
title={part.toolName}
|
|
toolName={part.toolName}
|
|
type="dynamic-tool"
|
|
/>
|
|
<ToolContent>
|
|
<ToolInput input={part.input} />
|
|
<InputRequestActions
|
|
canRespond={canRespond}
|
|
part={part}
|
|
onInputResponses={onInputResponses}
|
|
/>
|
|
<ToolOutput errorText={part.errorText} output={part.output} />
|
|
</ToolContent>
|
|
</Tool>
|
|
);
|
|
}
|
|
}
|
|
|
|
function InputRequestActions({
|
|
canRespond,
|
|
onInputResponses,
|
|
part,
|
|
}: {
|
|
readonly canRespond: boolean;
|
|
readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;
|
|
readonly part: EveDynamicToolPart;
|
|
}) {
|
|
const inputRequest = part.toolMetadata?.eve?.inputRequest;
|
|
if (!inputRequest) {
|
|
return null;
|
|
}
|
|
|
|
const inputResponse = part.toolMetadata?.eve?.inputResponse;
|
|
const selectedOption = inputRequest.options?.find(
|
|
(option) => option.id === inputResponse?.optionId,
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-3 rounded-md border border-yellow-500/30 bg-yellow-500/5 p-3">
|
|
<p className="text-muted-foreground text-sm">{inputRequest.prompt}</p>
|
|
{inputResponse ? (
|
|
<p className="font-medium text-sm">
|
|
Responded: {selectedOption?.label ?? inputResponse.text ?? inputResponse.optionId}
|
|
</p>
|
|
) : (
|
|
<div className="flex flex-wrap gap-2">
|
|
{inputRequest.options?.map((option) => (
|
|
<Button
|
|
disabled={!canRespond}
|
|
key={option.id}
|
|
onClick={() => {
|
|
void onInputResponses([
|
|
{
|
|
optionId: option.id,
|
|
requestId: inputRequest.requestId,
|
|
},
|
|
]);
|
|
}}
|
|
size="sm"
|
|
type="button"
|
|
variant={option.style === "danger" ? "destructive" : "default"}
|
|
>
|
|
{option.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function partKey(part: EveMessagePart, index: number): string {
|
|
switch (part.type) {
|
|
case "dynamic-tool":
|
|
return part.toolCallId;
|
|
default:
|
|
return `${part.type}:${index}`;
|
|
}
|
|
}
|