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/