diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2eb170c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# Repository Guidelines + +This document provides contributor guidance for this repository. + +## Project Structure & Module Organization +- `app/`: Next.js App Router pages, layouts, and API routes (e.g., `app/api/...`). +- `components/`: Reusable UI components and app-level widgets. +- `lib/`: Shared utilities, API helpers, and domain logic. +- `convex/`: Convex backend functions, schema, and auth helpers. +- `public/`: Static assets served by Next.js. +- `docs/` and `scripts/`: Reference docs and maintenance scripts. + +## Build, Test, and Development Commands +- `npm run dev`: Start the Next.js dev server. +- `npm run build`: Production build. +- `npm run start`: Run the production server locally after build. +- `npm run lint`: Run Next.js/ESLint linting. + +## Coding Style & Naming Conventions +- TypeScript + React (Next.js App Router). Use 2-space indentation as seen in existing files. +- Prefer file and folder names in `kebab-case` and React components in `PascalCase`. +- Tailwind CSS is used for styling; keep class lists ordered for readability. +- Linting: `next lint` (no Prettier config is present). + +## Testing Guidelines +- No dedicated test framework is configured yet. +- If you add tests, document the runner and add a script to `package.json`. + +## Commit & Pull Request Guidelines +- Commit history uses Conventional Commits (e.g., `feat:`, `fix:`). Follow that pattern. +- Commit changes made during a request, and only include files touched for that request. +- PRs should include: + - Clear description of changes and motivation. + - Linked issue/task if available. + - Screenshots for UI changes (before/after if relevant). + +## Security & Configuration Tips +- Copy `.env.example` to `.env.local` for local development. +- Never commit secrets (API keys, tokens). Keep them in `.env.local`. diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx deleted file mode 100644 index 9ead0da..0000000 --- a/app/(app)/dashboard/page.tsx +++ /dev/null @@ -1,319 +0,0 @@ -"use client" - -import { useQuery } from "convex/react" -import { useState } from "react" -import { api } from "@/convex/_generated/api" -import { useProject } from "@/components/project-context" -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Alert, AlertDescription } from "@/components/ui/alert" -import { Button } from "@/components/ui/button" -import { useMutation } from "convex/react" -import { Progress } from "@/components/ui/progress" - -export default function Page() { - const { selectedProjectId } = useProject() - const projects = useQuery(api.projects.getProjects) - const dataSources = useQuery( - api.dataSources.getProjectDataSources, - selectedProjectId ? { projectId: selectedProjectId as any } : "skip" - ) - const searchContext = useQuery( - api.projects.getSearchContext, - selectedProjectId ? { projectId: selectedProjectId as any } : "skip" - ) - const analysisJobs = useQuery( - api.analysisJobs.listByProject, - selectedProjectId ? { projectId: selectedProjectId as any } : "skip" - ) - const updateDataSourceStatus = useMutation(api.dataSources.updateDataSourceStatus) - const createAnalysis = useMutation(api.analyses.createAnalysis) - const createAnalysisJob = useMutation(api.analysisJobs.create) - const [reanalyzingId, setReanalyzingId] = useState(null) - const analysis = useQuery( - api.analyses.getLatestByProject, - selectedProjectId ? { projectId: selectedProjectId as any } : "skip" - ) - - const selectedProject = projects?.find((project) => project._id === selectedProjectId) - const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || [] - const isLoading = selectedProjectId && analysis === undefined - - if (!selectedProjectId && projects && projects.length === 0) { - return ( -
-
-

No projects yet

-

Complete onboarding to create your first project.

-
-
- ) - } - - if (isLoading) { - return ( -
- Loading analysis... -
- ) - } - - if (!analysis) { - return ( -
-
-

No analysis yet

-

Run onboarding to analyze a product for this project.

-
-
- ) - } - - const handleReanalyze = async (source: any) => { - if (!selectedProjectId) return - - setReanalyzingId(source._id) - await updateDataSourceStatus({ - dataSourceId: source._id, - analysisStatus: "pending", - lastError: undefined, - lastAnalyzedAt: undefined, - }) - - try { - const jobId = await createAnalysisJob({ - projectId: selectedProjectId as any, - dataSourceId: source._id, - }) - - const response = await fetch("/api/analyze", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ url: source.url, jobId }), - }) - - const data = await response.json() - - if (!response.ok) { - await updateDataSourceStatus({ - dataSourceId: source._id, - analysisStatus: "failed", - lastError: data.error || "Analysis failed", - lastAnalyzedAt: Date.now(), - }) - return - } - - await createAnalysis({ - projectId: selectedProjectId as any, - dataSourceId: source._id, - analysis: data.data, - }) - - await updateDataSourceStatus({ - dataSourceId: source._id, - analysisStatus: "completed", - lastError: undefined, - lastAnalyzedAt: Date.now(), - }) - } finally { - setReanalyzingId(null) - } - } - - return ( -
-
-
-

- {selectedProject?.name || analysis.productName} -

- {analysis.productName} -
-

{analysis.tagline}

-

{analysis.description}

-
- - {searchContext?.missingSources?.length > 0 && ( - - - Some selected sources don't have analysis yet:{" "} - {searchContext.missingSources - .map((missing: any) => - dataSources?.find((source: any) => source._id === missing.sourceId)?.name || - dataSources?.find((source: any) => source._id === missing.sourceId)?.url || - missing.sourceId - ) - .join(", ")} - . Run onboarding or re-analyze them for best results. - - - )} - -
- - - Key Features - - {analysis.features.length} - - - - Search Keywords - - {analysis.keywords.length} - - - - Target Users - - {analysis.personas.length} - - - - Competitors - - {analysis.competitors.length} - - - - Use Cases - - {analysis.useCases.length} - -
- - - - Sources - - - {dataSources && dataSources.length > 0 ? ( - dataSources.map((source: any) => ( -
- {source.name || source.url} -
- - {selectedSourceIds.includes(source._id) ? "active" : "inactive"} - - - {source.analysisStatus} - - -
-
- )) - ) : ( -

No data sources yet. Add a source during onboarding.

- )} -
-
- - {analysisJobs && analysisJobs.length > 0 && ( - - - Runs - - - {analysisJobs.slice(0, 5).map((job: any) => { - const sourceName = dataSources?.find((source: any) => source._id === job.dataSourceId)?.name - || dataSources?.find((source: any) => source._id === job.dataSourceId)?.url - || "Unknown source" - return ( -
-
-
- {sourceName}{" "} - - ({job.status}) - -
- {job.status === "failed" && ( - - )} -
- {(job.status === "running" || job.status === "pending") && ( -
- -
- {typeof job.progress === "number" ? `${job.progress}% complete` : "Starting..."} -
-
- )} - {job.status === "failed" && job.error && ( -
{job.error}
- )} -
- ) - })} -
-
- )} - - {searchContext?.context && ( - - - Combined Summary - - -
Keywords: {searchContext.context.keywords.length}
-
Problems: {searchContext.context.problemsSolved.length}
-
Competitors: {searchContext.context.competitors.length}
-
Use Cases: {searchContext.context.useCases.length}
-
-
- )} - -
- - - Key Features - - - {analysis.features.slice(0, 6).map((feature, index) => ( -
- -
-

{feature.name}

-

{feature.description}

-
-
- ))} -
-
- - - Top Problems - - - {analysis.problemsSolved.slice(0, 6).map((problem, index) => ( -
- -
-

{problem.problem}

-

{problem.emotionalImpact}

-
-
- ))} -
-
-
-
- ) -} diff --git a/app/(app)/leads/page.tsx b/app/(app)/leads/page.tsx deleted file mode 100644 index b398564..0000000 --- a/app/(app)/leads/page.tsx +++ /dev/null @@ -1,304 +0,0 @@ -'use client' - -import { useEffect, useMemo, useState } from 'react' -import { useQuery, useMutation } from 'convex/react' -import { api } from '@/convex/_generated/api' -import { useProject } from '@/components/project-context' -import { Card } from '@/components/ui/card' -import { Badge } from '@/components/ui/badge' -import { Button } from '@/components/ui/button' -import { ScrollArea } from '@/components/ui/scroll-area' -import { Separator } from '@/components/ui/separator' -import { Label } from '@/components/ui/label' -import { Textarea } from '@/components/ui/textarea' -import { ExternalLink, Mail, Tag, Target } from 'lucide-react' -type Lead = { - _id: string - title: string - url: string - snippet: string - platform: string - relevanceScore: number - intent: string - status?: string - matchedKeywords: string[] - matchedProblems: string[] - suggestedApproach: string - softPitch: boolean - createdAt: number - notes?: string - tags?: string[] -} - -export default function LeadsPage() { - const { selectedProjectId } = useProject() - const leads = useQuery( - api.opportunities.listByProject, - selectedProjectId - ? { - projectId: selectedProjectId as any, - limit: 200, - } - : "skip" - ) - const updateOpportunity = useMutation(api.opportunities.updateStatus) - const [selectedId, setSelectedId] = useState(null) - const [notes, setNotes] = useState('') - const [tags, setTags] = useState('') - const [sourceFilter, setSourceFilter] = useState('all') - const [ageFilter, setAgeFilter] = useState('all') - - const sortedLeads = useMemo(() => { - if (!leads) return [] - const normalized = leads as Lead[] - const now = Date.now() - const ageLimit = - ageFilter === '24h' - ? now - 24 * 60 * 60 * 1000 - : ageFilter === '7d' - ? now - 7 * 24 * 60 * 60 * 1000 - : ageFilter === '30d' - ? now - 30 * 24 * 60 * 60 * 1000 - : null - const filtered = normalized.filter((lead) => { - if (sourceFilter !== 'all' && lead.platform !== sourceFilter) return false - if (ageLimit && lead.createdAt < ageLimit) return false - return true - }) - return [...filtered].sort((a, b) => b.createdAt - a.createdAt) - }, [leads, sourceFilter, ageFilter]) - - const sourceOptions = useMemo(() => { - if (!leads) return [] - const set = new Set((leads as Lead[]).map((lead) => lead.platform)) - return Array.from(set).sort((a, b) => a.localeCompare(b)) - }, [leads]) - - const selectedLead = useMemo(() => { - if (!sortedLeads.length) return null - const found = sortedLeads.find((lead) => lead._id === selectedId) - return found ?? sortedLeads[0] - }, [sortedLeads, selectedId]) - - useEffect(() => { - if (!selectedLead) return - setNotes(selectedLead.notes || '') - setTags(selectedLead.tags?.join(', ') || '') - }, [selectedLead]) - - const handleSelect = (lead: Lead) => { - setSelectedId(lead._id) - setNotes(lead.notes || '') - setTags(lead.tags?.join(', ') || '') - } - - const handleSave = async () => { - if (!selectedLead) return - const tagList = tags - .split(',') - .map((tag) => tag.trim()) - .filter(Boolean) - await updateOpportunity({ - id: selectedLead._id as any, - status: selectedLead.status || 'new', - notes: notes || undefined, - tags: tagList.length ? tagList : undefined, - }) - } - - return ( -
-
-
-
- - Inbox -
-

Newest opportunities ready for outreach.

-
-
-
-
- - -
-
- - -
-
-
- -
- {sortedLeads.length === 0 && ( - - No leads yet. Run a search to populate the inbox. - - )} - {sortedLeads.map((lead) => ( - - ))} -
-
-
- -
-
-
-
-
Lead
-

Review the thread before reaching out.

-
- {selectedLead && ( - - )} -
-
- -
- {selectedLead ? ( - <> - -
-
- - {Math.round(selectedLead.relevanceScore * 100)}% match - - - {selectedLead.intent} - - - {selectedLead.status || 'new'} - -
-
{selectedLead.title}
-

{selectedLead.snippet}

-
-
- - -
-
- -
- {selectedLead.matchedKeywords.map((keyword, index) => ( - - {keyword} - - ))} -
-
-
- -
- {selectedLead.matchedProblems.map((problem, index) => ( - - {problem} - - ))} -
-
-
- -
-
- -

{selectedLead.suggestedApproach}

-
-
- -

- {selectedLead.softPitch ? 'Use a softer, story-led opener.' : 'Lead with a direct solution.'} -

-
-
-
- - -
- -