import { createSignal, Show, For, onMount } from "solid-js"; import { MessageCircle, X, Send, Bot, User, Loader } from "lucide-solid"; const API = "/api"; interface ChatMessage { role: "user" | "assistant"; content: string; intent?: string; status?: string; suggestedAction?: string; /** Semantic category: ticket_created | ticket_pending | kb_results | usage_info | navigation */ actionType?: string; /** Original user text — stored so ticket confirm has a description to send */ userQuery?: string; /** True once the user has clicked the confirm/action button on this message */ confirmed?: boolean; } interface ChatResponse { reply: string; conversation_id: string; intent: string; confidence: number; remaining_credits?: number; remaining_daily_actions?: number; credits_charged?: number; message?: string; status?: string; suggested_action?: string; action_type?: string; } function statusLabel(status?: string): string | null { switch (status) { case "ticket_created": return "Support ticket created"; case "kb_results": return "Help articles found"; case "usage_summary": return "AI usage summary"; default: return null; } } /** Returns the dashboard URL to navigate to for a given suggested_action key. */ function navUrl(action: string | undefined): string | null { switch (action) { case "open_support_ticket": return "/dashboard?nav=support"; case "open_help_search": return "/help-center"; case "open_account_settings": return "/dashboard?nav=settings"; case "open_billing": case "show_usage_modal": return "/dashboard?nav=credits"; case "open_jd_generator": return "/dashboard?nav=job_descriptions"; case "open_cover_letter": return "/dashboard?nav=cover_letter"; case "open_resume_tailor": return "/dashboard?nav=resume"; case "open_lead_unlock": return "/dashboard?nav=leads"; case "open_auto_apply": return "/dashboard?nav=auto_apply"; case "open_form_extract": return "/dashboard?nav=form_extract"; default: return null; } } /** Human-readable label for an action button. */ function actionLabel(action: string | undefined): string { switch (action) { case "open_support_ticket": return "View Tickets →"; case "open_help_search": return "Browse Help Center →"; case "open_account_settings": return "Account Settings →"; case "open_billing": case "show_usage_modal": return "AI Credits & Billing →"; case "open_jd_generator": return "Job Description Generator →"; case "open_cover_letter": return "Cover Letter Generator →"; case "open_resume_tailor": return "Resume Tailor →"; case "open_lead_unlock": return "Lead Requests →"; case "open_auto_apply": return "Auto Apply →"; case "open_form_extract": return "Form Assistant →"; case "create_ticket": return "Create Support Ticket"; case "save_profile": return "Save to Profile"; default: return ""; } } interface UsageStatus { remaining_credits: number; remaining_daily_actions: number; daily_action_limit: number; plan_code: string; plan_name: string; monthly_credits_total: number; monthly_credits_used: number; } export function AiChatWidget() { const [isOpen, setIsOpen] = createSignal(false); const [messages, setMessages] = createSignal([ { role: "assistant", content: "Hi! I'm Ask Ash, your Nxtgauge assistant. I can help you:\n• Search help articles & KB\n• Create support tickets\n• Explain your AI plan & usage\n• Answer questions about the platform", }, ]); const [input, setInput] = createSignal(""); const [isLoading, setIsLoading] = createSignal(false); const [conversationId, setConversationId] = createSignal(""); const [usage, setUsage] = createSignal(null); onMount(() => { const hasToken = typeof window !== "undefined" && !!sessionStorage.getItem("nxtgauge_access_token"); if (hasToken) fetchUsage(); }); const fetchUsage = async () => { try { const res = await fetch(`${API}/ai/usage/summary`, { headers: { Authorization: `Bearer ${sessionStorage.getItem("nxtgauge_access_token") || ""}`, }, credentials: "include", }); if (!res.ok) return; const data = await res.json(); const plan = data.plan_details || data.plan || {}; setUsage({ remaining_credits: plan.remaining_credits ?? data.monthly_remaining ?? 0, remaining_daily_actions: plan.remaining_daily_actions ?? data.daily_remaining ?? 0, daily_action_limit: plan.daily_action_limit ?? data.daily_limit ?? 0, plan_code: plan.plan_code ?? data.plan_code ?? "free", plan_name: plan.plan_name ?? data.plan ?? "Free", monthly_credits_total: plan.monthly_credits_total ?? data.monthly_limit ?? 0, monthly_credits_used: plan.monthly_credits_used ?? data.monthly_used ?? 0, }); } catch (err) { console.error("Failed to fetch AI usage", err); } }; const toggleChat = () => setIsOpen((v) => !v); const sendMessage = async () => { const text = input().trim(); if (!text || isLoading()) return; setIsLoading(true); const userMessage: ChatMessage = { role: "user", content: text }; setMessages((prev) => [...prev, userMessage]); setInput(""); try { let res = await fetch(`${API}/ai/chat/ask`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text, conversation_id: conversationId() || undefined, }), }); if (res.status === 401 || res.status === 403 || res.status === 404) { res = await fetch(`${API}/ai/chat/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: text, conversation_id: conversationId() || undefined, }), }); } if (!res.ok) throw new Error("AI request failed"); const data: ChatResponse = await res.json(); if (data.conversation_id && !conversationId()) { setConversationId(data.conversation_id); } // Update usage state from response if available if (typeof data.remaining_credits === "number") { setUsage((prev) => prev ? { ...prev, remaining_credits: data.remaining_credits!, remaining_daily_actions: data.remaining_daily_actions ?? prev.remaining_daily_actions, } : null ); } const assistantMessage: ChatMessage = { role: "assistant", content: data.message || data.reply, intent: data.intent, status: data.status, suggestedAction: data.suggested_action, actionType: data.action_type, userQuery: text, confirmed: false, }; setMessages((prev) => [...prev, assistantMessage]); } catch (err) { setMessages((prev) => [ ...prev, { role: "assistant", content: "I'm having trouble connecting right now. Please try again or contact support@nxtgauge.com.", }, ]); } finally { setIsLoading(false); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }; /** Called when user clicks "Create Support Ticket" on a ticket_pending message. */ const confirmCreateTicket = async (msgIdx: number, userQuery: string) => { // Mark the originating message as confirmed so the button disappears immediately setMessages((prev) => prev.map((m, i) => (i === msgIdx ? { ...m, confirmed: true } : m))); setIsLoading(true); try { const res = await fetch(`${API}/ai/chat/confirm`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionStorage.getItem("nxtgauge_access_token") || ""}`, }, credentials: "include", body: JSON.stringify({ action: "create_ticket", conversation_id: conversationId() || undefined, fields: { subject: userQuery.slice(0, 80), description: userQuery, category: "ai_assisted", priority: "medium", }, }), }); const data = await res.json().catch(() => ({})); if (res.ok && data.success) { setMessages((prev) => [ ...prev, { role: "assistant", content: data.message || `Support ticket created. Our team will get back to you shortly.`, status: "ticket_created", suggestedAction: "open_support_ticket", actionType: "ticket_created", confirmed: false, }, ]); } else { setMessages((prev) => [ ...prev, { role: "assistant", content: data.error || "Couldn't create the ticket right now. Please try again or email support@nxtgauge.com.", }, ]); } } catch { setMessages((prev) => [ ...prev, { role: "assistant", content: "Couldn't connect to create the ticket. Please email support@nxtgauge.com directly.", }, ]); } finally { setIsLoading(false); } }; /** Called when user clicks "Save to Profile" on a profile_draft message. */ const confirmSaveProfile = async (msgIdx: number, draftText: string) => { // Extract just the quoted improved text if it's wrapped in our "Here's an improved..." message const quotedMatch = draftText.match(/"([^"]+)"/); const cleanDraft = quotedMatch ? quotedMatch[1] : draftText; setMessages((prev) => prev.map((m, i) => (i === msgIdx ? { ...m, confirmed: true } : m))); setIsLoading(true); try { const res = await fetch(`${API}/ai/chat/confirm`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${sessionStorage.getItem("nxtgauge_access_token") || ""}`, }, credentials: "include", body: JSON.stringify({ action: "save_profile", conversation_id: conversationId() || undefined, fields: { draft_text: cleanDraft }, }), }); const data = await res.json().catch(() => ({})); if (res.ok && data.success) { setMessages((prev) => [ ...prev, { role: "assistant", content: data.message || "Your profile summary has been updated!", status: "profile_saved", confirmed: true, }, ]); } else { setMessages((prev) => [ ...prev, { role: "assistant", content: data.error || "Couldn't save the profile summary. Please try again or update it manually from your profile page.", }, ]); } } catch { setMessages((prev) => [ ...prev, { role: "assistant", content: "Couldn't connect to save your profile. Please update it manually from your profile page.", }, ]); } finally { setIsLoading(false); } }; return ( <> {/* Floating button */} {/* Chat window */}
{/* Header */}
AI Assistant { e.currentTarget.style.display = 'none'; }} />

AI Assistant

{(u) => (

{u().plan_name} · {u().remaining_credits} credits · {u().remaining_daily_actions}/{u().daily_action_limit} today

)}
{/* Quick actions */}
{(action) => ( )}
{/* Messages */}
{(msg, idx) => ( <>
}>

{msg.content}

{(label) => (

{label()}

)}
{/* Action buttons — only for unconfirmed assistant messages with a suggested action */}
{/* Navigation button — links to the relevant dashboard section */} {(url) => ( {actionLabel(msg.suggestedAction)} )} {/* Confirm-create button — for ticket_pending messages */} {/* Save-profile button — for profile_draft messages */}
)}
Thinking...
{/* Input */}
setInput(e.currentTarget.value)} onKeyDown={handleKeyDown} placeholder="Ask me anything..." aria-label="Chat message input" style={{ flex: 1, height: "40px", "border-radius": "20px", border: "1px solid #E5E7EB", padding: "0 16px", "font-size": "13px", outline: "none", }} />
); }