diff --git a/src/app.tsx b/src/app.tsx index 6c846d9..3e4c7e2 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,9 +1,10 @@ -import { MetaProvider, Title } from '@solidjs/meta'; -import { Router } from '@solidjs/router'; -import { FileRoutes } from '@solidjs/start/router'; -import { ErrorBoundary, Suspense } from 'solid-js'; -import { AuthProvider } from '~/lib/auth'; -import './app.css'; +import { MetaProvider, Title } from "@solidjs/meta"; +import { Router } from "@solidjs/router"; +import { FileRoutes } from "@solidjs/start/router"; +import { ErrorBoundary, Suspense } from "solid-js"; +import { AuthProvider } from "~/lib/auth"; +import { AiChatWidget } from "~/components/AiChatWidget"; +import "./app.css"; export default function App() { return ( @@ -14,16 +15,34 @@ export default function App() { ( -
-

Frontend Error

-

A runtime error occurred while rendering this page.

-
+                
+

Frontend Error

+

+ A runtime error occurred while rendering this page. +

+
                     {String((err as any)?.message || err)}
                   
)} > {props.children} + diff --git a/src/components/AiChatWidget.tsx b/src/components/AiChatWidget.tsx new file mode 100644 index 0000000..cba4122 --- /dev/null +++ b/src/components/AiChatWidget.tsx @@ -0,0 +1,333 @@ +import { createSignal, Show, For, onMount } from "solid-js"; +import { MessageCircle, X, Send, Bot, User, Loader } from "lucide-solid"; + +const API = "/api/gateway"; + +interface ChatMessage { + role: "user" | "assistant"; + content: string; + intent?: string; +} + +interface ChatResponse { + message: string; + conversation_id: string; + intent: string; + confidence: number; +} + +export function AiChatWidget() { + const [isOpen, setIsOpen] = createSignal(false); + const [messages, setMessages] = createSignal([ + { + role: "assistant", + content: + "Hi! I'm your AI assistant. I can help you create support tickets, fill out forms, generate job descriptions, or write cover letters. What can I help you with?", + }, + ]); + const [input, setInput] = createSignal(""); + const [isLoading, setIsLoading] = createSignal(false); + const [conversationId, setConversationId] = createSignal(""); + + 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 { + const res = await fetch(`${API}/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); + } + + const assistantMessage: ChatMessage = { + role: "assistant", + content: data.message, + intent: data.intent, + }; + 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(); + } + }; + + return ( + <> + {/* Floating button */} + + + {/* Chat window */} + +
+ {/* Header */} +
+
+ +
+

+ AI Assistant +

+

+ Powered by gemma3 +

+
+
+ +
+ + {/* Quick actions */} +
+ {["Create Ticket", "Job Description", "Cover Letter", "Fill Form"].map((label) => ( + + ))} +
+ + {/* Messages */} +
+ + {(msg) => ( +
+
+ }> + + +
+
+

{msg.content}

+ +

+ Intent: {msg.intent} +

+
+
+
+ )} +
+ +
+ + Thinking... +
+
+
+ + {/* Input */} +
+ setInput(e.currentTarget.value)} + onKeyDown={handleKeyDown} + placeholder="Ask me anything..." + style={{ + flex: 1, + height: "40px", + "border-radius": "20px", + border: "1px solid #E5E7EB", + padding: "0 16px", + "font-size": "13px", + outline: "none", + }} + /> + +
+
+
+ + + + ); +} diff --git a/src/components/DashboardShell.tsx b/src/components/DashboardShell.tsx index 2906c3c..53ec281 100644 --- a/src/components/DashboardShell.tsx +++ b/src/components/DashboardShell.tsx @@ -3,36 +3,49 @@ * Used for pages that need actual backend connectivity * (My Profile, My Portfolio, Verification) instead of the preview mock. */ -import { For, JSX, Show, createMemo, createSignal, onMount } from 'solid-js'; +import { For, JSX, Show, createMemo, createSignal, onMount } from "solid-js"; +import { AiChatWidget } from "./AiChatWidget"; import { - User, Briefcase, LayoutDashboard, FolderOpen, MapPin, Star, - CreditCard, Globe, ShieldCheck, HelpCircle, Settings, - RefreshCw, LogOut, Bell, ChevronRight, -} from 'lucide-solid'; + User, + Briefcase, + LayoutDashboard, + FolderOpen, + MapPin, + Star, + CreditCard, + Globe, + ShieldCheck, + HelpCircle, + Settings, + RefreshCw, + LogOut, + Bell, + ChevronRight, +} from "lucide-solid"; // ── Icon map (matches DashboardDesignPreview sidebar keys) ──────────────────── const ICON_MAP: Record = { - 'my dashboard': LayoutDashboard, - 'my profile': User, - 'my portfolio': FolderOpen, - 'leads': MapPin, - 'my responses': Star, - 'credits': CreditCard, - 'explore nxtgauge': Globe, - 'verification': ShieldCheck, - 'help center': HelpCircle, - 'settings': Settings, - 'switch services': RefreshCw, - 'jobs': Briefcase, - 'applications': Briefcase, - 'shortlisted candidates': User, - 'my applications': FolderOpen, - 'saved jobs': Star, - 'my requirements': FolderOpen, - 'received responses': Bell, - 'shortlisted responses': Star, - 'logout': LogOut, + "my dashboard": LayoutDashboard, + "my profile": User, + "my portfolio": FolderOpen, + leads: MapPin, + "my responses": Star, + credits: CreditCard, + "explore nxtgauge": Globe, + verification: ShieldCheck, + "help center": HelpCircle, + settings: Settings, + "switch services": RefreshCw, + jobs: Briefcase, + applications: Briefcase, + "shortlisted candidates": User, + "my applications": FolderOpen, + "saved jobs": Star, + "my requirements": FolderOpen, + "received responses": Bell, + "shortlisted responses": Star, + logout: LogOut, }; function SidebarIcon(props: { label: string }) { @@ -42,9 +55,9 @@ function SidebarIcon(props: { label: string }) { } function titleCase(value: string) { - return String(value || '') + return String(value || "") .toLowerCase() - .replace(/_/g, ' ') + .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); } @@ -61,14 +74,14 @@ interface Props { // ── Brand colours ───────────────────────────────────────────────────────────── -const ORANGE = '#FF5E13'; -const NAVY = '#0D0D2A'; +const ORANGE = "#FF5E13"; +const NAVY = "#0D0D2A"; // ── Component ───────────────────────────────────────────────────────────────── export default function DashboardShell(props: Props) { const roleLabel = createMemo(() => { - const k = String(props.roleKey || '').replace(/_/g, ' '); + const k = String(props.roleKey || "").replace(/_/g, " "); return k.charAt(0).toUpperCase() + k.slice(1).toLowerCase(); }); @@ -77,18 +90,21 @@ export default function DashboardShell(props: Props) { // Fetch unread notification count const fetchUnreadCount = async () => { try { - const token = typeof window !== 'undefined' ? window.sessionStorage.getItem('nxtgauge_access_token') || '' : ''; + const token = + typeof window !== "undefined" + ? window.sessionStorage.getItem("nxtgauge_access_token") || "" + : ""; if (!token) return; - const res = await fetch('/api/me/notifications/unread-count', { + const res = await fetch("/api/me/notifications/unread-count", { headers: { Authorization: `Bearer ${token}` }, - credentials: 'include', + credentials: "include", }); if (res.ok) { const data = await res.json(); setUnreadCount(data.unread_count || 0); } } catch (e) { - console.error('Failed to fetch unread count:', e); + console.error("Failed to fetch unread count:", e); } }; @@ -100,68 +116,94 @@ export default function DashboardShell(props: Props) { }); return ( -
- +
{/* ── Sidebar ──────────────────────────────────────────────────────── */} -