Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f042a906d | ||
|
|
ff5e663185 | ||
|
|
9d0daea052 | ||
|
|
31a10ee7d6 | ||
|
|
38abd208ff | ||
|
|
c34713eff5 | ||
|
|
d55aaf19a2 | ||
|
|
9f4b6faf28 | ||
|
|
57184f0a5c | ||
|
|
df568c7217 | ||
|
|
ed440d8935 | ||
|
|
70c183c71d | ||
|
|
b44d457bc5 | ||
|
|
15e4bbb62f | ||
|
|
5d95ba1c15 |
@@ -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>
|
||||
|
||||
@@ -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', ],
|
||||
)
|
||||
|
||||
786
po/alpaca.pot
786
po/alpaca.pot
File diff suppressed because it is too large
Load Diff
786
po/nb_NO.po
786
po/nb_NO.po
File diff suppressed because it is too large
Load Diff
832
po/pt_BR.po
832
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
786
po/zh_Hans.po
786
po/zh_Hans.po
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
2
src/icons/cross-large-symbolic.svg
Normal file
2
src/icons/cross-large-symbolic.svg
Normal 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 |
Reference in New Issue
Block a user