import { useState, useRef, useEffect, useCallback } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import AgentPanel from './AgentPanel'
import MemoryManager from '../services/memoryManager'
/* ── Slide-up ease ──────────────────────────────────────── */
const EASE_OUT = [0.25, 0.46, 0.45, 0.94]
/* ══════════════════════════════════════════════════════════
Typing dots
══════════════════════════════════════════════════════════ */
function TypingIndicator() {
return (
✦
{[0, 1, 2].map((i) => (
))}
)
}
/* ══════════════════════════════════════════════════════════
Message bubble
══════════════════════════════════════════════════════════ */
function Message({ message, index, onViewInsights }) {
const isUser = message.role === 'user'
return (
{/* Avatar */}
{isUser ? '›' : '✦'}
{/* Content column */}
{/* Insights link */}
{!isUser && message.metadata && (
onViewInsights(message.metadata)}
className="flex items-center gap-1.5 px-2 py-0.5 rounded-md
text-[10px] text-text-dim hover:text-text-muted
transition-colors duration-250 group"
whileHover={{ x: 1.5 }}
>
insights
{message.metadata.confidence && (
)}
)}
)
}
/* ══════════════════════════════════════════════════════════
Empty state
══════════════════════════════════════════════════════════ */
function EmptyState({ onPromptClick }) {
const prompts = [
'Latest AI breakthroughs',
'Explain quantum computing',
'Compare React vs Vue',
]
return (
{/* Logo */}
✦
What can I help you with?
Search the web, reason through topics,
and build on past conversations.
{/* Quick prompts */}
{prompts.map((prompt) => (
onPromptClick(prompt)}
>
{prompt}
))}
)
}
/* ══════════════════════════════════════════════════════════
Chat UI — main component
══════════════════════════════════════════════════════════ */
export default function ChatUI() {
const [messages, setMessages] = useState([])
const [input, setInput] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [panelOpen, setPanelOpen] = useState(false)
const [panelMeta, setPanelMeta] = useState(null)
const messagesEndRef = useRef(null)
const inputRef = useRef(null)
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages, isLoading])
useEffect(() => {
inputRef.current?.focus()
}, [])
const handleViewInsights = useCallback((metadata) => {
setPanelMeta(metadata)
setPanelOpen(true)
}, [])
const handlePromptClick = useCallback((prompt) => {
setInput(prompt)
inputRef.current?.focus()
}, [])
const handleSubmit = async (e) => {
e.preventDefault()
const query = input.trim()
if (!query || isLoading) return
setMessages((prev) => [...prev, { role: 'user', content: query }])
setInput('')
setIsLoading(true)
try {
// Inject stored memory context into the request
const profileContext = MemoryManager.getProfileForInjection()
const sessionContext = MemoryManager.getSessionForInjection()
const API_BASE = import.meta.env.VITE_API_URL || '/api'
const res = await fetch(`${API_BASE}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
profile_context: profileContext.length > 0 ? profileContext : null,
session_context: sessionContext.length > 0 ? sessionContext : null,
}),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
// Process memory extraction from the response
if (data.memory_extraction) {
MemoryManager.processExtraction(data.memory_extraction)
}
setMessages((prev) => [
...prev,
{
role: 'agent',
content: data.response,
metadata: {
source: data.source,
tools_used: data.tools_used,
steps_taken: data.steps_taken,
plan: data.plan,
confidence: data.confidence,
refinements: data.refinements,
memory_used: data.memory_used,
memory_hits: data.memory_hits,
decision: data.decision,
llm_calls: data.llm_calls,
steps_skipped: data.steps_skipped,
early_stopped: data.early_stopped,
cache_hits: data.cache_hits,
},
},
])
} catch (err) {
setMessages((prev) => [
...prev,
{
role: 'agent',
content: `Something went wrong — ${err.message}. Make sure the backend is running.`,
metadata: null,
},
])
} finally {
setIsLoading(false)
inputRef.current?.focus()
}
}
const isEmpty = messages.length === 0 && !isLoading
return (
<>
{/* ── Header ── */}
✦
Agent
Autonomous assistant
Online
{/* ── Messages ── */}
{isEmpty && }
{messages.map((msg, i) => (
))}
{isLoading && (
)}
{/* ── Input area ── */}
Groq · llama-3.1-8b-instant
setPanelOpen(false)}
metadata={panelMeta}
/>
>
)
}