| // src/components/threads/ThreadPanel.tsx | |
| import { useEffect, useRef, useState } from "react"; | |
| import { useThreadSystem } from "@/hooks/useThreadSystem"; | |
| import { useThreadContext } from "@/hooks/useThreadContext"; | |
| import { useThreadPersistence } from "@/hooks/useThreadPersistence"; | |
| import { ThreadHeader } from "./ThreadHeader"; | |
| import { ThreadParentBanner } from "./ThreadParentBanner"; | |
| import { ThreadMessageList } from "./ThreadMessageList"; | |
| import { ThreadReplyInput } from "./ThreadReplyInput"; | |
| import { AIContextPanel } from "./AIContextPanel"; | |
| import { ThreadBreadcrumb } from "./ThreadBreadcrumb"; | |
| import { | |
| X, Loader2, CornerDownRight, Sparkles, PanelRightClose, | |
| } from "lucide-react"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| interface ThreadPanelProps { | |
| chatId: string; | |
| currentUsername: string; | |
| currentUserId: number; | |
| onShowProfile: (username: string) => void; | |
| } | |
| export function ThreadPanel({ | |
| chatId, | |
| currentUsername, | |
| currentUserId, | |
| onShowProfile, | |
| }: ThreadPanelProps) { | |
| const { | |
| activeThread, | |
| threadMessages, | |
| messagesLoading, | |
| sending, | |
| hasMoreMessages, | |
| replyingTo, | |
| setReplyingTo, | |
| closeThread, | |
| sendReply, | |
| updateThreadStatus, | |
| deleteThread, | |
| toggleSubscription, | |
| loadMoreMessages, | |
| } = useThreadSystem(chatId); | |
| const { snapshot, loading: ctxLoading, isAiResponding, askAI, summarizeThread, getContextForPrompt } = | |
| useThreadContext(activeThread?.id ?? null); | |
| const { getDraft, saveDraft, clearDraft } = useThreadPersistence(chatId); | |
| const [input, setInput] = useState(""); | |
| const [aiPanelOpen, setAiPanelOpen] = useState(false); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| // Restore draft | |
| useEffect(() => { | |
| if (activeThread) { | |
| setInput(getDraft(activeThread.id)); | |
| } | |
| }, [activeThread?.id]); | |
| // Save draft on change | |
| useEffect(() => { | |
| if (activeThread && input) { | |
| saveDraft(activeThread.id, input); | |
| } | |
| }, [input, activeThread?.id]); | |
| // Auto‑scroll | |
| useEffect(() => { | |
| messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }, [threadMessages]); | |
| const handleSend = async () => { | |
| if (!input.trim() && !sending) return; | |
| await sendReply(input); | |
| setInput(""); | |
| if (activeThread) clearDraft(activeThread.id); | |
| }; | |
| const handleAiAsk = async (question: string) => { | |
| if (!question.trim()) return; | |
| // Show user message optimistically | |
| const reply = await askAI(question); | |
| // The AI response comes back as a thread message via WebSocket or direct insert | |
| // For simplicity, we display it inline | |
| setAiPanelOpen(false); | |
| }; | |
| if (!activeThread) { | |
| return ( | |
| <div className="w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 flex items-center justify-center h-full"> | |
| <div className="text-center text-gray-400 px-6"> | |
| <CornerDownRight className="w-10 h-10 mx-auto mb-3 opacity-30" /> | |
| <p className="text-sm">Select a thread to view the conversation</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <motion.div | |
| initial={{ x: "100%" }} | |
| animate={{ x: 0 }} | |
| exit={{ x: "100%" }} | |
| transition={{ type: "spring", damping: 25, stiffness: 300 }} | |
| className="w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 flex flex-col h-full shadow-xl" | |
| > | |
| {/* Breadcrumb */} | |
| <ThreadBreadcrumb thread={activeThread} onClose={closeThread} /> | |
| {/* Header */} | |
| <ThreadHeader | |
| thread={activeThread} | |
| onStatusChange={(status) => updateThreadStatus(activeThread.id, status)} | |
| onDelete={() => deleteThread(activeThread.id)} | |
| onToggleSubscribe={() => toggleSubscription(activeThread.id)} | |
| onToggleAiPanel={() => setAiPanelOpen(!aiPanelOpen)} | |
| aiPanelOpen={aiPanelOpen} | |
| onSummarize={summarizeThread} | |
| isAiResponding={isAiResponding} | |
| /> | |
| {/* AI Context Panel (collapsible) */} | |
| <AnimatePresence> | |
| {aiPanelOpen && ( | |
| <AIContextPanel | |
| threadId={activeThread.id} | |
| onSuggestion={(suggestion) => setInput((prev) => prev ? `${prev}\n${suggestion}` : suggestion)} | |
| /> | |
| )} | |
| </AnimatePresence> | |
| {/* Parent message banner */} | |
| {activeThread.parentMessageId && ( | |
| <ThreadParentBanner | |
| messageId={activeThread.parentMessageId} | |
| threadTitle={activeThread.title} | |
| /> | |
| )} | |
| {/* Messages */} | |
| <ThreadMessageList | |
| messages={threadMessages} | |
| loading={messagesLoading} | |
| currentUsername={currentUsername} | |
| currentUserId={currentUserId} | |
| onReply={(ctx) => setReplyingTo(ctx)} | |
| onShowProfile={onShowProfile} | |
| hasMore={hasMoreMessages} | |
| onLoadMore={loadMoreMessages} | |
| loadingMore={messagesLoading} | |
| /> | |
| <div ref={messagesEndRef} /> | |
| {/* Reply preview */} | |
| {replyingTo && ( | |
| <div className="px-4 py-2 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-100 dark:border-gray-700 flex items-center justify-between text-sm"> | |
| <span className="text-gray-500 truncate"> | |
| Replying to <strong>{replyingTo.authorUsername}</strong>: {replyingTo.body.substring(0, 50)}… | |
| </span> | |
| <button onClick={() => setReplyingTo(null)} className="text-red-400 hover:text-red-600"> | |
| <X className="w-4 h-4" /> | |
| </button> | |
| </div> | |
| )} | |
| {/* Input */} | |
| <ThreadReplyInput | |
| value={input} | |
| onChange={setInput} | |
| onSend={handleSend} | |
| sending={sending} | |
| onAiAsk={() => setAiPanelOpen(true)} | |
| placeholder={`Reply in "${activeThread.title}"...`} | |
| /> | |
| </motion.div> | |
| ); | |
| } | |
Xet Storage Details
- Size:
- 5.76 kB
- Xet hash:
- e6ed23dc8394cc4354232a4801d8889c36d41f8f76d44571c0cc1a3a9c76524a
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.