import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; export const getProjects = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) return []; return await ctx.db .query("projects") .withIndex("by_owner", (q) => q.eq("userId", userId)) .collect(); }, }); export const getDefaultProject = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) return null; return await ctx.db .query("projects") .filter((q) => q.and(q.eq(q.field("userId"), userId), q.eq(q.field("isDefault"), true))) .first(); }, }); export const createProject = mutation({ args: { name: v.string(), isDefault: v.boolean() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Unauthorized"); // If setting as default, unset other defaults? For now assume handled by UI or logic // Actually simplicity: just create. return await ctx.db.insert("projects", { userId, name: args.name, isDefault: args.isDefault, dorkingConfig: { selectedSourceIds: [] }, }); }, }); export const toggleDataSourceConfig = mutation({ args: { projectId: v.id("projects"), sourceId: v.id("dataSources"), selected: v.boolean() }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) throw new Error("Unauthorized"); const project = await ctx.db.get(args.projectId); if (!project || project.userId !== userId) throw new Error("Project not found or unauthorized"); let newSelectedIds = project.dorkingConfig.selectedSourceIds; if (args.selected) { if (!newSelectedIds.includes(args.sourceId)) { newSelectedIds.push(args.sourceId); } } else { newSelectedIds = newSelectedIds.filter((id) => id !== args.sourceId); } await ctx.db.patch(args.projectId, { dorkingConfig: { selectedSourceIds: newSelectedIds }, }); }, }); export const getSearchContext = query({ args: { projectId: v.id("projects") }, handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) return { context: null, selectedSourceIds: [], missingSources: [] }; const project = await ctx.db.get(args.projectId); if (!project || project.userId !== userId) { return { context: null, selectedSourceIds: [], missingSources: [] }; } const selectedSourceIds = project.dorkingConfig.selectedSourceIds || []; if (selectedSourceIds.length === 0) { return { context: null, selectedSourceIds, missingSources: [] }; } const analyses = []; const missingSources: { sourceId: string; reason: string }[] = []; for (const sourceId of selectedSourceIds) { const dataSource = await ctx.db.get(sourceId); if (!dataSource || dataSource.projectId !== project._id) { missingSources.push({ sourceId: sourceId as string, reason: "not_found" }); continue; } const latest = await ctx.db .query("analyses") .withIndex("by_dataSource_createdAt", (q) => q.eq("dataSourceId", sourceId)) .order("desc") .first(); if (!latest) { missingSources.push({ sourceId: sourceId as string, reason: "no_analysis" }); continue; } analyses.push(latest); } if (analyses.length === 0) { return { context: null, selectedSourceIds, missingSources }; } const merged = mergeAnalyses(analyses, project.name); return { context: merged, selectedSourceIds, missingSources }; }, }); function normalizeKey(value: string) { return value.trim().toLowerCase(); } function severityRank(severity: "high" | "medium" | "low") { if (severity === "high") return 3; if (severity === "medium") return 2; return 1; } function mergeAnalyses(analyses: any[], fallbackName: string) { const keywordMap = new Map(); const problemMap = new Map(); const competitorMap = new Map(); const personaMap = new Map(); const useCaseMap = new Map(); const featureMap = new Map(); let latestScrapedAt = analyses[0].scrapedAt; for (const analysis of analyses) { if (analysis.scrapedAt > latestScrapedAt) { latestScrapedAt = analysis.scrapedAt; } for (const keyword of analysis.keywords || []) { const key = normalizeKey(keyword.term); if (!keywordMap.has(key)) { keywordMap.set(key, keyword); } else if (keywordMap.get(key)?.type !== "differentiator" && keyword.type === "differentiator") { keywordMap.set(key, keyword); } } for (const problem of analysis.problemsSolved || []) { const key = normalizeKey(problem.problem); const existing = problemMap.get(key); if (!existing || severityRank(problem.severity) > severityRank(existing.severity)) { problemMap.set(key, problem); } } for (const competitor of analysis.competitors || []) { const key = normalizeKey(competitor.name); if (!competitorMap.has(key)) { competitorMap.set(key, competitor); } } for (const persona of analysis.personas || []) { const key = normalizeKey(`${persona.name}:${persona.role}`); if (!personaMap.has(key)) { personaMap.set(key, persona); } } for (const useCase of analysis.useCases || []) { const key = normalizeKey(useCase.scenario); if (!useCaseMap.has(key)) { useCaseMap.set(key, useCase); } } for (const feature of analysis.features || []) { const key = normalizeKey(feature.name); if (!featureMap.has(key)) { featureMap.set(key, feature); } } } const keywords = Array.from(keywordMap.values()) .sort((a, b) => { const aDiff = a.type === "differentiator" ? 0 : 1; const bDiff = b.type === "differentiator" ? 0 : 1; if (aDiff !== bDiff) return aDiff - bDiff; return a.term.length - b.term.length; }) .slice(0, 80); const problemsSolved = Array.from(problemMap.values()) .sort((a, b) => severityRank(b.severity) - severityRank(a.severity)) .slice(0, 15); const competitors = Array.from(competitorMap.values()).slice(0, 10); const personas = Array.from(personaMap.values()).slice(0, 6); const useCases = Array.from(useCaseMap.values()).slice(0, 10); const features = Array.from(featureMap.values()).slice(0, 20); const base = analyses[0]; return { productName: base.productName || fallbackName, tagline: base.tagline || "", description: base.description || "", category: base.category || "", positioning: base.positioning || "", features, problemsSolved, personas, keywords, useCases, competitors, dorkQueries: [], scrapedAt: latestScrapedAt, analysisVersion: "aggregated", }; }