import React, { useState, useEffect, FormEvent } from 'react' import { storeName } from '../consts' import '../styles/scrollbar.css' import '../styles/github.css' import { type Conversation, type Message } from '../types' import { type Model } from '../types/models' import { type IDBPDatabase } from 'idb' import { type Tool } from 'use-mcp/react' import ChatMessage from './ChatMessage' import ChatInput from './ChatInput' import { useAutoscroll } from '../hooks/useAutoscroll' import { useStreamResponse } from '../hooks/useStreamResponse' import { setApiKey } from '../utils/apiKeys' import ApiKeyModal from './ApiKeyModal' interface ConversationThreadProps { conversations: Conversation[] conversationId?: number setConversationId: (id: number) => void setConversations: React.Dispatch> db: IDBPDatabase selectedModel: Model onApiKeyUpdate: () => void mcpTools: Tool[] } const ConversationThread: React.FC = ({ conversations, conversationId, setConversationId, setConversations, db, selectedModel, onApiKeyUpdate, mcpTools, }) => { const [input, setInput] = useState('') const [apiKeyModal, setApiKeyModal] = useState<{ isOpen: boolean; model: Model | null }>({ isOpen: false, model: null, }) const { messagesEndRef, messagesContainerRef, scrollToBottom } = useAutoscroll() const handleApiKeyRequired = async (model: Model): Promise => { return new Promise((resolve) => { setApiKeyModal({ isOpen: true, model, }) // Store the resolve function to call when modal is closed window.apiKeyModalResolve = resolve }) } const handleApiKeySave = (apiKey: string) => { if (apiKeyModal.model) { setApiKey(apiKeyModal.model.provider.id, apiKey) setApiKeyModal({ isOpen: false, model: null }) // Notify parent that API key was updated onApiKeyUpdate() // Resolve the promise to continue with the request if (window.apiKeyModalResolve) { window.apiKeyModalResolve(true) delete window.apiKeyModalResolve } } } const handleApiKeyCancel = () => { setApiKeyModal({ isOpen: false, model: null }) // Resolve the promise with false to cancel the request if (window.apiKeyModalResolve) { window.apiKeyModalResolve(false) delete window.apiKeyModalResolve } } const { isLoading, setIsLoading, streamStarted, controller, streamResponse, aiResponseRef } = useStreamResponse({ conversationId, setConversations, scrollToBottom, selectedModel, onApiKeyRequired: handleApiKeyRequired, mcpTools, }) const currentConversation = conversations.find((conv) => conv.id === conversationId) || { messages: [], title: '' } //when new message chunks are streamed in, scroll to bottom useEffect(() => { scrollToBottom() }, [aiResponseRef.current]) //when conversation changes, scroll to bottom useEffect(scrollToBottom, [conversationId]) //when conversation changes, reset input useEffect(() => { setInput('') }, [conversationId]) const handleSubmit = async (e: FormEvent) => { e.preventDefault() if (!input.trim()) return if (currentConversation.messages.length === 0) { setConversations((prev) => { const updated = [...prev] updated.unshift({ id: conversationId, title: 'New conversation', messages: [], }) return updated }) } const userMessage: Message = { role: 'user', content: input } setInput('') setIsLoading(true) setConversations((prev) => { const updated = [...prev] const conv = updated.find((c) => c.id === conversationId) if (conv && conv.messages?.[conv.messages.length - 1] !== userMessage) { conv.messages.push(userMessage) } return updated }) await streamResponse([...currentConversation.messages, userMessage]) setIsLoading(false) } const storeMessages = async () => { if (!currentConversation.messages || currentConversation.messages.length === 0) { return } const store = db.transaction(storeName, 'readwrite').objectStore(storeName) const objectData = { id: conversationId, title: currentConversation.title, messages: currentConversation.messages, } const value = await store.put(objectData) setConversationId(Number(value)) } useEffect(() => { if (db && conversationId) { storeMessages() } }, [conversations]) console.log({ currentConversation }) return (
{currentConversation.messages.length === 0 ? (
{/* Title and description text hidden but kept in codebase */} {false && ( <>

What do you want to know?

AI chat template built with React, Vite and Cloudflare Workers AI.
Find the source code on{' '} GitHub .

)}
) : (
{currentConversation.messages.map((message, index) => ( ))} {isLoading && !streamStarted &&
Thinking...
}
)}
) } export default ConversationThread