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="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="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"> <release version="2.5.0" date="2024-10-06">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.5.0</url> <url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.5.0</url>
<description> <description>

View File

@@ -1,5 +1,5 @@
project('Alpaca', 'c', project('Alpaca', 'c',
version: '2.5.0', version: '2.5.1',
meson_version: '>= 0.62.0', meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ], 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/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/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/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">window.ui</file>
<file preprocess="xml-stripblanks">gtk/help-overlay.ui</file> <file preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
</gresource> </gresource>

View File

@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
window = None window = None
class edit_text_block(Gtk.TextView): class edit_text_block(Gtk.Box):
__gtype_name__ = 'AlpacaEditTextBlock' __gtype_name__ = 'AlpacaEditTextBlock'
def __init__(self, text:str): def __init__(self, text:str):
@@ -27,21 +27,71 @@ class edit_text_block(Gtk.TextView):
margin_bottom=5, margin_bottom=5,
margin_start=5, margin_start=5,
margin_end=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): spacing=5,
self.get_parent().get_parent().action_buttons.set_visible(True) orientation=1
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) self.text_view = Gtk.TextView(
window.save_history(self.get_parent().get_parent().get_parent().get_parent().get_parent().get_parent()) 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) self.get_parent().remove(self)
window.show_toast(_("Message edited successfully"), window.main_overlay) 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): class text_block(Gtk.Label):
__gtype_name__ = 'AlpacaTextBlock' __gtype_name__ = 'AlpacaTextBlock'
@@ -484,17 +534,14 @@ class message(Gtk.Overlay):
self.content_children = [] self.content_children = []
if text: if text:
self.content_children = [] 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) 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 markup_pattern = re.compile(r'<(b|u|tt|span.*)>(.*?)<\/(b|u|tt|span)>') #heh butt span, I'm so funny
parts = [] parts = []
pos = 0 pos = 0
# Code blocks # 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() start, end = match.span()
if pos < start: if pos < start:
normal_text = self.text[pos:start] normal_text = self.text[pos:start]
@@ -503,8 +550,17 @@ class message(Gtk.Overlay):
code_text = match.group(2) code_text = match.group(2)
parts.append({"type": "code", "text": code_text, "language": 'python3' if language == 'python' else language}) parts.append({"type": "code", "text": code_text, "language": 'python3' if language == 'python' else language})
pos = end 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 # Tables
for match in table_pattern.finditer(self.text): for match in table_pattern.finditer(self.text[pos:]):
start, end = match.span() start, end = match.span()
if pos < start: if pos < start:
normal_text = self.text[pos:start] normal_text = self.text[pos:start]
@@ -513,8 +569,8 @@ class message(Gtk.Overlay):
parts.append({"type": "table", "text": table_text}) parts.append({"type": "table", "text": table_text})
pos = end pos = end
# Text blocks # Text blocks
if pos < len(text): if pos < len(self.text[pos:]):
normal_text = text[pos:] normal_text = self.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()})
@@ -522,10 +578,12 @@ class message(Gtk.Overlay):
if part['type'] == 'normal': if part['type'] == 'normal':
text_b = text_block(self.bot) text_b = text_block(self.bot)
part['text'] = part['text'].replace("\n* ", "\n") part['text'] = part['text'].replace("\n* ", "\n")
part['text'] = code_pattern.sub(r'<tt>\1</tt>', part['text']) part['text'] = re.sub(r'`([^`\n]*?)`', r'<tt>\1</tt>', part['text'])
part['text'] = bold_pattern.sub(r'<b>\1</b>', part['text']) part['text'] = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', part['text'], flags=re.MULTILINE)
part['text'] = h1_pattern.sub(r'<span size="x-large">\1</span>', part['text']) part['text'] = re.sub(r'^#\s+(.*)', r'<span size="x-large">\1</span>', part['text'], flags=re.MULTILINE)
part['text'] = h2_pattern.sub(r'<span size="large">\1</span>', part['text']) 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 pos = 0
for match in markup_pattern.finditer(part['text']): for match in markup_pattern.finditer(part['text']):
start, end = match.span() start, end = match.span()

View File

@@ -7,7 +7,7 @@ import gi
gi.require_version('Gtk', '4.0') gi.require_version('Gtk', '4.0')
gi.require_version('GtkSource', '5') gi.require_version('GtkSource', '5')
from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk 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 ..internal import config_dir, data_dir, cache_dir, source_dir
from .. import available_models_descriptions, dialogs from .. import available_models_descriptions, dialogs
@@ -52,6 +52,23 @@ class model_selector_popup(Gtk.Popover):
child=scroller 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): class model_selector_button(Gtk.MenuButton):
__gtype_name__ = 'AlpacaModelSelectorButton' __gtype_name__ = 'AlpacaModelSelectorButton'
@@ -91,19 +108,18 @@ class model_selector_button(Gtk.MenuButton):
window.model_manager.verify_if_image_can_be_used() window.model_manager.verify_if_image_can_be_used()
def add_model(self, model_name:str): def add_model(self, model_name:str):
model_row = Gtk.ListBoxRow( vision = False
child = Gtk.Label( response = window.ollama_instance.request("POST", "api/show", json.dumps({"name": model_name}))
label=window.convert_model_name(model_name, 0), if response.status_code != 200:
halign=1, logger.error(f"Status code was {response.status_code}")
hexpand=True return
), try:
halign=0, vision = 'projector_info' in json.loads(response.text)
hexpand=True, except Exception as e:
name=model_name, logger.error(f"Error fetching vision info: {str(e)}")
tooltip_text=window.convert_model_name(model_name, 0) model_row = model_selector_row(model_name, vision)
) GLib.idle_add(self.get_popover().model_list_box.append, model_row)
self.get_popover().model_list_box.append(model_row) GLib.idle_add(self.change_model, model_name)
self.change_model(model_name)
def remove_model(self, model_name:str): 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)) 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 name=model_name
) )
self.error = None self.error = None
self.digests = []
def update(self, data): 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(): 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() sys.exit()
if 'error' in data: if 'error' in data:
self.error = data['error'] self.error = data['error']
@@ -273,7 +302,7 @@ class local_model_list(Gtk.ListBox):
def add_model(self, model_name:str): def add_model(self, model_name:str):
model = local_model(model_name) model = local_model(model_name)
self.append(model) GLib.idle_add(self.append, model)
if not self.get_visible(): if not self.get_visible():
self.set_visible(True) self.set_visible(True)
@@ -466,7 +495,7 @@ class model_manager_container(Gtk.Box):
else: else:
self.local_list.set_visible(True) self.local_list.set_visible(True)
for model in data['models']: for model in data['models']:
self.add_local_model(model['name']) threading.Thread(target=self.add_local_model, args=(model['name'], )).start()
else: else:
window.connection_error() window.connection_error()
except Exception as e: except Exception as e:
@@ -484,48 +513,18 @@ class model_manager_container(Gtk.Box):
def change_model(self, model_name:str): def change_model(self, model_name:str):
self.model_selector.change_model(model_name) 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): def verify_if_image_can_be_used(self):
logger.debug("Verifying if image can be used") logger.debug("Verifying if image can be used")
selected = self.get_selected_model() selected = self.model_selector.get_popover().model_list_box.get_selected_row()
if selected == None: if selected and selected.image_recognition:
return False
# first try ollama show API.
if self.has_vision(selected):
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(): for name, content in window.attachments.items():
if content['type'] == 'image': if content['type'] == 'image':
content['button'].set_css_classes(["flat"]) content['button'].set_css_classes(["flat"])
return True return True
elif selected:
for name, content in window.attachments.items(): for name, content in window.attachments.items():
if content['type'] == 'image': if content['type'] == 'image':
content['button'].set_css_classes(["flat", "error"]) content['button'].set_css_classes(["flat", "error"])
return False
def pull_model(self, model_name:str, modelfile:str=None): def pull_model(self, model_name:str, modelfile:str=None):
if ':' not in model_name: if ':' not in model_name:

View File

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