2026-04-15 20:03:47 +02:00
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 < ChatMessage [ ] > ( [
{
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 */ }
< button
onClick = { toggleChat }
style = { {
position : "fixed" ,
bottom : "24px" ,
right : "24px" ,
width : "56px" ,
height : "56px" ,
"border-radius" : "50%" ,
background : "#FF5E13" ,
border : "none" ,
cursor : "pointer" ,
display : "flex" ,
"align-items" : "center" ,
"justify-content" : "center" ,
"box-shadow" : "0 4px 16px rgba(255, 90, 19, 0.35)" ,
"z-index" : "9999" ,
transition : "transform 0.2s" ,
} }
title = "AI Assistant"
>
< Show when = { isOpen ( ) } fallback = { < MessageCircle size = { 24 } color = "#fff" / > } >
< X size = { 24 } color = "#fff" / >
< / Show >
< / button >
{ /* Chat window */ }
< Show when = { isOpen ( ) } >
< div
2026-05-01 02:54:25 +02:00
role = "dialog"
aria - label = "AI Assistant chat"
aria - modal = "true"
2026-04-15 20:03:47 +02:00
style = { {
position : "fixed" ,
bottom : "96px" ,
right : "24px" ,
width : "380px" ,
height : "520px" ,
background : "#fff" ,
"border-radius" : "16px" ,
"box-shadow" : "0 8px 40px rgba(0,0,0,0.15)" ,
display : "flex" ,
"flex-direction" : "column" ,
overflow : "hidden" ,
2026-05-01 02:54:25 +02:00
"z-index" : "9998" ,
2026-04-15 20:03:47 +02:00
} }
>
{ /* Header */ }
< div
style = { {
background : "linear-gradient(135deg, #FF5E13 0%, #E5470F 100%)" ,
padding : "16px 20px" ,
display : "flex" ,
"align-items" : "center" ,
"justify-content" : "space-between" ,
} }
>
< div style = { { display : "flex" , "align-items" : "center" , gap : "10px" } } >
< Bot size = { 22 } color = "#fff" / >
< div >
< p style = { { margin : 0 , color : "#fff" , "font-weight" : "700" , "font-size" : "15px" } } >
AI Assistant
< / p >
< p style = { { margin : 0 , color : "rgba(255,255,255,0.8)" , "font-size" : "11px" } } >
Powered by gemma3
< / p >
< / div >
< / div >
< button
onClick = { toggleChat }
2026-05-01 02:54:25 +02:00
aria - label = "Close chat"
2026-04-15 20:03:47 +02:00
style = { {
background : "none" ,
border : "none" ,
cursor : "pointer" ,
padding : "4px" ,
} }
>
< X size = { 20 } color = "#fff" / >
< / button >
< / div >
{ /* Quick actions */ }
< div
style = { {
padding : "10px 16px" ,
"border-bottom" : "1px solid #E5E7EB" ,
display : "flex" ,
gap : "8px" ,
"flex-wrap" : "wrap" ,
} }
>
{ [ "Create Ticket" , "Job Description" , "Cover Letter" , "Fill Form" ] . map ( ( label ) = > (
< button
2026-05-01 02:54:25 +02:00
aria - label = { ` Quick action: ${ label } ` }
2026-04-15 20:03:47 +02:00
onClick = { ( ) = > {
setInput ( ` ${ label . toLowerCase ( ) } : ` ) ;
} }
style = { {
padding : "4px 10px" ,
"border-radius" : "20px" ,
border : "1px solid #E5E7EB" ,
background : "#F9FAFB" ,
"font-size" : "11px" ,
cursor : "pointer" ,
color : "#374151" ,
} }
>
{ label }
< / button >
) ) }
< / div >
{ /* Messages */ }
< div
style = { {
flex : 1 ,
overflow : "auto" ,
padding : "16px" ,
display : "flex" ,
"flex-direction" : "column" ,
gap : "12px" ,
} }
>
< For each = { messages ( ) } >
{ ( msg ) = > (
< div
style = { {
display : "flex" ,
"align-items" : "flex-start" ,
gap : "8px" ,
"flex-direction" : msg . role === "user" ? "row-reverse" : "row" ,
} }
>
< div
style = { {
width : "28px" ,
height : "28px" ,
"border-radius" : "50%" ,
background : msg.role === "user" ? "#FF5E13" : "#E5E7EB" ,
display : "flex" ,
"align-items" : "center" ,
"justify-content" : "center" ,
"flex-shrink" : 0 ,
} }
>
< Show when = { msg . role === "user" } fallback = { < Bot size = { 14 } color = "#6B7280" / > } >
< User size = { 14 } color = "#fff" / >
< / Show >
< / div >
< div
style = { {
"max-width" : "75%" ,
padding : "10px 14px" ,
"border-radius" : "14px" ,
background : msg.role === "user" ? "#FF5E13" : "#F3F4F6" ,
color : msg.role === "user" ? "#fff" : "#111827" ,
"font-size" : "13px" ,
"line-height" : "1.5" ,
} }
>
< p style = { { margin : 0 , "white-space" : "pre-wrap" } } > { msg . content } < / p >
< Show when = { msg . intent && msg . role === "assistant" } >
< p
style = { {
margin : "4px 0 0" ,
"font-size" : "10px" ,
color : "#9CA3AF" ,
"font-style" : "italic" ,
} }
>
Intent : { msg . intent }
< / p >
< / Show >
< / div >
< / div >
) }
< / For >
< Show when = { isLoading ( ) } >
< div
style = { {
display : "flex" ,
"align-items" : "center" ,
gap : "8px" ,
color : "#9CA3AF" ,
"font-size" : "13px" ,
} }
>
< Loader size = { 14 } style = { { animation : "spin 1s linear infinite" } } / >
Thinking . . .
< / div >
< / Show >
< / div >
{ /* Input */ }
< div
style = { {
padding : "12px 16px" ,
"border-top" : "1px solid #E5E7EB" ,
display : "flex" ,
gap : "8px" ,
} }
>
< input
type = "text"
value = { input ( ) }
onInput = { ( e ) = > setInput ( e . currentTarget . value ) }
onKeyDown = { handleKeyDown }
placeholder = "Ask me anything..."
2026-05-01 02:54:25 +02:00
aria - label = "Chat message input"
2026-04-15 20:03:47 +02:00
style = { {
flex : 1 ,
height : "40px" ,
"border-radius" : "20px" ,
border : "1px solid #E5E7EB" ,
padding : "0 16px" ,
"font-size" : "13px" ,
outline : "none" ,
} }
/ >
< button
onClick = { sendMessage }
disabled = { isLoading ( ) || ! input ( ) . trim ( ) }
2026-05-01 02:54:25 +02:00
aria - label = "Send message"
2026-04-15 20:03:47 +02:00
style = { {
width : "40px" ,
height : "40px" ,
"border-radius" : "50%" ,
background : isLoading ( ) ? "#E5E7EB" : "#FF5E13" ,
border : "none" ,
cursor : isLoading ( ) ? "default" : "pointer" ,
display : "flex" ,
"align-items" : "center" ,
"justify-content" : "center" ,
} }
>
< Send size = { 16 } color = "#fff" / >
< / button >
< / div >
< / div >
< / Show >
< style > { `
@keyframes spin {
from { transform : rotate ( 0 deg ) ; }
to { transform : rotate ( 360 deg ) ; }
}
` }</style>
< / >
) ;
}