icebreaker 0.1.0

A local AI chat app powered by 🦀Rust, 🧊iced, 🤗Hugging Face, and 🦙llama.cpp
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod data;
mod icon;
mod screen;
mod widget;

use crate::data::assistant;
use crate::data::{Chat, Error};
use crate::screen::boot;
use crate::screen::conversation;
use crate::screen::search;
use crate::screen::Screen;

use iced::system;
use iced::{Element, Subscription, Task, Theme};

pub fn main() -> iced::Result {
    iced::application(Icebreaker::title, Icebreaker::update, Icebreaker::view)
        .font(icon::FONT_BYTES)
        .subscription(Icebreaker::subscription)
        .theme(Icebreaker::theme)
        .run_with(Icebreaker::new)
}

struct Icebreaker {
    screen: Screen,
    system: Option<system::Information>,
}

#[derive(Debug, Clone)]
enum Message {
    Loaded {
        last_chat: Result<Chat, Error>,
        system: Box<system::Information>,
    },
    Escape,
    Search(search::Message),
    Boot(boot::Message),
    Conversation(conversation::Message),
}

impl Icebreaker {
    pub fn new() -> (Self, Task<Message>) {
        (
            Self {
                screen: Screen::Loading,
                system: None,
            },
            Task::future(Chat::fetch_last_opened()).then(|last_chat| {
                system::fetch_information()
                    .map(Box::new)
                    .map(move |system| Message::Loaded {
                        last_chat: last_chat.clone(),
                        system,
                    })
            }),
        )
    }

    fn title(&self) -> String {
        match &self.screen {
            Screen::Loading => "Icebreaker".to_owned(),
            Screen::Search(search) => search.title(),
            Screen::Boot(boot) => boot.title(),
            Screen::Conversation(conversation) => conversation.title(),
        }
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Loaded { last_chat, system } => {
                let backend = assistant::Backend::detect(&system.graphics_adapter);
                self.system = Some(*system);

                if let Ok(last_chat) = last_chat {
                    let (conversation, task) = screen::Conversation::open(last_chat, backend);

                    self.screen = Screen::Conversation(conversation);

                    task.map(Message::Conversation)
                } else {
                    self.search()
                }
            }
            Message::Search(message) => {
                if let Screen::Search(search) = &mut self.screen {
                    let action = search.update(message);

                    match action {
                        search::Action::None => Task::none(),
                        search::Action::Run(task) => task.map(Message::Search),
                        search::Action::Boot(model) => {
                            let (boot, task) = screen::Boot::new(model, self.system.as_ref());

                            self.screen = Screen::Boot(boot);

                            task.map(Message::Boot)
                        }
                    }
                } else {
                    Task::none()
                }
            }
            Message::Boot(message) => {
                if let Screen::Boot(search) = &mut self.screen {
                    let action = search.update(message);

                    match action {
                        boot::Action::None => Task::none(),
                        boot::Action::Boot { file, backend } => {
                            let (conversation, task) = screen::Conversation::new(file, backend);

                            self.screen = Screen::Conversation(conversation);

                            task.map(Message::Conversation)
                        }
                        boot::Action::Abort => self.search(),
                    }
                } else {
                    Task::none()
                }
            }
            Message::Conversation(message) => {
                if let Screen::Conversation(conversation) = &mut self.screen {
                    let action = conversation.update(message);

                    match action {
                        conversation::Action::None => Task::none(),
                        conversation::Action::Run(task) => task.map(Message::Conversation),
                        conversation::Action::Back => self.search(),
                    }
                } else {
                    Task::none()
                }
            }
            Message::Escape => {
                if matches!(self.screen, Screen::Search(_)) {
                    Task::none()
                } else {
                    self.search()
                }
            }
        }
    }

    fn view(&self) -> Element<Message> {
        match &self.screen {
            Screen::Loading => screen::loading(),
            Screen::Search(search) => search.view().map(Message::Search),
            Screen::Boot(boot) => boot.view(self.theme()).map(Message::Boot),
            Screen::Conversation(conversation) => conversation.view().map(Message::Conversation),
        }
    }

    fn subscription(&self) -> Subscription<Message> {
        use iced::keyboard;

        let screen = match &self.screen {
            Screen::Loading => Subscription::none(),
            Screen::Search(search) => search.subscription().map(Message::Search),
            Screen::Boot(_) => Subscription::none(),
            Screen::Conversation(conversation) => {
                conversation.subscription().map(Message::Conversation)
            }
        };

        let hotkeys = keyboard::on_key_press(|key, _modifiers| match key {
            keyboard::Key::Named(keyboard::key::Named::Escape) => Some(Message::Escape),
            _ => None,
        });

        Subscription::batch([screen, hotkeys])
    }

    fn theme(&self) -> Theme {
        Theme::TokyoNight
    }

    fn search(&mut self) -> Task<Message> {
        let (search, task) = screen::Search::new();

        self.screen = Screen::Search(search);

        task.map(Message::Search)
    }
}