Spaces:
Sleeping
Sleeping
| import { useEffect, useRef, useState } from 'react'; | |
| import { Bot, Send, Sparkles } from 'lucide-react'; | |
| import { motion } from 'framer-motion'; | |
| import { askQuestion } from '../../api/client'; | |
| import { useRepoStore } from '../../store/useRepoStore'; | |
| import MessageBubble from './MessageBubble'; | |
| import SuggestedQuestions from './SuggestedQuestions'; | |
| import TypingIndicator from './TypingIndicator'; | |
| export default function ChatInterface() { | |
| const [input, setInput] = useState(''); | |
| const endRef = useRef(null); | |
| const { repoId, chatHistory, addChatMessage, isAsking, setAsking } = useRepoStore(); | |
| useEffect(() => { | |
| endRef.current?.scrollIntoView({ behavior: 'smooth' }); | |
| }, [chatHistory, isAsking]); | |
| const handleSend = async (question = input) => { | |
| const trimmed = question.trim(); | |
| if (!trimmed || !repoId || isAsking) return; | |
| const historyBeforeSend = chatHistory; | |
| setInput(''); | |
| addChatMessage({ role: 'user', content: trimmed }); | |
| setAsking(true); | |
| try { | |
| const response = await askQuestion(repoId, trimmed, historyBeforeSend); | |
| addChatMessage({ role: 'assistant', content: response.data.answer, details: response.data }); | |
| } catch (error) { | |
| addChatMessage({ | |
| role: 'assistant', | |
| content: error?.response?.data?.detail || 'IBM Bob could not complete that analysis. Please try another question.', | |
| error: true, | |
| }); | |
| } finally { | |
| setAsking(false); | |
| } | |
| }; | |
| return ( | |
| <section className="flex h-full min-h-[720px] flex-col overflow-hidden rounded-lg border border-white/10 bg-[#0a0f1a]/85"> | |
| <div className="flex items-center gap-3 border-b border-white/10 p-5"> | |
| <span className="flex h-10 w-10 items-center justify-center rounded-lg border border-cyan-400/25 bg-cyan-500/10"> | |
| <Bot className="h-5 w-5 text-cyan-200" /> | |
| </span> | |
| <div> | |
| <h3 className="font-semibold text-white">Engineering Assistant</h3> | |
| <p className="text-xs text-slate-500">Repository-aware IBM Bob analysis</p> | |
| </div> | |
| <div className="ml-auto flex items-center gap-2 text-xs text-emerald-300"> | |
| <span className="h-2 w-2 rounded-full bg-emerald-400 pulse-dot" /> | |
| Active | |
| </div> | |
| </div> | |
| <div className="flex-1 space-y-4 overflow-y-auto p-5"> | |
| {chatHistory.length === 0 && ( | |
| <div className="space-y-6 py-8 text-center"> | |
| <Sparkles className="mx-auto h-8 w-8 text-cyan-300" /> | |
| <div> | |
| <h4 className="font-semibold text-white">Ask about architecture, risks, APIs, or onboarding</h4> | |
| <p className="mt-1 text-sm text-slate-500">Answers are grounded in the scanned repository context.</p> | |
| </div> | |
| <SuggestedQuestions onSelect={handleSend} /> | |
| </div> | |
| )} | |
| {chatHistory.map((message, index) => ( | |
| <MessageBubble key={`${message.role}-${index}`} message={message} /> | |
| ))} | |
| {isAsking && <TypingIndicator />} | |
| <div ref={endRef} /> | |
| </div> | |
| <div className="border-t border-white/10 p-4"> | |
| <div className="flex items-end gap-3"> | |
| <textarea | |
| value={input} | |
| onChange={(event) => setInput(event.target.value)} | |
| onKeyDown={(event) => { | |
| if (event.key === 'Enter' && !event.shiftKey) { | |
| event.preventDefault(); | |
| handleSend(); | |
| } | |
| }} | |
| rows={1} | |
| placeholder="Ask a repository question..." | |
| className="min-h-11 max-h-32 flex-1 resize-none rounded-lg border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-slate-100 outline-none placeholder:text-slate-600 focus:border-cyan-400/50" | |
| /> | |
| <motion.button | |
| whileTap={{ scale: 0.96 }} | |
| onClick={() => handleSend()} | |
| disabled={!input.trim() || isAsking} | |
| className="flex h-11 w-11 items-center justify-center rounded-lg bg-cyan-500 text-slate-950 transition-colors hover:bg-cyan-300 disabled:cursor-not-allowed disabled:opacity-40" | |
| aria-label="Send question" | |
| > | |
| <Send className="h-4 w-4" /> | |
| </motion.button> | |
| </div> | |
| </div> | |
| </section> | |
| ); | |
| } | |