feat(ai): add action_type to AskAshResponse + POST /api/ai/chat/confirm
All checks were successful
build-and-release / build (catering-services) (push) Successful in 6s
build-and-release / build (companies) (push) Successful in 9s
build-and-release / build (developers) (push) Successful in 5s
build-and-release / build (cron) (push) Successful in 12s
build-and-release / build (customers) (push) Successful in 11s
build-and-release / build (gateway) (push) Successful in 7s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (graphic-designers) (push) Successful in 5s
build-and-release / build (employees) (push) Successful in 10s
build-and-release / build (job-seekers) (push) Successful in 5s
build-and-release / build (makeup-artists) (push) Successful in 6s
build-and-release / build (jobs) (push) Successful in 9s
build-and-release / build (payments) (push) Successful in 5s
build-and-release / build (photographers) (push) Successful in 10s
build-and-release / build (social-media-managers) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 6s
build-and-release / build (tutors) (push) Successful in 10s
build-and-release / build (video-editors) (push) Successful in 9s
backend-integration-tests / ai-credits (push) Successful in 9s
build-and-release / build (users) (push) Successful in 3m14s

AskAshResponse gains action_type: Option<String> 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 <noreply@anthropic.com>
This commit is contained in:
Tracewebstudio Dev 2026-08-14 18:06:04 +02:00
parent cc2dab112b
commit 38981ffe70

View file

@ -2215,6 +2215,13 @@ pub struct AskAshResponse {
pub ollama_used: bool,
pub status: Option<String>,
pub suggested_action: Option<String>,
/// 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<String>,
pub remaining_credits: Option<i32>,
pub remaining_daily_actions: Option<i32>,
}
@ -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<String>,
fields: Option<serde_json::Value>,
user_id: Option<Uuid>,
}
async fn ai_chat_confirm(
auth: AuthUser,
State(state): State<AppState>,
Json(body): Json<ChatConfirmRequest>,
) -> 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<AppState> {
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))