fixed docker

This commit is contained in:
2026-02-04 15:37:59 +00:00
parent 6d271ef65b
commit 025ce8f763
16 changed files with 135 additions and 93 deletions

View File

@@ -90,7 +90,7 @@ export async function POST(request: NextRequest) {
status: "failed",
error: "OpenAI API key not configured",
timeline: timeline.map((item) =>
item.status === "running" ? { ...item, status: "failed" } : item
item.status === "running" ? { ...item, status: "failed" as const } : item
),
},
{ token }
@@ -259,7 +259,7 @@ export async function POST(request: NextRequest) {
status: "failed",
error: error.message || "Manual analysis failed",
timeline: timeline.map((item) =>
item.status === "running" ? { ...item, status: "failed" } : item
item.status === "running" ? { ...item, status: "failed" as const } : item
),
},
{ token }

View File

@@ -89,7 +89,7 @@ export async function POST(request: NextRequest) {
status: "failed",
error: "OpenAI API key not configured",
timeline: timeline.map((item) =>
item.status === "running" ? { ...item, status: "failed" } : item
item.status === "running" ? { ...item, status: "failed" as const } : item
),
},
{ token }
@@ -270,7 +270,7 @@ export async function POST(request: NextRequest) {
status: "failed",
error: error.message || "Analysis failed",
timeline: timeline.map((item) =>
item.status === "running" ? { ...item, status: "failed" } : item
item.status === "running" ? { ...item, status: "failed" as const } : item
),
},
{ token }

View File

@@ -1,7 +1,7 @@
import { Checkout } from "@polar-sh/nextjs";
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
export const GET = async () => {
export const GET = async (request: NextRequest) => {
if (!process.env.POLAR_ACCESS_TOKEN || !process.env.POLAR_SUCCESS_URL) {
return NextResponse.json(
{
@@ -17,5 +17,5 @@ export const GET = async () => {
successUrl: process.env.POLAR_SUCCESS_URL,
});
return handler();
return handler(request);
};

View File

@@ -39,6 +39,7 @@ const bodySchema = z.object({
})
export async function POST(request: NextRequest) {
let ageFilters: SerperAgeFilter | undefined
try {
const requestId = request.headers.get("x-request-id") ?? undefined;
if (!(await isAuthenticatedNextjs())) {
@@ -51,7 +52,7 @@ export async function POST(request: NextRequest) {
const body = await request.json()
const { analysis, minAgeDays, maxAgeDays } = bodySchema.parse(body)
const ageFilters: SerperAgeFilter = {
ageFilters = {
minAgeDays,
maxAgeDays,
}
@@ -287,15 +288,15 @@ async function analyzeOpportunities(
const relevanceScore = Math.min(keywordScore + problemScore, 1)
// Determine intent
let intent: Opportunity['intent'] = 'looking-for'
let intent: Opportunity['intent'] = 'looking'
if (content.includes('frustrated') || content.includes('hate') || content.includes('sucks')) {
intent = 'frustrated'
} else if (content.includes('alternative') || content.includes('switching')) {
intent = 'alternative'
intent = 'comparing'
} else if (content.includes('vs') || content.includes('comparison') || content.includes('better')) {
intent = 'comparison'
intent = 'comparing'
} else if (content.includes('how to') || content.includes('fix') || content.includes('solution')) {
intent = 'problem-solving'
intent = 'learning'
}
// Find matching persona
@@ -305,16 +306,21 @@ async function analyzeOpportunities(
if (relevanceScore >= 0.3) {
opportunities.push({
id: result.url,
title: result.title,
url: result.url,
platform: result.source,
source: result.source,
snippet: result.snippet.slice(0, 300),
relevanceScore,
painPoints: matchedProblems.slice(0, 3),
suggestedApproach: generateApproach(intent, analysis.productName),
matchedKeywords: matchedKeywords.slice(0, 5),
matchedProblems: matchedProblems.slice(0, 3),
matchedPersona,
intent
intent,
emotionalIntensity: intent === 'frustrated' ? 'high' : matchedProblems.length > 0 ? 'medium' : 'low',
status: 'new',
suggestedApproach: generateApproach(intent, analysis.productName),
softPitch: false
})
}
}

View File

@@ -1,12 +1,9 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import ConvexClientProvider from './ConvexClientProvider'
import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server";
import { ThemeProvider } from "@/components/theme-provider";
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Sanati - Find Product Opportunities',
description: 'AI-powered product research and opportunity finding',
@@ -20,7 +17,7 @@ export default function RootLayout({
return (
<ConvexAuthNextjsServerProvider>
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<body className="font-sans">
<ThemeProvider
attribute="class"
defaultTheme="dark"

View File

@@ -69,19 +69,19 @@ export default function OnboardingPage() {
try {
await createAnalysis({
projectId: resolved.projectId,
dataSourceId: resolved.sourceId,
projectId: resolved.projectId as any,
dataSourceId: resolved.sourceId as any,
analysis,
})
await updateDataSourceStatus({
dataSourceId: resolved.sourceId,
dataSourceId: resolved.sourceId as any,
analysisStatus: 'completed',
lastAnalyzedAt: Date.now(),
})
} catch (err: any) {
await updateDataSourceStatus({
dataSourceId: resolved.sourceId,
dataSourceId: resolved.sourceId as any,
analysisStatus: 'failed',
lastError: err?.message || 'Failed to save analysis',
lastAnalyzedAt: Date.now(),
@@ -106,7 +106,7 @@ export default function OnboardingPage() {
})
await updateDataSourceStatus({
dataSourceId: sourceId,
dataSourceId: sourceId as any,
analysisStatus: 'pending',
lastError: undefined,
lastAnalyzedAt: undefined,
@@ -116,8 +116,8 @@ export default function OnboardingPage() {
setPendingProjectId(projectId)
const jobId = await createAnalysisJob({
projectId,
dataSourceId: sourceId,
projectId: projectId as any,
dataSourceId: sourceId as any,
})
setPendingJobId(jobId)
@@ -175,7 +175,7 @@ export default function OnboardingPage() {
setError(err.message || 'Failed to analyze website')
if (pendingSourceId && !manualFallback) {
await updateDataSourceStatus({
dataSourceId: pendingSourceId,
dataSourceId: pendingSourceId as any,
analysisStatus: 'failed',
lastError: err?.message || 'Failed to analyze',
lastAnalyzedAt: Date.now(),
@@ -255,8 +255,8 @@ export default function OnboardingPage() {
if (!resolvedJobId && resolvedProjectId && resolvedSourceId) {
resolvedJobId = await createAnalysisJob({
projectId: resolvedProjectId,
dataSourceId: resolvedSourceId,
projectId: resolvedProjectId as any,
dataSourceId: resolvedSourceId as any,
})
setPendingJobId(resolvedJobId)
}
@@ -307,8 +307,8 @@ export default function OnboardingPage() {
analysis: finalAnalysis,
sourceUrl: manualSourceUrl,
sourceName: finalAnalysis.productName,
projectId: resolvedProjectId || undefined,
dataSourceId: resolvedSourceId || undefined,
projectId: (resolvedProjectId || undefined) as any,
dataSourceId: (resolvedSourceId || undefined) as any,
})
}
@@ -324,7 +324,7 @@ export default function OnboardingPage() {
setError(err.message || 'Failed to analyze')
if (pendingSourceId) {
await updateDataSourceStatus({
dataSourceId: pendingSourceId,
dataSourceId: pendingSourceId as any,
analysisStatus: 'failed',
lastError: err?.message || 'Failed to analyze',
lastAnalyzedAt: Date.now(),

View File

@@ -45,7 +45,7 @@ const itemVariants = {
y: 0,
transition: {
duration: 0.5,
ease: "easeOut",
ease: [0.16, 1, 0.3, 1] as [number, number, number, number],
},
},
};

View File

@@ -23,7 +23,7 @@ const itemVariants = {
y: 0,
transition: {
duration: 0.8,
ease: [0.22, 1, 0.36, 1], // Custom easing (approx. easeOutQuint/Expo) for a premium feel
ease: [0.22, 1, 0.36, 1] as [number, number, number, number], // Custom easing (approx. easeOutQuint/Expo) for a premium feel
},
},
};

View File

@@ -100,7 +100,12 @@ export const getById = query({
export const listByProject = query({
args: {
projectId: v.id("projects"),
status: v.optional(v.string()),
status: v.optional(v.union(
v.literal("pending"),
v.literal("running"),
v.literal("completed"),
v.literal("failed")
)),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);

View File

@@ -58,7 +58,7 @@ export const listByProject = query({
if (args.minScore !== undefined) {
queryBuilder = queryBuilder.filter((q) =>
q.gte(q.field("relevanceScore"), args.minScore)
q.gte(q.field("relevanceScore"), args.minScore as number)
);
}

View File

@@ -64,7 +64,12 @@ export const update = mutation({
export const listByProject = query({
args: {
projectId: v.id("projects"),
status: v.optional(v.string()),
status: v.optional(v.union(
v.literal("pending"),
v.literal("running"),
v.literal("completed"),
v.literal("failed")
)),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);

View File

@@ -180,13 +180,20 @@ Return JSON:
const analysis = JSON.parse(jsonStr)
return {
id: result.url,
title: result.title,
url: result.url,
platform: result.source,
source: result.source,
snippet: result.snippet.slice(0, 300),
relevanceScore: analysis.relevanceScore || 0,
painPoints: analysis.painPoints || [],
suggestedApproach: analysis.suggestedApproach || ''
emotionalIntensity: (analysis.painPoints || []).length > 2 ? "high" : (analysis.painPoints || []).length > 0 ? "medium" : "low",
intent: "looking",
matchedKeywords: [],
matchedProblems: analysis.painPoints || [],
status: "new",
suggestedApproach: analysis.suggestedApproach || '',
softPitch: false,
}
} catch (e) {
// Fallback simple analysis
@@ -195,13 +202,20 @@ Return JSON:
const relevance = Math.min(overlap / Math.max(product.keywords.length * 0.5, 1), 1)
return {
id: result.url,
title: result.title,
url: result.url,
platform: result.source,
source: result.source,
snippet: result.snippet.slice(0, 300),
relevanceScore: relevance,
painPoints: ['Related to product domain'],
suggestedApproach: 'Share relevant insights about their problem'
emotionalIntensity: "low",
intent: "looking",
matchedKeywords: product.keywords.slice(0, 5),
matchedProblems: ['Related to product domain'],
status: "new",
suggestedApproach: 'Share relevant insights about their problem',
softPitch: false,
}
}
}

View File

@@ -24,7 +24,7 @@ export async function scrapeWebsite(url: string): Promise<ScrapedContent> {
let browser
try {
browser = await puppeteer.launch({
headless: 'new',
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
})

86
package-lock.json generated
View File

@@ -33,7 +33,7 @@
"jose": "^6.1.3",
"lucide-react": "^0.563.0",
"mini-svg-data-uri": "^1.4.4",
"next": "^15.0.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
"openai": "^4.28.0",
"puppeteer": "^22.0.0",
@@ -1119,15 +1119,15 @@
}
},
"node_modules/@next/env": {
"version": "15.5.11",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.11.tgz",
"integrity": "sha512-g9s5SS9gC7GJCEOR3OV3zqs7C5VddqxP9X+/6BpMbdXRkqsWfFf2CJPBZNvNEtAkKTNuRgRXAgNxSAXzfLdaTg==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
"cpu": [
"arm64"
],
@@ -1141,9 +1141,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
"cpu": [
"x64"
],
@@ -1157,9 +1157,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
"cpu": [
"arm64"
],
@@ -1173,9 +1173,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
"cpu": [
"arm64"
],
@@ -1189,9 +1189,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
"integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
"integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
"cpu": [
"x64"
],
@@ -1205,9 +1205,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
"cpu": [
"x64"
],
@@ -1221,9 +1221,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
"cpu": [
"arm64"
],
@@ -1237,9 +1237,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
"cpu": [
"x64"
],
@@ -2821,7 +2821,6 @@
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.19",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -4083,13 +4082,14 @@
}
},
"node_modules/next": {
"version": "15.5.11",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.11.tgz",
"integrity": "sha512-L2KPiKmqTDpRdeVDdPjhf43g2/VPe0NCNndq7OKDCgOLWtxe1kbr/zXGIZtYY7kZEAjRf7Bj/mwUFSr+tYC2Yg==",
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
"license": "MIT",
"dependencies": {
"@next/env": "15.5.11",
"@next/env": "16.1.6",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -4098,18 +4098,18 @@
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.5.7",
"@next/swc-darwin-x64": "15.5.7",
"@next/swc-linux-arm64-gnu": "15.5.7",
"@next/swc-linux-arm64-musl": "15.5.7",
"@next/swc-linux-x64-gnu": "15.5.7",
"@next/swc-linux-x64-musl": "15.5.7",
"@next/swc-win32-arm64-msvc": "15.5.7",
"@next/swc-win32-x64-msvc": "15.5.7",
"sharp": "^0.34.3"
"@next/swc-darwin-arm64": "16.1.6",
"@next/swc-darwin-x64": "16.1.6",
"@next/swc-linux-arm64-gnu": "16.1.6",
"@next/swc-linux-arm64-musl": "16.1.6",
"@next/swc-linux-x64-gnu": "16.1.6",
"@next/swc-linux-x64-musl": "16.1.6",
"@next/swc-win32-arm64-msvc": "16.1.6",
"@next/swc-win32-x64-msvc": "16.1.6",
"sharp": "^0.34.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"build": "NEXT_DISABLE_TURBOPACK=1 next build",
"start": "next start",
"lint": "next lint"
},
@@ -34,7 +34,7 @@
"jose": "^6.1.3",
"lucide-react": "^0.563.0",
"mini-svg-data-uri": "^1.4.4",
"next": "^15.0.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
"openai": "^4.28.0",
"puppeteer": "^22.0.0",

View File

@@ -1,6 +1,10 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -10,7 +14,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -18,9 +22,20 @@
}
],
"paths": {
"@/*": ["./*"]
}
"@/*": [
"./*"
]
},
"target": "ES2017"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}