import { useEffect, useRef, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Bot, Brain, CheckCircle2, CircleStop, Database, Loader2, MessageCircleMore, Send, Sparkles, XCircle, } from 'lucide-react'; import { get } from '@/lib/api'; import { streamPiggyChat, type PiggyChatContext, type PiggyChatEvent, type PiggyChatTurn, type PiggyStatus, } from '@/lib/piggy-chat'; import { Badge, Button, EmptyState, cn } from './ui'; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, } from './ui/drawer'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from './ui/sheet'; import { Textarea } from './ui/textarea'; interface ToolStep { id: string; name: string; arguments: unknown; state: 'running' | 'succeeded' | 'failed'; error?: string; } interface TranscriptMessage { id: string; role: 'user' | 'assistant'; content: string; reasoning?: string; tools?: ToolStep[]; error?: string; pending?: boolean; } export function PiggyAskButton({ context, prompt, label = 'Ask Piggy', variant = 'outline', }: { context?: PiggyChatContext; prompt?: string; label?: string; variant?: React.ComponentProps['variant']; }) { const [open, setOpen] = useState(false); const status = usePiggyStatus(); const unavailable = status.data && !status.data.canUse; return ( <> ); } export function PiggyChatWorkspace() { const status = usePiggyStatus(); if (status.isLoading) return
; if (!status.data?.canUse) { return ( } title="Piggy is unavailable" description={ status.data?.enabled ? 'This credential does not have read access.' : 'An administrator must enable Piggy and connect the internal service.' } /> ); } return ; } function ResponsivePiggyChat({ open, onOpenChange, context, initialPrompt, }: { open: boolean; onOpenChange(open: boolean): void; context?: PiggyChatContext; initialPrompt?: string; }) { const desktop = useDesktop(); if (desktop) { return ( Ask Piggy {context?.label ? `Working from ${context.label}` : 'Working from your PIG workspace'} ); } return ( Ask Piggy {context?.label ? `Working from ${context.label}` : 'Working from your PIG workspace'} ); } function PiggyChatPanel({ context, initialPrompt = '', className, }: { context?: PiggyChatContext; initialPrompt?: string; className?: string; }) { const [messages, setMessages] = useState([]); const [draft, setDraft] = useState(initialPrompt); const [running, setRunning] = useState(false); const abortRef = useRef(null); const bottomRef = useRef(null); useEffect(() => bottomRef.current?.scrollIntoView({ behavior: running ? 'auto' : 'smooth' }), [messages, running]); useEffect(() => () => abortRef.current?.abort(), []); const send = async () => { const message = draft.trim(); if (!message || running) return; const user: TranscriptMessage = { id: crypto.randomUUID(), role: 'user', content: message }; const assistantId = crypto.randomUUID(); const history: PiggyChatTurn[] = messages .filter((entry) => entry.content.trim()) .slice(-20) .map((entry) => ({ role: entry.role, content: entry.content })); setMessages((current) => [ ...current, user, { id: assistantId, role: 'assistant', content: '', reasoning: '', tools: [], pending: true }, ]); setDraft(''); setRunning(true); const abort = new AbortController(); abortRef.current = abort; try { for await (const event of streamPiggyChat({ message, history, context }, abort.signal)) { setMessages((current) => current.map((entry) => entry.id === assistantId ? applyEvent(entry, event) : entry, ), ); } } catch (error) { if (!abort.signal.aborted) { setMessages((current) => current.map((entry) => entry.id === assistantId ? { ...entry, pending: false, error: error instanceof Error ? error.message : 'Piggy chat failed.' } : entry, ), ); } } finally { abortRef.current = null; setRunning(false); } }; return (
{messages.length === 0 ? (

What should we inspect?

Piggy reads only through scoped PIG tools. It has no shell, filesystem or browser access.

{(context ? ['Summarise this record', 'What needs attention?', 'Which terms or dates matter most?'] : ['What needs attention across the book?', 'Summarise active commitments', 'Which renewals are approaching?'] ).map((suggestion) => ( ))}
) : (
{messages.map((message) => )}
)}
{ event.preventDefault(); void send(); }}> {context ? {context.label ?? context.type.replaceAll('_', ' ')} : null}