← back to Ventura Claw
app/app/chat/page.tsx
105 lines
"use client";
import { useEffect, useRef, useState } from "react";
import { GlassCard, StatusOrb } from "@/components/glass";
interface Connector { id: string; name: string; category: string; triggers: number; actions: number; }
interface Msg { role: "user"|"assistant"|"tool"; text: string; ts: number; }
export default function ChatPage() {
const [connectors, setConnectors] = useState<Connector[]>([]);
const [filter, setFilter] = useState("");
const [msgs, setMsgs] = useState<Msg[]>([]);
const [input, setInput] = useState("");
const [busy, setBusy] = useState(false);
const tail = useRef<HTMLDivElement>(null);
useEffect(() => {
fetch("/api/connectors").then(r => r.json()).then(d => setConnectors(d.connectors));
setMsgs([{
role: "assistant", ts: Date.now(),
text: "Hi — I'm wired into all 56 of your connectors. Ask me to post to all socials, refund a Stripe charge, draft a Mailchimp campaign, or run any cross-tool workflow."
}]);
}, []);
useEffect(() => { tail.current?.scrollIntoView({ behavior: "smooth" }); }, [msgs]);
async function send() {
if (!input.trim() || busy) return;
const text = input.trim();
setInput(""); setBusy(true);
setMsgs(m => [...m, { role: "user", text, ts: Date.now() }]);
const r = await fetch("/api/chat", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ message: text })
});
const d = await r.json();
setMsgs(m => [...m, { role: "assistant", text: d.reply, ts: Date.now() }]);
if (d.toolCalls?.length) {
for (const c of d.toolCalls) {
setMsgs(m => [...m, { role: "tool", text: `→ ${c.connector}.${c.action} ${c.queued?"(queued for approval)":"(executed)"}`, ts: Date.now() }]);
}
}
setBusy(false);
}
const cats = [...new Set(connectors.map(c => c.category))];
const visible = connectors.filter(c => !filter || c.name.toLowerCase().includes(filter.toLowerCase()));
return (
<main className="min-h-screen p-6 grid gap-4 grid-cols-[300px_1fr]">
<aside className="space-y-3 overflow-hidden">
<GlassCard className="p-4">
<div className="text-xs uppercase tracking-widest opacity-60 mb-2">Connected ({connectors.length})</div>
<input value={filter} onChange={e=>setFilter(e.target.value)} placeholder="filter…"
className="w-full px-3 py-2 rounded-lg bg-white/[0.05] border border-white/15 text-sm outline-none" />
</GlassCard>
<GlassCard className="p-3 max-h-[calc(100vh-180px)] overflow-y-auto space-y-1">
{cats.map(cat => (
<div key={cat}>
<div className="text-[10px] uppercase tracking-widest opacity-50 mt-3 mb-1 px-2">{cat.replace(/_/g," ")}</div>
{visible.filter(c=>c.category===cat).map(c => (
<div key={c.id} className="flex items-center justify-between px-2 py-1.5 rounded-lg hover:bg-white/[0.05]">
<div className="flex items-center gap-2 text-sm"><StatusOrb state="ok" /> {c.name}</div>
<div className="text-[10px] opacity-50">{c.triggers}t / {c.actions}a</div>
</div>
))}
</div>
))}
</GlassCard>
</aside>
<GlassCard className="flex flex-col p-4 h-[calc(100vh-3rem)]">
<div className="flex items-center justify-between mb-3">
<div>
<div className="text-lg font-semibold">VenturaClaw — Chat</div>
<div className="text-xs opacity-60">All connectors active. Sensitive actions go to the approval queue.</div>
</div>
<a href="/approval-queue" className="text-xs opacity-70 hover:opacity-100">Approval queue →</a>
</div>
<div className="flex-1 overflow-y-auto space-y-3 pr-2">
{msgs.map((m,i) => (
<div key={i} className={`flex ${m.role==="user"?"justify-end":"justify-start"}`}>
<div className={`max-w-[78%] px-4 py-2.5 rounded-2xl text-sm ${
m.role==="user" ? "bg-gradient-to-br from-cyan-300/25 to-violet-400/25 border border-white/20" :
m.role==="tool" ? "bg-emerald-400/10 border border-emerald-400/30 font-mono text-xs" :
"bg-white/[0.06] border border-white/15"
}`}>{m.text}</div>
</div>
))}
<div ref={tail} />
</div>
<div className="mt-3 flex gap-2">
<input value={input} onChange={e=>setInput(e.target.value)}
onKeyDown={e=>{if(e.key==="Enter") send();}}
placeholder='e.g. "post our newest product to all 10 social channels"'
className="flex-1 px-4 py-3 rounded-xl bg-white/[0.06] border border-white/15 outline-none focus:border-white/35 text-sm" />
<button onClick={send} disabled={busy}
className="px-5 py-3 rounded-xl bg-gradient-to-br from-cyan-300/30 to-violet-400/30 backdrop-blur-xl border border-white/20 text-sm hover:scale-[1.02] transition disabled:opacity-50">
{busy ? "…" : "Send"}
</button>
</div>
</GlassCard>
</main>
);
}