icebreaker 2026.2.0

A local AI chat app powered by 🦀Rust, 🧊iced, 🤗Hugging Face, and 🦙llama.cpp
use crate::core::assistant;
use crate::ui::markdown;
use crate::ui::{Markdown, Reasoning};

use iced::widget::column;
use iced::{Element, Theme};

#[derive(Debug, Default)]
pub struct Reply {
    reasoning: Option<Reasoning>,
    content: String,
    markdown: Markdown,
}

impl Reply {
    pub fn from_data(reply: assistant::Reply) -> Self {
        Self {
            reasoning: reply.reasoning.map(Reasoning::from_data),
            markdown: Markdown::parse(&reply.content),
            content: reply.content,
        }
    }

    pub fn to_data(&self) -> assistant::Reply {
        assistant::Reply {
            reasoning: self.reasoning.as_ref().map(Reasoning::to_data),
            content: self.content.as_str().to_owned(),
            last_token: None,
        }
    }

    pub fn to_text(&self) -> String {
        match &self.reasoning {
            Some(reasoning) if reasoning.show => {
                format!(
                    "{reasoning}\n\n{content}",
                    reasoning = reasoning
                        .thoughts
                        .iter()
                        .map(|thought| format!("> {thought}"))
                        .collect::<Vec<_>>()
                        .join("\n>\n"),
                    content = self.content.as_str()
                )
            }
            _ => self.content.as_str().to_owned(),
        }
    }

    pub fn update(&mut self, new_reply: assistant::Reply) {
        self.reasoning = new_reply.reasoning.map(Reasoning::from_data);
        self.content = new_reply.content;

        if let Some(reasoning) = &mut self.reasoning {
            reasoning.show = new_reply.last_token.is_none();
        }

        if let Some(token) = new_reply.last_token {
            self.markdown.push_str(&token);
        }
    }

    pub fn toggle_reasoning(&mut self, show: bool) {
        if let Some(reasoning) = &mut self.reasoning {
            reasoning.show = show;
        }
    }

    pub fn view<Message>(
        &self,
        theme: &Theme,
        on_reasoning_toggle: impl Fn(bool) -> Message,
        on_markdown_interaction: impl Fn(markdown::Interaction) -> Message + 'static,
    ) -> Element<'_, Message>
    where
        Message: Clone + 'static,
    {
        let message = self.markdown.view(theme).map(on_markdown_interaction);

        if let Some(reasoning) = &self.reasoning {
            column![reasoning.quote(on_reasoning_toggle), message]
                .spacing(20)
                .into()
        } else {
            message
        }
    }
}