Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7f11f17d2 | ||
|
|
60dd1de39d | ||
|
|
887a7645d4 | ||
|
|
340cf9375e | ||
|
|
1d37de6499 | ||
|
|
2d8cb68628 | ||
|
|
4ececd850b | ||
|
|
a4d26b2bda | ||
|
|
d7961f2510 | ||
|
|
45f5214ec9 | ||
|
|
5e1e770aee | ||
|
|
5eb18d00d3 | ||
|
|
bcbfd44e1f | ||
|
|
073c619d89 | ||
|
|
71ab8cfba4 | ||
|
|
0ec812099c | ||
|
|
f7f05a0538 | ||
|
|
82a0ab0d9e | ||
|
|
60b24da482 | ||
|
|
66d209e4c6 | ||
|
|
0c9ab4e17e | ||
|
|
fff3a68b29 | ||
|
|
580a104894 |
@@ -15,6 +15,7 @@ Alpaca is an [Ollama](https://github.com/ollama/ollama) client where you can man
|
||||
- Talk to multiple models in the same conversation
|
||||
- Pull and delete models from the app
|
||||
- Image recognition
|
||||
- Document recognition (plain text files)
|
||||
- Code highlighting
|
||||
- Multiple conversations
|
||||
- Notifications
|
||||
@@ -22,13 +23,12 @@ Alpaca is an [Ollama](https://github.com/ollama/ollama) client where you can man
|
||||
- Delete messages
|
||||
|
||||
## Future features!
|
||||
- Document recognition (Probably only Markdown because it's really easy for AI to read)
|
||||
- YouTube recognition (Ask questions about a YouTube video using the transcript)
|
||||
- Edit messages
|
||||
- Snap Package (maybe)
|
||||
|
||||
## Screenies
|
||||
Login to Ollama instance | Chatting with models | Managing models
|
||||
Code highlighting | Chatting with models | Managing models
|
||||
:-------------------------:|:-------------------------:|:-------------------------:
|
||||
 |  | 
|
||||
|
||||
@@ -50,11 +50,14 @@ For now Alpaca doesn't offer a way to do this from the GUI but it's really simpl
|
||||
Go to `~/.var/app/com.jeffser.Alpaca/config/server.json` and change the `"local_port"` value, by default it is `11435`.
|
||||
|
||||
### Backup all the chats
|
||||
The chat data is located in `~/.var/app/com.jeffser.Alpaca/config/chats.json` you can copy that file wherever you want to.
|
||||
The chat data is located in `~/.var/app/com.jeffser.Alpaca/data/chats` you can copy that directory wherever you want to.
|
||||
|
||||
### Force showing the welcome dialog
|
||||
To do that you just need to delete the file `~/.var/app/com.jeffser.Alpaca/config/server.json`, this won't affect your saved chats or models.
|
||||
|
||||
### Add/Change environment variables for Ollama
|
||||
You can change anything except `$HOME` and `$OLLAMA_HOST`, to do this go to `~/.var/app/com.jeffser.Alpaca/config/server.json` and change `ollama_overrides` accordingly, some overrides are available to change on the GUI.
|
||||
|
||||
---
|
||||
|
||||
## Thanks
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<li>Pull and delete models from the app</li>
|
||||
<li>Have multiple conversations</li>
|
||||
<li>Image recognition (Only available with compatible models)</li>
|
||||
<li>Plain text documents recognition</li>
|
||||
<li>Import and export chats</li>
|
||||
</ul>
|
||||
<p>Disclaimer</p>
|
||||
@@ -63,6 +64,25 @@
|
||||
<url type="homepage">https://github.com/Jeffser/Alpaca</url>
|
||||
<url type="donation">https://github.com/sponsors/Jeffser</url>
|
||||
<releases>
|
||||
<release version="0.9.5" date="2024-06-04">
|
||||
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.9.5</url>
|
||||
<description>
|
||||
<p>Quick Fix</p>
|
||||
<p>There were some errors when transitioning from the old version of chats to the new version. I apologize if this caused any corruption in your chat history. This should be the only time such a transition is needed.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.9.4" date="2024-06-04">
|
||||
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.9.4</url>
|
||||
<description>
|
||||
<p>Huge Update</p>
|
||||
<ul>
|
||||
<li>Added: Support for plain text files</li>
|
||||
<li>Added: New backend system for storing messages</li>
|
||||
<li>Added: Support for changing Ollama's overrides</li>
|
||||
<li>General Optimization</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.9.3" date="2024-06-01">
|
||||
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.9.3</url>
|
||||
<description>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('Alpaca', 'c',
|
||||
version: '0.9.3',
|
||||
version: '0.9.5',
|
||||
meson_version: '>= 0.62.0',
|
||||
default_options: [ 'warning_level=2', 'werror=false', ],
|
||||
)
|
||||
|
||||
541
po/alpaca.pot
541
po/alpaca.pot
File diff suppressed because it is too large
Load Diff
561
po/pt_BR.po
561
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
552
po/pt_BR.po~
552
po/pt_BR.po~
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,9 @@
|
||||
<file alias="icons/scalable/status/view-more-symbolic.svg">icons/view-more-symbolic.svg</file>
|
||||
<file alias="icons/scalable/status/document-open-symbolic.svg">icons/document-open-symbolic.svg</file>
|
||||
<file alias="icons/scalable/status/list-add-symbolic.svg">icons/list-add-symbolic.svg</file>
|
||||
<file alias="icons/scalable/status/brain-augemnted-symbolic.svg">icons/brain-augemnted-symbolic.svg</file>
|
||||
<file alias="icons/scalable/status/chain-link-loose-symbolic.svg">icons/chain-link-loose-symbolic.svg</file>
|
||||
<file alias="icons/scalable/status/document-text-symbolic.svg">icons/document-text-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">window.ui</file>
|
||||
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
|
||||
</gresource>
|
||||
|
||||
@@ -177,14 +177,14 @@ def pull_model(self, model_name):
|
||||
|
||||
# REMOVE IMAGE | WORKS
|
||||
|
||||
def remove_image_response(self, dialog, task):
|
||||
def remove_attached_file_response(self, dialog, task, button):
|
||||
if dialog.choose_finish(task) == 'remove':
|
||||
self.remove_image()
|
||||
self.remove_attached_file(button)
|
||||
|
||||
def remove_image(self):
|
||||
def remove_attached_file(self, button):
|
||||
dialog = Adw.AlertDialog(
|
||||
heading=_("Remove Image"),
|
||||
body=_("Are you sure you want to remove image?"),
|
||||
heading=_("Remove File"),
|
||||
body=_("Are you sure you want to remove file?"),
|
||||
close_response="cancel"
|
||||
)
|
||||
dialog.add_response("cancel", _("Cancel"))
|
||||
@@ -193,7 +193,7 @@ def remove_image(self):
|
||||
dialog.choose(
|
||||
parent = self,
|
||||
cancellable = None,
|
||||
callback = lambda dialog, task: remove_image_response(self, dialog, task)
|
||||
callback = lambda dialog, task, button=button: remove_attached_file_response(self, dialog, task, button)
|
||||
)
|
||||
|
||||
# RECONNECT REMOTE | WORKS
|
||||
@@ -228,7 +228,7 @@ def reconnect_remote(self, current_url):
|
||||
callback = lambda dialog, task, entry=entry: reconnect_remote_response(self, dialog, task, entry)
|
||||
)
|
||||
|
||||
# CREATE MODEL |
|
||||
# CREATE MODEL | WORKS
|
||||
|
||||
def create_model_from_existing_response(self, dialog, task, dropdown):
|
||||
model = dropdown.get_selected_item().get_string()
|
||||
@@ -267,3 +267,18 @@ def create_model_from_file_response(self, file_dialog, result):
|
||||
def create_model_from_file(self):
|
||||
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_gguf)
|
||||
file_dialog.open(self, None, lambda file_dialog, result: create_model_from_file_response(self, file_dialog, result))
|
||||
|
||||
# FILE CHOOSER | WORKS
|
||||
|
||||
def attach_file_response(self, file_dialog, result, file_type):
|
||||
try: file = file_dialog.open_finish(result)
|
||||
except: return
|
||||
self.attach_file(file.get_path(), file_type)
|
||||
|
||||
|
||||
def attach_file(self, filter, file_type):
|
||||
if file_type == 'image' and not self.verify_if_image_can_be_used():
|
||||
self.show_toast('error', 8, self.main_overlay)
|
||||
return
|
||||
file_dialog = Gtk.FileDialog(default_filter=filter)
|
||||
file_dialog.open(self, None, lambda file_dialog, result, file_type=file_type: attach_file_response(self, file_dialog, result, file_type))
|
||||
|
||||
2
src/icons/brain-augemnted-symbolic.svg
Normal file
2
src/icons/brain-augemnted-symbolic.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?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 5.976562 2 c 0.546876 0 1 0.453125 1 1 v 10 c 0 0.546875 -0.453124 1 -1 1 h -0.976562 c -1.652344 0 -3 -1.347656 -3 -3 v -6 c 0 -1.652344 1.347656 -3 3 -3 z m -5.976562 3 v 6 c 0 2.765625 2.234375 5 5 5 h 0.976562 c 1.660157 0 3 -1.339844 3 -3 v -10 c 0 -1.660156 -1.339843 -3 -3 -3 h -0.976562 c -2.765625 0 -5 2.234375 -5 5 z m 0 0"/><path d="m 1.488281 8.996094 h 1.511719 c 1.101562 0 1.988281 -0.886719 1.988281 -1.984375 v -0.515625 c 0 -0.273438 -0.222656 -0.5 -0.5 -0.5 c -0.273437 0 -0.5 0.226562 -0.5 0.5 v 0.515625 c 0 0.542969 -0.445312 0.984375 -0.988281 0.984375 h -1.511719 c -0.273437 0 -0.5 0.226562 -0.5 0.5 c 0 0.277344 0.226563 0.5 0.5 0.5 z m 0 0"/><path d="m 7.5 9.992188 h -1.511719 c -1.101562 0 -1.988281 0.886718 -1.988281 1.984374 v 0.515626 c 0 0.273437 0.222656 0.5 0.5 0.5 s 0.5 -0.226563 0.5 -0.5 v -0.515626 c 0 -0.539062 0.445312 -0.984374 0.988281 -0.984374 h 1.511719 c 0.277344 0 0.5 -0.226563 0.5 -0.5 c 0 -0.277344 -0.222656 -0.5 -0.5 -0.5 z m 0 0"/><path d="m 11.015625 14 h -1.035156 c -0.546875 0 -1 -0.453125 -1 -1 v -10 c 0 -0.546875 0.453125 -1 1 -1 h 1.035156 v -2 h -1.035156 c -1.664063 0 -3 1.339844 -3 3 v 10 c 0 1.660156 1.335937 3 3 3 h 1.035156 z m 0 0"/><path d="m 10 5 h 2.242188 l 2.148437 -2.6875 l -0.78125 -0.625 l -2 2.5 l 0.390625 -0.1875 h -2 z m 0 0"/><path d="m 10 11 h 2 l -0.390625 -0.1875 l 2 2.5 l 0.78125 -0.625 l -2.148437 -2.6875 h -2.242188 z m 0 0"/><path d="m 14.488281 1.976562 c -0.265625 0 -0.488281 -0.21875 -0.488281 -0.488281 c 0 -0.265625 0.222656 -0.488281 0.488281 -0.488281 c 0.269531 0 0.488281 0.222656 0.488281 0.488281 c 0 0.269531 -0.21875 0.488281 -0.488281 0.488281 z m 0 -1.976562 c -0.824219 0 -1.488281 0.664062 -1.488281 1.488281 s 0.664062 1.488281 1.488281 1.488281 s 1.488281 -0.664062 1.488281 -1.488281 s -0.664062 -1.488281 -1.488281 -1.488281 z m 0 0"/><path d="m 14.488281 13.976562 c -0.265625 0 -0.488281 -0.21875 -0.488281 -0.488281 c 0 -0.265625 0.222656 -0.488281 0.488281 -0.488281 c 0.269531 0 0.488281 0.222656 0.488281 0.488281 c 0 0.269531 -0.21875 0.488281 -0.488281 0.488281 z m 0 -1.976562 c -0.824219 0 -1.488281 0.664062 -1.488281 1.488281 s 0.664062 1.488281 1.488281 1.488281 s 1.488281 -0.664062 1.488281 -1.488281 s -0.664062 -1.488281 -1.488281 -1.488281 z m 0 0"/><path d="m 14.488281 7.976562 c -0.265625 0 -0.488281 -0.21875 -0.488281 -0.488281 c 0 -0.265625 0.222656 -0.488281 0.488281 -0.488281 c 0.269531 0 0.488281 0.222656 0.488281 0.488281 c 0 0.269531 -0.21875 0.488281 -0.488281 0.488281 z m 0 -1.976562 c -0.824219 0 -1.488281 0.664062 -1.488281 1.488281 s 0.664062 1.488281 1.488281 1.488281 s 1.488281 -0.664062 1.488281 -1.488281 s -0.664062 -1.488281 -1.488281 -1.488281 z m 0 0"/></g><path d="m 10 7.53125 h 4" fill="none" stroke="#222222"/><path d="m 4.5 4 h 3 c 0.277344 0 0.5 0.222656 0.5 0.5 s -0.222656 0.5 -0.5 0.5 h -3 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 s 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#222222"/></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
2
src/icons/chain-link-loose-symbolic.svg
Normal file
2
src/icons/chain-link-loose-symbolic.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?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 2.683594 9.777344 c -1.570313 -0.542969 -2.683594 -2.039063 -2.683594 -3.777344 c 0 -2.199219 1.800781 -4 4 -4 h 3 c 2.199219 0 4 1.800781 4 4 c 0 1.640625 -0.992188 3.070312 -2.421875 3.679688 l -0.785156 -1.839844 c 0.710937 -0.304688 1.207031 -1 1.207031 -1.839844 c 0 -1.125 -0.875 -2 -2 -2 h -3 c -1.125 0 -2 0.875 -2 2 c 0 0.890625 0.558594 1.621094 1.339844 1.890625 z m 0 0"/><path d="m 8 14 c -2.199219 0 -4 -1.800781 -4 -4 c 0 -1.621094 0.96875 -3.03125 2.367188 -3.65625 l 0.816406 1.828125 c -0.699219 0.3125 -1.183594 1 -1.183594 1.828125 c 0 1.125 0.875 2 2 2 h 3 c 1.125 0 2 -0.875 2 -2 c 0 -0.867188 -0.53125 -1.582031 -1.277344 -1.867188 l 0.714844 -1.867187 c 1.503906 0.574219 2.5625 2.039063 2.5625 3.734375 c 0 2.199219 -1.800781 4 -4 4 z m 0 0"/></g></svg>
|
||||
|
After Width: | Height: | Size: 934 B |
2
src/icons/document-text-symbolic.svg
Normal file
2
src/icons/document-text-symbolic.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?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 9 2 v 5 h 4 v -1 z m -4 2 v 1 h 3 v -1 z m 0 2 v 1 h 3 v -1 z m 0 2 v 1 h 6 v -1 z m 0 2 v 1 h 6 v -1 z m 0 2 v 1 h 6 v -1 z m 0 0"/><path d="m 2 13 c 0 1.660156 1.339844 3 3 3 h 6 c 1.660156 0 3 -1.339844 3 -3 v -6 c 0 -0.90625 -0.359375 -1.773438 -1 -2.414062 l -2.585938 -2.585938 c -0.640624 -0.640625 -1.507812 -1 -2.414062 -1 h -3 c -1.660156 0 -3 1.339844 -3 3 z m 3 -10 h 3 c 0.375 0 0.734375 0.148438 1 0.414062 l 2.585938 2.585938 c 0.265624 0.265625 0.414062 0.625 0.414062 1 v 6 c 0 0.546875 -0.453125 1 -1 1 h -6 c -0.546875 0 -1 -0.453125 -1 -1 v -9 c 0 -0.546875 0.453125 -1 1 -1 z m 0 0"/></g></svg>
|
||||
|
After Width: | Height: | Size: 771 B |
@@ -5,10 +5,14 @@ from time import sleep
|
||||
instance = None
|
||||
port = 11435
|
||||
data_dir = os.getenv("XDG_DATA_HOME")
|
||||
overrides = {}
|
||||
|
||||
def start():
|
||||
global instance
|
||||
instance = subprocess.Popen(["/app/bin/ollama", "serve"], env={**os.environ, 'OLLAMA_HOST': f"127.0.0.1:{port}", "HOME": data_dir}, stderr=subprocess.PIPE, text=True)
|
||||
global instance, overrides
|
||||
params = overrides.copy()
|
||||
params["OLLAMA_HOST"] = f"127.0.0.1:{port}" # You can't change this directly sorry :3
|
||||
params["HOME"] = data_dir
|
||||
instance = subprocess.Popen(["/app/bin/ollama", "serve"], env={**os.environ, **params}, stderr=subprocess.PIPE, text=True)
|
||||
print("Starting Alpaca's Ollama instance...")
|
||||
sleep(1)
|
||||
print("Started Alpaca's Ollama instance")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# main.py
|
||||
#
|
||||
# Copyright 2024 Unknown
|
||||
# Copyright 2024 Jeffser
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -47,7 +47,7 @@ class AlpacaApplication(Adw.Application):
|
||||
application_name='Alpaca',
|
||||
application_icon='com.jeffser.Alpaca',
|
||||
developer_name='Jeffry Samuel Eduarte Rojas',
|
||||
version='0.9.3',
|
||||
version='0.9.5',
|
||||
developers=['Jeffser https://jeffser.com'],
|
||||
designers=['Jeffser https://jeffser.com'],
|
||||
translator_credits='Alex K (Russian) https://github.com/alexkdeveloper\nJeffser (Spanish) https://jeffser.com\nDaimar Stein (Brazilian Portuguese) https://github.com/not-a-dev-stein\nLouis Chauvet-Villaret (French) https://github.com/loulou64490',
|
||||
|
||||
@@ -42,7 +42,8 @@ alpaca_sources = [
|
||||
'connection_handler.py',
|
||||
'available_models.py',
|
||||
'dialogs.py',
|
||||
'local_instance.py'
|
||||
'local_instance.py',
|
||||
'update_history.py'
|
||||
]
|
||||
|
||||
install_data(alpaca_sources, install_dir: moduledir)
|
||||
|
||||
39
src/update_history.py
Normal file
39
src/update_history.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# update_history.py
|
||||
# This script updates the old chats.json file to the structure needed for the new version
|
||||
import os, json, base64
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
|
||||
def update(self):
|
||||
old_data = None
|
||||
new_data = {"chats": {}}
|
||||
with open(os.path.join(self.config_dir, "chats.json"), 'r') as f:
|
||||
old_data = json.load(f)["chats"]
|
||||
for chat_name, content in old_data.items():
|
||||
directory = os.path.join(self.data_dir, "chats", chat_name)
|
||||
if not os.path.exists(directory): os.makedirs(directory)
|
||||
new_messages = {}
|
||||
for message in content['messages']:
|
||||
if message:
|
||||
message_id = self.generate_uuid()
|
||||
if 'images' in message:
|
||||
if not os.path.exists(os.path.join(directory, message_id)): os.makedirs(os.path.join(directory, message_id))
|
||||
new_images = []
|
||||
for image in message['images']:
|
||||
file_name = f"{self.generate_uuid()}.png"
|
||||
decoded = base64.b64decode(image)
|
||||
buffer = io.BytesIO(decoded)
|
||||
im = Image.open(buffer)
|
||||
im.save(os.path.join(directory, message_id, file_name))
|
||||
new_images.append(file_name)
|
||||
message['images'] = new_images
|
||||
new_messages[message_id] = message
|
||||
new_data['chats'][chat_name] = {}
|
||||
new_data['chats'][chat_name]['messages'] = new_messages
|
||||
|
||||
with open(os.path.join(self.data_dir, "chats", "chats.json"), "w+") as f:
|
||||
json.dump(new_data, f, indent=6)
|
||||
|
||||
os.remove(os.path.join(self.config_dir, "chats.json"))
|
||||
|
||||
470
src/window.py
470
src/window.py
@@ -1,6 +1,6 @@
|
||||
# window.py
|
||||
#
|
||||
# Copyright 2024 Unknown
|
||||
# Copyright 2024 Jeffser
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -21,17 +21,18 @@ import gi
|
||||
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, sys, gettext, locale, webbrowser, subprocess
|
||||
import json, requests, threading, os, re, base64, sys, gettext, locale, webbrowser, subprocess, uuid, shutil, tarfile, tempfile
|
||||
from time import sleep
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
from datetime import datetime
|
||||
from .available_models import available_models
|
||||
from . import dialogs, local_instance, connection_handler
|
||||
from . import dialogs, local_instance, connection_handler, update_history
|
||||
|
||||
@Gtk.Template(resource_path='/com/jeffser/Alpaca/window.ui')
|
||||
class AlpacaWindow(Adw.ApplicationWindow):
|
||||
config_dir = os.getenv("XDG_CONFIG_HOME")
|
||||
data_dir = os.getenv("XDG_DATA_HOME")
|
||||
app_dir = os.getenv("FLATPAK_DEST")
|
||||
cache_dir = os.getenv("XDG_CACHE_HOME")
|
||||
|
||||
@@ -51,8 +52,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
model_tweaks = {"temperature": 0.7, "seed": 0, "keep_alive": 5}
|
||||
local_models = []
|
||||
pulling_models = {}
|
||||
chats = {"chats": {_("New Chat"): {"messages": []}}, "selected_chat": "New Chat"}
|
||||
attached_image = {"path": None, "base64": None}
|
||||
chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat"}
|
||||
attachments = {}
|
||||
|
||||
#Override elements
|
||||
override_HSA_OVERRIDE_GFX_VERSION = Gtk.Template.Child()
|
||||
override_CUDA_VISIBLE_DEVICES = Gtk.Template.Child()
|
||||
override_HIP_VISIBLE_DEVICES = Gtk.Template.Child()
|
||||
|
||||
#Elements
|
||||
create_model_base = Gtk.Template.Child()
|
||||
@@ -68,6 +74,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
bot_message : Gtk.TextBuffer = None
|
||||
bot_message_box : Gtk.Box = None
|
||||
bot_message_view : Gtk.TextView = None
|
||||
file_preview_dialog = Gtk.Template.Child()
|
||||
file_preview_text_view = Gtk.Template.Child()
|
||||
welcome_dialog = Gtk.Template.Child()
|
||||
welcome_carousel = Gtk.Template.Child()
|
||||
welcome_previous_button = Gtk.Template.Child()
|
||||
@@ -79,10 +87,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
message_text_view = Gtk.Template.Child()
|
||||
send_button = Gtk.Template.Child()
|
||||
stop_button = Gtk.Template.Child()
|
||||
image_button = Gtk.Template.Child()
|
||||
chats_menu_button = Gtk.Template.Child()
|
||||
attachment_container = Gtk.Template.Child()
|
||||
attachment_box = Gtk.Template.Child()
|
||||
file_filter_image = Gtk.Template.Child()
|
||||
file_filter_json = Gtk.Template.Child()
|
||||
file_filter_tar = Gtk.Template.Child()
|
||||
file_filter_gguf = Gtk.Template.Child()
|
||||
file_filter_text = Gtk.Template.Child()
|
||||
model_drop_down = Gtk.Template.Child()
|
||||
model_string_list = Gtk.Template.Child()
|
||||
|
||||
@@ -109,7 +120,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
_("Could not pull model"),
|
||||
_("Cannot open image"),
|
||||
_("Cannot delete chat because it's the only one left"),
|
||||
_("There was an error with the local Ollama instance, so it has been reset")
|
||||
_("There was an error with the local Ollama instance, so it has been reset"),
|
||||
_("Image recognition is only available on specific models")
|
||||
],
|
||||
"info": [
|
||||
_("Please select a model before chatting"),
|
||||
@@ -134,22 +146,19 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
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', 'bakllava', 'moondream', 'llava-llama3']:
|
||||
self.image_button.set_sensitive(True)
|
||||
self.image_button.set_tooltip_text(_("Upload image"))
|
||||
for name, content in self.attachments.items():
|
||||
if content['type'] == 'image':
|
||||
content['button'].set_css_classes(["flat"])
|
||||
return True
|
||||
else:
|
||||
self.image_button.set_sensitive(False)
|
||||
self.image_button.set_tooltip_text(_("Only available on selected models"))
|
||||
self.image_button.set_css_classes(["circular"])
|
||||
self.attached_image = {"path": None, "base64": None}
|
||||
for name, content in self.attachments.items():
|
||||
if content['type'] == 'image':
|
||||
content['button'].set_css_classes(["flat", "error"])
|
||||
return False
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def stop_message(self, button=None):
|
||||
if self.loading_spinner: self.chat_container.remove(self.loading_spinner)
|
||||
if self.verify_if_image_can_be_used(): self.image_button.set_sensitive(True)
|
||||
self.image_button.set_css_classes(["circular"])
|
||||
self.attached_image = {"path": None, "base64": None}
|
||||
self.toggle_ui_sensitive(True)
|
||||
self.switch_send_stop_button()
|
||||
self.bot_message = None
|
||||
@@ -158,40 +167,59 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def send_message(self, button=None):
|
||||
if self.bot_message: 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
|
||||
current_model = self.model_drop_down.get_selected_item()
|
||||
if current_model is None:
|
||||
self.show_toast("info", 0, self.main_overlay)
|
||||
return
|
||||
id = self.generate_uuid()
|
||||
|
||||
attached_images = []
|
||||
attached_files = {}
|
||||
can_use_images = self.verify_if_image_can_be_used()
|
||||
for name, content in self.attachments.items():
|
||||
if content["type"] == 'image' and can_use_images: attached_images.append(name)
|
||||
else: attached_files[name] = content['type']
|
||||
if not os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id)):
|
||||
os.makedirs(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id))
|
||||
shutil.copy(content['path'], os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name))
|
||||
content["button"].get_parent().remove(content["button"])
|
||||
self.attachments = {}
|
||||
self.attachment_box.set_visible(False)
|
||||
|
||||
#{"path": file_path, "type": file_type, "content": content}
|
||||
|
||||
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"].append({
|
||||
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"][id] = {
|
||||
"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)
|
||||
})
|
||||
messages_to_send = []
|
||||
for message in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
|
||||
if message: messages_to_send.append(message)
|
||||
}
|
||||
if len(attached_images) > 0:
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"][id]['images'] = attached_images
|
||||
if len(attached_files.keys()) > 0:
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"][id]['files'] = attached_files
|
||||
data = {
|
||||
"model": current_model.get_string(),
|
||||
"messages": messages_to_send,
|
||||
"messages": self.convert_history_to_ollama(),
|
||||
"options": {"temperature": self.model_tweaks["temperature"], "seed": self.model_tweaks["seed"]},
|
||||
"keep_alive": f"{self.model_tweaks['keep_alive']}m"
|
||||
}
|
||||
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.switch_send_stop_button()
|
||||
self.toggle_ui_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"], id=len(self.chats["chats"][self.chats["selected_chat"]]["messages"])-1)
|
||||
#self.attachments[name] = {"path": file_path, "type": file_type, "content": content}
|
||||
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>", attached_images, attached_files, id=id)
|
||||
self.message_text_view.get_buffer().set_text("", 0)
|
||||
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
|
||||
self.chat_container.append(self.loading_spinner)
|
||||
self.show_message("", True, id=len(self.chats["chats"][self.chats["selected_chat"]]["messages"]))
|
||||
bot_id=self.generate_uuid()
|
||||
self.show_message("", True, id=bot_id)
|
||||
|
||||
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model']))
|
||||
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'], bot_id))
|
||||
thread.start()
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
@@ -218,22 +246,14 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
if not self.verify_connection():
|
||||
self.connection_error()
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def open_image(self, button):
|
||||
if "destructive-action" in button.get_css_classes():
|
||||
dialogs.remove_image(self)
|
||||
else:
|
||||
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_image)
|
||||
file_dialog.open(self, None, self.load_image)
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def chat_changed(self, listbox, row):
|
||||
if row and row.get_name() != self.chats["selected_chat"]:
|
||||
self.chats["selected_chat"] = row.get_name()
|
||||
self.load_history_into_chat()
|
||||
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"]) > 0:
|
||||
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()) > 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"]:
|
||||
if self.model_string_list.get_string(i) == self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]:
|
||||
self.model_drop_down.set_selected(i)
|
||||
break
|
||||
|
||||
@@ -314,6 +334,19 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
self.manage_models_dialog.present(self)
|
||||
thread.start()
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def override_changed(self, entry):
|
||||
name = entry.get_name()
|
||||
value = entry.get_text()
|
||||
if (not value and name not in local_instance.overrides) or (value and value in local_instance.overrides and local_instance.overrides[name] == value): return
|
||||
if not value: del local_instance.overrides[name]
|
||||
else: local_instance.overrides[name] = value
|
||||
self.save_server_config()
|
||||
if not self.run_remote: local_instance.reset()
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def link_button_handler(self, button):
|
||||
webbrowser.open(button.get_name())
|
||||
|
||||
def check_alphanumeric(self, editable, text, length, position):
|
||||
new_text = ''.join([char for char in text if char.isalnum() or char in ['-', '_']])
|
||||
@@ -365,19 +398,51 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
self.get_application().send_notification(None, notification)
|
||||
|
||||
def delete_message(self, message_element):
|
||||
message_index = int(message_element.get_name())
|
||||
if message_index < len(self.chats["chats"][self.chats["selected_chat"]]["messages"]):
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"][message_index] = None
|
||||
self.chat_container.remove(message_element)
|
||||
self.save_history()
|
||||
id = message_element.get_name()
|
||||
del self.chats["chats"][self.chats["selected_chat"]]["messages"][id]
|
||||
self.chat_container.remove(message_element)
|
||||
if os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id)):
|
||||
shutil.rmtree(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id))
|
||||
self.save_history()
|
||||
|
||||
def copy_message(self, message_element):
|
||||
message_index = int(message_element.get_name())
|
||||
id = message_element.get_name()
|
||||
clipboard = Gdk.Display().get_default().get_clipboard()
|
||||
clipboard.set(self.chats["chats"][self.chats["selected_chat"]]["messages"][message_index]["content"])
|
||||
clipboard.set(self.chats["chats"][self.chats["selected_chat"]]["messages"][id]["content"])
|
||||
self.show_toast("info", 5, self.main_overlay)
|
||||
|
||||
def show_message(self, msg:str, bot:bool, footer:str=None, image_base64:str=None, id:int=-1):
|
||||
def preview_file(self, file_path, file_type):
|
||||
content = self.get_content_of_file(file_path, file_type)
|
||||
if content:
|
||||
buffer = self.file_preview_text_view.get_buffer()
|
||||
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
|
||||
buffer.insert(buffer.get_start_iter(), content, len(content))
|
||||
self.file_preview_dialog.set_title(os.path.basename(file_path))
|
||||
self.file_preview_dialog.present(self)
|
||||
|
||||
def convert_history_to_ollama(self):
|
||||
messages = []
|
||||
for id, message in self.chats["chats"][self.chats["selected_chat"]]["messages"].items():
|
||||
new_message = message.copy()
|
||||
if 'files' in message and len(message['files']) > 0:
|
||||
del new_message['files']
|
||||
new_message['content'] = ''
|
||||
for name, file_type in message['files'].items():
|
||||
file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name)
|
||||
file_data = self.get_content_of_file(file_path, file_type)
|
||||
if file_data: new_message['content'] += f"```[{name}]\n{file_data}\n```"
|
||||
new_message['content'] += message['content']
|
||||
if 'images' in message and len(message['images']) > 0:
|
||||
new_message['images'] = []
|
||||
for name in message['images']:
|
||||
file_path = os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name)
|
||||
image_data = self.get_content_of_file(file_path, 'image')
|
||||
if image_data: new_message['images'].append(image_data)
|
||||
messages.append(new_message)
|
||||
return messages
|
||||
|
||||
|
||||
def show_message(self, msg:str, bot:bool, footer:str=None, images:list=None, files:dict=None, id:str=None):
|
||||
message_text = Gtk.TextView(
|
||||
editable=False,
|
||||
focusable=True,
|
||||
@@ -420,23 +485,65 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
)
|
||||
message_text.set_valign(Gtk.Align.CENTER)
|
||||
|
||||
if image_base64 is not None:
|
||||
image_data = base64.b64decode(image_base64)
|
||||
loader = GdkPixbuf.PixbufLoader.new()
|
||||
loader.write(image_data)
|
||||
loader.close()
|
||||
if images and len(images) > 0:
|
||||
image_container = Gtk.Box(
|
||||
orientation=0,
|
||||
spacing=12
|
||||
)
|
||||
image_scroller = Gtk.ScrolledWindow(
|
||||
margin_top=10,
|
||||
margin_start=10,
|
||||
margin_end=10,
|
||||
hexpand=True,
|
||||
height_request = 240,
|
||||
child=image_container
|
||||
)
|
||||
for image in images:
|
||||
raw_data = self.get_content_of_file(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, image), "image")
|
||||
if raw_data:
|
||||
image_data = base64.b64decode(raw_data)
|
||||
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(240, 240)
|
||||
image.set_hexpand(False)
|
||||
image.set_css_classes(["flat"])
|
||||
image_container.append(image)
|
||||
message_box.append(image_scroller)
|
||||
|
||||
pixbuf = loader.get_pixbuf()
|
||||
texture = Gdk.Texture.new_for_pixbuf(pixbuf)
|
||||
if files and len(files) > 0:
|
||||
file_container = Gtk.Box(
|
||||
orientation=0,
|
||||
spacing=12
|
||||
)
|
||||
file_scroller = Gtk.ScrolledWindow(
|
||||
margin_top=10,
|
||||
margin_start=10,
|
||||
margin_end=10,
|
||||
hexpand=True,
|
||||
child=file_container
|
||||
)
|
||||
for name, file_type in files.items():
|
||||
shown_name='.'.join(name.split(".")[:-1])[:20] + (name[20:] and '..') + f".{name.split('.')[-1]}"
|
||||
|
||||
image = Gtk.Image.new_from_paintable(texture)
|
||||
image.set_size_request(240, 240)
|
||||
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)
|
||||
button_content = Adw.ButtonContent(
|
||||
label=shown_name,
|
||||
icon_name="document-text-symbolic"
|
||||
)
|
||||
button = Gtk.Button(
|
||||
vexpand=False,
|
||||
valign=3,
|
||||
name=name,
|
||||
css_classes=["flat"],
|
||||
tooltip_text=name,
|
||||
child=button_content
|
||||
)
|
||||
button.connect("clicked", lambda button, file_path=os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, name), file_type=file_type: self.preview_file(file_path, file_type))
|
||||
file_container.append(button)
|
||||
message_box.append(file_scroller)
|
||||
|
||||
message_box.append(message_text)
|
||||
overlay = Gtk.Overlay(css_classes=["message"], name=id)
|
||||
@@ -490,7 +597,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
|
||||
def save_server_config(self):
|
||||
with open(os.path.join(self.config_dir, "server.json"), "w+") as f:
|
||||
json.dump({'remote_url': self.remote_url, 'run_remote': self.run_remote, 'local_port': local_instance.port, 'run_on_background': self.run_on_background, 'model_tweaks': self.model_tweaks}, f)
|
||||
json.dump({'remote_url': self.remote_url, 'run_remote': self.run_remote, 'local_port': local_instance.port, 'run_on_background': self.run_on_background, 'model_tweaks': self.model_tweaks, 'ollama_overrides': local_instance.overrides}, f, indent=6)
|
||||
|
||||
def verify_connection(self):
|
||||
response = connection_handler.simple_get(connection_handler.url)
|
||||
@@ -614,12 +721,12 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
clipboard.set(text)
|
||||
self.show_toast("info", 4, self.main_overlay)
|
||||
|
||||
def update_bot_message(self, data):
|
||||
def update_bot_message(self, data, id):
|
||||
if self.bot_message is None:
|
||||
self.save_history()
|
||||
sys.exit()
|
||||
vadjustment = self.chat_window.get_vadjustment()
|
||||
if self.chats["chats"][self.chats["selected_chat"]]["messages"][-1]['role'] == "user" or vadjustment.get_value() + 50 >= vadjustment.get_upper() - vadjustment.get_page_size():
|
||||
if (id in self.chats["chats"][self.chats["selected_chat"]]["messages"] and self.chats["chats"][self.chats["selected_chat"]]["messages"][id]['role'] == "user") or 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")
|
||||
@@ -627,34 +734,31 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text))
|
||||
self.save_history()
|
||||
else:
|
||||
if self.chats["chats"][self.chats["selected_chat"]]["messages"][-1]['role'] == "user":
|
||||
if id not in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
|
||||
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
|
||||
self.loading_spinner = None
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"].append({
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"][id] = {
|
||||
"role": "assistant",
|
||||
"model": data['model'],
|
||||
"date": datetime.now().strftime("%Y/%m/%d %H:%M"),
|
||||
"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']
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"][id]['content'] += data['message']['content']
|
||||
|
||||
def toggle_ui_sensitive(self, status):
|
||||
for element in [self.chat_list_box, self.add_chat_button]:
|
||||
for element in [self.chat_list_box, self.add_chat_button, self.chats_menu_button]:
|
||||
element.set_sensitive(status)
|
||||
|
||||
def switch_send_stop_button(self):
|
||||
self.stop_button.set_visible(self.send_button.get_visible())
|
||||
self.send_button.set_visible(not self.send_button.get_visible())
|
||||
|
||||
def run_message(self, messages, model):
|
||||
response = connection_handler.stream_post(f"{connection_handler.url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=self.update_bot_message)
|
||||
def run_message(self, messages, model, id):
|
||||
response = connection_handler.stream_post(f"{connection_handler.url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=lambda data, id=id: self.update_bot_message(data, id))
|
||||
GLib.idle_add(self.add_code_blocks)
|
||||
GLib.idle_add(self.switch_send_stop_button)
|
||||
GLib.idle_add(self.toggle_ui_sensitive, True)
|
||||
if self.verify_if_image_can_be_used(): GLib.idle_add(self.image_button.set_sensitive, True)
|
||||
GLib.idle_add(self.image_button.set_css_classes, ["circular"])
|
||||
self.attached_image = {"path": None, "base64": None}
|
||||
if self.loading_spinner:
|
||||
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
|
||||
self.loading_spinner = None
|
||||
@@ -754,70 +858,47 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
self.available_model_list_box.append(model)
|
||||
|
||||
def save_history(self):
|
||||
with open(os.path.join(self.config_dir, "chats.json"), "w+") as f:
|
||||
with open(os.path.join(self.data_dir, "chats", "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 i, message in enumerate(self.chats['chats'][self.chats["selected_chat"]]['messages']):
|
||||
for key, message in self.chats['chats'][self.chats["selected_chat"]]['messages'].items():
|
||||
if message:
|
||||
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, id=i)
|
||||
self.show_message(message['content'], False, f"\n\n<small>{message['date']}</small>", message['images'] if 'images' in message else None, message['files'] if 'files' in message else None, id=key)
|
||||
else:
|
||||
self.show_message(message['content'], True, f"\n\n<small>{message['model']}\t|\t{message['date']}</small>", id=i)
|
||||
self.show_message(message['content'], True, f"\n\n<small>{message['model']}\t|\t{message['date']}</small>", id=key)
|
||||
self.add_code_blocks()
|
||||
self.bot_message = None
|
||||
|
||||
def load_history(self):
|
||||
if os.path.exists(os.path.join(self.config_dir, "chats.json")):
|
||||
if os.path.exists(os.path.join(self.data_dir, "chats", "chats.json")):
|
||||
try:
|
||||
with open(os.path.join(self.config_dir, "chats.json"), "r") as f:
|
||||
with open(os.path.join(self.data_dir, "chats", "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": []}
|
||||
for chat_name, content in self.chats['chats'].items():
|
||||
for i, content in enumerate(content['messages']):
|
||||
if not content: del self.chats['chats'][chat_name]['messages'][i]
|
||||
if len(list(self.chats["chats"].keys())) == 0: self.chats["chats"][_("New Chat")] = {"messages": {}}
|
||||
except Exception as e:
|
||||
self.chats = {"chats": {_("New Chat"): {"messages": []}}, "selected_chat": _("New Chat")}
|
||||
self.chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": _("New Chat")}
|
||||
self.load_history_into_chat()
|
||||
|
||||
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 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)
|
||||
def generate_numbered_name(self, chat_name:str, compare_list:list) -> str:
|
||||
if chat_name in compare_list:
|
||||
for i in range(len(compare_list)):
|
||||
if "." in chat_name:
|
||||
if f"{'.'.join(chat_name.split('.')[:-1])} {i+1}.{chat_name.split('.')[-1]}" not in compare_list:
|
||||
chat_name = f"{'.'.join(chat_name.split('.')[:-1])} {i+1}.{chat_name.split('.')[-1]}"
|
||||
break
|
||||
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="PNG")
|
||||
image_data = output.getvalue()
|
||||
self.attached_image["base64"] = base64.b64encode(image_data).decode("utf-8")
|
||||
|
||||
self.image_button.set_css_classes(["destructive-action", "circular"])
|
||||
except Exception as e:
|
||||
self.show_toast("error", 5, self.main_overlay)
|
||||
|
||||
def remove_image(self):
|
||||
self.image_button.set_css_classes(["circular"])
|
||||
self.attached_image = {"path": None, "base64": None}
|
||||
|
||||
def generate_numbered_chat_name(self, chat_name) -> str:
|
||||
if chat_name in self.chats["chats"]:
|
||||
for i in range(len(list(self.chats["chats"].keys()))):
|
||||
if chat_name + f" {i+1}" not in self.chats["chats"]:
|
||||
chat_name += f" {i+1}"
|
||||
break
|
||||
if f"{chat_name} {i+1}" not in compare_list:
|
||||
chat_name = f"{chat_name} {i+1}"
|
||||
break
|
||||
return chat_name
|
||||
|
||||
def generate_uuid(self) -> str:
|
||||
return f"{datetime.today().strftime('%Y%m%d%H%M%S%f')}{uuid.uuid4().hex}"
|
||||
|
||||
def clear_chat(self):
|
||||
for widget in list(self.chat_container): self.chat_container.remove(widget)
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"] = []
|
||||
@@ -825,22 +906,26 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
|
||||
def delete_chat(self, chat_name):
|
||||
del self.chats['chats'][chat_name]
|
||||
if os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'])):
|
||||
shutil.rmtree(os.path.join(self.data_dir, "chats", self.chats['selected_chat']))
|
||||
self.save_history()
|
||||
self.update_chat_list()
|
||||
if len(self.chats['chats'])==0:
|
||||
self.new_chat()
|
||||
|
||||
def rename_chat(self, old_chat_name, new_chat_name, label_element):
|
||||
new_chat_name = self.generate_numbered_chat_name(new_chat_name)
|
||||
new_chat_name = self.generate_numbered_name(new_chat_name, self.chats["chats"].keys())
|
||||
self.chats["chats"][new_chat_name] = self.chats["chats"][old_chat_name]
|
||||
del self.chats["chats"][old_chat_name]
|
||||
if os.path.exists(os.path.join(self.data_dir, "chats", old_chat_name)):
|
||||
shutil.move(os.path.join(self.data_dir, "chats", old_chat_name), os.path.join(self.data_dir, "chats", new_chat_name))
|
||||
label_element.set_label(new_chat_name)
|
||||
label_element.get_parent().set_name(new_chat_name)
|
||||
self.save_history()
|
||||
|
||||
def new_chat(self):
|
||||
chat_name = self.generate_numbered_chat_name(_("New Chat"))
|
||||
self.chats["chats"][chat_name] = {"messages": []}
|
||||
chat_name = self.generate_numbered_name(_("New Chat"), self.chats["chats"].keys())
|
||||
self.chats["chats"][chat_name] = {"messages": {}}
|
||||
self.save_history()
|
||||
self.new_chat_element(chat_name, True)
|
||||
|
||||
@@ -946,35 +1031,74 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
|
||||
def on_export_current_chat(self, file_dialog, result):
|
||||
file = file_dialog.save_finish(result)
|
||||
data_to_export = {self.chats["selected_chat"]: self.chats["chats"][self.chats["selected_chat"]]}
|
||||
file.replace_contents_async(
|
||||
json.dumps(data_to_export, indent=4).encode("UTF-8"),
|
||||
etag=None,
|
||||
make_backup=False,
|
||||
flags=Gio.FileCreateFlags.NONE,
|
||||
cancellable=None,
|
||||
callback=self.on_replace_contents
|
||||
)
|
||||
if not file: return
|
||||
json_data = json.dumps({self.chats["selected_chat"]: self.chats["chats"][self.chats["selected_chat"]]}, indent=4).encode("UTF-8")
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
json_path = os.path.join(temp_dir, "data.json")
|
||||
with open(json_path, "wb") as json_file:
|
||||
json_file.write(json_data)
|
||||
|
||||
tar_path = os.path.join(temp_dir, f"{self.chats['selected_chat']}")
|
||||
with tarfile.open(tar_path, "w") as tar:
|
||||
tar.add(json_path, arcname="data.json")
|
||||
directory = os.path.join(self.data_dir, "chats", self.chats['selected_chat'])
|
||||
if os.path.exists(directory) and os.path.isdir(directory):
|
||||
tar.add(directory, arcname=os.path.basename(directory))
|
||||
|
||||
with open(tar_path, "rb") as tar:
|
||||
tar_content = tar.read()
|
||||
|
||||
file.replace_contents_async(
|
||||
tar_content,
|
||||
etag=None,
|
||||
make_backup=False,
|
||||
flags=Gio.FileCreateFlags.NONE,
|
||||
cancellable=None,
|
||||
callback=self.on_replace_contents
|
||||
)
|
||||
|
||||
def export_current_chat(self):
|
||||
file_dialog = Gtk.FileDialog(initial_name=f"{self.chats['selected_chat']}.json")
|
||||
file_dialog = Gtk.FileDialog(initial_name=f"{self.chats['selected_chat']}.tar")
|
||||
file_dialog.save(parent=self, cancellable=None, callback=self.on_export_current_chat)
|
||||
|
||||
def on_chat_imported(self, file_dialog, result):
|
||||
file = file_dialog.open_finish(result)
|
||||
if not file: return
|
||||
stream = file.read(None)
|
||||
data_stream = Gio.DataInputStream.new(stream)
|
||||
data, _ = data_stream.read_until('\0', None)
|
||||
data = json.loads(data)
|
||||
chat_name = list(data.keys())[0]
|
||||
chat_content = data[chat_name]
|
||||
self.chats['chats'][chat_name] = chat_content
|
||||
tar_content = data_stream.read_bytes(1024 * 1024, None)
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
tar_filename = os.path.join(temp_dir, "imported_chat.tar")
|
||||
|
||||
with open(tar_filename, "wb") as tar_file:
|
||||
tar_file.write(tar_content.get_data())
|
||||
|
||||
with tarfile.open(tar_filename, "r") as tar:
|
||||
tar.extractall(path=temp_dir)
|
||||
chat_name = None
|
||||
chat_content = None
|
||||
for member in tar.getmembers():
|
||||
if member.name == "data.json":
|
||||
json_filepath = os.path.join(temp_dir, member.name)
|
||||
with open(json_filepath, "r") as json_file:
|
||||
data = json.load(json_file)
|
||||
for chat_name, chat_content in data.items():
|
||||
new_chat_name = self.generate_numbered_name(chat_name, list(self.chats['chats'].keys()))
|
||||
self.chats['chats'][new_chat_name] = chat_content
|
||||
src_path = os.path.join(temp_dir, chat_name)
|
||||
if os.path.exists(src_path) and os.path.isdir(src_path):
|
||||
dest_path = os.path.join(self.data_dir, "chats", new_chat_name)
|
||||
shutil.copytree(src_path, dest_path)
|
||||
|
||||
|
||||
self.update_chat_list()
|
||||
self.save_history()
|
||||
self.show_toast("good", 3, self.main_overlay)
|
||||
|
||||
def import_chat(self):
|
||||
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_json)
|
||||
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_tar)
|
||||
file_dialog.open(self, None, self.on_chat_imported)
|
||||
|
||||
def switch_run_on_background(self):
|
||||
@@ -982,9 +1106,67 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
self.set_hide_on_close(self.run_on_background)
|
||||
self.verify_connection()
|
||||
|
||||
def get_content_of_file(self, file_path, file_type):
|
||||
if not os.path.exists(file_path): return None
|
||||
if file_type == 'image':
|
||||
try:
|
||||
with Image.open(file_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="PNG")
|
||||
image_data = output.getvalue()
|
||||
return base64.b64encode(image_data).decode("utf-8")
|
||||
except Exception as e:
|
||||
self.show_toast("error", 5, self.main_overlay)
|
||||
elif file_type == 'plain_text':
|
||||
with open(file_path, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
def remove_attached_file(self, button):
|
||||
del self.attachments[button.get_name()]
|
||||
button.get_parent().remove(button)
|
||||
if len(self.attachments) == 0: self.attachment_box.set_visible(False)
|
||||
|
||||
def attach_file(self, file_path, file_type):
|
||||
name = self.generate_numbered_name(os.path.basename(file_path), self.attachments.keys())
|
||||
content = self.get_content_of_file(file_path, file_type)
|
||||
if content:
|
||||
shown_name='.'.join(name.split(".")[:-1])[:20] + (name[20:] and '..') + f".{name.split('.')[-1]}"
|
||||
|
||||
button_content = Adw.ButtonContent(
|
||||
label=shown_name,
|
||||
icon_name={"image": "image-x-generic-symbolic", "plain_text": "document-text-symbolic"}[file_type]
|
||||
)
|
||||
button = Gtk.Button(
|
||||
vexpand=True,
|
||||
valign=3,
|
||||
name=name,
|
||||
css_classes=["flat"],
|
||||
tooltip_text=name,
|
||||
child=button_content
|
||||
)
|
||||
|
||||
self.attachments[name] = {"path": file_path, "type": file_type, "content": content, "button": button}
|
||||
button.connect("clicked", lambda button: dialogs.remove_attached_file(self, button))
|
||||
self.attachment_container.append(button)
|
||||
self.attachment_box.set_visible(True)
|
||||
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
GtkSource.init()
|
||||
if not os.path.exists(os.path.join(self.data_dir, "chats")):
|
||||
os.makedirs(os.path.join(self.data_dir, "chats"))
|
||||
if os.path.exists(os.path.join(self.config_dir, "chats.json")) and not os.path.exists(os.path.join(self.data_dir, "chats", "chats.json")):
|
||||
update_history.update(self)
|
||||
self.set_help_overlay(self.shortcut_window)
|
||||
self.get_application().set_accels_for_action("win.show-help-overlay", ['<primary>slash'])
|
||||
self.get_application().create_action('new_chat', lambda *_: self.new_chat(), ['<primary>n'])
|
||||
@@ -994,6 +1176,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
self.get_application().create_action('import_chat', lambda *_: self.import_chat())
|
||||
self.get_application().create_action('create_model_from_existing', lambda *_: dialogs.create_model_from_existing(self))
|
||||
self.get_application().create_action('create_model_from_file', lambda *_: dialogs.create_model_from_file(self))
|
||||
self.get_application().create_action('attach_image', lambda *_: dialogs.attach_file(self, self.file_filter_image, "image"))
|
||||
self.get_application().create_action('attach_plain_text', lambda *_: dialogs.attach_file(self, self.file_filter_text, "plain_text"))
|
||||
self.add_chat_button.connect("clicked", lambda button : self.new_chat())
|
||||
|
||||
self.create_model_name.get_delegate().connect("insert-text", self.check_alphanumeric)
|
||||
@@ -1012,6 +1196,16 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
self.temperature_spin.set_value(self.model_tweaks['temperature'])
|
||||
self.seed_spin.set_value(self.model_tweaks['seed'])
|
||||
self.keep_alive_spin.set_value(self.model_tweaks['keep_alive'])
|
||||
#Overrides
|
||||
if "ollama_overrides" in data: local_instance.overrides = data['ollama_overrides']
|
||||
for element in [
|
||||
self.override_HSA_OVERRIDE_GFX_VERSION,
|
||||
self.override_CUDA_VISIBLE_DEVICES,
|
||||
self.override_HIP_VISIBLE_DEVICES]:
|
||||
override = element.get_name()
|
||||
if override in local_instance.overrides:
|
||||
element.set_text(local_instance.overrides[override])
|
||||
|
||||
|
||||
self.background_switch.set_active(self.run_on_background)
|
||||
self.set_hide_on_close(self.run_on_background)
|
||||
|
||||
315
src/window.ui
315
src/window.ui
@@ -26,6 +26,7 @@
|
||||
<setter object="manage_models_dialog" property="width-request">360</setter>
|
||||
<setter object="create_model_dialog" property="width-request">360</setter>
|
||||
<setter object="preferences_dialog" property="width-request">360</setter>
|
||||
<setter object="file_preview_dialog" property="width-request">360</setter>
|
||||
</object>
|
||||
</child>
|
||||
<property name="content">
|
||||
@@ -163,92 +164,115 @@
|
||||
<object class="AdwClamp">
|
||||
<property name="maximum-size">1000</property>
|
||||
<property name="tightening-threshold">800</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">0</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>
|
||||
<child>
|
||||
<object class="GtkButton" id="image_button">
|
||||
<signal name="clicked" handler="open_image"/>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="sensitive">false</property>
|
||||
<property name="tooltip-text" translatable="yes">Only available on selected models</property>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">image-x-generic-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<style>
|
||||
<class name="card"/>
|
||||
<class name="message-input"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<style>
|
||||
<class name="message-input"/>
|
||||
<class name="undershoot-bottom"/>
|
||||
<class name="undershoot-top"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">1</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>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="attachment_box">
|
||||
<property name="visible">false</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="attachment_container">
|
||||
<property name="orientation">0</property>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="spacing">12</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">0</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="attachment_button">
|
||||
<property name="menu-model">attachment_menu</property>
|
||||
<property name="direction">0</property>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="tooltip-text" translatable="yes">Attach file</property>
|
||||
<style>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">chain-link-loose-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<style>
|
||||
<class name="card"/>
|
||||
<class name="message-input"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<style>
|
||||
<class name="message-input"/>
|
||||
<class name="undershoot-bottom"/>
|
||||
<class name="undershoot-top"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="GtkTextView" id="message_text_view">
|
||||
<property name="wrap-mode">word</property>
|
||||
<property name="top-margin">6</property>
|
||||
<property name="bottom-margin">6</property>
|
||||
<property name="hexpand">true</property>
|
||||
<style>
|
||||
<class name="message-input"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTextView" id="message_text_view">
|
||||
<property name="wrap-mode">word</property>
|
||||
<property name="top-margin">6</property>
|
||||
<property name="bottom-margin">6</property>
|
||||
<property name="hexpand">true</property>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<signal name="clicked" handler="send_message"/>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="tooltip-text" translatable="yes">Send message</property>
|
||||
<style>
|
||||
<class name="message-input"/>
|
||||
<class name="suggested-action"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">paper-plane-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="stop_button">
|
||||
<signal name="clicked" handler="stop_message"/>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="visible">false</property>
|
||||
<style>
|
||||
<class name="destructive-action"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">media-playback-stop-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<signal name="clicked" handler="send_message"/>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">paper-plane-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="stop_button">
|
||||
<signal name="clicked" handler="stop_message"/>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="visible">false</property>
|
||||
<style>
|
||||
<class name="destructive-action"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<child>
|
||||
<object class="AdwButtonContent">
|
||||
<property name="icon-name">media-playback-stop-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
@@ -302,7 +326,7 @@
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesPage" id="model_page">
|
||||
<property name="title" translatable="yes">Advanced Model Settings</property>
|
||||
<property name="title" translatable="yes">Model</property>
|
||||
<property name="icon-name">preferences-other-symbolic</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
@@ -358,6 +382,78 @@
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwPreferencesPage" id="instance_page">
|
||||
<property name="title" translatable="yes">Ollama Instance</property>
|
||||
<property name="icon-name">brain-augemnted-symbolic</property>
|
||||
<child>
|
||||
<object class="AdwPreferencesGroup">
|
||||
<property name="title" translatable="yes">Ollama Overrides</property>
|
||||
<property name="description" translatable="yes">Manage the arguments used on Ollama, any changes on this page only applies to the integrated instance, the instance will restart if you make changes</property>
|
||||
<child>
|
||||
<object class="AdwEntryRow" id="override_HSA_OVERRIDE_GFX_VERSION">
|
||||
<signal name="apply" handler="override_changed"/>
|
||||
<property name="name">HSA_OVERRIDE_GFX_VERSION</property>
|
||||
<property name="title" translatable="no">HSA_OVERRIDE_GFX_VERSION</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
<child type="prefix">
|
||||
<object class="GtkButton">
|
||||
<signal name="clicked" handler="link_button_handler"/>
|
||||
<property name="name">https://github.com/ollama/ollama/blob/main/docs/gpu.md#overrides</property>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="icon-name">globe-symbolic</property>
|
||||
<style>
|
||||
<class name="success"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwEntryRow" id="override_CUDA_VISIBLE_DEVICES">
|
||||
<signal name="apply" handler="override_changed"/>
|
||||
<property name="name">CUDA_VISIBLE_DEVICES</property>
|
||||
<property name="title" translatable="no">CUDA_VISIBLE_DEVICES</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
<child type="prefix">
|
||||
<object class="GtkButton">
|
||||
<signal name="clicked" handler="link_button_handler"/>
|
||||
<property name="name">https://github.com/ollama/ollama/blob/main/docs/gpu.md#gpu-selection</property>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="icon-name">globe-symbolic</property>
|
||||
<style>
|
||||
<class name="success"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwEntryRow" id="override_HIP_VISIBLE_DEVICES">
|
||||
<signal name="apply" handler="override_changed"/>
|
||||
<property name="name">HIP_VISIBLE_DEVICES</property>
|
||||
<property name="title" translatable="no">HIP_VISIBLE_DEVICES</property>
|
||||
<property name="show-apply-button">true</property>
|
||||
<child type="prefix">
|
||||
<object class="GtkButton">
|
||||
<signal name="clicked" handler="link_button_handler"/>
|
||||
<property name="name">https://github.com/ollama/ollama/blob/main/docs/gpu.md#gpu-selection-1</property>
|
||||
<property name="vexpand">false</property>
|
||||
<property name="valign">3</property>
|
||||
<property name="icon-name">globe-symbolic</property>
|
||||
<style>
|
||||
<class name="success"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
<object class="AdwDialog" id="create_model_dialog">
|
||||
@@ -555,6 +651,37 @@
|
||||
</child>
|
||||
</object>
|
||||
|
||||
<object class="AdwDialog" id="file_preview_dialog">
|
||||
<property name="can-close">true</property>
|
||||
<property name="width-request">450</property>
|
||||
<property name="height-request">450</property>
|
||||
<child>
|
||||
<object class="AdwToolbarView">
|
||||
<child type="top">
|
||||
<object class="AdwHeaderBar">
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</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>
|
||||
<child>
|
||||
<object class="GtkTextView" id="file_preview_text_view">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="vexpand">true</property>
|
||||
<property name="editable">false</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
<object class="AdwDialog" id="welcome_dialog">
|
||||
<property name="can-close">false</property>
|
||||
<property name="width-request">450</property>
|
||||
@@ -776,6 +903,32 @@
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="attachment_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Plain text file</attribute>
|
||||
<attribute name="action">app.attach_plain_text</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Image</attribute>
|
||||
<attribute name="action">app.attach_image</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<object class="GtkFileFilter" id="file_filter_text">
|
||||
<suffixes>
|
||||
<suffix></suffix>
|
||||
<suffix>txt</suffix>
|
||||
<suffix>md</suffix>
|
||||
<suffix>html</suffix>
|
||||
<suffix>css</suffix>
|
||||
<suffix>js</suffix>
|
||||
<suffix>py</suffix>
|
||||
<suffix>java</suffix>
|
||||
<suffix>json</suffix>
|
||||
<suffix>xml</suffix>
|
||||
</suffixes>
|
||||
</object>
|
||||
<object class="GtkFileFilter" id="file_filter_image">
|
||||
<mime-types>
|
||||
<mime-type>image/svg+xml</mime-type>
|
||||
@@ -785,9 +938,9 @@
|
||||
<mime-type>image/gif</mime-type>
|
||||
</mime-types>
|
||||
</object>
|
||||
<object class="GtkFileFilter" id="file_filter_json">
|
||||
<object class="GtkFileFilter" id="file_filter_tar">
|
||||
<mime-types>
|
||||
<mime-type>application/json</mime-type>
|
||||
<mime-type>application/x-tar</mime-type>
|
||||
</mime-types>
|
||||
</object>
|
||||
<object class="GtkFileFilter" id="file_filter_gguf">
|
||||
|
||||
Reference in New Issue
Block a user