-
Notifications
You must be signed in to change notification settings - Fork 0
style: Update AIChatWidget UI #20
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,20 @@ | ||
| // components/AIChatWidget.tsx | ||
| import React, { useState, useRef, useEffect, useCallback } from 'react'; | ||
| // Assuming you have a way to get the current user's JWT | ||
| import { useAuth } from '../hooks/useAuth'; // Placeholder for your auth hook | ||
| import { Sparkles, Bolt } from 'lucide-react'; | ||
|
|
||
| // --- Theme Simulation & Constants --- | ||
| // NOTE: These theme classes are defined here for self-containment. | ||
| const theme = { | ||
| 'theme-text-primary': 'text-gray-900 dark:text-gray-100', | ||
| 'theme-text-muted': 'text-gray-500 dark:text-gray-400', | ||
| 'theme-bg-primary': 'bg-gray-50 dark:bg-gray-800', | ||
| 'theme-button-primary': 'bg-indigo-600 hover:bg-indigo-700 text-white', | ||
| 'theme-button-secondary': 'bg-gray-200 dark:bg-gray-700', | ||
| 'theme-border': 'border-gray-300 dark:border-gray-700', | ||
| 'theme-input': 'bg-white dark:bg-gray-600 border theme-border p-2 rounded-lg focus:ring-indigo-500 focus:border-indigo-500', | ||
| 'automotive-card': 'bg-white dark:bg-gray-800' | ||
| }; | ||
|
|
||
| // --- TypeScript Interface Definitions --- | ||
| // --- TypeScript Interface Definitions (CRITICAL for TSX) --- | ||
| interface Message { | ||
| text: string; | ||
| sender: 'user' | 'ai' | 'system'; | ||
|
|
@@ -12,32 +23,47 @@ | |
| interface ChatResponse { | ||
| reply: string; | ||
| session_id: string; | ||
| tool_executed?: string | null; // Added tool_executed matching Python model | ||
| } | ||
|
|
||
| // Mocking the user authentication hook | ||
| interface AuthHook { | ||
| token: string | null; | ||
| isLoggedIn: boolean; | ||
| } | ||
|
|
||
| const API_ENDPOINT = '/api/v1/ai/chat'; // This will be routed by your API Gateway | ||
| // NOTE: Using localhost:8091 to test against the locally running FastAPI service | ||
| const API_ENDPOINT = 'http://localhost:8091/api/v1/ai/chat'; | ||
|
|
||
| // --- MOCK AUTH HOOK --- | ||
| // Provides a mock token; replace this with your actual useAuth hook | ||
| const useAuth = (): AuthHook => ({ | ||
| token: "mock-jwt-token-for-user-12345", | ||
| isLoggedIn: true | ||
| }); | ||
|
Comment on lines
+38
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove mock authentication before production deployment. The mock Replace with your actual authentication hook: -// --- MOCK AUTH HOOK ---
-// Provides a mock token; replace this with your actual useAuth hook
-const useAuth = (): AuthHook => ({
- token: "mock-jwt-token-for-user-12345",
- isLoggedIn: true
-});
+// Import your real auth hook
+import { useAuth } from '@/hooks/useAuth'; // or your actual auth hook location
🤖 Prompt for AI Agents |
||
|
|
||
| const AIChatWidget: React.FC = () => { | ||
| // 1. State Management | ||
| const { token: userToken } = useAuth(); | ||
|
|
||
| const [conversationHistory, setConversationHistory] = useState<Message[]>([ | ||
| { text: "Hello! I'm TechTorque Assistant. How can I help you with your services or appointments?", sender: 'ai' } | ||
| ]); | ||
| const [inputMessage, setInputMessage] = useState<string>(''); | ||
| const [sessionId, setSessionId] = useState<string | null>(null); | ||
| const [isLoading, setIsLoading] = useState<boolean>(false); | ||
|
|
||
| // 2. Auth/Token Retrieval (Crucial for secure API call) | ||
| const { token: userToken } = useAuth(); // Assuming this hook provides the JWT | ||
|
|
||
|
|
||
| const messagesEndRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| // Auto-scroll to the latest message | ||
| useEffect(() => { | ||
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | ||
| }, [conversationHistory]); | ||
|
|
||
| // 3. The Core Logic (Memoized for performance) | ||
| // 2. The Core Logic | ||
| const sendMessage = useCallback(async (message: string) => { | ||
| if (!message.trim() || isLoading || !userToken) return; | ||
| // Ensure token exists before proceeding with any action | ||
| if (!message.trim() || isLoading || !userToken) return; | ||
|
|
||
| // Add user message to history | ||
| const userMessage: Message = { text: message, sender: 'user' }; | ||
|
|
@@ -46,113 +72,131 @@ | |
| setIsLoading(true); | ||
|
|
||
| try { | ||
| // 4. API Payload (Must match the Python ChatRequest model!) | ||
| // 3. API Payload (Must match the Python ChatRequest model!) | ||
| const payload = { | ||
| query: message, | ||
| session_id: sessionId, | ||
| token: userToken, // Passed in body for Agent_Bot context retrieval | ||
| token: userToken, // Sent in body for Agent_Bot context | ||
| }; | ||
|
|
||
| const response = await fetch(API_ENDPOINT, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Authorization': `Bearer ${userToken}`, // Passed in header for Gateway validation | ||
| 'Authorization': `Bearer ${userToken}`, // Sent in header for Gateway validation | ||
|
Comment on lines
76
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Token is transmitted twice—clarify the intent or remove redundancy. The token is sent in both the request body (line 79) and the Authorization header (line 86). This duplication may cause confusion about which location is authoritative. If the Gateway validates via header and Agent_Bot needs it in the body, document this clearly: const payload = {
query: message,
session_id: sessionId,
- token: userToken, // Sent in body for Agent_Bot context
+ token: userToken, // REQUIRED: Agent_Bot service needs token in body for internal routing
};
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
- 'Authorization': `Bearer ${userToken}`, // Sent in header for Gateway validation
+ 'Authorization': `Bearer ${userToken}`, // REQUIRED: API Gateway validates token in header
},
body: JSON.stringify(payload),
});Otherwise, if only one is needed, remove the redundant transmission. 🤖 Prompt for AI Agents |
||
| }, | ||
| body: JSON.stringify(payload), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| // If 401/403, log out or show an error | ||
| // Handle non-200 responses (e.g., 401, 500) | ||
| const errorData = await response.json(); | ||
| throw new Error(errorData.detail || 'Failed to get a reply.'); | ||
| throw new Error(errorData.detail || `HTTP Error ${response.status}`); | ||
| } | ||
|
|
||
| const data: ChatResponse = await response.json(); | ||
|
|
||
| // 5. Update state with AI response and new session ID | ||
| const aiResponse: Message = { text: data.reply, sender: 'ai' }; | ||
| // 4. Update state with AI response and new session ID | ||
| let replyText = data.reply; | ||
| if (data.tool_executed) { | ||
| // Indicate tool execution for debugging/clarity | ||
| replyText = `⚙️ (Tool used: ${data.tool_executed}) ${replyText}`; | ||
| } | ||
|
|
||
| const aiResponse: Message = { text: replyText, sender: 'ai' }; | ||
| setConversationHistory(prev => [...prev, aiResponse]); | ||
| setSessionId(data.session_id); // CRITICAL: Save the session ID for the next turn | ||
| setSessionId(data.session_id); // CRITICAL: Save the session ID | ||
|
|
||
| } catch (error: any) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace Using Apply this diff: -} catch (error: any) {
+} catch (error: unknown) {
console.error("Chat Error:", error);
const errorMessage: Message = {
- text: "Sorry, I'm having trouble connecting to the services. Please try again later.",
+ text: error instanceof Error ? error.message : "Sorry, I'm having trouble connecting to the services. Please try again later.",
sender: 'system'
};Or if you prefer to keep the generic message: -} catch (error: any) {
+} catch (error: unknown) {
console.error("Chat Error:", error);
🧰 Tools🪛 GitHub Actions: Build and Test Frontend_Web[error] 110-110: ESLint: Unexpected any. Specify a different type. @typescript-eslint/no-explicit-any 🪛 GitHub Check: Install, Lint and Build[failure] 110-110: 🤖 Prompt for AI Agents |
||
| console.error("Chat Error:", error); | ||
| const errorMessage: Message = { | ||
| text: error.message.includes('401') | ||
| ? "Your session has expired. Please log in again." | ||
| : "Sorry, I'm having trouble with the services. Try again later.", | ||
| text: "Sorry, I'm having trouble connecting to the services. Please try again later.", | ||
| sender: 'system' | ||
| }; | ||
| setConversationHistory(prev => [...prev, errorMessage]); | ||
| } finally { | ||
| setIsLoading(false); | ||
| } | ||
| }, [isLoading, sessionId, userToken]); // Dependencies for useCallback | ||
| }, [isLoading, sessionId, userToken]); | ||
|
|
||
| // 6. Handler for form submission | ||
| // 5. Handler for form submission | ||
| const handleSubmit = (e: React.FormEvent) => { | ||
| e.preventDefault(); | ||
| sendMessage(inputMessage); | ||
| }; | ||
|
|
||
| // --- RENDER FUNCTION (JSX) --- | ||
| return ( | ||
| <div className="flex flex-col h-full bg-white rounded-lg shadow-xl"> | ||
| <div className={`flex flex-col h-full ${theme['automotive-card']} shadow-lg border ${theme['theme-border']} rounded-xl max-w-lg mx-auto md:max-w-xl`}> | ||
|
|
||
| {/* Header */} | ||
| <div className="p-4 border-b bg-gray-50"> | ||
| <h3 className="text-lg font-semibold text-indigo-700">TechTorque AI Assistant</h3> | ||
| <div className={`p-4 ${theme['theme-bg-primary']} border-b ${theme['theme-border']} rounded-t-xl flex items-center`}> | ||
| <div className="w-8 h-8 rounded-full bg-gradient-to-r from-indigo-500 to-cyan-400 flex items-center justify-center shadow-md mr-3"> | ||
| <Bolt className="w-4 h-4 text-white" /> | ||
| </div> | ||
| <h3 className={`text-lg font-bold ${theme['theme-text-primary']}`}> | ||
| TechTorque AI Assistant | ||
| </h3> | ||
| </div> | ||
|
|
||
| {/* Message Display Area */} | ||
| <div className="flex-1 overflow-y-auto p-4 space-y-3"> | ||
| <div className={`flex-1 overflow-y-auto p-4 space-y-3 ${theme['theme-bg-primary']}`}> | ||
| {conversationHistory.map((msg, index) => ( | ||
| <div | ||
| key={index} | ||
| className={`flex ${msg.sender === 'user' ? 'justify-end' : 'justify-start'}`} | ||
| > | ||
| <div className={`max-w-xs px-4 py-2 rounded-xl text-sm ${ | ||
| <div className={`max-w-[80%] md:max-w-[70%] px-4 py-2 rounded-xl text-sm break-words shadow-sm ${ | ||
| msg.sender === 'user' | ||
| ? 'bg-indigo-500 text-white rounded-br-none' | ||
| : 'bg-gray-200 text-gray-800 rounded-tl-none' | ||
| ? `${theme['theme-button-primary']} rounded-br-none` | ||
| : `${theme['theme-button-secondary']} ${theme['theme-text-primary']} rounded-tl-none` | ||
| }`}> | ||
| {msg.sender === 'ai' && <Sparkles className="w-4 h-4 inline mr-1" />} | ||
| {msg.text} | ||
| </div> | ||
| </div> | ||
| ))} | ||
|
|
||
| {/* Typing Indicator */} | ||
| {isLoading && ( | ||
| <div className="flex justify-start"> | ||
| <div className="max-w-xs px-4 py-2 rounded-xl text-sm bg-gray-200 text-gray-800 rounded-tl-none animate-pulse"> | ||
| Thinking... | ||
| <div className={`max-w-xs px-4 py-2 rounded-xl text-sm ${theme['theme-button-secondary']} ${theme['theme-text-muted']} rounded-tl-none animate-pulse`}> | ||
| <Sparkles className="w-4 h-4 inline mr-1" /> Thinking... | ||
| </div> | ||
| </div> | ||
| )} | ||
| <div ref={messagesEndRef} /> {/* Auto-scroll reference */} | ||
| <div ref={messagesEndRef} /> | ||
| </div> | ||
|
|
||
| {/* Input Form */} | ||
| <form onSubmit={handleSubmit} className="p-4 border-t flex space-x-2"> | ||
| <form onSubmit={handleSubmit} className={`p-4 border-t ${theme['theme-border']} flex space-x-2 ${theme['theme-bg-primary']} rounded-b-xl`}> | ||
| <input | ||
| type="text" | ||
| value={inputMessage} | ||
| onChange={(e) => setInputMessage(e.target.value)} | ||
| placeholder="Ask about appointments, status, or services..." | ||
| className="flex-1 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500" | ||
| placeholder={!userToken ? "Please sign in to chat..." : "Ask about status, scheduling, or services..."} | ||
| className={`${theme['theme-input']} flex-1 w-full`} | ||
| disabled={isLoading || !userToken} | ||
| /> | ||
| <button | ||
| type="submit" | ||
| className={`px-4 py-2 rounded-lg font-semibold transition duration-150 ${ | ||
| className={`px-4 py-2 rounded-lg font-semibold transition duration-150 ${theme['theme-button-primary']} ${ | ||
| isLoading || !inputMessage.trim() || !userToken | ||
| ? 'bg-indigo-300 text-white cursor-not-allowed' | ||
| : 'bg-indigo-600 text-white hover:bg-indigo-700' | ||
| ? 'opacity-60 cursor-not-allowed' | ||
| : '' | ||
| }`} | ||
| disabled={isLoading || !inputMessage.trim() || !userToken} | ||
| > | ||
| Send | ||
| </button> | ||
| </form> | ||
| {!userToken && <div className="p-2 text-center text-xs text-red-500">Please log in to use the assistant.</div>} | ||
|
|
||
| {/* Login Warning Message */} | ||
| {!userToken && ( | ||
| <div className={`p-2 text-center text-xs text-red-500 ${theme['theme-bg-primary']} border-t ${theme['theme-border']}`}> | ||
| <p className='text-xs'>Please log in to start using the AI assistant.</p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace hardcoded localhost URL with environment variable.
The API endpoint is hardcoded to
localhost:8091, which will fail in production, staging, and any non-local environment.Use an environment variable for the base URL:
Then configure
.env.local(local),.env.production, etc.:📝 Committable suggestion
🤖 Prompt for AI Agents