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