tahamajs's picture
download
raw
5.76 kB
// 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.