Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:35:58 +08:00

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>
);
}