| import { useState, useCallback } from 'react'; |
| import { sendChatMessageStream } from '../services/groq.service.ts'; |
| import { generateEnhancedArgument } from '../services/argument.service.ts'; |
| import { getUserId } from '../utils/index.ts'; |
| import type { ChatMessage, ChatState } from '../types/chat.types.ts'; |
|
|
| const generateId = () => Math.random().toString(36).substring(2, 15); |
|
|
| const initialState: ChatState = { |
| messages: [], |
| isLoading: false, |
| error: null, |
| }; |
|
|
| export const useChat = () => { |
| const [state, setState] = useState<ChatState>(initialState); |
|
|
| const addMessage = useCallback((message: Omit<ChatMessage, 'id' | 'timestamp'>) => { |
| setState(prev => ({ |
| ...prev, |
| messages: [...prev.messages, { |
| ...message, |
| id: generateId(), |
| timestamp: new Date(), |
| }], |
| })); |
| }, []); |
|
|
|
|
| const extractTopicAndPosition = useCallback(async (userInput: string): Promise<{ topic: string; position: string }> => { |
| |
| const fallback = { |
| topic: userInput.trim().slice(0, 100) || 'General Discussion', |
| position: 'positive' |
| }; |
|
|
| if (!userInput || !userInput.trim()) { |
| return fallback; |
| } |
|
|
| const extractionPrompt = `Extract the debate topic and position from the following text. Return ONLY a JSON object with "topic" and "position" keys. The position should be either "positive" (in favor) or "negative" (against). |
| |
| Text: "${userInput}" |
| |
| Example output: |
| {"topic": "Assisted suicide should be a criminal offence", "position": "positive"}`; |
|
|
| try { |
| const response = await sendChatMessageStream([ |
| { id: generateId(), role: 'user', content: extractionPrompt, timestamp: new Date() } |
| ], () => { }); |
|
|
| if (!response) { |
| console.warn('Empty response from extraction API'); |
| return fallback; |
| } |
|
|
| |
| const jsonMatch = response.match(/\{[\s\S]*?\}/); |
| if (jsonMatch) { |
| try { |
| const parsed = JSON.parse(jsonMatch[0]); |
| return { |
| topic: parsed.topic || fallback.topic, |
| position: (parsed.position?.toLowerCase().includes('neg')) ? 'negative' : 'positive' |
| }; |
| } catch (parseError) { |
| console.error('Failed to parse extracted JSON:', parseError, 'Raw match:', jsonMatch[0]); |
| } |
| } else { |
| console.warn('No JSON structure found in extraction response:', response); |
| } |
|
|
| return fallback; |
| } catch (error) { |
| console.error('Error in extractTopicAndPosition process:', error); |
| return fallback; |
| } |
| }, []); |
|
|
| const sendAudioMessage = useCallback(async (audioBlob: Blob, selectedTool?: string | null) => { |
| console.log('sendAudioMessage called with:', { size: audioBlob.size, type: audioBlob.type }); |
|
|
| |
| const userMessage: Omit<ChatMessage, 'id' | 'timestamp'> = { |
| role: 'user', |
| content: 'Audio message sent', |
| audioUrl: URL.createObjectURL(audioBlob), |
| tool: selectedTool ?? null, |
| }; |
|
|
| addMessage(userMessage); |
|
|
| |
| setState(prev => ({ ...prev, isLoading: true, error: null })); |
|
|
| try { |
| |
| const userId = getUserId(); |
| if (!userId) { |
| throw new Error('User ID not found. Please register or log in.'); |
| } |
|
|
| |
| const webhookUrl = process.env.REACT_APP_N8N_WEBHOOK_URL; |
| if (!webhookUrl) { |
| throw new Error('REACT_APP_N8N_WEBHOOK_URL environment variable is not set'); |
| } |
|
|
| console.log('Sending audio to webhook:', { userId, webhookUrl }); |
|
|
| |
| const formData = new FormData(); |
| const file = new File([audioBlob], 'recording.webm', { type: audioBlob.type }); |
| formData.append('file', file); |
| formData.append('user_id', userId); |
|
|
| console.log('FormData created with', formData.get('file') ? 'file' : 'no file'); |
|
|
| |
| const response = await fetch(`${webhookUrl}/debate-assistant`, { |
| method: 'POST', |
| body: formData, |
| }); |
|
|
| console.log('Response received:', response.status); |
|
|
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
|
|
| const data = await response.json(); |
| console.log('Response data:', data); |
|
|
| |
| const ttsText = data?.tts_text || data?.final_argument || data?.result?.result?.[0]?.text || 'No response received'; |
|
|
| |
| const assistantMessage: Omit<ChatMessage, 'id' | 'timestamp'> = { |
| role: 'assistant', |
| content: ttsText, |
| }; |
| addMessage(assistantMessage); |
|
|
| setState(prev => ({ ...prev, isLoading: false })); |
| } catch (error) { |
| console.error('Error in sendAudioMessage:', error); |
| const errorMessage = error instanceof Error ? error.message : 'Failed to send audio message'; |
| setState(prev => ({ ...prev, isLoading: false, error: errorMessage })); |
| } |
| }, [addMessage]); |
|
|
| const sendMessage = useCallback(async (content: string, selectedTool?: string | null, stance?: 'positive' | 'negative') => { |
| |
| const userMessage: Omit<ChatMessage, 'id' | 'timestamp'> = { |
| role: 'user', |
| content, |
| tool: selectedTool ?? null, |
| }; |
|
|
| addMessage(userMessage); |
|
|
| |
| setState(prev => ({ ...prev, isLoading: true, error: null })); |
|
|
| try { |
| |
| const isGenerateArgumentTool = selectedTool && ( |
| selectedTool.toLowerCase().includes('generate') && |
| selectedTool.toLowerCase().includes('argument') |
| ); |
|
|
| if (isGenerateArgumentTool) { |
| console.log('Generate argument tool detected:', selectedTool); |
| |
| let topic: string; |
| let position: string; |
|
|
| try { |
| const extracted = await extractTopicAndPosition(content); |
| topic = extracted.topic; |
| |
| position = stance || extracted.position; |
| } catch (error) { |
| console.error('Unexpected error in topic extraction:', error); |
| topic = content.slice(0, 100) || 'General Discussion'; |
| position = stance || 'positive'; |
| } |
|
|
| |
| const argumentResponse = await generateEnhancedArgument({ topic, position }); |
|
|
| |
| const assistantMessage: Omit<ChatMessage, 'id' | 'timestamp'> = { |
| role: 'assistant', |
| content: argumentResponse.enhanced_argument, |
| tool: selectedTool ?? 'generate_argument', |
| }; |
|
|
| addMessage(assistantMessage); |
| } else { |
| |
| |
| const { position } = await extractTopicAndPosition(content); |
|
|
| |
| const userId = getUserId(); |
| if (!userId) { |
| throw new Error('User ID not found. Please register or log in.'); |
| } |
|
|
| |
| const webhookUrl = process.env.REACT_APP_N8N_WEBHOOK_URL; |
| if (!webhookUrl) { |
| throw new Error('REACT_APP_N8N_WEBHOOK_URL environment variable is not set'); |
| } |
|
|
| |
| const response = await fetch(`${webhookUrl}/debate-assistant`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| text: content, |
| position: position, |
| user_id: userId, |
| }), |
| }); |
|
|
| if (!response.ok) { |
| throw new Error(`HTTP error! status: ${response.status}`); |
| } |
|
|
| const data = await response.json(); |
|
|
| |
| const finalArgument = data?.final_argument || data?.result?.result?.[0]?.text || 'No response received'; |
| const assistantTool = data?.tool_name ?? selectedTool ?? 'debate-assistant'; |
|
|
| |
| let finalContent = finalArgument; |
| try { |
| const parsed = JSON.parse(finalArgument); |
| if (parsed.argument) { |
| finalContent = parsed.argument; |
| } |
| } catch { |
| |
| } |
|
|
| |
| const assistantMessage: Omit<ChatMessage, 'id' | 'timestamp'> = { |
| role: 'assistant', |
| content: finalContent, |
| tool: assistantTool, |
| }; |
| addMessage(assistantMessage); |
| } |
|
|
| setState(prev => ({ ...prev, isLoading: false })); |
| } catch (error) { |
| const errorMessage = error instanceof Error ? error.message : 'Failed to send message'; |
| setState(prev => ({ ...prev, isLoading: false, error: errorMessage })); |
| } |
| }, [addMessage, extractTopicAndPosition]); |
|
|
| const clearMessages = useCallback(() => { |
| setState(initialState); |
| }, []); |
|
|
| const retryLastMessage = useCallback(() => { |
| if (state.messages.length >= 2) { |
| const lastUserMessage = [...state.messages] |
| .reverse() |
| .find(msg => msg.role === 'user'); |
|
|
| if (lastUserMessage) { |
| |
| setState(prev => { |
| const lastMessage = prev.messages[prev.messages.length - 1]; |
| if (lastMessage?.role === 'assistant') { |
| return { |
| ...prev, |
| messages: prev.messages.slice(0, -1), |
| error: null, |
| }; |
| } |
| return { ...prev, error: null }; |
| }); |
|
|
| |
| sendMessage(lastUserMessage.content); |
| } |
| } |
| }, [state.messages, sendMessage]); |
|
|
| return { |
| messages: state.messages, |
| isLoading: state.isLoading, |
| error: state.error, |
| sendMessage, |
| sendAudioMessage, |
| clearMessages, |
| retryLastMessage, |
| }; |
| }; |
|
|