Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.
Merged

Dev #26

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 @@ -13,6 +13,7 @@
"@stomp/stompjs": "^7.2.1",
"axios": "^1.12.2",
"js-cookie": "^3.0.5",
"lucide-react": "^0.553.0",
"next": "^16.0.1",
"react": "19.1.0",
"react-dom": "19.1.0",
Expand Down
37 changes: 37 additions & 0 deletions src/app/api/v1/ai/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// API Route to proxy chat requests to Agent_Bot service
import { NextRequest, NextResponse } from 'next/server';

const AGENT_BOT_URL = process.env.AGENT_BOT_URL || 'http://localhost:8091';

export async function POST(request: NextRequest) {
try {
// Get the request body
const body = await request.json();

// Get the authorization header
const authHeader = request.headers.get('authorization');

// Forward the request to Agent_Bot service
const response = await fetch(`${AGENT_BOT_URL}/api/v1/ai/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authHeader && { 'Authorization': authHeader }),
},
body: JSON.stringify(body),
});

// Get the response data
const data = await response.json();

// Return the response with appropriate status
return NextResponse.json(data, { status: response.status });

} catch (error) {
console.error('Error proxying to Agent_Bot:', error);
return NextResponse.json(
{ detail: 'Failed to connect to AI service' },
{ status: 500 }
);
}
}
141 changes: 93 additions & 48 deletions src/app/components/chatbot/AIChatWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
// components/AIChatWidget.tsx
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { Sparkles, Bolt } from 'lucide-react';
import Cookies from 'js-cookie';

// --- Theme Simulation & Constants ---
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 ---
interface Message {
text: string;
Expand All @@ -11,146 +23,179 @@ interface Message {
interface ChatResponse {
reply: string;
session_id: string;
tool_executed?: string | null;
}

const API_ENDPOINT = '/api/v1/ai/chat'; // This will be routed by your API Gateway
const API_ENDPOINT = 'http://localhost:8091/api/v1/ai/chat';

const AIChatWidget: React.FC = () => {
// 1. State Management
// State Management
const [userToken, setUserToken] = useState<string | null>(null);

const [conversationHistory, setConversationHistory] = useState<Message[]>([
{ text: "Hello! I'm TechTorque Assistant. How can I help you with your services or appointments?", sender: 'ai' }
{ text: "👋 Hello! I'm TechTorque Assistant, your friendly car service companion! 🚗\n\nI can help you with:\n✅ Booking appointments\n✅ Checking service status\n✅ Vehicle information\n✅ Pricing & estimates\n\nWhat can I do for you today? 😊", sender: 'ai' }
]);
const [inputMessage, setInputMessage] = useState<string>('');
const [sessionId, setSessionId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);

const messagesEndRef = useRef<HTMLDivElement>(null);

// Check for cookie on mount
useEffect(() => {
const token = Cookies.get('tt_access_token');
setUserToken(token || null);
}, []);

// Auto-scroll to the latest message
// Auto-scroll to latest message
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [conversationHistory]);

// 3. The Core Logic (Memoized for performance)
// Core Logic
const sendMessage = useCallback(async (message: string) => {
// Get token directly from cookies
const userToken = Cookies.get('tt_access_token');
const currentToken = Cookies.get('tt_access_token');

if (!message.trim() || isLoading || !userToken) return;
if (!message.trim() || isLoading || !currentToken) return;

// Add user message to history
const userMessage: Message = { text: message, sender: 'user' };
setConversationHistory(prev => [...prev, userMessage]);
setInputMessage('');
setIsLoading(true);

try {
// 4. 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: currentToken,
};

const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`, // Passed in header for Gateway validation
'Authorization': `Bearer ${currentToken}`,
},
body: JSON.stringify(payload),
});

if (!response.ok) {
// If 401/403, log out or show an error
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' };
let replyText = data.reply;
if (data.tool_executed) {
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);

} catch (error: unknown) {
console.error("Chat Error:", error);
const errorMessage: Message = {
text: (error instanceof Error && error.message.includes('401'))
? "Your session has expired. Please log in again."
: "Sorry, I'm having trouble with the services. Try again later.",
sender: 'system'
const errorMessage: Message = {
text: (error instanceof Error && error.message.includes('401'))
? "🔒 Your session has expired. Please log in again to continue chatting!"
: "⚠️ Oops! I'm having trouble connecting to my services right now. Please try again in a moment! 🔄",
sender: 'system'
};
setConversationHistory(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
}, [isLoading, sessionId]); // Dependencies for useCallback
}, [isLoading, sessionId]);

// 6. Handler for form submission
// 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-5 space-y-4 ${theme['theme-bg-primary']}`}>
{conversationHistory.map((msg, index) => (
<div
key={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 ${
msg.sender === 'user'
? 'bg-indigo-500 text-white rounded-br-none'
: 'bg-gray-200 text-gray-800 rounded-tl-none'
<div className={`max-w-[85%] px-5 py-3 rounded-2xl text-base leading-relaxed ${
msg.sender === 'user'
? 'bg-indigo-600 text-white rounded-br-none shadow-md'
: msg.sender === 'system'
? 'bg-red-100 text-red-800 border border-red-300 rounded-tl-none'
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-tl-none shadow-sm'
}`}>
{msg.text}
{msg.sender === 'ai' && <Sparkles className="w-4 h-4 inline mr-1" />}
<span className="whitespace-pre-wrap break-words">{msg.text}</span>
</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-[85%] px-5 py-3 rounded-2xl text-base bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-tl-none shadow-sm animate-pulse">
<Sparkles className="w-4 h-4 inline mr-1" />
<span className="flex items-center gap-1">
<span>Thinking</span>
<span className="inline-flex gap-1">
<span className="animate-bounce">.</span>
<span className="animate-bounce delay-100">.</span>
<span className="animate-bounce delay-200">.</span>
</span>
</span>
</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-3 ${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"
disabled={isLoading}
placeholder={!userToken ? "Please sign in to chat..." : "Ask about appointments, status, or services..."}
className="flex-1 px-4 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:bg-gray-800 dark:text-white"
disabled={isLoading || !userToken}
/>
<button
type="submit"
className={`px-4 py-2 rounded-lg font-semibold transition duration-150 ${
isLoading || !inputMessage.trim()
className={`px-6 py-3 rounded-xl font-semibold text-base transition duration-150 ${
isLoading || !inputMessage.trim() || !userToken
? 'bg-indigo-300 text-white cursor-not-allowed'
: 'bg-indigo-600 text-white hover:bg-indigo-700'
: 'bg-indigo-600 text-white hover:bg-indigo-700 shadow-md hover:shadow-lg'
}`}
disabled={isLoading || !inputMessage.trim()}
disabled={isLoading || !inputMessage.trim() || !userToken}
>
Send
</button>
</form>

{/* 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
2 changes: 1 addition & 1 deletion src/app/components/dashboards/CustomerDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ const CustomerDashboard: React.FC<CustomerDashboardProps> = ({ profile }) => {
{/* Floating AI Chat Widget */}
<div className="fixed bottom-6 right-6 z-50">
{chatOpen ? (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl w-96 h-[32rem] flex flex-col border border-gray-200 dark:border-gray-700">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-2xl w-[480px] h-[600px] flex flex-col border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-indigo-700 dark:text-indigo-400">TechTorque AI Assistant</h3>
<button
Expand Down