[Redesign] Frontend - Conversation-based Chat UI #60

Open
opened 2026-04-13 12:51:49 +02:00 by shoko · 0 comments
Owner

Detailed Implementation Guide for Frontend Chat UI


⚠️ Prerequisite

This issue assumes issue #59 (Backend Chat System) is COMPLETED.

The frontend will consume the API endpoints created in #59:

  • GET /api/conversations - List conversations
  • POST /api/conversations - Create conversation
  • GET /api/conversations/{id} - Get conversation with messages
  • POST /api/conversations/{id}/chat - Send message
  • POST /api/conversations/{id}/set-bot - Link bot to conversation

Summary

Redesign frontend to support conversation-based chat interface with left/right panes.


Current State (Before)

  • Single bot per chat
  • No conversation list
  • No anonymous user support
  • No rate limiting UI

Target State (After)

  • Conversations on left pane
  • Bot info on right pane
  • Anonymous user banner
  • Rate limit handling

Implementation Phases

Phase 1: Routing & Layout

1.1 Route Structure

Update src/frontend/src/App.tsx or router config:

// Routes
const routes = [
  { path: '/', element: <LandingPage /> },
  { path: '/home', element: <ConversationListPage /> },
  { path: '/chat', element: <ChatPage /> },  // No conversation selected
  { path: '/chat/:conversationId', element: <ChatPage /> },
];

1.2 Main Layout Component

Create src/frontend/src/components/ChatLayout.tsx:

interface ChatLayoutProps {
  leftPane?: React.ReactNode;
  rightPane?: React.ReactNode;
  children: React.ReactNode;
}

export function ChatLayout({ leftPane, rightPane, children }: ChatLayoutProps) {
  return (
    <div className="flex h-screen">
      {/* Left Pane - Conversation List */}
      {leftPane && (
        <div className="w-64 border-r border-gray-700">
          {leftPane}
        </div>
      )}
      
      {/* Main Chat Area */}
      <div className="flex-1 flex flex-col">
        {children}
      </div>
      
      {/* Right Pane - Bot Info */}
      {rightPane && (
        <div className="w-72 border-l border-gray-700 p-4">
          {rightPane}
        </div>
      )}
    </div>
  );
}

Check: Routes work, layout renders with conditional panes


Phase 2: API Services

2.1 Conversation API Client

Create src/frontend/src/services/conversation.ts:

import { api } from './api';

export interface Conversation {
  id: string;
  user_id: string | null;
  bot_id: string | null;
  title: string;
  created_at: string;
  updated_at: string;
}

export interface Message {
  id: string;
  conversation_id: string;
  role: 'user' | 'assistant';
  content: string;
  created_at: string;
}

export const conversationApi = {
  list: () => api.get<Conversation[]>('/conversations'),
  
  create: () => api.post<Conversation>('/conversations', {}),
  
  get: (id: string) => api.get<Conversation & { messages: Message[] }>(
    `/conversations/${id}`
  ),
  
  delete: (id: string) => api.delete(`/conversations/${id}`),
  
  chat: (conversationId: string, message: string) => api.post(
    `/conversations/${conversationId}/chat`,
    { message }
  ),
  
  setBot: (conversationId: string, botId: string) => api.post(
    `/conversations/${conversationId}/set-bot`,
    { bot_id: botId }
  ),
};

2.2 Auth Context

Create or update src/frontend/src/contexts/AuthContext.tsx:

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  anonymousToken: string | null;
}

export function AuthProvider({ children }) {
  const [state, setState] = useState<AuthState>({
    user: null,
    isAuthenticated: false,
    anonymousToken: getCookie('anonymous_token'),
  });
  
  // ... login/logout methods
  
  return (
    <AuthContext.Provider value={state}>
      {children}
    </AuthContext.Provider>
  );
}

Check: API calls work, auth state persists


Phase 3: Components

3.1 Conversation List (Left Pane)

Create src/frontend/src/components/ConversationList.tsx:

import { useState, useEffect } from 'react';
import { conversationApi, Conversation } from '../services/conversation';

export function ConversationList() {
  const [conversations, setConversations] = useState<Conversation[]>([]);
  const [loading, setLoading] = useState(true);
  const { conversationId } = useParams();
  
  useEffect(() => {
    loadConversations();
  }, []);
  
  async function loadConversations() {
    try {
      const data = await conversationApi.list();
      setConversations(data);
    } catch (error) {
      console.error('Failed to load conversations', error);
    } finally {
      setLoading(false);
    }
  }
  
  async function createConversation() {
    try {
      const newConv = await conversationApi.create();
      setConversations([newConv, ...conversations]);
      navigate(`/chat/${newConv.id}`);
    } catch (error) {
      console.error('Failed to create conversation', error);
    }
  }
  
  if (loading) return <div className="p-4">Loading...</div>;
  
  return (
    <div className="h-full flex flex-col">
      <button
        onClick={createConversation}
        className="m-2 p-2 bg-green-600 rounded hover:bg-green-700"
      >
        + New Chat
      </button>
      
      <div className="flex-1 overflow-y-auto">
        {conversations.map(conv => (
          <div
            key={conv.id}
            className={`p-3 cursor-pointer hover:bg-gray-800 ${
              conv.id === conversationId ? 'bg-gray-800' : ''
            }`}
            onClick={() => navigate(`/chat/${conv.id}`)}
          >
            <div className="font-medium truncate">{conv.title}</div>
            <div className="text-xs text-gray-400">
              {formatDate(conv.updated_at)}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

3.2 Chat Area (Main)

Create or update src/frontend/src/components/ChatArea.tsx:

export function ChatArea({ conversationId }: { conversationId?: string }) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [input, setInput] = useState('');
  const [loading, setLoading] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (conversationId) {
      loadConversation();
    }
  }, [conversationId]);
  
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  
  async function sendMessage() {
    if (!input.trim() || loading) return;
    
    const userMessage = input;
    setInput('');
    setLoading(true);
    
    try {
      const response = await conversationApi.chat(conversationId, userMessage);
      setMessages(prev => [...prev, response.messages]);
    } catch (error: any) {
      if (error.status === 429) {
        alert('Rate limited from the agent service. Please come back later.');
      } else if (error.status === 403) {
        alert("You've reached the limit. Please create an account to continue.");
      }
    } finally {
      setLoading(false);
    }
  }
  
  return (
    <div className="flex-1 overflow-y-auto p-4">
      {messages.map(msg => (
        <div
          key={msg.id}
          className={`mb-4 ${msg.role === 'user' ? 'text-right' : 'text-left'}`}
        >
          <div className={`inline-block max-w-[70%] p-3 rounded ${
            msg.role === 'user' 
              ? 'bg-gray-700' 
              : 'bg-blue-900'
          }`}>
            {msg.content}
          </div>
        </div>
      ))}
      <div ref={messagesEndRef} />
    </div>
  );
}

3.3 Bot Info Panel (Right Pane)

Create src/frontend/src/components/BotInfoPanel.tsx:

import { useState, useEffect } from 'react';
import { botApi, Bot } from '../services/bot';

interface BotInfoPanelProps {
  conversationId?: string;
  currentBotId?: string;
  onBotChange: (botId: string) => void;
}

export function BotInfoPanel({ conversationId, currentBotId, onBotChange }: BotInfoPanelProps) {
  const [bot, setBot] = useState<Bot | null>(null);
  const [showSelector, setShowSelector] = useState(false);
  const [userBots, setUserBots] = useState<Bot[]>([]);
  
  useEffect(() => {
    if (currentBotId) {
      loadBot(currentBotId);
    }
  }, [currentBotId]);
  
  async function loadBot(botId: string) {
    try {
      const data = await botApi.get(botId);
      setBot(data);
    } catch (error) {
      console.error('Failed to load bot', error);
    }
  }
  
  async function loadUserBots() {
    try {
      const data = await botApi.list();
      setUserBots(data);
      setShowSelector(true);
    } catch (error) {
      console.error('Failed to load bots', error);
    }
  }
  
  if (!bot) {
    return (
      <div className="text-gray-400">
        <p>No bot selected</p>
        <button
          onClick={loadUserBots}
          className="mt-2 text-green-500 hover:underline"
        >
          Select Bot
        </button>
      </div>
    );
  }
  
  return (
    <div>
      <h3 className="font-bold text-lg mb-2">{bot.name}</h3>
      
      <div className="space-y-2 text-sm">
        <div>
          <span className="text-gray-400">Chain:</span> {bot.chain}
        </div>
        <div>
          <span className="text-gray-400">Status:</span>{' '}
          <span className={bot.status === 'active' ? 'text-green-500' : 'text-gray-500'}>
            {bot.status}
          </span>
        </div>
        <div className="border-t border-gray-700 pt-2 mt-2">
          <span className="text-gray-400">Strategy:</span>
          <p className="mt-1">{bot.strategy}</p>
        </div>
      </div>
      
      <button
        onClick={loadUserBots}
        className="mt-4 w-full py-2 border border-gray-600 rounded hover:bg-gray-800"
      >
        Change Bot
      </button>
      
      {/* Bot Selector Modal */}
      {showSelector && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
          <div className="bg-gray-900 p-4 rounded-lg w-80">
            <h4 className="font-bold mb-2">Select Bot</h4>
            {userBots.map(b => (
              <div
                key={b.id}
                className="p-2 hover:bg-gray-800 cursor-pointer"
                onClick={() => {
                  onBotChange(b.id);
                  setShowSelector(false);
                }}
              >
                {b.name}
              </div>
            ))}
            <button
              onClick={() => setShowSelector(false)}
              className="mt-2 w-full py-2 border border-gray-600 rounded"
            >
              Cancel
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

3.4 Non-logged User Banner

Create src/frontend/src/components/AnonymousBanner.tsx:

interface AnonymousBannerProps {
  chatCount?: number;
}

export function AnonymousBanner({ chatCount = 0 }: AnonymousBannerProps) {
  const showWarning = chatCount >= 40;
  
  return (
    <div className={`p-2 text-center text-sm ${
      showWarning ? 'bg-yellow-900 text-yellow-200' : 'bg-gray-800 text-gray-400'
    }`}>
      {showWarning ? (
        <span>
          ⚠️ You've used {chatCount}/50 messages.{' '}
          <a href="/login" className="underline text-yellow-200">Login to continue</a>
        </span>
      ) : (
        <span>Your progress is not saved. <a href="/login" className="underline">Login to save</a></span>
      )}
    </div>
  );
}

3.5 Loading Animation (Custom)

Create src/frontend/src/components/CandlestickLoader.tsx:

export function CandlestickLoader() {
  return (
    <div className="flex items-center gap-1 h-8">
      {[...Array(4)].map((_, i) => (
        <div
          key={i}
          className="w-2 bg-green-500 rounded-sm animate-pulse"
          style={{
            height: `${Math.random() * 20 + 10}px`,
            animationDelay: `${i * 0.15}s`,
          }}
        />
      ))}
    </div>
  );
}

Phase 4: Page Components

4.1 Home Page (Conversation List)

Create src/frontend/src/pages/HomePage.tsx:

export function HomePage() {
  return (
    <ChatLayout leftPane={<ConversationList />}>
      <div className="flex-1 flex items-center justify-center text-gray-400">
        Select a conversation or start a new one
      </div>
    </ChatLayout>
  );
}

4.2 Chat Page

Create src/frontend/src/pages/ChatPage.tsx:

export function ChatPage() {
  const { conversationId } = useParams();
  const { isAuthenticated } = useAuth();
  
  // Load conversation
  const { data: conversation, isLoading } = useQuery(
    ['conversation', conversationId],
    () => conversationApi.get(conversationId!),
    { enabled: !!conversationId }
  );
  
  return (
    <ChatLayout
      leftPane={<ConversationList />}
      rightPane={
        conversation?.bot_id ? (
          <BotInfoPanel
            conversationId={conversationId}
            currentBotId={conversation.bot_id}
            onBotChange={handleBotChange}
          />
        ) : undefined
      }
    >
      {!isAuthenticated && <AnonymousBanner chatCount={chatCount} />}
      
      {isLoading ? (
        <div className="flex-1 flex items-center justify-center">
          <CandlestickLoader />
        </div>
      ) : (
        <ChatArea conversationId={conversationId} />
      )}
      
      <ChatInput onSend={handleSend} />
    </ChatLayout>
  );
}

Phase 5: Integration & Edge Cases

5.1 Handle Rate Limiting

// In ChatArea or API client
async function handleError(error: any) {
  if (error.status === 429) {
    // System rate limit
    setGlobalError('Rate limited from the agent service. Please come back later.');
  } else if (error.status === 403) {
    // Anonymous limit reached
    setShowLoginPrompt(true);
  }
}

5.2 Handle Non-logged User Actions

When user tries to:

  • Create bot → Show "Login to create bots"
  • Run backtest → Show "Login to run backtests"
  • Run simulation → Show "Login required"
function handleToolAction(action: string) {
  if (!isAuthenticated) {
    if (['create_bot', 'run_backtest', 'run_simulation'].includes(action)) {
      showLoginPrompt(`Login to ${action.replace('_', ' ')}`);
      return;
    }
  }
  // Proceed with action
}

What to Check / Verify

Routing

Check How
/ loads landing Navigate to /, verify landing page
/home shows list Navigate to /home, verify conversation list
/chat/{id} loads Navigate to /chat/abc, verify chat loads
Back button works Browser back button navigates correctly

Components

Check How
Conversation list loads See list of conversations
New conversation works Click +New, verify created
Messages display Send message, see it in list
Bot panel shows Select bot, verify details display
Bot selector works Click Change Bot, select different bot

Anonymous User

Check How
Banner shows Visit as non-logged user
Warning at 40+ Simulate 40 messages
Block at 50 Try 51st message, should block
Login prompt shows Click login prompt, verify modal

Loading States

Check How
Candlestick loader During API calls
Button disabled While loading
Input disabled While AI responding

How to Test

Unit Tests

// tests/components/ConversationList.test.tsx
test('renders conversation list', () => {
  render(<ConversationList conversations={mockConversations} />);
  expect(screen.getByText('+ New Chat')).toBeInTheDocument();
});

// tests/components/AnonymousBanner.test.tsx
test('shows warning at 40 messages', () => {
  render(<AnonymousBanner chatCount={40} />);
  expect(screen.getByText(/40\/50/)).toBeInTheDocument();
});

Integration Tests

// tests/pages/ChatPage.test.tsx
test('loads conversation and messages', async () => {
  const mockConversation = {
    id: '1',
    messages: [{ id: '1', role: 'user', content: 'hi' }],
  };
  
  render(<ChatPage />);
  
  // Should show loading first
  expect(screen.getByTestId('loader')).toBeInTheDocument();
  
  // After load, show messages
  await waitFor(() => {
    expect(screen.getByText('hi')).toBeInTheDocument();
  });
});

How to Debug

If Conversations Not Loading

// Check API call
const { data, error } = useQuery(['conversations'], () => conversationApi.list());
console.log('Data:', data);
console.log('Error:', error);

If Messages Not Showing

// Check conversation ID
const { conversationId } = useParams();
console.log('Conversation ID:', conversationId);

// Check messages array
console.log('Messages:', messages);

If Bot Panel Not Updating

// Check bot ID
console.log('Current Bot ID:', currentBotId);

// Check API response
const bot = await botApi.get(botId);
console.log('Bot data:', bot);

Files to Create/Modify

Action File
UPDATE App.tsx (routes)
CREATE services/conversation.ts
CREATE services/bot.ts
CREATE contexts/AuthContext.tsx
CREATE components/ChatLayout.tsx
CREATE components/ConversationList.tsx
CREATE components/ChatArea.tsx
CREATE components/ChatInput.tsx
CREATE components/BotInfoPanel.tsx
CREATE components/AnonymousBanner.tsx
CREATE components/CandlestickLoader.tsx
CREATE pages/HomePage.tsx
CREATE pages/ChatPage.tsx

Dependencies

  • Backend issue: #59 (MUST complete first)
  • UI Design issue: #61 (can be done in parallel or after)

Priority: HIGH


Acceptance Criteria

Routing

  • / shows landing page
  • /home shows conversation list
  • /chat/{id} loads specific conversation
  • Back/forward navigation works

Left Pane

  • Shows list of conversations
  • "+ New Chat" creates new conversation
  • Clicking conversation loads it
  • Shows conversation title and date

Main Chat Area

  • Shows messages correctly (user right, AI left)
  • Input field works
  • Send button triggers API call
  • Loading state shows candlestick animation

Right Pane

  • Shows "No bot selected" when no bot
  • Shows bot name, chain, strategy when bot selected
  • "Change Bot" opens selector
  • Selecting bot updates panel

Non-logged Users

  • Shows "Your progress is not saved" banner
  • At 40+ messages shows warning
  • At 50 messages blocks and prompts login
  • Cannot create bots (shows login prompt)
  • Cannot run backtest (shows login prompt)
  • Cannot run simulation (shows login prompt)

Rate Limiting

  • System limit (429) shows appropriate message
  • Anonymous limit (403) shows login prompt

Polish

  • Responsive layout (works on different sizes)
  • Error handling (shows errors gracefully)
  • Loading states for all async operations

Notes for Developer

  1. Start with routing — Get pages working first
  2. Use backend from #59 — API endpoints must exist first
  3. Test with real data — Use backend API, not mocks
  4. Handle errors — Don't let errors crash the app
  5. Loading states — Show feedback during API calls
  6. Reuse components — Don't duplicate logic
# Detailed Implementation Guide for Frontend Chat UI --- ## ⚠️ Prerequisite **This issue assumes issue #59 (Backend Chat System) is COMPLETED.** The frontend will consume the API endpoints created in #59: - `GET /api/conversations` - List conversations - `POST /api/conversations` - Create conversation - `GET /api/conversations/{id}` - Get conversation with messages - `POST /api/conversations/{id}/chat` - Send message - `POST /api/conversations/{id}/set-bot` - Link bot to conversation --- ## Summary Redesign frontend to support conversation-based chat interface with left/right panes. --- ## Current State (Before) - Single bot per chat - No conversation list - No anonymous user support - No rate limiting UI ## Target State (After) - Conversations on left pane - Bot info on right pane - Anonymous user banner - Rate limit handling --- # Implementation Phases ## Phase 1: Routing & Layout ### 1.1 Route Structure Update `src/frontend/src/App.tsx` or router config: ```typescript // Routes const routes = [ { path: '/', element: <LandingPage /> }, { path: '/home', element: <ConversationListPage /> }, { path: '/chat', element: <ChatPage /> }, // No conversation selected { path: '/chat/:conversationId', element: <ChatPage /> }, ]; ``` ### 1.2 Main Layout Component Create `src/frontend/src/components/ChatLayout.tsx`: ```typescript interface ChatLayoutProps { leftPane?: React.ReactNode; rightPane?: React.ReactNode; children: React.ReactNode; } export function ChatLayout({ leftPane, rightPane, children }: ChatLayoutProps) { return ( <div className="flex h-screen"> {/* Left Pane - Conversation List */} {leftPane && ( <div className="w-64 border-r border-gray-700"> {leftPane} </div> )} {/* Main Chat Area */} <div className="flex-1 flex flex-col"> {children} </div> {/* Right Pane - Bot Info */} {rightPane && ( <div className="w-72 border-l border-gray-700 p-4"> {rightPane} </div> )} </div> ); } ``` **Check:** Routes work, layout renders with conditional panes --- ## Phase 2: API Services ### 2.1 Conversation API Client Create `src/frontend/src/services/conversation.ts`: ```typescript import { api } from './api'; export interface Conversation { id: string; user_id: string | null; bot_id: string | null; title: string; created_at: string; updated_at: string; } export interface Message { id: string; conversation_id: string; role: 'user' | 'assistant'; content: string; created_at: string; } export const conversationApi = { list: () => api.get<Conversation[]>('/conversations'), create: () => api.post<Conversation>('/conversations', {}), get: (id: string) => api.get<Conversation & { messages: Message[] }>( `/conversations/${id}` ), delete: (id: string) => api.delete(`/conversations/${id}`), chat: (conversationId: string, message: string) => api.post( `/conversations/${conversationId}/chat`, { message } ), setBot: (conversationId: string, botId: string) => api.post( `/conversations/${conversationId}/set-bot`, { bot_id: botId } ), }; ``` ### 2.2 Auth Context Create or update `src/frontend/src/contexts/AuthContext.tsx`: ```typescript interface AuthState { user: User | null; isAuthenticated: boolean; anonymousToken: string | null; } export function AuthProvider({ children }) { const [state, setState] = useState<AuthState>({ user: null, isAuthenticated: false, anonymousToken: getCookie('anonymous_token'), }); // ... login/logout methods return ( <AuthContext.Provider value={state}> {children} </AuthContext.Provider> ); } ``` **Check:** API calls work, auth state persists --- ## Phase 3: Components ### 3.1 Conversation List (Left Pane) Create `src/frontend/src/components/ConversationList.tsx`: ```typescript import { useState, useEffect } from 'react'; import { conversationApi, Conversation } from '../services/conversation'; export function ConversationList() { const [conversations, setConversations] = useState<Conversation[]>([]); const [loading, setLoading] = useState(true); const { conversationId } = useParams(); useEffect(() => { loadConversations(); }, []); async function loadConversations() { try { const data = await conversationApi.list(); setConversations(data); } catch (error) { console.error('Failed to load conversations', error); } finally { setLoading(false); } } async function createConversation() { try { const newConv = await conversationApi.create(); setConversations([newConv, ...conversations]); navigate(`/chat/${newConv.id}`); } catch (error) { console.error('Failed to create conversation', error); } } if (loading) return <div className="p-4">Loading...</div>; return ( <div className="h-full flex flex-col"> <button onClick={createConversation} className="m-2 p-2 bg-green-600 rounded hover:bg-green-700" > + New Chat </button> <div className="flex-1 overflow-y-auto"> {conversations.map(conv => ( <div key={conv.id} className={`p-3 cursor-pointer hover:bg-gray-800 ${ conv.id === conversationId ? 'bg-gray-800' : '' }`} onClick={() => navigate(`/chat/${conv.id}`)} > <div className="font-medium truncate">{conv.title}</div> <div className="text-xs text-gray-400"> {formatDate(conv.updated_at)} </div> </div> ))} </div> </div> ); } ``` --- ### 3.2 Chat Area (Main) Create or update `src/frontend/src/components/ChatArea.tsx`: ```typescript export function ChatArea({ conversationId }: { conversationId?: string }) { const [messages, setMessages] = useState<Message[]>([]); const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const messagesEndRef = useRef<HTMLDivElement>(null); useEffect(() => { if (conversationId) { loadConversation(); } }, [conversationId]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); async function sendMessage() { if (!input.trim() || loading) return; const userMessage = input; setInput(''); setLoading(true); try { const response = await conversationApi.chat(conversationId, userMessage); setMessages(prev => [...prev, response.messages]); } catch (error: any) { if (error.status === 429) { alert('Rate limited from the agent service. Please come back later.'); } else if (error.status === 403) { alert("You've reached the limit. Please create an account to continue."); } } finally { setLoading(false); } } return ( <div className="flex-1 overflow-y-auto p-4"> {messages.map(msg => ( <div key={msg.id} className={`mb-4 ${msg.role === 'user' ? 'text-right' : 'text-left'}`} > <div className={`inline-block max-w-[70%] p-3 rounded ${ msg.role === 'user' ? 'bg-gray-700' : 'bg-blue-900' }`}> {msg.content} </div> </div> ))} <div ref={messagesEndRef} /> </div> ); } ``` --- ### 3.3 Bot Info Panel (Right Pane) Create `src/frontend/src/components/BotInfoPanel.tsx`: ```typescript import { useState, useEffect } from 'react'; import { botApi, Bot } from '../services/bot'; interface BotInfoPanelProps { conversationId?: string; currentBotId?: string; onBotChange: (botId: string) => void; } export function BotInfoPanel({ conversationId, currentBotId, onBotChange }: BotInfoPanelProps) { const [bot, setBot] = useState<Bot | null>(null); const [showSelector, setShowSelector] = useState(false); const [userBots, setUserBots] = useState<Bot[]>([]); useEffect(() => { if (currentBotId) { loadBot(currentBotId); } }, [currentBotId]); async function loadBot(botId: string) { try { const data = await botApi.get(botId); setBot(data); } catch (error) { console.error('Failed to load bot', error); } } async function loadUserBots() { try { const data = await botApi.list(); setUserBots(data); setShowSelector(true); } catch (error) { console.error('Failed to load bots', error); } } if (!bot) { return ( <div className="text-gray-400"> <p>No bot selected</p> <button onClick={loadUserBots} className="mt-2 text-green-500 hover:underline" > Select Bot </button> </div> ); } return ( <div> <h3 className="font-bold text-lg mb-2">{bot.name}</h3> <div className="space-y-2 text-sm"> <div> <span className="text-gray-400">Chain:</span> {bot.chain} </div> <div> <span className="text-gray-400">Status:</span>{' '} <span className={bot.status === 'active' ? 'text-green-500' : 'text-gray-500'}> {bot.status} </span> </div> <div className="border-t border-gray-700 pt-2 mt-2"> <span className="text-gray-400">Strategy:</span> <p className="mt-1">{bot.strategy}</p> </div> </div> <button onClick={loadUserBots} className="mt-4 w-full py-2 border border-gray-600 rounded hover:bg-gray-800" > Change Bot </button> {/* Bot Selector Modal */} {showSelector && ( <div className="fixed inset-0 bg-black/50 flex items-center justify-center"> <div className="bg-gray-900 p-4 rounded-lg w-80"> <h4 className="font-bold mb-2">Select Bot</h4> {userBots.map(b => ( <div key={b.id} className="p-2 hover:bg-gray-800 cursor-pointer" onClick={() => { onBotChange(b.id); setShowSelector(false); }} > {b.name} </div> ))} <button onClick={() => setShowSelector(false)} className="mt-2 w-full py-2 border border-gray-600 rounded" > Cancel </button> </div> </div> )} </div> ); } ``` --- ### 3.4 Non-logged User Banner Create `src/frontend/src/components/AnonymousBanner.tsx`: ```typescript interface AnonymousBannerProps { chatCount?: number; } export function AnonymousBanner({ chatCount = 0 }: AnonymousBannerProps) { const showWarning = chatCount >= 40; return ( <div className={`p-2 text-center text-sm ${ showWarning ? 'bg-yellow-900 text-yellow-200' : 'bg-gray-800 text-gray-400' }`}> {showWarning ? ( <span> ⚠️ You've used {chatCount}/50 messages.{' '} <a href="/login" className="underline text-yellow-200">Login to continue</a> </span> ) : ( <span>Your progress is not saved. <a href="/login" className="underline">Login to save</a></span> )} </div> ); } ``` --- ### 3.5 Loading Animation (Custom) Create `src/frontend/src/components/CandlestickLoader.tsx`: ```typescript export function CandlestickLoader() { return ( <div className="flex items-center gap-1 h-8"> {[...Array(4)].map((_, i) => ( <div key={i} className="w-2 bg-green-500 rounded-sm animate-pulse" style={{ height: `${Math.random() * 20 + 10}px`, animationDelay: `${i * 0.15}s`, }} /> ))} </div> ); } ``` --- ## Phase 4: Page Components ### 4.1 Home Page (Conversation List) Create `src/frontend/src/pages/HomePage.tsx`: ```typescript export function HomePage() { return ( <ChatLayout leftPane={<ConversationList />}> <div className="flex-1 flex items-center justify-center text-gray-400"> Select a conversation or start a new one </div> </ChatLayout> ); } ``` ### 4.2 Chat Page Create `src/frontend/src/pages/ChatPage.tsx`: ```typescript export function ChatPage() { const { conversationId } = useParams(); const { isAuthenticated } = useAuth(); // Load conversation const { data: conversation, isLoading } = useQuery( ['conversation', conversationId], () => conversationApi.get(conversationId!), { enabled: !!conversationId } ); return ( <ChatLayout leftPane={<ConversationList />} rightPane={ conversation?.bot_id ? ( <BotInfoPanel conversationId={conversationId} currentBotId={conversation.bot_id} onBotChange={handleBotChange} /> ) : undefined } > {!isAuthenticated && <AnonymousBanner chatCount={chatCount} />} {isLoading ? ( <div className="flex-1 flex items-center justify-center"> <CandlestickLoader /> </div> ) : ( <ChatArea conversationId={conversationId} /> )} <ChatInput onSend={handleSend} /> </ChatLayout> ); } ``` --- ## Phase 5: Integration & Edge Cases ### 5.1 Handle Rate Limiting ```typescript // In ChatArea or API client async function handleError(error: any) { if (error.status === 429) { // System rate limit setGlobalError('Rate limited from the agent service. Please come back later.'); } else if (error.status === 403) { // Anonymous limit reached setShowLoginPrompt(true); } } ``` ### 5.2 Handle Non-logged User Actions When user tries to: - Create bot → Show "Login to create bots" - Run backtest → Show "Login to run backtests" - Run simulation → Show "Login required" ```typescript function handleToolAction(action: string) { if (!isAuthenticated) { if (['create_bot', 'run_backtest', 'run_simulation'].includes(action)) { showLoginPrompt(`Login to ${action.replace('_', ' ')}`); return; } } // Proceed with action } ``` --- ## What to Check / Verify ### Routing | Check | How | |-------|-----| | `/` loads landing | Navigate to /, verify landing page | | `/home` shows list | Navigate to /home, verify conversation list | | `/chat/{id}` loads | Navigate to /chat/abc, verify chat loads | | Back button works | Browser back button navigates correctly | ### Components | Check | How | |-------|-----| | Conversation list loads | See list of conversations | | New conversation works | Click +New, verify created | | Messages display | Send message, see it in list | | Bot panel shows | Select bot, verify details display | | Bot selector works | Click Change Bot, select different bot | ### Anonymous User | Check | How | |-------|-----| | Banner shows | Visit as non-logged user | | Warning at 40+ | Simulate 40 messages | | Block at 50 | Try 51st message, should block | | Login prompt shows | Click login prompt, verify modal | ### Loading States | Check | How | |-------|-----| | Candlestick loader | During API calls | | Button disabled | While loading | | Input disabled | While AI responding | --- ## How to Test ### Unit Tests ```typescript // tests/components/ConversationList.test.tsx test('renders conversation list', () => { render(<ConversationList conversations={mockConversations} />); expect(screen.getByText('+ New Chat')).toBeInTheDocument(); }); // tests/components/AnonymousBanner.test.tsx test('shows warning at 40 messages', () => { render(<AnonymousBanner chatCount={40} />); expect(screen.getByText(/40\/50/)).toBeInTheDocument(); }); ``` ### Integration Tests ```typescript // tests/pages/ChatPage.test.tsx test('loads conversation and messages', async () => { const mockConversation = { id: '1', messages: [{ id: '1', role: 'user', content: 'hi' }], }; render(<ChatPage />); // Should show loading first expect(screen.getByTestId('loader')).toBeInTheDocument(); // After load, show messages await waitFor(() => { expect(screen.getByText('hi')).toBeInTheDocument(); }); }); ``` --- ## How to Debug ### If Conversations Not Loading ```typescript // Check API call const { data, error } = useQuery(['conversations'], () => conversationApi.list()); console.log('Data:', data); console.log('Error:', error); ``` ### If Messages Not Showing ```typescript // Check conversation ID const { conversationId } = useParams(); console.log('Conversation ID:', conversationId); // Check messages array console.log('Messages:', messages); ``` ### If Bot Panel Not Updating ```typescript // Check bot ID console.log('Current Bot ID:', currentBotId); // Check API response const bot = await botApi.get(botId); console.log('Bot data:', bot); ``` --- ## Files to Create/Modify | Action | File | |--------|------| | UPDATE | `App.tsx` (routes) | | CREATE | `services/conversation.ts` | | CREATE | `services/bot.ts` | | CREATE | `contexts/AuthContext.tsx` | | CREATE | `components/ChatLayout.tsx` | | CREATE | `components/ConversationList.tsx` | | CREATE | `components/ChatArea.tsx` | | CREATE | `components/ChatInput.tsx` | | CREATE | `components/BotInfoPanel.tsx` | | CREATE | `components/AnonymousBanner.tsx` | | CREATE | `components/CandlestickLoader.tsx` | | CREATE | `pages/HomePage.tsx` | | CREATE | `pages/ChatPage.tsx` | --- ## Dependencies - Backend issue: #59 (MUST complete first) - UI Design issue: #61 (can be done in parallel or after) --- ## Priority: HIGH --- ## Acceptance Criteria ### Routing - [ ] `/` shows landing page - [ ] `/home` shows conversation list - [ ] `/chat/{id}` loads specific conversation - [ ] Back/forward navigation works ### Left Pane - [ ] Shows list of conversations - [ ] "+ New Chat" creates new conversation - [ ] Clicking conversation loads it - [ ] Shows conversation title and date ### Main Chat Area - [ ] Shows messages correctly (user right, AI left) - [ ] Input field works - [ ] Send button triggers API call - [ ] Loading state shows candlestick animation ### Right Pane - [ ] Shows "No bot selected" when no bot - [ ] Shows bot name, chain, strategy when bot selected - [ ] "Change Bot" opens selector - [ ] Selecting bot updates panel ### Non-logged Users - [ ] Shows "Your progress is not saved" banner - [ ] At 40+ messages shows warning - [ ] At 50 messages blocks and prompts login - [ ] Cannot create bots (shows login prompt) - [ ] Cannot run backtest (shows login prompt) - [ ] Cannot run simulation (shows login prompt) ### Rate Limiting - [ ] System limit (429) shows appropriate message - [ ] Anonymous limit (403) shows login prompt ### Polish - [ ] Responsive layout (works on different sizes) - [ ] Error handling (shows errors gracefully) - [ ] Loading states for all async operations --- ## Notes for Developer 1. **Start with routing** — Get pages working first 2. **Use backend from #59** — API endpoints must exist first 3. **Test with real data** — Use backend API, not mocks 4. **Handle errors** — Don't let errors crash the app 5. **Loading states** — Show feedback during API calls 6. **Reuse components** — Don't duplicate logic
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shoko/randebu#60