diff --git a/browser-use-service/main.py b/browser-use-service/main.py index 249180b..7994527 100644 --- a/browser-use-service/main.py +++ b/browser-use-service/main.py @@ -513,26 +513,27 @@ def is_offer(text: str) -> bool: # Each run picks 2-4 random queries to vary behavior and reduce detection. FB_SEARCHES = [ "looking for web developer", - "need a website designed", - "need website built South Africa", "need someone to build my website", - "need web designer", - "looking for someone to create website", - "need ecommerce website built", - "want to hire web developer", - "need wordpress website", - "I need a website for my business", - "need a site for my business", - "looking for website designer South Africa", "need website for my small business", "who can build me a website", - "want to create a website for my business", - "looking for affordable web design", - "need a web developer South Africa", + "recommend a web developer", + "I need a website for my", + "need a site for my business", + "looking for web designer", "help me build a website", "need someone to design my website", - "looking for web designer near me", - "need a website for my startup", + "anyone know a web developer", + "need a web developer for my project", + "need website for my startup", + "want a website for my company", + "need a new website for my", + "looking for web developer for my business", + "need ecommerce website for my", + "looking for affordable web design", + "need wordpress website", + "who can design a website for my", + "looking for someone to create a website", + "recommendations for a web designer", ] VIEWPORTS = [ @@ -1401,26 +1402,38 @@ Return a JSON array like ["yes","no","yes"] matching the order above.""" logger.warning("AI classification failed, falling back to keyword filter: %s", e) if not ai_succeeded: - strict_keywords = [ + web_terms = [ "website", "web design", "web develop", "web dev", + "web designer", "web developer", "build my website", "build a website", "create a website", - "need web", "looking for web", "new website", - "landing page", "wordpress", - "need a website", "my website", "business website", - "need a designer", "help with my website", - "redesign", "update my website", + "landing page", "wordpress", "ecommerce", + "my website", "business website", + "site for my", "site for my business", + "new website", "redesign my website", + "help with my website", "update my website", + ] + request_terms = [ "looking for", "need a", "need an", "looking to", "need someone", "hire a", "want someone", "need help with", "would like", "build me", "design my", "make me a", "create my", + "looking", "need", "want", "help", + "who can", "i need", + "recommend", "anyone know", "anyone recommend", + "know a", "know any", "recommendation", + "suggest", "looking for recommendations", + "can anyone", "does anyone", ] filtered = [] for r in results: t = r['title'].lower() - if not any(kw in t for kw in strict_keywords): + has_web = any(kw in t for kw in web_terms) + has_request = any(kw in t for kw in request_terms) + if not has_web or not has_request: continue if any(kw in t for kw in ['i build website', 'i offer web', 'i am a web developer', - 'affordable web design package', 'web hosting']): + 'affordable web design package', 'web hosting', + 'i do web design', 'i develop websites']): continue filtered.append(r) return filtered[:10] diff --git a/database/migrations/run_all.sql b/database/migrations/run_all.sql index ea40526..e216a01 100644 --- a/database/migrations/run_all.sql +++ b/database/migrations/run_all.sql @@ -43,6 +43,9 @@ BEGIN; \echo '=== Running 011_calendar_events.sql (Calendar Events) ===' \i 011_calendar_events.sql +\echo '=== Running 012_invites.sql (Invites) ===' +\i 012_invites.sql + \echo '=== Running 012_sent_emails.sql (Sent Email Log) ===' \i 012_sent_emails.sql diff --git a/public/uploads/voice/b2ce8bd0-6805-4544-8d34-f7c27cb57833.webm b/public/uploads/voice/b2ce8bd0-6805-4544-8d34-f7c27cb57833.webm new file mode 100644 index 0000000..c0bd07c Binary files /dev/null and b/public/uploads/voice/b2ce8bd0-6805-4544-8d34-f7c27cb57833.webm differ diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index bfc2e1c..ae65747 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -920,7 +920,7 @@ const formatPreviewContent = (content: string) => {
- -
- )} + {bootState === "ready" && readyOverlay}
{messages.map((msg, i) => ( @@ -163,7 +233,10 @@ export function AIChat() {
- + + + +
)} @@ -192,10 +265,15 @@ export function AIChat() { disabled={loading || !input.trim()} className="h-9 w-9 rounded-lg bg-[#1BB0CE] hover:bg-[#1BB0CE]/80 disabled:opacity-40 flex items-center justify-center flex-none transition-colors" > - {loading ? : } + {loading ? ( + + + + + ) : } ) -} +} \ No newline at end of file diff --git a/src/lib/ai.ts b/src/lib/ai.ts index f9573fc..91cee9c 100644 --- a/src/lib/ai.ts +++ b/src/lib/ai.ts @@ -1,41 +1,76 @@ +import http from "http" + const AI_SERVICE = process.env.AI_SERVICE_URL || "http://localhost:3001" +function parseUrl(url: string) { + const u = new URL(url) + return { hostname: u.hostname, port: parseInt(u.port) || 80, path: u.pathname + u.search } +} + export async function chatWithAI(message: string, jwtToken: string) { - const res = await fetch(`${AI_SERVICE}/ai/chat`, { - method: "POST", - headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}` }, - body: JSON.stringify({ message }), + const body = JSON.stringify({ message }) + const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/chat`) + + const text = await new Promise((resolve, reject) => { + const req = http.request( + { hostname, port, path, method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${jwtToken}`, "Content-Length": Buffer.byteLength(body) } }, + (res) => { + let data = "" + res.on("data", (chunk) => data += chunk) + res.on("end", () => { + if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { + resolve(data) + } else { + reject(new Error(`AI error ${res.statusCode}: ${data.substring(0, 200)}`)) + } + }) + } + ) + req.on("error", reject) + req.write(body) + req.end() }) - if (!res.ok) { - const text = await res.text() - throw new Error(`AI service error (${res.status}): ${text}`) - } - - const data = await res.json() + const data = JSON.parse(text) return data.response || "" } -export async function fetchJobs() { +export async function checkAiServiceStatus() { + const { hostname, port, path } = parseUrl(`${AI_SERVICE}/health`) try { - const res = await fetch(`${AI_SERVICE}/ai/jobs`) - if (!res.ok) return [] - const data = await res.json() + await new Promise((resolve, reject) => { + const req = http.get({ hostname, port, path, timeout: 3000 }, (res) => { + let data = "" + res.on("data", (chunk) => data += chunk) + res.on("end", () => { + try { resolve(JSON.parse(data).status === "ok" ? undefined : reject(new Error("not ok"))) } catch { reject(new Error("bad response")) } + }) + }) + req.on("error", reject) + req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) }) + }) + return true + } catch { + return false + } +} + +export async function fetchJobs() { + const { hostname, port, path } = parseUrl(`${AI_SERVICE}/ai/jobs`) + try { + const text = await new Promise((resolve, reject) => { + const req = http.get({ hostname, port, path, timeout: 5000 }, (res) => { + let data = "" + res.on("data", (chunk) => data += chunk) + res.on("end", () => resolve(data)) + }) + req.on("error", reject) + req.on("timeout", () => { req.destroy(); reject(new Error("timeout")) }) + }) + const data = JSON.parse(text) return data.jobs || [] } catch { console.warn("Failed to fetch AI jobs") return [] } } - -export async function checkAiServiceStatus() { - try { - const res = await fetch(`${AI_SERVICE}/health`) - if (!res.ok) return false - const data = await res.json() - return data.status === "ok" - } catch { - console.warn("Failed to check AI service status") - return false - } -} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index cc58756..8794412 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -17,6 +17,7 @@ export interface SessionUser { id: string; username: string; email: string; + phone: string | null; firstName: string; lastName: string; role: string; @@ -85,7 +86,7 @@ export async function getUserByUsername(username: string) { export async function getUserById(id: string) { const result = await query( - ` SELECT u.id, u.username, u.email, u.first_name, u.last_name, + ` SELECT u.id, u.username, u.email, u.phone, u.first_name, u.last_name, u.is_active, u.avatar_url, r.name AS role_name FROM users u @@ -185,6 +186,7 @@ export function mapDbUserToSessionUser( id: dbUser.id as string, username: dbUser.username as string, email: dbUser.email as string, + phone: (dbUser.phone as string) || null, firstName: dbUser.first_name as string, lastName: dbUser.last_name as string, role: roleMapping[roleName] || roleName.toLowerCase(), @@ -201,7 +203,6 @@ export async function createSession(userId: string, role: string) { secure: process.env.NODE_ENV === "production", sameSite: "strict", path: "/", - maxAge: 60 * 60 * 24, // 24 hours }); return token; @@ -218,6 +219,27 @@ export async function destroySession() { }); } +export async function encryptPassword(password: string): Promise { + const result = await query("SELECT encrypt_password($1) AS encrypted", [password]); + return result.rows[0]?.encrypted; +} + +export async function decryptPassword(encrypted: string): Promise { + try { + const result = await query("SELECT decrypt_password($1) AS decrypted", [encrypted]); + return result.rows[0]?.decrypted || null; + } catch { + return null; + } +} + +export async function setSessionContext(userId: string, ip?: string) { + await query("SELECT set_config('app.current_user_id', $1, true)", [userId]); + if (ip) { + await query("SELECT set_config('app.current_ip', $1, true)", [ip]); + } +} + export async function getSessionUser(): Promise { try { const cookieStore = await cookies(); diff --git a/src/types/index.ts b/src/types/index.ts index 83030e9..d5677d9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,12 +4,13 @@ export type LeadStatus = | "pending" | "closed" | "ignored"; -export type UserRole = "super_admin" | "admin" | "sales"; +export type UserRole = "super_admin" | "admin" | "sales" | "dev"; export interface User { id: string; name: string; email: string; + phone?: string; role: UserRole; active: boolean; avatar: string;