import React, { createContext, useContext, useReducer, useEffect, useRef } from 'react';
import type { ReactNode } from 'react';
import type { AppState, ChatMessage, Step, DebugInfo, ConnectionStatus, WebSocketMessage } from '../types';
import { generateId } from '../utils';
interface AppContextType {
state: AppState;
dispatch: React.Dispatch<AppAction>;
sendMessage: (message: string) => Promise<void>;
}
type AppAction =
| { type: 'ADD_MESSAGE'; payload: ChatMessage }
| { type: 'SET_RESOURCES'; payload: any[] }
| { type: 'UPDATE_MESSAGE'; payload: { id: string; content: string } }
| { type: 'UPDATE_MESSAGE_FLAGS'; payload: { id: string; flags: Partial<ChatMessage> } }
| { type: 'REMOVE_MESSAGE'; payload: string }
| { type: 'CLEAR_MESSAGES' }
| { type: 'SET_PROCESSING'; payload: boolean }
| { type: 'SET_CONNECTION_STATUS'; payload: { status: ConnectionStatus; text: string } }
| { type: 'ADD_STEP'; payload: Step }
| { type: 'UPDATE_STEP'; payload: { id: string; status: Step['status']; result?: any; error?: string } }
| { type: 'CLEAR_STEPS' }
| { type: 'SET_STEPS'; payload: Step[] }
| { type: 'UPDATE_STEP_STATS'; payload: { successful: number; total: number } }
| { type: 'SET_DEBUG_INFO'; payload: DebugInfo | null }
| { type: 'TOGGLE_STEPS_VISIBILITY' }
| { type: 'SET_STEPS_VISIBILITY'; payload: boolean }
| { type: 'LOG_WS_MESSAGE'; payload: { type: string; data?: any } };
const initialState: AppState = {
messages: [],
resources: [],
isProcessing: false,
connectionStatus: 'loading',
connectionText: 'Connecting...',
steps: [],
stepStats: { successful: 0, total: 0 },
debugInfo: null,
stepsVisible: false,
wsMessages: {
total: 0,
chatChunks: 0,
lastMessages: []
},
};
function appReducer(state: AppState, action: AppAction): AppState {
switch (action.type) {
case 'ADD_MESSAGE':
return {
...state,
messages: [...state.messages, action.payload],
};
case 'UPDATE_MESSAGE':
return {
...state,
messages: state.messages.map(msg =>
msg.id === action.payload.id
? { ...msg, content: action.payload.content }
: msg
),
};
case 'SET_RESOURCES':
return {
...state,
resources: action.payload,
};
case 'UPDATE_MESSAGE_FLAGS':
return {
...state,
messages: state.messages.map(msg =>
msg.id === action.payload.id
? { ...msg, ...action.payload.flags }
: msg
),
};
case 'REMOVE_MESSAGE':
return {
...state,
messages: state.messages.filter(msg => msg.id !== action.payload),
};
case 'CLEAR_MESSAGES':
return {
...state,
messages: [],
};
case 'SET_PROCESSING':
return {
...state,
isProcessing: action.payload,
};
case 'SET_CONNECTION_STATUS':
return {
...state,
connectionStatus: action.payload.status,
connectionText: action.payload.text,
};
case 'ADD_STEP':
return {
...state,
steps: [...state.steps, action.payload],
};
case 'UPDATE_STEP':
return {
...state,
steps: state.steps.map(step =>
step.id === action.payload.id
? {
...step,
status: action.payload.status,
result: action.payload.result,
error: action.payload.error
}
: step
),
};
case 'CLEAR_STEPS':
return {
...state,
steps: [],
stepStats: { successful: 0, total: 0 },
};
case 'SET_STEPS':
return {
...state,
steps: action.payload,
};
case 'UPDATE_STEP_STATS':
return {
...state,
stepStats: action.payload,
};
case 'SET_DEBUG_INFO':
return {
...state,
debugInfo: action.payload,
};
case 'TOGGLE_STEPS_VISIBILITY':
return {
...state,
stepsVisible: !state.stepsVisible,
};
case 'SET_STEPS_VISIBILITY':
return {
...state,
stepsVisible: action.payload,
};
case 'LOG_WS_MESSAGE':
const newMessage = {
type: action.payload.type,
timestamp: Date.now(),
data: action.payload.data
};
return {
...state,
wsMessages: {
total: state.wsMessages.total + 1,
chatChunks: action.payload.type === 'chat_chunk'
? state.wsMessages.chatChunks + 1
: state.wsMessages.chatChunks,
lastMessages: [
newMessage,
...state.wsMessages.lastMessages.slice(0, 9) // Keep last 10 messages
]
}
};
default:
return state;
}
}
const AppContext = createContext<AppContextType | undefined>(undefined);
export function AppProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(appReducer, initialState);
const stateRef = useRef(state);
// Keep stateRef current
useEffect(() => {
stateRef.current = state;
}, [state]);
const sendMessage = async (message: string): Promise<void> => {
// Add user message
const userMessage: ChatMessage = {
id: generateId(),
content: message,
type: 'user',
timestamp: new Date(),
};
dispatch({ type: 'ADD_MESSAGE', payload: userMessage });
// Clear old steps and reset state for new request
dispatch({ type: 'CLEAR_STEPS' });
dispatch({ type: 'UPDATE_STEP_STATS', payload: { successful: 0, total: 0 } });
// Set loading state
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'loading', text: 'Awaiting API Response...' } });
dispatch({ type: 'SET_PROCESSING', payload: true });
// Create an initial bot message with live progress indicator
const initialBotMessage: ChatMessage = {
id: generateId(),
content: 'Processing your request...',
type: 'bot',
timestamp: new Date(),
showLiveProgress: true,
};
dispatch({ type: 'ADD_MESSAGE', payload: initialBotMessage });
try {
// Send request to backend - response will come via WebSocket
const response = await fetch('http://localhost:3500/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message }),
});
const result = await response.json();
// Check if this was a tool-based workflow (has content in response)
if (result.content) {
// Handle tool workflow response (traditional way)
dispatch({
type: 'UPDATE_MESSAGE_FLAGS',
payload: {
id: initialBotMessage.id,
flags: {
content: result.content,
showLiveProgress: false
}
}
});
dispatch({ type: 'SET_PROCESSING', payload: false });
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'ready', text: 'Ready' } });
}
// If result.success is true, it means streaming response will come via WebSocket
// The WebSocket handlers will update the UI
} catch (error) {
const errorMessage: ChatMessage = {
id: generateId(),
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
type: 'bot',
timestamp: new Date(),
};
dispatch({ type: 'ADD_MESSAGE', payload: errorMessage });
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'error', text: 'API Error' } });
dispatch({ type: 'SET_PROCESSING', payload: false });
}
};
// WebSocket connection and management
useEffect(() => {
let ws: WebSocket | null = null;
let reconnectInterval: number | null = null;
const connectWebSocket = () => {
if (reconnectInterval) {
clearInterval(reconnectInterval);
reconnectInterval = null;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
// Connect to the backend server on port 3500, not the Vite dev server
const wsUrl = `${protocol}//localhost:3500`;
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('π WebSocket connected');
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'ready', text: 'Connected' } });
};
ws.onclose = () => {
console.log('π WebSocket disconnected');
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'error', text: 'Disconnected' } });
attemptReconnect();
};
ws.onerror = (error) => {
console.error('π WebSocket error:', error);
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'error', text: 'Connection Error' } });
};
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
console.log('π‘ WebSocket message received:', message.type, message.data);
handleWebSocketMessage(message);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
} catch (error) {
console.error('Failed to create WebSocket connection:', error);
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'error', text: 'Connection Failed' } });
attemptReconnect();
}
};
const attemptReconnect = () => {
if (!reconnectInterval) {
reconnectInterval = setInterval(() => {
console.log('π Attempting to reconnect...');
connectWebSocket();
}, 5000);
}
};
const handleWebSocketMessage = (message: WebSocketMessage) => {
const { type, data, resources} = message;
console.log('π Processing WebSocket message:', type, data);
// Log all WebSocket messages for debugging
dispatch({ type: 'LOG_WS_MESSAGE', payload: { type, data } });
switch (type) {
case 'connection':
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'ready', text: 'Connected' } });
if (resources) {
dispatch({ type: 'SET_RESOURCES', payload: resources.resources || [] });
}
break;
case 'workflow_start':
dispatch({ type: 'SET_PROCESSING', payload: true });
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'loading', text: 'Processing...' } });
dispatch({ type: 'CLEAR_STEPS' });
console.log('π Workflow started via WebSocket');
// Find the last bot message and enable live progress for it
const workflowMessages = stateRef.current.messages;
const workflowBotMessage = [...workflowMessages].reverse().find(msg => msg.type === 'bot');
if (workflowBotMessage) {
dispatch({
type: 'UPDATE_MESSAGE_FLAGS',
payload: {
id: workflowBotMessage.id,
flags: {
showLiveProgress: true,
content: 'Starting workflow execution...'
}
}
});
}
break;
case 'chat_decision':
console.log(`π€ Chat Decision: ${data.needsTools ? 'Tools needed' : 'Simple chat'} - ${data.reasoning}`);
break;
case 'chat_stream_start':
console.log('π‘ Chat streaming started via WebSocket');
// Find the last bot message with showLiveProgress and reset its content
const messages = stateRef.current.messages;
const lastBotMessage = [...messages].reverse().find(msg => msg.type === 'bot' && msg.showLiveProgress);
if (lastBotMessage) {
dispatch({
type: 'UPDATE_MESSAGE_FLAGS',
payload: {
id: lastBotMessage.id,
flags: { content: '' } // Reset content for streaming
}
});
}
break;
case 'chat_chunk':
console.log('π¦ Chat chunk received:', data.content);
// Find the last bot message with showLiveProgress and append content
const currentMessages = stateRef.current.messages;
const liveBotMessage = [...currentMessages].reverse().find(msg => msg.type === 'bot' && msg.showLiveProgress);
if (liveBotMessage) {
const currentContent = liveBotMessage.content || '';
dispatch({
type: 'UPDATE_MESSAGE_FLAGS',
payload: {
id: liveBotMessage.id,
flags: { content: currentContent + data.content }
}
});
}
break;
case 'chat_complete':
console.log('β
Chat streaming completed:', data.content);
// Find the last bot message and finalize it
const allMessages = stateRef.current.messages;
const completeBotMessage = [...allMessages].reverse().find(msg => msg.type === 'bot' && msg.showLiveProgress);
if (completeBotMessage) {
dispatch({
type: 'UPDATE_MESSAGE_FLAGS',
payload: {
id: completeBotMessage.id,
flags: {
content: data.content,
showLiveProgress: false
}
}
});
}
dispatch({ type: 'SET_PROCESSING', payload: false });
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'ready', text: 'Ready' } });
break;
case 'plan_creation_start':
case 'plan_creation_complete':
case 'execution_start':
case 'cycle_start':
case 'step_start':
case 'step_complete':
case 'step_error':
case 'cycle_complete':
console.log(`οΏ½ WebSocket event: ${type}`, data);
// Handle step updates
if (type === 'step_start') {
const step: Step = {
id: `${data.cycleIndex}-${data.stepIndex}`,
cycleIndex: data.cycleIndex,
stepIndex: data.stepIndex,
cycle: data.step.cycle,
description: data.step.description,
type: data.step.type,
target: data.step.target,
status: 'running',
};
dispatch({ type: 'ADD_STEP', payload: step });
}
if (type === 'step_complete' || type === 'step_error') {
const stepId = `${data.cycleIndex}-${data.stepIndex}`;
dispatch({
type: 'UPDATE_STEP',
payload: {
id: stepId,
status: type === 'step_complete' ? 'success' : 'error',
result: data.result,
error: data.error,
}
});
// Update step stats in real-time
const currentSteps = stateRef.current.steps;
const updatedSteps = currentSteps.map(step =>
step.id === stepId
? { ...step, status: type === 'step_complete' ? 'success' : 'error' }
: step
);
const successful = updatedSteps.filter(s => s.status === 'success').length;
const total = updatedSteps.length;
dispatch({ type: 'UPDATE_STEP_STATS', payload: { successful, total } });
}
break;
case 'debug_info_updated':
if (data.debugInfo) {
dispatch({ type: 'SET_DEBUG_INFO', payload: data.debugInfo });
}
break;
case 'workflow_complete':
dispatch({ type: 'SET_PROCESSING', payload: false });
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'ready', text: 'Ready' } });
// Turn off live progress for all messages and add show steps button
stateRef.current.messages
.filter(msg => msg.showLiveProgress)
.forEach(msg => {
dispatch({
type: 'UPDATE_MESSAGE_FLAGS',
payload: {
id: msg.id,
flags: {
showLiveProgress: false,
showStepsButton: true // Add show steps button when workflow completes
}
}
});
});
// Handle execution summary and update steps
const executionData = data.summary || data.executionSummary;
if (executionData) {
const steps = executionData.results?.map((step: any, index: number) => ({
id: `${index}-${step.cycle || index}`,
cycleIndex: index,
stepIndex: index,
cycle: step.cycle || `Step ${index + 1}`,
description: step.step || 'Execution step',
type: 'execution',
target: step.target || 'target',
status: step.success ? 'success' : 'error',
result: step.result || step.text || step.attachments || null,
error: step.success ? null : 'Execution failed' + (step.error ? `: ${step.error}` : ''),
})) || [];
dispatch({ type: 'SET_STEPS', payload: steps });
const { successfulSteps, totalSteps } = executionData;
dispatch({ type: 'UPDATE_STEP_STATS', payload: { successful: Number(successfulSteps), total: Number(totalSteps) } });
}
// Note: Completion message is already handled in sendMessage function
// No need to add another completion message here to avoid duplicates
break;
case 'workflow_error':
dispatch({ type: 'SET_PROCESSING', payload: false });
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'error', text: 'Error' } });
const errorMessage: ChatMessage = {
id: generateId(),
content: `β Workflow error: ${data.error}`,
type: 'system',
timestamp: new Date(),
};
dispatch({ type: 'ADD_MESSAGE', payload: errorMessage });
break;
default:
break;
}
};
connectWebSocket();
dispatch({ type: 'SET_CONNECTION_STATUS', payload: { status: 'loading', text: 'Connecting...' } });
return () => {
if (ws) {
ws.close();
}
if (reconnectInterval) {
clearInterval(reconnectInterval);
}
};
}, []);
return (
<AppContext.Provider value={{ state, dispatch, sendMessage }}>
{children}
</AppContext.Provider>
);
}
export function useApp(): AppContextType {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
}