NLP-IBM-Debater / src /app /hooks /useChat.ts
S01Nour
feat/ui: chat adapted tools
a94de35
raw
history blame
10.3 kB
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 }> => {
// Default fallback values
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;
}
// Try to extract JSON from the response (handles markdown blocks and extra text)
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 });
// Add user audio message
const userMessage: Omit<ChatMessage, 'id' | 'timestamp'> = {
role: 'user',
content: 'Audio message sent',
audioUrl: URL.createObjectURL(audioBlob),
tool: selectedTool ?? null,
};
addMessage(userMessage);
// Set loading state
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get user ID
const userId = getUserId();
if (!userId) {
throw new Error('User ID not found. Please register or log in.');
}
// Get webhook URL
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 });
// Create form data for audio file upload
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');
// Call the debate-assistant endpoint with audio 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);
// Extract tts_text from response (based on your curl example)
const ttsText = data?.tts_text || data?.final_argument || data?.result?.result?.[0]?.text || 'No response received';
// Add assistant response with the TTS text
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') => {
// Add user message
const userMessage: Omit<ChatMessage, 'id' | 'timestamp'> = {
role: 'user',
content,
tool: selectedTool ?? null,
};
addMessage(userMessage);
// Set loading state
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Check if generate argument tool is selected (check for multiple possible names)
const isGenerateArgumentTool = selectedTool && (
selectedTool.toLowerCase().includes('generate') &&
selectedTool.toLowerCase().includes('argument')
);
if (isGenerateArgumentTool) {
console.log('Generate argument tool detected:', selectedTool);
// Extract topic and position from content
let topic: string;
let position: string;
try {
const extracted = await extractTopicAndPosition(content);
topic = extracted.topic;
// Use provided stance if available, otherwise use extracted position
position = stance || extracted.position;
} catch (error) {
console.error('Unexpected error in topic extraction:', error);
topic = content.slice(0, 100) || 'General Discussion';
position = stance || 'positive';
}
// Call the external API
const argumentResponse = await generateEnhancedArgument({ topic, position });
// Add assistant response with the enhanced argument
const assistantMessage: Omit<ChatMessage, 'id' | 'timestamp'> = {
role: 'assistant',
content: argumentResponse.enhanced_argument,
tool: selectedTool ?? 'generate_argument',
};
addMessage(assistantMessage);
} else {
// Regular chat - use n8n webhook /debate-assistant endpoint
// Extract topic and position from user input
const { position } = await extractTopicAndPosition(content);
// Get user ID
const userId = getUserId();
if (!userId) {
throw new Error('User ID not found. Please register or log in.');
}
// Get webhook URL
const webhookUrl = process.env.REACT_APP_N8N_WEBHOOK_URL;
if (!webhookUrl) {
throw new Error('REACT_APP_N8N_WEBHOOK_URL environment variable is not set');
}
// Call the debate-assistant endpoint
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();
// Extract final_argument from response
const finalArgument = data?.final_argument || data?.result?.result?.[0]?.text || 'No response received';
const assistantTool = data?.tool_name ?? selectedTool ?? 'debate-assistant';
// Parse if final_argument is a JSON string
let finalContent = finalArgument;
try {
const parsed = JSON.parse(finalArgument);
if (parsed.argument) {
finalContent = parsed.argument;
}
} catch {
// Not JSON, use as is
}
// Add assistant response with the final argument
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) {
// Remove the last failed assistant message if exists
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 };
});
// Resend the user message
sendMessage(lastUserMessage.content);
}
}
}, [state.messages, sendMessage]);
return {
messages: state.messages,
isLoading: state.isLoading,
error: state.error,
sendMessage,
sendAudioMessage,
clearMessages,
retryLastMessage,
};
};