'use server';
/**
* Feedback Admin Server Actions
* Admin actions for viewing and managing user feedback
*/
import { createAdminClient } from '@/lib/supabase-server';
import { requireAdmin } from '@/lib/admin-check';
import Anthropic from '@anthropic-ai/sdk';
import type { UserFeedback, FeedbackType, FeedbackStatus } from '@/lib/types/feedback';
export interface FeedbackFilters {
status?: FeedbackStatus | 'all';
type?: FeedbackType | 'all';
search?: string;
limit?: number;
offset?: number;
}
export interface FeedbackListResult {
feedback: UserFeedback[];
total: number;
}
export interface FeedbackStats {
total: number;
pending: number;
reviewed: number;
resolved: number;
dismissed: number;
positive: number;
negative: number;
general: number;
}
/**
* Get paginated feedback list with filters
*/
export async function getFeedback(filters: FeedbackFilters = {}): Promise<FeedbackListResult> {
await requireAdmin();
const {
status = 'all',
type = 'all',
search = '',
limit = 50,
offset = 0,
} = filters;
const adminClient = createAdminClient();
let query = adminClient
.from('user_feedback')
.select('*', { count: 'exact' })
.order('created_at', { ascending: false })
.range(offset, offset + limit - 1);
if (status !== 'all') {
query = query.eq('status', status);
}
if (type !== 'all') {
query = query.eq('feedback_type', type);
}
if (search) {
query = query.or(`additional_comments.ilike.%${search}%,message_content.ilike.%${search}%`);
}
const { data, error, count } = await query;
if (error) {
console.error('Error fetching feedback:', error);
return { feedback: [], total: 0 };
}
return {
feedback: (data || []) as UserFeedback[],
total: count || 0,
};
}
/**
* Get feedback statistics
*/
export async function getFeedbackStats(): Promise<FeedbackStats> {
await requireAdmin();
const adminClient = createAdminClient();
// Get counts by status
const { data: statusCounts, error: statusError } = await adminClient
.from('user_feedback')
.select('status');
// Get counts by type
const { data: typeCounts, error: typeError } = await adminClient
.from('user_feedback')
.select('feedback_type');
if (statusError || typeError) {
console.error('Error fetching feedback stats:', statusError || typeError);
return {
total: 0,
pending: 0,
reviewed: 0,
resolved: 0,
dismissed: 0,
positive: 0,
negative: 0,
general: 0,
};
}
const statusMap = (statusCounts || []).reduce(
(acc: Record<string, number>, item: { status: string }) => {
acc[item.status] = (acc[item.status] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
const typeMap = (typeCounts || []).reduce(
(acc: Record<string, number>, item: { feedback_type: string }) => {
acc[item.feedback_type] = (acc[item.feedback_type] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
const total = statusCounts?.length || 0;
return {
total,
pending: statusMap['pending'] || 0,
reviewed: statusMap['reviewed'] || 0,
resolved: statusMap['resolved'] || 0,
dismissed: statusMap['dismissed'] || 0,
positive: typeMap['positive'] || 0,
negative: typeMap['negative'] || 0,
general: typeMap['general'] || 0,
};
}
/**
* Update feedback status
*/
export async function updateFeedbackStatus(
feedbackId: string,
status: FeedbackStatus,
adminNotes?: string
): Promise<{ success: boolean; error?: string }> {
const { userId } = await requireAdmin();
const adminClient = createAdminClient();
const updateData: Record<string, unknown> = {
status,
reviewed_by: userId,
reviewed_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
};
if (adminNotes !== undefined) {
updateData.admin_notes = adminNotes;
}
const { error } = await adminClient
.from('user_feedback')
.update(updateData)
.eq('id', feedbackId);
if (error) {
console.error('Error updating feedback status:', error);
return { success: false, error: 'Failed to update feedback status' };
}
return { success: true };
}
/**
* Delete feedback entry
*/
export async function deleteFeedback(
feedbackId: string
): Promise<{ success: boolean; error?: string }> {
await requireAdmin();
const adminClient = createAdminClient();
const { error } = await adminClient
.from('user_feedback')
.delete()
.eq('id', feedbackId);
if (error) {
console.error('Error deleting feedback:', error);
return { success: false, error: 'Failed to delete feedback' };
}
return { success: true };
}
/**
* Create a GitHub issue from feedback using AI to generate title and body
*/
export async function createGitHubIssueFromFeedback(
feedbackId: string
): Promise<{ success: boolean; issueUrl?: string; issueNumber?: number; error?: string }> {
await requireAdmin();
const adminClient = createAdminClient();
// Get the feedback
const { data: feedback, error: fetchError } = await adminClient
.from('user_feedback')
.select('*')
.eq('id', feedbackId)
.single();
if (fetchError || !feedback) {
console.error('Error fetching feedback:', fetchError);
return { success: false, error: 'Feedback not found' };
}
// Check if issue already exists
if (feedback.github_issue_url) {
return { success: false, error: 'GitHub issue already exists for this feedback' };
}
const token = process.env.GITHUB_FEEDBACK_TOKEN || process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_FEEDBACK_REPO || 'northernvariables/CanadaGPT';
if (!token) {
return { success: false, error: 'GitHub token not configured' };
}
const [owner, repoName] = repo.split('/');
if (!owner || !repoName) {
return { success: false, error: 'Invalid GitHub repository configuration' };
}
try {
// Use AI to generate issue content
const aiContent = await generateIssueWithAI(feedback as UserFeedback);
// Create the GitHub issue
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: aiContent.title,
body: aiContent.body,
labels: aiContent.labels,
}),
});
if (!response.ok) {
const errorText = await response.text();
console.error('GitHub API error:', response.status, errorText);
return { success: false, error: `GitHub API error: ${response.status}` };
}
const issue = await response.json();
// Update feedback with issue info
const { error: updateError } = await adminClient
.from('user_feedback')
.update({
github_issue_number: issue.number,
github_issue_url: issue.html_url,
updated_at: new Date().toISOString(),
})
.eq('id', feedbackId);
if (updateError) {
console.error('Error updating feedback with issue info:', updateError);
// Issue was created, just failed to save the link
}
return {
success: true,
issueUrl: issue.html_url,
issueNumber: issue.number,
};
} catch (error) {
console.error('Error creating GitHub issue:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create GitHub issue',
};
}
}
/**
* Generate GitHub issue title and body using AI
*/
async function generateIssueWithAI(
feedback: UserFeedback
): Promise<{ title: string; body: string; labels: string[] }> {
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
const feedbackContext = `
Feedback Type: ${feedback.feedback_type}
User Comments: ${feedback.additional_comments || 'None provided'}
Related AI Response: ${feedback.message_content ? feedback.message_content.substring(0, 1500) : 'None'}
Page URL: ${feedback.page_url || 'Unknown'}
Locale: ${feedback.locale || 'Unknown'}
Screen Size: ${feedback.screen_size || 'Unknown'}
Created: ${feedback.created_at}
Admin Notes: ${feedback.admin_notes || 'None'}
`.trim();
const systemPrompt = `You are a technical writer creating GitHub issues from user feedback for a Canadian parliamentary information platform called CanadaGPT.
Your task is to create a well-structured GitHub issue that developers can act on.
RULES:
1. Create a concise, actionable title (max 80 chars) that summarizes the issue
2. For the body, use proper GitHub markdown formatting
3. Include all relevant context from the feedback
4. Suggest appropriate labels based on the feedback type and content
5. Be professional and constructive, even for negative feedback
6. If the feedback is vague, still create a useful issue that captures what we know
LABEL OPTIONS (choose 1-3 that apply):
- bug: Something isn't working correctly
- enhancement: New feature or improvement request
- ux: User experience issue
- ai-response: Related to AI/chatbot responses
- content: Related to data or content accuracy
- performance: Speed or performance issue
- accessibility: Accessibility concern
- i18n: Internationalization/translation issue
- user-feedback: General user feedback (always include this)
Respond with valid JSON only:
{
"title": "Issue title here",
"body": "Full markdown body here",
"labels": ["user-feedback", "other-relevant-labels"]
}`;
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1500,
system: systemPrompt,
messages: [
{
role: 'user',
content: `Create a GitHub issue from this user feedback:\n\n${feedbackContext}`,
},
],
});
// Extract text content
const textContent = response.content.find((block) => block.type === 'text');
if (!textContent || textContent.type !== 'text') {
throw new Error('No text response from AI');
}
// Parse JSON response
const jsonMatch = textContent.text.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
throw new Error('Could not parse AI response as JSON');
}
const parsed = JSON.parse(jsonMatch[0]);
// Ensure labels always includes user-feedback
const labels = Array.isArray(parsed.labels) ? parsed.labels : ['user-feedback'];
if (!labels.includes('user-feedback')) {
labels.unshift('user-feedback');
}
// Add feedback type label
const typeLabel = feedback.feedback_type === 'positive' ? 'positive-feedback' :
feedback.feedback_type === 'negative' ? 'negative-feedback' : 'general-feedback';
if (!labels.includes(typeLabel)) {
labels.push(typeLabel);
}
// Append footer to body
const body = `${parsed.body}
---
**Feedback Details**
- Feedback ID: \`${feedback.id}\`
- Type: ${feedback.feedback_type}
- Created: ${new Date(feedback.created_at).toLocaleString()}
- User ID: \`${feedback.user_id || 'Anonymous'}\`
_Generated with AI assistance from CanadaGPT admin panel_`;
return {
title: parsed.title.substring(0, 100), // Ensure title isn't too long
body,
labels,
};
}