15 Commits
2.5.0 ... 2.5.1

Author SHA1 Message Date
jeffser
7f042a906d Much better markup rendering 2024-10-09 21:31:08 -06:00
jeffser
ff5e663185 Update spanish 2024-10-09 21:07:20 -06:00
jeffser
9d0daea052 Languages updated 2024-10-09 21:05:44 -06:00
jeffser
31a10ee7d6 Added note 2024-10-09 21:05:23 -06:00
jeffser
38abd208ff Much better message block rendering 2024-10-09 20:48:04 -06:00
jeffser
c34713eff5 Preparing for 2.5.1 2024-10-09 20:02:18 -06:00
jeffser
d55aaf19a2 Nah, I'll leave then sudo thing 2024-10-09 12:51:02 -06:00
jeffser
9f4b6faf28 Remove sudo 2024-10-09 12:49:02 -06:00
jeffser
57184f0a5c Remove digests when canceling the download of a model 2024-10-09 10:46:16 -06:00
jeffser
df568c7217 Verify image recognition only at launch 2024-10-09 10:13:00 -06:00
jeffser
ed440d8935 New icon 2024-10-08 19:27:01 -06:00
jeffser
70c183c71d Rewritten edit system (cancel and save buttons) 2024-10-08 19:26:51 -06:00
jeffser
b44d457bc5 Removed unused variable 2024-10-08 19:01:38 -06:00
aritra saha
15e4bbb62f Update hi.po (#342) 2024-10-08 15:46:45 -06:00
aritra saha
5d95ba1c15 Update bn.po (#341) 2024-10-08 15:46:29 -06:00
21 changed files with 5940 additions and 5485 deletions

View File

@@ -78,6 +78,21 @@
<url type="contribute">https://github.com/Jeffser/Alpaca/discussions/154</url>
<url type="vcs-browser">https://github.com/Jeffser/Alpaca</url>
<releases>
<release version="2.5.1" date="2024-10-09">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.5.1</url>
<description>
<p>New</p>
<ul>
<li>Added 'Cancel' and 'Save' buttons when editing a message</li>
</ul>
<p>Fixes</p>
<ul>
<li>Better handling of image recognition</li>
<li>Remove unused files when canceling a model download</li>
<li>Better message blocks rendering</li>
</ul>
</description>
</release>
<release version="2.5.0" date="2024-10-06">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.5.0</url>
<description>

View File

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

File diff suppressed because it is too large Load Diff

854
po/bn.po

File diff suppressed because it is too large Load Diff

786
po/de.po

File diff suppressed because it is too large Load Diff

784
po/es.po

File diff suppressed because it is too large Load Diff

786
po/fr.po

File diff suppressed because it is too large Load Diff

786
po/he.po

File diff suppressed because it is too large Load Diff

857
po/hi.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

786
po/ru.po

File diff suppressed because it is too large Load Diff

786
po/te.po

File diff suppressed because it is too large Load Diff

786
po/tr.po

File diff suppressed because it is too large Load Diff

786
po/uk.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,7 @@
<file alias="icons/scalable/status/down-symbolic.svg">icons/down-symbolic.svg</file>
<file alias="icons/scalable/status/chat-bubble-text-symbolic.svg">icons/chat-bubble-text-symbolic.svg</file>
<file alias="icons/scalable/status/execute-from-symbolic.svg">icons/execute-from-symbolic.svg</file>
<file alias="icons/scalable/status/cross-large-symbolic.svg">icons/cross-large-symbolic.svg</file>
<file preprocess="xml-stripblanks">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource>

View File

@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
window = None
class edit_text_block(Gtk.TextView):
class edit_text_block(Gtk.Box):
__gtype_name__ = 'AlpacaEditTextBlock'
def __init__(self, text:str):
@@ -27,21 +27,71 @@ class edit_text_block(Gtk.TextView):
margin_bottom=5,
margin_start=5,
margin_end=5,
css_classes=["view", "editing_message_textview"]
)
self.get_buffer().insert(self.get_buffer().get_start_iter(), text, len(text.encode('utf-8')))
enter_key_controller = Gtk.EventControllerKey.new()
enter_key_controller.connect("key-pressed", lambda controller, keyval, keycode, state: self.edit_message() if keyval==Gdk.KEY_Return and not (state & Gdk.ModifierType.SHIFT_MASK) else None)
self.add_controller(enter_key_controller)
def edit_message(self):
self.get_parent().get_parent().action_buttons.set_visible(True)
self.get_parent().get_parent().set_text(self.get_buffer().get_text(self.get_buffer().get_start_iter(), self.get_buffer().get_end_iter(), False))
self.get_parent().get_parent().add_footer(self.get_parent().get_parent().dt)
window.save_history(self.get_parent().get_parent().get_parent().get_parent().get_parent().get_parent())
spacing=5,
orientation=1
)
self.text_view = Gtk.TextView(
halign=0,
hexpand=True,
css_classes=["view", "editing_message_textview"],
wrap_mode=3
)
cancel_button = Gtk.Button(
vexpand=False,
valign=2,
halign=2,
tooltip_text=_("Cancel"),
css_classes=['flat', 'circular'],
icon_name='cross-large-symbolic'
)
cancel_button.connect('clicked', lambda *_: self.cancel_edit())
save_button = Gtk.Button(
vexpand=False,
valign=2,
halign=2,
tooltip_text=_("Save Message"),
css_classes=['flat', 'circular'],
icon_name='paper-plane-symbolic'
)
save_button.connect('clicked', lambda *_: self.edit_message())
self.append(self.text_view)
button_container = Gtk.Box(
halign=2,
spacing=5
)
button_container.append(cancel_button)
button_container.append(save_button)
self.append(button_container)
self.text_view.get_buffer().insert(self.text_view.get_buffer().get_start_iter(), text, len(text.encode('utf-8')))
key_controller = Gtk.EventControllerKey.new()
key_controller.connect("key-pressed", self.handle_key)
self.text_view.add_controller(key_controller)
def handle_key(self, controller, keyval, keycode, state):
if keyval==Gdk.KEY_Return and not (state & Gdk.ModifierType.SHIFT_MASK):
self.save_edit()
return True
elif keyval==Gdk.KEY_Escape:
self.cancel_edit()
return True
def save_edit(self):
message_element = self.get_parent().get_parent()
message_element.action_buttons.set_visible(True)
message_element.set_text(self.text_view.get_buffer().get_text(self.text_view.get_buffer().get_start_iter(), self.text_view.get_buffer().get_end_iter(), False))
message_element.add_footer(message_element.dt)
window.save_history(message_element.get_parent().get_parent().get_parent().get_parent())
self.get_parent().remove(self)
window.show_toast(_("Message edited successfully"), window.main_overlay)
return True
def cancel_edit(self):
message_element = self.get_parent().get_parent()
message_element.action_buttons.set_visible(True)
message_element.set_text(message_element.text)
message_element.add_footer(message_element.dt)
self.get_parent().remove(self)
class text_block(Gtk.Label):
__gtype_name__ = 'AlpacaTextBlock'
@@ -484,17 +534,14 @@ class message(Gtk.Overlay):
self.content_children = []
if text:
self.content_children = []
code_block_pattern = re.compile(r'[```|`](\w*)\n(.*?)\n\s*[```|`]', re.DOTALL)
code_block_pattern = re.compile(r'```(\w*)\n(.*?)\n\s*```', re.DOTALL)
no_language_code_block_pattern = re.compile(r'`(\w*)\n(.*?)\n\s*`', re.DOTALL)
table_pattern = re.compile(r'((\r?\n){2}|^)([^\r\n]*\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)', re.MULTILINE)
bold_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
parts = []
pos = 0
# Code blocks
for match in code_block_pattern.finditer(self.text):
for match in code_block_pattern.finditer(self.text[pos:]):
start, end = match.span()
if pos < start:
normal_text = self.text[pos:start]
@@ -503,8 +550,17 @@ class message(Gtk.Overlay):
code_text = match.group(2)
parts.append({"type": "code", "text": code_text, "language": 'python3' if language == 'python' else language})
pos = end
for match in no_language_code_block_pattern.finditer(self.text[pos:]):
start, end = match.span()
if pos < start:
normal_text = self.text[pos:start]
parts.append({"type": "normal", "text": normal_text.strip()})
language = match.group(1)
code_text = match.group(2)
parts.append({"type": "code", "text": code_text, "language": None})
pos = end
# Tables
for match in table_pattern.finditer(self.text):
for match in table_pattern.finditer(self.text[pos:]):
start, end = match.span()
if pos < start:
normal_text = self.text[pos:start]
@@ -513,8 +569,8 @@ class message(Gtk.Overlay):
parts.append({"type": "table", "text": table_text})
pos = end
# Text blocks
if pos < len(text):
normal_text = text[pos:]
if pos < len(self.text[pos:]):
normal_text = self.text[pos:]
if normal_text.strip():
parts.append({"type": "normal", "text": normal_text.strip()})
@@ -522,10 +578,12 @@ class message(Gtk.Overlay):
if part['type'] == 'normal':
text_b = text_block(self.bot)
part['text'] = part['text'].replace("\n* ", "\n")
part['text'] = code_pattern.sub(r'<tt>\1</tt>', part['text'])
part['text'] = bold_pattern.sub(r'<b>\1</b>', part['text'])
part['text'] = h1_pattern.sub(r'<span size="x-large">\1</span>', part['text'])
part['text'] = h2_pattern.sub(r'<span size="large">\1</span>', part['text'])
part['text'] = re.sub(r'`([^`\n]*?)`', r'<tt>\1</tt>', part['text'])
part['text'] = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', part['text'], flags=re.MULTILINE)
part['text'] = re.sub(r'^#\s+(.*)', r'<span size="x-large">\1</span>', part['text'], flags=re.MULTILINE)
part['text'] = re.sub(r'^##\s+(.*)', r'<span size="large">\1</span>', part['text'], flags=re.MULTILINE)
part['text'] = re.sub(r'_(\((.*?)\)|\d+)', r'<sub>\2\1</sub>', part['text'], flags=re.MULTILINE)
part['text'] = re.sub(r'\^(\((.*?)\)|\d+)', r'<sup>\2\1</sup>', part['text'], flags=re.MULTILINE)
pos = 0
for match in markup_pattern.finditer(part['text']):
start, end = match.span()

View File

@@ -7,7 +7,7 @@ import gi
gi.require_version('Gtk', '4.0')
gi.require_version('GtkSource', '5')
from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk
import logging, os, datetime, re, shutil, threading, json, sys
import logging, os, datetime, re, shutil, threading, json, sys, glob
from ..internal import config_dir, data_dir, cache_dir, source_dir
from .. import available_models_descriptions, dialogs
@@ -52,6 +52,23 @@ class model_selector_popup(Gtk.Popover):
child=scroller
)
class model_selector_row(Gtk.ListBoxRow):
__gtype_name__ = 'AlpacaModelSelectorRow'
def __init__(self, model_name:str, image_recognition:bool):
super().__init__(
child = Gtk.Label(
label=window.convert_model_name(model_name, 0),
halign=1,
hexpand=True
),
halign=0,
hexpand=True,
name=model_name,
tooltip_text=window.convert_model_name(model_name, 0)
)
self.image_recognition = image_recognition
class model_selector_button(Gtk.MenuButton):
__gtype_name__ = 'AlpacaModelSelectorButton'
@@ -91,19 +108,18 @@ class model_selector_button(Gtk.MenuButton):
window.model_manager.verify_if_image_can_be_used()
def add_model(self, model_name:str):
model_row = Gtk.ListBoxRow(
child = Gtk.Label(
label=window.convert_model_name(model_name, 0),
halign=1,
hexpand=True
),
halign=0,
hexpand=True,
name=model_name,
tooltip_text=window.convert_model_name(model_name, 0)
)
self.get_popover().model_list_box.append(model_row)
self.change_model(model_name)
vision = False
response = window.ollama_instance.request("POST", "api/show", json.dumps({"name": model_name}))
if response.status_code != 200:
logger.error(f"Status code was {response.status_code}")
return
try:
vision = 'projector_info' in json.loads(response.text)
except Exception as e:
logger.error(f"Error fetching vision info: {str(e)}")
model_row = model_selector_row(model_name, vision)
GLib.idle_add(self.get_popover().model_list_box.append, model_row)
GLib.idle_add(self.change_model, model_name)
def remove_model(self, model_name:str):
self.get_popover().model_list_box.remove(next((model for model in list(self.get_popover().model_list_box) if model.get_name() == model_name), None))
@@ -183,9 +199,22 @@ class pulling_model(Gtk.ListBoxRow):
name=model_name
)
self.error = None
self.digests = []
def update(self, data):
if 'digest' in data and data['digest'] not in self.digests:
self.digests.append(data['digest'].replace(':', '-'))
if not self.get_parent():
logger.info("Pulling of '{}' was canceled".format(self.get_name()))
directory = os.path.join(data_dir, '.ollama', 'models', 'blobs')
for digest in self.digests:
files_to_delete = glob.glob(os.path.join(directory, digest + '*'))
for file in files_to_delete:
logger.info("Deleting '{}'".format(file))
try:
os.remove(file)
except Exception as e:
logger.error(f"Can't delete file {file}: {e}")
sys.exit()
if 'error' in data:
self.error = data['error']
@@ -273,7 +302,7 @@ class local_model_list(Gtk.ListBox):
def add_model(self, model_name:str):
model = local_model(model_name)
self.append(model)
GLib.idle_add(self.append, model)
if not self.get_visible():
self.set_visible(True)
@@ -466,7 +495,7 @@ class model_manager_container(Gtk.Box):
else:
self.local_list.set_visible(True)
for model in data['models']:
self.add_local_model(model['name'])
threading.Thread(target=self.add_local_model, args=(model['name'], )).start()
else:
window.connection_error()
except Exception as e:
@@ -484,48 +513,18 @@ class model_manager_container(Gtk.Box):
def change_model(self, model_name:str):
self.model_selector.change_model(model_name)
def has_vision(self, model_name) -> bool:
response = (
window.ollama_instance.request(
"POST", "api/show", json.dumps({"name": model_name})
)
)
if response.status_code != 200:
logger.error(f"Status code was {response.status_code}")
return False
try:
model_info = json.loads(response.text)
logger.debug(f"Vision for {model_name}: {'projector_info' in model_info}")
return 'projector_info' in model_info
except Exception as e:
logger.error(f"Error fetching vision info: {str(e)}")
return False
def verify_if_image_can_be_used(self):
logger.debug("Verifying if image can be used")
selected = self.get_selected_model()
if selected == None:
return False
# first try ollama show API.
if self.has_vision(selected):
selected = self.model_selector.get_popover().model_list_box.get_selected_row()
if selected and selected.image_recognition:
for name, content in window.attachments.items():
if content['type'] == 'image':
content['button'].set_css_classes(["flat"])
return True
# then fall back to the old method.)
selected = selected.split(":")[0]
with open(os.path.join(source_dir, 'available_models.json'), 'r', encoding="utf-8") as f:
if selected in [key for key, value in json.load(f).items() if value["image"]]:
for name, content in window.attachments.items():
if content['type'] == 'image':
content['button'].set_css_classes(["flat"])
return True
elif selected:
for name, content in window.attachments.items():
if content['type'] == 'image':
content['button'].set_css_classes(["flat", "error"])
return False
def pull_model(self, model_name:str, modelfile:str=None):
if ':' not in model_name:

View File

@@ -19,11 +19,6 @@ class terminal(Vte.Terminal):
self.set_pty(pty)
env = {
'TERM': "xterm-256color",
'SUDO_ASKPASS': "sh -c 'pkexec echo'"
}
pty.spawn_async(
GLib.get_current_dir(),
script,

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"><path d="m 3 2 c -0.265625 0 -0.519531 0.105469 -0.707031 0.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 l 4.292969 4.292969 l -4.292969 4.292969 c -0.390625 0.390625 -0.390625 1.023437 0 1.414062 s 1.023437 0.390625 1.414062 0 l 4.292969 -4.292969 l 4.292969 4.292969 c 0.390625 0.390625 1.023437 0.390625 1.414062 0 s 0.390625 -1.023437 0 -1.414062 l -4.292969 -4.292969 l 4.292969 -4.292969 c 0.390625 -0.390625 0.390625 -1.023437 0 -1.414062 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 s -0.519531 0.105469 -0.707031 0.292969 l -4.292969 4.292969 l -4.292969 -4.292969 c -0.1875 -0.1875 -0.441406 -0.292969 -0.707031 -0.292969 z m 0 0" fill="#222222"/></svg>

After

Width:  |  Height:  |  Size: 816 B