'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; import { Send, Bot, User, Loader2, Sparkles, X, Paperclip } from 'lucide-react'; const LLM_API = process.env.NEXT_PUBLIC_LLM_API_URL || 'https://llm.jeffemmett.com'; const RDESIGN_API = process.env.NEXT_PUBLIC_RDESIGN_API_URL || 'https://scribus.rspace.online'; interface Message { id: string; role: 'user' | 'assistant' | 'system' | 'tool'; content: string; timestamp: Date; toolCalls?: ToolCall[]; toolResults?: ToolResult[]; } interface ToolCall { id: string; name: string; arguments: Record; } interface ToolResult { toolCallId: string; name: string; result: unknown; } // MCP-style tools for the design assistant const DESIGN_TOOLS = [ { type: 'function' as const, function: { name: 'list_templates', description: 'List available Scribus document templates. Returns template names, categories, and variable placeholders.', parameters: { type: 'object', properties: { category: { type: 'string', description: 'Filter by category (flyer, poster, brochure, imported, general)' } } }, }, }, { type: 'function' as const, function: { name: 'export_template', description: 'Export a Scribus template to PDF or PNG. Supports variable substitution for dynamic content.', parameters: { type: 'object', properties: { template: { type: 'string', description: 'Template slug' }, variables: { type: 'object', description: 'Key-value pairs for template variables (e.g. {"title": "My Event", "date": "2026-04-01"})' }, format: { type: 'string', enum: ['pdf', 'png'], description: 'Output format' }, }, required: ['template'], }, }, }, { type: 'function' as const, function: { name: 'batch_export', description: 'Generate multiple documents from one template with different data. Like mail-merge: one template + multiple rows of variables = multiple PDFs.', parameters: { type: 'object', properties: { template: { type: 'string', description: 'Template slug' }, rows: { type: 'array', items: { type: 'object' }, description: 'Array of variable objects, one per document' }, }, required: ['template', 'rows'], }, }, }, { type: 'function' as const, function: { name: 'list_rswag_designs', description: 'List available rSwag merchandise designs (stickers, shirts, prints) that can be exported to print-ready PDFs.', parameters: { type: 'object', properties: { category: { type: 'string', description: 'stickers, shirts, or prints' } } }, }, }, { type: 'function' as const, function: { name: 'export_rswag_design', description: 'Export an rSwag design to a print-ready PDF with professional bleed and crop marks.', parameters: { type: 'object', properties: { design_slug: { type: 'string' }, category: { type: 'string', enum: ['stickers', 'shirts', 'prints'] }, paper_size: { type: 'string', enum: ['A4', 'A3', 'A5', 'Letter'] }, }, required: ['design_slug', 'category'], }, }, }, { type: 'function' as const, function: { name: 'check_job_status', description: 'Check the status of a running export/conversion job.', parameters: { type: 'object', properties: { job_id: { type: 'string' } }, required: ['job_id'], }, }, }, { type: 'function' as const, function: { name: 'open_studio', description: 'Open the interactive Scribus Studio (GUI in browser via noVNC). Use this when the user wants to design something visually or edit a template by hand.', parameters: { type: 'object', properties: {} }, }, }, ]; async function executeToolCall(name: string, args: Record): Promise { switch (name) { case 'list_templates': { const params = args.category ? `?category=${args.category}` : ''; const res = await fetch(`${RDESIGN_API}/templates${params}`); return res.json(); } case 'export_template': { const res = await fetch(`${RDESIGN_API}/export`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(args), }); return res.json(); } case 'batch_export': { const res = await fetch(`${RDESIGN_API}/export/batch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(args), }); return res.json(); } case 'list_rswag_designs': { const params = args.category ? `?category=${args.category}` : ''; const res = await fetch(`${RDESIGN_API}/rswag/designs${params}`); return res.json(); } case 'export_rswag_design': { const res = await fetch(`${RDESIGN_API}/rswag/export`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(args), }); return res.json(); } case 'check_job_status': { const res = await fetch(`${RDESIGN_API}/jobs/${args.job_id}`); return res.json(); } case 'open_studio': { window.open('/studio', '_blank'); return { status: 'opened', url: '/studio' }; } default: return { error: `Unknown tool: ${name}` }; } } const SYSTEM_PROMPT = `You are the rDesign AI assistant — a mycelial intelligence helping users create beautiful documents, export print-ready PDFs, and manage design templates. You have access to a self-hosted Scribus instance for professional desktop publishing. You can: - List and export document templates with variable substitution - Batch-generate documents (mail-merge style) from templates - Export rSwag merchandise designs to print-ready PDFs with bleed/crop marks - Convert InDesign IDML files to Scribus format - Open the interactive Scribus Studio for visual editing Be helpful, creative, and concise. When users want to create something, suggest appropriate templates or offer to open the Studio. When they need automated output, use the export tools. Like mycelium connecting a forest, you connect the design tools into a unified creative experience.`; export function DesignAssistant() { const [messages, setMessages] = useState([ { id: 'welcome', role: 'assistant', content: "Welcome to rDesign. I can help you create documents, export PDFs, batch-generate from templates, or prepare rSwag designs for print. What would you like to create?", timestamp: new Date(), }, ]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isOpen, setIsOpen] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const scrollToBottom = useCallback(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, []); useEffect(() => { scrollToBottom(); }, [messages, scrollToBottom]); const sendMessage = async () => { if (!input.trim() || isLoading) return; const userMessage: Message = { id: `user-${Date.now()}`, role: 'user', content: input.trim(), timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); setInput(''); setIsLoading(true); try { // Build conversation for LLM const conversationMessages = [ { role: 'system' as const, content: SYSTEM_PROMPT }, ...messages.filter((m) => m.role !== 'system' && m.id !== 'welcome').map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content, })), { role: 'user' as const, content: userMessage.content }, ]; // Call LiteLLM with tools let response = await fetch(`${LLM_API}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet', messages: conversationMessages, tools: DESIGN_TOOLS, max_tokens: 2048, }), }); if (!response.ok) { throw new Error(`LLM API error: ${response.status}`); } let data = await response.json(); let choice = data.choices?.[0]; // Handle tool calls in a loop const updatedMessages = [...conversationMessages]; while (choice?.message?.tool_calls?.length) { const toolCalls: ToolCall[] = choice.message.tool_calls.map((tc: { id: string; function: { name: string; arguments: string } }) => ({ id: tc.id, name: tc.function.name, arguments: JSON.parse(tc.function.arguments || '{}'), })); // Show tool call status const toolMessage: Message = { id: `tool-${Date.now()}`, role: 'assistant', content: toolCalls.map((tc) => `Running \`${tc.name}\`...`).join('\n'), timestamp: new Date(), toolCalls, }; setMessages((prev) => [...prev, toolMessage]); // Execute tools updatedMessages.push({ role: 'assistant' as const, content: choice.message.content || '', tool_calls: choice.message.tool_calls, } as { role: 'assistant'; content: string; tool_calls?: unknown[] }); for (const tc of toolCalls) { const result = await executeToolCall(tc.name, tc.arguments); updatedMessages.push({ role: 'tool' as const, content: JSON.stringify(result), tool_call_id: tc.id, } as { role: 'tool'; content: string; tool_call_id?: string }); } // Get next LLM response response = await fetch(`${LLM_API}/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet', messages: updatedMessages, tools: DESIGN_TOOLS, max_tokens: 2048, }), }); if (!response.ok) throw new Error(`LLM API error: ${response.status}`); data = await response.json(); choice = data.choices?.[0]; } // Final assistant message const assistantContent = choice?.message?.content || 'I encountered an issue processing that request.'; const assistantMessage: Message = { id: `assistant-${Date.now()}`, role: 'assistant', content: assistantContent, timestamp: new Date(), }; setMessages((prev) => [...prev, assistantMessage]); } catch (error) { const errorMessage: Message = { id: `error-${Date.now()}`, role: 'assistant', content: `Something went wrong: ${error instanceof Error ? error.message : 'Unknown error'}. Try again or check the API status.`, timestamp: new Date(), }; setMessages((prev) => [...prev, errorMessage]); } finally { setIsLoading(false); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; if (!isOpen) { return ( ); } return (
{/* Header */}
Design Assistant
Mycelial Intelligence
{/* Messages */}
{messages.map((msg) => (
{msg.role !== 'user' && (
)}
{msg.content.split('\n').map((line, i) => (

0 ? 'mt-1' : ''}> {/* Render links as clickable */} {line.includes('http') ? ( line.split(/(https?:\/\/\S+)/).map((part, j) => part.match(/^https?:\/\//) ? ( {part.includes('/output/') ? 'Download' : part} ) : ( part ) ) ) : ( line )}

))}
{msg.role === 'user' && (
)}
))} {isLoading && (
Thinking...
)}
{/* Input */}