new changes

This commit is contained in:
Harith_dml 2025-06-27 15:48:06 +05:30
commit b980f7a052
29 changed files with 5661 additions and 0 deletions

3
.bolt/config.json Normal file
View file

@ -0,0 +1,3 @@
{
"template": "bolt-vite-react-ts"
}

5
.bolt/prompt Normal file
View file

@ -0,0 +1,5 @@
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
Use icons from lucide-react for logos.

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env

28
eslint.config.js Normal file
View file

@ -0,0 +1,28 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
);

13
index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>IT Ticket Management System</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4066
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "vite-react-typescript-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.263.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@eslint/js": "^8.57.0",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"globals": "^13.20.0",
"postcss": "^8.4.24",
"tailwindcss": "^3.3.0",
"typescript": "^5.0.2",
"typescript-eslint": "^7.13.1",
"vite": "^4.4.5"
},
"engines": {
"node": ">=14.18.0"
}
}

6
postcss.config.js Normal file
View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

171
src/App.tsx Normal file
View file

@ -0,0 +1,171 @@
import React, { useState, useEffect } from 'react';
import { Computer, MessageSquare, History, Plus } from 'lucide-react';
import { TicketForm } from './components/TicketForm';
import { TicketHistory } from './components/TicketHistory';
import { ChatInterface } from './components/ChatInterface';
import { TicketConfirmation } from './components/TicketConfirmation';
import { apiService } from './services/apiService';
import { Ticket } from './types';
function App() {
const [activeTab, setActiveTab] = useState<'new' | 'history' | 'chat'>('new');
const [tickets, setTickets] = useState<Ticket[]>([]);
const [showConfirmation, setShowConfirmation] = useState<Ticket | null>(null);
const [isLoadingTickets, setIsLoadingTickets] = useState(false);
// Load tickets from backend on component mount
useEffect(() => {
loadTickets();
}, []);
const loadTickets = async () => {
setIsLoadingTickets(true);
try {
const backendTickets = await apiService.getTickets();
// Convert backend format to frontend format
const formattedTickets: Ticket[] = backendTickets.map((ticket: any) => ({
...ticket,
status: 'Open' as const, // Backend doesn't have status field yet
timestamp: new Date(ticket.timestamp).toLocaleString()
}));
setTickets(formattedTickets);
} catch (error) {
console.error('Error loading tickets:', error);
} finally {
setIsLoadingTickets(false);
}
};
const handleTicketSubmit = (ticket: Ticket) => {
setTickets(prev => [ticket, ...prev]);
setShowConfirmation(ticket);
};
const handleTicketUpdate = (ticketId: string, updates: Partial<Ticket>) => {
setTickets(prev =>
prev.map(ticket =>
ticket.id === ticketId ? { ...ticket, ...updates } : ticket
)
);
};
const handleCreateTicketFromChat = (conversation: string) => {
// This would typically show a form pre-filled with the conversation
// For now, we'll just switch to the new ticket tab
setActiveTab('new');
};
const tabs = [
{ id: 'new' as const, label: 'New Ticket', icon: Plus },
{ id: 'history' as const, label: 'History', icon: History },
{ id: 'chat' as const, label: 'Live Chat', icon: MessageSquare }
];
return (
<div className="min-h-screen bg-gray-950 text-white">
{/* Header */}
<div className="bg-gray-900 border-b border-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
<Computer className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold">DIGI TICKET</h1>
<p className="text-xs text-gray-400">IT Support System</p>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-gray-400">
<span>Total Tickets:</span>
<span className="text-white font-semibold">
{isLoadingTickets ? '...' : tickets.length}
</span>
</div>
</div>
</div>
</div>
{/* Navigation */}
<div className="bg-gray-900 border-b border-gray-800">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<nav className="flex space-x-1">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium transition-all duration-200 border-b-2 ${
activeTab === tab.id
? 'text-blue-400 border-blue-400 bg-blue-400/5'
: 'text-gray-400 border-transparent hover:text-gray-300 hover:border-gray-600'
}`}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
</button>
);
})}
</nav>
</div>
</div>
{/* Main Content */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{activeTab === 'new' && (
<div>
<div className="mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Submit New Ticket</h2>
<p className="text-gray-400">Use voice or text to describe your IT issue</p>
</div>
<TicketForm onTicketSubmit={handleTicketSubmit} />
</div>
)}
{activeTab === 'history' && (
<div>
<div className="mb-8 flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white mb-2">Ticket History</h2>
<p className="text-gray-400">View and manage all your support tickets</p>
</div>
<button
onClick={loadTickets}
disabled={isLoadingTickets}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
{isLoadingTickets ? 'Loading...' : 'Refresh'}
</button>
</div>
<TicketHistory
tickets={tickets}
onTicketUpdate={handleTicketUpdate}
/>
</div>
)}
{activeTab === 'chat' && (
<div>
<div className="mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Live Chat Support</h2>
<p className="text-gray-400">Get instant help from our AI assistant</p>
</div>
<ChatInterface onCreateTicket={handleCreateTicketFromChat} />
</div>
)}
</div>
{/* Confirmation Modal */}
{showConfirmation && (
<TicketConfirmation
ticket={showConfirmation}
onClose={() => setShowConfirmation(null)}
/>
)}
</div>
);
}
export default App;

View file

@ -0,0 +1,274 @@
import React, { useState, useRef, useEffect } from 'react';
import { Send, Bot, User, Loader, MessageSquare, UserPlus } from 'lucide-react';
import { ChatMessage } from '../types';
import { ChatService } from '../services/chatService';
interface ChatInterfaceProps {
onCreateTicket?: (conversation: string) => void;
}
export const ChatInterface: React.FC<ChatInterfaceProps> = ({ onCreateTicket }) => {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: '1',
role: 'assistant',
content: "Hello! I'm your IT support assistant. I'm here to help you with any technical issues you might be experiencing. What can I help you with today?",
timestamp: new Date().toLocaleString()
}
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showTicketForm, setShowTicketForm] = useState(false);
const [ticketFormData, setTicketFormData] = useState({ requester: '', email: '' });
const [isCreatingTicket, setIsCreatingTicket] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const [chatService] = useState(() => new ChatService());
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage: ChatMessage = {
id: Date.now().toString(),
role: 'user',
content: input,
timestamp: new Date().toLocaleString()
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await chatService.sendMessage([...messages, userMessage]);
const assistantMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response,
timestamp: new Date().toLocaleString()
};
setMessages(prev => [...prev, assistantMessage]);
} catch (error) {
console.error('Chat error:', error);
const errorMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: "I apologize, but I'm experiencing some technical difficulties. Please try again or create a support ticket if the issue persists.",
timestamp: new Date().toLocaleString()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleCreateTicketFromChat = async () => {
if (!ticketFormData.requester.trim() || !ticketFormData.email.trim()) {
return;
}
setIsCreatingTicket(true);
try {
const ticket = await chatService.createTicketFromConversation(
ticketFormData.requester,
ticketFormData.email,
messages
);
// Show success message
const successMessage: ChatMessage = {
id: (Date.now() + 2).toString(),
role: 'assistant',
content: `Great! I've created a support ticket for you. Your ticket ID is ${ticket.id}. Our ${ticket.department} team will review your request and get back to you soon.`,
timestamp: new Date().toLocaleString()
};
setMessages(prev => [...prev, successMessage]);
setShowTicketForm(false);
setTicketFormData({ requester: '', email: '' });
} catch (error) {
console.error('Error creating ticket from chat:', error);
const errorMessage: ChatMessage = {
id: (Date.now() + 2).toString(),
role: 'assistant',
content: "I'm sorry, there was an error creating your ticket. Please try again or use the main ticket form.",
timestamp: new Date().toLocaleString()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsCreatingTicket(false);
}
};
return (
<div className="bg-gray-900 rounded-xl border border-gray-800 h-[600px] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-800">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
<Bot className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-white">IT Support Chat</h3>
<p className="text-sm text-gray-400">Ask me anything about IT issues</p>
</div>
</div>
{messages.length > 1 && !showTicketForm && (
<button
onClick={() => setShowTicketForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-all duration-200 hover:scale-105 text-sm"
>
<MessageSquare className="w-4 h-4" />
<span>Create Ticket</span>
</button>
)}
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{message.role === 'assistant' && (
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-full flex items-center justify-center flex-shrink-0">
<Bot className="w-4 h-4 text-white" />
</div>
)}
<div
className={`max-w-[70%] p-3 rounded-lg ${
message.role === 'user'
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-100 border border-gray-700'
}`}
>
<p className="whitespace-pre-wrap leading-relaxed">{message.content}</p>
<p className="text-xs opacity-70 mt-2">{message.timestamp}</p>
</div>
{message.role === 'user' && (
<div className="w-8 h-8 bg-gray-700 rounded-full flex items-center justify-center flex-shrink-0">
<User className="w-4 h-4 text-gray-300" />
</div>
)}
</div>
))}
{isLoading && (
<div className="flex gap-3 justify-start">
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-full flex items-center justify-center">
<Bot className="w-4 h-4 text-white" />
</div>
<div className="bg-gray-800 border border-gray-700 rounded-lg p-3">
<div className="flex items-center gap-2">
<Loader className="w-4 h-4 animate-spin text-blue-400" />
<span className="text-gray-400">Thinking...</span>
</div>
</div>
</div>
)}
{/* Ticket Creation Form */}
{showTicketForm && (
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4">
<div className="flex items-center gap-2 mb-4">
<UserPlus className="w-5 h-5 text-green-400" />
<h4 className="text-white font-medium">Create Support Ticket</h4>
</div>
<div className="space-y-3">
<input
type="text"
placeholder="Your full name"
value={ticketFormData.requester}
onChange={(e) => setTicketFormData(prev => ({ ...prev, requester: e.target.value }))}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isCreatingTicket}
/>
<input
type="email"
placeholder="Your email address"
value={ticketFormData.email}
onChange={(e) => setTicketFormData(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={isCreatingTicket}
/>
<div className="flex gap-2">
<button
onClick={handleCreateTicketFromChat}
disabled={!ticketFormData.requester.trim() || !ticketFormData.email.trim() || isCreatingTicket}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isCreatingTicket ? (
<>
<Loader className="w-4 h-4 animate-spin" />
<span>Creating...</span>
</>
) : (
<>
<MessageSquare className="w-4 h-4" />
<span>Create Ticket</span>
</>
)}
</button>
<button
onClick={() => setShowTicketForm(false)}
disabled={isCreatingTicket}
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="p-4 border-t border-gray-800">
<div className="flex gap-3">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type your message here..."
className="flex-1 px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
rows={1}
disabled={isLoading}
/>
<button
onClick={handleSend}
disabled={!input.trim() || isLoading}
className="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-lg transition-all duration-200 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
>
<Send className="w-5 h-5" />
</button>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,162 @@
import React, { useEffect, useState } from 'react';
import { CheckCircle, Volume2, VolumeX, Copy, Check } from 'lucide-react';
import { Ticket } from '../types';
import { TextToSpeechService } from '../services/speechService';
interface TicketConfirmationProps {
ticket: Ticket;
onClose: () => void;
}
export const TicketConfirmation: React.FC<TicketConfirmationProps> = ({ ticket, onClose }) => {
const [isSpeaking, setIsSpeaking] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [ttsService] = useState(() => new TextToSpeechService());
const speechText = `Ticket submitted successfully. Ticket ID: ${ticket.id}. Department: ${ticket.department}. Team: ${ticket.sub_department}. Issue Type: ${ticket.issue_type}. Priority: ${ticket.urgency}. Summary: ${ticket.summary}.`;
useEffect(() => {
// Auto-play confirmation
ttsService.speak(speechText);
setIsSpeaking(true);
const timer = setTimeout(() => {
setIsSpeaking(false);
}, 5000);
return () => {
clearTimeout(timer);
ttsService.stop();
};
}, []);
const toggleSpeech = () => {
if (isSpeaking) {
ttsService.stop();
setIsSpeaking(false);
} else {
ttsService.speak(speechText);
setIsSpeaking(true);
setTimeout(() => setIsSpeaking(false), 5000);
}
};
const copyTicketId = async () => {
try {
await navigator.clipboard.writeText(ticket.id);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (error) {
console.error('Failed to copy ticket ID:', error);
}
};
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case 'Low': return 'text-green-400 bg-green-400/10 border-green-400/20';
case 'Medium': return 'text-yellow-400 bg-yellow-400/10 border-yellow-400/20';
case 'High': return 'text-orange-400 bg-orange-400/10 border-orange-400/20';
case 'Critical': return 'text-red-400 bg-red-400/10 border-red-400/20';
default: return 'text-gray-400 bg-gray-400/10 border-gray-400/20';
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<div className="bg-gray-900 rounded-xl p-6 max-w-md w-full border border-gray-800 shadow-2xl">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-green-600 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-8 h-8 text-white" />
</div>
<h2 className="text-2xl font-bold text-white mb-2">Ticket Submitted!</h2>
<p className="text-gray-400">Your support request has been received</p>
</div>
<div className="space-y-4 mb-6">
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-400">Ticket ID</span>
<button
onClick={copyTicketId}
className="flex items-center gap-1 text-blue-400 hover:text-blue-300 transition-colors"
>
{isCopied ? (
<>
<Check className="w-4 h-4" />
<span className="text-xs">Copied</span>
</>
) : (
<>
<Copy className="w-4 h-4" />
<span className="text-xs">Copy</span>
</>
)}
</button>
</div>
<p className="text-white font-mono text-lg">{ticket.id}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-800 rounded-lg p-3 border border-gray-700">
<p className="text-xs text-gray-400 mb-1">Department</p>
<p className="text-white font-medium">{ticket.department}</p>
</div>
<div className="bg-gray-800 rounded-lg p-3 border border-gray-700">
<p className="text-xs text-gray-400 mb-1">Team</p>
<p className="text-white font-medium">{ticket.sub_department}</p>
</div>
</div>
<div className="bg-gray-800 rounded-lg p-3 border border-gray-700">
<p className="text-xs text-gray-400 mb-1">Issue Type</p>
<p className="text-white font-medium">{ticket.issue_type}</p>
</div>
<div className="flex items-center justify-between bg-gray-800 rounded-lg p-3 border border-gray-700">
<div>
<p className="text-xs text-gray-400 mb-1">Priority</p>
<span className={`px-2 py-1 rounded-full text-xs font-medium border ${getUrgencyColor(ticket.urgency)}`}>
{ticket.urgency}
</span>
</div>
</div>
<div className="bg-gray-800 rounded-lg p-3 border border-gray-700">
<p className="text-xs text-gray-400 mb-1">Summary</p>
<p className="text-white text-sm leading-relaxed">{ticket.summary}</p>
</div>
</div>
<div className="flex gap-3">
<button
onClick={toggleSpeech}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-all duration-200 ${
isSpeaking
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{isSpeaking ? (
<>
<VolumeX className="w-4 h-4" />
<span>Stop</span>
</>
) : (
<>
<Volume2 className="w-4 h-4" />
<span>Listen</span>
</>
)}
</button>
<button
onClick={onClose}
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-all duration-200"
>
Close
</button>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,165 @@
import React, { useState } from 'react';
import { Send, User, Mail, FileText, Loader } from 'lucide-react';
import { VoiceRecorder } from './VoiceRecorder';
import { TextToSpeechService } from '../services/speechService';
import { apiService } from '../services/apiService';
import { Ticket } from '../types';
interface TicketFormProps {
onTicketSubmit: (ticket: Ticket) => void;
}
export const TicketForm: React.FC<TicketFormProps> = ({ onTicketSubmit }) => {
const [formData, setFormData] = useState({
requester: '',
email: '',
content: ''
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [ttsService] = useState(() => new TextToSpeechService());
const handleVoiceTranscript = (transcript: string) => {
setFormData(prev => ({ ...prev, content: transcript }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.requester.trim() || !formData.email.trim() || !formData.content.trim()) {
setError('Please fill in all required fields');
return;
}
setIsSubmitting(true);
setError(null);
try {
const ticket = await apiService.submitTicket({
requester: formData.requester,
email: formData.email,
content: formData.content
});
// Convert backend response to frontend Ticket format
const formattedTicket: Ticket = {
...ticket,
status: 'Open' as const,
timestamp: new Date(ticket.timestamp).toLocaleString()
};
onTicketSubmit(formattedTicket);
// Text-to-speech confirmation
const speechText = `Ticket submitted successfully. Ticket ID: ${ticket.id}. Department: ${ticket.department}. Priority: ${ticket.urgency}.`;
ttsService.speak(speechText);
// Reset form
setFormData({ requester: '', email: '', content: '' });
} catch (error) {
console.error('Error submitting ticket:', error);
setError(error instanceof Error ? error.message : 'Failed to submit ticket. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="max-w-2xl mx-auto">
<div className="bg-gray-900 rounded-xl p-6 shadow-2xl border border-gray-800">
<div className="flex items-center gap-3 mb-6">
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
<FileText className="w-5 h-5 text-white" />
</div>
<div>
<h2 className="text-xl font-semibold text-white">Create New Ticket</h2>
<p className="text-gray-400 text-sm">Submit your IT support request</p>
</div>
</div>
{error && (
<div className="mb-6 p-4 bg-red-900/50 border border-red-700 rounded-lg">
<p className="text-red-300 text-sm">{error}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
<User className="w-4 h-4 inline mr-2" />
Your Name
</label>
<input
type="text"
value={formData.requester}
onChange={(e) => setFormData(prev => ({ ...prev, requester: e.target.value }))}
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="Enter your full name"
required
disabled={isSubmitting}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
<Mail className="w-4 h-4 inline mr-2" />
Email Address
</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
placeholder="your.email@company.com"
required
disabled={isSubmitting}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-3">
Describe Your Issue
</label>
<VoiceRecorder
onTranscript={handleVoiceTranscript}
disabled={isSubmitting}
/>
<div className="mt-4">
<textarea
value={formData.content}
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
rows={6}
className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 resize-none"
placeholder="Type your issue description here or use voice recording above..."
required
disabled={isSubmitting}
/>
</div>
</div>
<button
type="submit"
disabled={isSubmitting || !formData.requester.trim() || !formData.email.trim() || !formData.content.trim()}
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-medium rounded-lg transition-all duration-200 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-lg shadow-blue-600/25"
>
{isSubmitting ? (
<>
<Loader className="w-5 h-5 animate-spin" />
<span>Processing Ticket...</span>
</>
) : (
<>
<Send className="w-5 h-5" />
<span>Submit Ticket</span>
</>
)}
</button>
</form>
</div>
</div>
);
};

View file

@ -0,0 +1,174 @@
import React, { useState } from 'react';
import { Clock, CheckCircle, AlertCircle, Filter, Search, User, Mail } from 'lucide-react';
import { Ticket } from '../types';
interface TicketHistoryProps {
tickets: Ticket[];
onTicketUpdate: (ticketId: string, updates: Partial<Ticket>) => void;
}
export const TicketHistory: React.FC<TicketHistoryProps> = ({ tickets, onTicketUpdate }) => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [urgencyFilter, setUrgencyFilter] = useState<string>('all');
const filteredTickets = tickets.filter(ticket => {
const matchesSearch =
ticket.summary.toLowerCase().includes(searchTerm.toLowerCase()) ||
ticket.requester.toLowerCase().includes(searchTerm.toLowerCase()) ||
ticket.content.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || ticket.status === statusFilter;
const matchesUrgency = urgencyFilter === 'all' || ticket.urgency === urgencyFilter;
return matchesSearch && matchesStatus && matchesUrgency;
});
const getStatusColor = (status: string) => {
switch (status) {
case 'Open': return 'text-blue-400 bg-blue-400/10';
case 'In Progress': return 'text-yellow-400 bg-yellow-400/10';
case 'Resolved': return 'text-green-400 bg-green-400/10';
case 'Closed': return 'text-gray-400 bg-gray-400/10';
default: return 'text-gray-400 bg-gray-400/10';
}
};
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case 'Low': return 'text-green-400 bg-green-400/10';
case 'Medium': return 'text-yellow-400 bg-yellow-400/10';
case 'High': return 'text-orange-400 bg-orange-400/10';
case 'Critical': return 'text-red-400 bg-red-400/10';
default: return 'text-gray-400 bg-gray-400/10';
}
};
const markAsResolved = (ticketId: string) => {
onTicketUpdate(ticketId, { status: 'Resolved' });
};
return (
<div className="space-y-6">
{/* Filters */}
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<div className="flex items-center gap-3 mb-4">
<Filter className="w-5 h-5 text-blue-400" />
<h3 className="text-lg font-semibold text-white">Filter Tickets</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500" />
<input
type="text"
placeholder="Search tickets..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Status</option>
<option value="Open">Open</option>
<option value="In Progress">In Progress</option>
<option value="Resolved">Resolved</option>
<option value="Closed">Closed</option>
</select>
<select
value={urgencyFilter}
onChange={(e) => setUrgencyFilter(e.target.value)}
className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="all">All Urgency</option>
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
<option value="Critical">Critical</option>
</select>
</div>
</div>
{/* Tickets List */}
<div className="space-y-4">
{filteredTickets.length === 0 ? (
<div className="bg-gray-900 rounded-xl p-8 text-center border border-gray-800">
<AlertCircle className="w-12 h-12 text-gray-500 mx-auto mb-4" />
<p className="text-gray-400">No tickets found matching your criteria</p>
</div>
) : (
filteredTickets.map((ticket) => (
<div key={ticket.id} className="bg-gray-900 rounded-xl p-6 border border-gray-800 hover:border-gray-700 transition-all duration-200">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-semibold text-white">#{ticket.id}</h3>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(ticket.status)}`}>
{ticket.status}
</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getUrgencyColor(ticket.urgency)}`}>
{ticket.urgency}
</span>
</div>
<p className="text-gray-300 font-medium mb-2">{ticket.summary}</p>
<div className="flex items-center gap-4 text-sm text-gray-400">
<div className="flex items-center gap-1">
<User className="w-4 h-4" />
<span>{ticket.requester}</span>
</div>
<div className="flex items-center gap-1">
<Mail className="w-4 h-4" />
<span>{ticket.email}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{ticket.timestamp}</span>
</div>
</div>
</div>
{ticket.status !== 'Resolved' && ticket.status !== 'Closed' && (
<button
onClick={() => markAsResolved(ticket.id)}
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-all duration-200 hover:scale-105"
>
<CheckCircle className="w-4 h-4" />
<span>Resolve</span>
</button>
)}
</div>
<div className="border-t border-gray-800 pt-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div>
<p className="text-xs text-gray-500 mb-1">Department</p>
<p className="text-white font-medium">{ticket.department}</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-1">Team</p>
<p className="text-white font-medium">{ticket.sub_department}</p>
</div>
<div>
<p className="text-xs text-gray-500 mb-1">Issue Type</p>
<p className="text-white font-medium">{ticket.issue_type}</p>
</div>
</div>
<div>
<p className="text-xs text-gray-500 mb-2">Description</p>
<p className="text-gray-300 leading-relaxed">{ticket.content}</p>
</div>
</div>
</div>
))
)}
</div>
</div>
);
};

View file

@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import { Mic, MicOff, Volume2 } from 'lucide-react';
import { SpeechService } from '../services/speechService';
interface VoiceRecorderProps {
onTranscript: (transcript: string) => void;
disabled?: boolean;
}
export const VoiceRecorder: React.FC<VoiceRecorderProps> = ({ onTranscript, disabled = false }) => {
const [isListening, setIsListening] = useState(false);
const [transcript, setTranscript] = useState('');
const [isSupported, setIsSupported] = useState(false);
const [speechService] = useState(() => new SpeechService());
useEffect(() => {
setIsSupported(speechService.isSupported());
}, [speechService]);
const startListening = () => {
if (!isSupported || disabled) return;
speechService.startListening(
(result) => {
setTranscript(result.transcript);
if (result.isFinal) {
onTranscript(result.transcript);
}
},
(error) => {
console.error('Speech recognition error:', error);
setIsListening(false);
}
);
setIsListening(true);
};
const stopListening = () => {
speechService.stopListening();
setIsListening(false);
if (transcript) {
onTranscript(transcript);
}
};
const toggleListening = () => {
if (isListening) {
stopListening();
} else {
startListening();
}
};
if (!isSupported) {
return (
<div className="flex items-center justify-center p-4 bg-gray-800 rounded-lg border border-gray-700">
<MicOff className="w-5 h-5 text-gray-500 mr-2" />
<span className="text-gray-400 text-sm">Speech recognition not supported in this browser</span>
</div>
);
}
return (
<div className="space-y-3">
<button
onClick={toggleListening}
disabled={disabled}
className={`
w-full flex items-center justify-center gap-3 px-6 py-4 rounded-lg font-medium transition-all duration-200
${isListening
? 'bg-red-600 hover:bg-red-700 text-white shadow-lg shadow-red-600/25'
: 'bg-blue-600 hover:bg-blue-700 text-white shadow-lg shadow-blue-600/25'
}
${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:scale-105'}
`}
>
{isListening ? (
<>
<div className="w-2 h-2 bg-white rounded-full animate-pulse"></div>
<MicOff className="w-5 h-5" />
<span>Stop Recording</span>
</>
) : (
<>
<Mic className="w-5 h-5" />
<span>Start Voice Recording</span>
<Volume2 className="w-4 h-4 opacity-70" />
</>
)}
</button>
{isListening && (
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700">
<div className="flex items-center gap-2 mb-2">
<div className="flex gap-1">
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" style={{ animationDelay: '0.1s' }}></div>
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
</div>
<span className="text-red-400 text-sm font-medium">Listening...</span>
</div>
<div className="text-gray-300 text-sm min-h-[1.5rem]">
{transcript || 'Speak now...'}
</div>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}

3
src/index.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

10
src/main.tsx Normal file
View file

@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);

View file

@ -0,0 +1,71 @@
const API_BASE_URL = 'http://localhost:3001/api';
export class ApiService {
private async makeRequest(endpoint: string, options: RequestInit = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`API request failed for ${endpoint}:`, error);
throw error;
}
}
async submitTicket(ticketData: {
requester: string;
email: string;
content: string;
}) {
return this.makeRequest('/submit-ticket', {
method: 'POST',
body: JSON.stringify(ticketData),
});
}
async getTickets() {
return this.makeRequest('/tickets');
}
async sendChatMessage(messages: Array<{ role: string; content: string }>) {
const response = await this.makeRequest('/chat', {
method: 'POST',
body: JSON.stringify({ messages }),
});
return response.response;
}
async createTicketFromChat(data: {
requester: string;
email: string;
conversation: string;
}) {
return this.makeRequest('/chat/create-ticket', {
method: 'POST',
body: JSON.stringify(data),
});
}
async getQuickResponse(issueType: string) {
return this.makeRequest('/chat/quick-response', {
method: 'POST',
body: JSON.stringify({ issue_type: issueType }),
});
}
}
export const apiService = new ApiService();

View file

@ -0,0 +1,43 @@
import { ChatMessage } from '../types';
import { apiService } from './apiService';
export class ChatService {
async sendMessage(messages: ChatMessage[]): Promise<string> {
try {
// Convert ChatMessage format to API format
const apiMessages = messages.map(msg => ({
role: msg.role,
content: msg.content
}));
const response = await apiService.sendChatMessage(apiMessages);
return response;
} catch (error) {
console.error('Chat service error:', error);
throw new Error('Failed to get response from chat service. Please try again or create a support ticket.');
}
}
async createTicketFromConversation(
requester: string,
email: string,
messages: ChatMessage[]
) {
try {
const conversation = messages
.map(msg => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`)
.join('\n\n');
const response = await apiService.createTicketFromChat({
requester,
email,
conversation
});
return response.ticket;
} catch (error) {
console.error('Error creating ticket from chat:', error);
throw error;
}
}
}

View file

@ -0,0 +1,96 @@
import { VoiceRecognitionResult } from '../types';
export class SpeechService {
private recognition: SpeechRecognition | null = null;
private isListening = false;
constructor() {
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
this.recognition = new SpeechRecognition();
this.recognition.continuous = true;
this.recognition.interimResults = true;
this.recognition.lang = 'en-US';
}
}
isSupported(): boolean {
return this.recognition !== null;
}
startListening(
onResult: (result: VoiceRecognitionResult) => void,
onError: (error: string) => void
): void {
if (!this.recognition || this.isListening) return;
this.recognition.onresult = (event) => {
let transcript = '';
let confidence = 0;
let isFinal = false;
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
transcript += result[0].transcript;
confidence = result[0].confidence;
isFinal = result.isFinal;
}
onResult({ transcript, confidence, isFinal });
};
this.recognition.onerror = (event) => {
onError(`Speech recognition error: ${event.error}`);
};
this.recognition.onend = () => {
this.isListening = false;
};
try {
this.recognition.start();
this.isListening = true;
} catch (error) {
onError('Failed to start speech recognition');
}
}
stopListening(): void {
if (this.recognition && this.isListening) {
this.recognition.stop();
this.isListening = false;
}
}
getIsListening(): boolean {
return this.isListening;
}
}
export class TextToSpeechService {
private synth: SpeechSynthesis;
constructor() {
this.synth = window.speechSynthesis;
}
speak(text: string, options: { rate?: number; pitch?: number; volume?: number } = {}): void {
if (!this.synth) return;
// Cancel any ongoing speech
this.synth.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.rate = options.rate || 1;
utterance.pitch = options.pitch || 1;
utterance.volume = options.volume || 1;
this.synth.speak(utterance);
}
stop(): void {
if (this.synth) {
this.synth.cancel();
}
}
}

View file

@ -0,0 +1,28 @@
import { Classification } from '../types';
import { apiService } from './apiService';
export class ITTicketClassifier {
async classifyTicket(ticketContent: string): Promise<Classification> {
try {
// The classification is now handled by the backend
// This method is kept for compatibility but won't be used directly
// since the backend handles both classification and ticket creation
return {
department: 'Processing...',
sub_department: 'Processing...',
issue_type: 'Processing...',
urgency: 'Medium',
summary: 'Processing ticket classification...'
};
} catch (error) {
console.error('Classification error:', error);
return {
department: 'Helpdesk',
sub_department: 'User Support',
issue_type: 'General Inquiry',
urgency: 'Low',
summary: 'General support request'
};
}
}
}

34
src/types/index.ts Normal file
View file

@ -0,0 +1,34 @@
export interface Ticket {
id: string;
requester: string;
email: string;
timestamp: string;
content: string;
status: 'Open' | 'In Progress' | 'Resolved' | 'Closed';
department: string;
sub_department: string;
issue_type: string;
urgency: 'Low' | 'Medium' | 'High' | 'Critical';
summary: string;
}
export interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: string;
}
export interface Classification {
department: string;
sub_department: string;
issue_type: string;
urgency: 'Low' | 'Medium' | 'High' | 'Critical';
summary: string;
}
export interface VoiceRecognitionResult {
transcript: string;
confidence: number;
isFinal: boolean;
}

43
src/types/speech.d.ts vendored Normal file
View file

@ -0,0 +1,43 @@
interface SpeechRecognition extends EventTarget {
continuous: boolean;
interimResults: boolean;
lang: string;
onresult: (event: SpeechRecognitionEvent) => void;
onerror: (event: SpeechRecognitionErrorEvent) => void;
onend: () => void;
start(): void;
stop(): void;
}
interface SpeechRecognitionEvent {
resultIndex: number;
results: SpeechRecognitionResultList;
}
interface SpeechRecognitionResultList {
length: number;
item(index: number): SpeechRecognitionResult;
[index: number]: SpeechRecognitionResult;
}
interface SpeechRecognitionResult {
length: number;
item(index: number): SpeechRecognitionAlternative;
[index: number]: SpeechRecognitionAlternative;
isFinal: boolean;
}
interface SpeechRecognitionAlternative {
transcript: string;
confidence: number;
}
interface SpeechRecognitionErrorEvent {
error: string;
message: string;
}
interface Window {
SpeechRecognition: typeof SpeechRecognition;
webkitSpeechRecognition: typeof SpeechRecognition;
}

1
src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

8
tailwind.config.js Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

24
tsconfig.app.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
tsconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

22
tsconfig.node.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

10
vite.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
optimizeDeps: {
exclude: ['lucide-react'],
},
});