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
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEveAgent } from "eve/react";
|
|
import { AlertCircleIcon } from "lucide-react";
|
|
import {
|
|
Conversation,
|
|
ConversationContent,
|
|
ConversationScrollButton,
|
|
} from "@/components/ai-elements/conversation";
|
|
import {
|
|
PromptInput,
|
|
type PromptInputMessage,
|
|
PromptInputSubmit,
|
|
PromptInputTextarea,
|
|
} from "@/components/ai-elements/prompt-input";
|
|
import { cn } from "@/lib/utils";
|
|
import { AgentMessage } from "./agent-message";
|
|
|
|
const AGENT_NAME = "Agent Browser Sandbox";
|
|
const BETA_TERMS_HREF = "https://vercel.com/docs/release-phases/public-beta-agreement";
|
|
|
|
type AgentStatus = ReturnType<typeof useEveAgent>["status"];
|
|
|
|
export function AgentChat() {
|
|
const agent = useEveAgent();
|
|
const isBusy = agent.status === "submitted" || agent.status === "streaming";
|
|
const isEmpty = agent.data.messages.length === 0;
|
|
|
|
const handleSubmit = async (message: PromptInputMessage) => {
|
|
const text = message.text.trim();
|
|
if (!text || isBusy) return;
|
|
|
|
await agent.send({ message: text });
|
|
};
|
|
|
|
const composer = (
|
|
<PromptInput onSubmit={handleSubmit}>
|
|
<PromptInputTextarea placeholder="Send a message…" />
|
|
<PromptInputSubmit onStop={agent.stop} status={agent.status} />
|
|
</PromptInput>
|
|
);
|
|
|
|
return (
|
|
<main className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
|
|
{isEmpty ? null : (
|
|
<header className="flex h-14 shrink-0 items-center justify-center gap-3 pl-4 pr-2">
|
|
<span className="flex min-w-0 items-center gap-2">
|
|
<span className="truncate text-muted-foreground text-sm">{AGENT_NAME}</span>
|
|
<StatusDot status={agent.status} />
|
|
</span>
|
|
<a
|
|
className="rounded-full border border-amber-500/30 px-2 py-0.5 font-medium text-amber-700 text-xs transition-colors hover:bg-amber-500/10 dark:text-amber-300"
|
|
href={BETA_TERMS_HREF}
|
|
rel="noreferrer"
|
|
target="_blank"
|
|
>
|
|
Public preview
|
|
</a>
|
|
</header>
|
|
)}
|
|
|
|
{agent.error ? (
|
|
<div className="mx-auto w-full max-w-3xl shrink-0 px-4 pt-2 sm:px-6">
|
|
<div className="flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5 text-sm">
|
|
<AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />
|
|
<div>
|
|
<p className="font-medium">Request failed</p>
|
|
<p className="mt-0.5 text-muted-foreground">{agent.error.message}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
{isEmpty ? null : (
|
|
<Conversation className="min-h-0 flex-1">
|
|
<ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 py-6 sm:px-6">
|
|
{agent.data.messages.map((message, index) => (
|
|
<AgentMessage
|
|
canRespond={!isBusy}
|
|
isStreaming={
|
|
agent.status === "streaming" && index === agent.data.messages.length - 1
|
|
}
|
|
key={message.id}
|
|
message={message}
|
|
onInputResponses={(inputResponses) => agent.send({ inputResponses })}
|
|
/>
|
|
))}
|
|
</ConversationContent>
|
|
<ConversationScrollButton />
|
|
</Conversation>
|
|
)}
|
|
|
|
<div
|
|
className={cn(
|
|
"mx-auto w-full px-4 sm:px-6",
|
|
isEmpty
|
|
? "flex max-w-xl flex-1 flex-col items-center justify-center gap-8 pb-[10vh]"
|
|
: "max-w-3xl shrink-0 pb-6",
|
|
)}
|
|
>
|
|
{isEmpty ? (
|
|
<div className="flex flex-col items-center gap-3 text-center">
|
|
<h1 className="font-medium text-5xl tracking-tighter">{AGENT_NAME}</h1>
|
|
<a
|
|
className="rounded-full border border-amber-500/30 px-2 py-0.5 font-medium text-amber-700 text-xs transition-colors hover:bg-amber-500/10 dark:text-amber-300"
|
|
href={BETA_TERMS_HREF}
|
|
rel="noreferrer"
|
|
target="_blank"
|
|
>
|
|
Public preview
|
|
</a>
|
|
</div>
|
|
) : null}
|
|
<div className="w-full">{composer}</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function StatusDot({ status }: { readonly status: AgentStatus }) {
|
|
const isLive = status === "submitted" || status === "streaming";
|
|
const tone =
|
|
status === "error"
|
|
? "bg-destructive"
|
|
: isLive
|
|
? "bg-emerald-500"
|
|
: status === "ready"
|
|
? "bg-muted-foreground"
|
|
: "bg-muted-foreground/50";
|
|
|
|
return (
|
|
<span className="relative flex size-1">
|
|
{isLive ? (
|
|
<span
|
|
className={cn(
|
|
"absolute inline-flex size-full animate-ping rounded-full opacity-75",
|
|
tone,
|
|
)}
|
|
/>
|
|
) : null}
|
|
<span className={cn("relative inline-flex size-1 rounded-full transition-colors", tone)} />
|
|
</span>
|
|
);
|
|
}
|