Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f00890f46 | ||
|
|
8ee265f926 | ||
|
|
bc94c33807 | ||
|
|
5ee5de4ebb | ||
|
|
e941648eb1 | ||
|
|
625bcb3f1f | ||
|
|
2725928363 | ||
|
|
b471ad70fe | ||
|
|
ce18aa7de2 | ||
|
|
25a761d7d1 | ||
|
|
d944af7a39 | ||
|
|
81555bef5b | ||
|
|
02acbb2d70 | ||
|
|
8ddce304b2 | ||
|
|
d989ec5324 | ||
|
|
01ea3a0dc8 | ||
|
|
5e7d590447 | ||
|
|
ed4fbc7950 | ||
|
|
07994db0a5 | ||
|
|
2290105ac1 | ||
|
|
3b695031bc | ||
|
|
83cb2c3b90 |
11
README.md
11
README.md
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 |
@@ -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
@@ -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',
|
||||||
|
|||||||
587
src/window.py
587
src/window.py
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
156
src/window.ui
156
src/window.ui
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user