From 38981ffe702183b5d02634a7fb379329a51f2744 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Fri, 14 Aug 2026 18:06:04 +0200 Subject: [PATCH] feat(ai): add action_type to AskAshResponse + POST /api/ai/chat/confirm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AskAshResponse gains action_type: Option so the frontend knows what kind of UI to render for each response: ticket_created — ticket was auto-created inline ticket_pending — user wants a ticket but none was created (show confirm button) kb_results — KB articles found usage_info — usage/credits summary navigation — navigate to the relevant feature page POST /api/ai/chat/confirm (new): executes AI-suggested actions the user confirms in the chat widget. action = 'create_ticket' creates a support ticket from the conversation text and returns the ticket id + subject. Additional actions (profile save, contact request) can be added here. Wired into ai_router() at /chat/confirm. Co-Authored-By: Claude Sonnet 4.6 --- apps/users/src/handlers/ai.rs | 109 ++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/apps/users/src/handlers/ai.rs b/apps/users/src/handlers/ai.rs index 38822e8..fe1ab6b 100644 --- a/apps/users/src/handlers/ai.rs +++ b/apps/users/src/handlers/ai.rs @@ -2215,6 +2215,13 @@ pub struct AskAshResponse { pub ollama_used: bool, pub status: Option, pub suggested_action: Option, + /// Semantic category of what the frontend should do next: + /// "ticket_created" — ticket was auto-created inline + /// "ticket_pending" — user wants a ticket but none was created yet (show confirm button) + /// "kb_results" — KB articles were found (show link to help center) + /// "usage_info" — usage/credits summary + /// "navigation" — navigate to the relevant feature page + pub action_type: Option, pub remaining_credits: Option, pub remaining_daily_actions: Option, } @@ -2350,6 +2357,7 @@ async fn ai_chat_ask( ollama_used: false, status: Some("usage_summary".to_string()), suggested_action: Some("show_usage_modal".to_string()), + action_type: Some("usage_info".to_string()), remaining_credits: Some(remaining_credits), remaining_daily_actions: Some(remaining_daily_actions), }), @@ -2375,6 +2383,7 @@ async fn ai_chat_ask( ollama_used: false, status: Some("kb_results".to_string()), suggested_action: Some("open_help_search".to_string()), + action_type: Some("kb_results".to_string()), remaining_credits: None, remaining_daily_actions: None, }), @@ -2409,6 +2418,7 @@ async fn ai_chat_ask( ollama_used: false, status: Some("ticket_created".to_string()), suggested_action: Some("open_support_ticket".to_string()), + action_type: Some("ticket_created".to_string()), remaining_credits: None, remaining_daily_actions: None, }), @@ -2530,6 +2540,12 @@ Assistant:"); }); } + // Classify action_type based on intent so the frontend knows what to render. + let action_type = match routed.intent { + phase3::Intent::TicketCreation | phase3::Intent::TechnicalSupport => "ticket_pending", + _ => "navigation", + }; + ( StatusCode::OK, Json(AskAshResponse { @@ -2544,6 +2560,7 @@ Assistant:"); ollama_used, status: Some("answered".to_string()), suggested_action: Some(routed.suggested_action.to_string()), + action_type: Some(action_type.to_string()), remaining_credits, remaining_daily_actions, }), @@ -2551,6 +2568,97 @@ Assistant:"); .into_response() } +// ── POST /api/ai/chat/confirm ───────────────────────────────────────────────── +// Executes an AI-suggested action that the user confirmed in the chat widget. +// Currently supports action = "create_ticket". Further actions can be added as +// the confirm flow matures; unknown actions return 400. + +#[derive(Debug, Deserialize)] +struct ChatConfirmRequest { + action: String, + conversation_id: Option, + fields: Option, + user_id: Option, +} + +async fn ai_chat_confirm( + auth: AuthUser, + State(state): State, + Json(body): Json, +) -> impl IntoResponse { + let user_id = body + .user_id + .filter(|u| *u != Uuid::nil()) + .unwrap_or(auth.user_id); + + match body.action.as_str() { + "create_ticket" => { + let fields = body.fields.unwrap_or_default(); + let subject = fields + .get("subject") + .and_then(|v| v.as_str()) + .unwrap_or("Support Request via AI Chat"); + let description = fields.get("description").and_then(|v| v.as_str()); + let category = fields + .get("category") + .and_then(|v| v.as_str()) + .unwrap_or("ai_assisted"); + let priority = fields + .get("priority") + .and_then(|v| v.as_str()) + .unwrap_or("medium"); + + let result = sqlx::query_as::<_, TicketRow>( + r#" + INSERT INTO support_tickets (user_id, subject, description, category, priority, status) + VALUES ($1, $2, $3, $4, $5, 'new') + RETURNING id, reference_number, subject, description, category, priority, status, + requester_name, requester_email, assigned_to, created_at, updated_at + "#, + ) + .bind(user_id) + .bind(subject) + .bind(description) + .bind(category) + .bind(priority) + .fetch_one(&state.pool) + .await; + + match result { + Ok(t) => ( + StatusCode::OK, + Json(serde_json::json!({ + "success": true, + "action": "create_ticket", + "ticket_id": t.id, + "reference_number": t.reference_number, + "subject": t.subject, + "message": format!("Support ticket #{} created: {}. Our team will be in touch.", t.reference_number, t.subject) + })), + ) + .into_response(), + Err(e) => { + tracing::error!("chat_confirm ticket creation failed: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ "success": false, "error": "Failed to create ticket" })), + ) + .into_response() + } + } + } + + other => { + tracing::warn!("ai_chat_confirm: unknown action '{}'", other); + ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ "success": false, "error": format!("Unknown action: {other}") })), + ) + .into_response() + } + } +} + // ── GET /api/ai/suggestions ─────────────────────────────────────────────────── #[derive(Debug, Serialize, Clone)] @@ -4259,6 +4367,7 @@ pub fn ai_router() -> Router { Router::new() .route("/chat/message", post(ai_chat_message)) .route("/chat/ask", post(ai_chat_ask)) + .route("/chat/confirm", post(ai_chat_confirm)) .route("/help/ask", post(ai_help_ask)) .route("/suggestions", get(ai_suggestions)) .route("/context", post(ai_save_context))