Mercurial
diff react_games/src/current.tsx @ 47:829623189a57
[Gara] Android commit. Bazelfied it.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sat, 13 Dec 2025 14:20:34 -0800 |
| parents | 0cfd7d9277b0 |
| children |
line wrap: on
line diff
--- a/react_games/src/current.tsx Thu Dec 04 06:50:40 2025 -0800 +++ b/react_games/src/current.tsx Sat Dec 13 14:20:34 2025 -0800 @@ -1,265 +1,379 @@ -import { CSSProperties, useEffect, useReducer, useState } from "react"; +import { CSSProperties, useReducer, useState, useRef, useEffect } from "react"; import ReactDOM from "react-dom/client"; /** - * 2048 - * - * 4 X 4 + * CONFIGURATION + * Replace this with your actual API key or fetch it from an environment variable. */ - -const MAX_WIDTH = 4; - -type Color = 'white' | 'orange' | 'yellow' | 'red'; - -type Cell = { - value: number; - color: Color; -} - -type Board = Cell[][]; +const OPENAI_API_KEY = "YOUR_OPENAI_API_KEY_HERE"; -type GameState = "in_progress" | "lost" | "won"; - -type Game = { - board: Board; - state: GameState; - steps: number; -} - -type Command = "u" | "d" | "l" | "r"; - -type GameAction = - { type: "move", command: Command } | { type: "calculate" }; - - -interface GameStyle { - container: CSSProperties; - board: CSSProperties; - cell: (color: Color) => CSSProperties; +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 gameStyle: GameStyle = { - container: { +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", - justifyContent: "center", - alignItems: "center", - height: "100vh" + gap: "15px", + }, + messageBubble: { + padding: "15px", + borderRadius: "8px", + lineHeight: "1.5", + maxWidth: "800px", + margin: "0 auto", + width: "100%", }, - board: { - display: "grid", - gridTemplateColumns: "repeat(4, 50px)", - background: "#EEFFEE", + inputBar: { + width: "100%", + height: "100px", + padding: "15px", + backgroundColor: "#40414f", + color: "white", + border: "none", + borderTop: "1px solid #555", + fontSize: "16px", + resize: "none", + outline: "none", }, - cell: (color: Color) => ({ - display: "flex", - justifyContent: "center", - alignItems: "center", - aspectRatio: "1 / 1 ", - margin: "10px", - background: color, - }) -} +}; + +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', + }; + } -function initializeBoard(): Board { - const board = Array.from({ length: MAX_WIDTH }, () => - Array.from({ length: MAX_WIDTH }, (): Cell => ({ value: 0, color: 'orange' })) - ); - let rowIndex: number; - let colIndex: number; - rowIndex = Math.floor(Math.random() * 4); - colIndex = Math.floor(Math.random() * 4); - board[rowIndex][colIndex].value = 2; - board[rowIndex-1][colIndex].value = 2; - return board; -} + 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]; -function initializeGame(): Game { - return { - board: initializeBoard(), - state: "in_progress", - steps: 0, + 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; } } - -function handleMove(board: Board, command: Command): Board { - // Deep copy the board and initialize the merged status for the new board - const copiedBoard = board.map(row => - row.map(cell => ({ ...cell, merged: false })) - ); - - let diff: { row: number, col: number }; - let startRow: number, endRow: number, stepRow: number; - let startCol: number, endCol: number, stepCol: number; - - const size = copiedBoard.length; - - switch (command) { - case "u": - diff = { row: -1, col: 0 }; - startRow = 0; endRow = size; stepRow = 1; - startCol = 0; endCol = size; stepCol = 1; - break; - case "d": - diff = { row: 1, col: 0 }; - startRow = size - 1; endRow = -1; stepRow = -1; - startCol = 0; endCol = size; stepCol = 1; - break; - case "l": - diff = { row: 0, col: -1 }; - startRow = 0; endRow = size; stepRow = 1; - startCol = 0; endCol = size; stepCol = 1; - break; - case "r": - diff = { row: 0, col: 1 }; - startRow = 0; endRow = size; stepRow = 1; - startCol = size - 1; endCol = -1; stepCol = -1; - break; - } - - for (let rowIndex = startRow; rowIndex !== endRow; rowIndex += stepRow) { - for (let colIndex = startCol; colIndex !== endCol; colIndex += stepCol) { - const currentCell = copiedBoard[rowIndex][colIndex]; - - if (currentCell.value === 0) continue; - - let r = rowIndex; - let c = colIndex; - let emptySlot: { r: number, c: number } = { r: rowIndex, c: colIndex }; - let finalSlot: { r: number, c: number } = { r: rowIndex, c: colIndex }; - - while (true) { - r += diff.row; - c += diff.col; - - if (r < 0 || r >= size || c < 0 || c >= size) { - finalSlot = emptySlot; - break; - } - - const nextCell = copiedBoard[r][c]; - - if (nextCell.value === 0) { - emptySlot = { r, c }; - finalSlot = emptySlot; - } else if (nextCell.value === currentCell.value && !nextCell.merged) { - finalSlot = { r, c }; - break; - } else { - finalSlot = emptySlot; - break; - } - } - - const targetCell = copiedBoard[finalSlot.r][finalSlot.c]; - - if (finalSlot.r === rowIndex && finalSlot.c === colIndex) { - continue; - } - - if (targetCell.value === currentCell.value && !targetCell.merged) { - targetCell.value *= 2; - targetCell.merged = true; - - copiedBoard[rowIndex][colIndex].value = 0; - - } else if (targetCell.value === 0) { - targetCell.value = currentCell.value; - copiedBoard[rowIndex][colIndex].value = 0; - } - } - } - - return copiedBoard; -} - -function addNewItemsToTheBoard(board: Board) { - let randomRowIndex: number; - let randomColIndex: number; +const initialMainPage: MainPage = { + activeChatId: null, + chatHistory: [], + currentMessages: [], + allChats: {}, + sendMessageStatus: 'idle' +}; - let zeroPos = 0; - board.forEach((row) => { - row.forEach((cell) => { - if (cell.value === 0) { - zeroPos += 1; - } - }) - }) - if (zeroPos === 0) { - return; - } +async function fetchOpenAICompletion(messages: Message[]) { + const apiMessages = messages.map(m => ({ + role: m.role, + content: m.content + })); - let curr = 0; - const maxAddedValues = zeroPos < 2 ? 1 : (zeroPos / 2) | 0; - while (curr < maxAddedValues) { - randomRowIndex = Math.floor(Math.random() * board.length) - randomColIndex = Math.floor(Math.random() * board.length) - if (board[randomRowIndex][randomColIndex].value === 0) - { - board[randomRowIndex][randomColIndex].value = 2; - curr++; - } - } -} + 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, + }) + }); -function gameDispatch(game: Game, gameAction: GameAction): Game { - switch(gameAction.type) { - case "move": { - const newBoard = handleMove(game.board, gameAction.command); - addNewItemsToTheBoard(newBoard); - return { - ...game, - board: newBoard, - } + if (!response.ok) { + throw new Error(`API Error: ${response.statusText}`); } - case "calculate": { - return { - ...game, - } - } + + const data = await response.json(); + return data.choices[0].message.content; + } catch (error) { + console.error(error); + throw error; } } function Current() { - const [game, dispatch] = useReducer(gameDispatch, null, initializeGame); + const [state, dispatch] = useReducer(mainPageDispatch, initialMainPage); + const [inputValue, setInputValue] = useState(""); + const messagesEndRef = useRef<HTMLDivElement>(null); + // Auto-scroll to bottom when messages change useEffect(() => { - window.addEventListener("keyup", (e) => { - switch(e.key) { - case "ArrowDown": { - dispatch({ type: "move", command: "d" }); - return; - } - case "ArrowUp": { - dispatch({ type: "move", command: "u" }); - return; - } - case "ArrowRight": { - dispatch({ type: "move", command: "r" }); - return; - } - case "ArrowLeft": { - dispatch({ type: "move", command: "l" }); - return; - } - default: - return; - } - }) + 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={gameStyle.container}> - <h1> 2048 </h1> - <div style={gameStyle.board}> - {game.board.map((row: Cell[]) => { - return row.map((cell: Cell) => (<div style={gameStyle.cell(cell.color)}> {cell.value} </div>)) - })} + <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/