diff --git a/ai-server/server.err b/ai-server/server.err new file mode 100644 index 0000000..e69de29 diff --git a/src/app/(dashboard)/ai-assistant/page.tsx b/src/app/(dashboard)/ai-assistant/page.tsx index cee5296..aa8f866 100644 --- a/src/app/(dashboard)/ai-assistant/page.tsx +++ b/src/app/(dashboard)/ai-assistant/page.tsx @@ -1,17 +1,33 @@ "use client" -import { useState, useCallback } from "react" +import { useState, useCallback, useEffect } from "react" import { AIChat } from "@/components/ai/ai-chat" import { JobSelector } from "@/components/ai/job-selector" -import { Bot, Lightbulb, Target, MessageSquare } from "lucide-react" +import { Bot, Lightbulb, Target, MessageSquare, Wifi, WifiOff } from "lucide-react" export default function AIAssistantPage() { const [selectedJob, setSelectedJob] = useState<{ job_title: string; keywords: string[]; industry: string; description: string } | null>(null) + const [aiOnline, setAiOnline] = useState(null) const handleJobSelect = useCallback((job: typeof selectedJob) => { setSelectedJob(job) }, []) + useEffect(() => { + let cancelled = false + const check = async () => { + try { + const res = await fetch("/api/ai/jobs") + if (!cancelled) setAiOnline(res.ok) + } catch { + if (!cancelled) setAiOnline(false) + } + } + check() + const id = setInterval(check, 15000) + return () => { cancelled = true; clearInterval(id) } + }, []) + return (
@@ -20,10 +36,22 @@ export default function AIAssistantPage() {
-
+

AI Sales Assistant

Uncensored sales tips and strategies powered by local AI

+
+ + {aiOnline === null ? "Checking..." : aiOnline ? "Connected" : "Disconnected"} + + {aiOnline === null ? ( + + ) : aiOnline ? ( + + ) : ( + + )} +
diff --git a/src/app/api/settings/website-theme/route.ts b/src/app/api/settings/website-theme/route.ts index 4996f7d..b67875f 100644 --- a/src/app/api/settings/website-theme/route.ts +++ b/src/app/api/settings/website-theme/route.ts @@ -12,7 +12,7 @@ export async function GET() { [user.id], ) - const websiteTheme = result.rows[0]?.website_theme || "spidey" + const websiteTheme = result.rows[0]?.website_theme || "default" return NextResponse.json({ websiteTheme }) } catch (error) { console.error("Website theme GET error:", error) @@ -26,7 +26,7 @@ export async function PUT(request: NextRequest) { if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) const body = await request.json() - const theme = body.websiteTheme || "spidey" + const theme = body.websiteTheme || "default" await query( `UPDATE users SET preferences = preferences || $2::jsonb WHERE id = $1`, diff --git a/src/components/ai/ai-chat.tsx b/src/components/ai/ai-chat.tsx index 14081ea..28b4ba7 100644 --- a/src/components/ai/ai-chat.tsx +++ b/src/components/ai/ai-chat.tsx @@ -1,24 +1,63 @@ "use client" import { useState, useRef, useEffect, Fragment } from "react" -import { Send, Bot, User, RefreshCw, AlertCircle, Check, Terminal } from "lucide-react" +import { Send, Bot, User, AlertCircle, Terminal, Sparkles, Lightbulb, Target, MessageSquare } from "lucide-react" function linkifyText(text: string) { const urlRegex = /(https?:\/\/[^\s<]+[^\s<.,;:!?)\]}>])/ const parts = text.split(urlRegex) return parts.map((part, i) => { if (part.startsWith("http://") || part.startsWith("https://")) { - return {part} + return ( + + {part} + + ) } return {part} }) } +function formatContent(text: string) { + const blocks: { type: "code" | "text"; content: string }[] = [] + const regex = /```(\w*)\n?([\s\S]*?)```/g + let last = 0 + let match: RegExpExecArray | null + while ((match = regex.exec(text)) !== null) { + if (match.index > last) { + blocks.push({ type: "text", content: text.slice(last, match.index) }) + } + blocks.push({ type: "code", content: match[2].trim() }) + last = match.index + match[0].length + } + if (last < text.length) { + blocks.push({ type: "text", content: text.slice(last) }) + } + return blocks.length ? blocks : [{ type: "text" as const, content: text }] +} + interface ChatMessage { role: "user" | "assistant" content: string } +function TypingDots() { + return ( +
+ + + +
+ ) +} + +const suggestions = [ + { icon: Terminal, label: "Lists", query: "Show me available job lists and categories" }, + { icon: Target, label: "Leads", query: "Pull recent leads" }, + { icon: Lightbulb, label: "Tips", query: "Give me general sales tips" }, + { icon: MessageSquare, label: "Objections", query: "How do I handle common objections?" }, +] + export function AIChat() { const [messages, setMessages] = useState([]) const [input, setInput] = useState("") @@ -26,6 +65,8 @@ export function AIChat() { const [error, setError] = useState("") const [bootState, setBootState] = useState<"booting" | "ready" | "error">("booting") const messagesEndRef = useRef(null) + const inputRef = useRef(null) + const chatContainerRef = useRef(null) const checkServer = async () => { try { @@ -80,8 +121,15 @@ export function AIChat() { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) }, [messages]) - const sendMessage = async () => { - const msg = input.trim() + useEffect(() => { + if (inputRef.current) { + inputRef.current.style.height = "auto" + inputRef.current.style.height = `${Math.min(inputRef.current.scrollHeight, 120)}px` + } + }, [input]) + + const sendMessage = async (text?: string) => { + const msg = (text || input).trim() if (!msg || loading) return setInput("") @@ -158,11 +206,10 @@ export function AIChat() { .robot-arm-r { transform-origin: top center; animation: armRight 0.3s ease-in-out infinite; } .robot-eye { animation: blink 2s ease-in-out infinite; } `} -
-

Servers booting...

-
+
+
- + @@ -176,104 +223,144 @@ export function AIChat() {
+
+

Waking up the AI...

+

Loading model — this takes about a minute on first run

+
) - const readyOverlay = ( -
-
-
- + const hasMessages = messages.length > 0 + + let mainArea: React.ReactNode + if (bootState === "booting") { + mainArea = bootOverlay + } else if (hasMessages) { + mainArea = ( +
+
+ {messages.map((msg, i) => { + const isUser = msg.role === "user" + return ( +
+ {!isUser && ( +
+ +
+ )} +
+
+ {formatContent(msg.content).map((block, bi) => + block.type === "code" ? ( +
+                          {block.content}
+                        
+ ) : ( + {linkifyText(block.content)} + ) + )} +
+
+ {isUser && ( +
+ +
+ )} +
+ ) + })} + {loading && ( +
+
+ +
+
+ +
+
+ )} +
-
-

Try these commands:

-
- lists - leads +
+ ) + } else { + mainArea = ( +
+
+
+ +
+
+

AI Sales Assistant

+

Ask me anything about sales strategies, outreach, and prospect targeting.

-
- ) - - if (bootState === "booting") return bootOverlay + ) + } return (
- {bootState === "ready" && readyOverlay} + {mainArea} + {bootState !== "booting" && ( +
+ {!hasMessages && ( +
+ {suggestions.map((s, i) => ( + + ))} +
+ )} -
- {messages.map((msg, i) => ( -
- {msg.role === "assistant" && ( -
- -
- )} -
+ + {error} +
+ )} + +
+