/**
* User Feedback API Routes
*
* POST /api/feedback - Submit feedback on AI responses or general site feedback
*/
import { createAdminClient } from '@/lib/supabase-server';
import { auth } from '@/auth';
import { NextRequest, NextResponse } from 'next/server';
import type {
FeedbackType,
SubmitFeedbackRequest,
SubmitFeedbackResponse,
} from '@/lib/types/feedback';
export const dynamic = 'force-dynamic';
/**
* POST /api/feedback
* Submit user feedback
*/
export async function POST(request: NextRequest): Promise<NextResponse<SubmitFeedbackResponse>> {
try {
// Check authentication using NextAuth
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, error: 'You must be logged in to submit feedback' },
{ status: 401 }
);
}
const userId = session.user.id;
const supabase = createAdminClient();
// Parse request body
const body = (await request.json()) as SubmitFeedbackRequest;
const {
feedback_type,
message_id,
conversation_id,
message_content,
additional_comments,
context,
} = body;
// Validate required fields
if (!feedback_type) {
return NextResponse.json(
{ success: false, error: 'Missing required field: feedback_type' },
{ status: 400 }
);
}
const validTypes: FeedbackType[] = ['positive', 'negative', 'general'];
if (!validTypes.includes(feedback_type)) {
return NextResponse.json(
{ success: false, error: 'Invalid feedback_type. Must be: positive, negative, or general' },
{ status: 400 }
);
}
// For chat feedback, require message context
if ((feedback_type === 'positive' || feedback_type === 'negative') && !message_id) {
return NextResponse.json(
{ success: false, error: 'message_id is required for chat feedback' },
{ status: 400 }
);
}
// Validate context
if (!context || typeof context !== 'object') {
return NextResponse.json(
{ success: false, error: 'Missing required field: context' },
{ status: 400 }
);
}
// Insert feedback into database
const { data: feedback, error: insertError } = await supabase
.from('user_feedback')
.insert({
user_id: userId,
feedback_type,
message_id: message_id || null,
conversation_id: conversation_id || null,
message_content: message_content || null,
additional_comments: additional_comments || null,
user_agent: context.userAgent || null,
page_url: context.pageUrl || null,
screen_size: context.screenSize || null,
locale: context.locale || null,
status: 'pending',
})
.select('id')
.single();
if (insertError) {
console.error('Error inserting feedback:', insertError);
return NextResponse.json(
{ success: false, error: 'Failed to submit feedback' },
{ status: 500 }
);
}
// Create GitHub issue if configured
let githubIssueUrl: string | undefined;
if (process.env.GITHUB_FEEDBACK_TOKEN && process.env.GITHUB_FEEDBACK_REPO) {
try {
const issueResult = await createGitHubIssue({
feedbackId: feedback.id,
feedbackType: feedback_type,
userId,
userEmail: session.user.email || 'unknown',
messageContent: message_content,
additionalComments: additional_comments,
pageUrl: context.pageUrl,
userAgent: context.userAgent,
screenSize: context.screenSize,
locale: context.locale,
isAdminOrModerator: session.user.isAdmin || session.user.isModerator,
});
if (issueResult) {
githubIssueUrl = issueResult.url;
// Update feedback record with GitHub issue info
await supabase
.from('user_feedback')
.update({
github_issue_number: issueResult.number,
github_issue_url: issueResult.url,
})
.eq('id', feedback.id);
}
} catch (githubError) {
// Log error but don't fail the request - feedback was saved successfully
console.error('Error creating GitHub issue:', githubError);
}
}
return NextResponse.json({
success: true,
feedbackId: feedback.id,
githubIssueUrl,
});
} catch (error) {
console.error('Error in POST /api/feedback:', error);
return NextResponse.json(
{ success: false, error: 'Internal server error' },
{ status: 500 }
);
}
}
/**
* Create a GitHub issue for the feedback
*/
async function createGitHubIssue(params: {
feedbackId: string;
feedbackType: FeedbackType;
userId: string;
userEmail: string;
messageContent?: string;
additionalComments?: string;
pageUrl?: string;
userAgent?: string;
screenSize?: string;
locale?: string;
isAdminOrModerator?: boolean;
}): Promise<{ number: number; url: string } | null> {
const token = process.env.GITHUB_FEEDBACK_TOKEN || process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_FEEDBACK_REPO || 'northernvariables/CanadaGPT';
if (!token) {
return null;
}
const [owner, repoName] = repo.split('/');
if (!owner || !repoName) {
console.error('Invalid GITHUB_FEEDBACK_REPO format. Expected: owner/repo');
return null;
}
const typeEmoji = params.feedbackType === 'positive' ? '๐' : params.feedbackType === 'negative' ? '๐' : '๐ฌ';
const typeLabel = params.feedbackType === 'positive' ? 'positive' : params.feedbackType === 'negative' ? 'negative' : 'general';
const title = `[Feedback] ${typeEmoji} ${typeLabel}: ${
params.additionalComments?.substring(0, 50) || 'No comment provided'
}${params.additionalComments && params.additionalComments.length > 50 ? '...' : ''}`;
const body = `## User Feedback
**Type:** ${params.feedbackType}
**Feedback ID:** \`${params.feedbackId}\`
**User ID:** \`${params.userId}\`
**User Email:** ${params.userEmail}
### User Comments
${params.additionalComments || '_No additional comments provided_'}
${params.messageContent ? `### AI Response (snapshot)
\`\`\`
${params.messageContent.substring(0, 2000)}${params.messageContent.length > 2000 ? '...' : ''}
\`\`\`
` : ''}
### Debug Context
- **Page URL:** ${params.pageUrl || 'Unknown'}
- **Screen Size:** ${params.screenSize || 'Unknown'}
- **Locale:** ${params.locale || 'Unknown'}
- **User Agent:** ${params.userAgent || 'Unknown'}
---
_Created automatically by CanadaGPT Feedback System_`;
const labels = ['user-feedback', typeLabel];
if (params.isAdminOrModerator) {
labels.push('claude-fix');
}
try {
const response = await fetch(`https://api.github.com/repos/${owner}/${repoName}/issues`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
body,
labels,
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error('GitHub API error:', response.status, errorText);
return null;
}
const issue = await response.json();
return {
number: issue.number,
url: issue.html_url,
};
} catch (error) {
console.error('Error calling GitHub API:', error);
return null;
}
}