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/