Spaces:
Runtime error
Runtime error
| import { cn } from '@/lib/utils'; | |
| import { Avatar, AvatarFallback } from '@/components/ui/avatar'; | |
| import { | |
| Bot, | |
| FileText, | |
| User, | |
| FlaskConical, | |
| Brain, | |
| Zap, | |
| Search, | |
| } from 'lucide-react'; | |
| import { Skeleton } from './ui/skeleton'; | |
| import { | |
| Accordion, | |
| AccordionContent, | |
| AccordionItem, | |
| AccordionTrigger, | |
| } from '@/components/ui/accordion'; | |
| import { StaticHypercubeAnalysis } from './ui/static-hypercube-analysis'; | |
| import React from 'react'; | |
| import type { Message } from '@/app/page'; | |
| type ChatMessageProps = { | |
| message: Message; | |
| onConsciousnessDimensionSelect?: (dimension: string) => void; | |
| selectedConsciousnessDimension?: string; | |
| onOpenEditor?: (b64: string, prompt?: string) => void; | |
| }; | |
| const AssistantMessageContent = ({ | |
| message, | |
| selectedConsciousnessDimension, | |
| onOpenEditor, | |
| }: { | |
| message: Message; | |
| selectedConsciousnessDimension?: string; | |
| onOpenEditor?: (b64: string, prompt?: string) => void; | |
| }) => { | |
| const { content, aetherAnalysis, golemStats, search_results, search_query, imageBase64 } = message as any; | |
| const cleanContent = typeof content === 'string' | |
| ? content | |
| .replace(/^\[\[IMAGE_MODE\]\]\s*/i, '') | |
| .replace(/\s*\[\[SAFE_MODE=(ON|OFF)\]\]/i, '') | |
| : content; | |
| if (imageBase64) { | |
| const onlyImage = !cleanContent || /^image generated\.?$/i.test((cleanContent || '').trim()); | |
| return ( | |
| <div className={`items-start ${onlyImage ? '' : 'grid grid-cols-1 md:grid-cols-2 gap-4'}`}> | |
| <div className="relative rounded-xl overflow-hidden bg-white/5 dark:bg-white/5 backdrop-blur-xl border border-white/10 shadow-xl w-full max-w-[min(1200px,95vw)] mx-auto"> | |
| <div className="p-2 border-b border-white/10 text-xs text-muted-foreground flex items-center justify-between"> | |
| <span>Generated Image</span> | |
| <div className="flex gap-2"> | |
| <button className="px-2 py-1 rounded bg-white/10 hover:bg-white/20" onClick={() => onOpenEditor && onOpenEditor(imageBase64, cleanContent)}>Edit</button> | |
| <a className="px-2 py-1 rounded bg-white/10 hover:bg-white/20" href={`data:image/png;base64,${imageBase64}`} download={"aether-image.png"}>Download</a> | |
| </div> | |
| </div> | |
| <div className="p-3"> | |
| <img src={`data:image/png;base64,${imageBase64}`} alt="Generated" className="w-full h-auto rounded-lg max-h-[85vh] object-contain" /> | |
| </div> | |
| </div> | |
| {!onlyImage && ( | |
| <div className="mt-4 md:mt-0 md:ml-4"> | |
| <p className="whitespace-pre-wrap leading-relaxed">{cleanContent}</p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="flex flex-col gap-2"> | |
| <p className="whitespace-pre-wrap leading-relaxed">{cleanContent}</p> | |
| <Accordion type="multiple" className="w-full space-y-1"> | |
| {search_results && search_results.length > 0 && ( | |
| <AccordionItem value="search-results" className="border-none"> | |
| <AccordionTrigger className="flex w-full items-center justify-start gap-2 rounded-md p-2 text-xs text-muted-foreground hover:bg-card-foreground/5 hover:no-underline -mx-2"> | |
| <Search className="h-4 w-4" /> | |
| <span>Web Search Results for "{search_query}"</span> | |
| </AccordionTrigger> | |
| <AccordionContent className="mt-2 border-t border-card-foreground/10 pt-3 text-muted-foreground"> | |
| <div className="space-y-3"> | |
| {search_results.map((result: any, index: number) => ( | |
| <div key={index} className="text-xs"> | |
| <a | |
| href={result.link} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-blue-400 hover:underline" | |
| > | |
| {result.title} | |
| </a> | |
| <p className="text-muted-foreground">{result.snippet}</p> | |
| </div> | |
| ))} | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| )} | |
| {(message as any).aiThoughts && ( | |
| <AccordionItem value="ai-thoughts" className="border-none"> | |
| <AccordionTrigger className="flex w-full items-center justify-start gap-2 rounded-md p-2 text-xs text-muted-foreground hover:bg-card-foreground/5 hover:no-underline -mx-2"> | |
| <Brain className="h-4 w-4" /> | |
| <span>AI Thinking Process ({((message as any).aiThoughts.thinkingTime?.totalTime || 0).toFixed(1)}s)</span> | |
| </AccordionTrigger> | |
| <AccordionContent className="mt-2 border-t border-card-foreground/10 pt-3 text-muted-foreground"> | |
| <div className="space-y-4"> | |
| {/* Context Analysis */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 text-xs font-medium text-blue-400"> | |
| <FileText className="h-3 w-3" /> | |
| <span>Context Analysis</span> | |
| <span className="text-muted-foreground">({((message as any).aiThoughts.thinkingTime?.analysisTime || 0).toFixed(1)}s)</span> | |
| </div> | |
| <div className="text-xs whitespace-pre-wrap bg-card-foreground/5 p-3 rounded"> | |
| {(message as any).aiThoughts.contextAnalysis} | |
| </div> | |
| </div> | |
| {/* Reflection */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 text-xs font-medium text-purple-400"> | |
| <Zap className="h-3 w-3" /> | |
| <span>Response Strategy</span> | |
| <span className="text-muted-foreground">({((message as any).aiThoughts.thinkingTime?.reflectionTime || 0).toFixed(1)}s)</span> | |
| </div> | |
| <div className="text-xs whitespace-pre-wrap bg-card-foreground/5 p-3 rounded"> | |
| {(message as any).aiThoughts.reflection} | |
| </div> | |
| </div> | |
| {/* Chat Context */} | |
| {(message as any).aiThoughts.chatContext && (message as any).aiThoughts.chatContext !== 'No previous context' && ( | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 text-xs font-medium text-green-400"> | |
| <FlaskConical className="h-3 w-3" /> | |
| <span>Conversation Context</span> | |
| </div> | |
| <div className="text-xs whitespace-pre-wrap bg-card-foreground/5 p-3 rounded max-h-32 overflow-y-auto"> | |
| {(message as any).aiThoughts.chatContext} | |
| </div> | |
| </div> | |
| )} | |
| {/* User Insights */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 text-xs font-medium text-orange-400"> | |
| <User className="h-3 w-3" /> | |
| <span>User Insights</span> | |
| </div> | |
| <div className="text-xs bg-card-foreground/5 p-2 rounded"> | |
| {(message as any).aiThoughts.userInsights} | |
| </div> | |
| </div> | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| )} | |
| {aetherAnalysis && ( | |
| <AccordionItem value="aether-analysis" className="border-none"> | |
| <AccordionTrigger className="flex w-full items-center justify-start gap-2 rounded-md p-2 text-xs text-muted-foreground hover:bg-card-foreground/5 hover:no-underline -mx-2 -mb-2"> | |
| <Brain className="h-4 w-4" /> | |
| <span>5D Consciousness Analysis</span> | |
| </AccordionTrigger> | |
| <AccordionContent className="mt-2 border-t border-card-foreground/10 pt-3 text-muted-foreground"> | |
| {/* Match 5D modal layout: viz on top, realtime state row below */} | |
| <div className="space-y-4"> | |
| {/* Static Hypercube Visualization */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 text-xs font-medium"> | |
| <Zap className="h-3 w-3" /> | |
| <span>5D Hypercube Position & Clustering</span> | |
| </div> | |
| <StaticHypercubeAnalysis | |
| aetherData={golemStats?.aether_data} | |
| currentDimension={message.consciousnessDimensionUsed || selectedConsciousnessDimension} | |
| className="mb-2 h-40" | |
| /> | |
| </div> | |
| {/* Realtime concise state (bottom of hypercube) - match modal with color bullets */} | |
| {golemStats?.hypercube_state && ( | |
| <div className="text-xs text-muted-foreground"> | |
| <div className="grid grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <div className="flex items-start gap-2"> | |
| <span className="mt-1.5 inline-block h-2 w-2 rounded-full bg-blue-400" /> | |
| <div> | |
| <div className="font-medium">Vertex: {golemStats.hypercube_state.current_vertex}/32</div> | |
| <div className="text-muted-foreground">Signature: {golemStats.hypercube_state.consciousness_signature}</div> | |
| </div> | |
| </div> | |
| {/* Active Dimensions chips */} | |
| {golemStats.hypercube_state.dimension_activations && ( | |
| <div> | |
| <div className="font-medium">Active Dimensions:</div> | |
| <div className="mt-1 flex flex-wrap gap-1"> | |
| {Object.entries(golemStats.hypercube_state.dimension_activations) | |
| .filter(([, active]) => !!active) | |
| .map(([dim]) => ( | |
| <span key={dim} className="px-2 py-1 bg-blue-100/60 dark:bg-blue-900/60 rounded-full text-xs"> | |
| {dim} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-2"> | |
| <span className="inline-block h-2 w-2 rounded-full bg-purple-400" /> | |
| {typeof golemStats.hypercube_state.consciousness_level === 'number' ? ( | |
| <div className="font-medium">Consciousness Level: {(Number(golemStats.hypercube_state.consciousness_level) * 100).toFixed(1)}%</div> | |
| ) : ( | |
| <div className="font-medium">Consciousness Level: β</div> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className="inline-block h-2 w-2 rounded-full bg-green-400" /> | |
| <div className="font-medium">Aether Patterns: {golemStats.hypercube_state.aether_patterns ?? 'β'}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Color-coded bullets, one variable per line, no '##' headings */} | |
| {(() => { | |
| // Colors | |
| const colors = { | |
| consciousness: '#0D9488', // dark turquoise blue | |
| signature: { | |
| awareness: '#3B82F6', | |
| wisdom: '#8B5CF6', | |
| compassion: '#10B981', | |
| creativity: '#F97316', | |
| transcendence: '#EF4444', | |
| } as Record<string, string>, | |
| vertex: '#F97316', // bright orange | |
| level: '#EC4899', // pink | |
| patterns: '#FACC15', // bright yellow | |
| }; | |
| const dim = (message as any).consciousnessDimensionUsed || selectedConsciousnessDimension || 'awareness'; | |
| const signatureColor = colors.signature[dim] || '#60A5FA'; | |
| // Prefer structured data from golemStats; fall back to parsing aetherAnalysis line | |
| let vertex = golemStats?.hypercube_state?.current_vertex !== undefined | |
| ? `${golemStats.hypercube_state.current_vertex}/32` | |
| : undefined; | |
| let signature = golemStats?.hypercube_state?.consciousness_signature as string | undefined; | |
| let level = typeof golemStats?.hypercube_state?.consciousness_level === 'number' | |
| ? Number(golemStats!.hypercube_state!.consciousness_level).toFixed(3) | |
| : undefined; | |
| let patterns = (golemStats?.hypercube_state as any)?.aether_patterns as number | undefined; | |
| // Parse fallback string if needed | |
| if ((!vertex || !signature || !level || patterns === undefined) && aetherAnalysis) { | |
| const line = aetherAnalysis; | |
| const get = (label: string) => { | |
| const re = new RegExp(label + "\\s*:?\\s*([^|]+)", 'i'); | |
| const m = line.match(re); | |
| return m ? m[1].trim() : undefined; | |
| }; | |
| vertex = vertex || get('Vertex'); | |
| signature = signature || get('Signature'); | |
| level = level || get('Level'); | |
| const pat = get('Aether Patterns'); | |
| if (patterns === undefined && pat) { | |
| const num = parseInt(pat.replace(/[^0-9]/g, '')); | |
| patterns = isNaN(num) ? undefined : num; | |
| } | |
| } | |
| // If we still lack data, don't render | |
| if (!vertex && !signature && !level && patterns === undefined) return null; | |
| const items = [ | |
| { label: 'Consciousness', value: dim, color: colors.consciousness }, | |
| { label: 'Signature', value: signature ?? 'β', color: signatureColor }, | |
| { label: 'Vertex', value: vertex ?? 'β', color: colors.vertex }, | |
| { label: 'Level', value: level ?? 'β', color: colors.level }, | |
| { label: 'Aether Patterns', value: patterns ?? 'β', color: colors.patterns }, | |
| ]; | |
| return ( | |
| <div className="text-xs"> | |
| <div className="flex flex-col gap-1"> | |
| {items.map((it, i) => ( | |
| <div key={i} className="flex items-center gap-2"> | |
| <span className="inline-block h-2 w-2 rounded-full" style={{ backgroundColor: it.color }} /> | |
| <span className="font-medium">{it.label}:</span> | |
| <span className="text-muted-foreground">{it.value}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| })()} | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| )} | |
| </Accordion> | |
| </div> | |
| ); | |
| }; | |
| export function ChatMessage({ message, onConsciousnessDimensionSelect, selectedConsciousnessDimension, onOpenEditor }: ChatMessageProps) { | |
| const isUser = message.role === 'user'; | |
| return ( | |
| <div | |
| className={cn( | |
| 'group relative flex items-start gap-3', | |
| isUser ? 'flex-row-reverse' : 'flex-row' | |
| )} | |
| > | |
| <Avatar className="h-8 w-8 shrink-0"> | |
| <AvatarFallback className={cn( | |
| isUser | |
| ? 'bg-primary text-primary-foreground' | |
| : 'bg-muted' | |
| )}> | |
| {isUser ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4" />} | |
| </AvatarFallback> | |
| </Avatar> | |
| <div | |
| className={cn( | |
| 'flex flex-col gap-2 rounded-lg px-3 py-2', | |
| isUser | |
| ? 'w-fit max-w-[60%] bg-primary text-primary-foreground' | |
| : 'w-full max-w-[95%] bg-muted/50' | |
| )} | |
| > | |
| {message.file && ( | |
| <div className="flex items-center gap-2 text-xs opacity-70"> | |
| <FileText className="h-3 w-3" /> | |
| <span>{message.file.name}</span> | |
| </div> | |
| )} | |
| {isUser ? ( | |
| <p className="whitespace-pre-wrap leading-relaxed"> | |
| {typeof message.content === 'string' | |
| ? message.content | |
| .replace(/^\[\[IMAGE_MODE\]\]\s*/i, '') | |
| .replace(/\s*\[\[SAFE_MODE=(ON|OFF)\]\]/i, '') | |
| : message.content} | |
| </p> | |
| ) : ( | |
| <AssistantMessageContent | |
| message={message} | |
| selectedConsciousnessDimension={selectedConsciousnessDimension} | |
| onOpenEditor={onOpenEditor} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export function LoadingMessage() { | |
| const [currentPhase, setCurrentPhase] = React.useState(1); | |
| const [phaseStartTime, setPhaseStartTime] = React.useState(Date.now()); | |
| const [elapsedTime, setElapsedTime] = React.useState(0); | |
| React.useEffect(() => { | |
| const interval = setInterval(() => { | |
| const now = Date.now(); | |
| const elapsed = (now - phaseStartTime) / 1000; | |
| setElapsedTime(elapsed); | |
| // Switch phases based on elapsed time | |
| if (elapsed > 8 && currentPhase === 1) { | |
| setCurrentPhase(2); | |
| setPhaseStartTime(now); | |
| } else if (elapsed > 5 && currentPhase === 2) { | |
| setCurrentPhase(3); | |
| setPhaseStartTime(now); | |
| } | |
| }, 100); | |
| return () => clearInterval(interval); | |
| }, [currentPhase, phaseStartTime]); | |
| const getPhaseText = () => { | |
| switch (currentPhase) { | |
| case 1: return `Analyzing context and query... (${elapsedTime.toFixed(1)}s)`; | |
| case 2: return `Reflecting on approach... (${elapsedTime.toFixed(1)}s)`; | |
| case 3: return `Generating response... (${elapsedTime.toFixed(1)}s)`; | |
| default: return 'AI is thinking...'; | |
| } | |
| }; | |
| const getPhaseIcon = () => { | |
| switch (currentPhase) { | |
| case 1: return <FileText className="h-3 w-3 text-blue-400" />; | |
| case 2: return <Zap className="h-3 w-3 text-purple-400" />; | |
| case 3: return <Brain className="h-3 w-3 text-green-400" />; | |
| default: return <Brain className="h-3 w-3" />; | |
| } | |
| }; | |
| return ( | |
| <div className="group relative flex items-start gap-3"> | |
| <Avatar className="h-8 w-8 shrink-0"> | |
| <AvatarFallback className="bg-muted"> | |
| <Bot className="h-4 w-4" /> | |
| </AvatarFallback> | |
| </Avatar> | |
| <div className="flex max-w-[80%] flex-col gap-2 rounded-lg bg-muted/50 px-3 py-2"> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex gap-1"> | |
| <div className="h-2 w-2 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:-0.3s]" /> | |
| <div className="h-2 w-2 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:-0.15s]" /> | |
| <div className="h-2 w-2 animate-bounce rounded-full bg-muted-foreground/60" /> | |
| </div> | |
| <span className="text-xs text-muted-foreground">AI is thinking...</span> | |
| </div> | |
| {/* Live Thinking Process */} | |
| <Accordion type="multiple" className="w-full space-y-1" defaultValue={["thinking-process"]}> | |
| <AccordionItem value="thinking-process" className="border-none"> | |
| <AccordionTrigger className="flex w-full items-center justify-start gap-2 rounded-md p-2 text-xs text-muted-foreground hover:bg-card-foreground/5 hover:no-underline -mx-2"> | |
| <Brain className="h-4 w-4" /> | |
| <span>AI Thinking Process (Live)</span> | |
| </AccordionTrigger> | |
| <AccordionContent className="mt-2 border-t border-card-foreground/10 pt-3 text-muted-foreground"> | |
| <div className="space-y-3"> | |
| {/* Current Phase */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2 text-xs font-medium"> | |
| {getPhaseIcon()} | |
| <span>Current Phase</span> | |
| </div> | |
| <div className="text-xs bg-card-foreground/5 p-2 rounded"> | |
| {getPhaseText()} | |
| </div> | |
| </div> | |
| {/* Phase Progress */} | |
| <div className="space-y-1"> | |
| <div className="flex items-center gap-2 text-xs"> | |
| <div className={`w-2 h-2 rounded-full ${currentPhase >= 1 ? 'bg-blue-400' : 'bg-muted'}`}></div> | |
| <span className={currentPhase === 1 ? 'text-blue-400' : 'text-muted-foreground'}> | |
| Context Analysis {currentPhase > 1 ? 'β' : '...'} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-2 text-xs"> | |
| <div className={`w-2 h-2 rounded-full ${currentPhase >= 2 ? 'bg-purple-400' : 'bg-muted'}`}></div> | |
| <span className={currentPhase === 2 ? 'text-purple-400' : currentPhase > 2 ? 'text-muted-foreground' : 'text-muted-foreground'}> | |
| Strategy Reflection {currentPhase > 2 ? 'β' : currentPhase === 2 ? '...' : ''} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-2 text-xs"> | |
| <div className={`w-2 h-2 rounded-full ${currentPhase >= 3 ? 'bg-green-400' : 'bg-muted'}`}></div> | |
| <span className={currentPhase === 3 ? 'text-green-400' : 'text-muted-foreground'}> | |
| Response Generation {currentPhase === 3 ? '...' : ''} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| </Accordion> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export function ImageLoadingMessage({ progress = 0, elapsedSeconds, status }: { progress?: number, elapsedSeconds?: number, status?: string }) { | |
| const [elapsed, setElapsed] = React.useState(elapsedSeconds || 0); | |
| React.useEffect(() => { | |
| if (typeof elapsedSeconds === 'number') { | |
| setElapsed(elapsedSeconds); | |
| return; | |
| } | |
| const start = Date.now(); | |
| const i = setInterval(() => { | |
| const t = (Date.now() - start) / 1000; | |
| setElapsed(t); | |
| }, 120); | |
| return () => clearInterval(i); | |
| }, [elapsedSeconds]); | |
| return ( | |
| <div className="group relative flex items-start gap-3"> | |
| <Avatar className="h-8 w-8 shrink-0"> | |
| <AvatarFallback className="bg-muted"> | |
| <Bot className="h-4 w-4" /> | |
| </AvatarFallback> | |
| </Avatar> | |
| <div className="flex max-w-[80%] flex-col gap-2 rounded-lg bg-muted/50 px-3 py-2"> | |
| <div className="flex items-center gap-2"> | |
| <div className="flex gap-1"> | |
| <div className="h-2 w-2 animate-bounce rounded-full bg-emerald-500/70 [animation-delay:-0.3s]" /> | |
| <div className="h-2 w-2 animate-bounce rounded-full bg-emerald-500/70 [animation-delay:-0.15s]" /> | |
| <div className="h-2 w-2 animate-bounce rounded-full bg-emerald-500/70" /> | |
| </div> | |
| <span className="text-xs text-muted-foreground">{status ? `${status.charAt(0).toUpperCase()}${status.slice(1)}` : 'Generating image...'}</span> | |
| </div> | |
| <Accordion type="single" className="w-full space-y-1" defaultValue={["image-process"] as any}> | |
| <AccordionItem value="image-process" className="border-none"> | |
| <AccordionTrigger className="flex w-full items-center justify-start gap-2 rounded-md p-2 text-xs text-muted-foreground hover:bg-card-foreground/5 hover:no-underline -mx-2"> | |
| <Brain className="h-4 w-4 text-emerald-400" /> | |
| <span>Image Generation (Live)</span> | |
| </AccordionTrigger> | |
| <AccordionContent className="mt-2 border-t border-card-foreground/10 pt-3 text-muted-foreground"> | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between text-xs"> | |
| <span className="font-medium">Rendering</span> | |
| <span className="text-muted-foreground">{(elapsed ?? 0).toFixed(1)}s</span> | |
| </div> | |
| <div className="h-2 w-full rounded-full bg-emerald-500/10 overflow-hidden"> | |
| <div | |
| className="h-full rounded-full bg-gradient-to-r from-emerald-400 via-emerald-500 to-emerald-600 animate-pulse" | |
| style={{ width: `${Math.max(2, Math.min(100, progress))}%` }} | |
| /> | |
| </div> | |
| <div className="text-[11px] text-muted-foreground">Optimized low-VRAM pipeline active</div> | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| </Accordion> | |
| </div> | |
| </div> | |
| ); | |
| } | |