34 Commits
0.2.2 ... 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
jeffser
e4360925b6 Added version notes 2024-05-16 15:15:42 -06:00
jeffser
425e1b0211 Only show (Save changes?) dialog when you change the url 2024-05-16 13:58:13 -06:00
jeffser
529687ffdb Removed Soup requirement since I'm now using requests 2024-05-16 11:54:59 -06:00
jeffser
34e3511d62 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-05-16 11:42:48 -06:00
jeffser
70e2c81eff Added credit to Alex K (Russian translation) 2024-05-16 11:42:42 -06:00
Alex K
d8ba1f5696 Add Russian translation (#1)
* Add files via upload

* Update ru.po
2024-05-16 11:37:35 -06:00
jeffser
b21f7490ec Fixed description 2024-05-16 10:01:08 -06:00
jeffser
33eed32a15 Changed brand colors 2024-05-16 09:58:57 -06:00
jeffser
b9887d9286 Fixed 'cannot close app on first setup' 2024-05-16 09:50:44 -06:00
jeffser
d1fbdad486 Fixed 'cannot close app on first setup' 2024-05-16 09:50:35 -06:00
Jeffry Samuel
28d0860522 Update README.md 2024-05-15 23:05:06 -06:00
Jeffry Samuel
a4981b8e9c Update README.md 2024-05-15 23:03:14 -06:00
12 changed files with 940 additions and 305 deletions

View File

@@ -19,26 +19,29 @@ An [Ollama](https://github.com/ollama/ollama) client made with GTK4 and Adwaita.
## Features!
- Talk to multiple models in the same conversation
- Pull and delete models from the app
- Image recognition
- Code highlighting
- Multiple conversations
- Notifications
## Future features!
- Persistent conversations
- Multiple conversations
- Image / document recognition
- Document recognition
- Import / Export chats
## Screenies
Login to Ollama instance | Chatting with models | Managing models
:-------------------------:|:-------------------------:|:-------------------------:
![Screenshot from 2024-05-12 19-58-28](https://github.com/Jeffser/Alpaca/assets/69224322/e28df5c9-6419-4800-bbbc-38821f096922) | ![Screenshot from 2024-05-12 20-01-08](https://github.com/Jeffser/Alpaca/assets/69224322/c4083864-8c39-40e6-83b6-aff9d62183ca) | ![Screenshot from 2024-05-12 20-01-31](https://github.com/Jeffser/Alpaca/assets/69224322/76deb8a2-13a5-480a-b99d-4de40159c229)
![Screenshot from 2024-05-12 19-58-28](https://jeffser.com/images/alpaca/screenie1.png) | ![Screenshot from 2024-05-12 20-01-08](https://jeffser.com/images/alpaca/screenie2.png) | ![Screenshot from 2024-05-12 20-01-31](https://jeffser.com/images/alpaca/screenie3.png)
## Preview
1. Clone repo using Gnome Builder
2. Press the `run` button
## Instalation
1. Clone repo using Gnome Builder
2. Build the app using the `build` button
3. Prepare the file using the `install` button (it doesn't actually install it, idk)
4. Then press the `export` button, it will export a `com.jeffser.Alpaca.flatpak` file, you can install it just by opening it
1. Go to the `releases` page
2. Download the latest flatpak package
3. Open it
## Usage
- You'll need an Ollama instance, I recommend using the [Docker image](https://ollama.com/blog/ollama-is-now-available-as-an-official-docker-image)

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",
"builddir" : true,
@@ -64,8 +78,8 @@
"sources" : [
{
"type" : "git",
"url" : "https://github.com/Jeffser/Alpaca.git",
"tag": "0.2.2"
"url" : "file:///home/tentri/Documents/Alpaca",
"branch" : "main"
}
]
}

View File

@@ -7,7 +7,8 @@
<name>Alpaca</name>
<summary>An Ollama client</summary>
<description>
<p>Made with GTK4 and Adwaita.</p>
<p>Chat with multiple AI models</p>
<p>An Ollama client</p>
<p>Features</p>
<ul>
<li>Talk to multiple models in the same conversation</li>
@@ -29,8 +30,8 @@
<category>Chat</category>
</categories>
<branding>
<color type="primary" scheme_preference="light">#ff00ff</color>
<color type="primary" scheme_preference="dark">#993d3d</color>
<color type="primary" scheme_preference="light">#8cdef5</color>
<color type="primary" scheme_preference="dark">#0f2b78</color>
</branding>
<screenshots>
<screenshot type="default">
@@ -51,8 +52,61 @@
<url type="homepage">https://github.com/Jeffser/Alpaca</url>
<url type="donation">https://github.com/sponsors/Jeffser</url>
<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">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.3.0</url>
<description>
<p>Fixes and features</p>
<ul>
<li>Russian translation (thanks github/alexkdeveloper)</li>
<li>Fixed: Cannot close app on first setup</li>
<li>Fixed: Brand colors for Flathub</li>
<li>Fixed: App description</li>
<li>Fixed: Only show 'save changes dialog' when you actually change the url</li>
</ul>
<p>
Please report any errors to the issues page, thank you.
</p>
</description>
</release>
<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>
<p>0.2.2 Bug fixes</p>
<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

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

View File

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

View File

@@ -0,0 +1 @@
ru

112
po/ru.po Normal file
View File

@@ -0,0 +1,112 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: 2024-05-16 19:29+0800\n"
"PO-Revision-Date: 2024-05-16 19:59+0800\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.4.2\n"
"X-Poedit-Basepath: ../src\n"
"X-Poedit-SearchPath-0: .\n"
#: gtk/help-overlay.ui:11
msgctxt "shortcut window"
msgid "General"
msgstr "Общие"
#: gtk/help-overlay.ui:14
msgctxt "shortcut window"
msgid "Show Shortcuts"
msgstr "Показывать ярлыки"
#: gtk/help-overlay.ui:20
msgctxt "shortcut window"
msgid "Quit"
msgstr "Выйти"
#: window.ui:30
msgid "Manage models"
msgstr "Управление моделями"
#: window.ui:44
msgid "Menu"
msgstr "Меню"
#: window.ui:106
msgid "Send"
msgstr "Отправить"
#: window.ui:137
msgid "Pulling Model"
msgstr "Тянущая модель"
#: window.ui:218
msgid "Previous"
msgstr "Предыдущий"
#: window.ui:233
msgid "Next"
msgstr "Следующий"
#: window.ui:259
msgid "Welcome to Alpaca"
msgstr "Добро пожаловать в Alpaca"
#: window.ui:260
msgid ""
"To get started, please ensure you have an Ollama instance set up. You can "
"either run Ollama locally on your machine or connect to a remote instance."
msgstr ""
"Для начала, пожалуйста, убедитесь, что у вас настроен экземпляр Ollama. Вы "
"можете либо запустить Ollama локально на своем компьютере, либо "
"подключиться к удаленному экземпляру."
#: window.ui:263
msgid "Ollama Website"
msgstr "Веб-сайт Ollama"
#: window.ui:279
msgid "Disclaimer"
msgstr "Отказ от ответственности"
#: window.ui:280
msgid ""
"Alpaca and its developers are not liable for any damages to devices or "
"software resulting from the execution of code generated by an AI model. "
"Please exercise caution and review the code carefully before running it."
msgstr ""
"Alpaca и ее разработчики не несут ответственности за любой ущерб, "
"причиненный устройствам или программному обеспечению в результате "
"выполнения кода, сгенерированного с помощью модели искусственного "
"интеллекта. Пожалуйста, будьте осторожны и внимательно ознакомьтесь с кодом "
"перед его запуском."
#: window.ui:292
msgid "Setup"
msgstr "Установка"
#: window.ui:293
msgid ""
"If you are running an Ollama instance locally and haven't modified the "
"default ports, you can use the default URL. Otherwise, please enter the URL "
"of your Ollama instance."
msgstr ""
"Если вы запускаете локальный экземпляр Ollama и не изменили порты по "
"умолчанию, вы можете использовать URL-адрес по умолчанию. В противном "
"случае, пожалуйста, введите URL-адрес вашего экземпляра Ollama."
#: window.ui:313
msgid "_Clear Conversation"
msgstr "_Очистить разговор"
#: window.ui:317
msgid "_Change Server"
msgstr "_Изменить Сервер"
#: window.ui:321
msgid "_About Alpaca"
msgstr "_О Программе"

File diff suppressed because one or more lines are too long

View File

@@ -48,9 +48,10 @@ class AlpacaApplication(Adw.Application):
application_name='Alpaca',
application_icon='com.jeffser.Alpaca',
developer_name='Jeffry Samuel Eduarte Rojas',
version='0.2.2',
version='0.5.0',
developers=['Jeffser https://jeffser.com'],
designers=['Jeffser https://jeffser.com'],
translator_credits='Alex K (Russian) https://github.com/alexkdeveloper',
copyright='© 2024 Jeffser',
issue_url='https://github.com/Jeffser/Alpaca/issues')
about.present()

View File

@@ -18,9 +18,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import gi
gi.require_version("Soup", "3.0")
from gi.repository import Adw, Gtk, GLib
import json, requests, threading, os
gi.require_version('GtkSource', '5')
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 .connection_handler import simple_get, simple_delete, stream_post, stream_post_fake
from .available_models import available_models
@@ -33,11 +36,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
ollama_url = None
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
current_chat_id="0"
chats = {"chats": {"0": {"messages": []}}}
chats = {"chats": {"0": {"messages": []}}, "selected_chat": "0"}
attached_image = {"path": None, "base64": None}
#Elements
bot_message : Gtk.TextBuffer = None
bot_message_box : Gtk.Box = None
bot_message_view : Gtk.TextView = None
connection_dialog = Gtk.Template.Child()
connection_carousel = Gtk.Template.Child()
connection_previous_button = Gtk.Template.Child()
@@ -49,26 +54,36 @@ class AlpacaWindow(Adw.ApplicationWindow):
connection_overlay = Gtk.Template.Child()
chat_container = Gtk.Template.Child()
chat_window = Gtk.Template.Child()
message_entry = Gtk.Template.Child()
message_text_view = 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_string_list = Gtk.Template.Child()
manage_models_button = 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_status_page = 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 = {
"error": [
"An error occurred",
"Failed to connect to server",
"Could not list local models",
"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": [
"Please select a model before chatting",
@@ -90,7 +105,14 @@ class AlpacaWindow(Adw.ApplicationWindow):
)
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(
editable=False,
focusable=False,
@@ -99,19 +121,57 @@ class AlpacaWindow(Adw.ApplicationWindow):
margin_bottom=12,
margin_start=12,
margin_end=12,
hexpand=True,
css_classes=["flat"]
)
message_buffer = message_text.get_buffer()
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))
message_box = Adw.Bin(
child=message_text,
css_classes=["card" if bot else None]
message_box = Gtk.Box(
orientation=1,
css_classes=[None if bot else "card"]
)
message_text.set_valign(Gtk.Align.CENTER)
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):
self.local_models = []
@@ -123,6 +183,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.model_string_list.append(model["name"])
self.local_models.append(model["name"])
self.model_drop_down.set_selected(0)
self.verify_if_image_can_be_used()
return
else:
self.show_connection_dialog(True)
@@ -133,78 +194,164 @@ class AlpacaWindow(Adw.ApplicationWindow):
if response['status'] == 'ok':
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)
self.message_entry.grab_focus_without_selecting()
#self.message_text_view.grab_focus_without_selecting()
self.update_list_local_models()
return True
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):
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']:
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))
vadjustment = self.chat_window.get_vadjustment()
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
self.save_history()
self.bot_message = None
else:
if self.bot_message is None:
GLib.idle_add(self.show_message, data['message']['content'], True)
self.chats["chats"][self.current_chat_id]["messages"].append({
if self.chats["chats"][self.chats["selected_chat"]]["messages"][-1]['role'] == "user":
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None
self.chats["chats"][self.chats["selected_chat"]]["messages"].append({
"role": "assistant",
"model": data['model'],
"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'])
self.chats["chats"][self.current_chat_id]["messages"][-1]['content'] += data['message']['content']
GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
self.chats["chats"][self.chats["selected_chat"]]["messages"][-1]['content'] += data['message']['content']
def send_message(self):
current_model = self.model_drop_down.get_selected_item()
if current_model is None:
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)
def run_message(self, messages, model):
response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=self.update_bot_message)
GLib.idle_add(self.add_code_blocks)
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':
GLib.idle_add(self.show_toast, 'error', 1, self.connection_overlay)
GLib.idle_add(self.show_connection_dialog, True)
def send_button_activate(self, button):
if not self.message_entry.get_text(): return
thread = threading.Thread(target=self.send_message)
def send_message(self, button):
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
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()
def delete_model(self, dialog, task, model_name, button):
def delete_model(self, dialog, task, model_name):
if dialog.choose_finish(task) == "delete":
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':
button.set_icon_name("folder-download-symbolic")
button.set_css_classes(["accent", "pull"])
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:
self.show_toast("error", 3, self.connection_overlay)
self.manage_models_dialog.close()
@@ -220,28 +367,30 @@ class AlpacaWindow(Adw.ApplicationWindow):
GLib.idle_add(self.pull_model_progress_bar.set_fraction, 0.0)
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":
data = {"name":model_name}
data = {"name":f"{model_name}:{tag}"}
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)
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)
if response['status'] == 'ok':
GLib.idle_add(button.set_icon_name, "user-trash-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_notification, "Task Complete", f"Model '{model_name}:{tag}' pulled successfully.", True, Gio.ThemedIcon.new("emblem-ok-symbolic"))
GLib.idle_add(self.show_toast, "good", 1, self.manage_models_overlay)
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.manage_models_dialog.close)
GLib.idle_add(self.show_connection_dialog, True)
print("pull fail")
def pull_model_start(self, dialog, task, model_name, button):
self.pull_model_status_page.set_description(model_name)
thread = threading.Thread(target=self.pull_model, args=(dialog, task, model_name, button))
def pull_model_start(self, dialog, task, model_name, tag_drop_down):
tag = tag_drop_down.get_selected_item().get_string()
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()
def model_action_button_activate(self, button, model_name):
@@ -261,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)
)
def model_delete_button_activate(self, model_name):
dialog = Adw.AlertDialog(
heading="Delete Model",
body=f"Are you sure you want to delete '{model_name}'?",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("delete", "Delete")
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.choose(
parent = self.manage_models_dialog,
cancellable = None,
callback = lambda dialog, task, model_name = model_name: self.delete_model(dialog, task, model_name)
)
def model_pull_button_activate(self, model_name):
tag_list = Gtk.StringList()
for tag in available_models[model_name]['tags']:
tag_list.append(tag)
tag_drop_down = Gtk.DropDown(
enable_search=True,
model=tag_list
)
dialog = Adw.AlertDialog(
heading="Pull Model",
body=f"Please select a tag to pull '{model_name}'",
extra_child=tag_drop_down,
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("pull", "Pull")
dialog.set_response_appearance("pull", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self.manage_models_dialog,
cancellable = None,
callback = lambda dialog, task, model_name = model_name, tag_drop_down = tag_drop_down: self.pull_model_start(dialog, task, model_name, tag_drop_down)
)
def update_list_available_models(self):
self.model_list_box.remove_all()
for model_name, model_description in available_models.items():
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,
subtitle = model_description,
title = model_name.split(":")[0],
subtitle = model_name.split(":")[1]
)
model_name += ":latest"
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,
valign = 3,
css_classes = ["accent", "pull"] if model_name not in self.local_models else ["error", "delete"])
button.connect("clicked", lambda button=button, model_name=model_name: self.model_action_button_activate(button, model_name))
css_classes = ["error", "delete"]
)
button.connect("clicked", lambda button=button, model_name=model_name: self.model_delete_button_activate(model_name))
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.manage_models_dialog.present(self)
def connection_carousel_page_changed(self, carousel, index):
if index == 0: self.connection_previous_button.set_sensitive(False)
@@ -312,7 +514,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
def clear_conversation(self):
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):
if dialog.choose_finish(task) == "empty":
@@ -341,26 +543,34 @@ class AlpacaWindow(Adw.ApplicationWindow):
with open(os.path.join(self.config_dir, "chats.json"), "w+") as f:
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):
if os.path.exists(os.path.join(self.config_dir, "chats.json")):
self.clear_conversation()
try:
with open(os.path.join(self.config_dir, "chats.json"), "r") as 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:
self.chats = {"chats": {"0": {"messages": []}}}
for message in self.chats['chats'][self.current_chat_id]['messages']:
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
self.chats = {"chats": {"New chat": {"messages": []}}, "selected_chat": "New chat"}
self.load_history_into_chat()
def closing_connection_dialog_response(self, dialog, task):
result = dialog.choose_finish(task)
if result == "cancel": return
if result == "save":
self.ollama_url = self.connection_url_entry.get_text()
elif result == "discard" and self.ollama_url is None: self.destroy()
self.connection_dialog.force_close()
if self.ollama_url is None or self.verify_connection() == False:
self.show_connection_dialog(True)
@@ -368,31 +578,226 @@ class AlpacaWindow(Adw.ApplicationWindow):
def closing_connection_dialog(self, dialog):
if self.get_visible() == False:
self.destroy()
else:
if self.ollama_url is None: self.destroy()
if self.ollama_url == self.connection_url_entry.get_text():
self.connection_dialog.force_close()
if self.ollama_url is None or self.verify_connection() == False:
self.show_connection_dialog(True)
self.show_toast("error", 1, self.connection_overlay)
return
dialog = Adw.AlertDialog(
heading=f"Save Changes?",
body=f"Do you want to save the URL change?",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("discard", "Discard")
dialog.add_response("save", "Save")
dialog.set_response_appearance("discard", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self,
cancellable = None,
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"Save Changes?",
body=f"Do you want to save the URL change?",
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("discard", "Discard")
dialog.add_response("save", "Save")
dialog.set_response_appearance("discard", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
dialog.add_response("remove", "Remove")
dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.choose(
parent = self,
cancellable = None,
callback = self.closing_connection_dialog_response
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):
super().__init__(**kwargs)
GtkSource.init()
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.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_previous_button.connect("clicked", self.connection_previous_button_activate)
self.connection_next_button.connect("clicked", self.connection_next_button_activate)
@@ -404,8 +809,4 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.ollama_url = f.read()
if self.verify_connection() is False: self.show_connection_dialog(True)
else: self.connection_dialog.present(self)
self.show_toast("funny", True, self.manage_models_overlay)
self.update_chat_list()

View File

@@ -4,116 +4,228 @@
<requires lib="Adw" version="1.0"/>
<template class="AlpacaWindow" parent="AdwApplicationWindow">
<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">
<object class="AdwToastOverlay" id="main_overlay">
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar" id="header_bar">
<property name="title-widget">
<object class="GtkBox">
<property name="orientation">0</property>
<property name="spacing">12</property>
<child>
<object class="GtkDropDown" id="model_drop_down">
<property name="enable-search">true</property>
<property name="model">
<object class="GtkStringList" id="model_string_list">
<items>
</items>
</object>
</property>
</object>
</child>
<child>
<object class="GtkButton" id="manage_models_button">
<property name="tooltip-text" translatable="yes">Manage models</property>
<child>
<object class="AdwButtonContent">
<property name="icon-name">package-x-generic-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</property>
<child type="end">
<object class="GtkMenuButton">
<property name="primary">True</property>
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">Menu</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkBox"><!--ACTUAL CONTENT-->
<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>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="margin-bottom">24</property>
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkScrolledWindow" id="chat_window">
<property name="margin-bottom">12</property>
<property name="has-frame">true</property>
<property name="propagate-natural-height">true</property>
<property name="min-content-width">500</property>
<property name="min-content-height">600</property>
<property name="kinetic-scrolling">1</property>
<property name="vexpand">true</property>
<style>
<class name="undershoot-top"/>
<class name="undershoot-bottom"/>
<class name="card"/>
</style>
<child>
<object class="GtkBox" id="chat_container">
<property name="orientation">1</property>
<property name="homogeneous">false</property>
<property name="hexpand">false</property>
<property name="vexpand">true</property>
<property name="spacing">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">0</property>
<property name="spacing">12</property>
<child>
<object class="GtkEntry" id="message_entry">
<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="send_button">
<object class="GtkButton" id="add_chat_button">
<property name="tooltip-text" translatable="yes">New chat</property>
<style>
<class name="suggested-action"/>
<class name="flat"/>
</style>
<child>
<object class="AdwButtonContent">
<property name="label" translatable="true">Send</property>
<property name="icon-name">send-to-symbolic</property>
<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">
</object><!--END OF CONTENT-->
<property name="selection-mode">single</property>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
</object>
</child>
</object>
</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<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">
<object class="GtkBox">
<property name="orientation">0</property>
<property name="spacing">12</property>
<child>
<object class="GtkDropDown" id="model_drop_down">
<property name="enable-search">true</property>
<property name="model">
<object class="GtkStringList" id="model_string_list">
<items>
</items>
</object>
</property>
</object>
</child>
<child>
<object class="GtkButton" id="manage_models_button">
<property name="tooltip-text" translatable="yes">Manage models</property>
<child>
<object class="AdwButtonContent">
<property name="icon-name">package-x-generic-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</property>
<child type="end">
<object class="GtkMenuButton">
<property name="primary">True</property>
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">Menu</property>
<property name="menu-model">primary_menu</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkBox"><!--ACTUAL CONTENT-->
<property name="orientation">1</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="margin-bottom">24</property>
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkScrolledWindow" id="chat_window">
<property name="margin-bottom">12</property>
<property name="has-frame">true</property>
<property name="propagate-natural-height">true</property>
<property name="min-content-width">500</property>
<property name="min-content-height">600</property>
<property name="kinetic-scrolling">1</property>
<property name="vexpand">true</property>
<style>
<class name="card"/>
</style>
<child>
<object class="GtkBox" id="chat_container">
<property name="orientation">1</property>
<property name="homogeneous">false</property>
<property name="hexpand">false</property>
<property name="vexpand">true</property>
<property name="spacing">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">0</property>
<property name="spacing">12</property>
<child>
<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>
<style>
<class name="view"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="orientation">1</property>
<property name="spacing">12</property>
<child>
<object class="GtkButton" id="send_button">
<style>
<class name="suggested-action"/>
</style>
<child>
<object class="AdwButtonContent">
<property name="label" translatable="true">Send</property>
<property name="icon-name">send-to-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="image_button">
<property name="sensitive">false</property>
<property name="tooltip-text" translatable="true">Requires model 'llava' to be selected</property>
<child>
<object class="AdwButtonContent">
<property name="label" translatable="true">Image</property>
<property name="icon-name">image-x-generic-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object><!--END OF CONTENT-->
</property>
</object>
</child>
</object>
</child>
</object>
@@ -166,7 +278,7 @@
</object>
</child>
<child>
<object class="GtkBox">
<object class="GtkScrolledWindow">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">0</property>
@@ -174,11 +286,26 @@
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<child>
<object class="GtkScrolledWindow">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<object class="GtkBox">
<property name="orientation">1</property>
<property name="spacing">12</property>
<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>
<style>
<class name="boxed-list"/>
@@ -310,7 +437,7 @@
<menu id="primary_menu">
<section>
<item>
<attribute name="label" translatable="yes">_Clear Conversation</attribute>
<attribute name="label" translatable="yes">_Clear Chat</attribute>
<attribute name="action">app.clear</attribute>
</item>
<item>
@@ -323,4 +450,13 @@
</item>
</section>
</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>