This commit is contained in:
2026-02-04 01:05:00 +00:00
parent f9222627ef
commit d02d95e680
30 changed files with 2449 additions and 326 deletions

View File

@@ -0,0 +1,87 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { convexAuthNextjsToken, isAuthenticatedNextjs } from "@convex-dev/auth/nextjs/server";
import { fetchMutation, fetchQuery } from "convex/nextjs";
import { api } from "@/convex/_generated/api";
import { scrapeWebsite, analyzeFromText } from "@/lib/scraper";
import { repromptSection } from "@/lib/analysis-pipeline";
const bodySchema = z.object({
analysisId: z.string().min(1),
sectionKey: z.enum([
"profile",
"features",
"competitors",
"keywords",
"problems",
"personas",
"useCases",
"dorkQueries",
]),
prompt: z.string().optional(),
});
export async function POST(request: NextRequest) {
if (!(await isAuthenticatedNextjs())) {
const redirectUrl = new URL("/auth", request.url);
const referer = request.headers.get("referer");
const nextPath = referer ? new URL(referer).pathname + new URL(referer).search : "/";
redirectUrl.searchParams.set("next", nextPath);
return NextResponse.redirect(redirectUrl);
}
const body = await request.json();
const parsed = bodySchema.parse(body);
const token = await convexAuthNextjsToken();
const analysis = await fetchQuery(
api.analyses.getById,
{ analysisId: parsed.analysisId as any },
{ token }
);
if (!analysis) {
return NextResponse.json({ error: "Analysis not found." }, { status: 404 });
}
const dataSource = await fetchQuery(
api.dataSources.getById,
{ dataSourceId: analysis.dataSourceId as any },
{ token }
);
if (!dataSource) {
return NextResponse.json({ error: "Data source not found." }, { status: 404 });
}
const isManual = dataSource.url.startsWith("manual:") || dataSource.url === "manual-input";
const featureText = (analysis.features || []).map((f: any) => f.name).join("\n");
const content = isManual
? await analyzeFromText(
analysis.productName,
analysis.description || "",
featureText
)
: await scrapeWebsite(dataSource.url);
const items = await repromptSection(
parsed.sectionKey,
content,
analysis as any,
parsed.prompt
);
await fetchMutation(
api.analysisSections.replaceSection,
{
analysisId: parsed.analysisId as any,
sectionKey: parsed.sectionKey,
items,
lastPrompt: parsed.prompt,
source: "ai",
},
{ token }
);
return NextResponse.json({ success: true, items });
}

21
app/api/checkout/route.ts Normal file
View File

@@ -0,0 +1,21 @@
import { Checkout } from "@polar-sh/nextjs";
import { NextResponse } from "next/server";
export const GET = async () => {
if (!process.env.POLAR_ACCESS_TOKEN || !process.env.POLAR_SUCCESS_URL) {
return NextResponse.json(
{
error:
"Missing POLAR_ACCESS_TOKEN or POLAR_SUCCESS_URL environment variables.",
},
{ status: 400 }
);
}
const handler = Checkout({
accessToken: process.env.POLAR_ACCESS_TOKEN,
successUrl: process.env.POLAR_SUCCESS_URL,
});
return handler();
};

View File

@@ -17,7 +17,9 @@ const searchSchema = z.object({
icon: z.string().optional(),
enabled: z.boolean(),
searchTemplate: z.string().optional(),
rateLimit: z.number()
rateLimit: z.number(),
site: z.string().optional(),
custom: z.boolean().optional()
})),
strategies: z.array(z.string()),
maxResults: z.number().default(50)
@@ -115,9 +117,21 @@ export async function POST(request: NextRequest) {
);
}
const resultUrls = Array.from(
new Set(searchResults.map((result) => result.url).filter(Boolean))
)
const existingUrls = await fetchQuery(
api.seenUrls.listExisting,
{ projectId: projectId as any, urls: resultUrls },
{ token }
)
const existingSet = new Set(existingUrls)
const newUrls = resultUrls.filter((url) => !existingSet.has(url))
const filteredResults = searchResults.filter((result) => !existingSet.has(result.url))
// Score and rank
console.log(' Scoring opportunities...')
const opportunities = scoreOpportunities(searchResults, analysis as EnhancedProductAnalysis)
const opportunities = scoreOpportunities(filteredResults, analysis as EnhancedProductAnalysis)
console.log(` ✓ Scored ${opportunities.length} opportunities`)
if (jobId) {
await fetchMutation(
@@ -135,18 +149,22 @@ export async function POST(request: NextRequest) {
);
}
if (newUrls.length > 0) {
await fetchMutation(
api.seenUrls.markSeenBatch,
{ projectId: projectId as any, urls: newUrls, source: "search" },
{ token }
);
}
return NextResponse.json({
success: true,
data: {
opportunities: opportunities.slice(0, 50),
stats: {
queriesGenerated: queries.length,
rawResults: searchResults.length,
opportunitiesFound: opportunities.length,
highRelevance: opportunities.filter(o => o.relevanceScore >= 0.7).length,
averageScore: opportunities.length > 0
? opportunities.reduce((a, o) => a + o.relevanceScore, 0) / opportunities.length
: 0
rawResults: filteredResults.length,
opportunitiesFound: opportunities.length
},
queries: queries.map(q => ({
query: q.query,