Merge remote-tracking branch 'caitlin/main' into main
Build & Auto-Repair / build (push) Has been cancelled

- Adopt color theme management in website-theme-provider
- FOUC-prevention inline script in layout.tsx
- Facebook scraper search in ai-assistant page
- addAssistantMessage + timestamp cleanup in ai-chat
- New scripts (run-migrations.mjs), browser-use-service, job-selector
- Keep deleted-by-theirs files: campaigns, projects, proposals, client portal
This commit is contained in:
JCBSComputer
2026-07-03 11:07:42 +02:00
17 changed files with 897 additions and 190 deletions
+41 -3
View File
@@ -1,7 +1,7 @@
"use client"
import { useState, useCallback, useRef, useEffect } from "react"
import { AIChat } from "@/components/ai/ai-chat"
import { AIChat, type AIChatHandle } from "@/components/ai/ai-chat"
import { JobSelector } from "@/components/ai/job-selector"
import { Bot, ChevronRight, Wifi, WifiOff } from "lucide-react"
@@ -17,7 +17,8 @@ export default function AIAssistantPage() {
const [selectedJob, setSelectedJob] = useState<{ job_title: string; keywords: string[]; industry: string; description: string } | null>(null)
const [recentPrompts, setRecentPrompts] = useState<string[]>([])
const [aiOnline, setAiOnline] = useState<boolean | null>(null)
const aiChatRef = useRef<{ fillInput: (text: string) => void } | null>(null)
const [searching, setSearching] = useState(false)
const aiChatRef = useRef<AIChatHandle | null>(null)
const handleJobSelect = useCallback((job: typeof selectedJob) => {
setSelectedJob(job)
@@ -38,6 +39,43 @@ export default function AIAssistantPage() {
return () => { cancelled = true; clearInterval(id) }
}, [])
const handleSearch = useCallback(async (job: NonNullable<typeof selectedJob>) => {
setSearching(true)
const keyword = job.keywords[0]
aiChatRef.current?.addAssistantMessage(`🔍 Searching Facebook for **${job.job_title}** leads...`)
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 360000)
const statusId = setTimeout(() => {
aiChatRef.current?.addAssistantMessage("⏳ Still searching Facebook (this can take up to 5 minutes)...")
}, 45000)
try {
const res = await fetch(`http://localhost:3008/scrape/facebook?force=true&query=${encodeURIComponent(keyword)}`, { method: "POST", signal: controller.signal })
clearTimeout(timeoutId)
clearTimeout(statusId)
const data = await res.json()
if (data.success && data.leads?.length > 0) {
const leadsText = data.leads.map((lead: any, i: number) =>
`**${i + 1}.** ${lead.author || "Unknown"}\n> ${(lead.content || "").slice(0, 300)}\n> 🔗 ${lead.url || "(no link available)"}`
).join("\n\n")
aiChatRef.current?.addAssistantMessage(`✅ Found **${data.leads.length}** leads:\n\n${leadsText}`)
} else {
const reason = data.error || data.flag_reason || "No leads found this time"
aiChatRef.current?.addAssistantMessage(`⚠️ ${reason}`)
}
} catch (err: any) {
clearTimeout(timeoutId)
clearTimeout(statusId)
const msg = err.name === "AbortError"
? "❌ Scraper timed out after 5 minutes. Try again or check the scraper service."
: `${err instanceof Error ? err.message : "Search failed"}`
aiChatRef.current?.addAssistantMessage(msg)
} finally {
setSearching(false)
}
}, [])
const handleTipClick = useCallback((tip: string) => {
aiChatRef.current?.fillInput(tip)
}, [])
@@ -95,7 +133,7 @@ export default function AIAssistantPage() {
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
<span className="text-primary text-[10px] font-bold uppercase tracking-[0.15em]">Target Job</span>
</div>
<JobSelector onSelect={handleJobSelect} />
<JobSelector onSelect={handleJobSelect} onSearch={handleSearch} searching={searching} />
{selectedJob && (
<div className="bg-card/50 border border-border rounded-xl p-3.5 mt-3 space-y-2">
<h4 className="text-sm font-semibold text-foreground">{selectedJob.job_title}</h4>