@fatagnus/convex-feedback 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +382 -0
  3. package/dist/convex/agents/bugReportAgent.d.ts +30 -0
  4. package/dist/convex/agents/bugReportAgent.d.ts.map +1 -0
  5. package/dist/convex/agents/bugReportAgent.js +243 -0
  6. package/dist/convex/agents/bugReportAgent.js.map +1 -0
  7. package/dist/convex/agents/feedbackAgent.d.ts +29 -0
  8. package/dist/convex/agents/feedbackAgent.d.ts.map +1 -0
  9. package/dist/convex/agents/feedbackAgent.js +232 -0
  10. package/dist/convex/agents/feedbackAgent.js.map +1 -0
  11. package/dist/convex/bugReports.d.ts +49 -0
  12. package/dist/convex/bugReports.d.ts.map +1 -0
  13. package/dist/convex/bugReports.js +321 -0
  14. package/dist/convex/bugReports.js.map +1 -0
  15. package/dist/convex/convex.config.d.ts +3 -0
  16. package/dist/convex/convex.config.d.ts.map +1 -0
  17. package/dist/convex/convex.config.js +6 -0
  18. package/dist/convex/convex.config.js.map +1 -0
  19. package/dist/convex/emails/bugReportEmails.d.ts +16 -0
  20. package/dist/convex/emails/bugReportEmails.d.ts.map +1 -0
  21. package/dist/convex/emails/bugReportEmails.js +403 -0
  22. package/dist/convex/emails/bugReportEmails.js.map +1 -0
  23. package/dist/convex/emails/feedbackEmails.d.ts +16 -0
  24. package/dist/convex/emails/feedbackEmails.d.ts.map +1 -0
  25. package/dist/convex/emails/feedbackEmails.js +389 -0
  26. package/dist/convex/emails/feedbackEmails.js.map +1 -0
  27. package/dist/convex/feedback.d.ts +49 -0
  28. package/dist/convex/feedback.d.ts.map +1 -0
  29. package/dist/convex/feedback.js +327 -0
  30. package/dist/convex/feedback.js.map +1 -0
  31. package/dist/convex/index.d.ts +10 -0
  32. package/dist/convex/index.d.ts.map +1 -0
  33. package/dist/convex/index.js +12 -0
  34. package/dist/convex/index.js.map +1 -0
  35. package/dist/convex/schema.d.ts +200 -0
  36. package/dist/convex/schema.d.ts.map +1 -0
  37. package/dist/convex/schema.js +150 -0
  38. package/dist/convex/schema.js.map +1 -0
  39. package/dist/convex/supportTeams.d.ts +29 -0
  40. package/dist/convex/supportTeams.d.ts.map +1 -0
  41. package/dist/convex/supportTeams.js +159 -0
  42. package/dist/convex/supportTeams.js.map +1 -0
  43. package/dist/index.d.ts +70 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +63 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/react/BugReportButton.d.ts +70 -0
  48. package/dist/react/BugReportButton.d.ts.map +1 -0
  49. package/dist/react/BugReportButton.js +371 -0
  50. package/dist/react/BugReportButton.js.map +1 -0
  51. package/dist/react/BugReportContext.d.ts +59 -0
  52. package/dist/react/BugReportContext.d.ts.map +1 -0
  53. package/dist/react/BugReportContext.js +107 -0
  54. package/dist/react/BugReportContext.js.map +1 -0
  55. package/dist/react/index.d.ts +36 -0
  56. package/dist/react/index.d.ts.map +1 -0
  57. package/dist/react/index.js +36 -0
  58. package/dist/react/index.js.map +1 -0
  59. package/dist/types.d.ts +89 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +5 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +101 -0
  64. package/src/convex/agents/bugReportAgent.ts +277 -0
  65. package/src/convex/agents/feedbackAgent.ts +264 -0
  66. package/src/convex/bugReports.ts +350 -0
  67. package/src/convex/convex.config.ts +7 -0
  68. package/src/convex/emails/bugReportEmails.ts +479 -0
  69. package/src/convex/emails/feedbackEmails.ts +465 -0
  70. package/src/convex/feedback.ts +356 -0
  71. package/src/convex/index.ts +28 -0
  72. package/src/convex/schema.ts +207 -0
  73. package/src/convex/supportTeams.ts +179 -0
  74. package/src/index.ts +77 -0
  75. package/src/react/BugReportButton.tsx +755 -0
  76. package/src/react/BugReportContext.tsx +146 -0
  77. package/src/react/index.ts +46 -0
  78. package/src/types.ts +93 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Feedback AI Agent
3
+ *
4
+ * Automatically analyzes feedback submissions and generates:
5
+ * - Summary of the feedback
6
+ * - Impact analysis (affected areas, users, systems)
7
+ * - Suggested action items
8
+ * - Estimated effort (low/medium/high)
9
+ * - Priority recommendations
10
+ *
11
+ * Triggers email notifications to the reporter and assigned team.
12
+ */
13
+
14
+ import { v } from "convex/values";
15
+ import { Agent, createThread } from "@convex-dev/agent";
16
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
17
+ import { components, internal } from "../_generated/api";
18
+ import { internalAction, internalQuery } from "../_generated/server";
19
+ import type { FeedbackPriority, Effort } from "../schema";
20
+
21
+ // Create OpenRouter provider
22
+ const openrouter = createOpenAICompatible({
23
+ name: "openrouter",
24
+ baseURL: "https://openrouter.ai/api/v1",
25
+ headers: {
26
+ Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
27
+ },
28
+ });
29
+
30
+ // Agent name constant
31
+ const AGENT_NAME = "Feedback Analyst";
32
+
33
+ // ============================================================================
34
+ // Agent Definition
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Feedback Analysis Agent
39
+ *
40
+ * Analyzes feedback submissions and generates comprehensive reports.
41
+ */
42
+ export const feedbackAgent = new Agent(components.agent, {
43
+ name: AGENT_NAME,
44
+ languageModel: openrouter.languageModel("anthropic/claude-sonnet-4"),
45
+
46
+ instructions: `You are the Feedback Analyst, an AI assistant that analyzes user feedback submissions.
47
+
48
+ When given feedback, you will:
49
+ 1. **Summarize** the feedback in 2-3 clear sentences
50
+ 2. **Analyze Impact** - identify what areas, users, or systems are affected
51
+ 3. **Suggest Action Items** - provide 3-5 concrete next steps to address the feedback
52
+ 4. **Estimate Effort** - rate as "low" (< 1 week), "medium" (1-4 weeks), or "high" (> 4 weeks)
53
+ 5. **Recommend Priority** - suggest "nice_to_have", "important", or "critical" based on the feedback content
54
+
55
+ ## Response Format
56
+ Always respond in the following JSON format:
57
+ \`\`\`json
58
+ {
59
+ "summary": "A 2-3 sentence summary of the feedback",
60
+ "impactAnalysis": "Detailed analysis of what's affected and potential consequences",
61
+ "actionItems": ["Action item 1", "Action item 2", "Action item 3"],
62
+ "estimatedEffort": "low" | "medium" | "high",
63
+ "suggestedPriority": "nice_to_have" | "important" | "critical",
64
+ "reasoning": "Brief explanation for your priority and effort estimates"
65
+ }
66
+ \`\`\`
67
+
68
+ ## Guidelines
69
+ - Be objective and constructive in your analysis
70
+ - Consider both technical and business impact
71
+ - Action items should be specific and actionable
72
+ - Effort estimation should account for development, testing, and deployment
73
+ - Priority should reflect urgency and user impact`,
74
+
75
+ tools: {},
76
+ maxSteps: 3,
77
+ });
78
+
79
+ // ============================================================================
80
+ // Internal Queries
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Get feedback by ID for analysis
85
+ */
86
+ export const getFeedbackForAnalysis = internalQuery({
87
+ args: {
88
+ feedbackId: v.id("feedback"),
89
+ },
90
+ returns: v.union(
91
+ v.object({
92
+ _id: v.id("feedback"),
93
+ type: v.string(),
94
+ title: v.string(),
95
+ description: v.string(),
96
+ priority: v.string(),
97
+ reporterName: v.string(),
98
+ reporterEmail: v.string(),
99
+ url: v.string(),
100
+ browserInfo: v.string(),
101
+ }),
102
+ v.null()
103
+ ),
104
+ handler: async (ctx, args) => {
105
+ const feedback = await ctx.db.get(args.feedbackId);
106
+ if (!feedback) {
107
+ return null;
108
+ }
109
+ return {
110
+ _id: feedback._id,
111
+ type: feedback.type,
112
+ title: feedback.title,
113
+ description: feedback.description,
114
+ priority: feedback.priority,
115
+ reporterName: feedback.reporterName,
116
+ reporterEmail: feedback.reporterEmail,
117
+ url: feedback.url,
118
+ browserInfo: feedback.browserInfo,
119
+ };
120
+ },
121
+ });
122
+
123
+ // ============================================================================
124
+ // Main Processing Action
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Analyze feedback using AI and send notifications.
129
+ * This is the main entry point triggered after feedback creation.
130
+ */
131
+ export const processFeedback = internalAction({
132
+ args: {
133
+ feedbackId: v.id("feedback"),
134
+ },
135
+ returns: v.object({
136
+ success: v.boolean(),
137
+ error: v.optional(v.string()),
138
+ }),
139
+ handler: async (ctx, args): Promise<{ success: boolean; error?: string }> => {
140
+ // Check if OpenRouter API key is configured
141
+ if (!process.env.OPENROUTER_API_KEY) {
142
+ console.log("OPENROUTER_API_KEY not configured, skipping AI analysis");
143
+ // Still try to send notifications without AI analysis
144
+ try {
145
+ await ctx.runAction(internal.emails.feedbackEmails.sendFeedbackNotifications, {
146
+ feedbackId: args.feedbackId,
147
+ analysis: null,
148
+ });
149
+ } catch {
150
+ console.log("Email notifications not configured");
151
+ }
152
+ return { success: true };
153
+ }
154
+
155
+ // Get the feedback
156
+ const feedback = await ctx.runQuery(internal.agents.feedbackAgent.getFeedbackForAnalysis, {
157
+ feedbackId: args.feedbackId,
158
+ });
159
+
160
+ if (!feedback) {
161
+ return { success: false, error: "Feedback not found" };
162
+ }
163
+
164
+ // Create a thread for this analysis
165
+ const threadId = await createThread(ctx, components.agent, {
166
+ title: `Feedback Analysis: ${feedback.title}`,
167
+ });
168
+
169
+ // Build the prompt for analysis
170
+ const analysisPrompt = `Please analyze the following feedback submission:
171
+
172
+ **Type:** ${feedback.type.replace("_", " ")}
173
+ **Title:** ${feedback.title}
174
+ **Description:** ${feedback.description}
175
+ **Reporter Priority:** ${feedback.priority.replace("_", " ")}
176
+ **Submitted From:** ${feedback.url}
177
+
178
+ Provide your analysis in the JSON format specified.`;
179
+
180
+ // Generate analysis using the agent
181
+ let result: { text: string };
182
+ try {
183
+ result = await feedbackAgent.generateText(
184
+ ctx,
185
+ { threadId },
186
+ { prompt: analysisPrompt }
187
+ );
188
+ } catch (error) {
189
+ console.error("AI analysis failed:", error);
190
+ return { success: false, error: `AI analysis failed: ${error instanceof Error ? error.message : String(error)}` };
191
+ }
192
+
193
+ // Parse the AI response
194
+ let analysis: {
195
+ summary: string;
196
+ impactAnalysis: string;
197
+ actionItems: string[];
198
+ estimatedEffort: Effort;
199
+ suggestedPriority: FeedbackPriority;
200
+ };
201
+
202
+ try {
203
+ // Extract JSON from the response (handle markdown code blocks)
204
+ const jsonMatch = result.text.match(/```json\s*([\s\S]*?)\s*```/);
205
+ const jsonStr = jsonMatch ? jsonMatch[1] : result.text;
206
+ const parsed = JSON.parse(jsonStr);
207
+
208
+ analysis = {
209
+ summary: parsed.summary || "Unable to generate summary",
210
+ impactAnalysis: parsed.impactAnalysis || "Unable to generate impact analysis",
211
+ actionItems: Array.isArray(parsed.actionItems) ? parsed.actionItems : [],
212
+ estimatedEffort: ["low", "medium", "high"].includes(parsed.estimatedEffort)
213
+ ? parsed.estimatedEffort
214
+ : "medium",
215
+ suggestedPriority: ["nice_to_have", "important", "critical"].includes(parsed.suggestedPriority)
216
+ ? parsed.suggestedPriority
217
+ : feedback.priority as FeedbackPriority,
218
+ };
219
+ } catch {
220
+ // If parsing fails, create a basic analysis from the text
221
+ analysis = {
222
+ summary: result.text.substring(0, 500),
223
+ impactAnalysis: "AI analysis parsing failed. Please review manually.",
224
+ actionItems: ["Review feedback manually", "Contact reporter for clarification"],
225
+ estimatedEffort: "medium",
226
+ suggestedPriority: feedback.priority as FeedbackPriority,
227
+ };
228
+ }
229
+
230
+ // Update feedback with analysis
231
+ await ctx.runMutation(internal.feedback.updateWithAnalysis, {
232
+ feedbackId: args.feedbackId,
233
+ aiSummary: analysis.summary,
234
+ aiImpactAnalysis: analysis.impactAnalysis,
235
+ aiActionItems: analysis.actionItems,
236
+ aiEstimatedEffort: analysis.estimatedEffort,
237
+ aiSuggestedPriority: analysis.suggestedPriority,
238
+ aiThreadId: threadId,
239
+ });
240
+
241
+ // Send email notifications
242
+ try {
243
+ await ctx.runAction(internal.emails.feedbackEmails.sendFeedbackNotifications, {
244
+ feedbackId: args.feedbackId,
245
+ analysis: {
246
+ summary: analysis.summary,
247
+ impactAnalysis: analysis.impactAnalysis,
248
+ actionItems: analysis.actionItems,
249
+ estimatedEffort: analysis.estimatedEffort,
250
+ suggestedPriority: analysis.suggestedPriority,
251
+ },
252
+ });
253
+
254
+ // Mark notifications as sent
255
+ await ctx.runMutation(internal.feedback.markNotificationsSent, {
256
+ feedbackId: args.feedbackId,
257
+ });
258
+ } catch {
259
+ console.log("Email notifications not configured or failed");
260
+ }
261
+
262
+ return { success: true };
263
+ },
264
+ });
@@ -0,0 +1,350 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query, internalMutation } from "./_generated/server";
3
+ import { internal } from "./_generated/api";
4
+ import {
5
+ bugSeverityValidator,
6
+ bugStatusValidator,
7
+ reporterTypeValidator,
8
+ effortValidator,
9
+ } from "./schema";
10
+
11
+ // Return validator for bug reports
12
+ const bugReportReturnValidator = v.object({
13
+ _id: v.id("bugReports"),
14
+ _creationTime: v.number(),
15
+ title: v.string(),
16
+ description: v.string(),
17
+ severity: bugSeverityValidator,
18
+ status: bugStatusValidator,
19
+ isArchived: v.optional(v.boolean()),
20
+ reporterType: reporterTypeValidator,
21
+ reporterId: v.string(),
22
+ reporterEmail: v.string(),
23
+ reporterName: v.string(),
24
+ url: v.string(),
25
+ route: v.optional(v.string()),
26
+ browserInfo: v.string(),
27
+ consoleErrors: v.optional(v.string()),
28
+ screenshotStorageId: v.optional(v.id("_storage")),
29
+ viewportWidth: v.number(),
30
+ viewportHeight: v.number(),
31
+ networkState: v.string(),
32
+ createdAt: v.number(),
33
+ updatedAt: v.number(),
34
+ // AI Analysis fields
35
+ aiSummary: v.optional(v.string()),
36
+ aiRootCauseAnalysis: v.optional(v.string()),
37
+ aiReproductionSteps: v.optional(v.array(v.string())),
38
+ aiSuggestedFix: v.optional(v.string()),
39
+ aiEstimatedEffort: v.optional(effortValidator),
40
+ aiSuggestedSeverity: v.optional(bugSeverityValidator),
41
+ aiAnalyzedAt: v.optional(v.number()),
42
+ aiThreadId: v.optional(v.string()),
43
+ notificationsSentAt: v.optional(v.number()),
44
+ });
45
+
46
+ /**
47
+ * Create a new bug report.
48
+ * This mutation is intentionally public (no auth required) because:
49
+ * 1. We want to capture bugs even when auth might be broken
50
+ * 2. The reporter info is passed explicitly from the client
51
+ */
52
+ export const create = mutation({
53
+ args: {
54
+ title: v.string(),
55
+ description: v.string(),
56
+ severity: bugSeverityValidator,
57
+ reporterType: reporterTypeValidator,
58
+ reporterId: v.string(),
59
+ reporterEmail: v.string(),
60
+ reporterName: v.string(),
61
+ url: v.string(),
62
+ route: v.optional(v.string()),
63
+ browserInfo: v.string(),
64
+ consoleErrors: v.optional(v.string()),
65
+ screenshotStorageId: v.optional(v.id("_storage")),
66
+ viewportWidth: v.number(),
67
+ viewportHeight: v.number(),
68
+ networkState: v.string(),
69
+ },
70
+ returns: v.id("bugReports"),
71
+ handler: async (ctx, args) => {
72
+ const now = Date.now();
73
+
74
+ const reportId = await ctx.db.insert("bugReports", {
75
+ title: args.title,
76
+ description: args.description,
77
+ severity: args.severity,
78
+ status: "open",
79
+ reporterType: args.reporterType,
80
+ reporterId: args.reporterId,
81
+ reporterEmail: args.reporterEmail,
82
+ reporterName: args.reporterName,
83
+ url: args.url,
84
+ route: args.route,
85
+ browserInfo: args.browserInfo,
86
+ consoleErrors: args.consoleErrors,
87
+ screenshotStorageId: args.screenshotStorageId,
88
+ viewportWidth: args.viewportWidth,
89
+ viewportHeight: args.viewportHeight,
90
+ networkState: args.networkState,
91
+ createdAt: now,
92
+ updatedAt: now,
93
+ });
94
+
95
+ // Schedule AI analysis and email notifications (runs immediately)
96
+ // This uses a try-catch to gracefully handle if agents aren't configured
97
+ try {
98
+ await ctx.scheduler.runAfter(0, internal.agents.bugReportAgent.processBugReport, {
99
+ bugReportId: reportId,
100
+ });
101
+ } catch {
102
+ // AI agent not available, continue without analysis
103
+ console.log("AI agent not configured, skipping analysis");
104
+ }
105
+
106
+ return reportId;
107
+ },
108
+ });
109
+
110
+ /**
111
+ * List bug reports with optional filters.
112
+ * By default, archived items are excluded unless includeArchived is true.
113
+ */
114
+ export const list = query({
115
+ args: {
116
+ status: v.optional(bugStatusValidator),
117
+ severity: v.optional(bugSeverityValidator),
118
+ includeArchived: v.optional(v.boolean()),
119
+ limit: v.optional(v.number()),
120
+ },
121
+ returns: v.array(bugReportReturnValidator),
122
+ handler: async (ctx, args) => {
123
+ const limit = args.limit ?? 50;
124
+ const includeArchived = args.includeArchived ?? false;
125
+
126
+ let reports;
127
+ if (args.status) {
128
+ const status = args.status;
129
+ reports = await ctx.db
130
+ .query("bugReports")
131
+ .withIndex("by_status", (q) => q.eq("status", status))
132
+ .order("desc")
133
+ .take(limit * 2);
134
+ } else if (args.severity) {
135
+ const severity = args.severity;
136
+ reports = await ctx.db
137
+ .query("bugReports")
138
+ .withIndex("by_severity", (q) => q.eq("severity", severity))
139
+ .order("desc")
140
+ .take(limit * 2);
141
+ } else {
142
+ reports = await ctx.db
143
+ .query("bugReports")
144
+ .withIndex("by_created")
145
+ .order("desc")
146
+ .take(limit * 2);
147
+ }
148
+
149
+ // Filter out archived items unless includeArchived is true
150
+ if (!includeArchived) {
151
+ reports = reports.filter((r) => !r.isArchived);
152
+ }
153
+
154
+ return reports.slice(0, limit);
155
+ },
156
+ });
157
+
158
+ /**
159
+ * Get a single bug report by ID.
160
+ */
161
+ export const get = query({
162
+ args: {
163
+ reportId: v.id("bugReports"),
164
+ },
165
+ returns: v.union(bugReportReturnValidator, v.null()),
166
+ handler: async (ctx, args) => {
167
+ return await ctx.db.get(args.reportId);
168
+ },
169
+ });
170
+
171
+ /**
172
+ * Update bug report status.
173
+ */
174
+ export const updateStatus = mutation({
175
+ args: {
176
+ reportId: v.id("bugReports"),
177
+ status: bugStatusValidator,
178
+ },
179
+ returns: v.null(),
180
+ handler: async (ctx, args) => {
181
+ const report = await ctx.db.get(args.reportId);
182
+ if (!report) {
183
+ throw new Error("Bug report not found");
184
+ }
185
+
186
+ await ctx.db.patch(args.reportId, {
187
+ status: args.status,
188
+ updatedAt: Date.now(),
189
+ });
190
+
191
+ return null;
192
+ },
193
+ });
194
+
195
+ /**
196
+ * Generate upload URL for screenshot.
197
+ */
198
+ export const generateUploadUrl = mutation({
199
+ args: {},
200
+ returns: v.string(),
201
+ handler: async (ctx) => {
202
+ return await ctx.storage.generateUploadUrl();
203
+ },
204
+ });
205
+
206
+ /**
207
+ * Get screenshot URL from storage ID.
208
+ */
209
+ export const getScreenshotUrl = query({
210
+ args: {
211
+ storageId: v.id("_storage"),
212
+ },
213
+ returns: v.union(v.string(), v.null()),
214
+ handler: async (ctx, args) => {
215
+ return await ctx.storage.getUrl(args.storageId);
216
+ },
217
+ });
218
+
219
+ /**
220
+ * Archive a bug report.
221
+ */
222
+ export const archive = mutation({
223
+ args: {
224
+ reportId: v.id("bugReports"),
225
+ },
226
+ returns: v.null(),
227
+ handler: async (ctx, args) => {
228
+ const report = await ctx.db.get(args.reportId);
229
+ if (!report) {
230
+ throw new Error("Bug report not found");
231
+ }
232
+
233
+ await ctx.db.patch(args.reportId, {
234
+ isArchived: true,
235
+ updatedAt: Date.now(),
236
+ });
237
+
238
+ return null;
239
+ },
240
+ });
241
+
242
+ /**
243
+ * Unarchive a bug report.
244
+ */
245
+ export const unarchive = mutation({
246
+ args: {
247
+ reportId: v.id("bugReports"),
248
+ },
249
+ returns: v.null(),
250
+ handler: async (ctx, args) => {
251
+ const report = await ctx.db.get(args.reportId);
252
+ if (!report) {
253
+ throw new Error("Bug report not found");
254
+ }
255
+
256
+ await ctx.db.patch(args.reportId, {
257
+ isArchived: false,
258
+ updatedAt: Date.now(),
259
+ });
260
+
261
+ return null;
262
+ },
263
+ });
264
+
265
+ /**
266
+ * Get AI analysis for a specific bug report.
267
+ */
268
+ export const getAiAnalysis = query({
269
+ args: {
270
+ reportId: v.id("bugReports"),
271
+ },
272
+ returns: v.union(
273
+ v.object({
274
+ aiSummary: v.union(v.string(), v.null()),
275
+ aiRootCauseAnalysis: v.union(v.string(), v.null()),
276
+ aiReproductionSteps: v.union(v.array(v.string()), v.null()),
277
+ aiSuggestedFix: v.union(v.string(), v.null()),
278
+ aiEstimatedEffort: v.union(effortValidator, v.null()),
279
+ aiSuggestedSeverity: v.union(bugSeverityValidator, v.null()),
280
+ aiAnalyzedAt: v.union(v.number(), v.null()),
281
+ notificationsSentAt: v.union(v.number(), v.null()),
282
+ }),
283
+ v.null()
284
+ ),
285
+ handler: async (ctx, args) => {
286
+ const report = await ctx.db.get(args.reportId);
287
+ if (!report) {
288
+ return null;
289
+ }
290
+
291
+ return {
292
+ aiSummary: report.aiSummary ?? null,
293
+ aiRootCauseAnalysis: report.aiRootCauseAnalysis ?? null,
294
+ aiReproductionSteps: report.aiReproductionSteps ?? null,
295
+ aiSuggestedFix: report.aiSuggestedFix ?? null,
296
+ aiEstimatedEffort: report.aiEstimatedEffort ?? null,
297
+ aiSuggestedSeverity: report.aiSuggestedSeverity ?? null,
298
+ aiAnalyzedAt: report.aiAnalyzedAt ?? null,
299
+ notificationsSentAt: report.notificationsSentAt ?? null,
300
+ };
301
+ },
302
+ });
303
+
304
+ /**
305
+ * Internal mutation to update bug report with AI analysis results
306
+ */
307
+ export const updateWithAnalysis = internalMutation({
308
+ args: {
309
+ bugReportId: v.id("bugReports"),
310
+ aiSummary: v.string(),
311
+ aiRootCauseAnalysis: v.string(),
312
+ aiReproductionSteps: v.array(v.string()),
313
+ aiSuggestedFix: v.string(),
314
+ aiEstimatedEffort: effortValidator,
315
+ aiSuggestedSeverity: bugSeverityValidator,
316
+ aiThreadId: v.string(),
317
+ },
318
+ returns: v.null(),
319
+ handler: async (ctx, args) => {
320
+ await ctx.db.patch(args.bugReportId, {
321
+ aiSummary: args.aiSummary,
322
+ aiRootCauseAnalysis: args.aiRootCauseAnalysis,
323
+ aiReproductionSteps: args.aiReproductionSteps,
324
+ aiSuggestedFix: args.aiSuggestedFix,
325
+ aiEstimatedEffort: args.aiEstimatedEffort,
326
+ aiSuggestedSeverity: args.aiSuggestedSeverity,
327
+ aiThreadId: args.aiThreadId,
328
+ aiAnalyzedAt: Date.now(),
329
+ updatedAt: Date.now(),
330
+ });
331
+ return null;
332
+ },
333
+ });
334
+
335
+ /**
336
+ * Internal mutation to mark notifications as sent
337
+ */
338
+ export const markNotificationsSent = internalMutation({
339
+ args: {
340
+ bugReportId: v.id("bugReports"),
341
+ },
342
+ returns: v.null(),
343
+ handler: async (ctx, args) => {
344
+ await ctx.db.patch(args.bugReportId, {
345
+ notificationsSentAt: Date.now(),
346
+ updatedAt: Date.now(),
347
+ });
348
+ return null;
349
+ },
350
+ });
@@ -0,0 +1,7 @@
1
+ import { defineComponent } from "convex/server";
2
+ import agent from "@convex-dev/agent/convex.config";
3
+
4
+ const feedback = defineComponent("feedback");
5
+ feedback.use(agent);
6
+
7
+ export default feedback;