Finally, support for multiple chats is here!

This commit is contained in:
jeffser 2024-05-19 00:17:00 -06:00
parent e941648eb1
commit 5ee5de4ebb
2 changed files with 341 additions and 115 deletions

View File

@ -36,8 +36,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
ollama_url = None ollama_url = None
local_models = [] local_models = []
#In the future I will at multiple chats, for now I'll save it like this so that past chats don't break in the future #In the future I will at multiple chats, for now I'll save it like this so that past chats don't break in the future
current_chat_id="0" chats = {"chats": {"0": {"messages": []}}, "selected_chat": "0"}
chats = {"chats": {"0": {"messages": []}}}
attached_image = {"path": None, "base64": None} attached_image = {"path": None, "base64": None}
#Elements #Elements
@ -71,6 +70,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
pull_model_status_page = Gtk.Template.Child() pull_model_status_page = Gtk.Template.Child()
pull_model_progress_bar = Gtk.Template.Child() pull_model_progress_bar = Gtk.Template.Child()
chat_list_box = Gtk.Template.Child()
add_chat_button = Gtk.Template.Child()
loading_spinner = None loading_spinner = None
toast_messages = { toast_messages = {
@ -80,7 +82,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
"Could not list local models", "Could not list local models",
"Could not delete model", "Could not delete model",
"Could not pull model", "Could not pull model",
"Cannot open image" "Cannot open image",
"Cannot delete chat because it's the only one left"
], ],
"info": [ "info": [
"Please select a model before chatting", "Please select a model before chatting",
@ -287,17 +290,17 @@ class AlpacaWindow(Adw.ApplicationWindow):
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text)) GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text))
self.save_history() self.save_history()
else: else:
if self.chats["chats"][self.current_chat_id]["messages"][-1]['role'] == "user": if self.chats["chats"][self.chats["selected_chat"]]["messages"][-1]['role'] == "user":
GLib.idle_add(self.chat_container.remove, self.loading_spinner) GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None self.loading_spinner = None
self.chats["chats"][self.current_chat_id]["messages"].append({ self.chats["chats"][self.chats["selected_chat"]]["messages"].append({
"role": "assistant", "role": "assistant",
"model": data['model'], "model": data['model'],
"date": datetime.now().strftime("%Y/%m/%d %H:%M"), "date": datetime.now().strftime("%Y/%m/%d %H:%M"),
"content": '' "content": ''
}) })
GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content']) GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
self.chats["chats"][self.current_chat_id]["messages"][-1]['content'] += data['message']['content'] self.chats["chats"][self.chats["selected_chat"]]["messages"][-1]['content'] += data['message']['content']
def run_message(self, messages, model): def run_message(self, messages, model):
response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=self.update_bot_message) response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=self.update_bot_message)
@ -319,7 +322,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.show_toast("info", 0, self.main_overlay) self.show_toast("info", 0, self.main_overlay)
return return
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M") formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
self.chats["chats"][self.current_chat_id]["messages"].append({ self.chats["chats"][self.chats["selected_chat"]]["messages"].append({
"role": "user", "role": "user",
"model": "User", "model": "User",
"date": formated_datetime, "date": formated_datetime,
@ -327,7 +330,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
}) })
data = { data = {
"model": current_model.get_string(), "model": current_model.get_string(),
"messages": self.chats["chats"][self.current_chat_id]["messages"] "messages": self.chats["chats"][self.chats["selected_chat"]]["messages"]
} }
if self.verify_if_image_can_be_used() and self.attached_image["base64"] is not None: if self.verify_if_image_can_be_used() and self.attached_image["base64"] is not None:
data["messages"][-1]["images"] = [self.attached_image["base64"]] data["messages"][-1]["images"] = [self.attached_image["base64"]]
@ -511,7 +514,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
def clear_conversation(self): def clear_conversation(self):
for widget in list(self.chat_container): self.chat_container.remove(widget) for widget in list(self.chat_container): self.chat_container.remove(widget)
self.chats["chats"][self.current_chat_id]["messages"] = [] self.chats["chats"][self.chats["selected_chat"]]["messages"] = []
def clear_conversation_dialog_response(self, dialog, task): def clear_conversation_dialog_response(self, dialog, task):
if dialog.choose_finish(task) == "empty": if dialog.choose_finish(task) == "empty":
@ -540,21 +543,27 @@ class AlpacaWindow(Adw.ApplicationWindow):
with open(os.path.join(self.config_dir, "chats.json"), "w+") as f: with open(os.path.join(self.config_dir, "chats.json"), "w+") as f:
json.dump(self.chats, f, indent=4) json.dump(self.chats, f, indent=4)
def load_history_into_chat(self):
for widget in list(self.chat_container): self.chat_container.remove(widget)
for message in self.chats['chats'][self.chats["selected_chat"]]['messages']:
if message['role'] == 'user':
self.show_message(message['content'], False, f"\n\n<small>{message['date']}</small>", message['images'][0] if 'images' in message and len(message['images']) > 0 else None)
else:
self.show_message(message['content'], True, f"\n\n<small>{message['model']}\t|\t{message['date']}</small>")
self.add_code_blocks()
self.bot_message = None
def load_history(self): def load_history(self):
if os.path.exists(os.path.join(self.config_dir, "chats.json")): if os.path.exists(os.path.join(self.config_dir, "chats.json")):
self.clear_conversation() self.clear_conversation()
try: try:
with open(os.path.join(self.config_dir, "chats.json"), "r") as f: with open(os.path.join(self.config_dir, "chats.json"), "r") as f:
self.chats = json.load(f) self.chats = json.load(f)
if "selected_chat" not in self.chats or self.chats["selected_chat"] not in self.chats["chats"]: self.chats["selected_chat"] = list(self.chats["chats"].keys())[0]
if len(list(self.chats["chats"].keys())) == 0: self.chats["chats"]["New chat"] = {"messages": []}
except Exception as e: except Exception as e:
self.chats = {"chats": {"0": {"messages": []}}} self.chats = {"chats": {"New chat": {"messages": [], "current_model": None}}, "selected_chat": "New chat"}
for message in self.chats['chats'][self.current_chat_id]['messages']: self.load_history_into_chat()
if message['role'] == 'user':
self.show_message(message['content'], False, f"\n\n<small>{message['date']}</small>", message['images'][0] if 'images' in message and len(message['images']) > 0 else None)
else:
self.show_message(message['content'], True, f"\n\n<small>{message['model']}\t|\t{message['date']}</small>")
self.add_code_blocks()
self.bot_message = None
def closing_connection_dialog_response(self, dialog, task): def closing_connection_dialog_response(self, dialog, task):
result = dialog.choose_finish(task) result = dialog.choose_finish(task)
@ -617,7 +626,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.image_button.set_css_classes(["destructive-action"]) self.image_button.set_css_classes(["destructive-action"])
self.image_button.get_child().set_icon_name("edit-delete-symbolic") self.image_button.get_child().set_icon_name("edit-delete-symbolic")
except Exception as e: except Exception as e:
print(e)
self.show_toast("error", 5, self.main_overlay) self.show_toast("error", 5, self.main_overlay)
def remove_image(self, dialog, task): def remove_image(self, dialog, task):
@ -645,14 +653,154 @@ class AlpacaWindow(Adw.ApplicationWindow):
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_image) file_dialog = Gtk.FileDialog(default_filter=self.file_filter_image)
file_dialog.open(self, None, self.load_image) file_dialog.open(self, None, self.load_image)
def chat_delete(self, dialog, task, chat_name):
if dialog.choose_finish(task) == "delete":
del self.chats['chats'][chat_name]
#self.save_history()
self.update_chat_list()
def chat_delete_dialog(self, chat_name):
if len(self.chats['chats'])==1:
self.show_toast("error", 6, self.main_overlay)
return
dialog = Adw.AlertDialog(
heading=f"Delete Chat",
body=f"Are you sure you want to delete '{chat_name}'?",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("delete", "Delete")
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, chat_name=chat_name: self.chat_delete(dialog, task, chat_name)
)
def chat_rename(self, dialog=None, task=None, old_chat_name:str="", entry=None):
if not entry: return
new_chat_name = entry.get_text()
if old_chat_name == new_chat_name: return
if new_chat_name and (not task or dialog.choose_finish(task) == "rename"):
dialog.force_close()
if new_chat_name in self.chats["chats"]: self.chat_rename_dialog(old_chat_name, f"The name '{new_chat_name}' is already in use", True)
else:
self.chats["chats"][new_chat_name] = self.chats["chats"][old_chat_name]
del self.chats["chats"][old_chat_name]
self.save_history()
self.update_chat_list()
def chat_rename_dialog(self, chat_name:str, body:str, error:bool=False):
entry = Gtk.Entry(
css_classes = ["error"] if error else None
)
dialog = Adw.AlertDialog(
heading=f"Rename Chat",
body=body,
extra_child=entry,
close_response="cancel"
)
entry.connect("activate", lambda entry, dialog=dialog, old_chat_name=chat_name: self.chat_rename(dialog=dialog, old_chat_name=old_chat_name, entry=entry))
dialog.add_response("cancel", "Cancel")
dialog.add_response("rename", "Rename")
dialog.set_response_appearance("rename", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, old_chat_name=chat_name, entry=entry: self.chat_rename(dialog=dialog, task=task, old_chat_name=old_chat_name, entry=entry)
)
def chat_new(self, dialog=None, task=None, entry=None):
if not entry: return
chat_name = entry.get_text()
if chat_name and (not task or dialog.choose_finish(task) == "create"):
dialog.force_close()
if chat_name in self.chats["chats"]: self.chat_new_dialog(f"The name '{chat_name}' is already in use", True)
else:
self.chats["chats"][chat_name] = {"messages": []}
self.chats["selected_chat"] = chat_name
self.save_history()
self.update_chat_list()
self.load_history_into_chat()
def chat_new_dialog(self, body:str, error:bool=False):
entry = Gtk.Entry(
css_classes = ["error"] if error else None
)
dialog = Adw.AlertDialog(
heading=f"Create Chat",
body=body,
extra_child=entry,
close_response="cancel"
)
entry.connect("activate", lambda entry, dialog=dialog: self.chat_new(dialog=dialog, entry=entry))
dialog.add_response("cancel", "Cancel")
dialog.add_response("create", "Create")
dialog.set_response_appearance("rename", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, entry=entry: self.chat_new(dialog=dialog, task=task, entry=entry)
)
def update_chat_list(self):
self.chat_list_box.remove_all()
for name, content in self.chats['chats'].items():
chat = Adw.ActionRow(
title = name,
margin_top = 6,
margin_start = 6,
margin_end = 6,
css_classes = ["card"]
)
button_delete = Gtk.Button(
icon_name = "edit-delete-symbolic",
vexpand = False,
valign = 3,
css_classes = ["error", "flat"]
)
button_delete.connect("clicked", lambda button, chat_name=name: self.chat_delete_dialog(chat_name=chat_name))
button_rename = Gtk.Button(
icon_name = "document-edit-symbolic",
vexpand = False,
valign = 3,
css_classes = ["accent", "flat"]
)
button_rename.connect("clicked", lambda button, chat_name=name: self.chat_rename_dialog(chat_name=chat_name, body=f"Renaming '{chat_name}'", error=False))
chat.add_suffix(button_delete)
chat.add_suffix(button_rename)
self.chat_list_box.append(chat)
if name==self.chats["selected_chat"]: self.chat_list_box.select_row(chat)
def chat_changed(self, listbox, row):
if row and row.get_title() != self.chats["selected_chat"]:
self.chats["selected_chat"] = row.get_title()
if "current_model" not in self.chats["chats"][self.chats["selected_chat"]] or self.chats["chats"][self.chats["selected_chat"]]["current_model"] not in self.local_models:
self.chats["chats"][self.chats["selected_chat"]]["current_model"] = self.model_drop_down.get_selected_item().get_string()
self.save_history()
else:
for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == self.chats["chats"][self.chats["selected_chat"]]["current_model"]:
self.model_drop_down.set_selected(i)
break
self.load_history_into_chat()
def selected_model_changed(self, pspec=None, user_data=None):
self.verify_if_image_can_be_used()
self.chats["chats"][self.chats["selected_chat"]]["current_model"] = self.model_drop_down.get_selected_item().get_string()
self.save_history()
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
GtkSource.init() GtkSource.init()
self.manage_models_button.connect("clicked", self.manage_models_button_activate) self.manage_models_button.connect("clicked", self.manage_models_button_activate)
self.send_button.connect("clicked", self.send_message) self.send_button.connect("clicked", self.send_message)
self.image_button.connect("clicked", self.open_image) self.image_button.connect("clicked", self.open_image)
self.add_chat_button.connect("clicked", lambda button : self.chat_new_dialog("Enter name for new chat", False))
self.set_default_widget(self.send_button) self.set_default_widget(self.send_button)
self.model_drop_down.connect("notify", self.verify_if_image_can_be_used) self.model_drop_down.connect("notify", self.selected_model_changed)
self.chat_list_box.connect("row-selected", self.chat_changed)
#self.message_text_view.set_activates_default(self.send_button) #self.message_text_view.set_activates_default(self.send_button)
self.connection_carousel.connect("page-changed", self.connection_carousel_page_changed) self.connection_carousel.connect("page-changed", self.connection_carousel_page_changed)
self.connection_previous_button.connect("clicked", self.connection_previous_button_activate) self.connection_previous_button.connect("clicked", self.connection_previous_button_activate)
@ -660,12 +808,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.connection_url_entry.connect("changed", lambda entry: entry.set_css_classes([])) self.connection_url_entry.connect("changed", lambda entry: entry.set_css_classes([]))
self.connection_dialog.connect("close-attempt", self.closing_connection_dialog) self.connection_dialog.connect("close-attempt", self.closing_connection_dialog)
self.load_history() self.load_history()
self.update_chat_list()
if os.path.exists(os.path.join(self.config_dir, "server.conf")): if os.path.exists(os.path.join(self.config_dir, "server.conf")):
with open(os.path.join(self.config_dir, "server.conf"), "r") as f: with open(os.path.join(self.config_dir, "server.conf"), "r") as f:
self.ollama_url = f.read() self.ollama_url = f.read()
if self.verify_connection() is False: self.show_connection_dialog(True) if self.verify_connection() is False: self.show_connection_dialog(True)
else: self.connection_dialog.present(self) else: self.connection_dialog.present(self)

View File

@ -4,150 +4,228 @@
<requires lib="Adw" version="1.0"/> <requires lib="Adw" version="1.0"/>
<template class="AlpacaWindow" parent="AdwApplicationWindow"> <template class="AlpacaWindow" parent="AdwApplicationWindow">
<property name="resizable">True</property> <property name="resizable">True</property>
<property name="width-request">600</property>
<property name="height-request">800</property>
<property name="default-width">1300</property>
<property name="default-height">800</property>
<!--
<property name="min-content-width">500</property>
<property name="min-content-height">600</property>
-->
<child>
<object class="AdwBreakpoint">
<condition>max-width: 800sp</condition>
<setter object="split_view_overlay" property="collapsed">true</setter>
</object>
</child>
<property name="content"> <property name="content">
<object class="AdwToastOverlay" id="main_overlay"> <object class="AdwToastOverlay" id="main_overlay">
<child> <child>
<object class="AdwToolbarView"> <object class="AdwOverlaySplitView" id="split_view_overlay">
<child type="top"> <property name="show-sidebar" bind-source="show_sidebar_button" bind-property="active" bind-flags="sync-create"/>
<object class="AdwHeaderBar" id="header_bar"> <property name="sidebar">
<property name="title-widget"> <object class="GtkBox">
<property name="spacing">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="orientation">1</property>
<child>
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">0</property>
<property name="spacing">12</property> <property name="spacing">12</property>
<child> <child>
<object class="GtkDropDown" id="model_drop_down"> <object class="GtkLabel">
<property name="enable-search">true</property> <property name="label" translatable="yes">Chats</property>
<property name="model"> <property name="hexpand">true</property>
<object class="GtkStringList" id="model_string_list"> <property name="halign">1</property>
<items> <style>
</items> <class name="title-1"/>
</object> </style>
</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkButton" id="manage_models_button"> <object class="GtkButton" id="add_chat_button">
<property name="tooltip-text" translatable="yes">Manage models</property> <property name="tooltip-text" translatable="yes">New chat</property>
<style>
<class name="flat"/>
</style>
<child> <child>
<object class="AdwButtonContent"> <object class="AdwButtonContent">
<property name="icon-name">package-x-generic-symbolic</property> <property name="icon-name">tab-new-symbolic</property>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
</object> </object>
</property> </child>
<child type="end"> <child>
<object class="GtkMenuButton"> <object class="GtkScrolledWindow">
<property name="primary">True</property> <property name="vexpand">true</property>
<property name="icon-name">open-menu-symbolic</property> <property name="hexpand">true</property>
<property name="tooltip-text" translatable="yes">Menu</property> <child>
<property name="menu-model">primary_menu</property> <object class="GtkListBox" id="chat_list_box">
<property name="selection-mode">single</property>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
</child> </property>
<property name="content"> <child>
<object class="GtkBox"><!--ACTUAL CONTENT--> <object class="AdwToolbarView">
<child type="top">
<property name="orientation">1</property> <object class="AdwHeaderBar" id="header_bar">
<property name="margin-start">24</property> <child type="start">
<property name="margin-end">24</property> <object class="GtkToggleButton" id="show_sidebar_button">
<property name="margin-bottom">24</property> <property name="icon-name">sidebar-show-symbolic</property>
<property name="vexpand">true</property> <property name="tooltip-text" translatable="true">Toggle Sidebar</property>
<property name="hexpand">true</property> <property name="active" bind-source="split_view_overlay" bind-property="show-sidebar" bind-flags="sync-create"/>
</object>
<child> </child>
<object class="GtkScrolledWindow" id="chat_window"> <property name="title-widget">
<property name="margin-bottom">12</property> <object class="GtkBox">
<property name="has-frame">true</property> <property name="orientation">0</property>
<property name="propagate-natural-height">true</property>
<property name="min-content-width">500</property>
<property name="min-content-height">600</property>
<property name="kinetic-scrolling">1</property>
<property name="vexpand">true</property>
<style>
<class name="card"/>
</style>
<child>
<object class="GtkBox" id="chat_container">
<property name="orientation">1</property>
<property name="homogeneous">false</property>
<property name="hexpand">false</property>
<property name="vexpand">true</property>
<property name="spacing">12</property> <property name="spacing">12</property>
<property name="margin-top">12</property> <child>
<property name="margin-bottom">12</property> <object class="GtkDropDown" id="model_drop_down">
<property name="margin-start">12</property> <property name="enable-search">true</property>
<property name="margin-end">12</property> <property name="model">
<object class="GtkStringList" id="model_string_list">
<items>
</items>
</object>
</property>
</object>
</child>
<child>
<object class="GtkButton" id="manage_models_button">
<property name="tooltip-text" translatable="yes">Manage models</property>
<child>
<object class="AdwButtonContent">
<property name="icon-name">package-x-generic-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</property>
<child type="end">
<object class="GtkMenuButton">
<property name="primary">True</property>
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">Menu</property>
<property name="menu-model">primary_menu</property>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
<property name="content">
<object class="GtkBox"><!--ACTUAL CONTENT-->
<property name="orientation">1</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="margin-bottom">24</property>
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkBox">
<property name="orientation">0</property>
<property name="spacing">12</property>
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow" id="chat_window">
<property name="margin-bottom">12</property>
<property name="has-frame">true</property> <property name="has-frame">true</property>
<property name="propagate-natural-height">true</property>
<property name="min-content-width">500</property>
<property name="min-content-height">600</property>
<property name="kinetic-scrolling">1</property>
<property name="vexpand">true</property>
<style> <style>
<class name="card"/> <class name="card"/>
<class name="view"/>
</style> </style>
<child> <child>
<object class="GtkTextView" id="message_text_view"> <object class="GtkBox" id="chat_container">
<property name="wrap-mode">word</property> <property name="orientation">1</property>
<property name="margin-top">6</property> <property name="homogeneous">false</property>
<property name="margin-bottom">6</property> <property name="hexpand">false</property>
<property name="margin-start">6</property> <property name="vexpand">true</property>
<property name="margin-end">6</property> <property name="spacing">12</property>
<property name="hexpand">true</property> <property name="margin-top">12</property>
<style> <property name="margin-bottom">12</property>
<class name="view"/> <property name="margin-start">12</property>
</style> <property name="margin-end">12</property>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">1</property> <property name="orientation">0</property>
<property name="spacing">12</property> <property name="spacing">12</property>
<child> <child>
<object class="GtkButton" id="send_button"> <object class="GtkScrolledWindow">
<property name="has-frame">true</property>
<style> <style>
<class name="suggested-action"/> <class name="card"/>
<class name="view"/>
</style> </style>
<child> <child>
<object class="AdwButtonContent"> <object class="GtkTextView" id="message_text_view">
<property name="label" translatable="true">Send</property> <property name="wrap-mode">word</property>
<property name="icon-name">send-to-symbolic</property> <property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="hexpand">true</property>
<style>
<class name="view"/>
</style>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkButton" id="image_button"> <object class="GtkBox">
<property name="sensitive">false</property> <property name="orientation">1</property>
<property name="tooltip-text" translatable="true">Requires model 'llava' to be selected</property> <property name="spacing">12</property>
<child> <child>
<object class="AdwButtonContent"> <object class="GtkButton" id="send_button">
<property name="label" translatable="true">Image</property> <style>
<property name="icon-name">image-x-generic-symbolic</property> <class name="suggested-action"/>
</style>
<child>
<object class="AdwButtonContent">
<property name="label" translatable="true">Send</property>
<property name="icon-name">send-to-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="image_button">
<property name="sensitive">false</property>
<property name="tooltip-text" translatable="true">Requires model 'llava' to be selected</property>
<child>
<object class="AdwButtonContent">
<property name="label" translatable="true">Image</property>
<property name="icon-name">image-x-generic-symbolic</property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
</object>
</child>
</object><!--END OF CONTENT--> </object><!--END OF CONTENT-->
</property> </property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
@ -220,7 +298,11 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkSeparator"></object> <object class="GtkSeparator">
<style>
<class name="spacer"/>
</style>
</object>
</child> </child>
<child> <child>
<object class="GtkListBox" id="available_model_list_box"> <object class="GtkListBox" id="available_model_list_box">
@ -355,7 +437,7 @@
<menu id="primary_menu"> <menu id="primary_menu">
<section> <section>
<item> <item>
<attribute name="label" translatable="yes">_Clear Conversation</attribute> <attribute name="label" translatable="yes">_Clear Chat</attribute>
<attribute name="action">app.clear</attribute> <attribute name="action">app.clear</attribute>
</item> </item>
<item> <item>
@ -368,7 +450,6 @@
</item> </item>
</section> </section>
</menu> </menu>
<object class="GtkFileFilter" id="file_filter_image"> <object class="GtkFileFilter" id="file_filter_image">
<mime-types> <mime-types>
<mime-type>image/svg+xml</mime-type> <mime-type>image/svg+xml</mime-type>