164 lines
4.7 KiB
TypeScript
164 lines
4.7 KiB
TypeScript
import { useState, useRef, useEffect, useCallback } from "react";
|
|
import { useAtom, useAtomValue } from "jotai";
|
|
import { chatMessagesAtom, chatStreamCountAtom } from "../atoms/chatAtoms";
|
|
import { IpcClient } from "@/ipc/ipc_client";
|
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
|
import { ChatHeader } from "./chat/ChatHeader";
|
|
import { MessagesList } from "./chat/MessagesList";
|
|
import { ChatInput } from "./chat/ChatInput";
|
|
import { VersionPane } from "./chat/VersionPane";
|
|
import { ChatError } from "./chat/ChatError";
|
|
|
|
interface ChatPanelProps {
|
|
chatId?: number;
|
|
isPreviewOpen: boolean;
|
|
onTogglePreview: () => void;
|
|
}
|
|
|
|
export function ChatPanel({
|
|
chatId,
|
|
isPreviewOpen,
|
|
onTogglePreview,
|
|
}: ChatPanelProps) {
|
|
const appId = useAtomValue(selectedAppIdAtom);
|
|
const [messages, setMessages] = useAtom(chatMessagesAtom);
|
|
const [appName, setAppName] = useState<string>("Chat");
|
|
const [isVersionPaneOpen, setIsVersionPaneOpen] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const streamCount = useAtomValue(chatStreamCountAtom);
|
|
// Reference to store the processed prompt so we don't submit it twice
|
|
const processedPromptRef = useRef<string | null>(null);
|
|
|
|
const messagesEndRef = useRef<HTMLDivElement | null>(null);
|
|
const messagesContainerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
// Scroll-related properties
|
|
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
|
const userScrollTimeoutRef = useRef<number | null>(null);
|
|
const lastScrollTopRef = useRef<number>(0);
|
|
|
|
const scrollToBottom = (behavior: ScrollBehavior = "smooth") => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior });
|
|
};
|
|
|
|
const handleScroll = () => {
|
|
if (!messagesContainerRef.current) return;
|
|
|
|
const container = messagesContainerRef.current;
|
|
const currentScrollTop = container.scrollTop;
|
|
|
|
if (currentScrollTop < lastScrollTopRef.current) {
|
|
setIsUserScrolling(true);
|
|
|
|
if (userScrollTimeoutRef.current) {
|
|
window.clearTimeout(userScrollTimeoutRef.current);
|
|
}
|
|
|
|
userScrollTimeoutRef.current = window.setTimeout(() => {
|
|
setIsUserScrolling(false);
|
|
}, 1000);
|
|
}
|
|
|
|
lastScrollTopRef.current = currentScrollTop;
|
|
};
|
|
|
|
useEffect(() => {
|
|
console.log("streamCount", streamCount);
|
|
scrollToBottom();
|
|
}, [streamCount]);
|
|
|
|
useEffect(() => {
|
|
const container = messagesContainerRef.current;
|
|
if (container) {
|
|
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
}
|
|
|
|
return () => {
|
|
if (container) {
|
|
container.removeEventListener("scroll", handleScroll);
|
|
}
|
|
if (userScrollTimeoutRef.current) {
|
|
window.clearTimeout(userScrollTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const fetchAppName = async () => {
|
|
if (!appId) return;
|
|
|
|
try {
|
|
const app = await IpcClient.getInstance().getApp(appId);
|
|
if (app?.name) {
|
|
setAppName(app.name);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch app name:", error);
|
|
}
|
|
};
|
|
|
|
fetchAppName();
|
|
}, [appId]);
|
|
|
|
const fetchChatMessages = useCallback(async () => {
|
|
if (!chatId) {
|
|
setMessages([]);
|
|
return;
|
|
}
|
|
const chat = await IpcClient.getInstance().getChat(chatId);
|
|
setMessages(chat.messages);
|
|
}, [chatId, setMessages]);
|
|
|
|
useEffect(() => {
|
|
fetchChatMessages();
|
|
}, [fetchChatMessages]);
|
|
|
|
// Auto-scroll effect when messages change
|
|
useEffect(() => {
|
|
if (
|
|
!isUserScrolling &&
|
|
messagesContainerRef.current &&
|
|
messages.length > 0
|
|
) {
|
|
const { scrollTop, clientHeight, scrollHeight } =
|
|
messagesContainerRef.current;
|
|
const threshold = 280;
|
|
const isNearBottom =
|
|
scrollHeight - (scrollTop + clientHeight) <= threshold;
|
|
|
|
if (isNearBottom) {
|
|
requestAnimationFrame(() => {
|
|
scrollToBottom("instant");
|
|
});
|
|
}
|
|
}
|
|
}, [messages, isUserScrolling]);
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<ChatHeader
|
|
isPreviewOpen={isPreviewOpen}
|
|
onTogglePreview={onTogglePreview}
|
|
onVersionClick={() => setIsVersionPaneOpen(!isVersionPaneOpen)}
|
|
/>
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{!isVersionPaneOpen && (
|
|
<div className="flex-1 flex flex-col min-w-0">
|
|
<MessagesList
|
|
messages={messages}
|
|
messagesEndRef={messagesEndRef}
|
|
ref={messagesContainerRef}
|
|
/>
|
|
<ChatError error={error} onDismiss={() => setError(null)} />
|
|
<ChatInput chatId={chatId} />
|
|
</div>
|
|
)}
|
|
<VersionPane
|
|
isVisible={isVersionPaneOpen}
|
|
onClose={() => setIsVersionPaneOpen(false)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|