use std::sync::Arc; use crate::{ chat::models::{ChatMessageRequest, ChatMessageResponse}, error::AppError, forms::{models::FormExtractRequest, service::FormService}, jobs::{models::GenerateJobDescriptionRequest, service::JobsService}, providers::{ help_center::help_center_provider::HelpCenterProvider, llm::ai_provider::AiProvider, }, tickets::{models::CreateTicketRequest, service::TicketService}, }; #[derive(Clone)] pub struct ChatOrchestrator { jobs_service: JobsService, form_service: FormService, help_center: Arc, ticket_service: TicketService, ai_provider: Arc, } impl ChatOrchestrator { pub fn new( jobs_service: JobsService, form_service: FormService, help_center: Arc, ticket_service: TicketService, ai_provider: Arc, ) -> Self { Self { jobs_service, form_service, help_center, ticket_service, ai_provider, } } pub async fn handle_chat( &self, request: ChatMessageRequest, ) -> Result { let intent = classify_intent(&request.message); let conversation_id = request.conversation_id.clone(); match intent.as_str() { "job_description_generation" => { let jd = self .jobs_service .generate_description(GenerateJobDescriptionRequest { role_title: request.message.clone(), seniority: None, department: None, employment_type: None, required_skills: vec!["communication".to_string()], optional_skills: None, responsibilities: None, company_context: None, }) .await?; Ok(ChatMessageResponse { intent: intent.clone(), reply: "Generated a draft job description.".to_string(), data: serde_json::to_value(jd).unwrap_or(serde_json::Value::Null), conversation_id: conversation_id.clone(), }) } "form_filling_assistance" => { let extracted = self .form_service .extract(FormExtractRequest { raw_user_input: request.message.clone(), expected_fields: None, }) .await?; Ok(ChatMessageResponse { intent: intent.clone(), reply: extracted.suggested_next_step.clone(), data: serde_json::to_value(extracted).unwrap_or(serde_json::Value::Null), conversation_id: conversation_id.clone(), }) } "search_kb" => { let matches = self.help_center.search(&request.message).await?; let reply = if matches.is_empty() { "No help article match found. Try rephrasing your question.".to_string() } else { let mut response = format!("Found {} help article(s):\n\n", matches.len()); for (i, article) in matches.iter().take(5).enumerate() { response.push_str(&format!("{}. **{}**\n{}\n\n", i + 1, article.title, article.summary)); } if matches.len() > 5 { response.push_str(&format!("...and {} more articles.", matches.len() - 5)); } response }; Ok(ChatMessageResponse { intent: intent.clone(), reply, data: serde_json::json!({ "matches": matches }), conversation_id: conversation_id.clone(), }) } "support_ticket_creation" => { let created = self .ticket_service .create(CreateTicketRequest { subject: request.message.chars().take(80).collect::(), description: request.message.clone(), priority: "medium".to_string(), category: "general".to_string(), user_id: request.user_id.unwrap_or_else(|| "anonymous".to_string()), conversation_id: conversation_id.clone(), source: Some("chatbot".to_string()), tags: Some(vec!["chat".to_string()]), metadata: None, }) .await?; Ok(ChatMessageResponse { intent: intent.clone(), reply: format!("Support ticket created: {}. Our team will respond shortly.", created.ticket_id), data: serde_json::to_value(created).unwrap_or(serde_json::Value::Null), conversation_id: conversation_id.clone(), }) } "explain_plan_limits" => { let reply = "Your AI plan determines how many AI actions you can use per month.\n\n\ - **Free AI**: 10 actions/month\n\ - **Starter AI**: 100 actions/month\n\ - **Growth AI**: 500 actions/month\n\ - **Pro AI**: 2000 actions/month\n\n\ You can purchase add-on packs for more usage. Would you like to upgrade your plan?"; Ok(ChatMessageResponse { intent: intent.clone(), reply: reply.to_string(), data: serde_json::json!({}), conversation_id: conversation_id.clone(), }) } "check_ai_pack_balance" => { let reply = "To check your AI balance, please visit the AI Usage section in your dashboard.\n\n\ You can view:\n\ - Monthly usage vs limit\n\ - Add-on balance remaining\n\ - Renewal date\n\n\ Would you like me to help with anything else?"; Ok(ChatMessageResponse { intent: intent.clone(), reply: reply.to_string(), data: serde_json::json!({}), conversation_id: conversation_id.clone(), }) } "generate_cover_letter" => { let reply = "I can help you generate a cover letter. Please provide:\n\ - The job title or position\n\ - Your key skills and experience\n\ - Any specific company or role details (optional)\n\n\ Or you can use the 'Generate Cover Letter' button on the job application page."; Ok(ChatMessageResponse { intent: intent.clone(), reply: reply.to_string(), data: serde_json::json!({}), conversation_id: conversation_id.clone(), }) } "improve_resume_summary" => { let reply = "I can help improve your resume summary. Please share:\n\ - Your current resume summary (or paste it here)\n\ - The type of role you're targeting\n\ - Your key skills and experience\n\n\ Or you can use the 'Improve Resume' feature in your profile page."; Ok(ChatMessageResponse { intent: intent.clone(), reply: reply.to_string(), data: serde_json::json!({}), conversation_id: conversation_id.clone(), }) } _ => { let generic = self .ai_provider .complete( "You are Nxtgauge workflow assistant. Keep answers concise and actionable. If users ask about features, guide them to use the appropriate buttons or pages.", &request.message, ) .await?; Ok(ChatMessageResponse { intent, reply: generic, data: serde_json::json!({}), conversation_id: conversation_id.clone(), }) } } } } fn classify_intent(message: &str) -> String { let text = message.to_lowercase(); if text.contains("job description") || text.contains("generate job") || text.contains("create job") || text.contains("write job") || (text.contains("jd") && text.len() < 10) { return "job_description_generation".to_string(); } if text.contains("cover letter") || text.contains("write a letter") || text.contains("generate letter") { return "generate_cover_letter".to_string(); } if text.contains("resume") && (text.contains("improve") || text.contains("rewrite") || text.contains("summary") || text.contains("tailor")) { return "improve_resume_summary".to_string(); } if text.contains("form") || text.contains("field") || text.contains("fill") { return "form_filling_assistance".to_string(); } if text.contains("search kb") || text.contains("find article") || text.contains("how do i") || text.contains("how to") || text.contains("where do i") || (text.contains("help") && !text.contains("help me")) || text.contains("article") || text.contains("kb") || text.contains("docs") || text.contains("documentation") { return "search_kb".to_string(); } if text.contains("ticket") || text.contains("support") || text.contains("issue") || text.contains("bug") || text.contains("problem") || text.contains("not working") || text.contains("error") { return "support_ticket_creation".to_string(); } if text.contains("ai plan") || text.contains("ai limit") || text.contains("ai package") || text.contains("ai credit") || text.contains("upgrade ai") { return "explain_plan_limits".to_string(); } if text.contains("balance") && (text.contains("ai") || text.contains("credit") || text.contains("action")) { return "check_ai_pack_balance".to_string(); } "general".to_string() }