feat(ai-chat): wire action buttons + ticket confirm flow in AiChatWidget
All checks were successful
build-and-release / build (push) Successful in 2m4s
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:
parent
55c832b620
commit
614b0ed43a
1 changed files with 210 additions and 43 deletions
|
|
@ -9,6 +9,12 @@ interface ChatMessage {
|
||||||
intent?: string;
|
intent?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
suggestedAction?: 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 {
|
interface ChatResponse {
|
||||||
|
|
@ -22,6 +28,7 @@ interface ChatResponse {
|
||||||
message?: string;
|
message?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
suggested_action?: string;
|
suggested_action?: string;
|
||||||
|
action_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusLabel(status?: string): string | null {
|
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 {
|
interface UsageStatus {
|
||||||
remaining_credits: number;
|
remaining_credits: number;
|
||||||
remaining_daily_actions: number;
|
remaining_daily_actions: number;
|
||||||
|
|
@ -149,6 +193,9 @@ export function AiChatWidget() {
|
||||||
intent: data.intent,
|
intent: data.intent,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
suggestedAction: data.suggested_action,
|
suggestedAction: data.suggested_action,
|
||||||
|
actionType: data.action_type,
|
||||||
|
userQuery: text,
|
||||||
|
confirmed: false,
|
||||||
};
|
};
|
||||||
setMessages((prev) => [...prev, assistantMessage]);
|
setMessages((prev) => [...prev, assistantMessage]);
|
||||||
} catch (err) {
|
} 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Floating button */}
|
{/* Floating button */}
|
||||||
|
|
@ -316,7 +422,8 @@ export function AiChatWidget() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<For each={messages()}>
|
<For each={messages()}>
|
||||||
{(msg) => (
|
{(msg, idx) => (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -369,6 +476,66 @@ export function AiChatWidget() {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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={{
|
||||||
|
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",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Support Ticket
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={isLoading()}>
|
<Show when={isLoading()}>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue