24 Commits
0.9.2 ... 0.9.4

Author SHA1 Message Date
jeffser
a4d26b2bda Added feature to description 2024-06-04 14:15:50 -06:00
jeffser
d7961f2510 Last minute fix 2024-06-04 14:14:09 -06:00
jeffser
45f5214ec9 Preparing for 0.9.4 (again) 2024-06-04 13:49:25 -06:00
jeffser
5e1e770aee Preparing for 0.9.4 2024-06-04 13:46:15 -06:00
jeffser
5eb18d00d3 Fix export 2024-06-04 13:39:50 -06:00
jeffser
bcbfd44e1f Fixed overrides 2024-06-04 13:34:48 -06:00
jeffser
073c619d89 New system for override and new overrides 2024-06-04 12:55:31 -06:00
jeffser
71ab8cfba4 Remove old file 2024-06-04 12:13:48 -06:00
jeffser
0ec812099c Update readme 2024-06-04 12:11:36 -06:00
jeffser
f7f05a0538 Rewrote a lot of code, new chat system and file uploading! 2024-06-04 12:07:15 -06:00
jeffser
82a0ab0d9e Preserve changes to override and added link button 2024-06-03 17:01:27 -06:00
jeffser
60b24da482 Fixed mistakes on readme 2024-06-03 16:37:03 -06:00
jeffser
66d209e4c6 New tip 2024-06-03 16:34:35 -06:00
jeffser
0c9ab4e17e Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-06-03 16:31:09 -06:00
jeffser
fff3a68b29 Added overrides system to Ollama instance 2024-06-03 16:31:03 -06:00
Louis Chauvet-Villaret
580a104894 the regular french update (baguette) (#87) 2024-06-03 15:51:44 -06:00
jeffser
3fe9b6e121 Changed sha256 for the new ones 2024-06-01 00:29:45 -06:00
jeffser
6979d1a775 Last minute fix to width of new model creation dialog 2024-06-01 00:24:56 -06:00
jeffser
90385ce461 Preparing for 0.9.3 2024-06-01 00:16:11 -06:00
jeffser
c579e6ec99 Custom models support! 2024-06-01 00:07:34 -06:00
jeffser
b8d93cfd17 New icon (document-open-symbolic) 2024-05-31 10:29:18 -06:00
jeffser
359f28f5c1 Manually added all icons so that they work on desktops that don't use Adwaita 2024-05-31 10:17:10 -06:00
jeffser
6e8bff8493 Disabled Gnome Search (for now) 2024-05-31 09:59:04 -06:00
jeffser
ad25d5a50f Fixed: Ollama instance wasn't shutting down 2024-05-30 19:15:19 -06:00
40 changed files with 3794 additions and 2092 deletions

View File

@@ -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 - Talk to multiple models in the same conversation
- Pull and delete models from the app - Pull and delete models from the app
- Image recognition - Image recognition
- Document recognition (plain text files)
- Code highlighting - Code highlighting
- Multiple conversations - Multiple conversations
- Notifications - Notifications
@@ -22,13 +23,12 @@ Alpaca is an [Ollama](https://github.com/ollama/ollama) client where you can man
- Delete messages - Delete messages
## Future features! ## 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) - YouTube recognition (Ask questions about a YouTube video using the transcript)
- Edit messages - Edit messages
- Snap Package (maybe) - Snap Package (maybe)
## Screenies ## Screenies
Login to Ollama instance | Chatting with models | Managing models Code highlighting | Chatting with models | Managing models
:-------------------------:|:-------------------------:|:-------------------------: :-------------------------:|:-------------------------:|:-------------------------:
![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) ![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)
@@ -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`. 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 ### 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 ### 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. 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 ## Thanks

View File

@@ -80,16 +80,16 @@
"sources": [ "sources": [
{ {
"type": "file", "type": "file",
"url": "https://github.com/ollama/ollama/releases/download/v0.1.38/ollama-linux-amd64", "url": "https://github.com/ollama/ollama/releases/download/v0.1.39/ollama-linux-amd64",
"sha256": "c3360812503a9756a507ebb9d78667e2b21800a760b45046bc48a8f3b81972f4", "sha256": "4d19be355842a6297c93ab5ada139fe396126736bf3c3882a879dc245dffe3af",
"only-arches": [ "only-arches": [
"x86_64" "x86_64"
] ]
}, },
{ {
"type": "file", "type": "file",
"url": "https://github.com/ollama/ollama/releases/download/v0.1.38/ollama-linux-arm64", "url": "https://github.com/ollama/ollama/releases/download/v0.1.39/ollama-linux-arm64",
"sha256": "f2d091afe665b2d5ba8b68e2473d36cdfaf80c61c7d2844a0a8f533c4e62f547", "sha256": "60ec2d36928c11d6c6d84fe91451308a46aafaedbdba44664e5a6080009ce097",
"only-arches": [ "only-arches": [
"aarch64" "aarch64"
] ]

View File

@@ -15,6 +15,7 @@
<li>Pull and delete models from the app</li> <li>Pull and delete models from the app</li>
<li>Have multiple conversations</li> <li>Have multiple conversations</li>
<li>Image recognition (Only available with compatible models)</li> <li>Image recognition (Only available with compatible models)</li>
<li>Plain text documents recognition</li>
<li>Import and export chats</li> <li>Import and export chats</li>
</ul> </ul>
<p>Disclaimer</p> <p>Disclaimer</p>
@@ -63,6 +64,30 @@
<url type="homepage">https://github.com/Jeffser/Alpaca</url> <url type="homepage">https://github.com/Jeffser/Alpaca</url>
<url type="donation">https://github.com/sponsors/Jeffser</url> <url type="donation">https://github.com/sponsors/Jeffser</url>
<releases> <releases>
<release version="0.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>
<p>Big Update</p>
<ul>
<li>Added: Support for GGUF models (experimental)</li>
<li>Added: Support for customization and creation of models</li>
<li>Fixed: Icons don't appear on non Gnome systems</li>
<li>Update Ollama to v0.1.39</li>
</ul>
</description>
</release>
<release version="0.9.2" date="2024-05-30"> <release version="0.9.2" date="2024-05-30">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.9.2</url> <url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.9.2</url>
<description> <description>

View File

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

File diff suppressed because it is too large Load Diff

584
po/es.po

File diff suppressed because it is too large Load Diff

595
po/es.po~

File diff suppressed because it is too large Load Diff

583
po/fr.po

File diff suppressed because it is too large Load Diff

944
po/fr.po~

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,24 @@
<file alias="icons/scalable/status/library-symbolic.svg">icons/library-symbolic.svg</file> <file alias="icons/scalable/status/library-symbolic.svg">icons/library-symbolic.svg</file>
<file alias="icons/scalable/status/paper-plane-symbolic.svg">icons/paper-plane-symbolic.svg</file> <file alias="icons/scalable/status/paper-plane-symbolic.svg">icons/paper-plane-symbolic.svg</file>
<file alias="icons/scalable/status/globe-symbolic.svg">icons/globe-symbolic.svg</file> <file alias="icons/scalable/status/globe-symbolic.svg">icons/globe-symbolic.svg</file>
<file alias="icons/scalable/status/controls-big-symbolic.svg">icons/controls-big-symbolic.svg</file> <file alias="icons/scalable/status/chat-message-new-symbolic.svg">icons/chat-message-new-symbolic.svg</file>
<file alias="icons/scalable/status/dialog-warning-symbolic.svg">icons/dialog-warning-symbolic.svg</file>
<file alias="icons/scalable/status/document-edit-symbolic.svg">icons/document-edit-symbolic.svg</file>
<file alias="icons/scalable/status/edit-copy-symbolic.svg">icons/edit-copy-symbolic.svg</file>
<file alias="icons/scalable/status/folder-download-symbolic.svg">icons/folder-download-symbolic.svg</file>
<file alias="icons/scalable/status/image-x-generic-symbolic.svg">icons/image-x-generic-symbolic.svg</file>
<file alias="icons/scalable/status/media-playback-stop-symbolic.svg">icons/media-playback-stop-symbolic.svg</file>
<file alias="icons/scalable/status/open-menu-symbolicsvg">icons/open-menu-symbolic.svg</file>
<file alias="icons/scalable/status/preferences-other-symbolic.svg">icons/preferences-other-symbolic.svg</file>
<file alias="icons/scalable/status/preferences-system-symbolic.svg">icons/preferences-system-symbolic.svg</file>
<file alias="icons/scalable/status/sidebar-show-symbolic.svg">icons/sidebar-show-symbolic.svg</file>
<file alias="icons/scalable/status/user-trash-symbolic.svg">icons/user-trash-symbolic.svg</file>
<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">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file> <file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource> </gresource>

View File

@@ -16,9 +16,15 @@ class SearchProvider:
return return
print("ALPACA D-Bus connection obtained") print("ALPACA D-Bus connection obtained")
interface_info = self.get_interface_info() interface_info = self.get_interface_info()
self.registration_id = self.connection.register_object(DBUS_OBJECT_PATH, interface_info, self.handle_method_call, None)
self.registration_id = self.connection.register_object(
DBUS_OBJECT_PATH,
interface_info,
self.handle_method_call,
None,
None
)
if self.registration_id > 0: if self.registration_id > 0:
print(f"ALPACA Object registered with ID: {self.registration_id}") print(f"ALPACA Object registered with ID: {self.registration_id}")
@@ -28,28 +34,38 @@ class SearchProvider:
def get_interface_info(self): def get_interface_info(self):
print("ALPACA get_interface_info") print("ALPACA get_interface_info")
xml = """ xml = """
<node> <node>
<interface name='org.gnome.Shell.SearchProvider2'> <interface name="org.gnome.Shell.SearchProvider2">
<method name='GetInitialResultSet'>
<arg type='as' name='terms' direction='in'/> <method name="GetInitialResultSet">
<arg type='as' name='results' direction='out'/> <arg type="as" name="terms" direction="in" />
</method> <arg type="as" name="results" direction="out" />
<method name='GetSubsearchResultSet'> </method>
<arg type='as' name='previous_results' direction='in'/>
<arg type='as' name='terms' direction='in'/> <method name="GetSubsearchResultSet">
<arg type='as' name='results' direction='out'/> <arg type="as" name="previous_results" direction="in" />
</method> <arg type="as" name="terms" direction="in" />
<method name='GetResultMetas'> <arg type="as" name="results" direction="out" />
<arg type='as' name='identifiers' direction='in'/> </method>
<arg type='a{sv}' name='metas' direction='out'/>
</method> <method name="GetResultMetas">
<method name='ActivateResult'> <arg type="as" name="identifiers" direction="in" />
<arg type='s' name='identifier' direction='in'/> <arg type="aa{sv}" name="metas" direction="out" />
<arg type='as' name='terms' direction='in'/> </method>
<arg type='i' name='timestamp' direction='in'/>
</method> <method name="ActivateResult">
</interface> <arg type="s" name="identifier" direction="in" />
</node> <arg type="as" name="terms" direction="in" />
<arg type="u" name="timestamp" direction="in" />
</method>
<method name="LaunchSearch">
<arg type="as" name="terms" direction="in" />
<arg type="u" name="timestamp" direction="in" />
</method>
</interface>
</node>
""" """
return Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0] return Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0]

View File

@@ -13,6 +13,19 @@ def simple_get(connection_url:str) -> dict:
except Exception as e: except Exception as e:
return {"status": "error", "status_code": 0} return {"status": "error", "status_code": 0}
def simple_post(connection_url:str, data) -> dict:
try:
headers = {
"Content-Type": "application/json"
}
response = requests.post(connection_url, headers=headers, data=data, stream=False)
if response.status_code == 200:
return {"status": "ok", "text": response.text, "status_code": response.status_code}
else:
return {"status": "error", "status_code": response.status_code}
except Exception as e:
return {"status": "error", "status_code": 0}
def simple_delete(connection_url:str, data) -> dict: def simple_delete(connection_url:str, data) -> dict:
try: try:
response = requests.delete(connection_url, json=data) response = requests.delete(connection_url, json=data)
@@ -38,4 +51,3 @@ def stream_post(connection_url:str, data, callback:callable) -> dict:
return {"status": "error", "status_code": response.status_code} return {"status": "error", "status_code": response.status_code}
except Exception as e: except Exception as e:
return {"status": "error", "status_code": 0} return {"status": "error", "status_code": 0}

View File

@@ -177,14 +177,14 @@ def pull_model(self, model_name):
# REMOVE IMAGE | WORKS # 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': 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( dialog = Adw.AlertDialog(
heading=_("Remove Image"), heading=_("Remove File"),
body=_("Are you sure you want to remove image?"), body=_("Are you sure you want to remove file?"),
close_response="cancel" close_response="cancel"
) )
dialog.add_response("cancel", _("Cancel")) dialog.add_response("cancel", _("Cancel"))
@@ -193,10 +193,10 @@ def remove_image(self):
dialog.choose( dialog.choose(
parent = self, parent = self,
cancellable = None, 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 | # RECONNECT REMOTE | WORKS
def reconnect_remote_response(self, dialog, task, entry): def reconnect_remote_response(self, dialog, task, entry):
response = dialog.choose_finish(task) response = dialog.choose_finish(task)
@@ -227,3 +227,58 @@ def reconnect_remote(self, current_url):
cancellable = None, cancellable = None,
callback = lambda dialog, task, entry=entry: reconnect_remote_response(self, dialog, task, entry) callback = lambda dialog, task, entry=entry: reconnect_remote_response(self, dialog, task, entry)
) )
# CREATE MODEL | WORKS
def create_model_from_existing_response(self, dialog, task, dropdown):
model = dropdown.get_selected_item().get_string()
if dialog.choose_finish(task) == 'accept' and model:
self.create_model(model, False)
def create_model_from_existing(self):
string_list = Gtk.StringList()
for model in self.local_models:
string_list.append(model)
dropdown = Gtk.DropDown()
dropdown.set_model(string_list)
dialog = Adw.AlertDialog(
heading=_("Select Model"),
body=_("This model will be used as the base for the new model"),
extra_child=dropdown
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, dropdown=dropdown: create_model_from_existing_response(self, dialog, task, dropdown)
)
def create_model_from_file_response(self, file_dialog, result):
try: file = file_dialog.open_finish(result)
except: return
try:
self.create_model(file.get_path(), True)
except Exception as e:
self.show_toast("error", 5, self.main_overlay) ##TODO NEW ERROR MESSAGE
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))

View 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

View 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

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 6 c 0 1.644531 1.355469 3 3 3 h 1 v 3 l 3 -3 v -1 c 0 -0.550781 -0.449219 -1 -1 -1 h -3 c -0.570312 0 -1 -0.429688 -1 -1 v -6 c 0 -0.554688 0.445312 -1 1 -1 h 10 c 0.554688 0 1 0.445312 1 1 v 4 c 0 0.550781 0.449219 1 1 1 s 1 -0.449219 1 -1 v -4 c 0 -1.644531 -1.355469 -3 -3 -3 z m 8 7 v 3 h -3 v 2 h 3 v 3 h 2 v -3 h 3 v -2 h -3 v -3 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 548 B

View File

@@ -1,2 +0,0 @@
<?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 4.550781 1 c -1.9375 0 -3.5 1.5625 -3.5 3.5 s 1.5625 3.5 3.5 3.5 h 7 c 1.941407 0 3.5 -1.5625 3.5 -3.5 s -1.558593 -3.5 -3.5 -3.5 z m 7 1 c 1.386719 0 2.5 1.113281 2.5 2.5 c 0 1.382812 -1.113281 2.5 -2.5 2.5 c -1.382812 0 -2.5 -1.117188 -2.5 -2.5 c 0 -1.386719 1.117188 -2.5 2.5 -2.5 z m 0 0"/><path d="m 4.550781 9 c -1.9375 0 -3.5 1.5625 -3.5 3.5 s 1.5625 3.5 3.5 3.5 h 7 c 1.941407 0 3.5 -1.5625 3.5 -3.5 s -1.558593 -3.5 -3.5 -3.5 z m 0 1 c 1.386719 0 2.5 1.113281 2.5 2.5 c 0 1.382812 -1.113281 2.5 -2.5 2.5 c -1.382812 0 -2.5 -1.117188 -2.5 -2.5 c 0 -1.386719 1.117188 -2.5 2.5 -2.5 z m 0 0" fill-opacity="0.35"/></g></svg>

Before

Width:  |  Height:  |  Size: 785 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7.90625 0.09375 c -0.527344 -0.0273438 -1.039062 0.28125 -1.4375 0.96875 l -6.25 11.59375 c -0.535156 0.964844 0.046875 2.34375 1.09375 2.34375 h 13.15625 c 0.980469 0 1.902344 -1.160156 1.21875 -2.34375 l -6.3125 -11.53125 c -0.398438 -0.644531 -0.941406 -1.003906 -1.46875 -1.03125 z m 1.09375 3.90625 v 5 c 0.007812 0.527344 -0.472656 1 -1 1 s -1.007812 -0.472656 -1 -1 v -5 z m -1 7 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 s -1 -0.449219 -1 -1 s 0.449219 -1 1 -1 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 12.277344 0.832031 c -0.578125 0.007813 -1.167969 0.230469 -1.691406 0.753907 l -9 9 c -0.375 0.375 -0.585938 0.882812 -0.585938 1.414062 v 3 h 3 c 0.53125 0 1.039062 -0.210938 1.414062 -0.585938 l 9 -9 c 1.789063 -1.789062 0.082032 -4.390624 -1.890624 -4.570312 c -0.082032 -0.011719 -0.164063 -0.011719 -0.246094 -0.011719 z m -1.777344 3.605469 l 1.0625 1.0625 l -7.0625 7.0625 l -1.0625 -1.0625 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 8 c 0 1.644531 1.355469 3 3 3 h 8.882812 c 0.832032 0 1.578126 -0.402344 2.054688 -0.9375 c 0.472656 -0.53125 0.738281 -1.167969 0.910156 -1.800781 l 0.972656 -2.609375 c 0.390626 -1.449219 -0.09375 -2.652344 -0.820312 -3.167969 c -0.484375 -0.34375 -0.714844 -0.292969 -1 -0.324219 v -1.160156 c 0 -0.855469 -0.558594 -1.589844 -1.09375 -1.828125 c -0.53125 -0.238281 -1.011719 -0.167969 -1.011719 -0.167969 l 0.105469 -0.003906 h -3.585938 l -1.707031 -1.707031 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 2 h 2.585938 l 1.707031 1.707031 c 0.1875 0.1875 0.441406 0.292969 0.707031 0.292969 h 4 c 0.035156 0 0.070312 -0.003906 0.105469 -0.007812 c 0 0 0.019531 0.019531 -0.011719 0.003906 c -0.035156 -0.011719 -0.09375 -0.25 -0.09375 0.003906 v 2 c 0 0.550781 0.449219 1 1 1 c 1 0 1.046875 0.703125 0.886719 1.128906 l -0.972657 2.609375 c -0.117187 0.4375 -0.296874 0.800781 -0.472656 0.996094 c -0.175781 0.199219 -0.285156 0.265625 -0.558594 0.265625 h -8.882812 c -0.570312 0 -1 -0.429688 -1 -1 v -8 c 0 -0.570312 0.46875 -0.792969 1 -1 z m 0 0"/>
<path d="m 7 6 l 0.042969 0.003906 c -0.914063 -0.042968 -1.75 0.390625 -2.195313 0.96875 c -0.710937 1.222656 -1.15625 2.277344 -1.800781 3.71875 c -0.171875 0.523438 0.117187 1.089844 0.640625 1.261719 c 0.527344 0.171875 1.09375 -0.117187 1.261719 -0.640625 c 0.488281 -1.011719 0.921875 -1.816406 1.339843 -2.808594 c 0.210938 -0.503906 0.703126 -0.492187 0.898438 -0.503906 h 5.8125 c 0.550781 0 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View 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

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 0 3 c 0 -1.644531 1.355469 -3 3 -3 h 5 c 1.644531 0 3 1.355469 3 3 c 0 0.550781 -0.449219 1 -1 1 s -1 -0.449219 -1 -1 c 0 -0.570312 -0.429688 -1 -1 -1 h -5 c -0.570312 0 -1 0.429688 -1 1 v 5 c 0 0.570312 0.429688 1 1 1 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 c -1.644531 0 -3 -1.355469 -3 -3 z m 5 5 c 0 -1.644531 1.355469 -3 3 -3 h 5 c 1.644531 0 3 1.355469 3 3 v 5 c 0 1.644531 -1.355469 3 -3 3 h -5 c -1.644531 0 -3 -1.355469 -3 -3 z m 2 0 v 5 c 0 0.570312 0.429688 1 1 1 h 5 c 0.570312 0 1 -0.429688 1 -1 v -5 c 0 -0.570312 -0.429688 -1 -1 -1 h -5 c -0.570312 0 -1 0.429688 -1 1 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 765 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 8 0 c -0.550781 0 -1 0.449219 -1 1 v 8.585938 l -2.292969 -2.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 4 4 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 l 4 -4 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 s -1.023437 -0.390625 -1.414062 0 l -2.292969 2.292969 v -8.585938 c 0 -0.550781 -0.449219 -1 -1 -1 z m -6 14 v 2 h 12 v -2 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 630 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 6 5 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 s 1 -0.449219 1 -1 s -0.449219 -1 -1 -1 z m 3.5 3 l -2 2 l -1.5 -1 l -2 2 v 0.5 c 0 0.5 0.5 0.5 0.5 0.5 h 7 s 0.472656 -0.035156 0.5 -0.5 v -1 z m 0 0"/>
<path d="m 4 1 c -1.644531 0 -3 1.355469 -3 3 v 8 c 0 1.644531 1.355469 3 3 3 h 8 c 1.644531 0 3 -1.355469 3 -3 v -8 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 8 c 0.570312 0 1 0.429688 1 1 v 8 c 0 0.570312 -0.429688 1 -1 1 h -8 c -0.570312 0 -1 -0.429688 -1 -1 v -8 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 708 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7 1 v 6 h -6 v 2 h 6 v 6 h 2 v -6 h 6 v -2 h -6 v -6 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 3.5 2 h 9 c 0.828125 0 1.5 0.671875 1.5 1.5 v 9 c 0 0.828125 -0.671875 1.5 -1.5 1.5 h -9 c -0.828125 0 -1.5 -0.671875 -1.5 -1.5 v -9 c 0 -0.828125 0.671875 -1.5 1.5 -1.5 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 1 2 h 14 v 2 h -14 z m 0 0"/>
<path d="m 1 7 h 14 v 2 h -14 z m 0 0"/>
<path d="m 1 12 h 14 v 2 h -14 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 12.5 0.0234375 c -0.292969 -0.0039063 -0.59375 0.0195313 -0.882812 0.0937505 c -1.867188 0.488281 -2.984376 2.382812 -2.496094 4.234374 c 0.101562 0.386719 0.289062 0.742188 0.5 1.054688 c -0.109375 0.015625 -1.625 2.59375 -1.625 2.59375 l 3.410156 0.023438 l 0.800781 -1.117188 c 0.378907 0.03125 0.78125 0.023438 1.171875 -0.078125 c 1.863282 -0.484375 2.980469 -2.382813 2.496094 -4.234375 c -0.105469 -0.386719 -0.292969 -0.738281 -0.503906 -1.054688 l -1.359375 2.304688 c -0.28125 0.476562 -0.882813 0.632812 -1.355469 0.355469 l -0.875 -0.507813 c -0.476562 -0.277344 -0.636719 -0.875 -0.355469 -1.347656 l 1.363281 -2.3085938 c -0.097656 -0.0039062 -0.195312 -0.0117187 -0.289062 -0.0117187 z m -8.875 0.6914065 c -0.011719 0 -0.019531 0 -0.027344 0.003906 c -0.230468 0.046875 -0.445312 0.132812 -0.65625 0.21875 c -0.019531 0.625 0.085938 1.441406 -0.15625 1.65625 c -0.242187 0.210938 -1.039062 0.035156 -1.652344 -0.03125 c -0.132812 0.238281 -0.238281 0.484375 -0.3125 0.75 c 0.476563 0.394531 1.136719 0.839844 1.152344 1.15625 c 0.015625 0.320312 -0.625 0.804688 -1.058594 1.25 c 0.101563 0.257812 0.25 0.492188 0.40625 0.71875 c 0.605469 -0.125 1.359376 -0.375 1.621094 -0.1875 c 0.261719 0.1875 0.261719 1.003906 0.34375 1.625 c 0.246094 0.074219 0.507813 0.105469 0.777344 0.125 c 0.28125 -0.554688 0.566406 -1.320312 0.875 -1.40625 c 0.316406 -0.089844 0.960938 0.457031 1.5 0.78125 c 0.214844 -0.152344 0.410156 -0.335938 0.589844 -0.53125 c -0.253906 -0.574219 -0.71875 -1.292969 -0.589844 -1.59375 c 0.125 -0.300781 0.96875 -0.46875 1.558594 -0.6875 c 0.003906 -0.074219 0.03125 -0.144531 0.03125 -0.21875 c 0 -0.191406 -0.035156 -0.378906 -0.0625 -0.5625 c -0.605469 -0.160156 -1.464844 -0.242188 -1.621094 -0.53125 c -0.160156 -0.289062 0.238281 -1.0625 0.433594 -1.65625 c -0.199219 -0.179688 -0.417969 -0.335938 -0.652344 -0.46875 c -0.503906 0.375 -1.085938 0.996094 -1.40625 0.9375 c -0.316406 -0.054688 -0.660156 -0.820312 -0.996094 -1.34375 c -0.03125 0.003906 -0.066406 0 -0.097656 -0.003906 z m 0.59375 1.878906 c 0.929688 0 1.6875 0.753906 1.6875 1.6875 s -0.757812 1.6875 -1.6875 1.6875 s -1.683594 -0.753906 -1.683594 -1.6875 s 0.753906 -1.6875 1.683594 -1.6875 z m 8.621094 6.332031 c -0.144532 0 -0.273438 0.023438 -0.359375 0.074219 h -9.050781 c -0.246094 -0.105469 -0.746094 -0.046875 -0.964844 0.179688 l -2.160156 2.113281 c -0.191407 0.175781 -0.30078175 0.484375 -0.304688 0.707031 c -0.0546875 0.886719 1 1.320312 1.636719 0.792969 l 1.363281 -1.332031 l 0.003906 3.539062 c 0 0.570312 0.457032 1 0.996094 1 h 7.992188 c 0.523437 0 0.996093 -0.445312 0.996093 -0.9375 l -0.003906 -3.601562 l 1.335937 1.308593 c 0.636719 0.527344 1.6875 0.09375 1.636719 -0.792969 c -0.003906 -0.222656 -0.113281 -0.53125 -0.300781 -0.703124 l -2.164062 -2.117188 c -0.144532 -0.152344 -0.417969 -0.226562 -0.652344 -0.230469 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 13.855469 0 l -1.539063 1.4375 c -0.453125 0.421875 -0.53125 1.148438 -0.269531 1.707031 l -5.886719 5.996094 c -0.011718 0 -0.019531 0 -0.03125 0 c -0.257812 -0.128906 -0.550781 -0.183594 -0.839844 -0.148437 c -0.328124 0.046874 -0.632812 0.199218 -0.867187 0.441406 l -3.945313 3.996094 c -0.3906245 0.375 -0.5468745 0.933593 -0.4062495 1.457031 c 0.1406255 0.523437 0.5546875 0.929687 1.0820315 1.058593 c 0.527344 0.132813 1.082031 -0.03125 1.453125 -0.425781 l 3.945312 -3.996093 c 0.472657 -0.453126 0.59375 -1.15625 0.296875 -1.738282 l 5.890625 -5.964844 c 0.558594 0.25 1.273438 0.148438 1.707031 -0.289062 l 1.414063 -1.5625 z m -10.308594 0.0898438 c -0.398437 0 -0.785156 0.0937502 -1.140625 0.2187502 l 1.882812 1.878906 c 0.390626 0.382812 0.390626 1 0 1.386719 l -0.710937 0.707031 c -0.386719 0.386719 -1 0.386719 -1.390625 0 l -1.882812 -1.878906 c -0.125 0.355468 -0.2187505 0.742187 -0.2187505 1.140625 c 0 1.90625 1.5507815 3.453125 3.4609375 3.453125 c 0.402344 0 0.789063 -0.09375 1.144531 -0.21875 l 1.175782 1.171875 h 0.058593 l 2.070313 -2.0625 l -1.203125 -1.203125 c 0.125 -0.359375 0.214843 -0.742188 0.214843 -1.140625 c 0 -1.90625 -1.546874 -3.4531252 -3.460937 -3.4531252 z m 6.550781 7.8906252 l -2.070312 2.066406 c 0.011718 0.027344 0.023437 0.058594 0.03125 0.089844 l 1.144531 1.140625 c -0.125 0.355468 -0.21875 0.742187 -0.21875 1.140625 c 0 1.902343 1.550781 3.449219 3.460937 3.449219 c 0.433594 0 0.855469 -0.101563 1.238282 -0.246094 l -2.007813 -2 c -0.386719 -0.386719 -0.386719 -1.035156 0 -1.417969 l 0.679688 -0.679687 c 0.195312 -0.191407 0.457031 -0.308594 0.710937 -0.308594 s 0.515625 0.117187 0.710938 0.308594 l 1.945312 1.941406 c 0.105469 -0.328125 0.183594 -0.683594 0.183594 -1.046875 c 0 -1.90625 -1.546875 -3.453125 -3.460938 -3.453125 c -0.398437 0 -0.785156 0.09375 -1.140624 0.21875 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 6.5 14 v -12 h -5 v 12 z m 0 0" fill-opacity="0.34902"/>
<path d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 8 c 0 1.644531 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355469 3 -3 v -8 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 10 c 0.570312 0 1 0.429688 1 1 v 8 c 0 0.570312 -0.429688 1 -1 1 h -10 c -0.570312 0 -1 -0.429688 -1 -1 v -8 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"/>
<path d="m 6 2 h 1 v 12 h -1 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<g fill="#2e3436">
<path d="m 1 3 h 14 c 0.550781 0 1 0.449219 1 1 s -0.449219 1 -1 1 h -14 c -0.550781 0 -1 -0.449219 -1 -1 s 0.449219 -1 1 -1 z m 0 0"/>
<path d="m 4 4 v -1.5 c 0 -1.386719 1.113281 -2.5 2.5 -2.5 h 2.980469 c 1.382812 0 2.5 1.113281 2.5 2.5 v 1.5 h -2 v -1.5 c 0 -0.269531 -0.230469 -0.5 -0.5 -0.5 h -2.980469 c -0.269531 0 -0.5 0.230469 -0.5 0.5 v 1.5 z m 0 0"/>
<path d="m 4 4 v 9 c 0 0.546875 0.453125 1 1 1 h 6 c 0.546875 0 1 -0.453125 1 -1 v -9 h 2 v 9 c 0 1.660156 -1.339844 3 -3 3 h -6 c -1.660156 0 -3 -1.339844 -3 -3 v -9 z m 0 0"/>
<path d="m 7 7 v 5 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -5 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 s 0.5 0.222656 0.5 0.5 z m 0 0"/>
<path d="m 10 7 v 5 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -5 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 s 0.5 0.222656 0.5 0.5 z m 0 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 7.996094 0 c -1.105469 0 -2 0.894531 -2 2 s 0.894531 2 2 2 c 1.101562 0 2 -0.894531 2 -2 s -0.898438 -2 -2 -2 z m 0 6 c -1.105469 0 -2 0.894531 -2 2 s 0.894531 2 2 2 c 1.101562 0 2 -0.894531 2 -2 s -0.898438 -2 -2 -2 z m 0 6 c -1.105469 0 -2 0.894531 -2 2 s 0.894531 2 2 2 c 1.101562 0 2 -0.894531 2 -2 s -0.898438 -2 -2 -2 z m 0 0" fill="#2e3436"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@@ -5,16 +5,25 @@ from time import sleep
instance = None instance = None
port = 11435 port = 11435
data_dir = os.getenv("XDG_DATA_HOME") data_dir = os.getenv("XDG_DATA_HOME")
overrides = {}
def start(): def start():
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...") print("Starting Alpaca's Ollama instance...")
sleep(1) sleep(1)
print("Started Alpaca's Ollama instance") print("Started Alpaca's Ollama instance")
def stop(): def stop():
if instance: instance.kill() global instance
print("Stopped Alpaca's Ollama instance") if instance:
instance.kill()
instance.wait()
instance = None
print("Stopped Alpaca's Ollama instance")
def reset(): def reset():
stop() stop()

View File

@@ -1,6 +1,6 @@
# main.py # main.py
# #
# Copyright 2024 Unknown # Copyright 2024 Jeffser
# #
# This program is free software: you can redistribute it and/or modify # 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 # 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_name='Alpaca',
application_icon='com.jeffser.Alpaca', application_icon='com.jeffser.Alpaca',
developer_name='Jeffry Samuel Eduarte Rojas', developer_name='Jeffry Samuel Eduarte Rojas',
version='0.9.2', version='0.9.4',
developers=['Jeffser https://jeffser.com'], developers=['Jeffser https://jeffser.com'],
designers=['Jeffser https://jeffser.com'], designers=['Jeffser https://jeffser.com'],
translator_credits='Alex K (Russian) https://github.com/alexkdeveloper\nJeffser (Spanish) https://jeffser.com\nDaimar Stein (Brazilian Portuguese) https://github.com/not-a-dev-stein\nLouis Chauvet-Villaret (French) https://github.com/loulou64490', 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',

View File

@@ -26,14 +26,14 @@ configure_file(
install_mode: 'r-xr-xr-x' install_mode: 'r-xr-xr-x'
) )
configure_file( #configure_file(
input: 'alpaca_search_provider.in', #input: 'alpaca_search_provider.in',
output: 'alpaca_search_provider', #output: 'alpaca_search_provider',
configuration: conf, #configuration: conf,
install: true, #install: true,
install_dir: get_option('bindir'), #install_dir: get_option('bindir'),
install_mode: 'r-xr-xr-x' #install_mode: 'r-xr-xr-x'
) #)
alpaca_sources = [ alpaca_sources = [
'__init__.py', '__init__.py',
@@ -42,7 +42,8 @@ alpaca_sources = [
'connection_handler.py', 'connection_handler.py',
'available_models.py', 'available_models.py',
'dialogs.py', 'dialogs.py',
'local_instance.py' 'local_instance.py',
'update_history.py'
] ]
install_data(alpaca_sources, install_dir: moduledir) install_data(alpaca_sources, install_dir: moduledir)

38
src/update_history.py Normal file
View File

@@ -0,0 +1,38 @@
# 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']:
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"))

View File

@@ -1,6 +1,6 @@
# window.py # window.py
# #
# Copyright 2024 Unknown # Copyright 2024 Jeffser
# #
# This program is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -21,18 +21,20 @@ import gi
gi.require_version('GtkSource', '5') gi.require_version('GtkSource', '5')
gi.require_version('GdkPixbuf', '2.0') gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf 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 time import sleep
from io import BytesIO from io import BytesIO
from PIL import Image from PIL import Image
from datetime import datetime from datetime import datetime
from .available_models import available_models 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') @Gtk.Template(resource_path='/com/jeffser/Alpaca/window.ui')
class AlpacaWindow(Adw.ApplicationWindow): class AlpacaWindow(Adw.ApplicationWindow):
config_dir = os.getenv("XDG_CONFIG_HOME") config_dir = os.getenv("XDG_CONFIG_HOME")
data_dir = os.getenv("XDG_DATA_HOME")
app_dir = os.getenv("FLATPAK_DEST") app_dir = os.getenv("FLATPAK_DEST")
cache_dir = os.getenv("XDG_CACHE_HOME")
__gtype_name__ = 'AlpacaWindow' __gtype_name__ = 'AlpacaWindow'
@@ -50,10 +52,20 @@ class AlpacaWindow(Adw.ApplicationWindow):
model_tweaks = {"temperature": 0.7, "seed": 0, "keep_alive": 5} model_tweaks = {"temperature": 0.7, "seed": 0, "keep_alive": 5}
local_models = [] local_models = []
pulling_models = {} pulling_models = {}
chats = {"chats": {_("New Chat"): {"messages": []}}, "selected_chat": "New Chat"} chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat"}
attached_image = {"path": None, "base64": None} 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 #Elements
create_model_base = Gtk.Template.Child()
create_model_name = Gtk.Template.Child()
create_model_system = Gtk.Template.Child()
create_model_template = Gtk.Template.Child()
create_model_dialog = Gtk.Template.Child()
temperature_spin = Gtk.Template.Child() temperature_spin = Gtk.Template.Child()
seed_spin = Gtk.Template.Child() seed_spin = Gtk.Template.Child()
keep_alive_spin = Gtk.Template.Child() keep_alive_spin = Gtk.Template.Child()
@@ -62,6 +74,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
bot_message : Gtk.TextBuffer = None bot_message : Gtk.TextBuffer = None
bot_message_box : Gtk.Box = None bot_message_box : Gtk.Box = None
bot_message_view : Gtk.TextView = 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_dialog = Gtk.Template.Child()
welcome_carousel = Gtk.Template.Child() welcome_carousel = Gtk.Template.Child()
welcome_previous_button = Gtk.Template.Child() welcome_previous_button = Gtk.Template.Child()
@@ -73,9 +87,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
message_text_view = Gtk.Template.Child() message_text_view = Gtk.Template.Child()
send_button = Gtk.Template.Child() send_button = Gtk.Template.Child()
stop_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_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_drop_down = Gtk.Template.Child()
model_string_list = Gtk.Template.Child() model_string_list = Gtk.Template.Child()
@@ -102,7 +120,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
_("Could not pull model"), _("Could not pull model"),
_("Cannot open image"), _("Cannot open image"),
_("Cannot delete chat because it's the only one left"), _("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": [ "info": [
_("Please select a model before chatting"), _("Please select a model before chatting"),
@@ -127,22 +146,19 @@ class AlpacaWindow(Adw.ApplicationWindow):
if self.model_drop_down.get_selected_item() == None: return True if self.model_drop_down.get_selected_item() == None: return True
selected = self.model_drop_down.get_selected_item().get_string().split(":")[0] selected = self.model_drop_down.get_selected_item().get_string().split(":")[0]
if selected in ['llava', 'bakllava', 'moondream', 'llava-llama3']: if selected in ['llava', 'bakllava', 'moondream', 'llava-llama3']:
self.image_button.set_sensitive(True) for name, content in self.attachments.items():
self.image_button.set_tooltip_text(_("Upload image")) if content['type'] == 'image':
content['button'].set_css_classes(["flat"])
return True return True
else: else:
self.image_button.set_sensitive(False) for name, content in self.attachments.items():
self.image_button.set_tooltip_text(_("Only available on selected models")) if content['type'] == 'image':
self.image_button.set_css_classes(["circular"]) content['button'].set_css_classes(["flat", "error"])
self.attached_image = {"path": None, "base64": None}
return False return False
@Gtk.Template.Callback() @Gtk.Template.Callback()
def stop_message(self, button=None): def stop_message(self, button=None):
if self.loading_spinner: self.chat_container.remove(self.loading_spinner) 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.toggle_ui_sensitive(True)
self.switch_send_stop_button() self.switch_send_stop_button()
self.bot_message = None self.bot_message = None
@@ -151,37 +167,59 @@ class AlpacaWindow(Adw.ApplicationWindow):
@Gtk.Template.Callback() @Gtk.Template.Callback()
def send_message(self, button=None): 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 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() current_model = self.model_drop_down.get_selected_item()
if current_model is None: if current_model is None:
self.show_toast("info", 0, self.main_overlay) self.show_toast("info", 0, self.main_overlay)
return 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") 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", "role": "user",
"model": "User", "model": "User",
"date": formated_datetime, "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) "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)
}) }
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 = { data = {
"model": current_model.get_string(), "model": current_model.get_string(),
"messages": self.chats["chats"][self.chats["selected_chat"]]["messages"], "messages": self.convert_history_to_ollama(),
"options": {"temperature": self.model_tweaks["temperature"], "seed": self.model_tweaks["seed"]}, "options": {"temperature": self.model_tweaks["temperature"], "seed": self.model_tweaks["seed"]},
"keep_alive": f"{self.model_tweaks['keep_alive']}m" "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.switch_send_stop_button()
self.toggle_ui_sensitive(False) 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.message_text_view.get_buffer().set_text("", 0)
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True) self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
self.chat_container.append(self.loading_spinner) 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() thread.start()
@Gtk.Template.Callback() @Gtk.Template.Callback()
@@ -208,22 +246,14 @@ class AlpacaWindow(Adw.ApplicationWindow):
if not self.verify_connection(): if not self.verify_connection():
self.connection_error() 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() @Gtk.Template.Callback()
def chat_changed(self, listbox, row): def chat_changed(self, listbox, row):
if row and row.get_name() != self.chats["selected_chat"]: if row and row.get_name() != self.chats["selected_chat"]:
self.chats["selected_chat"] = row.get_name() self.chats["selected_chat"] = row.get_name()
self.load_history_into_chat() 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()): 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) self.model_drop_down.set_selected(i)
break break
@@ -263,9 +293,93 @@ class AlpacaWindow(Adw.ApplicationWindow):
else: value = round(value, 1) else: value = round(value, 1)
if self.model_tweaks[spin.get_name()] is not None and self.model_tweaks[spin.get_name()] != value: if self.model_tweaks[spin.get_name()] is not None and self.model_tweaks[spin.get_name()] != value:
self.model_tweaks[spin.get_name()] = value self.model_tweaks[spin.get_name()] = value
print(self.model_tweaks)
self.save_server_config() self.save_server_config()
@Gtk.Template.Callback()
def create_model_start(self, button):
base = self.create_model_base.get_subtitle()
name = self.create_model_name.get_text()
system = self.create_model_system.get_text()
template = self.create_model_template.get_text()
if "/" in base:
modelfile = f"FROM {base}\nSYSTEM {system}\nTEMPLATE {template}"
else:
modelfile = f"FROM {base}\nSYSTEM {system}"
self.pulling_model_list_box.set_visible(True)
model_row = Adw.ActionRow(
title = name
)
thread = threading.Thread(target=self.pull_model_process, kwargs={"model": name, "modelfile": modelfile})
overlay = Gtk.Overlay()
progress_bar = Gtk.ProgressBar(
valign = 2,
show_text = False,
margin_start = 10,
margin_end = 10,
css_classes = ["osd", "horizontal", "bottom"]
)
button = Gtk.Button(
icon_name = "media-playback-stop-symbolic",
vexpand = False,
valign = 3,
css_classes = ["error"]
)
button.connect("clicked", lambda button, model_name=name : dialogs.stop_pull_model(self, model_name))
model_row.add_suffix(button)
self.pulling_models[name] = {"row": model_row, "progress_bar": progress_bar, "overlay": overlay}
overlay.set_child(model_row)
overlay.add_overlay(progress_bar)
self.pulling_model_list_box.append(overlay)
self.create_model_dialog.close()
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 ['-', '_']])
if new_text != text: editable.stop_emission_by_name("insert-text")
def create_model(self, model:str, file:bool):
name = ""
system = ""
template = ""
if not file:
response = connection_handler.simple_post(f"{connection_handler.url}/api/show", json.dumps({"name": model}))
if 'text' in response:
data = json.loads(response['text'])
for line in data['modelfile'].split('\n'):
if line.startswith('SYSTEM'):
system = line[len('SYSTEM'):].strip()
elif line.startswith('TEMPLATE'):
template = line[len('TEMPLATE'):].strip()
self.create_model_template.set_sensitive(False)
name = model.split(':')[0]
else:
self.create_model_template.set_sensitive(True)
template = '"""{{ if .System }}<|start_header_id|>system<|end_header_id|>\n\n{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>\n\n{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>\n{{ .Response }}<|eot_id|>"""'
name = model.split("/")[-1].split(".")[0]
self.create_model_base.set_subtitle(model)
self.create_model_name.set_text(name)
self.create_model_system.set_text(system)
self.create_model_template.set_text(template)
self.manage_models_dialog.close()
self.create_model_dialog.present(self)
def show_toast(self, message_type:str, message_id:int, overlay): def show_toast(self, message_type:str, message_id:int, overlay):
if message_type not in self.toast_messages or message_id > len(self.toast_messages[message_type] or message_id < 0): if message_type not in self.toast_messages or message_id > len(self.toast_messages[message_type] or message_id < 0):
message_type = "error" message_type = "error"
@@ -284,20 +398,49 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.get_application().send_notification(None, notification) self.get_application().send_notification(None, notification)
def delete_message(self, message_element): def delete_message(self, message_element):
message_index = int(message_element.get_name()) id = message_element.get_name()
if message_index < len(self.chats["chats"][self.chats["selected_chat"]]["messages"]): del self.chats["chats"][self.chats["selected_chat"]]["messages"][id]
self.chats["chats"][self.chats["selected_chat"]]["messages"][message_index] = None self.chat_container.remove(message_element)
self.chat_container.remove(message_element) if os.path.exists(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id)):
self.save_history() print("deleting " + id)
shutil.rmtree(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id))
self.save_history()
def copy_message(self, message_element): def copy_message(self, message_element):
message_index = int(message_element.get_name()) id = message_element.get_name()
print(message_index)
clipboard = Gdk.Display().get_default().get_clipboard() 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) 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)
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)
new_message['content'] += f"```[{name}]\n{self.get_content_of_file(file_path, file_type)}\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)
new_message['images'].append(self.get_content_of_file(file_path, 'image'))
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( message_text = Gtk.TextView(
editable=False, editable=False,
focusable=True, focusable=True,
@@ -340,23 +483,63 @@ class AlpacaWindow(Adw.ApplicationWindow):
) )
message_text.set_valign(Gtk.Align.CENTER) message_text.set_valign(Gtk.Align.CENTER)
if image_base64 is not None: if images and len(images) > 0:
image_data = base64.b64decode(image_base64) image_container = Gtk.Box(
loader = GdkPixbuf.PixbufLoader.new() orientation=0,
loader.write(image_data) spacing=12
loader.close() )
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:
image_data = base64.b64decode(self.get_content_of_file(os.path.join(self.data_dir, "chats", self.chats['selected_chat'], id, image), "image"))
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() if files and len(files) > 0:
texture = Gdk.Texture.new_for_pixbuf(pixbuf) 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) button_content = Adw.ButtonContent(
image.set_size_request(240, 240) label=shown_name,
image.set_margin_top(10) icon_name="document-text-symbolic"
image.set_margin_start(10) )
image.set_margin_end(10) button = Gtk.Button(
image.set_hexpand(False) vexpand=False,
image.set_css_classes(["flat"]) valign=3,
message_box.append(image) 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) message_box.append(message_text)
overlay = Gtk.Overlay(css_classes=["message"], name=id) overlay = Gtk.Overlay(css_classes=["message"], name=id)
@@ -376,7 +559,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
def update_list_local_models(self): def update_list_local_models(self):
self.local_models = [] self.local_models = []
response = connection_handler.simple_get(connection_handler.url + "/api/tags") response = connection_handler.simple_get(f"{connection_handler.url}/api/tags")
for i in range(self.model_string_list.get_n_items() -1, -1, -1): for i in range(self.model_string_list.get_n_items() -1, -1, -1):
self.model_string_list.remove(i) self.model_string_list.remove(i)
if response['status'] == 'ok': if response['status'] == 'ok':
@@ -410,7 +593,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
def save_server_config(self): def save_server_config(self):
with open(os.path.join(self.config_dir, "server.json"), "w+") as f: 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): def verify_connection(self):
response = connection_handler.simple_get(connection_handler.url) response = connection_handler.simple_get(connection_handler.url)
@@ -534,12 +717,12 @@ class AlpacaWindow(Adw.ApplicationWindow):
clipboard.set(text) clipboard.set(text)
self.show_toast("info", 4, self.main_overlay) 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: if self.bot_message is None:
self.save_history() self.save_history()
sys.exit() sys.exit()
vadjustment = self.chat_window.get_vadjustment() 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()) GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
if data['done']: if data['done']:
formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M") formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
@@ -547,37 +730,36 @@ class AlpacaWindow(Adw.ApplicationWindow):
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text)) GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text))
self.save_history() self.save_history()
else: 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) GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None self.loading_spinner = None
self.chats["chats"][self.chats["selected_chat"]]["messages"].append({ self.chats["chats"][self.chats["selected_chat"]]["messages"][id] = {
"role": "assistant", "role": "assistant",
"model": data['model'], "model": data['model'],
"date": datetime.now().strftime("%Y/%m/%d %H:%M"), "date": datetime.now().strftime("%Y/%m/%d %H:%M"),
"content": '' "content": ''
}) }
GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content']) GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
self.chats["chats"][self.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): 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) element.set_sensitive(status)
def switch_send_stop_button(self): def switch_send_stop_button(self):
self.stop_button.set_visible(self.send_button.get_visible()) self.stop_button.set_visible(self.send_button.get_visible())
self.send_button.set_visible(not self.send_button.get_visible()) self.send_button.set_visible(not self.send_button.get_visible())
def run_message(self, messages, model): 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=self.update_bot_message) 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.add_code_blocks)
GLib.idle_add(self.switch_send_stop_button) GLib.idle_add(self.switch_send_stop_button)
GLib.idle_add(self.toggle_ui_sensitive, True) 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) if self.loading_spinner:
GLib.idle_add(self.image_button.set_css_classes, ["circular"]) GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.attached_image = {"path": None, "base64": None} self.loading_spinner = None
if response['status'] == 'error': if response['status'] == 'error':
GLib.idle_add(self.connection_error) GLib.idle_add(self.connection_error)
print(response)
def pull_model_update(self, data, model_name): def pull_model_update(self, data, model_name):
if model_name in list(self.pulling_models.keys()): if model_name in list(self.pulling_models.keys()):
@@ -588,9 +770,14 @@ class AlpacaWindow(Adw.ApplicationWindow):
if len(list(self.pulling_models.keys())) == 0: if len(list(self.pulling_models.keys())) == 0:
GLib.idle_add(self.pulling_model_list_box.set_visible, False) GLib.idle_add(self.pulling_model_list_box.set_visible, False)
def pull_model_process(self, model): def pull_model_process(self, model, modelfile):
data = {"name":model} response = {}
response = connection_handler.stream_post(f"{connection_handler.url}/api/pull", data=json.dumps(data), callback=lambda data, model_name=model: self.pull_model_update(data, model_name)) if modelfile:
data = {"name": model, "modelfile": modelfile}
response = connection_handler.stream_post(f"{connection_handler.url}/api/create", data=json.dumps(data), callback=lambda data, model_name=model: self.pull_model_update(data, model_name))
else:
data = {"name": model}
response = connection_handler.stream_post(f"{connection_handler.url}/api/pull", data=json.dumps(data), callback=lambda data, model_name=model: self.pull_model_update(data, model_name))
GLib.idle_add(self.update_list_local_models) GLib.idle_add(self.update_list_local_models)
if response['status'] == 'ok': if response['status'] == 'ok':
@@ -618,7 +805,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
model_row = Adw.ActionRow( model_row = Adw.ActionRow(
title = model title = model
) )
thread = threading.Thread(target=self.pull_model_process, kwargs={"model": model}) thread = threading.Thread(target=self.pull_model_process, kwargs={"model": model, "modelfile": None})
overlay = Gtk.Overlay() overlay = Gtk.Overlay()
progress_bar = Gtk.ProgressBar( progress_bar = Gtk.ProgressBar(
valign = 2, valign = 2,
@@ -634,7 +821,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
css_classes = ["error"] css_classes = ["error"]
) )
button.connect("clicked", lambda button, model_name=model : dialogs.stop_pull_model(self, model_name)) button.connect("clicked", lambda button, model_name=model : dialogs.stop_pull_model(self, model_name))
#model_row.add_suffix(progress_bar)
model_row.add_suffix(button) model_row.add_suffix(button)
self.pulling_models[model] = {"row": model_row, "progress_bar": progress_bar, "overlay": overlay} self.pulling_models[model] = {"row": model_row, "progress_bar": progress_bar, "overlay": overlay}
overlay.set_child(model_row) overlay.set_child(model_row)
@@ -668,70 +854,47 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.available_model_list_box.append(model) self.available_model_list_box.append(model)
def save_history(self): 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) json.dump(self.chats, f, indent=4)
def load_history_into_chat(self): def load_history_into_chat(self):
for widget in list(self.chat_container): self.chat_container.remove(widget) for widget in list(self.chat_container): self.chat_container.remove(widget)
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:
if message['role'] == 'user': 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: 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.add_code_blocks()
self.bot_message = None self.bot_message = None
def load_history(self): def load_history(self):
if os.path.exists(os.path.join(self.config_dir, "chats.json")): if os.path.exists(os.path.join(self.data_dir, "chats", "chats.json")):
try: 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) 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 "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": []} 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]
except Exception as e: 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() self.load_history_into_chat()
def load_image(self, file_dialog, result): def generate_numbered_name(self, chat_name:str, compare_list:list) -> str:
try: file = file_dialog.open_finish(result) if chat_name in compare_list:
except: return for i in range(len(compare_list)):
try: if "." in chat_name:
self.attached_image["path"] = file.get_path() if f"{'.'.join(chat_name.split('.')[:-1])} {i+1}.{chat_name.split('.')[-1]}" not in compare_list:
with Image.open(self.attached_image["path"]) as img: chat_name = f"{'.'.join(chat_name.split('.')[:-1])} {i+1}.{chat_name.split('.')[-1]}"
width, height = img.size break
max_size = 240
if width > height:
new_width = max_size
new_height = int((max_size / width) * height)
else: else:
new_height = max_size if f"{chat_name} {i+1}" not in compare_list:
new_width = int((max_size / height) * width) chat_name = f"{chat_name} {i+1}"
resized_img = img.resize((new_width, new_height), Image.LANCZOS) break
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
return chat_name 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): def clear_chat(self):
for widget in list(self.chat_container): self.chat_container.remove(widget) for widget in list(self.chat_container): self.chat_container.remove(widget)
self.chats["chats"][self.chats["selected_chat"]]["messages"] = [] self.chats["chats"][self.chats["selected_chat"]]["messages"] = []
@@ -739,22 +902,26 @@ class AlpacaWindow(Adw.ApplicationWindow):
def delete_chat(self, chat_name): def delete_chat(self, chat_name):
del self.chats['chats'][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.save_history()
self.update_chat_list() self.update_chat_list()
if len(self.chats['chats'])==0: if len(self.chats['chats'])==0:
self.new_chat() self.new_chat()
def rename_chat(self, old_chat_name, new_chat_name, label_element): 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] self.chats["chats"][new_chat_name] = self.chats["chats"][old_chat_name]
del 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.set_label(new_chat_name)
label_element.get_parent().set_name(new_chat_name) label_element.get_parent().set_name(new_chat_name)
self.save_history() self.save_history()
def new_chat(self): def new_chat(self):
chat_name = self.generate_numbered_chat_name(_("New Chat")) chat_name = self.generate_numbered_name(_("New Chat"), self.chats["chats"].keys())
self.chats["chats"][chat_name] = {"messages": []} self.chats["chats"][chat_name] = {"messages": {}}
self.save_history() self.save_history()
self.new_chat_element(chat_name, True) self.new_chat_element(chat_name, True)
@@ -763,7 +930,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
del self.pulling_models[model_name] del self.pulling_models[model_name]
def delete_model(self, model_name): def delete_model(self, model_name):
response = connection_handler.simple_delete(connection_handler.url + "/api/delete", data={"name": model_name}) response = connection_handler.simple_delete(f"{connection_handler.url}/api/delete", data={"name": model_name})
self.update_list_local_models() self.update_list_local_models()
if response['status'] == 'ok': if response['status'] == 'ok':
self.show_toast("good", 0, self.manage_models_overlay) self.show_toast("good", 0, self.manage_models_overlay)
@@ -860,35 +1027,74 @@ class AlpacaWindow(Adw.ApplicationWindow):
def on_export_current_chat(self, file_dialog, result): def on_export_current_chat(self, file_dialog, result):
file = file_dialog.save_finish(result) file = file_dialog.save_finish(result)
data_to_export = {self.chats["selected_chat"]: self.chats["chats"][self.chats["selected_chat"]]} if not file: return
file.replace_contents_async( json_data = json.dumps({self.chats["selected_chat"]: self.chats["chats"][self.chats["selected_chat"]]}, indent=4).encode("UTF-8")
json.dumps(data_to_export, indent=4).encode("UTF-8"),
etag=None, with tempfile.TemporaryDirectory() as temp_dir:
make_backup=False, json_path = os.path.join(temp_dir, "data.json")
flags=Gio.FileCreateFlags.NONE, with open(json_path, "wb") as json_file:
cancellable=None, json_file.write(json_data)
callback=self.on_replace_contents
) 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): 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) file_dialog.save(parent=self, cancellable=None, callback=self.on_export_current_chat)
def on_chat_imported(self, file_dialog, result): def on_chat_imported(self, file_dialog, result):
file = file_dialog.open_finish(result) file = file_dialog.open_finish(result)
if not file: return
stream = file.read(None) stream = file.read(None)
data_stream = Gio.DataInputStream.new(stream) data_stream = Gio.DataInputStream.new(stream)
data, _ = data_stream.read_until('\0', None) tar_content = data_stream.read_bytes(1024 * 1024, None)
data = json.loads(data)
chat_name = list(data.keys())[0] with tempfile.TemporaryDirectory() as temp_dir:
chat_content = data[chat_name] tar_filename = os.path.join(temp_dir, "imported_chat.tar")
self.chats['chats'][chat_name] = chat_content
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.update_chat_list()
self.save_history() self.save_history()
self.show_toast("good", 3, self.main_overlay) self.show_toast("good", 3, self.main_overlay)
def import_chat(self): 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) file_dialog.open(self, None, self.on_chat_imported)
def switch_run_on_background(self): def switch_run_on_background(self):
@@ -896,9 +1102,64 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.set_hide_on_close(self.run_on_background) self.set_hide_on_close(self.run_on_background)
self.verify_connection() self.verify_connection()
def get_content_of_file(self, file_path, file_type):
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)
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): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
GtkSource.init() GtkSource.init()
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.set_help_overlay(self.shortcut_window)
self.get_application().set_accels_for_action("win.show-help-overlay", ['<primary>slash']) 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']) self.get_application().create_action('new_chat', lambda *_: self.new_chat(), ['<primary>n'])
@@ -906,8 +1167,13 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.get_application().create_action('send', lambda *_: self.send_message(self), ['Return']) self.get_application().create_action('send', lambda *_: self.send_message(self), ['Return'])
self.get_application().create_action('export_current_chat', lambda *_: self.export_current_chat()) self.get_application().create_action('export_current_chat', lambda *_: self.export_current_chat())
self.get_application().create_action('import_chat', lambda *_: self.import_chat()) 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.add_chat_button.connect("clicked", lambda button : self.new_chat())
self.create_model_name.get_delegate().connect("insert-text", self.check_alphanumeric)
self.remote_connection_entry.connect("entry-activated", lambda entry : entry.set_css_classes([])) self.remote_connection_entry.connect("entry-activated", lambda entry : entry.set_css_classes([]))
self.remote_connection_switch.connect("notify", lambda pspec, user_data : self.connection_switched()) self.remote_connection_switch.connect("notify", lambda pspec, user_data : self.connection_switched())
self.background_switch.connect("notify", lambda pspec, user_data : self.switch_run_on_background()) self.background_switch.connect("notify", lambda pspec, user_data : self.switch_run_on_background())
@@ -923,6 +1189,16 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.temperature_spin.set_value(self.model_tweaks['temperature']) self.temperature_spin.set_value(self.model_tweaks['temperature'])
self.seed_spin.set_value(self.model_tweaks['seed']) self.seed_spin.set_value(self.model_tweaks['seed'])
self.keep_alive_spin.set_value(self.model_tweaks['keep_alive']) 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.background_switch.set_active(self.run_on_background)
self.set_hide_on_close(self.run_on_background) self.set_hide_on_close(self.run_on_background)

View File

@@ -24,7 +24,9 @@
<setter object="split_view_overlay" property="collapsed">true</setter> <setter object="split_view_overlay" property="collapsed">true</setter>
<setter object="welcome_dialog" property="width-request">360</setter> <setter object="welcome_dialog" property="width-request">360</setter>
<setter object="manage_models_dialog" property="width-request">360</setter> <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="preferences_dialog" property="width-request">360</setter>
<setter object="file_preview_dialog" property="width-request">360</setter>
</object> </object>
</child> </child>
<property name="content"> <property name="content">
@@ -47,7 +49,7 @@
<object class="GtkMenuButton" id="chats_menu_button"> <object class="GtkMenuButton" id="chats_menu_button">
<property name="direction">1</property> <property name="direction">1</property>
<property name="halign">3</property> <property name="halign">3</property>
<property name="menu-model">chats_button_menu</property> <property name="menu-model">chats_menu</property>
<property name="icon-name">view-more-symbolic</property> <property name="icon-name">view-more-symbolic</property>
</object> </object>
</child> </child>
@@ -162,92 +164,115 @@
<object class="AdwClamp"> <object class="AdwClamp">
<property name="maximum-size">1000</property> <property name="maximum-size">1000</property>
<property name="tightening-threshold">800</property> <property name="tightening-threshold">800</property>
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<property name="orientation">0</property> <property name="orientation">1</property>
<property name="spacing">12</property> <property name="spacing">12</property>
<property name="margin-top">12</property> <property name="margin-top">12</property>
<property name="margin-bottom">12</property> <property name="margin-bottom">12</property>
<property name="margin-start">12</property> <property name="margin-start">12</property>
<property name="margin-end">12</property> <property name="margin-end">12</property>
<child> <child>
<object class="GtkButton" id="image_button"> <object class="GtkScrolledWindow" id="attachment_box">
<signal name="clicked" handler="open_image"/> <property name="visible">false</property>
<property name="vexpand">false</property> <child>
<property name="valign">3</property> <object class="GtkBox" id="attachment_container">
<property name="sensitive">false</property> <property name="orientation">0</property>
<property name="tooltip-text" translatable="yes">Only available on selected models</property> <property name="vexpand">false</property>
<child> <property name="spacing">12</property>
<object class="AdwButtonContent"> </object>
<property name="icon-name">image-x-generic-symbolic</property> </child>
</object> </object>
</child> </child>
</object> <child>
</child> <object class="GtkBox">
<child> <property name="orientation">0</property>
<object class="GtkBox"> <property name="spacing">12</property>
<style> <child>
<class name="card"/> <object class="GtkMenuButton" id="attachment_button">
<class name="message-input"/> <property name="menu-model">attachment_menu</property>
</style> <property name="direction">0</property>
<child> <property name="vexpand">false</property>
<object class="GtkScrolledWindow"> <property name="valign">3</property>
<property name="margin-start">12</property> <property name="tooltip-text" translatable="yes">Attach file</property>
<property name="margin-end">12</property> <style>
<style> <class name="circular"/>
<class name="message-input"/> </style>
<class name="undershoot-bottom"/> <child>
<class name="undershoot-top"/> <object class="AdwButtonContent">
</style> <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> <child>
<object class="GtkTextView" id="message_text_view"> <object class="GtkButton" id="send_button">
<property name="wrap-mode">word</property> <signal name="clicked" handler="send_message"/>
<property name="top-margin">6</property> <property name="vexpand">false</property>
<property name="bottom-margin">6</property> <property name="valign">3</property>
<property name="hexpand">true</property> <property name="tooltip-text" translatable="yes">Send message</property>
<style> <style>
<class name="message-input"/> <class name="suggested-action"/>
<class name="circular"/>
</style> </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> </object>
</child> </child>
</object> </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> </child>
</object> </object>
</child> </child>
</object> </object>
</child> </child>
@@ -301,7 +326,7 @@
</child> </child>
<child> <child>
<object class="AdwPreferencesPage" id="model_page"> <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> <property name="icon-name">preferences-other-symbolic</property>
<child> <child>
<object class="AdwPreferencesGroup"> <object class="AdwPreferencesGroup">
@@ -357,6 +382,199 @@
</child> </child>
</object> </object>
</child> </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">
<property name="can-close">true</property>
<property name="width-request">400</property>
<property name="height-request">600</property>
<child>
<object class="AdwToastOverlay" id="create_model_overlay">
<child>
<object class="AdwToolbarView">
<child type="bottom">
<object class="GtkActionBar">
<property name="revealed">true</property>
<child type="end">
<object class="GtkButton">
<property name="label" translatable="yes">Create</property>
<signal name="clicked" handler="create_model_start"/>
<style>
<class name="suggested-action" />
</style>
</object>
</child>
</object>
</child>
<child type="top">
<object class="AdwHeaderBar">
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title" translatable="yes">Create model</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkBox">
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="orientation">1</property>
<property name="spacing">12</property>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwActionRow" id="create_model_base">
<property name="title" translatable="yes">Base</property>
<property name="subtitle"></property>
<style>
<class name="property"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwEntryRow" id="create_model_name">
<property name="title" translatable="yes">Name</property>
<property name="input-purpose">alpha</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="create_model_system">
<property name="title" translatable="yes">Context</property>
<property name="input-purpose">alpha</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkListBox">
<style>
<class name="boxed-list"/>
<class name="card"/>
</style>
<property name="selection-mode">none</property>
<child>
<object class="AdwEntryRow" id="create_model_template">
<property name="title" translatable="yes">Template</property>
<property name="input-purpose">alpha</property>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Some models require a specific template. Please visit the model's website for more information if you're unsure.</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="halign">1</property>
<property name="wrap">true</property>
<style>
<class name="caption"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object> </object>
<object class="AdwDialog" id="manage_models_dialog"> <object class="AdwDialog" id="manage_models_dialog">
@@ -369,6 +587,14 @@
<object class="AdwToolbarView"> <object class="AdwToolbarView">
<child type="top"> <child type="top">
<object class="AdwHeaderBar"> <object class="AdwHeaderBar">
<child type="start">
<object class="GtkMenuButton">
<property name="primary">True</property>
<property name="icon-name">list-add-symbolic</property>
<property name="tooltip-text" translatable="yes">Create model</property>
<property name="menu-model">create_model_menu</property>
</object>
</child>
<property name="title-widget"> <property name="title-widget">
<object class="AdwWindowTitle"> <object class="AdwWindowTitle">
<property name="title" translatable="yes">Manage models</property> <property name="title" translatable="yes">Manage models</property>
@@ -425,6 +651,37 @@
</child> </child>
</object> </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"> <object class="AdwDialog" id="welcome_dialog">
<property name="can-close">false</property> <property name="can-close">false</property>
<property name="width-request">450</property> <property name="width-request">450</property>
@@ -622,7 +879,7 @@
</item> </item>
</section> </section>
</menu> </menu>
<menu id="chats_button_menu"> <menu id="chats_menu">
<section> <section>
<item> <item>
<attribute name="label" translatable="yes">Export current chat</attribute> <attribute name="label" translatable="yes">Export current chat</attribute>
@@ -634,6 +891,44 @@
</item> </item>
</section> </section>
</menu> </menu>
<menu id="create_model_menu">
<section>
<item>
<attribute name="label" translatable="yes">From existing model</attribute>
<attribute name="action">app.create_model_from_existing</attribute>
</item>
<item>
<attribute name="label" translatable="yes">From GGUF file (Testing)</attribute>
<attribute name="action">app.create_model_from_file</attribute>
</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"> <object class="GtkFileFilter" id="file_filter_image">
<mime-types> <mime-types>
<mime-type>image/svg+xml</mime-type> <mime-type>image/svg+xml</mime-type>
@@ -643,11 +938,16 @@
<mime-type>image/gif</mime-type> <mime-type>image/gif</mime-type>
</mime-types> </mime-types>
</object> </object>
<object class="GtkFileFilter" id="file_filter_json"> <object class="GtkFileFilter" id="file_filter_tar">
<mime-types> <mime-types>
<mime-type>application/json</mime-type> <mime-type>application/x-tar</mime-type>
</mime-types> </mime-types>
</object> </object>
<object class="GtkFileFilter" id="file_filter_gguf">
<suffixes>
<suffix>gguf</suffix>
</suffixes>
</object>
<object class="GtkShortcutsWindow" id="shortcut_window"> <object class="GtkShortcutsWindow" id="shortcut_window">
<property name="modal">1</property> <property name="modal">1</property>
<child> <child>