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

@@ -89,6 +89,7 @@ export default function OpportunitiesPage() {
const [replyText, setReplyText] = useState('')
const [stats, setStats] = useState<any>(null)
const [searchError, setSearchError] = useState('')
const [missingSources, setMissingSources] = useState<any[]>([])
const [statusFilter, setStatusFilter] = useState('all')
const [intentFilter, setIntentFilter] = useState('all')
const [minScore, setMinScore] = useState(0)
@@ -97,6 +98,7 @@ export default function OpportunitiesPage() {
const [notesInput, setNotesInput] = useState('')
const [tagsInput, setTagsInput] = useState('')
const projects = useQuery(api.projects.getProjects)
const latestAnalysis = useQuery(
api.analyses.getLatestByProject,
selectedProjectId ? { projectId: selectedProjectId as any } : 'skip'
@@ -137,12 +139,21 @@ export default function OpportunitiesPage() {
})) as Opportunity[]
}, [savedOpportunities, opportunities])
const selectedSources = useQuery(
api.dataSources.getProjectDataSources,
selectedProjectId ? { projectId: selectedProjectId as any } : "skip"
)
const selectedProject = projects?.find((project: any) => project._id === selectedProjectId)
const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || []
const activeSources = selectedSources?.filter((source: any) =>
selectedSourceIds.includes(source._id)
) || []
useEffect(() => {
const stored = localStorage.getItem('productAnalysis')
if (stored) {
setAnalysis(JSON.parse(stored))
} else {
router.push('/onboarding')
}
fetch('/api/opportunities')
@@ -160,6 +171,18 @@ export default function OpportunitiesPage() {
})
}, [router])
useEffect(() => {
if (!analysis && latestAnalysis) {
setAnalysis(latestAnalysis as any)
}
}, [analysis, latestAnalysis])
useEffect(() => {
if (!analysis && latestAnalysis === null) {
router.push('/onboarding')
}
}, [analysis, latestAnalysis, router])
const togglePlatform = (platformId: string) => {
setPlatforms(prev => prev.map(p =>
p.id === platformId ? { ...p, enabled: !p.enabled } : p
@@ -193,7 +216,7 @@ export default function OpportunitiesPage() {
const response = await fetch('/api/opportunities', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ analysis, config })
body: JSON.stringify({ projectId: selectedProjectId, config })
})
if (response.redirected) {
@@ -202,7 +225,11 @@ export default function OpportunitiesPage() {
}
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to search for opportunities')
}
if (data.success) {
const mapped = data.data.opportunities.map((opp: Opportunity) => ({
...opp,
@@ -211,6 +238,7 @@ export default function OpportunitiesPage() {
setOpportunities(mapped)
setGeneratedQueries(data.data.queries)
setStats(data.data.stats)
setMissingSources(data.data.missingSources || [])
await upsertOpportunities({
projectId: selectedProjectId as any,
@@ -353,7 +381,11 @@ export default function OpportunitiesPage() {
<div className="p-4 border-t border-border">
<Button
onClick={executeSearch}
disabled={isSearching || platforms.filter(p => p.enabled).length === 0}
disabled={
isSearching ||
platforms.filter(p => p.enabled).length === 0 ||
selectedSourceIds.length === 0
}
className="w-full"
>
{isSearching ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Searching...</> : <><Search className="mr-2 h-4 w-4" /> Find Opportunities</>}
@@ -389,6 +421,41 @@ export default function OpportunitiesPage() {
</Alert>
)}
{/* Active Sources */}
{activeSources.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-sm">Active Data Sources</CardTitle>
<CardDescription>
Sources selected for this project will drive opportunity search.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
{activeSources.map((source: any) => (
<Badge key={source._id} variant="secondary">
{source.name || source.url}
</Badge>
))}
</CardContent>
</Card>
)}
{selectedSources && selectedSourceIds.length === 0 && (
<Alert variant="destructive">
<AlertDescription>
No data sources selected. Add and select sources to generate opportunities.
</AlertDescription>
</Alert>
)}
{missingSources.length > 0 && (
<Alert>
<AlertDescription>
Some selected sources don&apos;t have analysis yet. Run onboarding or re-analyze them for best results.
</AlertDescription>
</Alert>
)}
{/* Generated Queries */}
{generatedQueries.length > 0 && (
<Collapsible open={showQueries} onOpenChange={setShowQueries}>