🚀 UTAS Oman Portal Demo - Complete AI-Enhanced University Portal

 Features:
• Real AI chatbot with OpenRouter API (bilingual Arabic/English)
• Comprehensive UTAS Oman content with programs and admissions
• Modern responsive design with Tailwind CSS
• Student/Admin authentication and dashboards
• Full course catalog with search and filtering
• Contact forms and application processes

🤖 AI Capabilities:
• Language detection (Arabic/English)
• UTAS Oman knowledge base integration
• Contextual responses about programs, admissions, scholarships
• No mock data - all responses from real AI

🛠️ Technology Stack:
• Next.js 14 + React 19 + TypeScript
• OpenRouter API with Meta LLaMA 3.1
• Prisma ORM with SQLite
• Tailwind CSS for styling

📋 Demo Ready:
• Test users file included (TEST-USERS.md)
• Navigation test checklist (NAVIGATION-TEST.md)
• Clean codebase with proper gitignore
• Production-ready configuration
This commit is contained in:
Krikorios
2025-07-13 19:18:26 +04:00
parent f83b27db61
commit 0c22188feb
52 changed files with 6454 additions and 14454 deletions
-143
View File
@@ -1,143 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { OpenAI } from 'openai'
import { PrismaClient } from '@prisma/client'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
const prisma = new PrismaClient()
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File
const url = formData.get('url') as string
if (!file && !url) {
return NextResponse.json({ error: 'File or URL is required' }, { status: 400 })
}
let altText = ''
let wcagScore = 0
const issues: string[] = []
const suggestions: string[] = []
if (file) {
// Convert file to base64 for OpenAI Vision API
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const base64 = buffer.toString('base64')
// Generate alt text using OpenAI Vision
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: 'Generate concise, descriptive alt text for this image that would be useful for screen readers. Focus on the most important visual elements and context.',
},
{
type: 'image_url',
image_url: {
url: `data:${file.type};base64,${base64}`,
},
},
],
},
],
max_tokens: 100,
})
altText = response.choices[0]?.message?.content || 'Unable to generate alt text'
}
// Simulate WCAG compliance checking
if (!altText || altText.length < 10) {
issues.push('Missing or inadequate alt text')
suggestions.push('Add descriptive alt text for all images')
wcagScore = 0.3
} else if (altText.length > 125) {
issues.push('Alt text is too long')
suggestions.push('Keep alt text under 125 characters')
wcagScore = 0.7
} else {
wcagScore = 0.95
}
// Additional WCAG checks (simulated)
if (url) {
// Simulate checking color contrast, heading structure, etc.
const randomFactor = Math.random()
if (randomFactor < 0.3) {
issues.push('Low color contrast detected')
suggestions.push('Ensure color contrast ratio is at least 4.5:1')
wcagScore = Math.min(wcagScore, 0.6)
}
if (randomFactor < 0.2) {
issues.push('Missing heading structure')
suggestions.push('Use proper heading hierarchy (h1, h2, h3, etc.)')
wcagScore = Math.min(wcagScore, 0.5)
}
}
// Save audit to database
const audit = await prisma.accessibilityAudit.create({
data: {
url: url || 'uploaded-image',
imagePath: file ? file.name : null,
altText,
wcagScore,
issues: JSON.stringify(issues),
suggestions: JSON.stringify(suggestions),
},
})
return NextResponse.json({
success: true,
audit: {
id: audit.id,
altText,
wcagScore,
issues,
suggestions,
},
})
} catch (error) {
console.error('Error in accessibility API:', error)
return NextResponse.json(
{ error: 'Failed to process accessibility audit' },
{ status: 500 }
)
}
}
export async function GET() {
try {
const audits = await prisma.accessibilityAudit.findMany({
orderBy: {
createdAt: 'desc',
},
take: 50,
})
const avgScore = audits.length > 0
? audits.reduce((sum: number, audit: { wcagScore: number | null }) =>
sum + (audit.wcagScore || 0), 0) / audits.length
: 0
return NextResponse.json({
audits,
statistics: {
totalAudits: audits.length,
averageScore: Math.round(avgScore * 100) / 100,
},
})
} catch (error) {
console.error('Error fetching accessibility audits:', error)
return NextResponse.json({ error: 'Failed to fetch audits' }, { status: 500 })
}
}
+61 -100
View File
@@ -1,109 +1,70 @@
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import UTASChatBot from '@/lib/chatbot';
// Mock chat responses
const mockResponses = [
{
trigger: ['library', 'hours', 'open'],
response: {
en: 'The library is open Monday-Friday 8:00 AM - 10:00 PM, Saturday 9:00 AM - 6:00 PM, and Sunday 12:00 PM - 8:00 PM. During exam periods, we have extended hours until midnight.',
ar: 'المكتبة مفتوحة من الاثنين إلى الجمعة من 8:00 صباحاً حتى 10:00 مساءً، يوم السبت من 9:00 صباحاً حتى 6:00 مساءً، والأحد من 12:00 ظهراً حتى 8:00 مساءً. خلال فترات الامتحانات، لدينا ساعات ممتدة حتى منتصف الليل.'
}
},
{
trigger: ['password', 'change', 'reset'],
response: {
en: 'To change your password, go to Settings > Account > Change Password. You can also reset it using the "Forgot Password" link on the login page.',
ar: 'لتغيير كلمة المرور الخاصة بك، اذهب إلى الإعدادات > الحساب > تغيير كلمة المرور. يمكنك أيضاً إعادة تعيينها باستخدام رابط "نسيت كلمة المرور" في صفحة تسجيل الدخول.'
}
},
{
trigger: ['registration', 'semester', 'enroll'],
response: {
en: 'Registration for the next semester opens on January 15th for continuing students and February 1st for new students. Please check your academic calendar for specific dates.',
ar: 'التسجيل للفصل الدراسي القادم يفتح في 15 يناير للطلاب المستمرين و 1 فبراير للطلاب الجدد. يرجى مراجعة التقويم الأكاديمي للتواريخ المحددة.'
}
},
{
trigger: ['help', 'support', 'contact'],
response: {
en: 'For academic support, contact Student Services at support@university.edu or call (555) 123-4567. For technical issues, email IT help desk at it@university.edu.',
ar: 'للدعم الأكاديمي، اتصل بخدمات الطلاب على support@university.edu أو اتصل بالرقم (555) 123-4567. للمشاكل التقنية، راسل مكتب المساعدة التقنية على it@university.edu.'
}
}
];
// Mental health keywords that trigger escalation
const mentalHealthKeywords = [
'depressed', 'depression', 'anxiety', 'anxious', 'stressed', 'stress',
'overwhelmed', 'suicide', 'self-harm', 'hurt myself', 'kill myself',
'hopeless', 'worthless', 'sad', 'crying', 'panic', 'fear',
'مكتئب', 'اكتئاب', 'قلق', 'قلقان', 'متوتر', 'توتر',
'مرهق', 'انتحار', 'إيذاء النفس', 'أؤذي نفسي', 'أقتل نفسي',
'يائس', 'عديم القيمة', 'حزين', 'بكاء', 'هلع', 'خوف'
];
function findBestResponse(message: string, language: string = 'en') {
const lowerMessage = message.toLowerCase();
// Check for mental health keywords first
const hasMentalHealthKeyword = mentalHealthKeywords.some(keyword =>
lowerMessage.includes(keyword.toLowerCase())
);
if (hasMentalHealthKeyword) {
return {
response: language === 'ar'
? 'أفهم أنك تمر بوقت صعب. من المهم أن تطلب المساعدة من المختصين. يمكنك التواصل مع خدمة الاستشارة الجامعية على الرقم (555) 123-4567 أو زيارة مركز الصحة النفسية في الحرم الجامعي. في حالات الطوارئ، اتصل بالرقم 911 أو خط المساعدة الوطني للأزمات النفسية.'
: 'I understand you\'re going through a difficult time. It\'s important to seek help from professionals. You can contact the university counseling service at (555) 123-4567 or visit the mental health center on campus. In emergencies, call 911 or the National Crisis Helpline.',
escalate: true,
category: 'mental_health'
};
}
// Look for FAQ matches
for (const faq of mockResponses) {
const hasMatch = faq.trigger.some(trigger =>
lowerMessage.includes(trigger.toLowerCase())
);
if (hasMatch) {
return {
response: faq.response[language as keyof typeof faq.response],
escalate: false,
category: 'faq'
};
}
}
// Default response
return {
response: language === 'ar'
? 'شكراً لك على سؤالك. يمكنني مساعدتك في العثور على المعلومات التي تحتاجها. جرب أن تسأل عن ساعات المكتبة، أو تغيير كلمة المرور، أو التسجيل للفصل الدراسي.'
: 'Thank you for your question. I can help you find the information you need. Try asking about library hours, changing your password, or semester registration.',
escalate: false,
category: 'general'
};
}
export async function POST(request: Request) {
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { message, language = 'en' } = body;
const {
message,
mode = 'general',
history = [],
conversationHistory = [],
systemPrompt = null,
language = 'en'
} = await request.json();
if (!message) {
return NextResponse.json({ error: 'Message is required' }, { status: 400 });
if (!message || typeof message !== 'string') {
return NextResponse.json({
error: 'Message is required and must be a string'
}, { status: 400 });
}
const result = findBestResponse(message, language);
// Initialize the chatbot with API key from environment
const apiKey = process.env.OPENROUTER_API_KEY || '';
const chatbot = new UTASChatBot(apiKey);
// Generate AI response using OpenRouter
const response = await chatbot.generateResponse(message);
// Log the interaction for demo purposes
console.log('UTAS Chat:', {
timestamp: new Date().toISOString(),
message: message.substring(0, 100),
response: response.substring(0, 100)
});
return NextResponse.json({
message: response,
response: response, // for backward compatibility
timestamp: new Date().toISOString(),
source: 'UTAS AI Assistant'
});
} catch (error) {
console.error('Chat API Error:', error);
return NextResponse.json({
response: result.response,
escalate: result.escalate,
category: result.category,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('Error processing chat:', error);
return NextResponse.json({ error: 'Failed to process message' }, { status: 500 });
message: "I apologize, but I'm experiencing technical difficulties. Please try again or contact UTAS directly at +61 3 6226 6200 or info@utas.edu.au for immediate assistance.",
timestamp: new Date().toISOString(),
source: 'UTAS AI Assistant',
error: true
}, { status: 500 });
}
}
export async function GET() {
return NextResponse.json({
service: 'UTAS AI Chat Assistant',
status: 'active',
features: [
'RAG-powered responses using UTAS knowledge base',
'OpenRouter AI integration (when API key provided)',
'Real-time course and program information',
'Application guidance and support',
'Campus and research information'
],
endpoints: {
POST: 'Send message and conversation history',
GET: 'Service status and information'
}
});
}
-104
View File
@@ -1,104 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { OpenAI } from 'openai'
import { PrismaClient } from '@prisma/client'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
const prisma = new PrismaClient()
type Message = {
sender: 'user' | 'bot'
text: string
timestamp: Date
}
export async function POST(request: NextRequest) {
try {
const { message, mode = 'general', history = [] } = await request.json()
// Get FAQs from database
const faqs = await prisma.fAQ.findMany({
where: {
language: 'en', // For demo, we'll use English FAQs
},
orderBy: {
priority: 'asc',
},
})
// Create context from FAQs
const faqContext = faqs.map((faq: { question: string; answer: string }) =>
`Q: ${faq.question}\nA: ${faq.answer}`
).join('\n\n')
// Check for mental health indicators if in mental health mode
let shouldEscalate = false
if (mode === 'mental_health') {
// Simple trigger phrase detection
const triggerPhrases = [
'anxious', 'anxiety', 'depressed', 'depression', 'stressed', 'stress',
'overwhelmed', 'panic', 'worried', 'fear', 'scared', 'sad', 'hopeless',
'suicide', 'self-harm', 'hurt myself', 'end it all', 'giving up'
]
const lowerMessage = message.toLowerCase()
shouldEscalate = triggerPhrases.some(phrase => lowerMessage.includes(phrase))
}
// Prepare system message based on mode
let systemMessage = ''
if (mode === 'mental_health') {
systemMessage = `You are a compassionate mental health support assistant for a university.
Provide empathetic, supportive responses. If the user mentions serious mental health concerns,
gently encourage them to speak with a professional counselor. Keep responses warm and understanding.`
} else {
systemMessage = `You are a helpful university assistant. Answer questions based on the following FAQ database:
${faqContext}
If you cannot find the answer in the FAQs, provide a helpful general response and suggest contacting
the appropriate university department. Keep responses concise and helpful.`
}
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: systemMessage },
...history.map((msg: Message) => ({
role: msg.sender === 'user' ? 'user' as const : 'assistant' as const,
content: msg.text,
})),
{ role: 'user', content: message },
],
max_tokens: 500,
temperature: 0.7,
})
const response = completion.choices[0]?.message?.content || 'I apologize, but I cannot provide a response at this time.'
// Log the chat session
await prisma.chatSession.create({
data: {
type: mode === 'mental_health' ? 'MENTAL_HEALTH' : 'GENERAL',
messages: JSON.stringify([
...history,
{ sender: 'user', text: message, timestamp: new Date() },
{ sender: 'bot', text: response, timestamp: new Date() },
]),
},
})
return NextResponse.json({
response,
shouldEscalate,
})
} catch (error) {
console.error('Error in chat API:', error)
return NextResponse.json(
{ error: 'Failed to process chat message' },
{ status: 500 }
)
}
}
+17 -24
View File
@@ -1,17 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import UTASChatBot from '@/lib/chatbot';
// Initialize the chatbot (will use OpenRouter if API key is available)
const chatbot = new UTASChatBot();
export async function POST(request: NextRequest) {
try {
const {
message,
conversationHistory = [],
systemPrompt = null,
language = 'en'
} = await request.json();
const { message } = await request.json();
if (!message || typeof message !== 'string') {
return NextResponse.json({
@@ -19,13 +11,12 @@ export async function POST(request: NextRequest) {
}, { status: 400 });
}
// Generate AI response using RAG and potentially OpenRouter with role-based context
const response = await chatbot.generateResponse(
message,
conversationHistory,
systemPrompt,
language
);
// Initialize the chatbot with API key from environment
const apiKey = process.env.OPENROUTER_API_KEY || '';
const chatbot = new UTASChatBot(apiKey);
// Generate AI response using OpenRouter
const response = await chatbot.generateResponse(message);
// Log the interaction for demo purposes
console.log('UTAS Chat:', {
@@ -34,19 +25,22 @@ export async function POST(request: NextRequest) {
response: response.substring(0, 100)
});
// Return both message and response for backward compatibility
return NextResponse.json({
message: response,
response: response,
timestamp: new Date().toISOString(),
source: 'UTAS AI Assistant'
source: 'UTAS Oman AI Assistant'
});
} catch (error) {
console.error('Chat API Error:', error);
return NextResponse.json({
message: "I apologize, but I'm experiencing technical difficulties. Please try again or contact UTAS directly at +61 3 6226 6200 or info@utas.edu.au for immediate assistance.",
message: "I apologize, but I'm experiencing technical difficulties. Please try again or contact UTAS Oman directly at +968 2414 3555 or admissions@utas.edu.om for immediate assistance.",
response: "I apologize, but I'm experiencing technical difficulties. Please try again or contact UTAS Oman directly at +968 2414 3555 or admissions@utas.edu.om for immediate assistance.",
timestamp: new Date().toISOString(),
source: 'UTAS AI Assistant',
source: 'UTAS Oman AI Assistant',
error: true
}, { status: 500 });
}
@@ -54,17 +48,16 @@ export async function POST(request: NextRequest) {
export async function GET() {
return NextResponse.json({
service: 'UTAS AI Chat Assistant',
service: 'UTAS Oman AI Chat Assistant',
status: 'active',
features: [
'RAG-powered responses using UTAS knowledge base',
'OpenRouter AI integration (when API key provided)',
'OpenRouter AI integration with multilingual support',
'Real-time course and program information',
'Application guidance and support',
'Campus and research information'
'UTAS Oman specific information'
],
endpoints: {
POST: 'Send message and conversation history',
POST: 'Send message for AI response',
GET: 'Service status and information'
}
});
View File
-58
View File
@@ -1,58 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function POST(request: NextRequest) {
try {
const { rating, type, feedback, sessionId, userId } = await request.json()
const survey = await prisma.survey.create({
data: {
rating,
type,
feedback,
sessionId,
userId,
},
})
return NextResponse.json({ success: true, survey })
} catch (error) {
console.error('Error creating survey:', error)
return NextResponse.json({ error: 'Failed to submit survey' }, { status: 500 })
}
}
export async function GET() {
try {
const surveys = await prisma.survey.findMany({
orderBy: {
createdAt: 'desc',
},
take: 100,
})
// Calculate statistics
const totalSurveys = surveys.length
const averageRating = totalSurveys > 0
? surveys.reduce((sum: number, survey: { rating: number }) => sum + survey.rating, 0) / totalSurveys
: 0
const ratingDistribution = surveys.reduce((acc: Record<number, number>, survey: { rating: number }) => {
acc[survey.rating] = (acc[survey.rating] || 0) + 1
return acc
}, {} as Record<number, number>)
return NextResponse.json({
surveys,
statistics: {
totalSurveys,
averageRating: Math.round(averageRating * 10) / 10,
ratingDistribution,
},
})
} catch (error) {
console.error('Error fetching surveys:', error)
return NextResponse.json({ error: 'Failed to fetch surveys' }, { status: 500 })
}
}
@@ -1,36 +0,0 @@
import { NextRequest, NextResponse } from 'next/server'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const email = searchParams.get('email')
if (!email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 })
}
const user = await prisma.user.findUnique({
where: { email },
include: {
enrollments: {
include: {
course: true,
},
},
advisor: true,
},
})
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
return NextResponse.json(user)
} catch (error) {
console.error('Error fetching user profile:', error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}