diff --git a/apps/users/src/handlers/ai.rs b/apps/users/src/handlers/ai.rs index 78f56f6..271791a 100644 --- a/apps/users/src/handlers/ai.rs +++ b/apps/users/src/handlers/ai.rs @@ -95,7 +95,9 @@ async fn call_ollama(_state: &AppState, model: &str, prompt: &str) -> Result (String, f32) { let prompt = format!( - "Classify this user message into one intent category. Categories: ticket_creation, form_filling, help_search, job_description_generation, general. \ + "Classify this user message into one intent category. Categories: \ + ticket_creation, form_filling, help_search, job_description_generation, \ + generate_cover_letter, improve_resume, request_view_contact, auto_apply_job, unknown, general. \ Return ONLY the intent name, nothing else.\n\nMessage: {}", message ); @@ -109,14 +111,27 @@ async fn classify_intent(message: &str, ollama_base: &str, model: &str) -> (Stri "form_filling" => "form_filling", "help_search" => "help_search", "job_description_generation" => "job_description_generation", + "generate_cover_letter" => "generate_cover_letter", + "improve_resume" => "improve_resume", + "request_view_contact" => "request_view_contact", + "auto_apply_job" => "auto_apply_job", + "unknown" => "unknown", _ => "general", }; (intent.to_string(), confidence) } - Err(_) => ("general".to_string(), 0.5), + Err(_) => ("unknown".to_string(), 0.0), } } +fn is_internal_admin(auth: &AuthUser) -> bool { + let active = auth.claims.active_role.as_str(); + active == "ADMIN" + || active == "SUPER_ADMIN" + || auth.claims.roles.contains(&"ADMIN".to_string()) + || auth.claims.roles.contains(&"SUPER_ADMIN".to_string()) +} + async fn call_ollama_inline(base_url: &str, model: &str, prompt: &str) -> Result { let url = format!("{}/api/generate", base_url); let req = OllamaGenerateRequest { @@ -199,7 +214,8 @@ async fn ai_chat_message( } _ => { "I couldn't find any help articles matching your question. \ - Try rephrasing or contact support if you need further assistance." + If you need further assistance, I can help you create a support ticket instead. \ + Just describe your issue and I'll guide you through the ticket creation process." .to_string() } } @@ -247,6 +263,17 @@ async fn ai_chat_message( } } } + "unknown" => { + "I'm not sure I understand your request. I can help you with:\n\n\ + - Creating support tickets\n\ + - Searching help articles\n\ + - Generating job descriptions\n\ + - Writing cover letters\n\ + - Improving your resume\n\ + - Applying to jobs\n\ + - Requesting to view lead contacts\n\n\ + Could you please rephrase your request?".to_string() + } _ => { let system_prompt = "You are a helpful AI assistant for Nxtgauge platform. Provide clear, concise responses. \ If the user needs support, guide them to create a ticket."; @@ -570,6 +597,10 @@ async fn ai_generate_job_field( auth: AuthUser, Json(body): Json, ) -> impl IntoResponse { + if is_internal_admin(&auth) { + return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI job description generation. Use the admin panel to manage jobs." }))).into_response(); + } + let company: Option = sqlx::query_scalar( "SELECT id FROM company_profiles WHERE user_id = $1" ) @@ -667,6 +698,10 @@ async fn ai_generate_cover_letter( auth: AuthUser, Json(body): Json, ) -> impl IntoResponse { + if is_internal_admin(&auth) { + return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI cover letter generation." }))).into_response(); + } + let seeker: Option<(Uuid, String, Option, i32, Vec)> = sqlx::query_as( "SELECT id, full_name, summary, experience_years, skills FROM job_seeker_profiles WHERE user_id = $1" ) @@ -776,6 +811,10 @@ async fn ai_tailor_resume( auth: AuthUser, Json(body): Json, ) -> impl IntoResponse { + if is_internal_admin(&auth) { + return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI resume tailoring." }))).into_response(); + } + let seeker: Option<(Uuid, String, Option, i32, Vec)> = sqlx::query_as( "SELECT id, full_name, summary, experience_years, skills FROM job_seeker_profiles WHERE user_id = $1" ) @@ -892,6 +931,10 @@ async fn ai_auto_apply( auth: AuthUser, Json(body): Json, ) -> impl IntoResponse { + if is_internal_admin(&auth) { + return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI auto-apply." }))).into_response(); + } + if body.job_ids.is_empty() || body.job_ids.len() > 10 { return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Select 1-10 jobs at a time" }))).into_response(); } @@ -1057,6 +1100,10 @@ async fn ai_auto_respond_to_lead( auth: AuthUser, Json(body): Json, ) -> impl IntoResponse { + if is_internal_admin(&auth) { + return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Admin users cannot use AI contact reveal. Use the admin panel to manage leads." }))).into_response(); + } + let leads_service_url = std::env::var("LEADS_SERVICE_URL") .unwrap_or_else(|_| "http://localhost:9118".to_string()); @@ -1074,6 +1121,42 @@ async fn ai_auto_respond_to_lead( return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Profile not found" }))).into_response(); }; + let approval_status: Option = sqlx::query_scalar( + "SELECT status FROM user_role_profiles WHERE id = $1" + ) + .bind(profile_id) + .fetch_optional(&state.pool) + .await + .ok() + .flatten(); + + let Some(status) = approval_status else { + return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Profile not found" }))).into_response(); + }; + + if status != "APPROVED" { + return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Your profile must be approved before you can request lead contact access. Please complete verification." }))).into_response(); + } + + let wallet: Option<(Uuid, i64)> = sqlx::query_as( + "SELECT id, balance FROM tracecoin_wallets WHERE user_id = $1" + ) + .bind(auth.user_id) + .fetch_optional(&state.pool) + .await + .ok() + .flatten(); + + let (wallet_id, balance) = match wallet { + Some((id, bal)) => (id, bal), + None => return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Wallet not found. Please contact support." }))).into_response(), + }; + + let tracecoins_cost = 30; + if balance < tracecoins_cost as i64 { + return (StatusCode::PAYMENT_REQUIRED, Json(serde_json::json!({ "error": format!("Insufficient balance. You need {} Tracecoins but have {}. Please top up your wallet.", tracecoins_cost, balance) }))).into_response(); + } + let today = chrono::Utc::now().date_naive(); let used: Option = sqlx::query_scalar( "SELECT generations_used FROM job_seeker_ai_usage WHERE job_seeker_id = $1 AND usage_date = $2"