Mercurial
view react_games/src/current.tsx @ 64:a30944e5719e
Added vibe coded markdown to html script since it is useful for me. Updated Dowa so that it can be compiled without dirnet for windows.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Tue, 23 Dec 2025 15:18:46 -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/