view react_games/src/current.tsx @ 174:1ba8c1df082c hg-web

Remove playground stuff.
author MrJuneJune <me@mrjunejune.com>
date Mon, 19 Jan 2026 18:59:23 -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/