feat(ai-chat): wire action buttons + ticket confirm flow in AiChatWidget
All checks were successful
build-and-release / build (push) Successful in 2m4s

ChatMessage gains actionType, userQuery, confirmed fields.
ChatResponse gains action_type from backend.

Action buttons now render below assistant messages:
- Navigation actions (open_help_search, open_billing, etc.) show a link
  chip to the correct dashboard section via navUrl()
- ticket_pending / create_ticket action shows a 'Create Support Ticket'
  button that calls POST /api/ai/chat/confirm, marks the message
  confirmed (hides the button), and appends the ticket result as a new
  assistant message

Helper functions navUrl() and actionLabel() map suggested_action keys
to dashboard URLs and human-readable labels respectively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tracewebstudio Dev 2026-08-14 18:06:07 +02:00
parent 55c832b620
commit 614b0ed43a

View file

@ -9,6 +9,12 @@ interface ChatMessage {
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 {
@ -22,6 +28,7 @@ interface ChatResponse {
message?: string;
status?: string;
suggested_action?: string;
action_type?: string;
}
function statusLabel(status?: string): string | null {
@ -37,6 +44,43 @@ function statusLabel(status?: string): string | 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";
default: return "";
}
}
interface UsageStatus {
remaining_credits: number;
remaining_daily_actions: number;
@ -149,6 +193,9 @@ export function AiChatWidget() {
intent: data.intent,
status: data.status,
suggestedAction: data.suggested_action,
actionType: data.action_type,
userQuery: text,
confirmed: false,
};
setMessages((prev) => [...prev, assistantMessage]);
} catch (err) {
@ -172,6 +219,65 @@ export function AiChatWidget() {
}
};
/** 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);
}
};
return (
<>
{/* Floating button */}
@ -316,59 +422,120 @@ export function AiChatWidget() {
}}
>
<For each={messages()}>
{(msg) => (
<div
style={{
display: "flex",
"align-items": "flex-start",
gap: "8px",
"flex-direction": msg.role === "user" ? "row-reverse" : "row",
}}
>
{(msg, idx) => (
<>
<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,
"align-items": "flex-start",
gap: "8px",
"flex-direction": msg.role === "user" ? "row-reverse" : "row",
}}
>
<Show when={msg.role === "user"} fallback={<Bot size={14} color="#6B7280" />}>
<User size={14} color="#fff" />
</Show>
<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.role === "assistant" && statusLabel(msg.status)}>
{(label) => (
<p
style={{
margin: "6px 0 0",
"font-size": "10px",
color: "#6B7280",
"font-weight": "600",
}}
>
{label()}
</p>
)}
</Show>
</div>
</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.role === "assistant" && statusLabel(msg.status)}>
{(label) => (
<p
{/* Action buttons — only for unconfirmed assistant messages with a suggested action */}
<Show when={msg.role === "assistant" && msg.suggestedAction && !msg.confirmed}>
<div
style={{
"padding-left": "36px",
"margin-top": "-4px",
display: "flex",
gap: "6px",
"flex-wrap": "wrap",
}}
>
{/* Navigation button — links to the relevant dashboard section */}
<Show when={msg.suggestedAction !== "create_ticket" && navUrl(msg.suggestedAction)}>
{(url) => (
<a
href={url()}
style={{
display: "inline-flex",
"align-items": "center",
padding: "5px 12px",
background: "#FFF5F0",
border: "1px solid #FF5E13",
"border-radius": "20px",
color: "#FF5E13",
"font-size": "11px",
"font-weight": "600",
"text-decoration": "none",
cursor: "pointer",
transition: "background 0.15s",
}}
>
{actionLabel(msg.suggestedAction)}
</a>
)}
</Show>
{/* Confirm-create button — for ticket_pending messages */}
<Show when={msg.suggestedAction === "create_ticket" || msg.actionType === "ticket_pending"}>
<button
onClick={() => confirmCreateTicket(idx(), msg.userQuery || msg.content)}
style={{
margin: "6px 0 0",
"font-size": "10px",
color: "#6B7280",
display: "inline-flex",
"align-items": "center",
padding: "5px 12px",
background: "#FF5E13",
border: "none",
"border-radius": "20px",
color: "#fff",
"font-size": "11px",
"font-weight": "600",
cursor: "pointer",
}}
>
{label()}
</p>
)}
</Show>
</div>
</div>
Create Support Ticket
</button>
</Show>
</div>
</Show>
</>
)}
</For>
<Show when={isLoading()}>