16 Commits
0.4.0 ... 0.5.0

Author SHA1 Message Date
jeffser
9f00890f46 Quick fix 2024-05-19 00:46:10 -06:00
jeffser
8ee265f926 Preparing for 0.5.0 2024-05-19 00:36:11 -06:00
jeffser
bc94c33807 Quick fix 2024-05-19 00:30:05 -06:00
jeffser
5ee5de4ebb Finally, support for multiple chats is here! 2024-05-19 00:17:00 -06:00
jeffser
e941648eb1 Added frame to message textview widget 2024-05-18 19:53:13 -06:00
jeffser
625bcb3f1f Fixed: code blocks shouldn't be editable 2024-05-18 19:48:43 -06:00
jeffser
2725928363 New tags for markup (bold, list, title, subtitle, monospace) 2024-05-18 19:33:22 -06:00
jeffser
b471ad70fe Initial support for Pango Markup 2024-05-18 18:10:01 -06:00
jeffser
ce18aa7de2 Added notifications if app is not active and a model pull finishes (and new symbolic icon) 2024-05-18 17:28:10 -06:00
jeffser
25a761d7d1 Added loading spinner when sending message 2024-05-18 16:46:20 -06:00
jeffser
d944af7a39 Fixed readme 2024-05-18 16:23:04 -06:00
jeffser
81555bef5b Deleted flatpak-builder folder 2024-05-18 15:54:14 -06:00
jeffser
02acbb2d70 Added support for multiple tags on a single model 2024-05-18 15:52:50 -06:00
jeffser
8ddce304b2 Quick fix 2024-05-18 11:46:27 -06:00
jeffser
d989ec5324 Added autoscroll when the user is at the bottom of the chat 2024-05-18 11:44:16 -06:00
jeffser
01ea3a0dc8 Fixed images margin 2024-05-18 11:32:44 -06:00
10 changed files with 523 additions and 255 deletions

View File

@@ -19,12 +19,15 @@ An [Ollama](https://github.com/ollama/ollama) client made with GTK4 and Adwaita.
## Features! ## Features!
- Talk to multiple models in the same conversation - Talk to multiple models in the same conversation
- Pull and delete models from the app - Pull and delete models from the app
- Image recognition
- Code highlighting
- Multiple conversations
- Notifications
## Future features! ## Future features!
- Multiple conversations - Document recognition
- Image / document recognition - Import / Export chats
- Notifications
- Code highlighting
## Screenies ## Screenies
Login to Ollama instance | Chatting with models | Managing models Login to Ollama instance | Chatting with models | Managing models

View File

@@ -78,7 +78,8 @@
"sources" : [ "sources" : [
{ {
"type" : "git", "type" : "git",
"url" : "file:///home/tentri/Documents/Alpaca" "url" : "file:///home/tentri/Documents/Alpaca",
"branch" : "main"
} }
] ]
} }

View File

@@ -52,6 +52,27 @@
<url type="homepage">https://github.com/Jeffser/Alpaca</url> <url type="homepage">https://github.com/Jeffser/Alpaca</url>
<url type="donation">https://github.com/sponsors/Jeffser</url> <url type="donation">https://github.com/sponsors/Jeffser</url>
<releases> <releases>
<release version="0.5.0" date="2024-05-19">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.5.0</url>
<description>
<p>Really Big Update</p>
<ul>
<li>Added multiple chats support!</li>
<li>Added Pango Markup support (bold, list, title, subtitle, monospace)</li>
<li>Added autoscroll if the user is at the bottom of the chat</li>
<li>Added support for multiple tags on a single model</li>
<li>Added better model management dialog</li>
<li>Added loading spinner when sending message</li>
<li>Added notifications if app is not active and a model pull finishes</li>
<li>Added new symbolic icon</li>
<li>Added frame to message textview widget</li>
<li>Fixed "code blocks shouldn't be editable"</li>
</ul>
<p>
Please report any errors to the issues page, thank you.
</p>
</description>
</release>
<release version="0.4.0" date="2024-05-17"> <release version="0.4.0" date="2024-05-17">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.4.0</url> <url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.4.0</url>
<description> <description>

View File

@@ -1 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#000" fill="#2e3436"><path d="M7.188 2.281c-.094.056-.192.125-.29.19L5.566 3.803a1.684 1.684 0 11-2.17 2.17L2.332 7.037c.506-.069 1.017-.136 1.2.026.242.214.139 1.031.155 1.656.213.088.427.171.657.219.04.008.085-.007.125 0 .337-.525.683-1.288 1-1.344.322-.057.905.562 1.406.937a3.67 3.67 0 00.656-.468c-.195-.595-.594-1.369-.437-1.657.158-.29 1.019-.37 1.625-.531.028-.183.062-.371.062-.562 0-.075-.027-.146-.031-.22-.587-.217-1.435-.385-1.562-.687-.128-.302.34-1.021.593-1.593a3.722 3.722 0 00-.593-.532zm3.875 3.25c-.165.475-.305 1.086-.47 1.563-.43.047-.84.14-1.218.312-.38-.322-.787-.773-1.156-1.093a5.562 5.562 0 00-.688.468c.177.46.453 1.001.625 1.469-.298.309-.531.67-.719 1.063-.494 0-1.102-.084-1.593-.094a5.68 5.68 0 00-.219.812c.435.24 1.006.468 1.438.72-.006.093-.032.185-.032.28 0 .333.049.66.125.97-.382.304-.898.63-1.28.937.015.044.04.083.058.127l.613.613c.417-.1.868-.223 1.266-.303.248.343.532.626.875.875-.027.135-.068.283-.104.428.174-.063.34-.155.482-.297l1.432-1.432a1.994 1.994 0 01.533-3.918c.919 0 1.684.623 1.918 1.467l1.338-1.338c.06-.06.11-.124.156-.191-.035-.062-.06-.13-.1-.188.096-.152.205-.31.315-.47.017-.348-.1-.7-.37-.971l-.177-.176c-.28.192-.561.387-.83.555-.345-.233-.746-.383-1.156-.5-.077-.507-.107-1.132-.187-1.625a5.44 5.44 0 00-.875-.063zm-9.247.608c-.087.068-.173.138-.254.205l.014.035z" style="marker:none" overflow="visible"/><path d="M8.707.293a1 1 0 00-1.415 0l-6.999 7a1 1 0 000 1.413l7 7.001a1 1 0 001.415 0l7-7a1 1 0 000-1.413zm-.708 2.121l5.587 5.587L8 13.586 2.414 7.999z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" overflow="visible"/></g></svg> <?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><g fill="#222222"><path d="m 14 3.175781 v 3.824219 c 0 2.179688 -1.820312 4 -4 4 h -3.585938 l -2 2 h 5.585938 l 3 3 v -3 c 1.644531 0 3 -1.355469 3 -3 v -4 c 0 -1.292969 -0.839844 -2.40625 -2 -2.824219 z m 0 0"/><path d="m 3 0 c -1.644531 0 -3 1.355469 -3 3 v 4 c 0 1.644531 1.355469 3 3 3 v 3 l 3 -3 h 4 c 1.644531 0 3 -1.355469 3 -3 v -4 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 0"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 522 B

View File

@@ -33,4 +33,4 @@ test('Validate schema file',
compile_schemas, compile_schemas,
args: ['--strict', '--dry-run', meson.current_source_dir()]) args: ['--strict', '--dry-run', meson.current_source_dir()])
subdir('icons') subdir('icons')

View File

@@ -1,5 +1,5 @@
project('Alpaca', project('Alpaca',
version: '0.4.0', version: '0.5.0',
meson_version: '>= 0.62.0', meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ], default_options: [ 'warning_level=2', 'werror=false', ],
) )

File diff suppressed because one or more lines are too long

View File

@@ -48,7 +48,7 @@ class AlpacaApplication(Adw.Application):
application_name='Alpaca', application_name='Alpaca',
application_icon='com.jeffser.Alpaca', application_icon='com.jeffser.Alpaca',
developer_name='Jeffry Samuel Eduarte Rojas', developer_name='Jeffry Samuel Eduarte Rojas',
version='0.4.0', version='0.5.0',
developers=['Jeffser https://jeffser.com'], developers=['Jeffser https://jeffser.com'],
designers=['Jeffser https://jeffser.com'], designers=['Jeffser https://jeffser.com'],
translator_credits='Alex K (Russian) https://github.com/alexkdeveloper', translator_credits='Alex K (Russian) https://github.com/alexkdeveloper',

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
@@ -64,12 +63,18 @@ class AlpacaWindow(Adw.ApplicationWindow):
manage_models_button = Gtk.Template.Child() manage_models_button = Gtk.Template.Child()
manage_models_dialog = Gtk.Template.Child() manage_models_dialog = Gtk.Template.Child()
model_list_box = Gtk.Template.Child() available_model_list_box = Gtk.Template.Child()
local_model_list_box = Gtk.Template.Child()
pull_model_dialog = Gtk.Template.Child() pull_model_dialog = Gtk.Template.Child()
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
toast_messages = { toast_messages = {
"error": [ "error": [
"An error occurred", "An error occurred",
@@ -77,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",
@@ -99,6 +105,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
) )
overlay.add_toast(toast) overlay.add_toast(toast)
def show_notification(self, title:str, body:str, only_when_focus:bool, icon:Gio.ThemedIcon=None):
if only_when_focus==False or self.is_active()==False:
notification = Gio.Notification.new(title)
notification.set_body(body)
if icon: notification.set_icon(icon)
self.get_application().send_notification(None, notification)
def show_message(self, msg:str, bot:bool, footer:str=None, image_base64:str=None): def show_message(self, msg:str, bot:bool, footer:str=None, image_base64:str=None):
message_text = Gtk.TextView( message_text = Gtk.TextView(
editable=False, editable=False,
@@ -133,6 +146,11 @@ class AlpacaWindow(Adw.ApplicationWindow):
image = Gtk.Image.new_from_paintable(texture) image = Gtk.Image.new_from_paintable(texture)
image.set_size_request(360, 360) image.set_size_request(360, 360)
image.set_margin_top(10)
image.set_margin_start(10)
image.set_margin_end(10)
image.set_hexpand(False)
image.set_css_classes(["flat"])
message_box.append(image) message_box.append(image)
message_box.append(message_text) message_box.append(message_text)
@@ -202,6 +220,11 @@ class AlpacaWindow(Adw.ApplicationWindow):
normal_text = text[pos:] normal_text = text[pos:]
if normal_text.strip(): if normal_text.strip():
parts.append({"type": "normal", "text": normal_text.strip()}) parts.append({"type": "normal", "text": normal_text.strip()})
bold_pattern = re.compile(r'\*\*(.*?)\*\*') #"**text**"
code_pattern = re.compile(r'`(.*?)`') #"`text`"
h1_pattern = re.compile(r'^#\s(.*)$') #"# text"
h2_pattern = re.compile(r'^##\s(.*)$') #"## text"
markup_pattern = re.compile(r'<(b|u|tt|span.*)>(.*?)<\/(b|u|tt|span)>') #heh butt span, I'm so funny
for part in parts: for part in parts:
if part['type'] == 'normal': if part['type'] == 'normal':
message_text = Gtk.TextView( message_text = Gtk.TextView(
@@ -216,13 +239,32 @@ class AlpacaWindow(Adw.ApplicationWindow):
css_classes=["flat"] css_classes=["flat"]
) )
message_buffer = message_text.get_buffer() message_buffer = message_text.get_buffer()
footer = None
if part['text'].split("\n")[-1] == parts[-1]['text'].split("\n")[-1]: if part['text'].split("\n")[-1] == parts[-1]['text'].split("\n")[-1]:
footer = "\n<small>" + part['text'].split('\n')[-1] + "</small>" footer = "\n\n<small>" + part['text'].split('\n')[-1] + "</small>"
part['text'] = '\n'.join(part['text'].split("\n")[:-1]) part['text'] = '\n'.join(part['text'].split("\n")[:-1])
message_buffer.insert(message_buffer.get_end_iter(), part['text'])
message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer)) part['text'] = part['text'].replace("\n* ", "\n")
else: #part['text'] = GLib.markup_escape_text(part['text'])
message_buffer.insert(message_buffer.get_end_iter(), part['text']) part['text'] = code_pattern.sub(r'<tt>\1</tt>', part['text'])
part['text'] = bold_pattern.sub(r'<b>\1</b>', part['text'])
part['text'] = h1_pattern.sub(r'<span size="x-large">\1</span>', part['text'])
part['text'] = h2_pattern.sub(r'<span size="large">\1</span>', part['text'])
position = 0
for match in markup_pattern.finditer(part['text']):
start, end = match.span()
if position < start:
message_buffer.insert(message_buffer.get_end_iter(), part['text'][position:start])
message_buffer.insert_markup(message_buffer.get_end_iter(), match.group(0), len(match.group(0)))
position = end
if position < len(part['text']):
message_buffer.insert(message_buffer.get_end_iter(), part['text'][position:])
if footer: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer))
self.bot_message_box.append(message_text) self.bot_message_box.append(message_text)
else: else:
language = GtkSource.LanguageManager.get_default().get_language(part['language']) language = GtkSource.LanguageManager.get_default().get_language(part['language'])
@@ -232,29 +274,33 @@ class AlpacaWindow(Adw.ApplicationWindow):
source_view = GtkSource.View( source_view = GtkSource.View(
auto_indent=True, indent_width=4, buffer=buffer, show_line_numbers=True auto_indent=True, indent_width=4, buffer=buffer, show_line_numbers=True
) )
source_view.set_editable(False)
source_view.get_style_context().add_class("card") source_view.get_style_context().add_class("card")
self.bot_message_box.append(source_view) self.bot_message_box.append(source_view)
self.bot_message = None self.bot_message = None
self.bot_message_box = None self.bot_message_box = None
def update_bot_message(self, data): def update_bot_message(self, data):
vadjustment = self.chat_window.get_vadjustment()
if vadjustment.get_value() + 50 >= vadjustment.get_upper() - vadjustment.get_page_size():
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
if data['done']: if data['done']:
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M") formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
text = f"\n<small>{data['model']}\t|\t{formated_datetime}</small>" text = f"\n<small>{data['model']}\t|\t{formated_datetime}</small>"
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))
vadjustment = self.chat_window.get_vadjustment()
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
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":
self.chats["chats"][self.current_chat_id]["messages"].append({ GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None
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)
@@ -276,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,
@@ -284,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"]]
@@ -294,21 +340,18 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.show_message(self.message_text_view.get_buffer().get_text(self.message_text_view.get_buffer().get_start_iter(), self.message_text_view.get_buffer().get_end_iter(), False), False, f"\n\n<small>{formated_datetime}</small>", self.attached_image["base64"]) self.show_message(self.message_text_view.get_buffer().get_text(self.message_text_view.get_buffer().get_start_iter(), self.message_text_view.get_buffer().get_end_iter(), False), False, f"\n\n<small>{formated_datetime}</small>", self.attached_image["base64"])
self.message_text_view.get_buffer().set_text("", 0) self.message_text_view.get_buffer().set_text("", 0)
self.show_message("", True) self.show_message("", True)
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
self.chat_container.append(self.loading_spinner)
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'])) thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model']))
thread.start() thread.start()
def delete_model(self, dialog, task, model_name, button): def delete_model(self, dialog, task, model_name):
if dialog.choose_finish(task) == "delete": if dialog.choose_finish(task) == "delete":
response = simple_delete(self.ollama_url + "/api/delete", data={"name": model_name}) response = simple_delete(self.ollama_url + "/api/delete", data={"name": model_name})
self.update_list_local_models()
self.update_list_available_models()
if response['status'] == 'ok': if response['status'] == 'ok':
button.set_icon_name("folder-download-symbolic")
button.set_css_classes(["accent", "pull"])
self.show_toast("good", 0, self.manage_models_overlay) self.show_toast("good", 0, self.manage_models_overlay)
for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == model_name:
self.model_string_list.remove(i)
self.model_drop_down.set_selected(0)
break
else: else:
self.show_toast("error", 3, self.connection_overlay) self.show_toast("error", 3, self.connection_overlay)
self.manage_models_dialog.close() self.manage_models_dialog.close()
@@ -324,27 +367,30 @@ class AlpacaWindow(Adw.ApplicationWindow):
GLib.idle_add(self.pull_model_progress_bar.set_fraction, 0.0) GLib.idle_add(self.pull_model_progress_bar.set_fraction, 0.0)
except Exception as e: print(e) except Exception as e: print(e)
def pull_model(self, dialog, task, model_name, button): def pull_model(self, dialog, task, model_name, tag):
if dialog.choose_finish(task) == "pull": if dialog.choose_finish(task) == "pull":
data = {"name":model_name} data = {"name":f"{model_name}:{tag}"}
GLib.idle_add(self.pull_model_dialog.present, self.manage_models_dialog) GLib.idle_add(self.pull_model_dialog.present, self.manage_models_dialog)
response = stream_post(f"{self.ollama_url}/api/pull", data=json.dumps(data), callback=self.pull_model_update) response = stream_post(f"{self.ollama_url}/api/pull", data=json.dumps(data), callback=self.pull_model_update)
GLib.idle_add(self.update_list_local_models)
GLib.idle_add(self.update_list_available_models)
GLib.idle_add(self.pull_model_dialog.force_close) GLib.idle_add(self.pull_model_dialog.force_close)
if response['status'] == 'ok': if response['status'] == 'ok':
GLib.idle_add(button.set_icon_name, "user-trash-symbolic") GLib.idle_add(self.show_notification, "Task Complete", f"Model '{model_name}:{tag}' pulled successfully.", True, Gio.ThemedIcon.new("emblem-ok-symbolic"))
GLib.idle_add(button.set_css_classes, ["error", "delete"])
GLib.idle_add(self.model_string_list.append, model_name)
GLib.idle_add(self.show_toast, "good", 1, self.manage_models_overlay) GLib.idle_add(self.show_toast, "good", 1, self.manage_models_overlay)
else: else:
GLib.idle_add(self.show_notification, "Pull Model Error", f"Failed to pull model '{model_name}:{tag}' due to network error.", True, Gio.ThemedIcon.new("dialog-error-symbolic"))
GLib.idle_add(self.show_toast, "error", 4, self.connection_overlay) GLib.idle_add(self.show_toast, "error", 4, self.connection_overlay)
GLib.idle_add(self.manage_models_dialog.close) GLib.idle_add(self.manage_models_dialog.close)
GLib.idle_add(self.show_connection_dialog, True) GLib.idle_add(self.show_connection_dialog, True)
def pull_model_start(self, dialog, task, model_name, button): def pull_model_start(self, dialog, task, model_name, tag_drop_down):
self.pull_model_status_page.set_description(model_name) tag = tag_drop_down.get_selected_item().get_string()
thread = threading.Thread(target=self.pull_model, args=(dialog, task, model_name, button)) self.pull_model_status_page.set_description(f"{model_name}:{tag}")
thread = threading.Thread(target=self.pull_model, args=(dialog, task, model_name, tag))
thread.start() thread.start()
def model_action_button_activate(self, button, model_name): def model_action_button_activate(self, button, model_name):
@@ -364,27 +410,81 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.delete_model(dialog, task, model_name, button) if action == "delete" else self.pull_model_start(dialog, task, model_name,button) self.delete_model(dialog, task, model_name, button) if action == "delete" else self.pull_model_start(dialog, task, model_name,button)
) )
def model_delete_button_activate(self, model_name):
dialog = Adw.AlertDialog(
heading="Delete Model",
body=f"Are you sure you want to delete '{model_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.manage_models_dialog,
cancellable = None,
callback = lambda dialog, task, model_name = model_name: self.delete_model(dialog, task, model_name)
)
def model_pull_button_activate(self, model_name):
tag_list = Gtk.StringList()
for tag in available_models[model_name]['tags']:
tag_list.append(tag)
tag_drop_down = Gtk.DropDown(
enable_search=True,
model=tag_list
)
dialog = Adw.AlertDialog(
heading="Pull Model",
body=f"Please select a tag to pull '{model_name}'",
extra_child=tag_drop_down,
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("pull", "Pull")
dialog.set_response_appearance("pull", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self.manage_models_dialog,
cancellable = None,
callback = lambda dialog, task, model_name = model_name, tag_drop_down = tag_drop_down: self.pull_model_start(dialog, task, model_name, tag_drop_down)
)
def update_list_available_models(self): def update_list_available_models(self):
self.model_list_box.remove_all() self.local_model_list_box.remove_all()
for model_name, model_description in available_models.items(): self.available_model_list_box.remove_all()
for model_name in self.local_models:
model = Adw.ActionRow( model = Adw.ActionRow(
title = model_name, title = model_name.split(":")[0],
subtitle = model_description, subtitle = model_name.split(":")[1]
) )
if ":" not in model_name: model_name += ":latest"
button = Gtk.Button( button = Gtk.Button(
icon_name = "folder-download-symbolic" if model_name not in self.local_models else "user-trash-symbolic", icon_name = "user-trash-symbolic",
vexpand = False, vexpand = False,
valign = 3, valign = 3,
css_classes = ["accent", "pull"] if model_name not in self.local_models else ["error", "delete"]) css_classes = ["error", "delete"]
button.connect("clicked", lambda button=button, model_name=model_name: self.model_action_button_activate(button, model_name)) )
button.connect("clicked", lambda button=button, model_name=model_name: self.model_delete_button_activate(model_name))
model.add_suffix(button) model.add_suffix(button)
self.model_list_box.append(model) self.local_model_list_box.append(model)
def manage_models_button_activate(self, button): for name, model_info in available_models.items():
self.manage_models_dialog.present(self) model = Adw.ActionRow(
title = name,
subtitle = model_info['description'],
)
button = Gtk.Button(
icon_name = "folder-download-symbolic",
vexpand = False,
valign = 3,
css_classes = ["accent", "pull"]
)
button.connect("clicked", lambda button=button, model_name=name: self.model_pull_button_activate(model_name))
model.add_suffix(button)
self.available_model_list_box.append(model)
def manage_models_button_activate(self, button=None):
self.update_list_local_models()
self.update_list_available_models() self.update_list_available_models()
self.manage_models_dialog.present(self)
def connection_carousel_page_changed(self, carousel, index): def connection_carousel_page_changed(self, carousel, index):
if index == 0: self.connection_previous_button.set_sensitive(False) if index == 0: self.connection_previous_button.set_sensitive(False)
@@ -414,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":
@@ -443,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": []}}, "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)
@@ -520,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):
@@ -548,14 +653,150 @@ 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()
self.load_history_into_chat()
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"]) > 0:
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"]]["messages"][-1]["model"]:
self.model_drop_down.set_selected(i)
break
def selected_model_changed(self, pspec=None, user_data=None):
self.verify_if_image_can_be_used()
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)
@@ -568,7 +809,4 @@ class AlpacaWindow(Adw.ApplicationWindow):
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)
self.update_chat_list()

View File

@@ -4,149 +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="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>
@@ -199,7 +278,7 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkBox"> <object class="GtkScrolledWindow">
<property name="hexpand">true</property> <property name="hexpand">true</property>
<property name="vexpand">true</property> <property name="vexpand">true</property>
<property name="margin-top">0</property> <property name="margin-top">0</property>
@@ -207,11 +286,26 @@
<property name="margin-start">24</property> <property name="margin-start">24</property>
<property name="margin-end">24</property> <property name="margin-end">24</property>
<child> <child>
<object class="GtkScrolledWindow"> <object class="GtkBox">
<property name="hexpand">true</property> <property name="orientation">1</property>
<property name="vexpand">true</property> <property name="spacing">12</property>
<child> <child>
<object class="GtkListBox" id="model_list_box"> <object class="GtkListBox" id="local_model_list_box">
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
<child>
<object class="GtkSeparator">
<style>
<class name="spacer"/>
</style>
</object>
</child>
<child>
<object class="GtkListBox" id="available_model_list_box">
<property name="selection-mode">none</property> <property name="selection-mode">none</property>
<style> <style>
<class name="boxed-list"/> <class name="boxed-list"/>
@@ -343,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>
@@ -356,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>