Skip to content
Merged
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
53 changes: 52 additions & 1 deletion app/ai/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,40 @@ import { useRouter } from 'next/navigation';
import Link from 'next/link';
import ReactMarkdown from 'react-markdown';

// Action button types
type ActionType =
| 'event_register'
| 'event_view'
| 'hackathon_view'
| 'hackathon_register'
| 'internship_apply'
| 'blog_read'
| 'learn_more';

interface ActionButton {
type: ActionType;
label: string;
url: string;
metadata?: Record<string, unknown>;
variant?: 'primary' | 'secondary';
}

interface Message {
id: string;
text: string;
sender: 'user' | 'ai';
timestamp: Date;
context?: string;
isTyping?: boolean;
actions?: ActionButton[];
}

interface AIResponse {
success: boolean;
response: string;
context: string;
timestamp: string;
actions?: ActionButton[];
error?: string;
}

Expand Down Expand Up @@ -251,6 +271,16 @@ export default function AIPage() {
const parsed = JSON.parse(data);

if (parsed.done) {
// Stream complete - attach actions if provided
if (parsed.actions && parsed.actions.length > 0) {
setMessages(prev =>
prev.map(msg =>
msg.id === streamingId
? { ...msg, actions: parsed.actions }
: msg
)
);
}
break;
}

Expand Down Expand Up @@ -288,7 +318,8 @@ export default function AIPage() {
text: data.response,
sender: 'ai',
timestamp: new Date(),
context: data.context
context: data.context,
actions: data.actions
};
setMessages(prev => [...prev, aiMessage]);
} else {
Expand Down Expand Up @@ -491,6 +522,26 @@ export default function AIPage() {
<p className="text-sm sm:text-base leading-relaxed whitespace-pre-wrap">{message.text}</p>
)}

{/* Action Buttons */}
{message.actions && message.actions.length > 0 && message.sender === 'ai' && (
<div className="flex flex-wrap gap-2 sm:gap-3 mt-4 pt-4 border-t border-gray-700/50">
{message.actions.map((action, index) => (
<Button
key={index}
variant={action.variant === 'primary' ? 'default' : 'outline'}
size="sm"
className={action.variant === 'primary'
? 'bg-blue-600 hover:bg-blue-700 text-white text-xs sm:text-sm'
: 'border-gray-600 hover:bg-gray-800 text-gray-300 text-xs sm:text-sm'
}
onClick={() => router.push(action.url)}
>
{action.label}
</Button>
))}
</div>
)}

<div className="flex items-center justify-between mt-2 sm:mt-3 gap-2 sm:gap-3">
<span className={`text-xs ${message.sender === 'user' ? 'text-gray-300' : 'text-gray-500'
}`}>
Expand Down
164 changes: 140 additions & 24 deletions app/api/ai/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@ import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@supabase/supabase-js';
import { createClient as createServerClient } from '@/lib/supabase/server';

// Action button types
type ActionType =
| 'event_register'
| 'event_view'
| 'hackathon_view'
| 'hackathon_register'
| 'internship_apply'
| 'blog_read'
| 'learn_more';

interface ActionButton {
type: ActionType;
label: string;
url: string;
metadata?: Record<string, unknown>;
variant?: 'primary' | 'secondary';
}


// Rate limiting map (in production, use Redis or database)
const rateLimit = new Map();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
Expand Down Expand Up @@ -243,6 +262,7 @@ interface PlatformStats {

interface Event {
id: string;
slug?: string; // Optional for backwards compatibility
title: string;
description: string;
excerpt?: string;
Expand All @@ -265,6 +285,7 @@ interface Event {

interface Hackathon {
id: string;
slug?: string; // Optional for backwards compatibility
title: string;
description: string;
excerpt?: string;
Expand Down Expand Up @@ -331,7 +352,7 @@ async function getEvents(limit = 10) {
const { data, error } = await supabase
.from('events')
.select(`
id, title, description, excerpt, date, time, duration,
id, slug, title, description, excerpt, date, time, duration,
location, locations, status, event_type, registration_deadline,
capacity, registered, category, categories, tags, price, organizer
`)
Expand All @@ -353,7 +374,7 @@ async function getHackathons(limit = 10) {
const { data, error } = await supabase
.from('hackathons')
.select(`
id, title, description, excerpt, date, time, duration,
id, slug, title, description, excerpt, date, time, duration,
registration_deadline, status, location, locations,
capacity, registered, category, categories, tags,
price, organizer, prize, prize_details, team_size
Expand Down Expand Up @@ -566,6 +587,89 @@ async function getContextualData(userMessage: string, context: string): Promise<
}
}

// Detect actionable buttons based on AI response and context
function detectActions(
userMessage: string,
aiResponse: string,
contextData: ContextData
): ActionButton[] {
const actions: ActionButton[] = [];
const lowerResponse = aiResponse.toLowerCase();
const lowerMessage = userMessage.toLowerCase();

// Detect if user is asking about availability/details (not just browsing)
const isSeekingAction = lowerMessage.includes('available') ||
lowerMessage.includes('register') ||
lowerMessage.includes('join') ||
lowerMessage.includes('sign up') ||
lowerMessage.includes('happening') ||
lowerMessage.includes('upcoming') ||
lowerMessage.includes('what events') ||
lowerMessage.includes('show me') ||
lowerMessage.includes('tell me about');

console.log('🔍 detectActions called:', {
userMessage,
isSeekingAction,
hasEvents: !!contextData.events,
eventCount: contextData.events?.length || 0,
aiResponsePreview: aiResponse.substring(0, 100)
});

// Detect mentioned events
if (contextData.events && contextData.events.length > 0) {
contextData.events.forEach((event: Event) => {
// Check if event is mentioned in the response
const eventMentioned = lowerResponse.includes(event.title.toLowerCase());

console.log('🎯 Checking event:', {
eventTitle: event.title,
eventTitleLower: event.title.toLowerCase(),
eventMentioned,
isSeekingAction
});

if (eventMentioned && isSeekingAction) {
// Only add view button - registration happens on the event page itself
// Use slug if available, otherwise fall back to ID
const eventUrl = event.slug ? `/events/${event.slug}` : `/events/${event.id}`;
actions.push({
type: 'event_view',
label: `View ${event.title}`,
url: eventUrl,
metadata: { eventId: event.id, eventSlug: event.slug, eventTitle: event.title },
variant: 'primary' // Make it primary since it's the main action
});
}
});
}

// Detect mentioned hackathons
if (contextData.hackathons && contextData.hackathons.length > 0) {
contextData.hackathons.forEach((hackathon: Hackathon) => {
const hackathonMentioned = lowerResponse.includes(hackathon.title.toLowerCase());

if (hackathonMentioned && isSeekingAction) {
// Only add view button - same as events
// Use slug if available, otherwise fall back to ID
const hackathonUrl = hackathon.slug ? `/hackathons/${hackathon.slug}` : `/hackathons/${hackathon.id}`;
actions.push({
type: 'hackathon_view',
label: `View ${hackathon.title}`,
url: hackathonUrl,
metadata: { hackathonId: hackathon.id, hackathonSlug: hackathon.slug, hackathonTitle: hackathon.title },
variant: 'primary'
});
}
});
}

// Limit to max 4 actions to avoid overwhelming UI
const finalActions = actions.slice(0, 4);
console.log('✅ detectActions returning:', finalActions.length, 'actions', finalActions);
return finalActions;
}
Comment on lines +590 to +671
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug logging and consider title matching edge cases.

Several observations:

  1. Debug console.log statements (lines 611-617, 625-630, 669) should be removed or converted to conditional debug logging for production.

  2. Title substring matching may produce false positives. For example, if an event is titled "AI", it would match any response containing "ai" anywhere. Consider more precise matching (word boundaries or normalized comparison).

  3. Long titles in button labels (View ${event.title}) could overflow UI. Consider truncating.

 function detectActions(
   userMessage: string,
   aiResponse: string,
   contextData: ContextData
 ): ActionButton[] {
   const actions: ActionButton[] = [];
   const lowerResponse = aiResponse.toLowerCase();
   const lowerMessage = userMessage.toLowerCase();

   const isSeekingAction = lowerMessage.includes('available') ||
     lowerMessage.includes('register') ||
     // ... other checks

-  console.log('🔍 detectActions called:', {
-    userMessage,
-    isSeekingAction,
-    hasEvents: !!contextData.events,
-    eventCount: contextData.events?.length || 0,
-    aiResponsePreview: aiResponse.substring(0, 100)
-  });

   if (contextData.events && contextData.events.length > 0) {
     contextData.events.forEach((event: Event) => {
-      const eventMentioned = lowerResponse.includes(event.title.toLowerCase());
+      // Use word boundary matching to avoid false positives
+      const titlePattern = new RegExp(`\\b${event.title.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
+      const eventMentioned = titlePattern.test(aiResponse);

-      console.log('🎯 Checking event:', {
-        eventTitle: event.title,
-        eventTitleLower: event.title.toLowerCase(),
-        eventMentioned,
-        isSeekingAction
-      });

       if (eventMentioned && isSeekingAction) {
         const eventUrl = event.slug ? `/events/${event.slug}` : `/events/${event.id}`;
+        const truncatedTitle = event.title.length > 30 
+          ? event.title.substring(0, 27) + '...' 
+          : event.title;
         actions.push({
           type: 'event_view',
-          label: `View ${event.title}`,
+          label: `View ${truncatedTitle}`,
           url: eventUrl,
           // ...
         });
       }
     });
   }
   // ... similar for hackathons

-  console.log('✅ detectActions returning:', finalActions.length, 'actions', finalActions);
   return finalActions;
 }

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


function buildPrompt(userMessage: string, contextData: ContextData, context: string) {
const message = userMessage.toLowerCase().trim();

Expand All @@ -589,30 +693,30 @@ function buildPrompt(userMessage: string, contextData: ContextData, context: str
if (isDirectInternshipQuery) {
return `🚨 MANDATORY INTERNSHIP RESPONSE 🚨

You MUST respond with this exact structure for ANY internship-related query:
You MUST respond with this exact structure for ANY internship- related query:

"Yes! Codeunia runs its own comprehensive internship programs:
"Yes! Codeunia runs its own comprehensive internship programs:

🆓 **Codeunia Starter Internship (FREE)**:
- Perfect for beginners and intermediate learners
- Real tasks with mentor check-ins
- Certificate upon completion
- Community access and weekly standups
- Resume and GitHub review
🆓 ** Codeunia Starter Internship(FREE) **:
- Perfect for beginners and intermediate learners
- Real tasks with mentor check - ins
- Certificate upon completion
- Community access and weekly standups
- Resume and GitHub review

💰 **Codeunia Pro Internship (₹4999)**:
- For intermediate and advanced developers
- Production-grade projects with weekly reviews
- 1:1 mentor sessions
- Letter of recommendation
- Premium certificate and LinkedIn assets
- Priority career guidance
💰 ** Codeunia Pro Internship(₹4999) **:
- For intermediate and advanced developers
- Production - grade projects with weekly reviews
- 1: 1 mentor sessions
- Letter of recommendation
- Premium certificate and LinkedIn assets
- Priority career guidance

Both programs are run BY Codeunia WITH Codeunia mentors ON Codeunia projects!

These are Codeunia's own internship programs - we're not just a platform that connects you to external companies. We run comprehensive, hands-on internship programs internally with dedicated mentorship and real projects.
These are Codeunia's own internship programs - we're not just a platform that connects you to external companies.We run comprehensive, hands - on internship programs internally with dedicated mentorship and real projects.

Would you like more details about either program or help choosing which one is right for you?"
Would you like more details about either program or help choosing which one is right for you ? "

❌ DO NOT suggest external programs
❌ DO NOT say you don't have information
Expand Down Expand Up @@ -642,11 +746,11 @@ Would you like more details about either program or help choosing which one is r
message.includes('explain');

if (isSimpleGreeting) {
return `You are Codeunia AI Assistant. The user just said "${userMessage}".
return `You are Codeunia AI Assistant.The user just said "${userMessage}".

Respond with a brief, friendly greeting (2-3 sentences max) and ask how you can help them with Codeunia's events, hackathons, or opportunities.
Respond with a brief, friendly greeting(2 - 3 sentences max) and ask how you can help them with Codeunia's events, hackathons, or opportunities.

Keep it short, welcoming, and conversational. Don't provide detailed information unless specifically asked.`;
Keep it short, welcoming, and conversational.Don't provide detailed information unless specifically asked.`;
}

if (isGeneralQuestion) {
Expand Down Expand Up @@ -967,8 +1071,15 @@ export async function POST(request: NextRequest) {
const { done, value } = await reader.read();

if (done) {
// Send completion event
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
// Detect actions after full response is accumulated
const detectedActions = detectActions(message, fullResponse, contextData);
console.log('🎯 Streaming complete, detected actions:', detectedActions);

// Send completion event with actions
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
done: true,
actions: detectedActions
})}\n\n`));
Comment on lines 1073 to +1082
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug logging in streaming completion.

The action detection integration is correct, but the console.log on line 1076 should be removed for production.

 // Detect actions after full response is accumulated
 const detectedActions = detectActions(message, fullResponse, contextData);
-console.log('🎯 Streaming complete, detected actions:', detectedActions);

 // Send completion event with actions
 controller.enqueue(encoder.encode(`data: ${JSON.stringify({
   done: true,
   actions: detectedActions
 })}\n\n`));
📝 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
if (done) {
// Send completion event
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
// Detect actions after full response is accumulated
const detectedActions = detectActions(message, fullResponse, contextData);
console.log('🎯 Streaming complete, detected actions:', detectedActions);
// Send completion event with actions
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
done: true,
actions: detectedActions
})}\n\n`));
if (done) {
// Detect actions after full response is accumulated
const detectedActions = detectActions(message, fullResponse, contextData);
// Send completion event with actions
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
done: true,
actions: detectedActions
})}\n\n`));
🤖 Prompt for AI Agents
In app/api/ai/route.ts around lines 1073 to 1082, remove the development
console.log on line 1076 that prints the detected actions after streaming
completes; instead either drop the log entirely or replace it with a structured
logger call (e.g. processLogger.debug or similar) if you need trace-level output
in prod. Ensure no leftover console.* calls remain in this completion branch and
run tests/linting to confirm no unused variables are introduced when the log is
removed.


// Save to database after stream completes
try {
Expand Down Expand Up @@ -1076,10 +1187,15 @@ export async function POST(request: NextRequest) {
console.error('Error saving to database:', dbSaveError);
}

// Detect actions for non-streaming mode
const detectedActions = detectActions(message, aiResponse, contextData);
console.log('🎯 Non-streaming complete, detected actions:', detectedActions);

return NextResponse.json({
success: true,
response: aiResponse,
context: finalContext,
actions: detectedActions,
Comment on lines +1190 to +1198
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove debug logging in non-streaming response.

Same as streaming mode — remove the debug console.log on line 1192.

 // Detect actions for non-streaming mode
 const detectedActions = detectActions(message, aiResponse, contextData);
-console.log('🎯 Non-streaming complete, detected actions:', detectedActions);

 return NextResponse.json({
   success: true,
   response: aiResponse,
   context: finalContext,
   actions: detectedActions,
   timestamp: new Date().toISOString()
 });
📝 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
// Detect actions for non-streaming mode
const detectedActions = detectActions(message, aiResponse, contextData);
console.log('🎯 Non-streaming complete, detected actions:', detectedActions);
return NextResponse.json({
success: true,
response: aiResponse,
context: finalContext,
actions: detectedActions,
// Detect actions for non-streaming mode
const detectedActions = detectActions(message, aiResponse, contextData);
return NextResponse.json({
success: true,
response: aiResponse,
context: finalContext,
actions: detectedActions,
🤖 Prompt for AI Agents
In app/api/ai/route.ts around lines 1190 to 1198, remove the debug console.log
call on line 1192 that prints "🎯 Non-streaming complete, detected actions:" and
its argument; simply delete that line (or replace with a proper structured
logger call at debug level if persistent logging is desired) so the
non-streaming response no longer emits debug output.

timestamp: new Date().toISOString()
});
}
Expand Down
Loading
Loading