Yassine Mhirsi commited on
Commit
ee3eb53
·
1 Parent(s): c01020f

argument generation

Browse files
src/app/components/chat/ChatInput.tsx CHANGED
@@ -4,7 +4,7 @@ import { useMCPTools } from '../../hooks/useMCPTools.ts';
4
  import type { MCPTool } from '../../types/index.ts';
5
 
6
  type ChatInputProps = {
7
- onSubmit?: (message: string) => void;
8
  placeholder?: string;
9
  };
10
 
@@ -31,7 +31,7 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
31
  e.preventDefault();
32
  if (input.trim()) {
33
  if (onSubmit) {
34
- onSubmit(input);
35
  }
36
  console.log('Submitted:', input);
37
  setInput('');
@@ -253,6 +253,17 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
253
  <textarea
254
  value={input}
255
  onChange={(e) => setInput(e.target.value)}
 
 
 
 
 
 
 
 
 
 
 
256
  placeholder={placeholder}
257
  className="w-full bg-transparent text-zinc-800 dark:text-gray-300 placeholder-zinc-400 dark:placeholder-gray-500 resize-none border-none outline-none text-base leading-relaxed min-h-[24px] max-h-32 transition-all duration-200"
258
  rows={1}
@@ -442,9 +453,18 @@ const ChatInput = ({ onSubmit, placeholder = 'Ask a follow-up...' }: ChatInputPr
442
 
443
  <button
444
  type="button"
445
- className="h-8 px-3 rounded-lg text-sm font-medium hover:opacity-90 transition-all duration-200 hover:scale-105 flex items-center justify-center bg-teal-900 dark:bg-[#032827] text-teal-300 dark:text-[#2DD4BF]"
 
 
 
 
 
 
 
 
 
446
  >
447
- LLM
448
  </button>
449
  </div>
450
 
 
4
  import type { MCPTool } from '../../types/index.ts';
5
 
6
  type ChatInputProps = {
7
+ onSubmit?: (message: string, selectedTool?: string | null) => void;
8
  placeholder?: string;
9
  };
10
 
 
31
  e.preventDefault();
32
  if (input.trim()) {
33
  if (onSubmit) {
34
+ onSubmit(input, selectedTool);
35
  }
36
  console.log('Submitted:', input);
37
  setInput('');
 
253
  <textarea
254
  value={input}
255
  onChange={(e) => setInput(e.target.value)}
256
+ onKeyDown={(e) => {
257
+ if (e.key === 'Enter' && !e.shiftKey) {
258
+ e.preventDefault();
259
+ if (input.trim()) {
260
+ if (onSubmit) {
261
+ onSubmit(input, selectedTool);
262
+ }
263
+ setInput('');
264
+ }
265
+ }
266
+ }}
267
  placeholder={placeholder}
268
  className="w-full bg-transparent text-zinc-800 dark:text-gray-300 placeholder-zinc-400 dark:placeholder-gray-500 resize-none border-none outline-none text-base leading-relaxed min-h-[24px] max-h-32 transition-all duration-200"
269
  rows={1}
 
453
 
454
  <button
455
  type="button"
456
+ onClick={() => {
457
+ if (input.trim()) {
458
+ if (onSubmit) {
459
+ onSubmit(input, selectedTool);
460
+ }
461
+ setInput('');
462
+ }
463
+ }}
464
+ disabled={!input.trim()}
465
+ className="h-8 px-3 rounded-lg text-sm font-medium hover:opacity-90 transition-all duration-200 hover:scale-105 flex items-center justify-center bg-teal-900 dark:bg-[#032827] text-teal-300 dark:text-[#2DD4BF] disabled:opacity-50 disabled:cursor-not-allowed"
466
  >
467
+ Send
468
  </button>
469
  </div>
470
 
src/app/components/chat/MessageList.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef } from 'react';
2
+ import type { ChatMessage } from '../../types/chat.types.ts';
3
+ import { Loader2, AlertCircle, RotateCcw } from 'lucide-react';
4
+
5
+ type MessageListProps = {
6
+ messages: ChatMessage[];
7
+ isLoading?: boolean;
8
+ error?: string | null;
9
+ onRetry?: () => void;
10
+ };
11
+
12
+ const MessageList = ({ messages, isLoading = false, error = null, onRetry }: MessageListProps) => {
13
+ const messagesEndRef = useRef<HTMLDivElement>(null);
14
+
15
+ const scrollToBottom = () => {
16
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
17
+ };
18
+
19
+ useEffect(() => {
20
+ scrollToBottom();
21
+ }, [messages, isLoading]);
22
+
23
+ const formatTime = (date: Date) => {
24
+ return date.toLocaleTimeString('en-US', {
25
+ hour: '2-digit',
26
+ minute: '2-digit',
27
+ });
28
+ };
29
+
30
+ return (
31
+ <div className="flex-1 overflow-y-auto px-4 py-6 space-y-4">
32
+ {messages.length === 0 && !isLoading && (
33
+ <div className="flex flex-col items-center justify-center h-full text-center">
34
+ <div className="mb-4">
35
+ <div className="w-16 h-16 bg-gradient-to-br from-teal-400 to-blue-500 rounded-full flex items-center justify-center">
36
+ <svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
38
+ </svg>
39
+ </div>
40
+ </div>
41
+ <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
42
+ Start a conversation
43
+ </h3>
44
+ <p className="text-sm text-gray-500 dark:text-gray-400 max-w-md">
45
+ Ask me anything! I'm powered by Meta's Llama 4 Scout model and can help with a wide range of topics.
46
+ </p>
47
+ </div>
48
+ )}
49
+
50
+ {messages.map((message) => (
51
+ <div
52
+ key={message.id}
53
+ className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
54
+ >
55
+ <div
56
+ className={`max-w-3xl px-4 py-3 rounded-2xl ${
57
+ message.role === 'user'
58
+ ? 'bg-teal-500 text-white ml-12'
59
+ : 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 mr-12'
60
+ }`}
61
+ >
62
+ <div className="whitespace-pre-wrap break-words">{message.content}</div>
63
+ <div
64
+ className={`text-xs mt-1 ${
65
+ message.role === 'user'
66
+ ? 'text-teal-100'
67
+ : 'text-gray-500 dark:text-gray-400'
68
+ }`}
69
+ >
70
+ {formatTime(message.timestamp)}
71
+ </div>
72
+ </div>
73
+ </div>
74
+ ))}
75
+
76
+ {isLoading && (
77
+ <div className="flex justify-start">
78
+ <div className="bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100 mr-12 max-w-3xl px-4 py-3 rounded-2xl">
79
+ <div className="flex items-center space-x-2">
80
+ <Loader2 className="w-4 h-4 animate-spin" />
81
+ <span className="text-sm">Thinking...</span>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ )}
86
+
87
+ {error && (
88
+ <div className="flex justify-start">
89
+ <div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-100 mr-12 max-w-3xl px-4 py-3 rounded-2xl border border-red-200 dark:border-red-800">
90
+ <div className="flex items-start space-x-2">
91
+ <AlertCircle className="w-5 h-5 text-red-500 dark:text-red-400 flex-shrink-0 mt-0.5" />
92
+ <div className="flex-1">
93
+ <p className="text-sm font-medium">Error</p>
94
+ <p className="text-sm opacity-90">{error}</p>
95
+ {onRetry && (
96
+ <button
97
+ onClick={onRetry}
98
+ className="mt-2 flex items-center space-x-1 text-sm text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 transition-colors"
99
+ >
100
+ <RotateCcw className="w-3 h-3" />
101
+ <span>Retry</span>
102
+ </button>
103
+ )}
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ )}
109
+
110
+ <div ref={messagesEndRef} />
111
+ </div>
112
+ );
113
+ };
114
+
115
+ export default MessageList;
src/app/hooks/index.ts CHANGED
@@ -6,4 +6,5 @@ export { default as useApi } from './useApi.ts';
6
  export { useApi as useApiHook } from './useApi.ts';
7
  export { useTheme } from './useTheme.ts';
8
  export { useAuth } from './useAuth.ts';
 
9
 
 
6
  export { useApi as useApiHook } from './useApi.ts';
7
  export { useTheme } from './useTheme.ts';
8
  export { useAuth } from './useAuth.ts';
9
+ export { useChat } from './useChat.ts';
10
 
src/app/hooks/useChat.ts ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+ import { sendChatMessageStream } from '../services/groq.service.ts';
3
+ import { generateEnhancedArgument } from '../services/argument.service.ts';
4
+ import type { ChatMessage, ChatState } from '../types/chat.types.ts';
5
+
6
+ const generateId = () => Math.random().toString(36).substring(2, 15);
7
+
8
+ const initialState: ChatState = {
9
+ messages: [],
10
+ isLoading: false,
11
+ error: null,
12
+ };
13
+
14
+ export const useChat = () => {
15
+ const [state, setState] = useState<ChatState>(initialState);
16
+
17
+ const addMessage = useCallback((message: Omit<ChatMessage, 'id' | 'timestamp'>) => {
18
+ setState(prev => ({
19
+ ...prev,
20
+ messages: [...prev.messages, {
21
+ ...message,
22
+ id: generateId(),
23
+ timestamp: new Date(),
24
+ }],
25
+ }));
26
+ }, []);
27
+
28
+
29
+ const extractTopicAndPosition = useCallback(async (userInput: string): Promise<{ topic: string; position: string }> => {
30
+ 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).
31
+
32
+ Text: "${userInput}"
33
+
34
+ Example output:
35
+ {"topic": "Assisted suicide should be a criminal offence", "position": "positive"}`;
36
+
37
+ try {
38
+ const response = await sendChatMessageStream([
39
+ { id: generateId(), role: 'user', content: extractionPrompt, timestamp: new Date() }
40
+ ], () => {});
41
+
42
+ // Parse the JSON response
43
+ const jsonMatch = response.match(/\{[^}]+\}/);
44
+ if (jsonMatch) {
45
+ return JSON.parse(jsonMatch[0]);
46
+ }
47
+ throw new Error('Failed to extract topic and position');
48
+ } catch (error) {
49
+ throw new Error('Failed to extract topic and position from input');
50
+ }
51
+ }, []);
52
+
53
+ const sendMessage = useCallback(async (content: string, selectedTool?: string | null) => {
54
+ // Add user message
55
+ const userMessage: Omit<ChatMessage, 'id' | 'timestamp'> = {
56
+ role: 'user',
57
+ content,
58
+ };
59
+ addMessage(userMessage);
60
+
61
+ // Set loading state
62
+ setState(prev => ({ ...prev, isLoading: true, error: null }));
63
+
64
+ try {
65
+ // Get all messages including the new user message
66
+ const updatedMessages = [...state.messages, { ...userMessage, id: generateId(), timestamp: new Date() }];
67
+
68
+ // Check if generate argument tool is selected (check for multiple possible names)
69
+ const isGenerateArgumentTool = selectedTool && (
70
+ selectedTool.toLowerCase().includes('generate') &&
71
+ selectedTool.toLowerCase().includes('argument')
72
+ );
73
+
74
+ if (isGenerateArgumentTool) {
75
+ console.log('Generate argument tool detected:', selectedTool);
76
+ // Extract topic and position using LLM
77
+ const { topic, position } = await extractTopicAndPosition(content);
78
+
79
+ // Call the external API
80
+ const argumentResponse = await generateEnhancedArgument({ topic, position });
81
+
82
+ // Add assistant response with the enhanced argument
83
+ const assistantMessage: Omit<ChatMessage, 'id' | 'timestamp'> = {
84
+ role: 'assistant',
85
+ content: argumentResponse.enhanced_argument,
86
+ };
87
+ addMessage(assistantMessage);
88
+ } else {
89
+ // Regular chat with streaming
90
+ // Create an empty assistant message that will be updated with streaming content
91
+ const assistantId = generateId();
92
+ const assistantMessage: ChatMessage = {
93
+ id: assistantId,
94
+ role: 'assistant',
95
+ content: '',
96
+ timestamp: new Date(),
97
+ };
98
+
99
+ setState(prev => ({
100
+ ...prev,
101
+ messages: [...prev.messages, assistantMessage],
102
+ }));
103
+
104
+ // Send to Groq API with streaming
105
+ await sendChatMessageStream(updatedMessages, (chunk) => {
106
+ setState(prev => ({
107
+ ...prev,
108
+ messages: prev.messages.map(msg =>
109
+ msg.id === assistantId ? { ...msg, content: msg.content + chunk } : msg
110
+ ),
111
+ }));
112
+ });
113
+ }
114
+
115
+ setState(prev => ({ ...prev, isLoading: false }));
116
+ } catch (error) {
117
+ const errorMessage = error instanceof Error ? error.message : 'Failed to send message';
118
+ setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
119
+ }
120
+ }, [state.messages, addMessage, extractTopicAndPosition]);
121
+
122
+ const clearMessages = useCallback(() => {
123
+ setState(initialState);
124
+ }, []);
125
+
126
+ const retryLastMessage = useCallback(() => {
127
+ if (state.messages.length >= 2) {
128
+ const lastUserMessage = [...state.messages]
129
+ .reverse()
130
+ .find(msg => msg.role === 'user');
131
+
132
+ if (lastUserMessage) {
133
+ // Remove the last failed assistant message if exists
134
+ setState(prev => {
135
+ const lastMessage = prev.messages[prev.messages.length - 1];
136
+ if (lastMessage?.role === 'assistant') {
137
+ return {
138
+ ...prev,
139
+ messages: prev.messages.slice(0, -1),
140
+ error: null,
141
+ };
142
+ }
143
+ return { ...prev, error: null };
144
+ });
145
+
146
+ // Resend the user message
147
+ sendMessage(lastUserMessage.content);
148
+ }
149
+ }
150
+ }, [state.messages, sendMessage]);
151
+
152
+ return {
153
+ messages: state.messages,
154
+ isLoading: state.isLoading,
155
+ error: state.error,
156
+ sendMessage,
157
+ clearMessages,
158
+ retryLastMessage,
159
+ };
160
+ };
src/app/pages/ChatPage.tsx CHANGED
@@ -1,17 +1,46 @@
1
  import React from 'react';
2
  import ChatInput from '../components/chat/ChatInput.tsx';
 
 
3
 
4
  const ChatPage = () => {
5
- const handleMessageSubmit = (message: string) => {
6
- console.log('Message submitted:', message);
7
- // TODO: Implement chat message handling
 
8
  };
9
 
 
 
10
  return (
11
- <div className="flex min-h-screen items-center justify-center bg-white dark:bg-black px-4 pt-20 pb-10 transition-colors duration-200">
12
- <div className="w-full max-w-4xl">
13
- <ChatInput onSubmit={handleMessageSubmit} />
14
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  </div>
16
  );
17
  };
 
1
  import React from 'react';
2
  import ChatInput from '../components/chat/ChatInput.tsx';
3
+ import MessageList from '../components/chat/MessageList.tsx';
4
+ import { useChat } from '../hooks/useChat.ts';
5
 
6
  const ChatPage = () => {
7
+ const { messages, isLoading, error, sendMessage, retryLastMessage } = useChat();
8
+
9
+ const handleMessageSubmit = (message: string, selectedTool?: string | null) => {
10
+ sendMessage(message, selectedTool);
11
  };
12
 
13
+ const hasConversation = messages.length > 0;
14
+
15
  return (
16
+ <div className={`flex min-h-screen bg-white dark:bg-black transition-colors duration-200 ${hasConversation ? 'flex-col' : ''}`}>
17
+ {hasConversation ? (
18
+ <>
19
+ {/* Messages */}
20
+ <div className="flex-1 max-w-4xl w-full mx-auto pt-20">
21
+ <MessageList
22
+ messages={messages}
23
+ isLoading={isLoading}
24
+ error={error}
25
+ onRetry={retryLastMessage}
26
+ />
27
+ </div>
28
+
29
+ {/* Input */}
30
+ <div className="px-4 py-4">
31
+ <div className="max-w-4xl mx-auto">
32
+ <ChatInput onSubmit={handleMessageSubmit} placeholder="Type your message..." />
33
+ </div>
34
+ </div>
35
+ </>
36
+ ) : (
37
+ /* Centered input when no conversation */
38
+ <div className="flex items-center justify-center px-4 pt-20 pb-10 w-full">
39
+ <div className="w-full max-w-4xl">
40
+ <ChatInput onSubmit={handleMessageSubmit} placeholder="Ask me anything..." />
41
+ </div>
42
+ </div>
43
+ )}
44
  </div>
45
  );
46
  };
src/app/services/argument.service.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type ArgumentRequest = {
2
+ topic: string;
3
+ position: string;
4
+ };
5
+
6
+ export type ArgumentResponse = {
7
+ topic: string;
8
+ position: string;
9
+ original_argument: string;
10
+ enhanced_argument: string;
11
+ process: string;
12
+ };
13
+
14
+ /**
15
+ * Calls the external N8N webhook to generate enhanced arguments
16
+ */
17
+ export async function generateEnhancedArgument(request: ArgumentRequest): Promise<ArgumentResponse> {
18
+ const webhookUrl = process.env.REACT_APP_N8N_WEBHOOK_URL;
19
+
20
+ if (!webhookUrl) {
21
+ throw new Error('REACT_APP_N8N_WEBHOOK_URL environment variable is not set');
22
+ }
23
+
24
+ try {
25
+ const response = await fetch(`${webhookUrl}/generate-argument-enhanced`, {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ },
30
+ body: JSON.stringify(request),
31
+ });
32
+
33
+ if (!response.ok) {
34
+ throw new Error(`HTTP error! status: ${response.status}`);
35
+ }
36
+
37
+ const data: ArgumentResponse = await response.json();
38
+ return data;
39
+ } catch (error) {
40
+ if (error instanceof Error) {
41
+ throw error;
42
+ }
43
+ throw new Error('Failed to generate enhanced argument');
44
+ }
45
+ }
src/app/services/groq.service.ts ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ChatMessage } from '../types/chat.types.ts';
2
+
3
+ const GROQ_API_URL = 'https://api.groq.com/openai/v1';
4
+ const MODEL = 'meta-llama/llama-4-scout-17b-16e-instruct';
5
+
6
+ export type GroqMessage = {
7
+ role: 'user' | 'assistant' | 'system';
8
+ content: string;
9
+ };
10
+
11
+ export type GroqChatResponse = {
12
+ id: string;
13
+ object: string;
14
+ created: number;
15
+ model: string;
16
+ choices: Array<{
17
+ index: number;
18
+ message: {
19
+ role: 'assistant';
20
+ content: string;
21
+ };
22
+ finish_reason: string;
23
+ }>;
24
+ usage: {
25
+ prompt_tokens: number;
26
+ completion_tokens: number;
27
+ total_tokens: number;
28
+ };
29
+ };
30
+
31
+ export type GroqStreamChunk = {
32
+ id: string;
33
+ object: string;
34
+ created: number;
35
+ model: string;
36
+ choices: Array<{
37
+ index: number;
38
+ delta: {
39
+ role?: 'assistant';
40
+ content?: string;
41
+ };
42
+ finish_reason: string | null;
43
+ }>;
44
+ };
45
+
46
+ export type GroqError = {
47
+ error: {
48
+ message: string;
49
+ type: string;
50
+ code?: string;
51
+ };
52
+ };
53
+
54
+ /**
55
+ * Sends messages to Groq API and returns the assistant's response
56
+ */
57
+ export async function sendChatMessage(messages: ChatMessage[]): Promise<string> {
58
+ const apiKey = process.env.REACT_APP_GROQ_API_KEY;
59
+
60
+ if (!apiKey) {
61
+ throw new Error('REACT_APP_GROQ_API_KEY environment variable is not set');
62
+ }
63
+
64
+ // Convert our ChatMessage format to Groq's expected format
65
+ const groqMessages: GroqMessage[] = messages.map(msg => ({
66
+ role: msg.role,
67
+ content: msg.content
68
+ }));
69
+
70
+ try {
71
+ const response = await fetch(`${GROQ_API_URL}/chat/completions`, {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Authorization': `Bearer ${apiKey}`,
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ body: JSON.stringify({
78
+ model: MODEL,
79
+ messages: groqMessages,
80
+ temperature: 0.7,
81
+ max_tokens: 4096,
82
+ top_p: 1,
83
+ stream: true,
84
+ }),
85
+ });
86
+
87
+ if (!response.ok) {
88
+ const errorData: GroqError = await response.json();
89
+ throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`);
90
+ }
91
+
92
+ const reader = response.body?.getReader();
93
+ if (!reader) {
94
+ throw new Error('Failed to get response reader');
95
+ }
96
+
97
+ const decoder = new TextDecoder();
98
+ let fullContent = '';
99
+
100
+ while (true) {
101
+ const { done, value } = await reader.read();
102
+ if (done) break;
103
+
104
+ const chunk = decoder.decode(value, { stream: true });
105
+ const lines = chunk.split('\n');
106
+
107
+ for (const line of lines) {
108
+ if (line.startsWith('data: ')) {
109
+ const data = line.slice(6);
110
+ if (data === '[DONE]') continue;
111
+
112
+ try {
113
+ const parsed: GroqStreamChunk = JSON.parse(data);
114
+ const content = parsed.choices[0]?.delta?.content;
115
+ if (content) {
116
+ fullContent += content;
117
+ }
118
+ } catch (e) {
119
+ // Skip invalid JSON
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ return fullContent;
126
+ } catch (error) {
127
+ if (error instanceof Error) {
128
+ throw error;
129
+ }
130
+ throw new Error('Failed to send message to Groq API');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Sends messages to Groq API with streaming support
136
+ */
137
+ export async function sendChatMessageStream(
138
+ messages: ChatMessage[],
139
+ onChunk: (chunk: string) => void
140
+ ): Promise<string> {
141
+ const apiKey = process.env.REACT_APP_GROQ_API_KEY;
142
+
143
+ if (!apiKey) {
144
+ throw new Error('REACT_APP_GROQ_API_KEY environment variable is not set');
145
+ }
146
+
147
+ // Convert our ChatMessage format to Groq's expected format
148
+ const groqMessages: GroqMessage[] = messages.map(msg => ({
149
+ role: msg.role,
150
+ content: msg.content
151
+ }));
152
+
153
+ try {
154
+ const response = await fetch(`${GROQ_API_URL}/chat/completions`, {
155
+ method: 'POST',
156
+ headers: {
157
+ 'Authorization': `Bearer ${apiKey}`,
158
+ 'Content-Type': 'application/json',
159
+ },
160
+ body: JSON.stringify({
161
+ model: MODEL,
162
+ messages: groqMessages,
163
+ temperature: 0.7,
164
+ max_tokens: 4096,
165
+ top_p: 1,
166
+ stream: true,
167
+ }),
168
+ });
169
+
170
+ if (!response.ok) {
171
+ const errorData: GroqError = await response.json();
172
+ throw new Error(errorData.error?.message || `HTTP error! status: ${response.status}`);
173
+ }
174
+
175
+ const reader = response.body?.getReader();
176
+ if (!reader) {
177
+ throw new Error('Failed to get response reader');
178
+ }
179
+
180
+ const decoder = new TextDecoder();
181
+ let fullContent = '';
182
+
183
+ while (true) {
184
+ const { done, value } = await reader.read();
185
+ if (done) break;
186
+
187
+ const chunk = decoder.decode(value, { stream: true });
188
+ const lines = chunk.split('\n');
189
+
190
+ for (const line of lines) {
191
+ if (line.startsWith('data: ')) {
192
+ const data = line.slice(6);
193
+ if (data === '[DONE]') continue;
194
+
195
+ try {
196
+ const parsed: GroqStreamChunk = JSON.parse(data);
197
+ const content = parsed.choices[0]?.delta?.content;
198
+ if (content) {
199
+ fullContent += content;
200
+ onChunk(content);
201
+ }
202
+ } catch (e) {
203
+ // Skip invalid JSON
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ return fullContent;
210
+ } catch (error) {
211
+ if (error instanceof Error) {
212
+ throw error;
213
+ }
214
+ throw new Error('Failed to send message to Groq API');
215
+ }
216
+ }
src/app/types/chat.types.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type MessageRole = 'user' | 'assistant' | 'system';
2
+
3
+ export type ChatMessage = {
4
+ id: string;
5
+ role: MessageRole;
6
+ content: string;
7
+ timestamp: Date;
8
+ };
9
+
10
+ export type ChatState = {
11
+ messages: ChatMessage[];
12
+ isLoading: boolean;
13
+ error: string | null;
14
+ };
15
+
16
+ export type ChatHistory = {
17
+ messages: ChatMessage[];
18
+ createdAt: Date;
19
+ updatedAt: Date;
20
+ };
src/app/types/index.ts CHANGED
@@ -7,4 +7,5 @@ export * from './api.types.ts';
7
  export * from './analysis.types.ts';
8
  export * from './user.types.ts';
9
  export * from './mcp.types.ts';
 
10
 
 
7
  export * from './analysis.types.ts';
8
  export * from './user.types.ts';
9
  export * from './mcp.types.ts';
10
+ export * from './chat.types.ts';
11