2023-07-19 19:29:17 +05:30
|
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2023-08-02 21:22:22 +05:30
|
|
|
from datetime import datetime
|
|
|
|
import locale
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
from gi.repository import Gtk, Gio, Adw, GLib
|
2023-08-02 21:22:22 +05:30
|
|
|
from babel.dates import format_date, format_datetime, format_time
|
|
|
|
from babel import Locale
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
class CustomEntry(Gtk.TextView):
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
super().__init__(**kwargs)
|
2023-08-02 21:00:19 +05:30
|
|
|
super().set_css_name("entry")
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
@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()
|
2023-07-21 05:05:27 +05:30
|
|
|
status_no_chat_thread = Gtk.Template.Child()
|
|
|
|
status_no_thread = Gtk.Template.Child()
|
|
|
|
status_no_thread_main = Gtk.Template.Child()
|
2023-07-19 19:29:17 +05:30
|
|
|
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()
|
2023-07-21 05:05:27 +05:30
|
|
|
thread_stack = Gtk.Template.Child()
|
2023-07-19 19:29:17 +05:30
|
|
|
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)
|
2023-08-02 21:00:19 +05:30
|
|
|
self.message_entry.set_top_margin(7)
|
|
|
|
self.message_entry.set_bottom_margin(7)
|
2023-07-19 19:29:17 +05:30
|
|
|
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, ["<primary>Escape"])
|
2023-08-02 20:18:10 +05:30
|
|
|
self.create_action("clear_all", self.on_clear_all)
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
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):
|
2023-07-21 05:21:26 +05:30
|
|
|
print("LOADING")
|
2023-07-19 19:29:17 +05:30
|
|
|
self.threads_list.remove_all()
|
|
|
|
if self.app.data["chats"]:
|
2023-07-21 05:05:27 +05:30
|
|
|
self.thread_stack.set_visible_child(self.threads_list)
|
2023-07-21 05:21:26 +05:30
|
|
|
self.stack.set_visible_child(self.main)
|
2023-07-19 19:29:17 +05:30
|
|
|
for chat in self.app.data["chats"]:
|
|
|
|
thread = ThreadItem(self, chat)
|
|
|
|
self.threads_list.append(thread)
|
|
|
|
self.threads.append(thread)
|
2023-07-21 05:28:54 +05:30
|
|
|
|
2023-08-02 20:18:10 +05:30
|
|
|
try:
|
|
|
|
if not chat["content"]:
|
|
|
|
self.stack.set_visible_child(self.status_no_chat)
|
|
|
|
except KeyError:
|
2023-07-21 05:28:54 +05:30
|
|
|
self.stack.set_visible_child(self.status_no_chat)
|
2023-08-02 20:40:06 +05:30
|
|
|
self.stack.set_visible_child(self.status_no_thread_main)
|
2023-07-19 19:29:17 +05:30
|
|
|
else:
|
2023-07-21 05:05:27 +05:30
|
|
|
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)
|
|
|
|
|
2023-07-21 05:21:26 +05:30
|
|
|
@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)
|
|
|
|
|
2023-07-21 05:05:27 +05:30
|
|
|
def do_size_allocate(self, width, height, baseline):
|
2023-07-21 05:21:26 +05:30
|
|
|
try:
|
|
|
|
self.has_been_allocated
|
|
|
|
except Exception:
|
|
|
|
self.has_been_allocated = True
|
|
|
|
self.load_threads()
|
2023-07-21 05:05:27 +05:30
|
|
|
|
|
|
|
Adw.ApplicationWindow.do_size_allocate(self, width, height, baseline)
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
@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()
|
2023-08-02 20:11:05 +05:30
|
|
|
i = 0
|
2023-07-19 19:29:17 +05:30
|
|
|
for item in self.content:
|
2023-08-02 20:11:05 +05:30
|
|
|
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)
|
2023-07-19 19:29:17 +05:30
|
|
|
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"]
|
2023-08-02 20:18:10 +05:30
|
|
|
self.stack.set_visible_child(self.status_no_chat)
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
# PROVIDER - ONLINE
|
|
|
|
def load_provider_selector(self):
|
|
|
|
provider_menu = Gio.Menu()
|
|
|
|
|
|
|
|
for provider in self.app.providers.values():
|
2023-08-02 20:36:49 +05:30
|
|
|
section = Gio.Menu()
|
2023-07-19 19:29:17 +05:30
|
|
|
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))
|
2023-08-02 20:36:49 +05:30
|
|
|
section.append_item(item_provider)
|
|
|
|
provider_menu.append_section(_("Providers"), section)
|
2023-07-19 19:29:17 +05:30
|
|
|
else:
|
2023-08-02 20:36:49 +05:30
|
|
|
section = Gio.Menu()
|
2023-07-19 19:29:17 +05:30
|
|
|
item_provider = Gio.MenuItem()
|
|
|
|
item_provider.set_label(_("Preferences"))
|
|
|
|
item_provider.set_action_and_target_value("app.preferences", None)
|
2023-08-02 20:36:49 +05:30
|
|
|
section.append_item(item_provider)
|
2023-07-19 19:29:17 +05:30
|
|
|
|
2023-08-02 20:18:10 +05:30
|
|
|
item_provider = Gio.MenuItem()
|
|
|
|
item_provider.set_label(_("Clear all"))
|
|
|
|
item_provider.set_action_and_target_value("win.clear_all", None)
|
2023-08-02 20:36:49 +05:30
|
|
|
section.append_item(item_provider)
|
|
|
|
|
|
|
|
provider_menu.append_section(None, section)
|
2023-08-02 20:18:10 +05:30
|
|
|
|
2023-07-19 19:29:17 +05:30
|
|
|
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()
|
|
|
|
|
|
|
|
for provider in self.app.models:
|
2023-08-02 20:36:49 +05:30
|
|
|
section = Gio.Menu()
|
2023-07-19 19:29:17 +05:30
|
|
|
item_provider = Gio.MenuItem()
|
|
|
|
item_provider.set_label(provider)
|
|
|
|
item_provider.set_action_and_target_value(
|
|
|
|
"app.set_model",
|
|
|
|
GLib.Variant("s", provider))
|
2023-08-02 20:36:49 +05:30
|
|
|
section.append_item(item_provider)
|
|
|
|
provider_menu.append_section(_("Models"), section)
|
2023-07-19 19:29:17 +05:30
|
|
|
else:
|
2023-08-02 20:36:49 +05:30
|
|
|
section = Gio.Menu()
|
2023-07-19 19:29:17 +05:30
|
|
|
item_provider = Gio.MenuItem()
|
|
|
|
item_provider.set_label(_("Preferences"))
|
|
|
|
item_provider.set_action_and_target_value("app.preferences", None)
|
2023-08-02 20:36:49 +05:30
|
|
|
section.append_item(item_provider)
|
2023-07-19 19:29:17 +05:30
|
|
|
|
2023-08-02 20:18:10 +05:30
|
|
|
item_provider = Gio.MenuItem()
|
|
|
|
item_provider.set_label(_("Clear all"))
|
|
|
|
item_provider.set_action_and_target_value("win.clear_all", None)
|
2023-08-02 20:36:49 +05:30
|
|
|
section.append_item(item_provider)
|
|
|
|
|
|
|
|
provider_menu.append_section(None, section)
|
2023-08-02 20:18:10 +05:30
|
|
|
|
2023-07-19 19:29:17 +05:30
|
|
|
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()
|
|
|
|
|
2023-07-24 14:40:37 +05:30
|
|
|
@Gtk.Template.Callback()
|
|
|
|
def on_emoji(self, *args):
|
|
|
|
self.message_entry.do_insert_emoji(self.message_entry)
|
|
|
|
|
2023-07-19 19:29:17 +05:30
|
|
|
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)
|
|
|
|
|
2023-08-02 21:22:22 +05:30
|
|
|
def get_time(self):
|
|
|
|
return format_time(datetime.now())
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
|
|
|
|
def add_user_item(self, content):
|
|
|
|
self.content.append(
|
|
|
|
{
|
2023-08-02 23:49:45 +05:30
|
|
|
"role": self.app.user_name,
|
2023-07-19 19:29:17 +05:30
|
|
|
"content": content,
|
2023-08-02 21:22:22 +05:30
|
|
|
"time": self.get_time(),
|
2023-07-19 19:29:17 +05:30
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
self.threads_row_activated_cb()
|
|
|
|
|
|
|
|
def add_assistant_item(self, content):
|
|
|
|
c = {
|
2023-08-02 23:49:45 +05:30
|
|
|
"role": self.app.bot_name,
|
2023-07-19 19:29:17 +05:30
|
|
|
"content": content,
|
2023-08-02 21:22:22 +05:30
|
|
|
"time": self.get_time(),
|
2023-07-19 19:29:17 +05:30
|
|
|
}
|
|
|
|
|
2023-08-02 23:32:46 +05:30
|
|
|
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"
|
2023-07-19 19:29:17 +05:30
|
|
|
|
|
|
|
|
|
|
|
self.content.append(c)
|
|
|
|
|
|
|
|
self.threads_row_activated_cb()
|
|
|
|
|
|
|
|
|
2023-07-24 14:40:37 +05:30
|
|
|
|