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
-
@@ -859,7 +890,7 @@
-
+