-
Notifications
You must be signed in to change notification settings - Fork 2
Add Smart Action Buttons in Unio AI Responses (Backend + Frontend Integration) #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ackathon slugs for improved navigation.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThe 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.
Example instruction:
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. Comment |
There was a problem hiding this 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.
ActionTypeandActionButtonare defined identically in three files (app/ai/page.tsx,components/ai/AIChat.tsx, andapp/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:
Using
indexas the key works here since actions don't reorder, but using a unique identifier would be safer:
router.push()is designed for internal navigation. Ifaction.urlcould 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
📒 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
mapto target the specific message by ID is appropriate.
316-323: LGTM!Non-streaming mode correctly propagates
actionsfrom 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
slugas optional maintains backwards compatibility with existing data that may not have slugs populated.
349-391: LGTM!Database queries correctly include the
slugfield for both events and hackathons.
| // 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove debug logging and consider title matching edge cases.
Several observations:
-
Debug console.log statements (lines 611-617, 625-630, 669) should be removed or converted to conditional debug logging for production.
-
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).
-
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.
| 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`)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| // 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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.
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:
Context-aware matching:
Date-aware logic:
Integrated actions into both:
2. Frontend — Button Rendering
Files Updated:
components/ai/AIChat.tsxapp/ai/page.tsxKey Additions:
Extended
MessageandAIResponseinterfaces withactions?: ActionButton[].UI rendering block for AI-generated action buttons.
Clean, responsive layout:
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:
🧪 Testing Scenarios Covered
All scenarios behave as expected.
Authored by: @akshay0611
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.