i-self 0.4.3

Personal developer-companion CLI: scans your repos, indexes code semantically, watches your activity, and moves AI-agent sessions between tools (Claude Code, Aider, Goose, OpenAI Codex CLI, Continue.dev, OpenCode).
#![allow(dead_code)]

use axum::{
    extract::{Path, State},
    response::{Html, Json, IntoResponse},
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;

use super::AppState;

// Dashboard HTML
pub async fn dashboard_handler() -> Html<&'static str> {
    Html(include_str!("dashboard.html"))
}

// API Handlers
pub async fn get_profile(
    State(state): State<Arc<RwLock<AppState>>>,
) -> impl IntoResponse {
    let state = state.read().await;
    
    match &state.profile {
        Some(profile) => Json(profile).into_response(),
        None => (StatusCode::NOT_FOUND, "Profile not found").into_response(),
    }
}

pub async fn get_stats(
    State(state): State<Arc<RwLock<AppState>>>,
) -> impl IntoResponse {
    let state = state.read().await;

    // Surface the active embedding backend so the UI can show a degraded-mode
    // banner when the user is on the hash-bucket fallback. Constructing the
    // generator is cheap (it just reads env); we don't cache it on AppState
    // because the env can change between runs.
    let embedder_id = crate::semantic::EmbeddingGenerator::new(
        crate::semantic::SemanticConfig::default(),
    )
    .ok()
    .map(|g| (g.id(), g.is_semantic()));

    let stats = serde_json::json!({
        "has_profile": state.profile.is_some(),
        "has_semantic_search": state.semantic_search.is_some(),
        "has_ai": state.ai_assistant.is_some(),
        "team_count": state.team_profiles.len(),
        "embedder": embedder_id.as_ref().map(|(id, _)| id.as_str()),
        "embedder_is_semantic": embedder_id.as_ref().map(|(_, sem)| *sem).unwrap_or(false),
    });

    Json(stats)
}

// Semantic Search
#[derive(Debug, Deserialize)]
pub struct SearchRequest {
    query: String,
    #[serde(default = "default_top_k")]
    top_k: usize,
    language: Option<String>,
}

fn default_top_k() -> usize {
    10
}

#[derive(Debug, Serialize)]
pub struct SearchResponse {
    results: Vec<SearchResultItem>,
    total: usize,
    query: String,
}

#[derive(Debug, Serialize)]
pub struct SearchResultItem {
    content: String,
    language: String,
    source_file: String,
    repository: String,
    score: f32,
    highlights: Vec<String>,
}

pub async fn semantic_search(
    State(state): State<Arc<RwLock<AppState>>>,
    Json(request): Json<SearchRequest>,
) -> impl IntoResponse {
    let state = state.read().await;
    
    match &state.semantic_search {
        Some(search) => {
            let results = match search.search(&request.query, request.top_k).await {
                Ok(r) => r,
                Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
            };
            
            let items: Vec<SearchResultItem> = results.into_iter()
                .map(|r| SearchResultItem {
                    content: r.embedding.content,
                    language: r.embedding.metadata.language,
                    source_file: r.embedding.metadata.source_file,
                    repository: r.embedding.metadata.repository,
                    score: r.score,
                    highlights: r.highlights,
                })
                .collect();
            
            let response = SearchResponse {
                total: items.len(),
                query: request.query,
                results: items,
            };
            
            Json(response).into_response()
        }
        None => (StatusCode::SERVICE_UNAVAILABLE, "Semantic search not initialized").into_response(),
    }
}

pub async fn search_stats(
    State(state): State<Arc<RwLock<AppState>>>,
) -> impl IntoResponse {
    let state = state.read().await;
    
    match &state.semantic_search {
        Some(search) => {
            let stats = search.stats();
            Json(stats).into_response()
        }
        None => (StatusCode::SERVICE_UNAVAILABLE, "Semantic search not initialized").into_response(),
    }
}

// AI Assistant
#[derive(Debug, Deserialize)]
pub struct AskRequest {
    question: String,
    context: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct AskResponse {
    answer: String,
    model: String,
}

pub async fn ai_ask(
    State(state): State<Arc<RwLock<AppState>>>,
    Json(request): Json<AskRequest>,
) -> impl IntoResponse {
    let state = state.read().await;
    
    match &state.ai_assistant {
        Some(assistant) => {
            match assistant.ask(&request.question, None).await {
                Ok(answer) => {
                    let response = AskResponse {
                        answer,
                        model: "gpt-4".to_string(), // Should come from config
                    };
                    Json(response).into_response()
                }
                Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
            }
        }
        None => (StatusCode::SERVICE_UNAVAILABLE, "AI assistant not initialized").into_response(),
    }
}

#[derive(Debug, Deserialize)]
pub struct ExplainRequest {
    code: String,
    language: String,
}

pub async fn ai_explain(
    State(state): State<Arc<RwLock<AppState>>>,
    Json(request): Json<ExplainRequest>,
) -> impl IntoResponse {
    let state = state.read().await;
    
    match &state.ai_assistant {
        Some(assistant) => {
            match assistant.explain_code(&request.code, &request.language).await {
                Ok(explanation) => {
                    Json(serde_json::json!({ "explanation": explanation })).into_response()
                }
                Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
            }
        }
        None => (StatusCode::SERVICE_UNAVAILABLE, "AI assistant not initialized").into_response(),
    }
}

#[derive(Debug, Deserialize)]
pub struct GenerateRequest {
    description: String,
    language: String,
}

pub async fn ai_generate(
    State(state): State<Arc<RwLock<AppState>>>,
    Json(request): Json<GenerateRequest>,
) -> impl IntoResponse {
    let state = state.read().await;
    
    match &state.ai_assistant {
        Some(assistant) => {
            match assistant.generate_code(&request.description, &request.language, None).await {
                Ok(code) => {
                    Json(serde_json::json!({ "code": code })).into_response()
                }
                Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
            }
        }
        None => (StatusCode::SERVICE_UNAVAILABLE, "AI assistant not initialized").into_response(),
    }
}

// Team
pub async fn list_teams(
    State(state): State<Arc<RwLock<AppState>>>,
) -> impl IntoResponse {
    let state = state.read().await;
    let teams: Vec<String> = state.team_profiles.keys().cloned().collect();
    Json(teams).into_response()
}

pub async fn get_team(
    State(state): State<Arc<RwLock<AppState>>>,
    Path(name): Path<String>,
) -> impl IntoResponse {
    let state = state.read().await;
    
    match state.team_profiles.get(&name) {
        Some(team) => Json(team).into_response(),
        None => (StatusCode::NOT_FOUND, format!("Team {} not found", name)).into_response(),
    }
}

pub async fn aggregate_team(
    State(_state): State<Arc<RwLock<AppState>>>,
    Path(name): Path<String>,
) -> impl IntoResponse {
    // This would trigger team aggregation
    Json(serde_json::json!({ "status": "aggregation started", "team": name })).into_response()
}

// Static files
pub async fn static_handler(
    axum::extract::Path(path): axum::extract::Path<String>,
) -> impl IntoResponse {
    // Serve static files (CSS, JS)
    match path.as_str() {
        "style.css" => (
            [("content-type", "text/css")],
            include_str!("static/style.css")
        ).into_response(),
        "app.js" => (
            [("content-type", "application/javascript")],
            include_str!("static/app.js")
        ).into_response(),
        _ => (StatusCode::NOT_FOUND, "Not found").into_response(),
    }
}