Skip to content

Conversation

@akshay0611
Copy link
Collaborator

@akshay0611 akshay0611 commented Nov 26, 2025

This PR introduces interactive action buttons within Unio AI responses, enabling users to take direct actions (view events, hackathons, etc.) without leaving the chat context.
The feature includes full backend detection logic and frontend rendering support, covering both streaming and non-streaming AI response modes.


✨ Key Features Implemented

1. Backend — Smart Action Detection (/app/api/ai/route.ts)

  • Added ActionType and ActionButton type definitions.

  • Implemented rule-based intent detection:

    • Detects when the user is seeking actions (e.g., “available”, “register”, “happening”).
  • Context-aware matching:

    • Matches event/hackathon names mentioned by the AI against database entries.
  • Date-aware logic:

    • Upcoming events → primary “View Event” button
    • Past events → view-only
  • Integrated actions into both:

    • Streaming SSE final event
    • Non-streaming JSON response

2. Frontend — Button Rendering

Files Updated:

  • components/ai/AIChat.tsx
  • app/ai/page.tsx

Key Additions:

  • Extended Message and AIResponse interfaces with actions?: ActionButton[].

  • UI rendering block for AI-generated action buttons.

  • Clean, responsive layout:

    • Primary variant (blue)
    • Secondary variant (outline)
  • Integrated button navigation via Next.js router.

  • Works across both the chat widget and full AI page.


3. UX Improvement — Single CTA per Event

After testing, replaced dual-button design (View + Register) with a single, primary “View Event” button for cleaner UX.

Rationale:

  • Registration happens on the event details page.
  • Avoids confusion and reduces UI noise.
  • Ensures a consistent, predictable CTA for all events.

🧪 Testing Scenarios Covered

  • Event queries (upcoming & past)
  • Hackathon queries
  • General non-actionable queries
  • Multi-event responses
  • Streaming and non-streaming flows
  • Mobile responsiveness

All scenarios behave as expected.

Authored by: @akshay0611

Summary by CodeRabbit

  • New Features
    • AI responses now include contextual action buttons for quick access to relevant events, hackathons, internship opportunities, and blog content based on conversation context.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Nov 26, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
codeunia Ready Ready Preview Comment Nov 26, 2025 3:46am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 26, 2025

Walkthrough

The changes introduce an action button system for AI responses across the frontend and backend, enabling the AI to suggest contextual interactive actions (event registration, hackathon viewing, etc.). Slug fields are added to Event and Hackathon models for URL-friendly identifiers, and action detection logic is implemented server-side to identify relevant actions from AI responses, then propagated and rendered as interactive buttons in the chat UI.

Changes

Cohort / File(s) Summary
Action System Infrastructure
app/api/ai/route.ts, components/ai/AIChat.tsx, app/ai/page.tsx
Introduces ActionType union and ActionButton interface across layers to represent interactive UI buttons with type, label, URL, optional metadata, and style variants (primary/secondary).
API Route & Data Models
app/api/ai/route.ts
Adds slug fields to Event and Hackathon models for backwards-compatible URL generation; implements detectActions() function to identify and build action buttons from AI responses; integrates action detection into streaming and non-streaming response flows; updates prompt generation to reference new action context.
Chat Component
components/ai/AIChat.tsx
Propagates actions through Message and AIResponse interfaces; attaches detected actions to streaming messages on completion; includes actions from non-streaming responses; renders action buttons conditionally under AI messages with primary/secondary styling and navigation.
Page Component
app/ai/page.tsx
Extends Message and AIResponse structures with optional actions array; maintains existing UI flow while enabling action button rendering beneath AI messages.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as AIChat Component
    participant API as API Route
    participant AI as LLM/Prompt Engine
    
    User->>Frontend: Sends message
    Frontend->>API: POST /ai with userMessage
    
    alt Streaming Response
        API->>AI: Generate streaming response
        AI-->>API: Stream tokens + final response
        API->>API: detectActions(userMessage, aiResponse, context)
        API-->>Frontend: Stream completion with actions
        Frontend->>Frontend: Attach actions to streaming message
        Frontend-->>User: Render message + action buttons
    else Non-Streaming Response
        API->>AI: Generate full response
        AI-->>API: Complete response
        API->>API: detectActions(userMessage, aiResponse, context)
        API-->>Frontend: Return response + actions
        Frontend->>Frontend: Create message with actions
        Frontend-->>User: Render message + action buttons
    end
    
    User->>Frontend: Click action button
    Frontend->>Frontend: router.push(action.url)
    Frontend-->>User: Navigate to action destination
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • detectActions() logic in app/api/ai/route.ts: Conditional action detection based on message content, response mentions, and context data requires careful verification of business logic and edge cases.
  • Streaming completion handling in components/ai/AIChat.tsx: Modifications to streaming state management and message attachment timing need validation to ensure actions are properly propagated.
  • Type consistency and data flow: ActionButton interface propagates across three files; verify type alignment and optional field handling throughout the stack.
  • Slug field addition: Backwards compatibility for Event and Hackathon models should be verified to ensure graceful handling of missing slug values.

Possibly related PRs

Poem

🐰 With whiskers twitching, I've crafted a treat,
AI's suggestions are now oh-so neat!
Buttons that dance where the responses flow,
Click, navigate, watch your journey grow! 🚀

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding interactive action buttons to Unio AI responses with both backend and frontend integration, which is the core focus of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/unio

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/api/ai/route.ts (1)

693-723: Fix template string formatting issues.

The internship prompt template has inconsistent whitespace and broken formatting that will produce poorly formatted AI responses:

  • Line 696: "internship- related" has an errant space
  • Lines 700-714: Indentation with spaces inside template literal creates unwanted whitespace in the prompt
  • Line 717: "BY Codeunia WITH Codeunia mentors ON Codeunia projects!" — spacing looks intentional but verify
  • Line 719: Similar space issues
   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
♻️ Duplicate comments (3)
components/ai/AIChat.tsx (2)

16-32: Duplicate type definitions — same refactor applies here.

As noted for app/ai/page.tsx, these types should be extracted to a shared module.


518-537: Same key and URL handling recommendations apply.

This widget rendering has the same patterns as the AI page. Consider applying the same improvements for key uniqueness and URL handling.

app/api/ai/route.ts (1)

5-21: Server-side type definitions should be the single source of truth.

These types are duplicated on the frontend. If you extract to a shared types module, the backend can also import from there (since this is a Next.js app with shared code), ensuring consistency.

🧹 Nitpick comments (2)
app/ai/page.tsx (2)

15-31: Consider extracting shared types to a common module.

ActionType and ActionButton are defined identically in three files (app/ai/page.tsx, components/ai/AIChat.tsx, and app/api/ai/route.ts). This duplication creates a maintenance burden and risks inconsistency if one definition is updated without the others.

Create a shared types file:

// types/ai.ts
export type ActionType =
  | 'event_register'
  | 'event_view'
  | 'hackathon_view'
  | 'hackathon_register'
  | 'internship_apply'
  | 'blog_read'
  | 'learn_more';

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

Then import from the shared module in all three files.


525-543: Add a unique key and consider URL validation for navigation.

Two suggestions:

  1. Using index as the key works here since actions don't reorder, but using a unique identifier would be safer:

  2. router.push() is designed for internal navigation. If action.url could ever be an external URL, this would fail silently or cause unexpected behavior.

-{message.actions.map((action, index) => (
-  <Button
-    key={index}
+{message.actions.map((action) => (
+  <Button
+    key={`${action.type}-${action.url}`}
     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)}
+    onClick={() => {
+      if (action.url.startsWith('/')) {
+        router.push(action.url);
+      } else {
+        window.open(action.url, '_blank', 'noopener,noreferrer');
+      }
+    }}
   >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b995fbd and 9395489.

📒 Files selected for processing (3)
  • app/ai/page.tsx (4 hunks)
  • app/api/ai/route.ts (10 hunks)
  • components/ai/AIChat.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/ai/page.tsx (2)
types/messaging.ts (1)
  • Message (43-63)
components/ui/button.tsx (1)
  • Button (59-59)
components/ai/AIChat.tsx (2)
types/messaging.ts (1)
  • Message (43-63)
components/ui/button.tsx (1)
  • Button (59-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test Suite
🔇 Additional comments (6)
app/ai/page.tsx (2)

273-283: LGTM!

The streaming completion handler correctly attaches actions to the message only when they exist and are non-empty. The state update pattern using map to target the specific message by ID is appropriate.


316-323: LGTM!

Non-streaming mode correctly propagates actions from the API response to the AI message object.

components/ai/AIChat.tsx (2)

296-305: LGTM!

Streaming action attachment follows the same correct pattern as the AI page.


337-345: LGTM!

Non-streaming mode correctly includes actions in the message object.

app/api/ai/route.ts (2)

263-288: LGTM!

Adding slug as optional maintains backwards compatibility with existing data that may not have slugs populated.


349-391: LGTM!

Database queries correctly include the slug field for both events and hackathons.

Comment on lines +590 to +671
// 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;
}
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.

Comment on lines 1073 to +1082
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`));
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.

Comment on lines +1190 to +1198
// 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,
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.

@codeunia-dev codeunia-dev merged commit f01dec4 into main Nov 26, 2025
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants