Mercurial
view react_games/src/current.tsx @ 279:b3b547563ec7
Add Google connector service and agent wiki
Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code.
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Mon, 17 Aug 2026 22:22:36 -0700 |
| 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/