41 Commits
1.0.2 ... 1.0.5

Author SHA1 Message Date
jeffser
8c0ec3957f Preparing for 1.0.5 2024-08-02 23:56:17 -06:00
jeffser
72063a15d9 Better check for message finishing 2024-08-02 23:48:03 -06:00
jeffser
0d1b15aafc New feature: Regenerate response 2024-08-02 23:42:35 -06:00
jeffser
ca10369bdc Added message to support dialog 2024-08-02 21:44:19 -06:00
jeffser
42af75d8d2 typo 2024-08-02 20:53:19 -06:00
jeffser
a02871dd28 'S fixed again :3 2024-08-02 20:50:04 -06:00
jeffser
e65a8bc648 Proper GGUF / name Model pulling 2024-08-02 20:47:04 -06:00
jeffser
b373b6a34f Sidebar button 2024-08-02 16:44:24 -06:00
jeffser
6d6a0255e2 Better model name handling internally 2024-08-02 16:00:47 -06:00
jeffser
003d6a3d5f Restore last model used 2024-08-02 15:30:03 -06:00
jeffser
77a2c60fe5 Fixed message entry shadow 2024-08-02 15:19:28 -06:00
jeffser
ac3bd699ee Changed width request for model dropdown 2024-08-02 14:51:08 -06:00
jeffser
596498c81e Fixed 'S problem with title generation 2024-08-02 14:42:11 -06:00
jeffser
c95f764c77 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-02 14:39:26 -06:00
jeffser
5c5be05843 Reverted back to standard styles 2024-08-02 14:39:19 -06:00
aritra saha
3fb26ec49e updated translation (#180)
* Update bn.po

* Update bn.po

* Update bn.po

* Update bn.po

* Update zh_CN.po
2024-08-02 11:19:23 -06:00
jeffser
3f767d22e9 New relase notes 2024-08-01 14:56:38 -06:00
jeffser
7f3fb0d82d Tweaks to chat title generation 2024-08-01 14:52:37 -06:00
jeffser
d56c132459 Fixed title of model tag selector dialog 2024-08-01 14:48:11 -06:00
jeffser
acdce762c9 Translations update 2024-08-01 14:37:06 -06:00
jeffser
bd557d9652 Preparing for 1.0.4 2024-08-01 14:21:55 -06:00
Jeffry Samuel
3363d13fa0 changed support dialog frequency 2024-08-01 14:03:38 -06:00
Jeffry Samuel
52ba44e260 Update README.md 2024-08-01 14:02:28 -06:00
Nokse22
f06c2dae23 Added tables (#179) 2024-08-01 13:44:56 -06:00
jeffser
55a636f4d1 typo 2024-08-01 01:30:20 -06:00
jeffser
0fc8730272 dialog added to version notes 2024-08-01 01:27:49 -06:00
jeffser
61a2bc466e Changed content rating because of the donation dialog 2024-08-01 01:13:14 -06:00
jeffser
62b1923bf4 Added support dialog 2024-08-01 01:11:27 -06:00
jeffser
8e25376a12 Merge branch 'main' of github.com-jeffser:Jeffser/Alpaca 2024-08-01 00:49:26 -06:00
jeffser
a9ab5d45a4 Preparing for 1.0.3 2024-08-01 00:49:19 -06:00
Jeffry Samuel
ce2a2f0b93 :) 2024-08-01 00:40:45 -06:00
jeffser
9cb6b0b665 Code block and markup fixes 2024-08-01 00:33:26 -06:00
jeffser
dfc21fc0e9 Fixed date format for Chinese translation 2024-07-31 23:32:49 -06:00
jeffser
19b089e6c6 Fixed problems with localization 2024-07-31 22:36:39 -06:00
jeffser
02aa2734e0 Changed window min height 2024-07-31 22:08:48 -06:00
jeffser
66f9fd7231 better check for connection 2024-07-31 21:17:31 -06:00
jeffser
1b125cb704 Appearance changes 2024-07-31 21:13:23 -06:00
jeffser
29f5d85c7b Added string to translation list 2024-07-31 19:55:30 -06:00
jeffser
c192a1f31c Added compatibility with generic code blocks without explicit language 2024-07-31 19:54:47 -06:00
jeffser
3b20daf807 Added bearer token entry to connection error dialog 2024-07-31 19:42:46 -06:00
jeffser
760c00e8ae Added German template 2024-07-30 12:48:20 -06:00
20 changed files with 8992 additions and 3773 deletions

View File

@@ -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
@@ -59,7 +59,7 @@ You can change anything except `$HOME` and `$OLLAMA_HOST`, to do this go to `~/.
- [TylerLaBree](https://github.com/TylerLaBree) for their requests and ideas
- [Alexkdeveloper](https://github.com/alexkdeveloper) for their help translating the app to Russian
- [Imbev](https://github.com/imbev) for their reports and suggestions
- [Nokse](https://github.com/Nokse22) for their contributions to the UI
- [Nokse](https://github.com/Nokse22) for their contributions to the UI and table rendering
- [Louis Chauvet-Villaret](https://github.com/loulou64490) for their suggestions and help translating the app to French
- [CounterFlow64](https://github.com/CounterFlow64) for their help translating the app to Norwegian

View File

@@ -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,61 @@
<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.5" date="2024-08-02">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.5</url>
<description>
<p>New</p>
<ul>
<li>Regenerate any response, even if they are incomplete</li>
<li>Support for pulling models by name:tag</li>
<li>Stable support for GGUF model files</li>
<li>Restored sidebar toggle button</li>
</ul>
<p>Fixes</p>
<ul>
<li>Reverted back to standard styles</li>
<li>Fixed generated titles having "'S" for some reason</li>
<li>Changed min width for model dropdown</li>
<li>Changed message entry shadow</li>
<li>The last model used is now restored when the user changes chat</li>
<li>Better check for message finishing</li>
</ul>
</description>
</release>
<release version="1.0.4" date="2024-08-01">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.4</url>
<description>
<p>New</p>
<ul>
<li>Added table rendering (Thanks Nokse)</li>
</ul>
<p>Fixes</p>
<ul>
<li>Made support dialog more common</li>
<li>Dialog title on tag chooser when downloading models didn't display properly</li>
<li>Prevent chat generation from generating a title with multiple lines</li>
</ul>
</description>
</release>
<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>

View File

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

File diff suppressed because it is too large Load Diff

1332
po/bn.po

File diff suppressed because it is too large Load Diff

1697
po/de.po Normal file

File diff suppressed because it is too large Load Diff

1335
po/es.po

File diff suppressed because it is too large Load Diff

1233
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

1310
po/ru.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@
<file alias="icons/scalable/status/edit-find-symbolic.svg">icons/edit-find-symbolic.svg</file>
<file alias="icons/scalable/status/edit-symbolic.svg">icons/edit-symbolic.svg</file>
<file alias="icons/scalable/status/image-missing-symbolic.svg">icons/image-missing-symbolic.svg</file>
<file alias="icons/scalable/status/update-symbolic.svg">icons/update-symbolic.svg</file>
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource>

View File

@@ -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
@@ -212,7 +224,7 @@ def create_model_from_existing_response(self, dialog, task, dropdown):
def create_model_from_existing(self):
string_list = Gtk.StringList()
for model in self.local_models:
string_list.append(model)
string_list.append(self.convert_model_name(model, 0))
dropdown = Gtk.DropDown()
dropdown.set_model(string_list)
@@ -245,6 +257,27 @@ 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))
def create_model_from_name_response(self, dialog, task, entry):
model = entry.get_text().lower().strip()
if dialog.choose_finish(task) == 'accept' and model:
self.pull_model(model)
def create_model_from_name(self):
entry = Gtk.Entry()
entry.get_delegate().connect("insert-text", self.check_alphanumeric)
dialog = Adw.AlertDialog(
heading=_("Pull Model"),
body=_("Input the name of the model in this format\nname:tag"),
extra_child=entry
)
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, entry=entry: create_model_from_name_response(self, dialog, task, entry)
)
# FILE CHOOSER | WORKS
def attach_file_response(self, file_dialog, result):
@@ -357,3 +390,33 @@ 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')
elif res == 'nope':
self.show_toast(_("Visit Alpaca's website if you change your mind!"), self.main_overlay)
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)
)

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 7.957031 2 c -0.082031 0 -0.164062 0.003906 -0.246093 0.007812 c -0.1875 0.011719 -0.375 0.03125 -0.5625 0.0625 c -1.582032 0.226563 -3.007813 1.070313 -3.96875 2.34375 c -0.804688 1.074219 -1.183594 2.332032 -1.179688 3.585938 h 2.003906 c 0 -0.832031 0.253906 -1.671875 0.796875 -2.398438 c 1.335938 -1.777343 3.820313 -2.113281 5.597657 -0.78125 c 0.429687 0.320313 0.769531 0.734376 1.03125 1.1875 h -1.4375 c -0.550782 0 -1 0.449219 -1 1 v 1 h 6 v -6 h -1 c -0.550782 0 -1 0.449219 -1 1 v 1.6875 c -1.113282 -1.695312 -3.007813 -2.710937 -5.039063 -2.695312 z m 0 0"/><path d="m 8.035156 15.007812 c 0.082032 0 0.164063 -0.003906 0.246094 -0.007812 c 0.1875 -0.011719 0.375 -0.03125 0.5625 -0.0625 c 1.582031 -0.226562 3.007812 -1.066406 3.96875 -2.34375 c 0.804688 -1.074219 1.183594 -2.332031 1.179688 -3.585938 h -2.003907 c -0.003906 0.832032 -0.257812 1.675782 -0.796875 2.398438 c -1.335937 1.777344 -3.820312 2.113281 -5.597656 0.78125 c -0.429688 -0.320312 -0.769531 -0.734375 -1.03125 -1.1875 h 1.4375 c 0.550781 0 1 -0.449219 1 -1 v -1 h -6 v 6 h 1 c 0.550781 0 1 -0.449219 1 -1 v -1.6875 c 1.113281 1.695312 3.007812 2.710938 5.035156 2.695312 z m 0 0"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -43,7 +43,8 @@ alpaca_sources = [
'dialogs.py',
'local_instance.py',
'available_models.json',
'available_models_descriptions.py'
'available_models_descriptions.py',
'table_widget.py'
]
install_data(alpaca_sources, install_dir: moduledir)

View File

@@ -2,7 +2,7 @@
box-shadow: none;
border-width: 0;
}
.message_text_view {
.message_text_view, .modelfile_textview {
background-color: rgba(0,0,0,0);
}
.chat_image_button {

126
src/table_widget.py Normal file
View File

@@ -0,0 +1,126 @@
import gi
from gi.repository import Adw
from gi.repository import Gtk, GObject, Gio
import re
class MarkdownTable:
def __init__(self):
self.headers = []
self.rows = Gio.ListStore()
self.alignments = []
def __repr__(self):
table_repr = 'Headers: {}\n'.format(self.headers)
table_repr += 'Alignments: {}\n'.format(self.alignments)
table_repr += 'Rows:\n'
for row in self.rows:
table_repr += ' | '.join(row) + '\n'
return table_repr
class Row(GObject.GObject):
def __init__(self, _values):
super().__init__()
self.values = _values
def get_column_value(self, index):
return self.values[index]
class TableWidget(Gtk.Frame):
__gtype_name__ = 'TableWidget'
def __init__(self, markdown):
super().__init__()
self.table = MarkdownTable()
self.set_halign(Gtk.Align.START)
self.table_widget = Gtk.ColumnView(
show_column_separators=True,
show_row_separators=True,
reorderable=False,
)
scrolled_window = Gtk.ScrolledWindow(
vscrollbar_policy=Gtk.PolicyType.NEVER,
propagate_natural_width=True
)
self.set_child(scrolled_window)
try:
self.parse_markdown_table(markdown)
self.make_table()
scrolled_window.set_child(self.table_widget)
except:
label = Gtk.Label(
label=markdown.lstrip('\n').rstrip('\n'),
selectable=True,
margin_top=6,
margin_bottom=6,
margin_start=6,
margin_end=6
)
scrolled_window.set_child(label)
def parse_markdown_table(self, markdown_text):
# Define regex patterns for matching the table components
header_pattern = r'^\|(.+?)\|$'
separator_pattern = r'^\|(\s*[:-]+:?\s*\|)+$'
row_pattern = r'^\|(.+?)\|$'
# Split the text into lines
lines = markdown_text.strip().split('\n')
# Extract headers
header_match = re.match(header_pattern, lines[0], re.MULTILINE)
if header_match:
headers = [header.strip() for header in header_match.group(1).replace("*", "").split('|') if header.strip()]
self.table.headers = headers
# Extract alignments
separator_match = re.match(separator_pattern, lines[1], re.MULTILINE)
if separator_match:
alignments = []
separator_columns = lines[1].replace(" ", "").split('|')[1:-1]
for sep in separator_columns:
if ':' in sep:
if sep.startswith('-') and sep.endswith(':'):
alignments.append(1)
elif sep.startswith(':') and sep.endswith('-'):
alignments.append(0)
else:
alignments.append(0.5)
else:
alignments.append(0) # Default alignment is start
self.table.alignments = alignments
# Extract rows
for line in lines[2:]:
row_match = re.match(row_pattern, line, re.MULTILINE)
if row_match:
rows = line.split('|')[1:-1]
row = Row(rows)
self.table.rows.append(row)
def make_table(self):
def _on_factory_setup(_factory, list_item, align):
label = Gtk.Label(xalign=align, ellipsize=3, selectable=True)
list_item.set_child(label)
def _on_factory_bind(_factory, list_item, index):
label_widget = list_item.get_child()
row = list_item.get_item()
label_widget.set_label(row.get_column_value(index))
for index, column_name in enumerate(self.table.headers):
column = Gtk.ColumnViewColumn(title=column_name, expand=True)
factory = Gtk.SignalListItemFactory()
factory.connect("setup", _on_factory_setup, self.table.alignments[index])
factory.connect("bind", _on_factory_bind, index)
column.set_factory(factory)
self.table_widget.append_column(column)
selection = Gtk.NoSelection.new(model=self.table.rows)
self.table_widget.set_model(model=selection)

View File

@@ -21,14 +21,14 @@ 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
from pypdf import PdfReader
from datetime import datetime
from . import dialogs, local_instance, connection_handler, available_models_descriptions
from .table_widget import TableWidget
logger = logging.getLogger(__name__)
@@ -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()
@@ -68,11 +68,11 @@ class AlpacaWindow(Adw.ApplicationWindow):
override_HIP_VISIBLE_DEVICES = Gtk.Template.Child()
#Elements
regenerate_button : Gtk.Button = None
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()
create_model_modelfile = Gtk.Template.Child()
temperature_spin = Gtk.Template.Child()
seed_spin = Gtk.Template.Child()
keep_alive_spin = Gtk.Template.Child()
@@ -135,7 +135,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
def verify_if_image_can_be_used(self, pspec=None, user_data=None):
logger.debug("Verifying if image can be used")
if self.model_drop_down.get_selected_item() == None: return True
selected = self.model_drop_down.get_selected_item().get_string().split(" (")[0].lower()
selected = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1).split(":")[0]
if selected in [key for key, value in self.available_models.items() if value["image"]]:
for name, content in self.attachments.items():
if content['type'] == 'image':
@@ -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()
@@ -183,8 +183,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.chats['order'].remove(self.chats['selected_chat'])
self.chats['order'].insert(0, self.chats['selected_chat'])
self.save_history()
current_model = self.model_drop_down.get_selected_item().get_string().split(' (')
current_model = '{}:{}'.format(current_model[0].replace(' ', '-').lower(), current_model[1][:-1])
current_model = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1)
if current_model is None:
self.show_toast(_("Please select a model before chatting"), self.main_overlay)
return
@@ -229,7 +228,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)
@@ -282,14 +281,19 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.chats["selected_chat"] = row.get_child().get_name()
self.load_history_into_chat()
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()) > 0:
last_model_used = self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]
last_model_used = self.convert_model_name(last_model_used, 0)
for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]:
if self.model_string_list.get_string(i) == last_model_used:
self.model_drop_down.set_selected(i)
break
self.save_history()
@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:
@@ -340,19 +344,18 @@ class AlpacaWindow(Adw.ApplicationWindow):
@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}"
name = self.create_model_name.get_text().lower().replace(":", "")
modelfile_buffer = self.create_model_modelfile.get_buffer()
modelfile_raw = modelfile_buffer.get_text(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter(), False)
modelfile = ["FROM {}".format(self.create_model_base.get_subtitle()), "SYSTEM {}".format(self.create_model_system.get_text())]
for line in modelfile_raw.split('\n'):
if not line.startswith('SYSTEM') and not line.startswith('FROM'):
modelfile.append(line)
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})
thread = threading.Thread(target=self.pull_model_process, kwargs={"model": name, "modelfile": '\n'.join(modelfile)})
overlay = Gtk.Overlay()
progress_bar = Gtk.ProgressBar(
valign = 2,
@@ -374,7 +377,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
overlay.set_child(model_row)
overlay.add_overlay(progress_bar)
self.pulling_model_list_box.append(overlay)
self.create_model_dialog.close()
self.navigation_view_manage_models.pop()
self.manage_models_dialog.present(self)
thread.start()
@@ -412,38 +415,37 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.available_model_list_box.set_visible(True)
self.no_results_page.set_visible(False)
def convert_model_name(self, name:str, mode:int) -> str: # mode=0 name:tag -> Name (tag) | mode=1 Name (tag) -> name:tag
if mode == 0: return "{} ({})".format(name.split(":")[0].replace("-", " ").title(), name.split(":")[1])
if mode == 1: return "{}:{}".format(name.split(" (")[0].replace(" ", "-").lower(), name.split(" (")[1][:-1])
def check_alphanumeric(self, editable, text, length, position):
new_text = ''.join([char for char in text if char.isalnum() or char in ['-', '_']])
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 = ""
modelfile_buffer = self.create_model_modelfile.get_buffer()
modelfile_buffer.delete(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter())
self.create_model_system.set_text('')
if not file:
response = connection_handler.simple_post(f"{connection_handler.url}/api/show", json.dumps({"name": model}))
response = connection_handler.simple_post(f"{connection_handler.url}/api/show", json.dumps({"name": self.convert_model_name(model, 1)}))
if response.status_code == 200:
data = json.loads(response.text)
modelfile = []
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]
self.create_model_system.set_text(line[len('SYSTEM'):].strip())
if not line.startswith('SYSTEM') and not line.startswith('FROM') and not line.startswith('#'):
modelfile.append(line)
self.create_model_name.set_text(self.convert_model_name(model, 1).split(':')[0] + "-custom")
modelfile_buffer.insert(modelfile_buffer.get_start_iter(), '\n'.join(modelfile), len('\n'.join(modelfile).encode('utf-8')))
else:
##TODO ERROR MESSAGE
return
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)
self.create_model_name.set_text(model.split("/")[-1].split(".")[0])
self.create_model_base.set_subtitle(self.convert_model_name(model, 1))
self.navigation_view_manage_models.push_by_tag('model_create_page')
def show_toast(self, message:str, overlay):
logger.info(message)
@@ -571,13 +573,12 @@ Generate a title following these rules:
```PROMPT
{message['content']}
```"""
current_model = self.model_drop_down.get_selected_item().get_string().split(' (')
current_model = '{}:{}'.format(current_model[0].replace(' ', '-').lower(), current_model[1][:-1])
current_model = self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1)
data = {"model": current_model, "prompt": prompt, "stream": False}
if 'images' in message: data["images"] = message['images']
response = connection_handler.simple_post(f"{connection_handler.url}/api/generate", data=json.dumps(data))
new_chat_name = json.loads(response.text)["response"].strip().removeprefix("Title: ").removeprefix("title: ").strip('\'"').title()
new_chat_name = json.loads(response.text)["response"].strip().removeprefix("Title: ").removeprefix("title: ").strip('\'"').replace('\n', ' ').title().replace('\'S', '\'s')
new_chat_name = new_chat_name[:50] + (new_chat_name[50:] and '...')
self.rename_chat(label_element.get_name(), new_chat_name, label_element)
@@ -596,7 +597,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",
@@ -613,6 +614,11 @@ Generate a title following these rules:
css_classes = ["flat", "circular"],
tooltip_text = _("Edit Message")
)
regenerate_button = Gtk.Button(
icon_name = "update-symbolic",
css_classes = ["flat", "circular"],
tooltip_text = _("Regenerate Message")
)
button_container = Gtk.Box(
orientation=0,
@@ -728,9 +734,10 @@ Generate a title following these rules:
delete_button.connect("clicked", lambda button, element=overlay: self.delete_message(element))
copy_button.connect("clicked", lambda button, element=overlay: self.copy_message(element))
edit_button.connect("clicked", lambda button, element=overlay, textview=message_text, button_container=button_container: self.edit_message(element, textview, button_container))
regenerate_button.connect('clicked', lambda button, id=id, bot_message_box=message_box, bot_message_button_container=button_container : self.regenerate_message(id, bot_message_box, bot_message_button_container))
button_container.append(delete_button)
button_container.append(copy_button)
if not bot: button_container.append(edit_button)
button_container.append(regenerate_button if bot else edit_button)
overlay.add_overlay(button_container)
self.chat_container.append(overlay)
@@ -753,39 +760,43 @@ Generate a title following these rules:
else:
self.local_model_list_box.set_visible(True)
for model in json.loads(response.text)['models']:
model_name = self.convert_model_name(model["name"], 0)
model_row = Adw.ActionRow(
title = "<b>{}</b>".format(model["name"].split(":")[0].replace("-", " ").title()),
subtitle = model["name"].split(":")[1]
title = "<b>{}</b>".format(model_name.split(" (")[0]),
subtitle = model_name.split(" (")[1][:-1]
)
button = Gtk.Button(
icon_name = "user-trash-symbolic",
vexpand = False,
valign = 3,
css_classes = ["error", "circular"],
tooltip_text = _("Remove '{} ({})'").format(model["name"].split(":")[0].replace('-', ' ').title(), model["name"].split(":")[1])
tooltip_text = _("Remove '{}'").format(model_name)
)
button.connect("clicked", lambda button=button, model_name=model["name"]: dialogs.delete_model(self, model_name))
model_row.add_suffix(button)
self.local_model_list_box.append(model_row)
self.model_string_list.append(f"{model['name'].split(':')[0].replace('-', ' ').title()} ({model['name'].split(':')[1]})")
self.model_string_list.append(model_name)
self.local_models.append(model["name"])
self.model_drop_down.set_selected(0)
self.verify_if_image_can_be_used()
#self.verify_if_image_can_be_used()
return
else:
self.connection_error()
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 +814,33 @@ 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
# Match tables
table_pattern = re.compile(r'((\r?\n){2}|^)([^\r\n]*\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)', re.MULTILINE)
for match in table_pattern.finditer(text):
start, end = match.span()
if pos < start:
normal_text = text[pos:start]
parts.append({"type": "normal", "text": normal_text.strip()})
table_text = match.group(0)
parts.append({"type": "table", "text": table_text})
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 +874,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'])
elif part['type'] == 'code':
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 +904,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)
@@ -880,6 +913,9 @@ Generate a title following these rules:
code_block_box.append(source_view)
self.bot_message_box.append(code_block_box)
self.style_manager.connect("notify::dark", self.on_theme_changed, buffer)
elif part['type'] == 'table':
table = TableWidget(part['text'])
self.bot_message_box.append(table)
vadjustment = self.chat_window.get_vadjustment()
vadjustment.set_value(vadjustment.get_upper())
self.bot_message = None
@@ -916,25 +952,19 @@ Generate a title following these rules:
vadjustment = self.chat_window.get_vadjustment()
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))
if 'done' in data and data['done']:
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{self.convert_model_name(data['model'], 0)}\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
first_paragraph = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), False).split("\n")[0]
GLib.idle_add(self.show_notification, self.chats["selected_chat"], first_paragraph[:100] + (first_paragraph[100:] and '...'), Gio.ThemedIcon.new("chat-message-new-symbolic"))
else:
if id not in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
if not self.chats["chats"][self.chats["selected_chat"]]["messages"][id]["content"] and self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None
self.chats["chats"][self.chats["selected_chat"]]["messages"][id] = {
"role": "assistant",
"model": data['model'],
"date": datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
"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"][id]['content'] += data['message']['content']
@@ -949,17 +979,69 @@ Generate a title following these rules:
def run_message(self, messages, model, id):
logger.debug("Running message")
self.bot_message_button_container.set_visible(False)
response = connection_handler.stream_post(f"{connection_handler.url}/api/chat", data=json.dumps({"model": model, "messages": messages}), callback=lambda data, id=id: self.update_bot_message(data, id))
GLib.idle_add(self.add_code_blocks)
GLib.idle_add(self.switch_send_stop_button)
GLib.idle_add(self.toggle_ui_sensitive, True)
if self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None
if response.status_code != 200:
self.chats["chats"][self.chats["selected_chat"]]["messages"][id] = {
"role": "assistant",
"model": model,
"date": datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
"content": ''
}
if self.regenerate_button:
GLib.idle_add(self.chat_container.remove, self.regenerate_button)
try:
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))
if response.status_code != 200: raise Exception('Network Error')
GLib.idle_add(self.add_code_blocks)
except Exception as e:
GLib.idle_add(self.connection_error)
self.regenerate_button = Gtk.Button(
child=Adw.ButtonContent(
icon_name='update-symbolic',
label=_('Regenerate Response')
),
css_classes=["suggested-action"],
halign=3
)
GLib.idle_add(self.chat_container.append, self.regenerate_button)
self.regenerate_button.connect('clicked', lambda button, id=id, bot_message_box=self.bot_message_box, bot_message_button_container=self.bot_message_button_container : self.regenerate_message(id, bot_message_box, bot_message_button_container))
finally:
GLib.idle_add(self.switch_send_stop_button)
GLib.idle_add(self.toggle_ui_sensitive, True)
if self.loading_spinner:
GLib.idle_add(self.chat_container.remove, self.loading_spinner)
self.loading_spinner = None
def regenerate_message(self, id, bot_message_box, bot_message_button_container):
self.bot_message_button_container = bot_message_button_container
self.bot_message_view = Gtk.TextView(
editable=False,
focusable=True,
wrap_mode= Gtk.WrapMode.WORD,
margin_top=12,
margin_bottom=12,
hexpand=True,
css_classes=["flat"]
)
self.bot_message = self.bot_message_view.get_buffer()
for widget in list(bot_message_box): bot_message_box.remove(widget)
bot_message_box.append(self.bot_message_view)
history = self.convert_history_to_ollama()[:list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()).index(id)]
if id in self.chats["chats"][self.chats["selected_chat"]]["messages"]:
del self.chats["chats"][self.chats["selected_chat"]]["messages"][id]
data = {
"model": self.convert_model_name(self.model_drop_down.get_selected_item().get_string(), 1),
"messages": history,
"options": {"temperature": self.model_tweaks["temperature"], "seed": self.model_tweaks["seed"]},
"keep_alive": f"{self.model_tweaks['keep_alive']}m"
}
self.switch_send_stop_button()
self.toggle_ui_sensitive(False)
thread = threading.Thread(target=self.run_message, args=(data['messages'], data['model'], id))
thread.start()
def pull_model_update(self, data, model_name):
if 'error' in data:
self.pulling_models[model_name]['error'] = data['error']
return
if model_name in list(self.pulling_models.keys()):
if 'completed' in data and 'total' in data:
GLib.idle_add(self.pulling_models[model_name]['row'].set_subtitle, '<tt>{}%</tt>'.format(round(data['completed'] / data['total'] * 100, 2)))
@@ -980,28 +1062,31 @@ Generate a title following these rules:
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)
if response.status_code == 200:
if response.status_code == 200 and 'error' not in self.pulling_models[model]:
GLib.idle_add(self.show_notification, _("Task Complete"), _("Model '{}' pulled successfully.").format(model), Gio.ThemedIcon.new("emblem-ok-symbolic"))
GLib.idle_add(self.show_toast, _("Model '{}' pulled successfully.").format(model), self.manage_models_overlay)
GLib.idle_add(self.pulling_models[model]['overlay'].get_parent().get_parent().remove, self.pulling_models[model]['overlay'].get_parent())
del self.pulling_models[model]
elif response.status_code == 200 and self.pulling_models[model]['error']:
GLib.idle_add(self.show_notification, _("Pull Model Error"), _("Failed to pull model '{}': {}").format(model, self.pulling_models[model]['error']), Gio.ThemedIcon.new("dialog-error-symbolic"))
GLib.idle_add(self.show_toast, _("Error pulling '{}': {}").format(model, self.pulling_models[model]['error']), self.manage_models_overlay)
else:
GLib.idle_add(self.show_notification, _("Pull Model Error"), _("Failed to pull model '{}' due to network error.").format(model), Gio.ThemedIcon.new("dialog-error-symbolic"))
GLib.idle_add(self.pulling_models[model]['overlay'].get_parent().get_parent().remove, self.pulling_models[model]['overlay'].get_parent())
del self.pulling_models[model]
GLib.idle_add(self.show_toast, _("Error pulling '{}'").format(model), self.manage_models_overlay)
GLib.idle_add(self.manage_models_dialog.close)
GLib.idle_add(self.connection_error)
GLib.idle_add(self.pulling_models[model]['overlay'].get_parent().get_parent().remove, self.pulling_models[model]['overlay'].get_parent())
del self.pulling_models[model]
if len(list(self.pulling_models.keys())) == 0:
GLib.idle_add(self.pulling_model_list_box.set_visible, False)
def pull_model(self, model):
if model in list(self.pulling_models.keys()) or model in self.local_models or ":" not in model: return
logger.info("Pulling model")
if model in list(self.pulling_models.keys()) or model in self.local_models:
return
self.pulling_model_list_box.set_visible(True)
#self.pulling_model_list_box.connect('row_selected', lambda list_box, row: dialogs.stop_pull_model(self, row.get_name()) if row else None) #It isn't working for some reason
model_name = self.convert_model_name(model, 0)
model_row = Adw.ActionRow(
title = "<b>{}</b> <small>{}</small>".format(model.split(":")[0].replace("-", " ").title(), model.split(":")[1]),
title = "<b>{}</b> <small>{}</small>".format(model_name.split(" (")[0], model_name.split(" (")[1][:-1]),
name = model
)
thread = threading.Thread(target=self.pull_model_process, kwargs={"model": model, "modelfile": None})
@@ -1018,7 +1103,7 @@ Generate a title following these rules:
vexpand = False,
valign = 3,
css_classes = ["error", "circular"],
tooltip_text = _("Stop Pulling '{} ({})'").format(model.split(':')[0].replace('-', ' ').title(), model.split(':')[1])
tooltip_text = _("Stop Pulling '{}'").format(model_name)
)
button.connect("clicked", lambda button, model_name=model : dialogs.stop_pull_model(self, model_name))
model_row.add_suffix(button)
@@ -1037,7 +1122,7 @@ Generate a title following these rules:
def list_available_model_tags(self, model_name):
logger.debug("Listing available model tags")
self.navigation_view_manage_models.push_by_tag('model_tags_page')
self.navigation_view_manage_models.find_page('model_tags_page').set_title(model_name.capitalize())
self.navigation_view_manage_models.find_page('model_tags_page').set_title(model_name.replace("-", " ").title())
self.model_link_button.set_name(self.available_models[model_name]['url'])
self.model_link_button.set_tooltip_text(self.available_models[model_name]['url'])
self.available_model_list_box.unselect_all()
@@ -1082,11 +1167,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{self.convert_model_name(message['model'], 0)}\n<small>{formated_date}</small>", id=key)
self.add_code_blocks()
self.bot_message = None
@@ -1102,6 +1187,13 @@ Generate a title following these rules:
self.chats["order"] = []
for chat_name in self.chats["chats"].keys():
self.chats["order"].append(chat_name)
if len(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys()) > 0:
last_model_used = self.chats["chats"][self.chats["selected_chat"]]["messages"][list(self.chats["chats"][self.chats["selected_chat"]]["messages"].keys())[-1]]["model"]
last_model_used = self.convert_model_name(last_model_used, 0)
for i in range(self.model_string_list.get_n_items()):
if self.model_string_list.get_string(i) == last_model_used:
self.model_drop_down.set_selected(i)
break
except Exception as e:
logger.error(e)
self.chats = {"chats": {}, "selected_chat": None, "order": []}
@@ -1234,9 +1326,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 +1346,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)
@@ -1521,6 +1614,7 @@ Generate a title following these rules:
self.get_application().create_action('import_chat', lambda *_: self.import_chat(), ['<primary>i'])
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('create_model_from_name', lambda *_: dialogs.create_model_from_name(self))
self.get_application().create_action('delete_chat', self.chat_actions)
self.get_application().create_action('rename_chat', self.chat_actions)
self.get_application().create_action('rename_current_chat', self.current_chat_actions)
@@ -1558,7 +1652,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, 49) == 0 or True:
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)

View File

@@ -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>
@@ -14,7 +14,6 @@
<object class="AdwBreakpoint">
<condition>max-width: 800sp</condition>
<setter object="split_view_overlay" property="collapsed">true</setter>
<setter object="show_sidebar_button" property="visible">true</setter>
</object>
</child>
<child>
@@ -23,10 +22,8 @@
<object class="AdwBreakpoint">
<condition>max-width: 500sp</condition>
<setter object="split_view_overlay" property="collapsed">true</setter>
<setter object="show_sidebar_button" property="visible">true</setter>
<setter object="welcome_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="file_preview_dialog" property="width-request">360</setter>
</object>
@@ -80,7 +77,6 @@
<object class="AdwHeaderBar" id="header_bar">
<child type="start">
<object class="GtkToggleButton" id="show_sidebar_button">
<property name="visible">false</property>
<property name="icon-name">sidebar-show-symbolic</property>
<property name="tooltip-text" translatable="yes">Toggle Sidebar</property>
<property name="active" bind-source="split_view_overlay" bind-property="show-sidebar" bind-flags="sync-create"/>
@@ -93,7 +89,7 @@
<child>
<object class="GtkDropDown" id="model_drop_down">
<signal name="notify" handler="verify_if_image_can_be_used"/>
<property name="width-request">150</property>
<property name="width-request">175</property>
<property name="enable-search">true</property>
<property name="tooltip-text">Select Model</property>
<property name="model">
@@ -217,6 +213,8 @@
<object class="GtkScrolledWindow">
<property name="max-content-height">150</property>
<property name="propagate-natural-height">true</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<style>
<class name="message_input_scroll_window"/>
</style>
@@ -226,8 +224,6 @@
<class name="message_text_view"/>
</style>
<property name="wrap-mode">word</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="top-margin">10</property>
<property name="bottom-margin">10</property>
<property name="hexpand">true</property>
@@ -244,8 +240,9 @@
<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"/>
<class name="suggested-action"/>
</style>
<child>
<object class="AdwButtonContent">
@@ -456,127 +453,6 @@
</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"/>
<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 class="AdwDialog" id="manage_models_dialog">
<property name="can-close">true</property>
<property name="width-request">400</property>
@@ -730,6 +606,143 @@
</property>
</object>
</child>
<child>
<object class="AdwNavigationPage">
<property name="title" translatable="yes">Create Model</property>
<property name="tag">model_create_page</property>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkButton">
<signal name="clicked" handler="link_button_handler"/>
<property name="icon-name">globe-symbolic</property>
</object>
</child>
</object>
</child>
<property name="content">
<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="sensitive">false</property>
<property name="subtitle"/>
<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="GtkBox">
<property name="height-request">140</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<style>
<class name="card"/>
</style>
<child>
<object class="GtkScrolledWindow">
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<child>
<object class="GtkTextView" id="create_model_modelfile">
<style>
<class name="modelfile_textview"/>
</style>
<property name="wrap-mode">word</property>
<property name="top-margin">10</property>
<property name="bottom-margin">10</property>
<property name="hexpand">true</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Some models require a modelfile, Alpaca fills FROM and SYSTEM (context) instructions automatically. Please visit the model's website or Ollama documentation for more information if you're unsure.</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="halign">1</property>
<property name="wrap">true</property>
<style>
<class name="caption"/>
</style>
</object>
</child>
</object>
</child>
<child>
<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>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
@@ -1085,9 +1098,13 @@ By downloading any model you accept their license agreement available on the mod
<attribute name="action">app.create_model_from_existing</attribute>
</item>
<item>
<attribute name="label" translatable="yes">From GGUF File (Experimental)</attribute>
<attribute name="label" translatable="yes">From GGUF File</attribute>
<attribute name="action">app.create_model_from_file</attribute>
</item>
<item>
<attribute name="label" translatable="yes">From Name</attribute>
<attribute name="action">app.create_model_from_name</attribute>
</item>
</section>
</menu>
<object class="GtkFileFilter" id="file_filter_attachments">