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 ;
2026-06-15 17:04:02 +05:30
status? : string ;
suggestedAction? : string ;
2026-04-15 20:03:47 +02:00
}
interface ChatResponse {
2026-06-14 18:04:47 +02:00
reply : string ;
2026-04-15 20:03:47 +02:00
conversation_id : string ;
intent : string ;
confidence : number ;
2026-06-15 06:19:05 +05:30
remaining_credits? : number ;
remaining_daily_actions? : number ;
credits_charged? : number ;
2026-06-15 06:50:09 +05:30
message? : string ;
2026-06-15 17:04:02 +05:30
status? : string ;
suggested_action? : 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 ;
}
2026-06-15 06:19:05 +05:30
}
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 ;
2026-04-15 20:03:47 +02:00
}
export function AiChatWidget() {
const [ isOpen , setIsOpen ] = createSignal ( false ) ;
const [ messages , setMessages ] = createSignal < ChatMessage [ ] > ( [
{
role : "assistant" ,
content :
2026-06-14 18:04:47 +02:00
"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" ,
2026-04-15 20:03:47 +02:00
} ,
] ) ;
const [ input , setInput ] = createSignal ( "" ) ;
const [ isLoading , setIsLoading ] = createSignal ( false ) ;
const [ conversationId , setConversationId ] = createSignal ( "" ) ;
2026-06-15 06:19:05 +05:30
const [ usage , setUsage ] = createSignal < UsageStatus | null > ( null ) ;
onMount ( ( ) = > {
fetchUsage ( ) ;
} ) ;
const fetchUsage = async ( ) = > {
try {
2026-06-15 09:39:26 +05:30
const res = await fetch ( ` ${ API } /api/ai/usage/summary ` ) ;
2026-06-15 06:19:05 +05:30
if ( ! res . ok ) return ;
const data = await res . json ( ) ;
2026-06-15 09:39:26 +05:30
const plan = data . plan_details || data . plan || { } ;
2026-06-15 06:19:05 +05:30
setUsage ( {
2026-06-15 09:39:26 +05:30
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 ,
2026-06-15 06:19:05 +05:30
} ) ;
} catch ( err ) {
console . error ( "Failed to fetch AI usage" , err ) ;
}
} ;
2026-04-15 20:03:47 +02:00
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 {
2026-06-15 06:50:09 +05:30
let res = await fetch ( ` ${ API } /api/ai/chat/ask ` , {
2026-04-15 20:03:47 +02:00
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( {
message : text ,
conversation_id : conversationId ( ) || undefined ,
} ) ,
} ) ;
2026-06-15 06:50:09 +05:30
if ( res . status === 401 || res . status === 403 || res . status === 404 ) {
res = await fetch ( ` ${ API } /api/ai/chat/message ` , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( {
message : text ,
conversation_id : conversationId ( ) || undefined ,
} ) ,
} ) ;
}
2026-04-15 20:03:47 +02:00
if ( ! res . ok ) throw new Error ( "AI request failed" ) ;
const data : ChatResponse = await res . json ( ) ;
if ( data . conversation_id && ! conversationId ( ) ) {
setConversationId ( data . conversation_id ) ;
}
2026-06-15 06:19:05 +05:30
// 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
) ;
}
2026-04-15 20:03:47 +02:00
const assistantMessage : ChatMessage = {
role : "assistant" ,
2026-06-15 06:50:09 +05:30
content : data.message || data . reply ,
2026-04-15 20:03:47 +02:00
intent : data.intent ,
2026-06-15 17:04:02 +05:30
status : data.status ,
suggestedAction : data.suggested_action ,
2026-04-15 20:03:47 +02:00
} ;
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" } } >
2026-05-05 20:14:56 +02:00
< img
src = "/ai-assistant-logo.png"
alt = "AI Assistant"
2026-06-14 18:04:47 +02:00
style = { { width : "26px" , height : "26px" , "border-radius" : "6px" , "object-fit" : "contain" , "background" : "transparent" } }
onError = { ( e ) = > { e . currentTarget . style . display = 'none' ; } }
2026-05-05 20:14:56 +02:00
/ >
2026-04-15 20:03:47 +02:00
< div >
< p style = { { margin : 0 , color : "#fff" , "font-weight" : "700" , "font-size" : "15px" } } >
AI Assistant
< / p >
2026-06-15 06:19:05 +05:30
< Show when = { usage ( ) } >
{ ( u ) = > (
< p style = { { margin : "2px 0 0" , color : "rgba(255,255,255,0.85)" , "font-size" : "11px" } } >
{ u ( ) . plan_name } · { u ( ) . remaining_credits } credits · { u ( ) . remaining_daily_actions } / { u ( ) . daily_action_limit } today
< / p >
) }
< / Show >
2026-04-15 20:03:47 +02:00
< / 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" ,
} }
>
2026-06-14 18:04:47 +02:00
{ [
{ label : "Support Ticket" , text : "I need help with " } ,
{ label : "Search KB" , text : "How do I " } ,
{ label : "AI Plan" , text : "Explain my AI plan" } ,
{ label : "Check Balance" , text : "Check my AI balance" } ,
] . map ( ( action ) = > (
2026-04-15 20:03:47 +02:00
< button
2026-06-14 18:04:47 +02:00
aria - label = { ` Quick action: ${ action . label } ` }
2026-04-15 20:03:47 +02:00
onClick = { ( ) = > {
2026-06-14 18:04:47 +02:00
setInput ( action . text ) ;
2026-04-15 20:03:47 +02:00
} }
style = { {
padding : "4px 10px" ,
"border-radius" : "20px" ,
border : "1px solid #E5E7EB" ,
background : "#F9FAFB" ,
"font-size" : "11px" ,
cursor : "pointer" ,
color : "#374151" ,
} }
>
2026-06-14 18:04:47 +02:00
{ action . label }
2026-04-15 20:03:47 +02:00
< / 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 >
2026-06-15 17:04:02 +05:30
< Show when = { msg . role === "assistant" && statusLabel ( msg . status ) } >
{ ( label ) = > (
< p
style = { {
margin : "6px 0 0" ,
"font-size" : "10px" ,
color : "#6B7280" ,
"font-weight" : "600" ,
} }
>
{ label ( ) }
< / p >
) }
2026-04-15 20:03:47 +02:00
< / 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>
< / >
) ;
}