"use client"; import * as React from "react"; import { Loader2, Send, Sparkles, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; import { api } from "@/lib/trpc"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, } from "@/components/ui/sheet"; type ChatRole = "user" | "assistant"; type ChatMessage = { id: string; role: ChatRole; content: string; }; function TypingDots() { return (
{[0, 1, 2].map((i) => ( ))}
); } export function OpsCopilotPanel({ workspaceSlug, open, onOpenChange, }: { workspaceSlug: string; open: boolean; onOpenChange: (open: boolean) => void; }) { const [messages, setMessages] = React.useState([]); const [input, setInput] = React.useState(""); const [draftOpen, setDraftOpen] = React.useState(false); const [draftPlanSlug, setDraftPlanSlug] = React.useState(""); const [draftEpicSlug, setDraftEpicSlug] = React.useState(""); const [draftSlug, setDraftSlug] = React.useState(""); const [draftTitle, setDraftTitle] = React.useState(""); const [draftBody, setDraftBody] = React.useState(""); const bottomRef = React.useRef(null); const copilotMut = api.opsCopilot.generateText.useMutation({ onSuccess: (data) => { setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), role: "assistant", content: data.text }, ]); }, }); const createTaskMut = api.backlog.createTask.useMutation({ onSuccess: () => { setDraftOpen(false); setDraftPlanSlug(""); setDraftEpicSlug(""); setDraftSlug(""); setDraftTitle(""); setDraftBody(""); }, }); React.useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, copilotMut.isPending]); const send = React.useCallback(() => { const trimmed = input.trim(); if (!trimmed || copilotMut.isPending) return; if (!workspaceSlug) return; const userMsg: ChatMessage = { id: crypto.randomUUID(), role: "user", content: trimmed, }; const next = [...messages, userMsg]; setMessages(next); setInput(""); const history = next.slice(0, -1).map((m) => ({ role: m.role, content: m.content, })); copilotMut.mutate({ workspace: workspaceSlug, prompt: trimmed, messages: history.length > 0 ? history : undefined, }); }, [input, copilotMut, messages, workspaceSlug]); const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }; const handleDraftTask = () => { if (!workspaceSlug) return; if (!draftPlanSlug.trim() || !draftEpicSlug.trim() || !draftSlug.trim() || !draftTitle.trim()) { return; } createTaskMut.mutate({ workspace: workspaceSlug, planSlug: draftPlanSlug.trim().toLowerCase(), epicSlug: draftEpicSlug.trim().toLowerCase(), slug: draftSlug.trim().toLowerCase(), title: draftTitle.trim(), bodyMarkdown: draftBody.trim() || undefined, }); }; const isBusy = copilotMut.isPending; const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant"); return (
Ops copilot
Ask "what needs me?" — grounded in fleet, convoys and backlog. Links use [workspace/slug]. {workspaceSlug ? ` · ${workspaceSlug}` : " · no workspace"}
{messages.length === 0 && !isBusy ? (

What needs you?

Try "what needs my approval?" — fleet → needs you will be cited with [fleet] links.

{["What needs me?", "What's ready to start?", "Show my gate inbox"].map((chip) => ( ))}
{!workspaceSlug && (

No workspace selected — create or select one first.

)}
) : null}
{messages.map((m) => (
{m.content}
))} {isBusy && (
)} {copilotMut.error && (

{copilotMut.error.message}

)} {copilotMut.data && (

Model: {copilotMut.data.resolvedAlias} ({copilotMut.data.modelId})

)}

{lastAssistant ? "Need to create work from this answer?" : "Draft a Task from chat"}

{draftOpen && (

Create backlog task via create-backlog-item shape

setDraftPlanSlug(e.target.value)} placeholder="automation-foundation" className="h-8 text-xs" />
setDraftEpicSlug(e.target.value)} placeholder="ops-copilot" className="h-8 text-xs" />
setDraftSlug(e.target.value)} placeholder="triage-bug" className="h-8 text-xs" />
setDraftTitle(e.target.value)} placeholder="Triage bug from copilot" className="h-8 text-xs" />