Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55a636f4d1 | ||
|
|
0fc8730272 | ||
|
|
61a2bc466e | ||
|
|
62b1923bf4 | ||
|
|
8e25376a12 | ||
|
|
a9ab5d45a4 | ||
|
|
ce2a2f0b93 | ||
|
|
9cb6b0b665 | ||
|
|
dfc21fc0e9 | ||
|
|
19b089e6c6 | ||
|
|
02aa2734e0 | ||
|
|
66f9fd7231 | ||
|
|
1b125cb704 | ||
|
|
29f5d85c7b | ||
|
|
c192a1f31c | ||
|
|
3b20daf807 | ||
|
|
760c00e8ae |
@@ -22,7 +22,7 @@ Alpaca is an [Ollama](https://github.com/ollama/ollama) client where you can man
|
||||
- Import / Export chats
|
||||
- Delete / Edit messages
|
||||
- YouTube recognition (Ask questions about a YouTube video using the transcript)
|
||||
- Website recognition (Ask questions about a certain question by parsing the url)
|
||||
- Website recognition (Ask questions about a certain website by parsing the url)
|
||||
|
||||
## Screenies
|
||||
Chatting with a model | Image recognition | Code highlighting
|
||||
|
||||
@@ -70,7 +70,9 @@
|
||||
<caption>Multiple models being downloaded</caption>
|
||||
</screenshot>
|
||||
</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="homepage">https://jeffser.com/alpaca/</url>
|
||||
<url type="donation">https://github.com/sponsors/Jeffser</url>
|
||||
@@ -78,6 +80,25 @@
|
||||
<url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url>
|
||||
<url type="vcs-browser">https://github.com/Jeffser/Alpaca</url>
|
||||
<releases>
|
||||
<release version="1.0.3" date="2024-08-01">
|
||||
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.3</url>
|
||||
<description>
|
||||
<p>New</p>
|
||||
<ul>
|
||||
<li>Bearer Token entry on connection error dialog</li>
|
||||
<li>Small appearance changes</li>
|
||||
<li>Compatibility with code blocks without explicit language</li>
|
||||
<li>Rare, optional and dismissible support dialog</li>
|
||||
</ul>
|
||||
<p>Fixes</p>
|
||||
<ul>
|
||||
<li>Date format for Simplified Chinese translation</li>
|
||||
<li>Bug with unsupported localizations</li>
|
||||
<li>Min height being too large to be used on mobile</li>
|
||||
<li>Remote connection checker bug</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.2" date="2024-07-29">
|
||||
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.2</url>
|
||||
<description>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('Alpaca', 'c',
|
||||
version: '1.0.2',
|
||||
version: '1.0.3',
|
||||
meson_version: '>= 0.62.0',
|
||||
default_options: [ 'warning_level=2', 'werror=false', ],
|
||||
)
|
||||
|
||||
@@ -172,26 +172,38 @@ def remove_attached_file(self, name):
|
||||
|
||||
# RECONNECT REMOTE | WORKS
|
||||
|
||||
def reconnect_remote_response(self, dialog, task, entry):
|
||||
def reconnect_remote_response(self, dialog, task, url_entry, bearer_entry):
|
||||
response = dialog.choose_finish(task)
|
||||
if not task or response == "remote":
|
||||
self.connect_remote(entry.get_text())
|
||||
self.connect_remote(url_entry.get_text(), bearer_entry.get_text())
|
||||
elif response == "local":
|
||||
self.connect_local()
|
||||
elif response == "close":
|
||||
self.destroy()
|
||||
|
||||
def reconnect_remote(self, current_url):
|
||||
entry = Gtk.Entry(
|
||||
def reconnect_remote(self, current_url, current_bearer_token):
|
||||
entry_url = Gtk.Entry(
|
||||
css_classes = ["error"],
|
||||
text = current_url
|
||||
text = current_url,
|
||||
placeholder_text = "URL"
|
||||
)
|
||||
entry_bearer_token = Gtk.Entry(
|
||||
css_classes = ["error"] if current_bearer_token else None,
|
||||
text = current_bearer_token,
|
||||
placeholder_text = "Bearer Token (Optional)"
|
||||
)
|
||||
container = Gtk.Box(
|
||||
orientation = 1,
|
||||
spacing = 10
|
||||
)
|
||||
container.append(entry_url)
|
||||
container.append(entry_bearer_token)
|
||||
dialog = Adw.AlertDialog(
|
||||
heading=_("Connection Error"),
|
||||
body=_("The remote instance has disconnected"),
|
||||
extra_child=entry
|
||||
extra_child=container
|
||||
)
|
||||
entry.connect("activate", lambda entry, dialog: reconnect_remote_response(self, dialog, None, entry))
|
||||
#entry.connect("activate", lambda entry, dialog: reconnect_remote_response(self, dialog, None, entry))
|
||||
dialog.add_response("close", _("Close Alpaca"))
|
||||
dialog.add_response("local", _("Use local instance"))
|
||||
dialog.add_response("remote", _("Connect"))
|
||||
@@ -199,7 +211,7 @@ def reconnect_remote(self, current_url):
|
||||
dialog.choose(
|
||||
parent = self,
|
||||
cancellable = None,
|
||||
callback = lambda dialog, task, entry=entry: reconnect_remote_response(self, dialog, task, entry)
|
||||
callback = lambda dialog, task, url_entry=entry_url, bearer_entry=entry_bearer_token: reconnect_remote_response(self, dialog, task, url_entry, bearer_entry)
|
||||
)
|
||||
|
||||
# CREATE MODEL | WORKS
|
||||
@@ -357,3 +369,31 @@ def attach_website(self, url):
|
||||
cancellable = None,
|
||||
callback = lambda dialog, task, url=url: attach_website_response(self, dialog, task, url)
|
||||
)
|
||||
|
||||
# Begging for money :3
|
||||
|
||||
def support_response(self, dialog, task):
|
||||
res = dialog.choose_finish(task)
|
||||
if res == 'later': return
|
||||
elif res == 'support':
|
||||
self.show_toast(_("Thank you!"), self.main_overlay)
|
||||
os.system('xdg-open https://github.com/sponsors/Jeffser')
|
||||
self.show_support = False
|
||||
self.save_server_config()
|
||||
|
||||
def support(self):
|
||||
dialog = Adw.AlertDialog(
|
||||
heading=_("Support"),
|
||||
body=_("Are you enjoying Alpaca? Consider sponsoring the project!"),
|
||||
close_response="nope"
|
||||
)
|
||||
dialog.add_response("nope", _("Don't show again"))
|
||||
dialog.set_response_appearance("nope", Adw.ResponseAppearance.DESTRUCTIVE)
|
||||
dialog.add_response("later", _("Later"))
|
||||
dialog.add_response("support", _("Support"))
|
||||
dialog.set_response_appearance("support", Adw.ResponseAppearance.SUGGESTED)
|
||||
dialog.choose(
|
||||
parent = self,
|
||||
cancellable = None,
|
||||
callback = lambda dialog, task: support_response(self, dialog, task)
|
||||
)
|
||||
|
||||
@@ -12,3 +12,7 @@
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
.chat_row:selected {
|
||||
background: mix(@theme_bg_color, @theme_selected_bg_color, 0.3);
|
||||
color: mix(@window_fg_color, @theme_selected_bg_color, 0.5);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import gi
|
||||
gi.require_version('GtkSource', '5')
|
||||
gi.require_version('GdkPixbuf', '2.0')
|
||||
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
|
||||
import json, requests, threading, os, re, base64, sys, gettext, locale, subprocess, uuid, shutil, tarfile, tempfile, logging
|
||||
import json, requests, threading, os, re, base64, sys, gettext, locale, subprocess, uuid, shutil, tarfile, tempfile, logging, random
|
||||
from time import sleep
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
@@ -44,7 +44,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
|
||||
localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locale')
|
||||
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
gettext.bindtextdomain('com.jeffser.Alpaca', localedir)
|
||||
gettext.textdomain('com.jeffser.Alpaca')
|
||||
_ = gettext.gettext
|
||||
@@ -61,6 +60,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
pulling_models = {}
|
||||
chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat", "order": []}
|
||||
attachments = {}
|
||||
show_support = True
|
||||
|
||||
#Override elements
|
||||
override_HSA_OVERRIDE_GFX_VERSION = Gtk.Template.Child()
|
||||
@@ -167,7 +167,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
buffer = self.editing_message["text_view"].get_buffer()
|
||||
text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).rstrip('\n')
|
||||
footer = "<small>" + self.editing_message["footer"] + "</small>"
|
||||
buffer.insert_markup(buffer.get_end_iter(), footer, len(footer))
|
||||
buffer.insert_markup(buffer.get_end_iter(), footer, len(footer.encode('utf-8')))
|
||||
self.chats["chats"][self.chats["selected_chat"]]["messages"][self.editing_message["id"]]["content"] = text
|
||||
self.editing_message = None
|
||||
self.save_history()
|
||||
@@ -229,7 +229,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
|
||||
#self.attachments[name] = {"path": file_path, "type": file_type, "content": content}
|
||||
raw_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)
|
||||
formated_date = self.generate_datetime_format(current_datetime)
|
||||
formated_date = GLib.markup_escape_text(self.generate_datetime_format(current_datetime))
|
||||
self.show_message(raw_message, False, f"\n\n<small>{formated_date}</small>", attached_images, attached_files, id=id)
|
||||
self.message_text_view.get_buffer().set_text("", 0)
|
||||
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
|
||||
@@ -290,6 +290,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
||||
|
||||
@Gtk.Template.Callback()
|
||||
def change_remote_url(self, entry):
|
||||
if not entry.get_text().startswith("http"):
|
||||
entry.set_text("http://{}".format(entry.get_text()))
|
||||
return
|
||||
self.remote_url = entry.get_text()
|
||||
logger.debug(f"Changing remote url: {self.remote_url}")
|
||||
if self.run_remote:
|
||||
@@ -596,7 +599,7 @@ Generate a title following these rules:
|
||||
)
|
||||
message_buffer = message_text.get_buffer()
|
||||
message_buffer.insert(message_buffer.get_end_iter(), msg)
|
||||
if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer))
|
||||
if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer.encode('utf-8')))
|
||||
|
||||
delete_button = Gtk.Button(
|
||||
icon_name = "user-trash-symbolic",
|
||||
@@ -778,14 +781,18 @@ Generate a title following these rules:
|
||||
|
||||
def save_server_config(self):
|
||||
with open(os.path.join(self.config_dir, "server.json"), "w+") as f:
|
||||
json.dump({'remote_url': self.remote_url, 'remote_bearer_token': self.remote_bearer_token, '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)
|
||||
json.dump({'remote_url': self.remote_url, 'remote_bearer_token': self.remote_bearer_token, '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, 'show_support': self.show_support}, f, indent=6)
|
||||
|
||||
def verify_connection(self):
|
||||
response = connection_handler.simple_get(f"{connection_handler.url}/api/tags")
|
||||
if response.status_code == 200:
|
||||
self.save_server_config()
|
||||
self.update_list_local_models()
|
||||
return response.status_code == 200
|
||||
try:
|
||||
response = connection_handler.simple_get(f"{connection_handler.url}/api/tags")
|
||||
if response.status_code == 200:
|
||||
self.save_server_config()
|
||||
self.update_list_local_models()
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return False
|
||||
|
||||
def add_code_blocks(self):
|
||||
text = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), True)
|
||||
@@ -803,13 +810,23 @@ Generate a title following these rules:
|
||||
code_text = match.group(2)
|
||||
parts.append({"type": "code", "text": code_text, "language": language})
|
||||
pos = end
|
||||
# Match code blocks without language
|
||||
no_lang_code_block_pattern = re.compile(r'`\n(.*?)\n`', re.DOTALL)
|
||||
for match in no_lang_code_block_pattern.finditer(text):
|
||||
start, end = match.span()
|
||||
if pos < start:
|
||||
normal_text = text[pos:start]
|
||||
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||
code_text = match.group(1)
|
||||
parts.append({"type": "code", "text": code_text, "language": None})
|
||||
pos = end
|
||||
# Extract any remaining normal text after the last code block
|
||||
if pos < len(text):
|
||||
normal_text = text[pos:]
|
||||
if normal_text.strip():
|
||||
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||
bold_pattern = re.compile(r'\*\*(.*?)\*\*') #"**text**"
|
||||
code_pattern = re.compile(r'`(.*?)`') #"`text`"
|
||||
code_pattern = re.compile(r'`([^`\n]*?)`') #"`text`"
|
||||
h1_pattern = re.compile(r'^#\s(.*)$') #"# text"
|
||||
h2_pattern = re.compile(r'^##\s(.*)$') #"## text"
|
||||
markup_pattern = re.compile(r'<(b|u|tt|span.*)>(.*?)<\/(b|u|tt|span)>') #heh butt span, I'm so funny
|
||||
@@ -843,17 +860,19 @@ Generate a title following these rules:
|
||||
start, end = match.span()
|
||||
if position < start:
|
||||
message_buffer.insert(message_buffer.get_end_iter(), part['text'][position:start])
|
||||
message_buffer.insert_markup(message_buffer.get_end_iter(), match.group(0), len(match.group(0)))
|
||||
message_buffer.insert_markup(message_buffer.get_end_iter(), match.group(0), len(match.group(0).encode('utf-8')))
|
||||
position = end
|
||||
|
||||
if position < len(part['text']):
|
||||
message_buffer.insert(message_buffer.get_end_iter(), part['text'][position:])
|
||||
|
||||
if footer: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer))
|
||||
if footer: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer.encode('utf-8')))
|
||||
|
||||
self.bot_message_box.append(message_text)
|
||||
else:
|
||||
language = GtkSource.LanguageManager.get_default().get_language(part['language'])
|
||||
language = None
|
||||
if part['language']:
|
||||
language = GtkSource.LanguageManager.get_default().get_language(part['language'])
|
||||
if language:
|
||||
buffer = GtkSource.Buffer.new_with_language(language)
|
||||
else:
|
||||
@@ -871,7 +890,7 @@ Generate a title following these rules:
|
||||
source_view.set_editable(False)
|
||||
code_block_box = Gtk.Box(css_classes=["card"], orientation=1, overflow=1)
|
||||
title_box = Gtk.Box(margin_start=12, margin_top=3, margin_bottom=3, margin_end=3)
|
||||
title_box.append(Gtk.Label(label=language.get_name() if language else part['language'], hexpand=True, xalign=0))
|
||||
title_box.append(Gtk.Label(label=language.get_name() if language else _("Code Block"), hexpand=True, xalign=0))
|
||||
copy_button = Gtk.Button(icon_name="edit-copy-symbolic", css_classes=["flat", "circular"], tooltip_text=_("Copy Message"))
|
||||
copy_button.connect("clicked", self.on_copy_code_clicked, buffer)
|
||||
title_box.append(copy_button)
|
||||
@@ -917,9 +936,9 @@ Generate a title following these rules:
|
||||
if id not in self.chats["chats"][self.chats["selected_chat"]]["messages"] or vadjustment.get_value() + 50 >= vadjustment.get_upper() - vadjustment.get_page_size():
|
||||
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
|
||||
if data['done']:
|
||||
formated_date = self.generate_datetime_format(datetime.strptime(self.chats["chats"][self.chats["selected_chat"]]["messages"][id]["date"], '%Y/%m/%d %H:%M:%S'))
|
||||
text = f"\n\n<small>{data['model'].split(':')[0].replace('-', ' ').title()} ({data['model'].split(':')[1]})\n{formated_date}</small>"
|
||||
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text))
|
||||
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(self.chats["chats"][self.chats["selected_chat"]]["messages"][id]["date"], '%Y/%m/%d %H:%M:%S')))
|
||||
text = f"\n\n{data['model'].split(':')[0].replace('-', ' ').title()} ({data['model'].split(':')[1]})\n<small>{formated_date}</small>"
|
||||
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text.encode('utf-8')))
|
||||
self.save_history()
|
||||
GLib.idle_add(self.bot_message_button_container.set_visible, True)
|
||||
#Notification
|
||||
@@ -1082,11 +1101,11 @@ Generate a title following these rules:
|
||||
for widget in list(self.chat_container): self.chat_container.remove(widget)
|
||||
for key, message in self.chats['chats'][self.chats["selected_chat"]]['messages'].items():
|
||||
if message:
|
||||
formated_date = self.generate_datetime_format(datetime.strptime(message['date'] + (":00" if message['date'].count(":") == 1 else ""), '%Y/%m/%d %H:%M:%S'))
|
||||
formated_date = GLib.markup_escape_text(self.generate_datetime_format(datetime.strptime(message['date'] + (":00" if message['date'].count(":") == 1 else ""), '%Y/%m/%d %H:%M:%S')))
|
||||
if message['role'] == 'user':
|
||||
self.show_message(message['content'], False, f"\n\n<small>{formated_date}</small>", message['images'] if 'images' in message else None, message['files'] if 'files' in message else None, id=key)
|
||||
else:
|
||||
self.show_message(message['content'], True, f"\n\n<small>{message['model'].split(':')[0].replace('-', ' ').title()} ({message['model'].split(':')[1]})\n{formated_date}</small>", id=key)
|
||||
self.show_message(message['content'], True, f"\n\n{message['model'].split(':')[0].replace('-', ' ').title()} ({message['model'].split(':')[1]})\n<small>{formated_date}</small>", id=key)
|
||||
self.add_code_blocks()
|
||||
self.bot_message = None
|
||||
|
||||
@@ -1234,9 +1253,10 @@ Generate a title following these rules:
|
||||
logger.debug("Showing preferences dialog")
|
||||
self.preferences_dialog.present(self)
|
||||
|
||||
def connect_remote(self, url):
|
||||
def connect_remote(self, url, bearer_token):
|
||||
logger.debug(f"Connecting to remote: {url}")
|
||||
connection_handler.url = url
|
||||
connection_handler.bearer_token = bearer_token
|
||||
self.remote_url = connection_handler.url
|
||||
self.remote_connection_entry.set_text(self.remote_url)
|
||||
if self.verify_connection() == False: self.connection_error()
|
||||
@@ -1253,7 +1273,7 @@ Generate a title following these rules:
|
||||
def connection_error(self):
|
||||
logger.error("Connection error")
|
||||
if self.run_remote:
|
||||
dialogs.reconnect_remote(self, connection_handler.url)
|
||||
dialogs.reconnect_remote(self, connection_handler.url, connection_handler.bearer_token)
|
||||
else:
|
||||
local_instance.reset()
|
||||
self.show_toast(_("There was an error with the local Ollama instance, so it has been reset"), self.main_overlay)
|
||||
@@ -1558,7 +1578,11 @@ Generate a title following these rules:
|
||||
if override in local_instance.overrides:
|
||||
element.set_text(local_instance.overrides[override])
|
||||
|
||||
|
||||
#Support dialog
|
||||
if 'show_support' not in data or data['show_support']:
|
||||
if random.randint(0, 99) == 0:
|
||||
dialogs.support(self)
|
||||
if 'show_support' in data: self.show_support = data['show_support']
|
||||
self.background_switch.set_active(self.run_on_background)
|
||||
self.set_hide_on_close(self.run_on_background)
|
||||
self.remote_connection_entry.set_text(self.remote_url)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<signal name="close-request" handler="closing_app"/>
|
||||
<property name="resizable">True</property>
|
||||
<property name="width-request">360</property>
|
||||
<property name="height-request">700</property>
|
||||
<property name="height-request">400</property>
|
||||
<property name="default-width">1300</property>
|
||||
<property name="default-height">800</property>
|
||||
<property name="title">Alpaca</property>
|
||||
@@ -244,7 +244,7 @@
|
||||
<property name="valign">3</property>
|
||||
<property name="tooltip-text" translatable="yes">Send Message</property>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
<class name="accent"/>
|
||||
<class name="circular"/>
|
||||
</style>
|
||||
<child>
|
||||
|
||||
Reference in New Issue
Block a user