@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.
- package/LICENSE +177 -0
- package/README.md +382 -0
- package/dist/convex/agents/bugReportAgent.d.ts +30 -0
- package/dist/convex/agents/bugReportAgent.d.ts.map +1 -0
- package/dist/convex/agents/bugReportAgent.js +243 -0
- package/dist/convex/agents/bugReportAgent.js.map +1 -0
- package/dist/convex/agents/feedbackAgent.d.ts +29 -0
- package/dist/convex/agents/feedbackAgent.d.ts.map +1 -0
- package/dist/convex/agents/feedbackAgent.js +232 -0
- package/dist/convex/agents/feedbackAgent.js.map +1 -0
- package/dist/convex/bugReports.d.ts +49 -0
- package/dist/convex/bugReports.d.ts.map +1 -0
- package/dist/convex/bugReports.js +321 -0
- package/dist/convex/bugReports.js.map +1 -0
- package/dist/convex/convex.config.d.ts +3 -0
- package/dist/convex/convex.config.d.ts.map +1 -0
- package/dist/convex/convex.config.js +6 -0
- package/dist/convex/convex.config.js.map +1 -0
- package/dist/convex/emails/bugReportEmails.d.ts +16 -0
- package/dist/convex/emails/bugReportEmails.d.ts.map +1 -0
- package/dist/convex/emails/bugReportEmails.js +403 -0
- package/dist/convex/emails/bugReportEmails.js.map +1 -0
- package/dist/convex/emails/feedbackEmails.d.ts +16 -0
- package/dist/convex/emails/feedbackEmails.d.ts.map +1 -0
- package/dist/convex/emails/feedbackEmails.js +389 -0
- package/dist/convex/emails/feedbackEmails.js.map +1 -0
- package/dist/convex/feedback.d.ts +49 -0
- package/dist/convex/feedback.d.ts.map +1 -0
- package/dist/convex/feedback.js +327 -0
- package/dist/convex/feedback.js.map +1 -0
- package/dist/convex/index.d.ts +10 -0
- package/dist/convex/index.d.ts.map +1 -0
- package/dist/convex/index.js +12 -0
- package/dist/convex/index.js.map +1 -0
- package/dist/convex/schema.d.ts +200 -0
- package/dist/convex/schema.d.ts.map +1 -0
- package/dist/convex/schema.js +150 -0
- package/dist/convex/schema.js.map +1 -0
- package/dist/convex/supportTeams.d.ts +29 -0
- package/dist/convex/supportTeams.d.ts.map +1 -0
- package/dist/convex/supportTeams.js +159 -0
- package/dist/convex/supportTeams.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/react/BugReportButton.d.ts +70 -0
- package/dist/react/BugReportButton.d.ts.map +1 -0
- package/dist/react/BugReportButton.js +371 -0
- package/dist/react/BugReportButton.js.map +1 -0
- package/dist/react/BugReportContext.d.ts +59 -0
- package/dist/react/BugReportContext.d.ts.map +1 -0
- package/dist/react/BugReportContext.js +107 -0
- package/dist/react/BugReportContext.js.map +1 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +36 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +101 -0
- package/src/convex/agents/bugReportAgent.ts +277 -0
- package/src/convex/agents/feedbackAgent.ts +264 -0
- package/src/convex/bugReports.ts +350 -0
- package/src/convex/convex.config.ts +7 -0
- package/src/convex/emails/bugReportEmails.ts +479 -0
- package/src/convex/emails/feedbackEmails.ts +465 -0
- package/src/convex/feedback.ts +356 -0
- package/src/convex/index.ts +28 -0
- package/src/convex/schema.ts +207 -0
- package/src/convex/supportTeams.ts +179 -0
- package/src/index.ts +77 -0
- package/src/react/BugReportButton.tsx +755 -0
- package/src/react/BugReportContext.tsx +146 -0
- package/src/react/index.ts +46 -0
- 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
|
+
});
|