import { useState, useRef, useEffect, useCallback } from 'react' import { useParams } from 'react-router-dom' import { apiFetch } from '@core/lib/api' import { useAppPath } from '@core/hooks/useAppPath' import { Button } from '@core/components/ui/button' import { Textarea } from '@core/components/ui/textarea' import { Badge } from '@core/components/ui/badge' import { Skeleton } from '@core/components/ui/skeleton' import { Bot, User, ChevronRight, Send, RefreshCw, Zap, Wrench, Code2, AlertTriangle, Plus, FileCode2, } from 'lucide-react' // ─── TYPES ──────────────────────────────────────────────────────────────────── interface Message { id: string role: 'user' | 'assistant' | 'system' content: string sources?: string[] tool_calls_made?: string[] loading?: boolean } // ─── MARKDOWN RENDERER ──────────────────────────────────────────────────────── // Minimal inline renderer — avoids pulling in a full markdown library. function renderMarkdown(text: string): React.ReactNode[] { const parts: React.ReactNode[] = [] let key = 0 // Split into code blocks and text blocks const segments = text.split(/(```[\s\S]*?```)/g) for (const seg of segments) { if (seg.startsWith('```')) { const lines = seg.slice(3, -3).split('\n') const lang = lines[0].trim() const code = lines.slice(lang ? 1 : 0).join('\n') parts.push(
          {lang && 
{lang}
} {code}
) } else { // Inline: bold, code spans, line breaks const inline = seg.split(/(`[^`]+`|\*\*[^*]+\*\*|\n)/g).map((chunk, i) => { if (chunk.startsWith('`') && chunk.endsWith('`')) { return {chunk.slice(1, -1)} } if (chunk.startsWith('**') && chunk.endsWith('**')) { return {chunk.slice(2, -2)} } if (chunk === '\n') return
return chunk }) parts.push({inline}) } } return parts } // ─── MESSAGE BUBBLE ─────────────────────────────────────────────────────────── function MessageBubble({ message, projectId, appPath }: { message: Message projectId: string appPath: (p: string) => string }) { const isUser = message.role === 'user' if (message.loading) { return (
) } return (
{isUser ? : }
{isUser ? {message.content} :
{renderMarkdown(message.content)}
}
{/* Tool calls + sources */} {!isUser && (message.tool_calls_made?.length || message.sources?.length) ? (
{(message.tool_calls_made || []).map(tc => ( {tc} ))} {(message.sources || []).map(src => ( {src} ))}
) : null}
) } // ─── SUGGESTION CHIPS ───────────────────────────────────────────────────────── const SUGGESTIONS = [ 'What does the authentication middleware do?', 'Which functions write to the embeddings table?', 'Is it safe to refactor the createHandler function?', 'What are the side effects of the runAgent function?', 'Which chunks have no callers (potential dead code)?', 'Show me undocumented chunks in this project', ] // ─── MAIN PAGE ──────────────────────────────────────────────────────────────── export default function CILAgentPage() { const { id: projectId } = useParams<{ id: string }>() const appPath = useAppPath() const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [sending, setSending] = useState(false) const [projectTitle, setProjectTitle] = useState('') const bottomRef = useRef(null) const textareaRef = useRef(null) // Load project title useEffect(() => { if (!projectId) return apiFetch(`/api/custom_cil-project?action=get&id=${projectId}`) .then(r => r.json()) .then(data => { const p = data?.project || data?.data?.project if (p?.title) setProjectTitle(p.title) }) .catch(() => {}) }, [projectId]) // Scroll to bottom on new messages useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) const sendMessage = useCallback(async (text: string) => { if (!text.trim() || sending) return const userMsg: Message = { id: crypto.randomUUID(), role: 'user', content: text.trim(), } const loadingMsg: Message = { id: crypto.randomUUID(), role: 'assistant', content: '', loading: true, } setMessages(prev => [...prev, userMsg, loadingMsg]) setInput('') setSending(true) try { const res = await apiFetch('/api/custom_cil-agent?action=ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: text.trim(), project_id: projectId, }), }) const data = await res.json() const r = data?.data || data const answer = r?.answer || 'Sorry, I could not generate a response.' setMessages(prev => [ ...prev.slice(0, -1), // remove loading { id: crypto.randomUUID(), role: 'assistant', content: answer, sources: r?.sources || [], tool_calls_made: r?.tool_calls_made || [], }, ]) } catch (err) { setMessages(prev => [ ...prev.slice(0, -1), { id: crypto.randomUUID(), role: 'assistant', content: `Error: ${err instanceof Error ? err.message : String(err)}`, }, ]) } finally { setSending(false) setTimeout(() => textareaRef.current?.focus(), 50) } }, [projectId, sending]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault() sendMessage(input) } } const clearConversation = () => { setMessages([]) setInput('') textareaRef.current?.focus() } const isEmpty = messages.length === 0 return (
{/* Header */}
Code Intelligence {projectId && ( <> {projectTitle || 'Project'} )} CIL Agent
{projectId && ( {projectTitle || projectId.slice(0, 8)} )} {!isEmpty && ( )}
{/* Messages */}
{isEmpty && (

CIL Codebase Assistant

Ask me anything about this codebase. I'll search the indexed chunk contracts and call graph to answer accurately.

{SUGGESTIONS.map(s => ( ))}
{!projectId && (
No project scope — searching across all indexed chunks
)}
)} {messages.map(msg => ( ))}
{/* Input bar */}