Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f767d22e9 | ||
|
|
7f3fb0d82d | ||
|
|
d56c132459 | ||
|
|
acdce762c9 | ||
|
|
bd557d9652 | ||
|
|
3363d13fa0 | ||
|
|
52ba44e260 | ||
|
|
f06c2dae23 | ||
|
|
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
|
- Import / Export chats
|
||||||
- Delete / Edit messages
|
- Delete / Edit messages
|
||||||
- YouTube recognition (Ask questions about a YouTube video using the transcript)
|
- 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
|
## Screenies
|
||||||
Chatting with a model | Image recognition | Code highlighting
|
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
|
- [TylerLaBree](https://github.com/TylerLaBree) for their requests and ideas
|
||||||
- [Alexkdeveloper](https://github.com/alexkdeveloper) for their help translating the app to Russian
|
- [Alexkdeveloper](https://github.com/alexkdeveloper) for their help translating the app to Russian
|
||||||
- [Imbev](https://github.com/imbev) for their reports and suggestions
|
- [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
|
- [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
|
- [CounterFlow64](https://github.com/CounterFlow64) for their help translating the app to Norwegian
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,9 @@
|
|||||||
<caption>Multiple models being downloaded</caption>
|
<caption>Multiple models being downloaded</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://jeffser.com/alpaca/</url>
|
<url type="homepage">https://jeffser.com/alpaca/</url>
|
||||||
<url type="donation">https://github.com/sponsors/Jeffser</url>
|
<url type="donation">https://github.com/sponsors/Jeffser</url>
|
||||||
@@ -78,6 +80,40 @@
|
|||||||
<url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url>
|
<url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url>
|
||||||
<url type="vcs-browser">https://github.com/Jeffser/Alpaca</url>
|
<url type="vcs-browser">https://github.com/Jeffser/Alpaca</url>
|
||||||
<releases>
|
<releases>
|
||||||
|
<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">
|
<release version="1.0.2" date="2024-07-29">
|
||||||
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.2</url>
|
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/1.0.2</url>
|
||||||
<description>
|
<description>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
project('Alpaca', 'c',
|
project('Alpaca', 'c',
|
||||||
version: '1.0.2',
|
version: '1.0.4',
|
||||||
meson_version: '>= 0.62.0',
|
meson_version: '>= 0.62.0',
|
||||||
default_options: [ 'warning_level=2', 'werror=false', ],
|
default_options: [ 'warning_level=2', 'werror=false', ],
|
||||||
)
|
)
|
||||||
|
|||||||
1218
po/alpaca.pot
1218
po/alpaca.pot
File diff suppressed because it is too large
Load Diff
1303
po/nb_NO.po
1303
po/nb_NO.po
File diff suppressed because it is too large
Load Diff
1235
po/pt_BR.po
1235
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
1245
po/zh_CN.po
1245
po/zh_CN.po
File diff suppressed because it is too large
Load Diff
@@ -172,26 +172,38 @@ def remove_attached_file(self, name):
|
|||||||
|
|
||||||
# RECONNECT REMOTE | WORKS
|
# 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)
|
response = dialog.choose_finish(task)
|
||||||
if not task or response == "remote":
|
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":
|
elif response == "local":
|
||||||
self.connect_local()
|
self.connect_local()
|
||||||
elif response == "close":
|
elif response == "close":
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def reconnect_remote(self, current_url):
|
def reconnect_remote(self, current_url, current_bearer_token):
|
||||||
entry = Gtk.Entry(
|
entry_url = Gtk.Entry(
|
||||||
css_classes = ["error"],
|
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(
|
dialog = Adw.AlertDialog(
|
||||||
heading=_("Connection Error"),
|
heading=_("Connection Error"),
|
||||||
body=_("The remote instance has disconnected"),
|
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("close", _("Close Alpaca"))
|
||||||
dialog.add_response("local", _("Use local instance"))
|
dialog.add_response("local", _("Use local instance"))
|
||||||
dialog.add_response("remote", _("Connect"))
|
dialog.add_response("remote", _("Connect"))
|
||||||
@@ -199,7 +211,7 @@ def reconnect_remote(self, current_url):
|
|||||||
dialog.choose(
|
dialog.choose(
|
||||||
parent = self,
|
parent = self,
|
||||||
cancellable = None,
|
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
|
# CREATE MODEL | WORKS
|
||||||
@@ -357,3 +369,31 @@ def attach_website(self, url):
|
|||||||
cancellable = None,
|
cancellable = None,
|
||||||
callback = lambda dialog, task, url=url: attach_website_response(self, dialog, task, url)
|
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)
|
||||||
|
)
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ alpaca_sources = [
|
|||||||
'dialogs.py',
|
'dialogs.py',
|
||||||
'local_instance.py',
|
'local_instance.py',
|
||||||
'available_models.json',
|
'available_models.json',
|
||||||
'available_models_descriptions.py'
|
'available_models_descriptions.py',
|
||||||
|
'table_widget.py'
|
||||||
]
|
]
|
||||||
|
|
||||||
install_data(alpaca_sources, install_dir: moduledir)
|
install_data(alpaca_sources, install_dir: moduledir)
|
||||||
|
|||||||
@@ -12,3 +12,7 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 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);
|
||||||
|
}
|
||||||
|
|||||||
126
src/table_widget.py
Normal file
126
src/table_widget.py
Normal 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)
|
||||||
@@ -21,14 +21,14 @@ import gi
|
|||||||
gi.require_version('GtkSource', '5')
|
gi.require_version('GtkSource', '5')
|
||||||
gi.require_version('GdkPixbuf', '2.0')
|
gi.require_version('GdkPixbuf', '2.0')
|
||||||
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
|
from gi.repository import Adw, Gtk, Gdk, GLib, GtkSource, Gio, GdkPixbuf
|
||||||
import json, requests, threading, os, re, base64, sys, gettext, locale, 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 time import sleep
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pypdf import PdfReader
|
from pypdf import PdfReader
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from . import dialogs, local_instance, connection_handler, available_models_descriptions
|
from . import dialogs, local_instance, connection_handler, available_models_descriptions
|
||||||
|
from .table_widget import TableWidget
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -44,7 +44,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locale')
|
localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'locale')
|
||||||
|
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
|
||||||
gettext.bindtextdomain('com.jeffser.Alpaca', localedir)
|
gettext.bindtextdomain('com.jeffser.Alpaca', localedir)
|
||||||
gettext.textdomain('com.jeffser.Alpaca')
|
gettext.textdomain('com.jeffser.Alpaca')
|
||||||
_ = gettext.gettext
|
_ = gettext.gettext
|
||||||
@@ -61,6 +60,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
|||||||
pulling_models = {}
|
pulling_models = {}
|
||||||
chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat", "order": []}
|
chats = {"chats": {_("New Chat"): {"messages": {}}}, "selected_chat": "New Chat", "order": []}
|
||||||
attachments = {}
|
attachments = {}
|
||||||
|
show_support = True
|
||||||
|
|
||||||
#Override elements
|
#Override elements
|
||||||
override_HSA_OVERRIDE_GFX_VERSION = Gtk.Template.Child()
|
override_HSA_OVERRIDE_GFX_VERSION = Gtk.Template.Child()
|
||||||
@@ -167,7 +167,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
|||||||
buffer = self.editing_message["text_view"].get_buffer()
|
buffer = self.editing_message["text_view"].get_buffer()
|
||||||
text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).rstrip('\n')
|
text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).rstrip('\n')
|
||||||
footer = "<small>" + self.editing_message["footer"] + "</small>"
|
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.chats["chats"][self.chats["selected_chat"]]["messages"][self.editing_message["id"]]["content"] = text
|
||||||
self.editing_message = None
|
self.editing_message = None
|
||||||
self.save_history()
|
self.save_history()
|
||||||
@@ -229,7 +229,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
#self.attachments[name] = {"path": file_path, "type": file_type, "content": content}
|
#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)
|
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.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.message_text_view.get_buffer().set_text("", 0)
|
||||||
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
|
self.loading_spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
|
||||||
@@ -290,6 +290,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def change_remote_url(self, entry):
|
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()
|
self.remote_url = entry.get_text()
|
||||||
logger.debug(f"Changing remote url: {self.remote_url}")
|
logger.debug(f"Changing remote url: {self.remote_url}")
|
||||||
if self.run_remote:
|
if self.run_remote:
|
||||||
@@ -577,7 +580,7 @@ Generate a title following these rules:
|
|||||||
if 'images' in message: data["images"] = message['images']
|
if 'images' in message: data["images"] = message['images']
|
||||||
response = connection_handler.simple_post(f"{connection_handler.url}/api/generate", data=json.dumps(data))
|
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()
|
||||||
new_chat_name = new_chat_name[:50] + (new_chat_name[50:] and '...')
|
new_chat_name = new_chat_name[:50] + (new_chat_name[50:] and '...')
|
||||||
self.rename_chat(label_element.get_name(), new_chat_name, label_element)
|
self.rename_chat(label_element.get_name(), new_chat_name, label_element)
|
||||||
|
|
||||||
@@ -596,7 +599,7 @@ Generate a title following these rules:
|
|||||||
)
|
)
|
||||||
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))
|
if footer is not None: message_buffer.insert_markup(message_buffer.get_end_iter(), footer, len(footer.encode('utf-8')))
|
||||||
|
|
||||||
delete_button = Gtk.Button(
|
delete_button = Gtk.Button(
|
||||||
icon_name = "user-trash-symbolic",
|
icon_name = "user-trash-symbolic",
|
||||||
@@ -778,14 +781,18 @@ Generate a title following these rules:
|
|||||||
|
|
||||||
def save_server_config(self):
|
def save_server_config(self):
|
||||||
with open(os.path.join(self.config_dir, "server.json"), "w+") as f:
|
with open(os.path.join(self.config_dir, "server.json"), "w+") as f:
|
||||||
json.dump({'remote_url': self.remote_url, '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):
|
def verify_connection(self):
|
||||||
|
try:
|
||||||
response = connection_handler.simple_get(f"{connection_handler.url}/api/tags")
|
response = connection_handler.simple_get(f"{connection_handler.url}/api/tags")
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
self.save_server_config()
|
self.save_server_config()
|
||||||
self.update_list_local_models()
|
self.update_list_local_models()
|
||||||
return response.status_code == 200
|
return response.status_code == 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return False
|
||||||
|
|
||||||
def add_code_blocks(self):
|
def add_code_blocks(self):
|
||||||
text = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), True)
|
text = self.bot_message.get_text(self.bot_message.get_start_iter(), self.bot_message.get_end_iter(), True)
|
||||||
@@ -803,13 +810,33 @@ Generate a title following these rules:
|
|||||||
code_text = match.group(2)
|
code_text = match.group(2)
|
||||||
parts.append({"type": "code", "text": code_text, "language": language})
|
parts.append({"type": "code", "text": code_text, "language": language})
|
||||||
pos = end
|
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
|
# Extract any remaining normal text after the last code block
|
||||||
if pos < len(text):
|
if pos < len(text):
|
||||||
normal_text = text[pos:]
|
normal_text = text[pos:]
|
||||||
if normal_text.strip():
|
if normal_text.strip():
|
||||||
parts.append({"type": "normal", "text": normal_text.strip()})
|
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||||
bold_pattern = re.compile(r'\*\*(.*?)\*\*') #"**text**"
|
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"
|
h1_pattern = re.compile(r'^#\s(.*)$') #"# text"
|
||||||
h2_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
|
markup_pattern = re.compile(r'<(b|u|tt|span.*)>(.*?)<\/(b|u|tt|span)>') #heh butt span, I'm so funny
|
||||||
@@ -843,16 +870,18 @@ Generate a title following these rules:
|
|||||||
start, end = match.span()
|
start, end = match.span()
|
||||||
if position < start:
|
if position < start:
|
||||||
message_buffer.insert(message_buffer.get_end_iter(), part['text'][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
|
position = end
|
||||||
|
|
||||||
if position < len(part['text']):
|
if position < len(part['text']):
|
||||||
message_buffer.insert(message_buffer.get_end_iter(), part['text'][position:])
|
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)
|
self.bot_message_box.append(message_text)
|
||||||
else:
|
elif part['type'] == 'code':
|
||||||
|
language = None
|
||||||
|
if part['language']:
|
||||||
language = GtkSource.LanguageManager.get_default().get_language(part['language'])
|
language = GtkSource.LanguageManager.get_default().get_language(part['language'])
|
||||||
if language:
|
if language:
|
||||||
buffer = GtkSource.Buffer.new_with_language(language)
|
buffer = GtkSource.Buffer.new_with_language(language)
|
||||||
@@ -871,7 +900,7 @@ Generate a title following these rules:
|
|||||||
source_view.set_editable(False)
|
source_view.set_editable(False)
|
||||||
code_block_box = Gtk.Box(css_classes=["card"], orientation=1, overflow=1)
|
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 = 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 = 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)
|
copy_button.connect("clicked", self.on_copy_code_clicked, buffer)
|
||||||
title_box.append(copy_button)
|
title_box.append(copy_button)
|
||||||
@@ -880,6 +909,9 @@ Generate a title following these rules:
|
|||||||
code_block_box.append(source_view)
|
code_block_box.append(source_view)
|
||||||
self.bot_message_box.append(code_block_box)
|
self.bot_message_box.append(code_block_box)
|
||||||
self.style_manager.connect("notify::dark", self.on_theme_changed, buffer)
|
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 = self.chat_window.get_vadjustment()
|
||||||
vadjustment.set_value(vadjustment.get_upper())
|
vadjustment.set_value(vadjustment.get_upper())
|
||||||
self.bot_message = None
|
self.bot_message = None
|
||||||
@@ -917,9 +949,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():
|
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())
|
GLib.idle_add(vadjustment.set_value, vadjustment.get_upper())
|
||||||
if data['done']:
|
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'))
|
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<small>{data['model'].split(':')[0].replace('-', ' ').title()} ({data['model'].split(':')[1]})\n{formated_date}</small>"
|
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))
|
GLib.idle_add(self.bot_message.insert_markup, self.bot_message.get_end_iter(), text, len(text.encode('utf-8')))
|
||||||
self.save_history()
|
self.save_history()
|
||||||
GLib.idle_add(self.bot_message_button_container.set_visible, True)
|
GLib.idle_add(self.bot_message_button_container.set_visible, True)
|
||||||
#Notification
|
#Notification
|
||||||
@@ -1037,7 +1069,7 @@ Generate a title following these rules:
|
|||||||
def list_available_model_tags(self, model_name):
|
def list_available_model_tags(self, model_name):
|
||||||
logger.debug("Listing available model tags")
|
logger.debug("Listing available model tags")
|
||||||
self.navigation_view_manage_models.push_by_tag('model_tags_page')
|
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_name(self.available_models[model_name]['url'])
|
||||||
self.model_link_button.set_tooltip_text(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()
|
self.available_model_list_box.unselect_all()
|
||||||
@@ -1082,11 +1114,11 @@ Generate a title following these rules:
|
|||||||
for widget in list(self.chat_container): self.chat_container.remove(widget)
|
for widget in list(self.chat_container): self.chat_container.remove(widget)
|
||||||
for key, message in self.chats['chats'][self.chats["selected_chat"]]['messages'].items():
|
for key, message in self.chats['chats'][self.chats["selected_chat"]]['messages'].items():
|
||||||
if message:
|
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':
|
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)
|
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:
|
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.add_code_blocks()
|
||||||
self.bot_message = None
|
self.bot_message = None
|
||||||
|
|
||||||
@@ -1234,9 +1266,10 @@ Generate a title following these rules:
|
|||||||
logger.debug("Showing preferences dialog")
|
logger.debug("Showing preferences dialog")
|
||||||
self.preferences_dialog.present(self)
|
self.preferences_dialog.present(self)
|
||||||
|
|
||||||
def connect_remote(self, url):
|
def connect_remote(self, url, bearer_token):
|
||||||
logger.debug(f"Connecting to remote: {url}")
|
logger.debug(f"Connecting to remote: {url}")
|
||||||
connection_handler.url = url
|
connection_handler.url = url
|
||||||
|
connection_handler.bearer_token = bearer_token
|
||||||
self.remote_url = connection_handler.url
|
self.remote_url = connection_handler.url
|
||||||
self.remote_connection_entry.set_text(self.remote_url)
|
self.remote_connection_entry.set_text(self.remote_url)
|
||||||
if self.verify_connection() == False: self.connection_error()
|
if self.verify_connection() == False: self.connection_error()
|
||||||
@@ -1253,7 +1286,7 @@ Generate a title following these rules:
|
|||||||
def connection_error(self):
|
def connection_error(self):
|
||||||
logger.error("Connection error")
|
logger.error("Connection error")
|
||||||
if self.run_remote:
|
if self.run_remote:
|
||||||
dialogs.reconnect_remote(self, connection_handler.url)
|
dialogs.reconnect_remote(self, connection_handler.url, connection_handler.bearer_token)
|
||||||
else:
|
else:
|
||||||
local_instance.reset()
|
local_instance.reset()
|
||||||
self.show_toast(_("There was an error with the local Ollama instance, so it has been reset"), self.main_overlay)
|
self.show_toast(_("There was an error with the local Ollama instance, so it has been reset"), self.main_overlay)
|
||||||
@@ -1558,7 +1591,11 @@ Generate a title following these rules:
|
|||||||
if override in local_instance.overrides:
|
if override in local_instance.overrides:
|
||||||
element.set_text(local_instance.overrides[override])
|
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:
|
||||||
|
dialogs.support(self)
|
||||||
|
if 'show_support' in data: self.show_support = data['show_support']
|
||||||
self.background_switch.set_active(self.run_on_background)
|
self.background_switch.set_active(self.run_on_background)
|
||||||
self.set_hide_on_close(self.run_on_background)
|
self.set_hide_on_close(self.run_on_background)
|
||||||
self.remote_connection_entry.set_text(self.remote_url)
|
self.remote_connection_entry.set_text(self.remote_url)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<signal name="close-request" handler="closing_app"/>
|
<signal name="close-request" handler="closing_app"/>
|
||||||
<property name="resizable">True</property>
|
<property name="resizable">True</property>
|
||||||
<property name="width-request">360</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-width">1300</property>
|
||||||
<property name="default-height">800</property>
|
<property name="default-height">800</property>
|
||||||
<property name="title">Alpaca</property>
|
<property name="title">Alpaca</property>
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
<property name="valign">3</property>
|
<property name="valign">3</property>
|
||||||
<property name="tooltip-text" translatable="yes">Send Message</property>
|
<property name="tooltip-text" translatable="yes">Send Message</property>
|
||||||
<style>
|
<style>
|
||||||
<class name="suggested-action"/>
|
<class name="accent"/>
|
||||||
<class name="circular"/>
|
<class name="circular"/>
|
||||||
</style>
|
</style>
|
||||||
<child>
|
<child>
|
||||||
|
|||||||
Reference in New Issue
Block a user