import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { aceAvatar, Button, ChatComposer, serialize, Timeline, toast, useLayoutLeft, } from "@ace/ui"; import { IconSidebar } from "@ace/ui/icons"; import type { Listing, ModelRef } from "@ace/host/protocol"; import { host } from "./host"; import { toEvents } from "./timeline"; import { useTranscript } from "./transcript"; const MODES = [ { id: "chat", name: "Chat", placeholder: "Mention @ace to ask the agent" }, { id: "ace", name: "Ace", placeholder: "Ask the agent", mention: "ace" }, ]; const MENTIONS = [{ name: "ace", avatar: aceAvatar }]; const ACE = /(^|\s)@ace\b/i; const key = (model: ModelRef) => `${model.provider}/${model.modelId}`; type Props = { channel: Listing; user: string; remote: boolean }; /** One channel's chat: the original timeline and composer over the channel's transcript. */ export function Conversation({ channel, user, remote }: Props) { const left = useLayoutLeft(); const transcript = useTranscript(channel.id); // Runs use the credentials of the host that runs the channel, so offer that host's models. const [models, setModels] = useState([]); const status = useSyncExternalStore(host.subscribe, () => host.status); const settingsVersion = useSyncExternalStore(host.subscribe, () => host.settingsVersion); useEffect(() => { if (status !== "open") return; let active = true; host.request({ op: "models", host: channel.host }).then( (models) => { if (active) setModels(models); }, () => { if (active) setModels([]); }, ); return () => { active = false; }; }, [channel.host, status, settingsVersion]); const [mode, setMode] = useState("ace"); const chat = transcript.info?.chats.find((value) => value.id === 1); const [picked, setPicked] = useState(); const model = picked || (chat?.model ? key(chat.model) : key(channel.model)); const events = useMemo(() => toEvents(transcript.items, channel.id, transcript.busy), [ transcript.items, channel.id, transcript.busy, ]); const archived = channel.state === "archived"; async function send(text: string, mode: string) { const invoke = mode === "ace" || ACE.test(text); const body = text.trim(); if (!body) return; try { if (!invoke) { return void (await host.channel(channel.id, { op: "say", author: user, text: body })); } const [provider, ...rest] = model.split("/"); const selected = { provider: provider!, modelId: rest.join("/") }; await host.channel(channel.id, { op: "ask", author: user, text: body, model: selected }); } catch (error) { toast.error("Could not send", { description: (error as Error).message }); } } return ( <>
#{channel.name} {remote && {channel.host}} {chat?.lane && ( lane {chat.lane} )}
{transcript.draft && (
{transcript.draft}
)}
({ id: key(value), name: value.modelId, vendor: value.provider, }))} model={model} onModelChange={setPicked} busy={transcript.busy} canStop={transcript.busy} onStop={() => void host.channel(channel.id, { op: "stop" })} canSend={!archived} onSend={({ doc, mode }) => void send(serialize(doc), mode)} />
); }