5 Commits
0.1.2 ... 0.2.2

Author SHA1 Message Date
jeffser
76486da3d4 Fixes for 0.2.2 2024-05-14 00:27:02 -06:00
jeffser
c7303cd278 Quick fix for 0.2.1 2024-05-13 13:53:58 -06:00
jeffser
5866c5d4fc New features and changes for 0.2.0 2024-05-13 13:26:32 -06:00
jeffser
190bf7017f New features and changes for 0.2.0 2024-05-13 13:25:46 -06:00
Jeffry Samuel
43b2e469ef Better readme 2024-05-12 23:55:36 -06:00
8 changed files with 444 additions and 151 deletions

View File

@@ -1,16 +1,20 @@
<img src="https://jeffser.com/images/alpaca/logo.svg">
# Alpaca # Alpaca
An [Ollama](https://github.com/ollama/ollama) client made with GTK4 and Adwaita. An [Ollama](https://github.com/ollama/ollama) client made with GTK4 and Adwaita.
## Disclaimer
This project is not affiliated at all with Ollama, I'm not responsible for any damages to your device or software caused by running code given by any models.
## ‼I NEED AN ICON‼
I'm not a graphic designer, it would mean the world to me if someone could make a [GNOME icon](https://developer.gnome.org/hig/guidelines/app-icons.html) for this app.
## ⚠THIS IS UNDER DEVELOPMENT⚠
This is my first GTK4 / Adwaita / Python app, so it might crash and some features are still under development, please report any errors if you can, thank you! ---
> [!WARNING]
> This project is not affiliated at all with Ollama, I'm not responsible for any damages to your device or software caused by running code given by any models.
> [!important]
> This is my first GTK4 / Adwaita / Python app, so it might crash and some features are still under development, please report any errors if you can, thank you!
## Features! ## Features!
- Talk to multiple models in the same conversation - Talk to multiple models in the same conversation
@@ -22,9 +26,9 @@ This is my first GTK4 / Adwaita / Python app, so it might crash and some feature
- Image / document recognition - Image / document recognition
## Screenies ## Screenies
![Screenshot from 2024-05-12 19-58-28](https://github.com/Jeffser/Alpaca/assets/69224322/e28df5c9-6419-4800-bbbc-38821f096922) Login to Ollama instance | Chatting with models | Managing models
![Screenshot from 2024-05-12 20-01-08](https://github.com/Jeffser/Alpaca/assets/69224322/c4083864-8c39-40e6-83b6-aff9d62183ca) :-------------------------:|:-------------------------:|:-------------------------:
![Screenshot from 2024-05-12 20-01-31](https://github.com/Jeffser/Alpaca/assets/69224322/76deb8a2-13a5-480a-b99d-4de40159c229) ![Screenshot from 2024-05-12 19-58-28](https://github.com/Jeffser/Alpaca/assets/69224322/e28df5c9-6419-4800-bbbc-38821f096922) | ![Screenshot from 2024-05-12 20-01-08](https://github.com/Jeffser/Alpaca/assets/69224322/c4083864-8c39-40e6-83b6-aff9d62183ca) | ![Screenshot from 2024-05-12 20-01-31](https://github.com/Jeffser/Alpaca/assets/69224322/76deb8a2-13a5-480a-b99d-4de40159c229)
## Preview ## Preview
1. Clone repo using Gnome Builder 1. Clone repo using Gnome Builder

View File

@@ -65,7 +65,7 @@
{ {
"type" : "git", "type" : "git",
"url" : "https://github.com/Jeffser/Alpaca.git", "url" : "https://github.com/Jeffser/Alpaca.git",
"tag": "0.1.2" "tag": "0.2.2"
} }
] ]
} }

View File

@@ -35,7 +35,7 @@
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://jeffser.com/images/alpaca/screenie1.png</image> <image>https://jeffser.com/images/alpaca/screenie1.png</image>
<caption>Login into an Ollama instance</caption> <caption>Welcome dialog</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://jeffser.com/images/alpaca/screenie2.png</image> <image>https://jeffser.com/images/alpaca/screenie2.png</image>
@@ -46,13 +46,53 @@
<caption>Managing models</caption> <caption>Managing models</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>
<content_rating type="oars-1.1"> <content_rating type="oars-1.1" />
<content_attribute id="money-purchasing">mild</content_attribute>
</content_rating>
<url type="bugtracker">https://github.com/Jeffser/Alpaca/issues</url> <url type="bugtracker">https://github.com/Jeffser/Alpaca/issues</url>
<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.2.2" date="2024-05-14">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.2.1</url>
<description>
<p>0.2.2 Bug fixes</p>
<ul>
<li>Toast messages appearing behind dialogs</li>
<li>Local model list not updating when changing servers</li>
<li>Closing the setup dialog closes the whole app</li>
</ul>
<p>
Please report any errors to the issues page, thank you.
</p>
</description>
</release>
<release version="0.2.1" date="2024-05-14">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.2.1</url>
<description>
<p>0.2.1 Data saving fix</p>
<p>The app didn't save the config files and chat history to the right directory, this is now fixed</p>
<p>
Please report any errors to the issues page, thank you.
</p>
</description>
</release>
<release version="0.2.0" date="2024-05-14">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.2.0</url>
<description>
<p>0.2.0</p>
<p>Big Update</p>
<p>New Features</p>
<ul>
<li>Restore chat after closing the app</li>
<li>A button to clear the chat</li>
<li>Fixed multiple bugs involving how messages are shown</li>
<li>Added welcome dialog</li>
<li>More stability</li>
</ul>
<p>
Please report any errors to the issues page, thank you.
</p>
</description>
</release>
<release version="0.1.2" date="2024-05-13"> <release version="0.1.2" date="2024-05-13">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.1.2</url> <url type="details">https://github.com/Jeffser/Alpaca/releases/tag/0.1.2</url>
<description> <description>

View File

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

View File

@@ -7,9 +7,9 @@ def simple_get(connection_url:str) -> dict:
if response.status_code == 200: if response.status_code == 200:
return {"status": "ok", "text": response.text, "status_code": response.status_code} return {"status": "ok", "text": response.text, "status_code": response.status_code}
else: else:
return {"status": "error", "text": f"Failed to connect to {connection_url}. Status code: {response.status_code}", "status_code": response.status_code} return {"status": "error", "status_code": response.status_code}
except Exception as e: except Exception as e:
return {"status": "error", "text": f"An error occurred while trying to connect to {connection_url}", "status_code": 0} return {"status": "error", "status_code": 0}
def simple_delete(connection_url:str, data) -> dict: def simple_delete(connection_url:str, data) -> dict:
try: try:
@@ -19,7 +19,7 @@ def simple_delete(connection_url:str, data) -> dict:
else: else:
return {"status": "error", "text": "Failed to delete", "status_code": response.status_code} return {"status": "error", "text": "Failed to delete", "status_code": response.status_code}
except Exception as e: except Exception as e:
return {"status": "error", "text": f"An error occurred while trying to connect to {connection_url}", "status_code": 0} return {"status": "error", "status_code": 0}
def stream_post(connection_url:str, data, callback:callable) -> dict: def stream_post(connection_url:str, data, callback:callable) -> dict:
try: try:
@@ -31,11 +31,11 @@ def stream_post(connection_url:str, data, callback:callable) -> dict:
for line in response.iter_lines(): for line in response.iter_lines():
if line: if line:
callback(json.loads(line.decode("utf-8"))) callback(json.loads(line.decode("utf-8")))
return {"status": "ok", "text": "All good", "status_code": response.status_code} return {"status": "ok", "status_code": response.status_code}
else: else:
return {"status": "error", "text": "Error posting data", "status_code": response.status_code} return {"status": "error", "status_code": response.status_code}
except Exception as e: except Exception as e:
return {"status": "error", "text": f"An error occurred while trying to connect to {connection_url}", "status_code": 0} return {"status": "error", "status_code": 0}
from time import sleep from time import sleep
@@ -58,4 +58,4 @@ def stream_post_fake(connection_url:str, data, callback:callable) -> dict:
sleep(.1) sleep(.1)
data = {"status": msg} data = {"status": msg}
callback(data) callback(data)
return {"status": "ok", "text": "All good", "status_code": 200} return {"status": "ok", "status_code": 200}

View File

@@ -33,8 +33,9 @@ class AlpacaApplication(Adw.Application):
super().__init__(application_id='com.jeffser.Alpaca', super().__init__(application_id='com.jeffser.Alpaca',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS) flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.quit(), ['<primary>q']) self.create_action('quit', lambda *_: self.quit(), ['<primary>q'])
self.create_action('clear', lambda *_: AlpacaWindow.clear_conversation_dialog(self.props.active_window), ['<primary>e'])
self.create_action('reconnect', lambda *_: AlpacaWindow.show_connection_dialog(self.props.active_window), ['<primary>r'])
self.create_action('about', self.on_about_action) self.create_action('about', self.on_about_action)
self.create_action('preferences', self.on_preferences_action)
def do_activate(self): def do_activate(self):
win = self.props.active_window win = self.props.active_window
@@ -47,16 +48,13 @@ 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.1.2', version='0.2.2',
developers=['Jeffser https://jeffser.com'], developers=['Jeffser https://jeffser.com'],
designers=['Jeffser https://jeffser.com'], designers=['Jeffser https://jeffser.com'],
copyright='© 2024 Jeffser', copyright='© 2024 Jeffser',
issue_url='https://github.com/Jeffser/Alpaca/issues') issue_url='https://github.com/Jeffser/Alpaca/issues')
about.present() about.present()
def on_preferences_action(self, widget, _):
print('app.preferences action activated')
def create_action(self, name, callback, shortcuts=None): def create_action(self, name, callback, shortcuts=None):
action = Gio.SimpleAction.new(name, None) action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback) action.connect("activate", callback)

View File

@@ -20,24 +20,35 @@
import gi import gi
gi.require_version("Soup", "3.0") gi.require_version("Soup", "3.0")
from gi.repository import Adw, Gtk, GLib from gi.repository import Adw, Gtk, GLib
import json, requests, threading import json, requests, threading, os
from datetime import datetime from datetime import datetime
from .connection_handler import simple_get, simple_delete, stream_post, stream_post_fake from .connection_handler import simple_get, simple_delete, stream_post, stream_post_fake
from .available_models import available_models from .available_models import available_models
@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.path.join(os.getenv("XDG_CONFIG_HOME"), "/", os.path.expanduser("~/.var/app/com.jeffser.Alpaca/config"))
__gtype_name__ = 'AlpacaWindow' __gtype_name__ = 'AlpacaWindow'
#Variables #Variables
ollama_url = None ollama_url = None
local_models = [] local_models = []
messages_history = [] #In the future I will at multiple chats, for now I'll save it like this so that past chats don't break in the future
current_chat_id="0"
chats = {"chats": {"0": {"messages": []}}}
#Elements #Elements
bot_message : Gtk.TextBuffer = None bot_message : Gtk.TextBuffer = None
overlay = Gtk.Template.Child() connection_dialog = Gtk.Template.Child()
connection_carousel = Gtk.Template.Child()
connection_previous_button = Gtk.Template.Child()
connection_next_button = Gtk.Template.Child()
connection_url_entry = Gtk.Template.Child()
main_overlay = Gtk.Template.Child()
pull_overlay = Gtk.Template.Child()
manage_models_overlay = Gtk.Template.Child()
connection_overlay = Gtk.Template.Child()
chat_container = Gtk.Template.Child() chat_container = Gtk.Template.Child()
chat_window = Gtk.Template.Child()
message_entry = Gtk.Template.Child() message_entry = Gtk.Template.Child()
send_button = Gtk.Template.Child() send_button = Gtk.Template.Child()
model_drop_down = Gtk.Template.Child() model_drop_down = Gtk.Template.Child()
@@ -51,14 +62,35 @@ class AlpacaWindow(Adw.ApplicationWindow):
pull_model_status_page = Gtk.Template.Child() pull_model_status_page = Gtk.Template.Child()
pull_model_progress_bar = Gtk.Template.Child() pull_model_progress_bar = Gtk.Template.Child()
def show_toast(self, msg:str): toast_messages = {
"error": [
"An error occurred",
"Failed to connect to server",
"Could not list local models",
"Could not delete model",
"Could not pull model"
],
"info": [
"Please select a model before chatting",
"Conversation cannot be cleared while receiving a message"
],
"good": [
"Model deleted successfully",
"Model pulled successfully"
]
}
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):
message_type = "error"
message_id = 0
toast = Adw.Toast( toast = Adw.Toast(
title=msg, title=self.toast_messages[message_type][message_id],
timeout=2 timeout=2
) )
self.overlay.add_toast(toast) overlay.add_toast(toast)
def show_message(self, msg:str, bot:bool): def show_message(self, msg:str, bot:bool, footer:str=None):
message_text = Gtk.TextView( message_text = Gtk.TextView(
editable=False, editable=False,
focusable=False, focusable=False,
@@ -71,6 +103,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
) )
message_buffer = message_text.get_buffer() message_buffer = message_text.get_buffer()
message_buffer.insert(message_buffer.get_end_iter(), msg) message_buffer.insert(message_buffer.get_end_iter(), msg)
if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer))
message_box = Adw.Bin( message_box = Adw.Bin(
child=message_text, child=message_text,
css_classes=["card" if bot else None] css_classes=["card" if bot else None]
@@ -82,93 +116,77 @@ class AlpacaWindow(Adw.ApplicationWindow):
def update_list_local_models(self): def update_list_local_models(self):
self.local_models = [] self.local_models = []
response = simple_get(self.ollama_url + "/api/tags") response = simple_get(self.ollama_url + "/api/tags")
for i in range(self.model_string_list.get_n_items() -1, -1, -1):
self.model_string_list.remove(i)
if response['status'] == 'ok': if response['status'] == 'ok':
for model in json.loads(response['text'])['models']: for model in json.loads(response['text'])['models']:
self.model_string_list.append(model["name"]) self.model_string_list.append(model["name"])
self.local_models.append(model["name"]) self.local_models.append(model["name"])
self.model_drop_down.set_selected(0) self.model_drop_down.set_selected(0)
return return
#IF IT CONTINUES THEN THERE WAS EN ERROR
self.show_toast(response['text'])
self.show_connection_dialog()
def dialog_response(self, dialog, task):
self.ollama_url = dialog.get_extra_child().get_text()
if dialog.choose_finish(task) == "login":
response = simple_get(self.ollama_url)
if response['status'] == 'ok':
if "Ollama is running" in response['text']:
self.message_entry.grab_focus_without_selecting()
self.update_list_local_models()
return
else:
response = {"status": "error", "text": f"Unexpected response from {self.ollama_url} : {response['text']}"}
#IF IT CONTINUES THEN THERE WAS EN ERROR
self.show_toast(response['text'])
self.show_connection_dialog()
else: else:
self.destroy() self.show_connection_dialog(True)
self.show_toast("error", 2, self.connection_overlay)
def show_connection_dialog(self): def verify_connection(self):
dialog = Adw.AlertDialog( response = simple_get(self.ollama_url)
heading="Login", if response['status'] == 'ok':
body="Please enter the Ollama instance URL", if "Ollama is running" in response['text']:
close_response="cancel" with open(os.path.join(self.config_dir, "server.conf"), "w+") as f: f.write(self.ollama_url)
) self.message_entry.grab_focus_without_selecting()
dialog.add_response("cancel", "Cancel") self.update_list_local_models()
dialog.add_response("login", "Login") return True
dialog.set_response_appearance("login", Adw.ResponseAppearance.SUGGESTED) return False
entry = Gtk.Entry(text="http://localhost:11434") #FOR TESTING PURPOSES
dialog.set_extra_child(entry)
dialog.choose(parent = self, cancellable = None, callback = self.dialog_response)
def update_bot_message(self, data): def update_bot_message(self, data):
if data['done']: if data['done']:
try: formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
api_datetime = data['created_at'] text = f"\n\n<small>{data['model']}\t|\t{formated_datetime}</small>"
api_datetime = api_datetime[:-4] + api_datetime[-1] GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text))
formated_datetime = datetime.strptime(api_datetime, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y/%m/%d %H:%M") vadjustment = self.chat_window.get_vadjustment()
text = f"\n\n<small>{data['model']}\t|\t{formated_datetime}</small>" GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text)) self.save_history()
except Exception as e: print(e)
self.bot_message = None self.bot_message = None
else: else:
if self.bot_message is None: if self.bot_message is None:
GLib.idle_add(self.show_message, data['message']['content'], True) GLib.idle_add(self.show_message, data['message']['content'], True)
self.messages_history.append({ self.chats["chats"][self.current_chat_id]["messages"].append({
"role": "assistant", "role": "assistant",
"model": data['model'],
"date": datetime.now().strftime("%Y/%m/%d %H:%M"),
"content": data['message']['content'] "content": data['message']['content']
}) })
else: else:
GLib.idle_add(self.bot_message.insert_at_cursor, data['message']['content'], len(data['message']['content'])) GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
self.messages_history[-1]['content'] += data['message']['content'] self.chats["chats"][self.current_chat_id]["messages"][-1]['content'] += data['message']['content']
#else: GLib.idle_add(self.bot_message.insert, self.bot_message.get_end_iter(), data['message']['content'])
def send_message(self): def send_message(self):
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:
GLib.idle_add(self.show_toast, "Please pull a model") GLib.idle_add(self.show_toast, "info", 0, self.main_overlay)
return return
self.messages_history.append({ formated_datetime = datetime.now().strftime("%Y/%m/%d %H:%M")
self.chats["chats"][self.current_chat_id]["messages"].append({
"role": "user", "role": "user",
"model": "User",
"date": formated_datetime,
"content": self.message_entry.get_text() "content": self.message_entry.get_text()
}) })
data = { data = {
"model": current_model.get_string(), "model": current_model.get_string(),
"messages": self.messages_history "messages": self.chats["chats"][self.current_chat_id]["messages"]
} }
GLib.idle_add(self.message_entry.set_sensitive, False) GLib.idle_add(self.message_entry.set_sensitive, False)
GLib.idle_add(self.send_button.set_sensitive, False) GLib.idle_add(self.send_button.set_sensitive, False)
GLib.idle_add(self.show_message, self.message_entry.get_text(), False) GLib.idle_add(self.show_message, self.message_entry.get_text(), False, f"\n\n<small>{formated_datetime}</small>")
self.save_history()
GLib.idle_add(self.message_entry.get_buffer().set_text, "", 0) GLib.idle_add(self.message_entry.get_buffer().set_text, "", 0)
response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps(data), callback=self.update_bot_message) response = stream_post(f"{self.ollama_url}/api/chat", data=json.dumps(data), callback=self.update_bot_message)
GLib.idle_add(self.send_button.set_sensitive, True) GLib.idle_add(self.send_button.set_sensitive, True)
GLib.idle_add(self.message_entry.set_sensitive, True) GLib.idle_add(self.message_entry.set_sensitive, True)
if response['status'] == 'error': if response['status'] == 'error':
self.show_toast(f"{response['text']}") GLib.idle_add(self.show_toast, 'error', 1, self.connection_overlay)
self.show_connection_dialog() GLib.idle_add(self.show_connection_dialog, True)
def send_button_activate(self, button): def send_button_activate(self, button):
if not self.message_entry.get_text(): return if not self.message_entry.get_text(): return
@@ -178,22 +196,19 @@ class AlpacaWindow(Adw.ApplicationWindow):
def delete_model(self, dialog, task, model_name, button): def delete_model(self, dialog, task, model_name, button):
if dialog.choose_finish(task) == "delete": if dialog.choose_finish(task) == "delete":
response = simple_delete(self.ollama_url + "/api/delete", data={"name": model_name}) response = simple_delete(self.ollama_url + "/api/delete", data={"name": model_name})
print(response)
if response['status'] == 'ok': if response['status'] == 'ok':
button.set_icon_name("folder-download-symbolic") button.set_icon_name("folder-download-symbolic")
button.set_css_classes(["accent", "pull"]) button.set_css_classes(["accent", "pull"])
self.show_toast(f"Model '{model_name}' deleted successfully") self.show_toast("good", 0, self.manage_models_overlay)
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) == model_name: if self.model_string_list.get_string(i) == model_name:
self.model_string_list.remove(i) self.model_string_list.remove(i)
self.model_drop_down.set_selected(0) self.model_drop_down.set_selected(0)
break break
elif response['status_code'] == '404':
self.show_toast(f"Delete request failed: Model was not found")
else: else:
self.show_toast(response['text']) self.show_toast("error", 3, self.connection_overlay)
self.manage_models_dialog.close() self.manage_models_dialog.close()
self.show_connection_dialog() self.show_connection_dialog(True)
def pull_model_update(self, data): def pull_model_update(self, data):
try: try:
@@ -216,11 +231,12 @@ class AlpacaWindow(Adw.ApplicationWindow):
GLib.idle_add(button.set_icon_name, "user-trash-symbolic") GLib.idle_add(button.set_icon_name, "user-trash-symbolic")
GLib.idle_add(button.set_css_classes, ["error", "delete"]) GLib.idle_add(button.set_css_classes, ["error", "delete"])
GLib.idle_add(self.model_string_list.append, model_name) GLib.idle_add(self.model_string_list.append, model_name)
GLib.idle_add(self.show_toast, f"Model '{model_name}' pulled successfully") GLib.idle_add(self.show_toast, "good", 1, self.manage_models_overlay)
else: else:
GLib.idle_add(self.show_toast, response['text']) GLib.idle_add(self.show_toast, "error", 4, self.connection_overlay)
GLib.idle_add(self.manage_models_dialog.close) GLib.idle_add(self.manage_models_dialog.close)
GLib.idle_add(self.show_connection_dialog) GLib.idle_add(self.show_connection_dialog, True)
print("pull fail")
def pull_model_start(self, dialog, task, model_name, button): def pull_model_start(self, dialog, task, model_name, button):
@@ -230,7 +246,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
def model_action_button_activate(self, button, model_name): def model_action_button_activate(self, button, model_name):
action = list(set(button.get_css_classes()) & set(["delete", "pull"]))[0] action = list(set(button.get_css_classes()) & set(["delete", "pull"]))[0]
print(f"action: {action}")
dialog = Adw.AlertDialog( dialog = Adw.AlertDialog(
heading=f"{action.capitalize()} Model", heading=f"{action.capitalize()} Model",
body=f"Are you sure you want to {action} '{model_name}'?", body=f"Are you sure you want to {action} '{model_name}'?",
@@ -264,18 +279,133 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.model_list_box.append(model) self.model_list_box.append(model)
def manage_models_button_activate(self, button): def manage_models_button_activate(self, button):
self.manage_models_dialog.present(self) self.manage_models_dialog.present(self)
self.update_list_available_models() self.update_list_available_models()
def connection_carousel_page_changed(self, carousel, index):
if index == 0: self.connection_previous_button.set_sensitive(False)
else: self.connection_previous_button.set_sensitive(True)
if index == carousel.get_n_pages()-1: self.connection_next_button.set_label("Connect")
else: self.connection_next_button.set_label("Next")
def connection_previous_button_activate(self, button):
self.connection_carousel.scroll_to(self.connection_carousel.get_nth_page(self.connection_carousel.get_position()-1), True)
def connection_next_button_activate(self, button):
if button.get_label() == "Next": self.connection_carousel.scroll_to(self.connection_carousel.get_nth_page(self.connection_carousel.get_position()+1), True)
else:
self.ollama_url = self.connection_url_entry.get_text()
if self.verify_connection():
self.connection_dialog.force_close()
else:
self.show_connection_dialog(True)
self.show_toast("error", 1, self.connection_overlay)
def show_connection_dialog(self, error:bool=False):
self.connection_carousel.scroll_to(self.connection_carousel.get_nth_page(self.connection_carousel.get_n_pages()-1),False)
if self.ollama_url is not None: self.connection_url_entry.set_text(self.ollama_url)
if error: self.connection_url_entry.set_css_classes(["error"])
else: self.connection_url_entry.set_css_classes([])
self.connection_dialog.present(self)
def clear_conversation(self):
for widget in list(self.chat_container): self.chat_container.remove(widget)
self.chats["chats"][self.current_chat_id]["messages"] = []
def clear_conversation_dialog_response(self, dialog, task):
if dialog.choose_finish(task) == "empty":
self.clear_conversation()
self.save_history()
def clear_conversation_dialog(self):
if self.bot_message is not None:
self.show_toast("info", 1, self.main_overlay)
return
dialog = Adw.AlertDialog(
heading=f"Clear Conversation",
body=f"Are you sure you want to clear the conversation?",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("empty", "Empty")
dialog.set_response_appearance("empty", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.choose(
parent = self,
cancellable = None,
callback = self.clear_conversation_dialog_response
)
def save_history(self):
with open(os.path.join(self.config_dir, "chats.json"), "w+") as f:
json.dump(self.chats, f, indent=4)
def load_history(self):
if os.path.exists(os.path.join(self.config_dir, "chats.json")):
self.clear_conversation()
try:
with open(os.path.join(self.config_dir, "chats.json"), "r") as f:
self.chats = json.load(f)
except Exception as e:
self.chats = {"chats": {"0": {"messages": []}}}
for message in self.chats['chats'][self.current_chat_id]['messages']:
if message['role'] == 'user':
self.show_message(message['content'], False, f"\n\n<small>{message['date']}</small>")
else:
self.show_message(message['content'], True, f"\n\n<small>{message['model']}\t|\t{message['date']}</small>")
self.bot_message = None
def closing_connection_dialog_response(self, dialog, task):
result = dialog.choose_finish(task)
if result == "cancel": return
if result == "save":
self.ollama_url = self.connection_url_entry.get_text()
self.connection_dialog.force_close()
if self.ollama_url is None or self.verify_connection() == False:
self.show_connection_dialog(True)
self.show_toast("error", 1, self.connection_overlay)
def closing_connection_dialog(self, dialog):
if self.get_visible() == False:
self.destroy()
else:
dialog = Adw.AlertDialog(
heading=f"Save Changes?",
body=f"Do you want to save the URL change?",
close_response="cancel"
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("discard", "Discard")
dialog.add_response("save", "Save")
dialog.set_response_appearance("discard", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
dialog.choose(
parent = self,
cancellable = None,
callback = self.closing_connection_dialog_response
)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.manage_models_button.connect("clicked", self.manage_models_button_activate) self.manage_models_button.connect("clicked", self.manage_models_button_activate)
self.send_button.connect("clicked", self.send_button_activate) self.send_button.connect("clicked", self.send_button_activate)
self.set_default_widget(self.send_button) self.set_default_widget(self.send_button)
self.message_entry.set_activates_default(self.send_button) self.message_entry.set_activates_default(self.send_button)
self.message_entry.set_text("Hi") #FOR TESTING PURPOSES self.connection_carousel.connect("page-changed", self.connection_carousel_page_changed)
self.show_connection_dialog() self.connection_previous_button.connect("clicked", self.connection_previous_button_activate)
self.connection_next_button.connect("clicked", self.connection_next_button_activate)
self.connection_url_entry.connect("changed", lambda entry: entry.set_css_classes([]))
self.connection_dialog.connect("close-attempt", self.closing_connection_dialog)
self.load_history()
if os.path.exists(os.path.join(self.config_dir, "server.conf")):
with open(os.path.join(self.config_dir, "server.conf"), "r") as f:
self.ollama_url = f.read()
if self.verify_connection() is False: self.show_connection_dialog(True)
else: self.connection_dialog.present(self)
self.show_toast("funny", True, self.manage_models_overlay)

View File

@@ -5,7 +5,7 @@
<template class="AlpacaWindow" parent="AdwApplicationWindow"> <template class="AlpacaWindow" parent="AdwApplicationWindow">
<property name="resizable">True</property> <property name="resizable">True</property>
<property name="content"> <property name="content">
<object class="AdwToastOverlay" id="overlay"> <object class="AdwToastOverlay" id="main_overlay">
<child> <child>
<object class="AdwToolbarView"> <object class="AdwToolbarView">
<child type="top"> <child type="top">
@@ -36,7 +36,6 @@
</object> </object>
</child> </child>
</object> </object>
</property> </property>
<child type="end"> <child type="end">
<object class="GtkMenuButton"> <object class="GtkMenuButton">
@@ -119,63 +118,26 @@
</child> </child>
</object> </object>
</property> </property>
<object class="AdwDialog" id="pull_model_dialog"> <object class="AdwDialog" id="pull_model_dialog">
<property name="can-close">false</property> <property name="can-close">false</property>
<property name="width-request">400</property> <property name="width-request">400</property>
<child> <child>
<object class="AdwToolbarView"> <object class="AdwToastOverlay" id="pull_overlay">
<child> <child>
<object class="AdwStatusPage" id="pull_model_status_page"> <object class="AdwToolbarView">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">24</property>
<property name="margin-bottom">24</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="title" translatable="yes">Pulling Model</property>
<child> <child>
<object class="GtkProgressBar" id="pull_model_progress_bar"> <object class="AdwStatusPage" id="pull_model_status_page">
<property name="show-text">true</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<object class="AdwDialog" id="manage_models_dialog">
<property name="can-close">true</property>
<property name="width-request">400</property>
<property name="height-request">600</property>
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title">Manage models</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">0</property>
<property name="margin-bottom">24</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<child>
<object class="GtkScrolledWindow">
<property name="hexpand">true</property> <property name="hexpand">true</property>
<property name="vexpand">true</property> <property name="vexpand">true</property>
<property name="margin-top">24</property>
<property name="margin-bottom">24</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<property name="title" translatable="yes">Pulling Model</property>
<child> <child>
<object class="GtkListBox" id="model_list_box"> <object class="GtkProgressBar" id="pull_model_progress_bar">
<property name="selection-mode">none</property> <property name="show-text">true</property>
<style>
<class name="boxed-list"/>
</style>
</object> </object>
</child> </child>
</object> </object>
@@ -185,16 +147,175 @@
</object> </object>
</child> </child>
</object> </object>
<object class="AdwDialog" id="manage_models_dialog">
<property name="can-close">true</property>
<property name="width-request">400</property>
<property name="height-request">600</property>
<child>
<object class="AdwToastOverlay" id="manage_models_overlay">
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<property name="title-widget">
<object class="AdwWindowTitle">
<property name="title">Manage models</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkBox">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="margin-top">0</property>
<property name="margin-bottom">24</property>
<property name="margin-start">24</property>
<property name="margin-end">24</property>
<child>
<object class="GtkScrolledWindow">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<child>
<object class="GtkListBox" id="model_list_box">
<property name="selection-mode">none</property>
<style>
<class name="boxed-list"/>
</style>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
<object class="AdwDialog" id="connection_dialog">
<property name="can-close">false</property>
<property name="width-request">450</property>
<property name="height-request">450</property>
<child>
<object class="AdwToastOverlay" id="connection_overlay">
<child>
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
</object>
</child>
<child type="bottom">
<object class="GtkActionBar">
<property name="hexpand">true</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-bottom">5</property>
<child type="start">
<object class="GtkButton" id="connection_previous_button">
<property name="tooltip-text" translatable="yes">Previous</property>
<property name="label">Previous</property>
<property name="sensitive">false</property>
<style>
<class name="raised"/>
</style>
</object>
</child>
<child type="center">
<object class="AdwCarouselIndicatorDots">
<property name="carousel">connection_carousel</property>
</object>
</child>
<child type="end">
<object class="GtkButton" id="connection_next_button">
<property name="tooltip-text" translatable="yes">Next</property>
<property name="label">Next</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="AdwCarousel" id="connection_carousel">
<property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="allow-long-swipes">true</property>
<property name="allow-scroll-wheel">true</property>
<property name="spacing">12</property>
<child>
<object class="AdwStatusPage">
<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>
<property name="icon-name">com.jeffser.Alpaca</property>
<property name="title" translatable="yes">Welcome to Alpaca</property>
<property name="description" translatable="yes">To get started, please ensure you have an Ollama instance set up. You can either run Ollama locally on your machine or connect to a remote instance.</property>
<child>
<object class="GtkLinkButton">
<property name="label" translatable="true">Ollama Website</property>
<property name="uri">https://ollama.com/</property>
<property name="margin-top">12</property>
</object>
</child>
</object>
</child>
<child>
<object class="AdwStatusPage">
<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>
<property name="icon-name">dialog-warning-symbolic</property>
<property name="title" translatable="yes">Disclaimer</property>
<property name="description" translatable="yes">Alpaca and its developers are not liable for any damages to devices or software resulting from the execution of code generated by an AI model. Please exercise caution and review the code carefully before running it.</property>
</object>
</child>
<child>
<object class="AdwStatusPage">
<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>
<property name="icon-name">preferences-other-symbolic</property>
<property name="title" translatable="yes">Setup</property>
<property name="description" translatable="yes">If you are running an Ollama instance locally and haven't modified the default ports, you can use the default URL. Otherwise, please enter the URL of your Ollama instance.</property>
<child>
<object class="GtkEntry" id="connection_url_entry">
<property name="text">http://localhost:11434</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</template> </template>
<menu id="primary_menu"> <menu id="primary_menu">
<section> <section>
<item> <item>
<attribute name="label" translatable="yes">_Preferences</attribute> <attribute name="label" translatable="yes">_Clear Conversation</attribute>
<attribute name="action">app.preferences</attribute> <attribute name="action">app.clear</attribute>
</item> </item>
<item> <item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute> <attribute name="label" translatable="yes">_Change Server</attribute>
<attribute name="action">win.show-help-overlay</attribute> <attribute name="action">app.reconnect</attribute>
</item> </item>
<item> <item>
<attribute name="label" translatable="yes">_About Alpaca</attribute> <attribute name="label" translatable="yes">_About Alpaca</attribute>