j-cli 12.9.34

A fast CLI tool for alias management, daily reports, and productivity
//! Chat UI 组件(薄封装)
//!
//! 通用组件复用 `tui::components`,仅保留 chat 专属组件。

use crate::theme::Theme;
use crate::tui::components::{
    LABEL_WIDTH, TOGGLE_OFF, TOGGLE_ON, cursor_spans, desc_span, label_span, pointer_span,
    value_style,
};
use ratatui::{
    style::{Modifier, Style},
    text::{Line, Span},
};

// ── 预览值(内部)───────────────────────────────────────────

/// 长文本截断预览(替换换行为空格,超 40 字符截断)
fn render_preview_value(raw: &str) -> String {
    if raw.is_empty() {
        return "(\u{7a7a})".to_string();
    }
    let flat: String = raw
        .chars()
        .map(|c| if c == '\n' { ' ' } else { c })
        .collect();
    if flat.chars().count() > 40 {
        let truncated: String = flat.chars().take(40).collect();
        format!("{truncated}...")
    } else {
        flat
    }
}

// ── API Key 遮罩字段行(chat 专属)─────────────────────────────

/// API Key 字段(未编辑时使用 api_key 颜色)
pub fn secret_field_row<'a>(
    label: &str,
    value: &str,
    selected: bool,
    editing: bool,
    cursor: usize,
    theme: &Theme,
) -> Line<'a> {
    if editing && selected {
        let vs = value_style(selected, editing, theme);
        let mut spans = vec![
            pointer_span(selected, theme),
            label_span(label, LABEL_WIDTH, selected, theme),
            Span::styled("  ", Style::default()),
        ];
        spans.extend(cursor_spans(value, cursor, vs, theme));
        Line::from(spans)
    } else {
        let vs = if selected {
            Style::default().fg(theme.text_white)
        } else {
            Style::default().fg(theme.config_api_key)
        };
        Line::from(vec![
            pointer_span(selected, theme),
            label_span(label, LABEL_WIDTH, selected, theme),
            Span::styled("  ", Style::default()),
            Span::styled(
                if value.is_empty() {
                    "(\u{7a7a})".to_string()
                } else {
                    value.to_string()
                },
                vs,
            ),
        ])
    }
}

// ── Global tab 三列布局行(chat 专属)───────────────────────────

/// Global tab 可编辑文本行(三列: label | value | desc)
pub fn global_text_row<'a>(
    label: &str,
    value: &str,
    desc: &str,
    selected: bool,
    editing: bool,
    cursor: usize,
    theme: &Theme,
) -> Line<'a> {
    let vs = value_style(selected, editing, theme);
    if editing && selected {
        let mut spans = vec![
            pointer_span(selected, theme),
            label_span(label, LABEL_WIDTH, selected, theme),
            Span::styled("  ", Style::default()),
        ];
        spans.extend(cursor_spans(value, cursor, vs, theme));
        Line::from(spans)
    } else {
        let display_value = if value.is_empty() {
            "(\u{7a7a})".to_string()
        } else {
            value.to_string()
        };
        Line::from(vec![
            pointer_span(selected, theme),
            label_span(label, LABEL_WIDTH, selected, theme),
            Span::styled("  ", Style::default()),
            Span::styled(display_value, vs),
            desc_span(desc, 30, theme),
        ])
    }
}

/// Global tab 开关行(三列: label | toggle | desc)
pub fn global_toggle_row<'a>(
    label: &str,
    is_on: bool,
    desc: &str,
    selected: bool,
    hint: &str,
    theme: &Theme,
) -> Line<'a> {
    let toggle_style = if is_on {
        Style::default()
            .fg(theme.config_toggle_on)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default().fg(theme.config_toggle_off)
    };
    let toggle_text = if is_on {
        format!("{TOGGLE_ON} \u{5f00}\u{542f}")
    } else {
        format!("{TOGGLE_OFF} \u{5173}\u{95ed}")
    };
    Line::from(vec![
        pointer_span(selected, theme),
        label_span(label, LABEL_WIDTH, selected, theme),
        Span::styled("  ", Style::default()),
        Span::styled(toggle_text, toggle_style),
        desc_span(desc, 30, theme),
        Span::styled(
            if selected {
                format!("  ({hint})")
            } else {
                String::new()
            },
            Style::default().fg(theme.config_dim),
        ),
    ])
}

/// Global tab 预览行(三列: label | preview | desc)
pub fn global_preview_row<'a>(
    label: &str,
    raw: &str,
    desc: &str,
    selected: bool,
    hint: &str,
    theme: &Theme,
) -> Line<'a> {
    let vs = value_style(selected, false, theme);
    Line::from(vec![
        pointer_span(selected, theme),
        label_span(label, LABEL_WIDTH, selected, theme),
        Span::styled("  ", Style::default()),
        Span::styled(render_preview_value(raw), vs),
        desc_span(desc, 30, theme),
        Span::styled(
            if selected {
                format!("  ({hint})")
            } else {
                String::new()
            },
            Style::default().fg(theme.config_dim),
        ),
    ])
}

/// Global tab 主题行(三列: label | theme | desc)
pub fn global_theme_row<'a>(
    label: &str,
    name: &str,
    desc: &str,
    selected: bool,
    hint: &str,
    theme: &Theme,
) -> Line<'a> {
    Line::from(vec![
        pointer_span(selected, theme),
        label_span(label, LABEL_WIDTH, selected, theme),
        Span::styled("  ", Style::default()),
        Span::styled(
            format!("\u{1f3a8} {name}"),
            Style::default()
                .fg(theme.config_toggle_on)
                .add_modifier(Modifier::BOLD),
        ),
        desc_span(desc, 30, theme),
        Span::styled(
            if selected {
                format!("  ({hint})")
            } else {
                String::new()
            },
            Style::default().fg(theme.config_dim),
        ),
    ])
}

// ── 欢迎框(chat 专属)─────────────────────────────────────────

/// 自适应居中欢迎框(主题感知渐变色)
///
/// 渐变色从 Theme.welcome_gradient_start/mid/end 读取,
/// 并基于 quote_idx 做正弦偏移产生变体,保证每次启动略有不同
/// 但不偏离主题基调。
pub fn welcome_box<'a>(width: u16, theme: &Theme, quote_idx: usize) -> Vec<Line<'a>> {
    use unicode_width::UnicodeWidthStr;

    // 框体内部宽度:取终端内宽的一半,最少 30,最多 55
    let inner = ((width as usize) / 2).clamp(30, 55);
    let box_w = inner + 2;

    let total_w = width as usize;
    let left_pad = if total_w > box_w {
        (total_w - box_w) / 2
    } else {
        0
    };
    let pad: String = " ".repeat(left_pad);

    let border_style = Style::default().fg(theme.welcome_border);

    // ── 渐变色调色板(已迁移至 palette.rs) ──────────────────
    use super::palette;
    let triple = palette::get_gradient(theme.welcome_palette, quote_idx);
    let (start_c, mid_c, end_c) = triple;

    // ── 顶部边框:嵌入 ◈ 装饰符 ──
    // 形如:╭──── ◈ ────╮
    let ornament = "";
    let orn_w = UnicodeWidthStr::width(ornament);
    let bar_sides = inner.saturating_sub(orn_w);
    let left_h = bar_sides / 2;
    let right_h = bar_sides - left_h;
    let h_bar_top = format!(
        "\u{256d}{}{}{}\u{256e}",
        "\u{2500}".repeat(left_h),
        ornament,
        "\u{2500}".repeat(right_h),
    );
    let h_bar_bot = format!("\u{2570}{}\u{256f}", "\u{2500}".repeat(inner));
    let empty_row = format!("\u{2502}{}\u{2502}", " ".repeat(inner));

    // ── 诗句自然换行 ──
    // 文字有效宽度:框内减去两侧各 1 格呼吸空间
    let text_area = inner.saturating_sub(2);
    let quote = super::quotes::get_quote(quote_idx);

    // ── 诗句换行逻辑 ──
    // 先判断整句是否能放下:能放下则整行显示,否则按标点断行
    let quote_width = quote
        .chars()
        .map(|c| UnicodeWidthStr::width(c.to_string().as_str()))
        .sum::<usize>();

    let cn_break = ['', '', '', '', '', ''];
    let en_break = [',', '.', '!', '?'];

    // 能放下就不断行,直接返回单行
    let lines_chars: Vec<Vec<char>> = if quote_width <= text_area {
        vec![quote.chars().collect()]
    } else {
        // 放不下才按标点断行
        let mut lines_chars: Vec<Vec<char>> = Vec::new();
        let mut cur: Vec<char> = Vec::new();
        let mut cur_w = 0usize;

        for ch in quote.chars() {
            let cw = UnicodeWidthStr::width(ch.to_string().as_str());
            // 超宽:先把当前行入列,再开新行
            if cur_w + cw > text_area && !cur.is_empty() {
                lines_chars.push(std::mem::take(&mut cur));
                cur_w = 0;
            }
            cur.push(ch);
            cur_w += cw;
            // 中文标点:无条件断行
            if cn_break.contains(&ch) {
                lines_chars.push(std::mem::take(&mut cur));
                cur_w = 0;
            } else if en_break.contains(&ch) && cur_w * 2 >= text_area {
                // 英文标点:需已累积至半行以上
                lines_chars.push(std::mem::take(&mut cur));
                cur_w = 0;
            }
        }
        if !cur.is_empty() {
            lines_chars.push(cur);
        }
        lines_chars
    };

    // ── 全局渐变:整句诗从首字到末字连续插值 ──
    let total_chars: usize = lines_chars.iter().map(|l| l.len()).sum();
    // 至少 2 以避免除零;单字时视为首尾同色
    let total_n = total_chars.max(2);

    let mut quote_lines: Vec<Line<'a>> = Vec::new();
    let mut global_idx = 0usize;

    for line_chars in &lines_chars {
        let line_w: usize = line_chars
            .iter()
            .map(|c| UnicodeWidthStr::width(c.to_string().as_str()))
            .sum();
        // 居中:两侧留白至少 1 格
        let pl = if inner > line_w + 2 {
            (inner - line_w) / 2
        } else {
            1
        };
        let pr = inner.saturating_sub(line_w + pl);

        let mut spans: Vec<Span<'a>> = vec![Span::styled(
            format!("{}\u{2502}{}", pad, " ".repeat(pl)),
            border_style,
        )];

        for (i, &ch) in line_chars.iter().enumerate() {
            let gi = global_idx + i;
            let t = gi as f32 / (total_n - 1) as f32;
            // 三色分段插值:前半段 start→mid,后半段 mid→end
            let (from, to, local_t) = if t <= 0.5 {
                (start_c, mid_c, t * 2.0)
            } else {
                (mid_c, end_c, (t - 0.5) * 2.0)
            };
            let r = (from.0 as f32 * (1.0 - local_t) + to.0 as f32 * local_t).round() as u8;
            let g = (from.1 as f32 * (1.0 - local_t) + to.1 as f32 * local_t).round() as u8;
            let b = (from.2 as f32 * (1.0 - local_t) + to.2 as f32 * local_t).round() as u8;
            spans.push(Span::styled(
                ch.to_string(),
                Style::default().fg(ratatui::style::Color::Rgb(r, g, b)),
            ));
        }

        spans.push(Span::styled(
            format!("{}\u{2502}", " ".repeat(pr)),
            border_style,
        ));

        quote_lines.push(Line::from(spans));
        global_idx += line_chars.len();

        // 多行诗:行间插入空行,让视觉更通透
        if lines_chars.len() > 1 {
            quote_lines.push(Line::from(Span::styled(
                format!("{pad}{empty_row}"),
                border_style,
            )));
        }
    }

    // 多行诗插入了行间空行,移除最后一个多余的
    if lines_chars.len() > 1 && quote_lines.len() > 1 {
        quote_lines.pop();
    }

    // ── 内边距:单行留两行,多行各留一行 ──
    let pad_rows = if lines_chars.len() == 1 { 2 } else { 1 };
    let make_empty =
        || -> Line<'a> { Line::from(Span::styled(format!("{pad}{empty_row}"), border_style)) };

    let mut result: Vec<Line<'a>> = vec![
        Line::from(""),
        Line::from(""),
        Line::from(Span::styled(format!("{pad}{h_bar_top}"), border_style)),
    ];
    for _ in 0..pad_rows {
        result.push(make_empty());
    }
    result.extend(quote_lines);
    for _ in 0..pad_rows {
        result.push(make_empty());
    }
    result.push(Line::from(Span::styled(
        format!("{pad}{h_bar_bot}"),
        border_style,
    )));

    result
}