This commit is contained in:
@@ -18,8 +18,32 @@ export default function AIAssistantPage() {
|
||||
const [recentPrompts, setRecentPrompts] = useState<string[]>([])
|
||||
const [aiOnline, setAiOnline] = useState<boolean | null>(null)
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [msgCount, setMsgCount] = useState(0)
|
||||
const [tokenEstimate, setTokenEstimate] = useState("0")
|
||||
const aiChatRef = useRef<AIChatHandle | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("ai-chat-messages")
|
||||
if (saved) {
|
||||
try {
|
||||
const msgs = JSON.parse(saved)
|
||||
if (Array.isArray(msgs)) {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
const todayMsgs = msgs.filter((m: any) => {
|
||||
if (!m.ts) return true
|
||||
return new Date(m.ts).getTime() >= today.getTime()
|
||||
})
|
||||
const userMsgs = todayMsgs.filter((m: any) => m.role === "user")
|
||||
setMsgCount(userMsgs.length)
|
||||
const totalChars = todayMsgs.reduce((s: number, m: any) => s + (m.content?.length || 0), 0)
|
||||
const tokens = Math.round(totalChars / 4)
|
||||
setTokenEstimate(tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens))
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleJobSelect = useCallback((job: typeof selectedJob) => {
|
||||
setSelectedJob(job)
|
||||
}, [])
|
||||
@@ -41,36 +65,72 @@ export default function AIAssistantPage() {
|
||||
|
||||
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 prompt = `You are a lead generation assistant. Generate 5 realistic prospect profiles for my business targeting ${job.job_title} clients (${job.industry}). ${job.description ? "Service: " + job.description : ""} Keywords: ${job.keywords.join(", ")}.
|
||||
|
||||
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)
|
||||
IMPORTANT: Return ONLY a valid JSON array. Do NOT use markdown, code blocks, or any formatting. Just raw JSON.
|
||||
|
||||
Example format:
|
||||
[{"companyName":"Acme Corp","contactName":"John Smith","email":"john@acme.com","phone":"555-0100","description":"Brief description"}]
|
||||
|
||||
Use realistic company names, names, and emails for the ${job.industry} industry.`
|
||||
aiChatRef.current?.addAssistantMessage(`🔍 Finding leads for **${job.job_title}**...`)
|
||||
|
||||
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}`)
|
||||
const res = await fetch("/api/ai/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: prompt }),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
throw new Error(data.error || `Error ${res.status}`)
|
||||
}
|
||||
} 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)
|
||||
|
||||
const data = await res.json()
|
||||
const raw = data.response || ""
|
||||
let leads: any[]
|
||||
try {
|
||||
let text = raw.trim()
|
||||
const jsonMatch = text.match(/\[[\s\S]*\]/)
|
||||
if (jsonMatch) text = jsonMatch[0]
|
||||
leads = JSON.parse(text)
|
||||
if (!Array.isArray(leads)) leads = [leads]
|
||||
} catch (e) {
|
||||
aiChatRef.current?.addAssistantMessage(`⚠️ AI response wasn't valid JSON. Showing raw output:\n\n${raw.slice(0, 1500)}`)
|
||||
setSearching(false)
|
||||
return
|
||||
}
|
||||
|
||||
let created = 0
|
||||
let lastError = ""
|
||||
for (const lead of leads.slice(0, 5)) {
|
||||
try {
|
||||
const r = await fetch("/api/leads", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
companyName: lead.companyName || "Unknown Company",
|
||||
contactName: lead.contactName || "",
|
||||
email: lead.email || "",
|
||||
phone: lead.phone || "",
|
||||
description: lead.description || "",
|
||||
status: "open",
|
||||
}),
|
||||
})
|
||||
if (r.ok) created++
|
||||
else { const e = await r.json().catch(() => ({})); lastError = e.error || r.statusText; }
|
||||
} catch (e) { lastError = "Network error" }
|
||||
}
|
||||
|
||||
if (created > 0) {
|
||||
aiChatRef.current?.addAssistantMessage(`✅ Created **${created}** lead${created > 1 ? "s" : ""} for **${job.job_title}**! Go to the Leads tab to see them.`)
|
||||
} else {
|
||||
aiChatRef.current?.addAssistantMessage(`❌ Failed to create leads. Last error: ${lastError || "Unknown"}. Make sure you're logged in with an account that has permission to create leads.`)
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : "Search failed"
|
||||
aiChatRef.current?.addAssistantMessage(`❌ Error: ${msg}. Make sure Ollama is running with the model loaded.`)
|
||||
} finally {
|
||||
setSearching(false)
|
||||
}
|
||||
@@ -89,6 +149,12 @@ export default function AIAssistantPage() {
|
||||
const next = [msg, ...prev.filter((p) => p !== msg)].slice(0, 3)
|
||||
return next
|
||||
})
|
||||
setMsgCount((prev) => prev + 1)
|
||||
setTokenEstimate((prev) => {
|
||||
const raw = parseInt(prev.replace("k", "")) * (prev.includes("k") ? 1000 : 1)
|
||||
const updated = raw + Math.round(msg.length / 4)
|
||||
return updated >= 1000 ? `${(updated / 1000).toFixed(1)}k` : String(updated)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
@@ -186,11 +252,11 @@ export default function AIAssistantPage() {
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="text-muted-foreground text-[10px] uppercase tracking-wide">Messages today</div>
|
||||
<div className="text-foreground text-sm font-semibold mt-0.5">24</div>
|
||||
<div className="text-foreground text-sm font-semibold mt-0.5">{msgCount}</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="text-muted-foreground text-[10px] uppercase tracking-wide">Tokens used</div>
|
||||
<div className="text-foreground text-sm font-semibold mt-0.5">12.4k</div>
|
||||
<div className="text-foreground text-sm font-semibold mt-0.5">{tokenEstimate}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user