Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"axios": "^1.12.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.553.0",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0"
Expand Down
126 changes: 85 additions & 41 deletions src/app/components/chatbot/AIChatWidget.tsx
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';
Expand All @@ -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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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:

-const API_ENDPOINT = 'http://localhost:8091/api/v1/ai/chat';
+const API_ENDPOINT = `${process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8091'}/api/v1/ai/chat`;

Then configure .env.local (local), .env.production, etc.:

NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.com
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const API_ENDPOINT = 'http://localhost:8091/api/v1/ai/chat';
const API_ENDPOINT = `${process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8091'}/api/v1/ai/chat`;
🤖 Prompt for AI Agents
In src/app/components/chatbot/AIChatWidget.tsx around line 36, replace the
hardcoded API endpoint 'http://localhost:8091/api/v1/ai/chat' with a URL built
from an environment variable: read process.env.NEXT_PUBLIC_API_BASE_URL (falling
back to a sensible default only for local dev if needed), append
'/api/v1/ai/chat' while normalizing slashes to avoid double slashes, and
export/use that constant; update docs/.env files to include
NEXT_PUBLIC_API_BASE_URL for each environment.


// --- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove mock authentication before production deployment.

The mock useAuth hook returns a hardcoded token and always reports the user as logged in. This bypasses real authentication and poses a security risk.

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

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/components/chatbot/AIChatWidget.tsx around lines 38 to 43, remove the
mock useAuth hook that returns a hardcoded token and replace it with your real
authentication hook (e.g., import and call your existing useAuth/useUser hook);
ensure the component reads the token from the real hook, handles the
unauthenticated state (null/undefined token or isLoggedIn false) gracefully
(show login prompt or disable features), and do not leave any hardcoded tokens
in source code or logs.


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' };
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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
In src/app/components/chatbot/AIChatWidget.tsx around lines 76 to 86, the user
token is being sent twice (in payload.session_id/token and again in the
Authorization header) causing ambiguity; either remove the token field from the
JSON payload and rely solely on the Authorization header for Gateway validation,
or if the downstream Agent_Bot requires the token in the body, keep both but add
a clear comment explaining that header is for Gateway auth and body.token is for
Agent_Bot, and update any related types/tests to reflect the chosen approach.

},
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) {

Check failure on line 110 in src/app/components/chatbot/AIChatWidget.tsx

View workflow job for this annotation

GitHub Actions / Install, Lint and Build

Unexpected any. Specify a different type

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace any type with specific error type.

Using any defeats TypeScript's type safety and is flagged by the linter.

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);

Committable suggestion skipped: line range outside the PR's diff.

🧰 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:
Unexpected any. Specify a different type

🤖 Prompt for AI Agents
In src/app/components/chatbot/AIChatWidget.tsx around line 110, the catch clause
currently uses the `any` type which bypasses TypeScript safety; change the catch
parameter to `unknown` (or `error: unknown`) and then narrow it before use
(e.g., use `if (error instanceof Error) { const msg = error.message } else {
const msg = String(error) }`) or explicitly type-guard/extract the message so
you never assume `any`; update subsequent error handling/logging to use the
safely extracted message.

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>
);
};
Expand Down
Loading