Mercurial
view react_games/src/current.tsx @ 71:75de5903355c
Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Sun, 28 Dec 2025 20:34:22 -0800 |
| parents | 829623189a57 |
| children |
line wrap: on
line source
import { CSSProperties, useReducer, useState, useRef, useEffect } from "react"; import ReactDOM from "react-dom/client"; /** * CONFIGURATION * Replace this with your actual API key or fetch it from an environment variable. */ const OPENAI_API_KEY = "YOUR_OPENAI_API_KEY_HERE"; interface ChatStyle { mainContainer: CSSProperties; sideBar: CSSProperties; sideBarItem: CSSProperties; // Added for hover/layout mainChat: CSSProperties; mainMessage: CSSProperties; messageBubble: CSSProperties; // Added for styling messages inputBar: CSSProperties; } const STYLES: ChatStyle = { mainContainer: { display: "flex", backgroundColor: "#1e1e1e", color: "white", fontFamily: "sans-serif", height: "100vh", margin: 0, }, sideBar: { width: "250px", height: "100vh", overflowY: "auto", backgroundColor: "#000000", borderRight: "1px solid #333", padding: "1rem", }, sideBarItem: { padding: "10px", cursor: "pointer", backgroundColor: "#333", marginBottom: "5px", borderRadius: "5px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", }, mainChat: { flex: 1, display: "flex", flexDirection: "column", backgroundColor: "#343541", height: "100vh", }, mainMessage: { flex: 1, padding: "20px", overflowY: "auto", display: "flex", flexDirection: "column", gap: "15px", }, messageBubble: { padding: "15px", borderRadius: "8px", lineHeight: "1.5", maxWidth: "800px", margin: "0 auto", width: "100%", }, inputBar: { width: "100%", height: "100px", padding: "15px", backgroundColor: "#40414f", color: "white", border: "none", borderTop: "1px solid #555", fontSize: "16px", resize: "none", outline: "none", }, }; type Chat = { id: string; title: string; createdAt: number; }; type ChatHistory = Chat[]; type Message = { id: string; role: 'user' | 'assistant'; // Changed from 'author' to match OpenAI spec usually content: string; // Changed from 'message' to 'content' createdAt: number; }; type MainPage = { activeChatId: string | null; chatHistory: ChatHistory; currentMessages: Message[]; allChats: Record<string, Message[]>; sendMessageStatus: 'idle' | 'inProgress' | 'failed' | 'success'; }; // Expanded Actions type MainPageAction = | { type: 'select_chat'; payload: { chatId: string } } | { type: 'user_message_sent'; payload: { content: string; newChatId?: string } } | { type: 'api_response_received'; payload: { content: string } } | { type: 'api_error'; payload: { error: string } }; function mainPageDispatch(state: MainPage, action: MainPageAction): MainPage { switch (action.type) { case 'select_chat': { const { chatId } = action.payload; return { ...state, activeChatId: chatId, currentMessages: state.allChats[chatId] || [], sendMessageStatus: 'idle', }; } case 'user_message_sent': { const { content, newChatId } = action.payload; const newMessage: Message = { id: Date.now().toString(), role: 'user', content: content, createdAt: Date.now(), }; // If we are starting a brand new chat (no active ID) if (newChatId && !state.activeChatId) { const newChatMetadata: Chat = { id: newChatId, title: content.substring(0, 30) + (content.length > 30 ? "..." : ""), createdAt: Date.now() }; return { ...state, activeChatId: newChatId, sendMessageStatus: 'inProgress', chatHistory: [newChatMetadata, ...state.chatHistory], currentMessages: [newMessage], allChats: { ...state.allChats, [newChatId]: [newMessage] } }; } const chatId = state.activeChatId!; const updatedMessages = [...state.currentMessages, newMessage]; return { ...state, sendMessageStatus: 'inProgress', currentMessages: updatedMessages, allChats: { ...state.allChats, [chatId]: updatedMessages } }; } case 'api_response_received': { if (!state.activeChatId) return state; const newMsg: Message = { id: Date.now().toString(), role: 'assistant', content: action.payload.content, createdAt: Date.now() }; const updatedMessages = [...state.currentMessages, newMsg]; return { ...state, sendMessageStatus: 'success', currentMessages: updatedMessages, allChats: { ...state.allChats, [state.activeChatId]: updatedMessages } }; } case 'api_error': { return { ...state, sendMessageStatus: 'failed' }; } default: return state; } } const initialMainPage: MainPage = { activeChatId: null, chatHistory: [], currentMessages: [], allChats: {}, sendMessageStatus: 'idle' }; async function fetchOpenAICompletion(messages: Message[]) { const apiMessages = messages.map(m => ({ role: m.role, content: m.content })); try { const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OPENAI_API_KEY}` }, body: JSON.stringify({ model: "gpt-3.5-turbo", // or gpt-4 messages: apiMessages, }) }); if (!response.ok) { throw new Error(`API Error: ${response.statusText}`); } const data = await response.json(); return data.choices[0].message.content; } catch (error) { console.error(error); throw error; } } function Current() { const [state, dispatch] = useReducer(mainPageDispatch, initialMainPage); const [inputValue, setInputValue] = useState(""); const messagesEndRef = useRef<HTMLDivElement>(null); // Auto-scroll to bottom when messages change useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [state.currentMessages]); const handleSendMessage = async () => { if (!inputValue.trim()) return; // 1. Determine if this is a new chat or existing const isNewChat = !state.activeChatId; const currentChatId = state.activeChatId || crypto.randomUUID(); // Generate ID for new chat const textToSend = inputValue; setInputValue(""); // Clear input immediately // 2. Dispatch User Message (Optimistic UI) dispatch({ type: 'user_message_sent', payload: { content: textToSend, newChatId: isNewChat ? currentChatId : undefined } }); try { // 3. Prepare context (include previous messages + new one) // Note: We reconstruct the array here because state update is async and might not be ready const contextMessages: Message[] = [ ...(isNewChat ? [] : state.currentMessages), { id: 'temp', role: 'user', content: textToSend, createdAt: Date.now() } ]; // 4. Call API const aiResponse = await fetchOpenAICompletion(contextMessages); // 5. Dispatch Success dispatch({ type: 'api_response_received', payload: { content: aiResponse } }); } catch (error) { dispatch({ type: 'api_error', payload: { error: 'Failed to fetch' } }); alert("Failed to connect to OpenAI. Check API Key."); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; return ( <div style={STYLES.mainContainer}> {/* Sidebar */} <div style={STYLES.sideBar}> <h3 style={{color: '#ececf1', marginBottom: '20px'}}>History</h3> <div style={{...STYLES.sideBarItem, border: '1px dashed #555'}} onClick={() => { // Reset to empty view for a new chat // We can achieve this by setting activeChatId to null locally if we wanted // But for this simple reducer, we can just reload the page or add a 'reset' action. // For now, let's just allow clicking existing ones. window.location.reload(); }} > + New Chat </div> {state.chatHistory.map((hist) => ( <div key={hist.id} style={{ ...STYLES.sideBarItem, backgroundColor: state.activeChatId === hist.id ? '#555' : '#333' }} onClick={() => dispatch({ type: 'select_chat', payload: { chatId: hist.id } })} > {hist.title} </div> ))} </div> {/* Main Chat Area */} <div style={STYLES.mainChat}> <div style={STYLES.mainMessage}> {state.currentMessages.length === 0 && ( <div style={{color: '#666', textAlign: 'center', marginTop: '40%'}}> Send a message to start... </div> )} {state.currentMessages.map((msg) => ( <div key={msg.id} style={{ ...STYLES.messageBubble, backgroundColor: msg.role === 'assistant' ? '#444654' : 'transparent' }} > <strong style={{color: msg.role === 'assistant' ? '#10a37f' : '#ececf1'}}> {msg.role === 'assistant' ? 'AI' : 'You'}: </strong> <div style={{whiteSpace: 'pre-wrap', marginTop: '5px'}}>{msg.content}</div> </div> ))} {state.sendMessageStatus === 'inProgress' && ( <div style={{...STYLES.messageBubble, color: '#888'}}>AI is typing...</div> )} <div ref={messagesEndRef} /> </div> {/* Input Area */} <textarea style={STYLES.inputBar} placeholder="Send a message..." value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleKeyDown} disabled={state.sendMessageStatus === 'inProgress'} /> </div> </div> ); } ReactDOM.createRoot(document.getElementById("root")!).render(<Current />); https://www.linkedin.com/in/drakewong/ https://www.linkedin.com/in/dmitry-manannikov/