Files
CRM_ENVR/src/components/ai/job-selector.tsx
T
Ace d35c806d5b feat: Enhance provider components with detailed documentation and comments
- Added comprehensive comments and JSDoc-style documentation to NotificationProvider, ThemeProvider, UserProvider, and WebsiteThemeProvider for better clarity and maintainability.
- Improved type definitions in index.ts for better code understanding and usage.
- Introduced Docker support with Dockerfiles for various services including AI server, signaling server, and browser-use service.
- Created a docker-compose.yml file to orchestrate multiple services including PostgreSQL, AI, scraper, and frontend.
- Added a startup guide (startup.txt) for setting up the CRM environment on Ubuntu with Docker.
- Included a .dockerignore file to exclude unnecessary files from Docker builds.
2026-07-13 13:05:30 +02:00

106 lines
4.4 KiB
TypeScript

// ── AI Assistant: Job Selector Component ──
// Dropdown that fetches and displays job categories from the API.
// Allows the user to pick a job, then triggers a Facebook search.
"use client"
import { useState, useEffect } from "react"
import { Briefcase, ChevronDown, Loader2, Search } from "lucide-react"
/** A job category returned from the API */
interface Job {
job_title: string
keywords: string[]
industry: string
description: string
}
interface JobSelectorProps {
/** Called when a job is selected or cleared (null) */
onSelect: (job: Job | null) => void
/** Called when the user clicks the "Search Facebook" button */
onSearch?: (job: Job) => void
/** Whether a search is currently in progress */
searching?: boolean
}
/**
* JobSelector — dropdown to pick a job category and initiate a Facebook search.
* Fetches available jobs on mount and provides a "Search Facebook" CTA once a job is selected.
*/
export function JobSelector({ onSelect, onSearch, searching }: JobSelectorProps) {
const [jobs, setJobs] = useState<Job[]>([])
const [loading, setLoading] = useState(true)
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<Job | null>(null)
// ── Fetch available job categories on mount ──
useEffect(() => {
fetch("/api/ai/jobs")
.then((r) => r.json())
.then((data) => setJobs(data.jobs || []))
.catch(() => setJobs([]))
.finally(() => setLoading(false))
}, [])
// ── Select a job, close dropdown, notify parent ──
const handleSelect = (job: Job) => {
setSelected(job)
setOpen(false)
onSelect(job)
}
return (
<div className="relative">
{/* ── Dropdown trigger button ── */}
<button
type="button"
onClick={() => setOpen(!open)}
className="w-full flex items-center gap-2.5 bg-card/50 border border-border hover:border-primary/20 rounded-xl px-4 py-3 text-sm text-muted-foreground hover:text-foreground transition-all duration-200"
>
<Briefcase className="h-4 w-4 text-[#f97316] flex-none" />
<span className="flex-1 text-left truncate">
{selected ? selected.job_title : loading ? "Loading jobs..." : "Select a job category"}
</span>
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" /> : <ChevronDown className={`h-3.5 w-3.5 text-muted-foreground/60 transition-transform duration-200 ${open ? "rotate-180" : ""}`} />}
</button>
{/* ── Dropdown menu — visible when open ── */}
{open && (
<>
{/* Backdrop to close on click outside */}
<div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
<div className="absolute top-full left-0 right-0 mt-1.5 z-20 bg-card border border-border rounded-xl shadow-xl max-h-60 overflow-y-auto">
{jobs.map((job, i) => (
<button
key={i}
type="button"
onClick={() => handleSelect(job)}
className="w-full text-left px-4 py-3 text-sm text-muted-foreground hover:bg-muted hover:text-foreground transition-all duration-150 border-b border-border/20 last:border-b-0 border-l-2 border-l-transparent hover:border-l-primary/40"
>
<div className="font-medium">{job.job_title}</div>
<div className="text-xs text-muted-foreground/60 mt-0.5">{job.industry} {job.description}</div>
</button>
))}
{jobs.length === 0 && !loading && (
<div className="px-4 py-4 text-xs text-muted-foreground/60 text-center">No job categories loaded</div>
)}
</div>
</>
)}
{/* ── "Search Facebook" CTA — shown only when a job is selected ── */}
{selected && onSearch && (
<button
type="button"
onClick={() => onSearch(selected)}
disabled={searching}
className="w-full flex items-center justify-center gap-2 mt-3 bg-primary hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed text-primary-foreground text-sm font-semibold rounded-xl px-4 py-3 transition-all duration-200 shadow-lg shadow-primary/20"
>
{searching ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
{searching ? "Searching..." : "Search Facebook"}
</button>
)}
</div>
)
}