fixed docker

This commit is contained in:
2026-02-04 14:29:42 +00:00
parent f1e13f87f6
commit b0ef60ff32
6 changed files with 704 additions and 205 deletions

View File

@@ -23,7 +23,7 @@ const searchSchema = z.object({
custom: z.boolean().optional() custom: z.boolean().optional()
})), })),
strategies: z.array(z.string()), strategies: z.array(z.string()),
maxResults: z.number().default(50) maxResults: z.number().default(50),
minAgeDays: z.number().min(0).max(365).optional(), minAgeDays: z.number().min(0).max(365).optional(),
maxAgeDays: z.number().min(0).max(365).optional() maxAgeDays: z.number().min(0).max(365).optional()
}) })

View File

@@ -68,6 +68,8 @@ import type {
} from '@/lib/types' } from '@/lib/types'
import { estimateSearchTime } from '@/lib/query-generator' import { estimateSearchTime } from '@/lib/query-generator'
const AGE_OPTIONS = [0, 1, 3, 7, 14, 30, 60, 90, 180, 365]
const STRATEGY_INFO: Record<SearchStrategy, { name: string; description: string }> = { const STRATEGY_INFO: Record<SearchStrategy, { name: string; description: string }> = {
'direct-keywords': { name: 'Direct Keywords', description: 'People looking for your product category' }, 'direct-keywords': { name: 'Direct Keywords', description: 'People looking for your product category' },
'problem-pain': { name: 'Problem/Pain', description: 'People experiencing problems you solve' }, 'problem-pain': { name: 'Problem/Pain', description: 'People experiencing problems you solve' },
@@ -176,21 +178,21 @@ const GOAL_PRESETS: {
title: "High-intent leads", title: "High-intent leads",
description: "Shortlist people actively searching to buy or switch.", description: "Shortlist people actively searching to buy or switch.",
strategies: ["direct-keywords", "competitor-alternative", "comparison", "recommendation"], strategies: ["direct-keywords", "competitor-alternative", "comparison", "recommendation"],
maxQueries: 30, maxQueries: 10,
}, },
{ {
id: "pain-first", id: "pain-first",
title: "Problem pain", title: "Problem pain",
description: "Find people expressing frustration or blockers.", description: "Find people expressing frustration or blockers.",
strategies: ["problem-pain", "emotional-frustrated"], strategies: ["problem-pain", "emotional-frustrated"],
maxQueries: 40, maxQueries: 15,
}, },
{ {
id: "market-scan", id: "market-scan",
title: "Market scan", title: "Market scan",
description: "Broader sweep to map demand and platforms.", description: "Broader sweep to map demand and platforms.",
strategies: ["direct-keywords", "problem-pain", "how-to", "recommendation"], strategies: ["direct-keywords", "problem-pain", "how-to", "recommendation"],
maxQueries: 50, maxQueries: 20,
}, },
] ]
@@ -208,7 +210,7 @@ export default function OpportunitiesPage() {
'problem-pain', 'problem-pain',
'competitor-alternative' 'competitor-alternative'
]) ])
const [maxQueries, setMaxQueries] = useState(50) const [maxQueries, setMaxQueries] = useState(20)
const [minAgeDays, setMinAgeDays] = useState(0) const [minAgeDays, setMinAgeDays] = useState(0)
const [maxAgeDays, setMaxAgeDays] = useState(30) const [maxAgeDays, setMaxAgeDays] = useState(30)
const [goalPreset, setGoalPreset] = useState<string>('high-intent') const [goalPreset, setGoalPreset] = useState<string>('high-intent')
@@ -338,7 +340,7 @@ export default function OpportunitiesPage() {
setStrategies(parsed.strategies) setStrategies(parsed.strategies)
} }
if (typeof parsed.maxQueries === 'number') { if (typeof parsed.maxQueries === 'number') {
setMaxQueries(Math.min(Math.max(parsed.maxQueries, 10), 50)) setMaxQueries(Math.min(Math.max(parsed.maxQueries, 5), 20))
} }
if (typeof parsed.goalPreset === 'string') { if (typeof parsed.goalPreset === 'string') {
setGoalPreset(parsed.goalPreset) setGoalPreset(parsed.goalPreset)
@@ -366,7 +368,7 @@ export default function OpportunitiesPage() {
} }
} else if (defaultPlatformsRef.current) { } else if (defaultPlatformsRef.current) {
setStrategies(['direct-keywords', 'problem-pain', 'competitor-alternative']) setStrategies(['direct-keywords', 'problem-pain', 'competitor-alternative'])
setMaxQueries(50) setMaxQueries(20)
setGoalPreset('high-intent') setGoalPreset('high-intent')
setPlatforms(defaultPlatformsRef.current) setPlatforms(defaultPlatformsRef.current)
} }
@@ -495,7 +497,7 @@ export default function OpportunitiesPage() {
searchTemplate: platform.searchTemplate ?? "", searchTemplate: platform.searchTemplate ?? "",
})), })),
strategies, strategies,
maxResults: Math.min(maxQueries, 50), maxResults: Math.min(maxQueries, 20),
minAgeDays: minAgeDays > 0 ? minAgeDays : undefined, minAgeDays: minAgeDays > 0 ? minAgeDays : undefined,
maxAgeDays: maxAgeDays > 0 ? maxAgeDays : undefined, maxAgeDays: maxAgeDays > 0 ? maxAgeDays : undefined,
} }
@@ -745,7 +747,68 @@ export default function OpportunitiesPage() {
</ScrollArea> </ScrollArea>
<div className="p-4 border-t border-border space-y-2"> <div className="p-4 border-t border-border space-y-4">
<div className="space-y-3">
<Label className="text-sm font-medium uppercase text-muted-foreground">Lead freshness</Label>
<div className="text-xs text-muted-foreground flex items-center justify-between">
<span>Set min/max age to target thread freshness</span>
<span>
{minAgeDays === 0 ? 'newest' : `${minAgeDays}+ days`} {maxAgeDays > 0 ? `up to ${maxAgeDays} days` : 'any age'}
</span>
</div>
<div className="space-y-2">
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Min age (older)</span>
<span>{minAgeDays} day{minAgeDays === 1 ? '' : 's'}</span>
</div>
<select
value={String(minAgeDays)}
onChange={(event) => {
const next = Number(event.target.value)
const limited = maxAgeDays > 0 ? Math.min(next, maxAgeDays) : next
setMinAgeDays(limited)
}}
className="h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
{AGE_OPTIONS.map((value) => (
<option
key={`min-age-${value}`}
value={value}
disabled={maxAgeDays > 0 && value > maxAgeDays}
>
{value === 0 ? 'Newest (0 days)' : `${value} day${value === 1 ? '' : 's'}`}
</option>
))}
</select>
</div>
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Max age (newer)</span>
<span>{maxAgeDays > 0 ? `${maxAgeDays} days` : 'Any'}</span>
</div>
<select
value={String(maxAgeDays)}
onChange={(event) => {
const next = Number(event.target.value)
const adjusted = next === 0 ? 0 : Math.max(next, minAgeDays)
setMaxAgeDays(adjusted)
}}
className="h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
{AGE_OPTIONS.map((value) => (
<option
key={`max-age-${value}`}
value={value}
disabled={value !== 0 && value < minAgeDays}
>
{value === 0 ? 'Any age' : `${value} day${value === 1 ? '' : 's'}`}
</option>
))}
</select>
</div>
</div>
</div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium uppercase text-muted-foreground">Search Volume</Label> <Label className="text-sm font-medium uppercase text-muted-foreground">Search Volume</Label>
<div className="space-y-2"> <div className="space-y-2">
@@ -756,8 +819,8 @@ export default function OpportunitiesPage() {
<Slider <Slider
value={[maxQueries]} value={[maxQueries]}
onValueChange={([v]) => setMaxQueries(v)} onValueChange={([v]) => setMaxQueries(v)}
min={10} min={5}
max={50} max={20}
step={5} step={5}
/> />
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
@@ -783,9 +846,6 @@ export default function OpportunitiesPage() {
<span>·</span> <span>·</span>
<span>max {maxQueries} queries</span> <span>max {maxQueries} queries</span>
</div> </div>
<div className="text-xs text-muted-foreground">
Lead age window: {minAgeDays === 0 ? 'newest' : `${minAgeDays}+ days old`} {maxAgeDays > 0 ? `up to ${maxAgeDays} days` : 'any age'}
</div>
{platforms.filter(p => p.enabled).length === 0 && ( {platforms.filter(p => p.enabled).length === 0 && (
<p className="text-xs text-muted-foreground">Select at least one source to search.</p> <p className="text-xs text-muted-foreground">Select at least one source to search.</p>
)} )}
@@ -852,42 +912,6 @@ export default function OpportunitiesPage() {
</div> </div>
))} ))}
</div> </div>
<div className="space-y-3">
<Label className="text-sm font-medium uppercase text-muted-foreground">Lead freshness</Label>
<p className="text-xs text-muted-foreground">
Restrict opportunities by lead age. Set a maximum age to avoid archived threads and an optional minimum age to skip brand-new posts.
</p>
<div className="space-y-2">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Min age (older than)</span>
<span>{minAgeDays} day{minAgeDays === 1 ? '' : 's'}</span>
</div>
<Slider
value={[minAgeDays]}
onValueChange={([value]) => {
const limited = maxAgeDays > 0 ? Math.min(value, maxAgeDays) : value
setMinAgeDays(limited)
}}
min={0}
max={365}
step={1}
/>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Max age (newer than)</span>
<span>{maxAgeDays > 0 ? `${maxAgeDays} days` : 'Any'}</span>
</div>
<Slider
value={[maxAgeDays]}
onValueChange={([value]) => {
const nextMax = Math.max(value, minAgeDays)
setMaxAgeDays(nextMax)
}}
min={0}
max={365}
step={1}
/>
</div>
</div>
</div> </div>
</ScrollArea> </ScrollArea>
</DialogContent> </DialogContent>

View File

@@ -96,6 +96,15 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
api.analysisJobs.getById, api.analysisJobs.getById,
pendingJobId ? { jobId: pendingJobId as any } : "skip" pendingJobId ? { jobId: pendingJobId as any } : "skip"
); );
const opportunities = useQuery(
api.opportunities.listByProject,
selectedProjectId
? {
projectId: selectedProjectId as any,
limit: 200,
}
: "skip"
);
// Set default selected project // Set default selected project
React.useEffect(() => { React.useEffect(() => {
@@ -118,6 +127,18 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const editingProject = projects?.find((project) => project._id === editingProjectId); const editingProject = projects?.find((project) => project._id === editingProjectId);
const canDeleteProject = (projects?.length ?? 0) > 1; const canDeleteProject = (projects?.length ?? 0) > 1;
const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || []; const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || [];
const inboxCount = React.useMemo(() => {
if (!opportunities) return 0;
const normalized = opportunities as { status?: string }[];
return normalized.filter((lead) => {
const status = lead.status ?? "new";
if (status === "ignored") return false;
if (status === "converted") return false;
if (status === "archived") return false;
if (status === "sent") return false;
return true;
}).length;
}, [opportunities]);
const selectedProjectName = selectedProject?.name || "Select Project"; const selectedProjectName = selectedProject?.name || "Select Project";
const handleToggle = async (sourceId: string, checked: boolean) => { const handleToggle = async (sourceId: string, checked: boolean) => {
@@ -416,7 +437,13 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
> >
<Link href="/app/inbox"> <Link href="/app/inbox">
<Inbox /> <Inbox />
<span>Inbox</span> <span className="truncate">Inbox</span>
{inboxCount > 0 && (
<div className="relative ml-auto flex h-5 min-w-[1.25rem] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground group-data-[collapsible=icon]:hidden">
<span className="absolute -inset-0.5 animate-ping rounded-full bg-primary/60" />
<span className="relative">{inboxCount}</span>
</div>
)}
</Link> </Link>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>

View File

@@ -15,3 +15,4 @@
- Changed global font from Montserrat back to Inter in `app/layout.tsx` - Changed global font from Montserrat back to Inter in `app/layout.tsx`
- Fixed syntax error in `app/api/opportunities/route.ts` by adding a missing comma in the `searchSchema` definition.

753
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@
"jose": "^6.1.3", "jose": "^6.1.3",
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"mini-svg-data-uri": "^1.4.4", "mini-svg-data-uri": "^1.4.4",
"next": "14.1.0", "next": "^15.0.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"openai": "^4.28.0", "openai": "^4.28.0",
"puppeteer": "^22.0.0", "puppeteer": "^22.0.0",