File size: 6,713 Bytes
1f7ead8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | import React, { useState, useEffect, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import { Send, Bot, User, Sparkles } from 'lucide-react';
import { querySystem } from '../api';
import { motion } from 'framer-motion';
const Notebook = ({ messages, setMessages, onCitationClick, notebookId }) => { // Accept notebookId
// const [messages, setMessages] = useState([ ... ]); // Removed internal state
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const scrollRef = useRef(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const handleSend = async () => {
if (!input.trim()) return;
const userMsg = { role: 'user', content: input };
setMessages(prev => [...prev, userMsg]);
setInput('');
setLoading(true);
try {
// Pass notebookId to querySystem
const response = await querySystem(userMsg.content, notebookId);
const botMsg = {
role: 'assistant',
content: response.answer,
citations: response.citations
};
setMessages(prev => [...prev, botMsg]);
} catch (err) {
setMessages(prev => [...prev, { role: 'assistant', content: 'Connection error. Please ensure background services are running.' }]);
}
setLoading(false);
};
return (
<div className="flex flex-col h-full bg-white relative">
{/* Scrollable Notebook Area */}
<div className="flex-1 overflow-y-auto px-12 py-8" ref={scrollRef}>
<div className="max-w-3xl mx-auto space-y-10 pb-32">
{messages.map((msg, idx) => (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
key={idx}
className={`group ${msg.role === 'user' ? '' : ''}`}
>
{msg.role === 'user' ? (
<h3 className="text-lg font-semibold text-slate-900 mb-4">{msg.content}</h3>
) : (
<div className="bg-white">
<div className="prose prose-slate max-w-none prose-headings:font-bold prose-h1:text-2xl prose-h2:text-xl prose-p:text-slate-700 prose-li:text-slate-700">
<ReactMarkdown
remarkPlugins={[remarkMath]}
rehypePlugins={[rehypeKatex]}
>
{msg.content}
</ReactMarkdown>
</div>
{/* Citation Chips */}
{msg.citations && msg.citations.length > 0 && (
<div className="flex flex-wrap gap-2 mt-6 pt-4 border-t border-slate-100">
{msg.citations.map((cit, i) => (
<button
key={i}
onClick={() => onCitationClick(cit)} // Prop to open right panel
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-slate-50 border border-slate-200 text-xs font-medium text-slate-600 hover:bg-blue-50 hover:border-blue-200 hover:text-blue-600 transition-colors"
>
<span className="w-4 h-4 rounded-full bg-slate-200 text-slate-600 flex items-center justify-center text-[10px] font-bold">{i + 1}</span>
{cit.paper_id.slice(0, 20)}...
</button>
))}
</div>
)}
</div>
)}
</motion.div>
))}
{loading && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex items-center gap-2 text-slate-400 italic"
>
<Sparkles size={16} className="animate-spin" />
<span>Analyzing documents...</span>
</motion.div>
)}
</div>
</div>
{/* Input Area - Fixed at bottom center like a floating bar */}
<div className="absolute bottom-6 left-0 right-0 px-4">
<div className="max-w-3xl mx-auto">
<div className="bg-white p-2 rounded-2xl shadow-xl border border-slate-200 flex items-center gap-2">
<input
type="text"
className="flex-1 bg-transparent border-0 px-4 py-3 text-slate-700 placeholder-slate-400 focus:ring-0 text-base"
placeholder="Ask a question about your sources..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSend()}
/>
<button
onClick={handleSend}
disabled={loading || !input.trim()}
className="p-3 rounded-xl bg-slate-900 text-white hover:bg-slate-700 disabled:opacity-50 transition-all"
>
<Send size={18} />
</button>
</div>
<p className="text-center text-xs text-slate-400 mt-3">
AI can make mistakes. Check important info.
</p>
</div>
</div>
</div>
);
};
export default Notebook;
|