# window.py # # Copyright 2023 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later from datetime import datetime import locale from gi.repository import Gtk, Gio, Adw, GLib from babel.dates import format_date, format_datetime, format_time from babel import Locale from bavarder.constants import app_id, build_type, rootdir from bavarder.widgets.thread_item import ThreadItem from bavarder.widgets.item import Item from bavarder.threading import KillableThread from bavarder.views.export_dialog import ExportDialog class CustomEntry(Gtk.TextView): def __init__(self, **kwargs): super().__init__(**kwargs) super().set_css_name("entry") @Gtk.Template(resource_path=f'{rootdir}/ui/window.ui') class BavarderWindow(Adw.ApplicationWindow): __gtype_name__ = 'BavarderWindow' split_view = Gtk.Template.Child() threads_list = Gtk.Template.Child() title = Gtk.Template.Child() main_list = Gtk.Template.Child() status_no_chat = Gtk.Template.Child() status_no_chat_thread = Gtk.Template.Child() status_no_thread = Gtk.Template.Child() status_no_thread_main = Gtk.Template.Child() status_no_internet = Gtk.Template.Child() scrolled_window = Gtk.Template.Child() local_mode_toggle = Gtk.Template.Child() provider_selector_button = Gtk.Template.Child() model_selector_button = Gtk.Template.Child() banner = Gtk.Template.Child() toast_overlay = Gtk.Template.Child() stack = Gtk.Template.Child() thread_stack = Gtk.Template.Child() main = Gtk.Template.Child() threads = [] def __init__(self, **kwargs): super().__init__(**kwargs) self.app = Gtk.Application.get_default() self.settings = Gio.Settings(schema_id=app_id) CustomEntry.set_css_name("entry") self.message_entry = CustomEntry() self.message_entry.set_hexpand(True) self.message_entry.set_accepts_tab(False) self.message_entry.set_top_margin(7) self.message_entry.set_bottom_margin(7) self.message_entry.set_margin_start(5) self.message_entry.set_margin_end(5) self.message_entry.set_wrap_mode(Gtk.WrapMode.WORD) self.message_entry.add_css_class("chat-entry") self.scrolled_window.set_child(self.message_entry) self.load_threads() self.local_mode_toggle.set_active(self.app.local_mode) self.on_local_mode_toggled(self.local_mode_toggle) self.create_action("cancel", self.cancel, ["Escape"]) self.create_action("clear_all", self.on_clear_all) self.create_action("export", self.on_export, ["e"]) self.settings.bind( "width", self, "default-width", Gio.SettingsBindFlags.DEFAULT ) self.settings.bind( "height", self, "default-height", Gio.SettingsBindFlags.DEFAULT ) self.settings.bind( "is-maximized", self, "maximized", Gio.SettingsBindFlags.DEFAULT ) self.settings.bind( "is-fullscreen", self, "fullscreened", Gio.SettingsBindFlags.DEFAULT ) @property def chat(self): try: return self.threads_list.get_selected_row().get_child().chat except AttributeError: # create a new chat self.on_new_chat_action() finally: return self.threads_list.get_selected_row().get_child().chat @property def content(self): try: return self.chat["content"] except KeyError: # no content self.chat["content"] = [] finally: return self.chat["content"] def load_threads(self): print("LOADING") self.threads_list.remove_all() if self.app.data["chats"]: self.thread_stack.set_visible_child(self.threads_list) self.stack.set_visible_child(self.main) for chat in self.app.data["chats"]: thread = ThreadItem(self, chat) self.threads_list.append(thread) self.threads.append(thread) try: if not chat["content"]: self.stack.set_visible_child(self.status_no_chat) except KeyError: self.stack.set_visible_child(self.status_no_chat) self.stack.set_visible_child(self.status_no_thread_main) else: if self.props.default_width < 500: self.thread_stack.set_visible_child(self.status_no_thread) self.stack.set_visible_child(self.status_no_chat) else: self.stack.set_visible_child(self.status_no_thread_main) self.thread_stack.set_visible_child(self.status_no_chat_thread) @Gtk.Template.Callback() def mobile_mode_apply(self, *args): if not self.app.data["chats"]: self.thread_stack.set_visible_child(self.status_no_thread) self.stack.set_visible_child(self.status_no_chat) @Gtk.Template.Callback() def mobile_mode_unapply(self, *args): if not self.app.data["chats"]: self.stack.set_visible_child(self.status_no_thread_main) self.thread_stack.set_visible_child(self.status_no_chat_thread) def do_size_allocate(self, width, height, baseline): try: self.has_been_allocated except Exception: self.has_been_allocated = True self.load_threads() Adw.ApplicationWindow.do_size_allocate(self, width, height, baseline) @Gtk.Template.Callback() def threads_row_activated_cb(self, *args): self.split_view.set_collapsed(True) self.split_view.set_show_content(True) self.title.set_title(self.chat["title"]) if self.content: self.stack.set_visible_child(self.main) self.main_list.remove_all() i = 0 for item in self.content: i += 1 item = Item(self, self.chat, item) self.main_list.append(item) for i in range(i): row = self.main_list.get_row_at_index(i) row.set_selectable(False) row.set_activatable(False) else: self.stack.set_visible_child(self.status_no_chat) @Gtk.Template.Callback() def on_new_chat_action(self, *args): self.app.on_new_chat_action(_, _) # @Gtk.Template.Callback() # def scroll_down(self, *args): # self.scrolled_window.emit("scroll-child", Gtk.ScrollType.END, False) def on_clear_all(self, *args): if self.content: self.stack.set_visible_child(self.main) self.main_list.remove_all() del self.chat["content"] self.stack.set_visible_child(self.status_no_chat) def on_export(self, *args): if self.content: dialog = ExportDialog(self, self.chat["content"]) dialog.set_transient_for(self) dialog.present() # PROVIDER - ONLINE def load_provider_selector(self): provider_menu = Gio.Menu() section = Gio.Menu() for provider in self.app.providers.values(): if provider.enabled: item_provider = Gio.MenuItem() item_provider.set_label(provider.name) item_provider.set_action_and_target_value( "app.set_provider", GLib.Variant("s", provider.slug)) section.append_item(item_provider) else: if self.app.providers: provider_menu.append_section(_("Providers"), section) section = Gio.Menu() item_provider = Gio.MenuItem() item_provider.set_label(_("Preferences")) item_provider.set_action_and_target_value("app.preferences", None) section.append_item(item_provider) item_provider = Gio.MenuItem() item_provider.set_label(_("Clear all")) item_provider.set_action_and_target_value("win.clear_all", None) section.append_item(item_provider) item_provider = Gio.MenuItem() item_provider.set_label(_("Export")) item_provider.set_action_and_target_value("win.export", None) section.append_item(item_provider) provider_menu.append_section(None, section) self.provider_selector_button.set_menu_model(provider_menu) # MODEL - OFFLINE def load_model_selector(self): provider_menu = Gio.Menu() if not self.app.models: self.app.list_models() section = Gio.Menu() for provider in self.app.models: item_provider = Gio.MenuItem() item_provider.set_label(provider) item_provider.set_action_and_target_value( "app.set_model", GLib.Variant("s", provider)) section.append_item(item_provider) else: if self.app.models: provider_menu.append_section(_("Models"), section) section = Gio.Menu() item_provider = Gio.MenuItem() item_provider.set_label(_("Preferences")) item_provider.set_action_and_target_value("app.preferences", None) section.append_item(item_provider) item_provider = Gio.MenuItem() item_provider.set_label(_("Clear all")) item_provider.set_action_and_target_value("win.clear_all", None) section.append_item(item_provider) item_provider = Gio.MenuItem() item_provider.set_label(_("Export")) item_provider.set_action_and_target_value("win.export", None) section.append_item(item_provider) provider_menu.append_section(None, section) self.model_selector_button.set_menu_model(provider_menu) @Gtk.Template.Callback() def on_local_mode_toggled(self, widget): self.app.local_mode = widget.get_active() if self.app.local_mode: self.local_mode_toggle.set_icon_name("cloud-disabled-symbolic") self.model_selector_button.set_visible(True) self.provider_selector_button.set_visible(False) else: self.local_mode_toggle.set_icon_name("cloud-filled-symbolic") self.provider_selector_button.set_visible(True) self.model_selector_button.set_visible(False) def check_network(self): if self.app.check_network(): # Internet if not self.content: self.status_no_chat.set_visible(True) self.status_no_internet.set_visible(False) else: self.status_no_chat.set_visible(False) self.status_no_internet.set_visible(False) else: self.status_no_chat.set_visible(False) self.status_no_internet.set_visible(True) @Gtk.Template.Callback() def on_ask(self, *args): if not self.threads: # no chat self.on_new_chat_action() self.threads_list.select_row(self.threads_list.get_row_at_index(0)) prompt = self.message_entry.get_buffer().props.text.strip() if prompt: self.message_entry.get_buffer().set_text("") self.add_user_item(prompt) def thread_run(): self.toast = Adw.Toast() self.toast.set_title(_("Generating response")) self.toast.set_button_label(_("Cancel")) self.toast.set_action_name("win.cancel") self.toast.set_timeout(0) self.toast_overlay.add_toast(self.toast) response = self.app.ask(prompt, self.chat) GLib.idle_add(cleanup, response, self.toast) def cleanup(response, toast): self.t.join() self.toast.dismiss() self.add_assistant_item(response) self.t = KillableThread(target=thread_run) self.t.start() @Gtk.Template.Callback() def on_emoji(self, *args): self.message_entry.do_insert_emoji(self.message_entry) def cancel(self, *args): try: self.t.kill() self.t.join() self.toast.dismiss() except AttributeError: # nothing to stop pass def create_action(self, name, callback, shortcuts=None): action = Gio.SimpleAction.new(name, None) action.connect("activate", callback) self.add_action(action) if shortcuts: self.app.set_accels_for_action(f"win.{name}", shortcuts) def get_time(self): return format_time(datetime.now()) def add_user_item(self, content): self.content.append( { "role": self.app.user_name, "content": content, "time": self.get_time(), } ) self.threads_row_activated_cb() def add_assistant_item(self, content): c = { "role": self.app.bot_name, "content": content, "time": self.get_time(), } if self.app.local_mode: if self.app.setup_chat(): c["model"] = self.app.model_name else: c["model"] = "bavarder" else: l = list(self.app.providers.values()) for p in l: if p.enabled and p.slug == self.app.current_provider: c["model"] = self.app.current_provider break else: c["model"] = "bavarder" self.content.append(c) self.threads_row_activated_cb()