22 Commits
0.3.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
jeffser
5e7d590447 Fixed message footer 2024-05-17 01:00:32 -06:00
jeffser
ed4fbc7950 Fixed: messages didn't load if they don't have an image 2024-05-17 00:47:41 -06:00
jeffser
07994db0a5 I forgot to change something oops 2024-05-17 00:45:25 -06:00
jeffser
2290105ac1 Preparing update to 0.4.0 2024-05-17 00:39:49 -06:00
jeffser
3b695031bc Added code syntax highlighting, Image recognition (llava model), multiline prompt, and some fixes 2024-05-17 00:35:34 -06:00
jeffser
83cb2c3b90 Code blocks v1 2024-05-16 20:13:18 -06:00
10 changed files with 787 additions and 286 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

@@ -57,6 +57,20 @@
} }
] ]
}, },
{
"name": "python3-pillow",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"pillow\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/ef/43/c50c17c5f7d438e836c169e343695534c38c77f60e7c90389bd77981bc21/pillow-10.3.0.tar.gz",
"sha256": "9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"
}
]
},
{ {
"name" : "alpaca", "name" : "alpaca",
"builddir" : true, "builddir" : true,
@@ -64,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,43 @@
<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">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.4.0</url>
<description>
<p>Big Update</p>
<ul>
<li>Added code highlighting</li>
<li>Added image recognition (llava model)</li>
<li>Added multiline prompt</li>
<li>Fixed some small bugs</li>
<li>General optimization</li>
</ul>
<p>
Please report any errors to the issues page, thank you.
</p>
</description>
</release>
<release version="0.3.0" date="2024-05-16"> <release version="0.3.0" date="2024-05-16">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.3.0</url> <url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.3.0</url>
<description> <description>
@@ -69,7 +106,7 @@
</description> </description>
</release> </release>
<release version="0.2.2" date="2024-05-14"> <release version="0.2.2" date="2024-05-14">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.2.1</url> <url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.2.2</url>
<description> <description>
<p>0.2.2 Bug fixes</p> <p>0.2.2 Bug fixes</p>
<ul> <ul>

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

@@ -1,5 +1,5 @@
project('Alpaca', project('Alpaca',
version: '0.2.2', 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.2.2', 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

@@ -18,8 +18,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import gi import gi
from gi.repository import Adw, Gtk, GLib gi.require_version('GtkSource', '5')
import json, requests, threading, os gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
import json, requests, threading, os, re, base64
from io import BytesIO
from PIL import Image
from datetime import datetime from datetime import datetime
from .connection_handler import simple_get, simple_delete, stream_post, stream_post_fake from .connection_handler import simple_get, simple_delete, stream_post, stream_post_fake
from .available_models import available_models from .available_models import available_models
@@ -32,11 +36,13 @@ 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}
#Elements #Elements
bot_message : Gtk.TextBuffer = None bot_message : Gtk.TextBuffer = None
bot_message_box : Gtk.Box = None
bot_message_view : Gtk.TextView = None
connection_dialog = Gtk.Template.Child() connection_dialog = Gtk.Template.Child()
connection_carousel = Gtk.Template.Child() connection_carousel = Gtk.Template.Child()
connection_previous_button = Gtk.Template.Child() connection_previous_button = Gtk.Template.Child()
@@ -48,26 +54,36 @@ class AlpacaWindow(Adw.ApplicationWindow):
connection_overlay = Gtk.Template.Child() connection_overlay = Gtk.Template.Child()
chat_container = Gtk.Template.Child() chat_container = Gtk.Template.Child()
chat_window = Gtk.Template.Child() chat_window = Gtk.Template.Child()
message_entry = Gtk.Template.Child() message_text_view = Gtk.Template.Child()
send_button = Gtk.Template.Child() send_button = Gtk.Template.Child()
image_button = Gtk.Template.Child()
file_filter_image = Gtk.Template.Child()
model_drop_down = Gtk.Template.Child() model_drop_down = Gtk.Template.Child()
model_string_list = Gtk.Template.Child() model_string_list = Gtk.Template.Child()
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",
"Failed to connect to server", "Failed to connect to server",
"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 delete chat because it's the only one left"
], ],
"info": [ "info": [
"Please select a model before chatting", "Please select a model before chatting",
@@ -89,7 +105,14 @@ class AlpacaWindow(Adw.ApplicationWindow):
) )
overlay.add_toast(toast) overlay.add_toast(toast)
def show_message(self, msg:str, bot:bool, footer:str=None): 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):
message_text = Gtk.TextView( message_text = Gtk.TextView(
editable=False, editable=False,
focusable=False, focusable=False,
@@ -98,19 +121,57 @@ class AlpacaWindow(Adw.ApplicationWindow):
margin_bottom=12, margin_bottom=12,
margin_start=12, margin_start=12,
margin_end=12, margin_end=12,
hexpand=True,
css_classes=["flat"] css_classes=["flat"]
) )
message_buffer = message_text.get_buffer() message_buffer = message_text.get_buffer()
message_buffer.insert(message_buffer.get_end_iter(), msg) message_buffer.insert(message_buffer.get_end_iter(), msg)
if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer)) if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer))
message_box = Adw.Bin( message_box = Gtk.Box(
child=message_text, orientation=1,
css_classes=["card" if bot else None] css_classes=[None if bot else "card"]
) )
message_text.set_valign(Gtk.Align.CENTER) message_text.set_valign(Gtk.Align.CENTER)
self.chat_container.append(message_box) self.chat_container.append(message_box)
if bot: self.bot_message = message_buffer
if image_base64 is not None:
image_data = base64.b64decode(image_base64)
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(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(message_text)
if bot:
self.bot_message = message_buffer
self.bot_message_view = message_text
self.bot_message_box = message_box
def verify_if_image_can_be_used(self, pspec=None, user_data=None):
if self.model_drop_down.get_selected_item() == None: return True
selected = self.model_drop_down.get_selected_item().get_string().split(":")[0]
if selected in ['llava']:
self.image_button.set_sensitive(True)
return True
else:
self.image_button.set_sensitive(False)
self.image_button.set_css_classes([])
self.image_button.get_child().set_icon_name("image-x-generic-symbolic")
self.attached_image = {"path": None, "base64": None}
return False
def update_list_local_models(self): def update_list_local_models(self):
self.local_models = [] self.local_models = []
@@ -122,6 +183,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.model_string_list.append(model["name"]) self.model_string_list.append(model["name"])
self.local_models.append(model["name"]) self.local_models.append(model["name"])
self.model_drop_down.set_selected(0) self.model_drop_down.set_selected(0)
self.verify_if_image_can_be_used()
return return
else: else:
self.show_connection_dialog(True) self.show_connection_dialog(True)
@@ -132,78 +194,164 @@ class AlpacaWindow(Adw.ApplicationWindow):
if response['status'] == 'ok': if response['status'] == 'ok':
if "Ollama is running" in response['text']: if "Ollama is running" in response['text']:
with open(os.path.join(self.config_dir, "server.conf"), "w+") as f: f.write(self.ollama_url) with open(os.path.join(self.config_dir, "server.conf"), "w+") as f: f.write(self.ollama_url)
self.message_entry.grab_focus_without_selecting() #self.message_text_view.grab_focus_without_selecting()
self.update_list_local_models() self.update_list_local_models()
return True return True
return False return False
def add_code_blocks(self):
text = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), True)
GLib.idle_add(self.bot_message_view.get_parent().remove, self.bot_message_view)
# Define a regular expression pattern to match code blocks
code_block_pattern = re.compile(r'```(\w+)\n(.*?)\n```', re.DOTALL)
parts = []
pos = 0
for match in code_block_pattern.finditer(text):
start, end = match.span()
if pos < start:
normal_text = text[pos:start]
parts.append({"type": "normal", "text": normal_text.strip()})
language = match.group(1)
code_text = match.group(2)
parts.append({"type": "code", "text": code_text, "language": language})
pos = end
# Extract any remaining normal text after the last code block
if pos < len(text):
normal_text = text[pos:]
if 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:
if part['type'] == 'normal':
message_text = Gtk.TextView(
editable=False,
focusable=False,
wrap_mode= Gtk.WrapMode.WORD,
margin_top=12,
margin_bottom=12,
margin_start=12,
margin_end=12,
hexpand=True,
css_classes=["flat"]
)
message_buffer = message_text.get_buffer()
footer = None
if part['text'].split("\n")[-1] == parts[-1]['text'].split("\n")[-1]:
footer = "\n\n<small>" + part['text'].split('\n')[-1] + "</small>"
part['text'] = '\n'.join(part['text'].split("\n")[:-1])
part['text'] = part['text'].replace("\n* ", "\n")
#part['text'] = GLib.markup_escape_text(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)
else:
language = GtkSource.LanguageManager.get_default().get_language(part['language'])
buffer = GtkSource.Buffer.new_with_language(language)
buffer.set_text(part['text'])
buffer.set_style_scheme(GtkSource.StyleSchemeManager.get_default().get_scheme('classic-dark'))
source_view = GtkSource.View(
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")
self.bot_message_box.append(source_view)
self.bot_message = 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\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()
self.bot_message = None
else: else:
if self.bot_message is None: if self.chats["chats"][self.chats["selected_chat"]]["messages"][-1]['role'] == "user":
GLib.idle_add(self.show_message, data['message']['content'], True) GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.chats["chats"][self.current_chat_id]["messages"].append({ 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": data['message']['content'] "content": ''
}) })
else:
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 send_message(self): def run_message(self, messages, model):
current_model = self.model_drop_down.get_selected_item() response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=self.update_bot_message)
if current_model is None: GLib.idle_add(self.add_code_blocks)
GLib.idle_add(self.show_toast, "info", 0, self.main_overlay)
return
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
self.chats["chats"][self.current_chat_id]["messages"].append({
"role": "user",
"model": "User",
"date": formated_datetime,
"content": self.message_entry.get_text()
})
data = {
"model": current_model.get_string(),
"messages": self.chats["chats"][self.current_chat_id]["messages"]
}
GLib.idle_add(self.message_entry.set_sensitive, False)
GLib.idle_add(self.send_button.set_sensitive, False)
GLib.idle_add(self.show_message, self.message_entry.get_text(), False, f"\n\n<small>{formated_datetime}</small>")
self.save_history()
GLib.idle_add(self.message_entry.get_buffer().set_text, "", 0)
response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps(data), callback=self.update_bot_message)
GLib.idle_add(self.send_button.set_sensitive, True) GLib.idle_add(self.send_button.set_sensitive, True)
GLib.idle_add(self.message_entry.set_sensitive, True) GLib.idle_add(self.image_button.set_sensitive, True)
GLib.idle_add(self.image_button.set_css_classes, [])
GLib.idle_add(self.image_button.get_child().set_icon_name, "image-x-generic-symbolic")
self.attached_image = {"path": None, "base64": None}
GLib.idle_add(self.message_text_view.set_sensitive, True)
if response['status'] == 'error': if response['status'] == 'error':
GLib.idle_add(self.show_toast, 'error', 1, self.connection_overlay) GLib.idle_add(self.show_toast, 'error', 1, self.connection_overlay)
GLib.idle_add(self.show_connection_dialog, True) GLib.idle_add(self.show_connection_dialog, True)
def send_button_activate(self, button): def send_message(self, button):
if not self.message_entry.get_text(): return if not 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): return
thread = threading.Thread(target=self.send_message) current_model = self.model_drop_down.get_selected_item()
if current_model is None:
self.show_toast("info", 0, self.main_overlay)
return
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
self.chats["chats"][self.chats["selected_chat"]]["messages"].append({
"role": "user",
"model": "User",
"date": formated_datetime,
"content": 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)
})
data = {
"model": current_model.get_string(),
"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:
data["messages"][-1]["images"] = [self.attached_image["base64"]]
self.message_text_view.set_sensitive(False)
self.send_button.set_sensitive(False)
self.image_button.set_sensitive(False)
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.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.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()
@@ -219,28 +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)
print("pull fail")
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):
@@ -260,28 +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 update_list_available_models(self): def model_delete_button_activate(self, model_name):
self.model_list_box.remove_all() dialog = Adw.AlertDialog(
for model_name, model_description in available_models.items(): heading="Delete Model",
model = Adw.ActionRow( body=f"Are you sure you want to delete '{model_name}'?",
title = model_name, close_response="cancel"
subtitle = model_description, )
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):
self.local_model_list_box.remove_all()
self.available_model_list_box.remove_all()
for model_name in self.local_models:
model = Adw.ActionRow(
title = model_name.split(":")[0],
subtitle = model_name.split(":")[1]
) )
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():
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)
self.manage_models_dialog.present(self) 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)
@@ -311,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":
@@ -340,20 +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>")
else:
self.show_message(message['content'], True, f"\n\n<small>{message['model']}\t|\t{message['date']}</small>")
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)
@@ -391,12 +601,203 @@ class AlpacaWindow(Adw.ApplicationWindow):
callback = self.closing_connection_dialog_response callback = self.closing_connection_dialog_response
) )
def load_image(self, file_dialog, result):
try: file = file_dialog.open_finish(result)
except: return
try:
self.attached_image["path"] = file.get_path()
'''with open(self.attached_image["path"], "rb") as image_file:
self.attached_image["base64"] = base64.b64encode(image_file.read()).decode("utf-8")'''
with Image.open(self.attached_image["path"]) as img:
width, height = img.size
max_size = 240
if width > height:
new_width = max_size
new_height = int((max_size / width) * height)
else:
new_height = max_size
new_width = int((max_size / height) * width)
resized_img = img.resize((new_width, new_height), Image.LANCZOS)
with BytesIO() as output:
resized_img.save(output, format="JPEG")
image_data = output.getvalue()
self.attached_image["base64"] = base64.b64encode(image_data).decode("utf-8")
self.image_button.set_css_classes(["destructive-action"])
self.image_button.get_child().set_icon_name("edit-delete-symbolic")
except Exception as e:
self.show_toast("error", 5, self.main_overlay)
def remove_image(self, dialog, task):
if dialog.choose_finish(task) == 'remove':
self.image_button.set_css_classes([])
self.image_button.get_child().set_icon_name("image-x-generic-symbolic")
self.attached_image = {"path": None, "base64": None}
def open_image(self, button):
if "destructive-action" in button.get_css_classes():
dialog = Adw.AlertDialog(
heading=f"Remove Image?",
body=f"Are you sure you want to remove image?",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("remove", "Remove")
dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.choose(
parent = self,
cancellable = None,
callback = self.remove_image
)
else:
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_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()
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_button_activate) self.send_button.connect("clicked", self.send_message)
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.message_entry.set_activates_default(self.send_button) 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.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)
self.connection_next_button.connect("clicked", self.connection_next_button_activate) self.connection_next_button.connect("clicked", self.connection_next_button_activate)
@@ -408,8 +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.show_toast("funny", True, self.manage_models_overlay) self.update_chat_list()

View File

@@ -4,12 +4,89 @@
<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>
<object class="AdwOverlaySplitView" id="split_view_overlay">
<property name="show-sidebar" bind-source="show_sidebar_button" bind-property="active" bind-flags="sync-create"/>
<property name="sidebar">
<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">
<property name="spacing">12</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Chats</property>
<property name="hexpand">true</property>
<property name="halign">1</property>
<style>
<class name="title-1"/>
</style>
</object>
</child>
<child>
<object class="GtkButton" id="add_chat_button">
<property name="tooltip-text" translatable="yes">New chat</property>
<style>
<class name="flat"/>
</style>
<child>
<object class="AdwButtonContent">
<property name="icon-name">tab-new-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkListBox" id="chat_list_box">
<property name="selection-mode">single</property>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
<child> <child>
<object class="AdwToolbarView"> <object class="AdwToolbarView">
<child type="top"> <child type="top">
<object class="AdwHeaderBar" id="header_bar"> <object class="AdwHeaderBar" id="header_bar">
<child type="start">
<object class="GtkToggleButton" id="show_sidebar_button">
<property name="icon-name">sidebar-show-symbolic</property>
<property name="tooltip-text" translatable="true">Toggle Sidebar</property>
<property name="active" bind-source="split_view_overlay" bind-property="show-sidebar" bind-flags="sync-create"/>
</object>
</child>
<property name="title-widget"> <property name="title-widget">
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">0</property> <property name="orientation">0</property>
@@ -49,7 +126,6 @@
</child> </child>
<property name="content"> <property name="content">
<object class="GtkBox"><!--ACTUAL CONTENT--> <object class="GtkBox"><!--ACTUAL CONTENT-->
<property name="orientation">1</property> <property name="orientation">1</property>
<property name="margin-start">24</property> <property name="margin-start">24</property>
<property name="margin-end">24</property> <property name="margin-end">24</property>
@@ -67,8 +143,6 @@
<property name="kinetic-scrolling">1</property> <property name="kinetic-scrolling">1</property>
<property name="vexpand">true</property> <property name="vexpand">true</property>
<style> <style>
<class name="undershoot-top"/>
<class name="undershoot-bottom"/>
<class name="card"/> <class name="card"/>
</style> </style>
<child> <child>
@@ -92,10 +166,32 @@
<property name="orientation">0</property> <property name="orientation">0</property>
<property name="spacing">12</property> <property name="spacing">12</property>
<child> <child>
<object class="GtkEntry" id="message_entry"> <object class="GtkScrolledWindow">
<property name="has-frame">true</property>
<style>
<class name="card"/>
<class name="view"/>
</style>
<child>
<object class="GtkTextView" id="message_text_view">
<property name="wrap-mode">word</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> <property name="hexpand">true</property>
<style>
<class name="view"/>
</style>
</object> </object>
</child> </child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">1</property>
<property name="spacing">12</property>
<child> <child>
<object class="GtkButton" id="send_button"> <object class="GtkButton" id="send_button">
<style> <style>
@@ -109,6 +205,20 @@
</child> </child>
</object> </object>
</child> </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>
</child>
</object>
</child>
</object> </object>
</child> </child>
@@ -117,6 +227,8 @@
</object> </object>
</child> </child>
</object> </object>
</child>
</object>
</property> </property>
<object class="AdwDialog" id="pull_model_dialog"> <object class="AdwDialog" id="pull_model_dialog">
@@ -166,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>
@@ -174,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"/>
@@ -310,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>
@@ -323,4 +450,13 @@
</item> </item>
</section> </section>
</menu> </menu>
<object class="GtkFileFilter" id="file_filter_image">
<mime-types>
<mime-type>image/svg+xml</mime-type>
<mime-type>image/png</mime-type>
<mime-type>image/jpeg</mime-type>
<mime-type>image/webp</mime-type>
<mime-type>image/gif</mime-type>
</mime-types>
</object>
</interface> </interface>