feat: Implement data source management and analysis flow, allowing users to add and analyze websites for project opportunities.
This commit is contained in:
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user