feat: Implement data source management and analysis flow, allowing users to add and analyze websites for project opportunities.

This commit is contained in:
2026-02-03 20:35:03 +00:00
parent 885bbbf954
commit c47614bc66
9 changed files with 587 additions and 54 deletions

View File

@@ -67,3 +67,162 @@ export const toggleDataSourceConfig = mutation({
});
},
});
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<string, any>();
const problemMap = new Map<string, any>();
const competitorMap = new Map<string, any>();
const personaMap = new Map<string, any>();
const useCaseMap = new Map<string, any>();
const featureMap = new Map<string, any>();
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",
};
}