diff --git a/src/custom_widgets/chat_widget.py b/src/custom_widgets/chat_widget.py index 7e64d45c..cecccb31 100644 --- a/src/custom_widgets/chat_widget.py +++ b/src/custom_widgets/chat_widget.py @@ -340,6 +340,11 @@ def __init__(self): self.connect("row-selected", lambda listbox, row: self.chat_changed(row)) self.tab_list = [] + def update_profile_pictures(self): + for tab in self.tab_list: + for message in tab.chat_window.messages.values(): + message.update_profile_picture() + def update_welcome_screens(self, show_prompts:bool): for tab in self.tab_list: if tab.chat_window.welcome_screen: diff --git a/src/custom_widgets/message_widget.py b/src/custom_widgets/message_widget.py index 0ccc0913..5bffb265 100644 --- a/src/custom_widgets/message_widget.py +++ b/src/custom_widgets/message_widget.py @@ -425,7 +425,7 @@ def regenerate_message(self): class footer(Gtk.Box): __gtype_name__ = 'AlpacaMessageFooter' - def __init__(self, dt:datetime.datetime, model:str=None, system:bool=False): + def __init__(self, dt:datetime.datetime, message_element, model:str=None, system:bool=False): super().__init__( orientation=0, hexpand=True, @@ -439,16 +439,24 @@ def __init__(self, dt:datetime.datetime, model:str=None, system:bool=False): ellipsize=3, wrap_mode=2, margin_end=10, + margin_start=10 if message_element.profile_picture_data else 0, xalign=0, focusable=True, - css_classes=['dim-label'] + css_classes=[] if message_element.profile_picture_data else ['dim-label'] ) message_author = "" if model: - message_author = window.convert_model_name(model, 0) + " • " + model_name = window.convert_model_name(model, 0).rstrip(" (latest)") + if message_element.profile_picture_data: + message_author = model_name + else: + message_author = "{} • ".format(model_name) if system: message_author = "{} • ".format(_("System")) - label.set_markup("{}{}".format(message_author, GLib.markup_escape_text(self.format_datetime(dt)))) + if message_element.profile_picture_data: + label.set_markup("{}\n{}".format(message_author, GLib.markup_escape_text(self.format_datetime(dt)))) + else: + label.set_markup("{}{}".format(message_author, GLib.markup_escape_text(self.format_datetime(dt)))) self.append(label) def format_datetime(self, dt:datetime) -> str: @@ -462,14 +470,44 @@ def format_datetime(self, dt:datetime) -> str: def add_options_button(self): self.popup = option_popup(self.get_parent().get_parent()) - self.options_button = Gtk.MenuButton( - icon_name='view-more-horizontal-symbolic', - css_classes=['message_options_button', 'flat', 'circular', 'dim-label'], - popover=self.popup - ) - self.prepend(self.options_button) + message_element = self.get_parent().get_parent() + + if self.options_button: + self.options_button.get_parent().remove(self.options_button) + + if message_element.profile_picture_data: + image_data = base64.b64decode(message_element.profile_picture_data) + loader = GdkPixbuf.PixbufLoader.new() + loader.write(image_data) + loader.close() + pixbuf = loader.get_pixbuf() + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + message_element.profile_picture = Gtk.Image.new_from_paintable(texture) + message_element.profile_picture.set_size_request(40, 40) + self.options_button = Gtk.MenuButton( + width_request=40, + height_request=40, + css_classes=['circular'], + valign=1, + popover=self.popup, + margin_top=5 + ) + self.options_button.set_overflow(1) + self.options_button.set_child(message_element.profile_picture) + list(self.options_button)[0].add_css_class('circular') + list(self.options_button)[0].set_overflow(1) + message_element.prepend(self.options_button) + + if not self.options_button: + self.options_button = Gtk.MenuButton( + icon_name='view-more-horizontal-symbolic', + css_classes=['message_options_button', 'flat', 'circular', 'dim-label'], + popover=self.popup + ) + self.prepend(self.options_button) + -class message(Adw.Bin): +class message(Gtk.Box): __gtype_name__ = 'AlpacaMessage' def __init__(self, message_id:str, model:str=None, system:bool=False): @@ -484,12 +522,18 @@ def __init__(self, message_id:str, model:str=None, system:bool=False): self.attachment_c = None self.spinner = None self.text = None + self.profile_picture_data = None + self.profile_picture = None + if self.bot and self.model: + model_row = window.model_manager.model_selector.get_model_by_name(self.model) + if model_row: + self.profile_picture_data = model_row.profile_picture_data self.container = Gtk.Box( orientation=1, halign='fill', css_classes=["response_message"] if self.bot or self.system else ["card", "user_message"], - spacing=10, + spacing=5, width_request=-1 if self.bot or self.system else 100 ) @@ -498,7 +542,17 @@ def __init__(self, message_id:str, model:str=None, system:bool=False): name=message_id, halign=0 if self.bot or self.system else 2 ) - self.set_child(self.container) + + self.append(self.container) + + def update_profile_picture(self): + if self.bot and self.model: + model_row = window.model_manager.model_selector.get_model_by_name(self.model) + if model_row: + new_profile_picture_data = model_row.profile_picture_data + if new_profile_picture_data != self.profile_picture_data: + self.profile_picture_data = new_profile_picture_data + self.add_footer(self.dt) def add_attachment(self, name:str, attachment_type:str, content:str): if attachment_type == 'image': @@ -514,8 +568,10 @@ def add_attachment(self, name:str, attachment_type:str, content:str): def add_footer(self, dt:datetime.datetime): self.dt = dt - self.footer = footer(self.dt, self.model, self.system) - self.container.append(self.footer) + if self.footer: + self.container.remove(self.footer) + self.footer = footer(self.dt, self, self.model, self.system) + self.container.prepend(self.footer) self.footer.add_options_button() def update_message(self, data:dict): diff --git a/src/custom_widgets/model_widget.py b/src/custom_widgets/model_widget.py index 4fab23ea..9fd98081 100644 --- a/src/custom_widgets/model_widget.py +++ b/src/custom_widgets/model_widget.py @@ -6,8 +6,8 @@ import gi gi.require_version('Gtk', '4.0') gi.require_version('GtkSource', '5') -from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk -import logging, os, datetime, re, shutil, threading, json, sys, glob, icu +from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk, GdkPixbuf +import logging, os, datetime, re, shutil, threading, json, sys, glob, icu, base64, sqlite3 from ..internal import config_dir, data_dir, cache_dir, source_dir from .. import available_models_descriptions from . import dialog_widget @@ -72,6 +72,13 @@ def __init__(self, model_name:str, data:dict): ) self.data = data self.image_recognition = 'projector_info' in self.data + self.profile_picture_data = None + sqlite_con = sqlite3.connect(window.sqlite_path) + cursor = sqlite_con.cursor() + picture = cursor.execute("SELECT picture FROM model WHERE id=?", (self.get_name(),)).fetchone() + if picture: + self.profile_picture_data = picture[0] + sqlite_con.close() class model_selector_button(Gtk.MenuButton): __gtype_name__ = 'AlpacaModelSelectorButton' @@ -119,10 +126,19 @@ def remove_model(self, model_name:str): self.get_popover().model_list_box.remove(next((model for model in list(self.get_popover().model_list_box) if model.get_name() == model_name), None)) self.model_changed(self.get_popover().model_list_box) window.title_stack.set_visible_child_name('model_selector' if len(window.model_manager.get_model_list()) > 0 else 'no_models') + sqlite_con = sqlite3.connect(window.sqlite_path) + cursor = sqlite_con.cursor() + cursor.execute("DELETE FROM model WHERE id=?", (self.get_name(),)) + sqlite_con.commit() + sqlite_con.close() + window.chat_list_box.update_profile_pictures() def clear_list(self): self.get_popover().model_list_box.remove_all() + def get_model_by_name(self, model_name:str) -> object: + return next((model for model in list(self.get_popover().model_list_box) if model.get_name() == model_name), None) + class pulling_model(Gtk.ListBoxRow): __gtype_name__ = 'AlpacaPullingModel' @@ -377,23 +393,73 @@ def __init__(self, model_name:str, categories:list): name=model_name ) + def change_pfp(self, file_dialog, result, button, model): + file = file_dialog.open_finish(result) + if file: + model.profile_picture_data = window.get_content_of_file(file.get_path(), 'image') + image_data = base64.b64decode(model.profile_picture_data) + loader = GdkPixbuf.PixbufLoader.new() + loader.write(image_data) + loader.close() + pixbuf = loader.get_pixbuf() + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + image = Gtk.Image.new_from_paintable(texture) + image.set_size_request(64, 64) + button.set_overflow(1) + button.set_child(image) + sqlite_con = sqlite3.connect(window.sqlite_path) + cursor = sqlite_con.cursor() + if cursor.execute("SELECT picture FROM model WHERE id=?", (self.get_name(),)).fetchone(): + cursor.execute("UPDATE model SET picture=? WHERE id=?", (model.profile_picture_data, self.get_name())) + else: + cursor.execute("INSERT INTO model (id, picture) VALUES (?, ?)", (self.get_name(), model.profile_picture_data)) + sqlite_con.commit() + sqlite_con.close() + window.chat_list_box.update_profile_pictures() + def show_information(self, button): model = next((element for element in list(window.model_manager.model_selector.get_popover().model_list_box) if element.get_name() == self.get_name()), None) model_name = model.get_child().get_label() - - window.model_detail_page.set_title(' ('.join(model_name.split(' (')[:-1])) - window.model_detail_page.set_description(' ('.join(model_name.split(' (')[-1:])[:-1]) window.model_detail_create_button.set_name(model_name) window.model_detail_create_button.set_tooltip_text(_("Create Model Based on '{}'").format(model_name)) - details_flow_box = Gtk.FlowBox( - valign=1, - hexpand=True, - vexpand=False, - selection_mode=0, - max_children_per_line=2, - min_children_per_line=1, - ) + actionrow = Adw.ActionRow( + title="{}".format(' ('.join(model_name.split(' (')[:-1])), + subtitle=' ('.join(model_name.split(' (')[-1:])[:-1], + css_classes=["card"] + ) + pfp_button = Gtk.Button( + css_classes=['circular'], + valign=3, + icon_name='brain-augemnted-symbolic', + width_request=64, + height_request=64, + margin_top=10, + margin_bottom=10, + tooltip_text=_("Change Model Picture") + ) + if model.profile_picture_data: + image_data = base64.b64decode(model.profile_picture_data) + loader = GdkPixbuf.PixbufLoader.new() + loader.write(image_data) + loader.close() + pixbuf = loader.get_pixbuf() + texture = Gdk.Texture.new_for_pixbuf(pixbuf) + image = Gtk.Image.new_from_paintable(texture) + image.set_size_request(64, 64) + pfp_button.set_overflow(1) + pfp_button.set_child(image) + + file_filter = Gtk.FileFilter() + file_filter.add_suffix('png') + file_filter.add_suffix('jpg') + file_filter.add_suffix('jpeg') + file_filter.add_suffix('webp') + + pfp_button.connect('clicked', lambda button: Gtk.FileDialog(default_filter=file_filter).open(window, None, lambda file_dialog, result, button=button, model=model: self.change_pfp(file_dialog, result, button, model))) + + actionrow.add_prefix(pfp_button) + window.model_detail_header.set_child(actionrow) translation_strings={ 'modified_at': _('Modified At'), @@ -403,53 +469,31 @@ def show_information(self, button): 'parameter_size': _('Parameter Size'), 'quantization_level': _('Quantization Level') } - + window.model_detail_system.set_label(model.data['system'] if 'system' in model.data else '') + window.model_detail_information.remove_all() if 'modified_at' in model.data and model.data['modified_at']: - details_flow_box.append(information_bow( + window.model_detail_information.append(information_bow( title=translation_strings['modified_at'], subtitle=datetime.datetime.strptime(':'.join(model.data['modified_at'].split(':')[:2]), '%Y-%m-%dT%H:%M').strftime('%Y-%m-%d %H:%M') )) for name, value in model.data['details'].items(): if isinstance(value, str): - details_flow_box.append(information_bow( + window.model_detail_information.append(information_bow( title=translation_strings[name] if name in translation_strings else name.replace('_', ' ').title(), subtitle=value )) - - categories_box = Gtk.FlowBox( - hexpand=True, - vexpand=False, - orientation=0, - selection_mode=0, - valign=1, - halign=0 - ) + window.model_detail_categories.remove_all() languages = ['en'] if self.get_name() in available_models: languages = available_models[self.get_name()]['languages'] for category in self.categories + ['language:' + icu.Locale(lan).getDisplayLanguage(icu.Locale(lan)).title() for lan in languages]: - categories_box.append(category_pill(category, True)) + window.model_detail_categories.append(category_pill(category, True)) if 'multilingual' in self.categories and len(languages) == 1: window.model_tag_flow_box.append(category_pill('language:Others...', True)) - container_box = Gtk.Box( - orientation=1, - spacing=10, - hexpand=True, - vexpand=True, - margin_top=12, - margin_bottom=12, - margin_start=12, - margin_end=12 - ) - - container_box.append(details_flow_box) - container_box.append(categories_box) - - window.model_detail_page.set_child(container_box) window.navigation_view_manage_models.push_by_tag('model_information') class local_model_list(Gtk.ListBox): @@ -739,8 +783,8 @@ def update_local_list(self): logger.error(e) window.connection_error() window.title_stack.set_visible_child_name('model_selector' if len(window.model_manager.get_model_list()) > 0 else 'no_models') - #window.title_stack.set_visible_child_name('model_selector') - window.chat_list_box.update_welcome_screens(len(self.get_model_list()) > 0) + GLib.idle_add(window.chat_list_box.update_profile_pictures) + #GLib.idle_add(self.chat_list_box.update_welcome_screens, len(self.model_manager.get_model_list()) > 0) #Should only be called when the app starts def update_available_list(self): diff --git a/src/style.css b/src/style.css index 45542b64..aea74864 100644 --- a/src/style.css +++ b/src/style.css @@ -48,7 +48,7 @@ stacksidebar { min-height: 20px; } .message > box { - padding: 5px 5px 10px 5px; + padding: 5px 5px 5px 5px; } diff --git a/src/window.py b/src/window.py index ea7c8fba..99b24bba 100644 --- a/src/window.py +++ b/src/window.py @@ -66,6 +66,7 @@ class AlpacaWindow(Adw.ApplicationWindow): create_model_name = Gtk.Template.Child() create_model_system = Gtk.Template.Child() create_model_modelfile = Gtk.Template.Child() + create_model_modelfile_section = Gtk.Template.Child() tweaks_group = Gtk.Template.Child() preferences_dialog = Gtk.Template.Child() shortcut_window : Gtk.ShortcutsWindow = Gtk.Template.Child() @@ -104,7 +105,10 @@ class AlpacaWindow(Adw.ApplicationWindow): title_stack = Gtk.Template.Child() manage_models_dialog = Gtk.Template.Child() model_scroller = Gtk.Template.Child() - model_detail_page = Gtk.Template.Child() + model_detail_header = Gtk.Template.Child() + model_detail_information = Gtk.Template.Child() + model_detail_categories = Gtk.Template.Child() + model_detail_system = Gtk.Template.Child() model_detail_create_button = Gtk.Template.Child() ollama_information_label = Gtk.Template.Child() default_model_combo = Gtk.Template.Child() @@ -338,7 +342,7 @@ def instance_idle_timer_changed(self, spin): @Gtk.Template.Callback() def create_model_start(self, button): - name = self.create_model_name.get_text().lower().replace(":", "") + name = self.create_model_name.get_text().lower().replace(":", "").replace(" ", "-") modelfile_buffer = self.create_model_modelfile.get_buffer() modelfile_raw = modelfile_buffer.get_text(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter(), False) modelfile = ["FROM {}".format(self.create_model_base.get_subtitle()), "SYSTEM {}".format(self.create_model_system.get_text())] @@ -464,19 +468,21 @@ def create_model(self, model:str, file:bool): modelfile_buffer.delete(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter()) self.create_model_system.set_text('') if not file: - data = next((element for element in list(self.model_manager.model_selector.get_popover().model_list_box) if element.get_name() == self.convert_model_name(model, 1)), None).data + data = self.model_manager.model_selector.get_model_by_name(self.convert_model_name(model, 1)).data modelfile = [] + if 'system' in data and data['system']: + self.create_model_system.set_text(data['system']) for line in data['modelfile'].split('\n'): - if line.startswith('SYSTEM'): - self.create_model_system.set_text(line[len('SYSTEM'):].strip()) if not line.startswith('SYSTEM') and not line.startswith('FROM') and not line.startswith('#'): modelfile.append(line) self.create_model_name.set_text(self.convert_model_name(model, 1).split(':')[0] + "-custom") modelfile_buffer.insert(modelfile_buffer.get_start_iter(), '\n'.join(modelfile), len('\n'.join(modelfile).encode('utf-8'))) self.create_model_base.set_subtitle(self.convert_model_name(model, 1)) + self.create_model_modelfile_section.set_visible(False) else: self.create_model_name.set_text(os.path.splitext(os.path.basename(model))[0]) self.create_model_base.set_subtitle(model) + self.create_model_modelfile_section.set_visible(True) self.navigation_view_manage_models.push_by_tag('model_create_page') def show_toast(self, message:str, overlay): @@ -1004,7 +1010,7 @@ def prepare_alpaca(self, configuration:dict, save:bool): self.model_scroller.set_child(self.model_manager) #Chat History - GLib.idle_add(self.load_history) + self.load_history() if self.get_application().args.new_chat: self.chat_list_box.new_chat(self.get_application().args.new_chat) @@ -1101,6 +1107,12 @@ def setup_sqlite(self): name TEXT NOT NULL, content TEXT NOT NULL ) + """, + "model": """ + CREATE TABLE model ( + id TEXT NOT NULL PRIMARY KEY, + picture TEXT NOT NULL + ) """ } @@ -1214,7 +1226,7 @@ def __init__(self, **kwargs): self.file_preview_remove_button.connect('clicked', lambda button : dialog_widget.simple(_('Remove Attachment?'), _("Are you sure you want to remove attachment?"), lambda button=button: self.remove_attached_file(button.get_name()), _('Remove'), 'destructive')) self.attachment_button.connect("clicked", lambda button, file_filter=self.file_filter_attachments: dialog_widget.simple_file(file_filter, generic_actions.attach_file)) - self.create_model_name.get_delegate().connect("insert-text", lambda *_: self.check_alphanumeric(*_, ['-', '.', '_'])) + self.create_model_name.get_delegate().connect("insert-text", lambda *_: self.check_alphanumeric(*_, ['-', '.', '_', ' '])) self.set_focus(self.message_text_view) configuration = { # Defaults diff --git a/src/window.ui b/src/window.ui index 94beb7b3..0c6fe0f8 100644 --- a/src/window.ui +++ b/src/window.ui @@ -783,12 +783,43 @@ true true - - brain-augemnted-symbolic - text - + + 10 + 12 + 12 + 12 + 12 + 1 + + + + + + 2 + true + true + + + + + 1 + true + true + 0 + 2 + 1 + + + + + true + False + 0 + 0 + 1 + 0 + + @@ -859,7 +890,7 @@ - +