Compare commits

..

No commits in common. "main" and "2.5.0" have entirely different histories.
main ... 2.5.0

36 changed files with 10496 additions and 12926 deletions

View File

@ -33,7 +33,7 @@ Alpaca is an [Ollama](https://github.com/ollama/ollama) client where you can man
Normal conversation | Image recognition | Code highlighting | YouTube transcription | Model management Normal conversation | Image recognition | Code highlighting | YouTube transcription | Model management
:------------------:|:-----------------:|:-----------------:|:---------------------:|:----------------: :------------------:|:-----------------:|:-----------------:|:---------------------:|:----------------:
![screenie1](https://jeffser.com/images/alpaca/screenie1.png) | ![screenie2](https://jeffser.com/images/alpaca/screenie2.png) | ![screenie3](https://jeffser.com/images/alpaca/screenie3.png) | ![screenie4](https://jeffser.com/images/alpaca/screenie5.png) | ![screenie5](https://jeffser.com/images/alpaca/screenie6.png) ![screenie1](https://jeffser.com/images/alpaca/screenie1.png) | ![screenie2](https://jeffser.com/images/alpaca/screenie2.png) | ![screenie3](https://jeffser.com/images/alpaca/screenie3.png) | ![screenie4](https://jeffser.com/images/alpaca/screenie4.png) | ![screenie5](https://jeffser.com/images/alpaca/screenie5.png)
## Installation ## Installation
@ -68,7 +68,7 @@ Language | Contributors
🇷🇺 Russian | [Alex K](https://github.com/alexkdeveloper) 🇷🇺 Russian | [Alex K](https://github.com/alexkdeveloper)
🇪🇸 Spanish | [Jeffry Samuel](https://github.com/jeffser) 🇪🇸 Spanish | [Jeffry Samuel](https://github.com/jeffser)
🇫🇷 French | [Louis Chauvet-Villaret](https://github.com/loulou64490) , [Théo FORTIN](https://github.com/topiga) 🇫🇷 French | [Louis Chauvet-Villaret](https://github.com/loulou64490) , [Théo FORTIN](https://github.com/topiga)
🇧🇷 Brazilian Portuguese | [Daimar Stein](https://github.com/not-a-dev-stein) , [Bruno Antunes](https://github.com/antun3s) 🇧🇷 Brazilian Portuguese | [Daimar Stein](https://github.com/not-a-dev-stein)
🇳🇴 Norwegian | [CounterFlow64](https://github.com/CounterFlow64) 🇳🇴 Norwegian | [CounterFlow64](https://github.com/CounterFlow64)
🇮🇳 Bengali | [Aritra Saha](https://github.com/olumolu) 🇮🇳 Bengali | [Aritra Saha](https://github.com/olumolu)
🇨🇳 Simplified Chinese | [Yuehao Sui](https://github.com/8ar10der) , [Aleksana](https://github.com/Aleksanaa) 🇨🇳 Simplified Chinese | [Yuehao Sui](https://github.com/8ar10der) , [Aleksana](https://github.com/Aleksanaa)
@ -91,7 +91,6 @@ Want to add a language? Visit [this discussion](https://github.com/Jeffser/Alpac
- [Nokse](https://github.com/Nokse22) for their contributions to the UI and table rendering - [Nokse](https://github.com/Nokse22) for their contributions to the UI and table rendering
- [Louis Chauvet-Villaret](https://github.com/loulou64490) for their suggestions - [Louis Chauvet-Villaret](https://github.com/loulou64490) for their suggestions
- [Aleksana](https://github.com/Aleksanaa) for her help with better handling of directories - [Aleksana](https://github.com/Aleksanaa) for her help with better handling of directories
- [Gnome Builder Team](https://gitlab.gnome.org/GNOME/gnome-builder) for the awesome IDE I use to develop Alpaca
- Sponsors for giving me enough money to be able to take a ride to my campus every time I need to <3 - Sponsors for giving me enough money to be able to take a ride to my campus every time I need to <3
- Everyone that has shared kind words of encouragement! - Everyone that has shared kind words of encouragement!

View File

@ -111,45 +111,6 @@
} }
] ]
}, },
{
"name": "python3-youtube-transcript-api",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"youtube-transcript-api\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl",
"sha256": "922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz",
"sha256": "223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl",
"sha256": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl",
"sha256": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl",
"sha256": "ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"
},
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/52/42/5f57d37d56bdb09722f226ed81cc1bec63942da745aa27266b16b0e16a5d/youtube_transcript_api-0.6.2-py3-none-any.whl",
"sha256": "019dbf265c6a68a0591c513fff25ed5a116ce6525832aefdfb34d4df5567121c"
}
]
},
{ {
"name": "python3-html2text", "name": "python3-html2text",
"buildsystem": "simple", "buildsystem": "simple",

View File

@ -63,14 +63,10 @@
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://jeffser.com/images/alpaca/screenie4.png</image> <image>https://jeffser.com/images/alpaca/screenie4.png</image>
<caption>A Python script running inside integrated terminal</caption>
</screenshot>
<screenshot>
<image>https://jeffser.com/images/alpaca/screenie5.png</image>
<caption>A conversation involving a YouTube video transcript</caption> <caption>A conversation involving a YouTube video transcript</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://jeffser.com/images/alpaca/screenie6.png</image> <image>https://jeffser.com/images/alpaca/screenie5.png</image>
<caption>Multiple models being downloaded</caption> <caption>Multiple models being downloaded</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>
@ -82,63 +78,6 @@
<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.7.0" date="2024-10-15">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.7.0</url>
<description>
<p>New</p>
<ul>
<li>User messages are now compacted into bubbles</li>
</ul>
<p>Fixes</p>
<ul>
<li>Fixed re connection dialog not working when 'use local instance' is selected</li>
<li>Fixed model manager not adapting to large system fonts</li>
</ul>
</description>
</release>
<release version="2.6.5" date="2024-10-13">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.6.5</url>
<description>
<p>New</p>
<ul>
<li>Details page for models</li>
<li>Model selector gets replaced with 'manage models' button when there are no models downloaded</li>
<li>Added warning when model is too big for the device</li>
<li>Added AMD GPU indicator in preferences</li>
</ul>
</description>
</release>
<release version="2.6.0" date="2024-10-11">
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.6.0</url>
<description>
<p>New</p>
<ul>
<li>Better system for handling dialogs</li>
<li>Better system for handling instance switching</li>
<li>Remote connection dialog</li>
</ul>
<p>Fixes</p>
<ul>
<li>Fixed: Models get duplicated when switching remote and local instance</li>
<li>Better internal instance manager</li>
</ul>
</description>
</release>
<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.7.0', version: '2.5.0',
meson_version: '>= 0.62.0', meson_version: '>= 0.62.0',
default_options: [ 'warning_level=2', 'werror=false', ], default_options: [ 'warning_level=2', 'werror=false', ],
) )

View File

@ -5,11 +5,9 @@ src/main.py
src/window.py src/window.py
src/available_models_descriptions.py src/available_models_descriptions.py
src/connection_handler.py src/connection_handler.py
src/dialogs.py
src/window.ui src/window.ui
src/generic_actions.py
src/custom_widgets/chat_widget.py src/custom_widgets/chat_widget.py
src/custom_widgets/message_widget.py src/custom_widgets/message_widget.py
src/custom_widgets/model_widget.py src/custom_widgets/model_widget.py
src/custom_widgets/table_widget.py src/custom_widgets/table_widget.py
src/custom_widgets/dialog_widget.py
src/custom_widgets/terminal_widget.py

File diff suppressed because it is too large Load Diff

1560
po/bn.po

File diff suppressed because it is too large Load Diff

1487
po/de.po

File diff suppressed because it is too large Load Diff

1502
po/es.po

File diff suppressed because it is too large Load Diff

1576
po/fr.po

File diff suppressed because it is too large Load Diff

1486
po/he.po

File diff suppressed because it is too large Load Diff

1556
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

1479
po/ru.po

File diff suppressed because it is too large Load Diff

1486
po/te.po

File diff suppressed because it is too large Load Diff

1486
po/tr.po

File diff suppressed because it is too large Load Diff

1600
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

@ -1,4 +1,4 @@
name: jeffser-alpaca name: alpaca
base: core24 base: core24
adopt-info: alpaca adopt-info: alpaca
@ -63,15 +63,14 @@ parts:
ollama: ollama:
plugin: dump plugin: dump
source: source:
- on amd64: https://github.com/ollama/ollama/releases/download/v0.3.12/ollama-linux-amd64.tgz - on amd64: https://github.com/ollama/ollama/releases/download/v0.3.10/ollama-linux-amd64.tgz
- on arm64: https://github.com/ollama/ollama/releases/download/v0.3.12/ollama-linux-arm64.tgz - on arm64: https://github.com/ollama/ollama/releases/download/v0.3.10/ollama-linux-arm64.tgz
# Alpaca app # Alpaca app
alpaca: alpaca:
plugin: meson plugin: meson
source-type: git source-type: git
source: https://github.com/Jeffser/Alpaca.git source: https://github.com/Jeffser/Alpaca.git
source-tag: 2.6.5
source-depth: 1 source-depth: 1
meson-parameters: meson-parameters:
- --prefix=/snap/alpaca/current/usr - --prefix=/snap/alpaca/current/usr

View File

@ -32,8 +32,6 @@
<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 alias="icons/scalable/status/info-outline-symbolic.svg">icons/info-outline-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

@ -11,8 +11,6 @@ logger = getLogger(__name__)
window = None window = None
AMD_support_label = "\n<a href='https://github.com/Jeffser/Alpaca/wiki/AMD-Support'>{}</a>".format(_('Alpaca Support'))
def log_output(pipe): def log_output(pipe):
with open(os.path.join(data_dir, 'tmp.log'), 'a') as f: with open(os.path.join(data_dir, 'tmp.log'), 'a') as f:
with pipe: with pipe:
@ -21,18 +19,7 @@ def log_output(pipe):
print(line, end='') print(line, end='')
f.write(line) f.write(line)
f.flush() f.flush()
if 'msg="model request too large for system"' in line: except:
window.show_toast(_("Model request too large for system"), window.main_overlay)
elif 'msg="amdgpu detected, but no compatible rocm library found.' in line:
if bool(os.getenv("FLATPAK_ID")):
window.ollama_information_label.set_label(_("AMD GPU detected but the extension is missing, Ollama will use CPU.") + AMD_support_label)
else:
window.ollama_information_label.set_label(_("AMD GPU detected but ROCm is missing, Ollama will use CPU.") + AMD_support_label)
window.ollama_information_label.set_css_classes(['dim-label', 'error'])
elif 'msg="amdgpu is supported"' in line:
window.ollama_information_label.set_label(_("Using AMD GPU type '{}'").format(line.split('=')[-1]))
window.ollama_information_label.set_css_classes(['dim-label', 'success'])
except Exception as e:
pass pass
class instance(): class instance():
@ -129,10 +116,10 @@ class instance():
self.instance = instance self.instance = instance
if not self.idle_timer: if not self.idle_timer:
self.start_timer() self.start_timer()
window.ollama_information_label.set_label(_("Integrated Ollama instance is running"))
window.ollama_information_label.set_css_classes(['dim-label', 'success'])
else: else:
self.remote = True self.remote = True
if not self.remote_url:
window.remote_connection_entry.set_text('http://0.0.0.0:11434')
window.remote_connection_switch.set_sensitive(True) window.remote_connection_switch.set_sensitive(True)
window.remote_connection_switch.set_active(True) window.remote_connection_switch.set_active(True)
@ -145,8 +132,6 @@ class instance():
self.instance.terminate() self.instance.terminate()
self.instance.wait() self.instance.wait()
self.instance = None self.instance = None
window.ollama_information_label.set_label(_("Integrated Ollama instance is not running"))
window.ollama_information_label.set_css_classes(['dim-label'])
logger.info("Stopped Alpaca's Ollama instance") logger.info("Stopped Alpaca's Ollama instance")
def reset(self): def reset(self):

View File

@ -66,8 +66,7 @@ class chat(Gtk.ScrolledWindow):
vexpand=True, vexpand=True,
hexpand=True, hexpand=True,
css_classes=["undershoot-bottom"], css_classes=["undershoot-bottom"],
name=name, name=name
hscrollbar_policy=2
) )
self.messages = {} self.messages = {}
self.welcome_screen = None self.welcome_screen = None
@ -87,8 +86,6 @@ class chat(Gtk.ScrolledWindow):
self.stop_message() self.stop_message()
for widget in list(self.container): for widget in list(self.container):
self.container.remove(widget) self.container.remove(widget)
self.show_welcome_screen(len(window.model_manager.get_model_list()) > 0)
print('clear chat for some reason')
def add_message(self, message_id:str, model:str=None): def add_message(self, message_id:str, model:str=None):
msg = message(message_id, model) msg = message(message_id, model)
@ -105,9 +102,7 @@ class chat(Gtk.ScrolledWindow):
if self.welcome_screen: if self.welcome_screen:
self.container.remove(self.welcome_screen) self.container.remove(self.welcome_screen)
self.welcome_screen = None self.welcome_screen = None
if len(list(self.container)) > 0: self.clear_chat()
self.clear_chat()
return
button_container = Gtk.Box( button_container = Gtk.Box(
orientation=1, orientation=1,
spacing=10, spacing=10,
@ -338,8 +333,6 @@ class chat_list(Gtk.ListBox):
window.save_history() window.save_history()
def rename_chat(self, old_chat_name:str, new_chat_name:str): def rename_chat(self, old_chat_name:str, new_chat_name:str):
if new_chat_name == old_chat_name:
return
tab = self.get_tab_by_name(old_chat_name) tab = self.get_tab_by_name(old_chat_name)
if tab: if tab:
new_chat_name = window.generate_numbered_name(new_chat_name, [tab.chat_window.get_name() for tab in self.tab_list]) new_chat_name = window.generate_numbered_name(new_chat_name, [tab.chat_window.get_name() for tab in self.tab_list])

View File

@ -1,173 +0,0 @@
#dialog_widget.py
"""
Handles all dialogs
"""
import gi
gi.require_version('Gtk', '4.0')
gi.require_version('GtkSource', '5')
from gi.repository import Gtk, Gio, Adw, Gdk, GLib
window=None
button_appearance={
'suggested': Adw.ResponseAppearance.SUGGESTED,
'destructive': Adw.ResponseAppearance.DESTRUCTIVE
}
# Don't call this directly outside this script
class baseDialog(Adw.AlertDialog):
__gtype_name__ = 'AlpacaDialogBase'
def __init__(self, heading:str, body:str, close_response:str, options:dict):
self.options = options
super().__init__(
heading=heading,
body=body,
close_response=close_response
)
for option, data in self.options.items():
self.add_response(option, option)
if 'appearance' in data:
self.set_response_appearance(option, button_appearance[data['appearance']])
if 'default' in data and data['default']:
self.set_default_response(option)
class Options(baseDialog):
__gtype_name__ = 'AlpacaDialogOptions'
def __init__(self, heading:str, body:str, close_response:str, options:dict):
super().__init__(
heading,
body,
close_response,
options
)
self.choose(
parent = window,
cancellable = None,
callback = self.response
)
def response(self, dialog, task):
result = dialog.choose_finish(task)
if result in self.options and 'callback' in self.options[result]:
self.options[result]['callback']()
class Entry(baseDialog):
__gtype_name__ = 'AlpacaDialogEntry'
def __init__(self, heading:str, body:str, close_response:str, options:dict, entries:list or dict):
super().__init__(
heading,
body,
close_response,
options
)
self.container = Gtk.Box(
orientation=1,
spacing=10
)
if isinstance(entries, dict):
entries = [entries]
for data in entries:
entry = Gtk.Entry()
if 'placeholder' in data and data['placeholder']:
entry.set_placeholder_text(data['placeholder'])
if 'css' in data and data['css']:
entry.set_css_classes(data['css'])
if 'text' in data and data['text']:
entry.set_text(data['text'])
self.container.append(entry)
self.set_extra_child(self.container)
self.connect('realize', lambda *_: list(self.container)[0].grab_focus())
self.choose(
parent = window,
cancellable = None,
callback = self.response
)
def response(self, dialog, task):
result = dialog.choose_finish(task)
if result in self.options and 'callback' in self.options[result]:
entry_results = []
for entry in list(self.container):
entry_results.append(entry.get_text())
self.options[result]['callback'](*entry_results)
class DropDown(baseDialog):
__gtype_name__ = 'AlpacaDialogDropDown'
def __init__(self, heading:str, body:str, close_response:str, options:dict, items:list):
super().__init__(
heading,
body,
close_response,
options
)
string_list = Gtk.StringList()
for item in items:
string_list.append(item)
self.set_extra_child(Gtk.DropDown(
enable_search=len(items) > 10,
model=string_list
))
self.connect('realize', lambda *_: self.get_extra_child().grab_focus())
self.choose(
parent = window,
cancellable = None,
callback = lambda dialog, task, dropdown=self.get_extra_child(): self.response(dialog, task, dropdown.get_selected_item().get_string())
)
def response(self, dialog, task, item:str):
result = dialog.choose_finish(task)
if result in self.options and 'callback' in self.options[result]:
self.options[result]['callback'](item)
def simple(heading:str, body:str, callback:callable, button_name:str=_('Accept'), button_appearance:str='suggested'):
options = {
_('Cancel'): {},
button_name: {
'appearance': button_appearance,
'callback': callback,
'default': True
}
}
return Options(heading, body, 'cancel', options)
def simple_entry(heading:str, body:str, callback:callable, entries:list or dict, button_name:str=_('Accept'), button_appearance:str='suggested'):
options = {
_('Cancel'): {},
button_name: {
'appearance': button_appearance,
'callback': callback,
'default': True
}
}
return Entry(heading, body, 'cancel', options, entries)
def simple_dropdown(heading:str, body:str, callback:callable, items:list, button_name:str=_('Accept'), button_appearance:str='suggested'):
options = {
_('Cancel'): {},
button_name: {
'appearance': button_appearance,
'callback': callback,
'default': True
}
}
return DropDown(heading, body, 'cancel', options, items)
def simple_file(file_filter:Gtk.FileFilter, callback:callable):
file_dialog = Gtk.FileDialog(default_filter=file_filter)
file_dialog.open(window, None, lambda file_dialog, result: callback(file_dialog.open_finish(result)) if result else None)

View File

@ -10,13 +10,13 @@ from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk
import logging, os, datetime, re, shutil, threading, sys import logging, os, datetime, re, shutil, threading, sys
from ..internal import config_dir, data_dir, cache_dir, source_dir from ..internal import config_dir, data_dir, cache_dir, source_dir
from .table_widget import TableWidget from .table_widget import TableWidget
from . import dialog_widget, terminal_widget from .. import dialogs
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
window = None window = None
class edit_text_block(Gtk.Box): class edit_text_block(Gtk.TextView):
__gtype_name__ = 'AlpacaEditTextBlock' __gtype_name__ = 'AlpacaEditTextBlock'
def __init__(self, text:str): def __init__(self, text:str):
@ -27,71 +27,21 @@ class edit_text_block(Gtk.Box):
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)
spacing=5, def edit_message(self):
orientation=1 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.text_view = Gtk.TextView( self.get_parent().get_parent().add_footer(self.get_parent().get_parent().dt)
halign=0, window.save_history(self.get_parent().get_parent().get_parent().get_parent().get_parent().get_parent())
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'
@ -180,13 +130,7 @@ class code_block(Gtk.Box):
logger.debug("Running script") logger.debug("Running script")
start = self.buffer.get_start_iter() start = self.buffer.get_start_iter()
end = self.buffer.get_end_iter() end = self.buffer.get_end_iter()
dialog_widget.simple( dialogs.run_script(window, self.buffer.get_text(start, end, False), language_name)
_('Run Script'),
_('Make sure you understand what this script does before running it, Alpaca is not responsible for any damages to your device or data'),
lambda script=self.buffer.get_text(start, end, False), language_name=language_name: terminal_widget.run_terminal(script, language_name),
_('Execute'),
'destructive'
)
class attachment(Gtk.Button): class attachment(Gtk.Button):
__gtype_name__ = 'AlpacaAttachment' __gtype_name__ = 'AlpacaAttachment'
@ -230,8 +174,7 @@ class attachment_container(Gtk.ScrolledWindow):
self.container = Gtk.Box( self.container = Gtk.Box(
orientation=0, orientation=0,
spacing=10, spacing=12
valign=1
) )
super().__init__( super().__init__(
@ -239,8 +182,7 @@ class attachment_container(Gtk.ScrolledWindow):
margin_start=10, margin_start=10,
margin_end=10, margin_end=10,
hexpand=True, hexpand=True,
child=self.container, child=self.container
vscrollbar_policy=2
) )
def add_file(self, file:attachment): def add_file(self, file:attachment):
@ -296,7 +238,6 @@ class image(Gtk.Button):
tooltip_text=_("Missing Image") tooltip_text=_("Missing Image")
) )
image_texture.update_property([4], [_("Missing image")]) image_texture.update_property([4], [_("Missing image")])
self.set_overflow(1)
self.connect("clicked", lambda button, file_path=os.path.join(head, '{selected_chat}', last_dir, file_name): window.preview_file(file_path, 'image', None)) self.connect("clicked", lambda button, file_path=os.path.join(head, '{selected_chat}', last_dir, file_name): window.preview_file(file_path, 'image', None))
class image_container(Gtk.ScrolledWindow): class image_container(Gtk.ScrolledWindow):
@ -469,15 +410,10 @@ class message(Gtk.Overlay):
orientation=1, orientation=1,
halign='fill', halign='fill',
css_classes=["response_message"] if self.bot else ["card", "user_message"], css_classes=["response_message"] if self.bot else ["card", "user_message"],
spacing=5, spacing=12
width_request=-1 if self.bot else 375
) )
super().__init__( super().__init__(css_classes=["message"], name=message_id)
css_classes=["message"],
name=message_id,
halign=0 if self.bot else 2
)
self.set_child(self.container) self.set_child(self.container)
def add_attachments(self, attachments:dict): def add_attachments(self, attachments:dict):
@ -548,14 +484,17 @@ 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[pos:]): for match in code_block_pattern.finditer(self.text):
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]
@ -564,17 +503,8 @@ 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[pos:]): for match in table_pattern.finditer(self.text):
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]
@ -583,8 +513,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(self.text): if pos < len(text):
normal_text = self.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()})
@ -592,12 +522,10 @@ 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'] = re.sub(r'`([^`\n]*?)`', r'<tt>\1</tt>', part['text']) part['text'] = code_pattern.sub(r'<tt>\1</tt>', part['text'])
part['text'] = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', part['text'], flags=re.MULTILINE) part['text'] = bold_pattern.sub(r'<b>\1</b>', part['text'])
part['text'] = re.sub(r'^#\s+(.*)', r'<span size="x-large">\1</span>', 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="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'_(\((.*?)\)|\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()
@ -626,7 +554,7 @@ class message(Gtk.Overlay):
if self.spinner: if self.spinner:
self.container.remove(self.spinner) self.container.remove(self.spinner)
self.spinner = None self.spinner = None
self.spinner = Gtk.Spinner(spinning=True, margin_top=10, margin_bottom=10, hexpand=True) self.spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
self.container.append(self.spinner) self.container.append(self.spinner)
self.container.append(text_b) self.container.append(text_b)
self.container.queue_draw() self.container.queue_draw()

View File

@ -7,10 +7,9 @@ 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, glob import logging, os, datetime, re, shutil, threading, json, sys
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 from .. import available_models_descriptions, dialogs
from . import dialog_widget
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -53,24 +52,6 @@ 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, data:dict):
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.data = data
self.image_recognition = 'projector_info' in self.data
class model_selector_button(Gtk.MenuButton): class model_selector_button(Gtk.MenuButton):
__gtype_name__ = 'AlpacaModelSelectorButton' __gtype_name__ = 'AlpacaModelSelectorButton'
@ -82,10 +63,11 @@ class model_selector_button(Gtk.MenuButton):
orientation=0, orientation=0,
spacing=5 spacing=5
) )
self.label = Gtk.Label() self.label = Gtk.Label(label=_('Select a Model'))
container.append(self.label) container.append(self.label)
container.append(Gtk.Image.new_from_icon_name("down-symbolic")) container.append(Gtk.Image.new_from_icon_name("down-symbolic"))
super().__init__( super().__init__(
tooltip_text=_('Select a Model'),
child=container, child=container,
popover=self.popover, popover=self.popover,
halign=3 halign=3
@ -104,28 +86,28 @@ class model_selector_button(Gtk.MenuButton):
self.label.set_label(window.convert_model_name(model_name, 0)) self.label.set_label(window.convert_model_name(model_name, 0))
self.set_tooltip_text(window.convert_model_name(model_name, 0)) self.set_tooltip_text(window.convert_model_name(model_name, 0))
elif len(list(listbox)) == 0: elif len(list(listbox)) == 0:
window.title_stack.set_visible_child_name('no_models') self.label.set_label(_("Select a Model"))
self.set_tooltip_text(_("Select a Model"))
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):
data = None model_row = Gtk.ListBoxRow(
response = window.ollama_instance.request("POST", "api/show", json.dumps({"name": model_name})) child = Gtk.Label(
if response.status_code != 200: label=window.convert_model_name(model_name, 0),
logger.error(f"Status code was {response.status_code}") halign=1,
return hexpand=True
try: ),
data = json.loads(response.text) halign=0,
except Exception as e: hexpand=True,
logger.error(f"Error fetching 'api - show' info: {str(e)}") name=model_name,
model_row = model_selector_row(model_name, data) tooltip_text=window.convert_model_name(model_name, 0)
GLib.idle_add(self.get_popover().model_list_box.append, model_row) )
GLib.idle_add(self.change_model, model_name) self.get_popover().model_list_box.append(model_row)
GLib.idle_add(window.title_stack.set_visible_child_name, 'model_selector') 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))
self.model_changed(self.get_popover().model_list_box) self.model_changed(self.get_popover().model_list_box)
window.title_stack.set_visible_child_name('model_selector' if len(window.model_manager.get_model_list()) > 0 else 'no_models')
def clear_list(self): def clear_list(self):
self.get_popover().model_list_box.remove_all() self.get_popover().model_list_box.remove_all()
@ -180,13 +162,7 @@ class pulling_model(Gtk.ListBoxRow):
css_classes = ["error", "circular"], css_classes = ["error", "circular"],
tooltip_text = _("Stop Pulling '{}'").format(window.convert_model_name(model_name, 0)) tooltip_text = _("Stop Pulling '{}'").format(window.convert_model_name(model_name, 0))
) )
stop_button.connect('clicked', lambda *i: dialog_widget.simple( stop_button.connect('clicked', lambda *_: dialogs.stop_pull_model(window, self))
_('Stop Download?'),
_("Are you sure you want to stop pulling '{}'?").format(window.convert_model_name(self.get_name(), 0)),
self.stop,
_('Stop'),
'destructive'
))
container_box = Gtk.Box( container_box = Gtk.Box(
hexpand=True, hexpand=True,
@ -207,27 +183,9 @@ class pulling_model(Gtk.ListBoxRow):
name=model_name name=model_name
) )
self.error = None self.error = None
self.digests = []
def stop(self):
if len(list(self.get_parent())) == 1:
self.get_parent().set_visible(False)
self.get_parent().remove(self)
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']
@ -249,37 +207,6 @@ class pulling_model_list(Gtk.ListBox):
visible=False visible=False
) )
class information_bow(Gtk.Box):
__gtype_name__ = 'AlpacaModelInformationBow'
def __init__(self, title:str, subtitle:str):
self.title = title
self.subtitle = subtitle
title_label = Gtk.Label(
label=self.title,
css_classes=['subtitle', 'caption', 'dim-label'],
hexpand=True,
margin_top=10,
margin_start=0,
margin_end=0
)
subtitle_label = Gtk.Label(
label=self.subtitle if self.subtitle else '(none)',
css_classes=['heading'],
hexpand=True,
margin_bottom=10,
margin_start=0,
margin_end=0
)
super().__init__(
spacing=5,
orientation=1,
css_classes=['card']
)
self.append(title_label)
self.append(subtitle_label)
class local_model(Gtk.ListBoxRow): class local_model(Gtk.ListBoxRow):
__gtype_name__ = 'AlpacaLocalModel' __gtype_name__ = 'AlpacaLocalModel'
@ -307,16 +234,6 @@ class local_model(Gtk.ListBoxRow):
description_box.append(model_label) description_box.append(model_label)
description_box.append(tag_label) description_box.append(tag_label)
info_button = Gtk.Button(
icon_name = "info-outline-symbolic",
vexpand = False,
valign = 3,
css_classes = ["circular"],
tooltip_text = _("Details")
)
info_button.connect('clicked', self.show_information)
delete_button = Gtk.Button( delete_button = Gtk.Button(
icon_name = "user-trash-symbolic", icon_name = "user-trash-symbolic",
vexpand = False, vexpand = False,
@ -324,14 +241,7 @@ class local_model(Gtk.ListBoxRow):
css_classes = ["error", "circular"], css_classes = ["error", "circular"],
tooltip_text = _("Remove '{}'").format(window.convert_model_name(model_name, 0)) tooltip_text = _("Remove '{}'").format(window.convert_model_name(model_name, 0))
) )
delete_button.connect('clicked', lambda *_, model_name=model_name: dialogs.delete_model(window, model_name))
delete_button.connect('clicked', lambda *i: dialog_widget.simple(
_('Delete Model?'),
_("Are you sure you want to delete '{}'?").format(model_title),
lambda model_name=model_name: window.model_manager.remove_local_model(model_name),
_('Delete'),
'destructive'
))
container_box = Gtk.Box( container_box = Gtk.Box(
hexpand=True, hexpand=True,
@ -344,7 +254,6 @@ class local_model(Gtk.ListBoxRow):
margin_end=10 margin_end=10
) )
container_box.append(description_box) container_box.append(description_box)
container_box.append(info_button)
container_box.append(delete_button) container_box.append(delete_button)
super().__init__( super().__init__(
@ -352,53 +261,6 @@ class local_model(Gtk.ListBoxRow):
name=model_name name=model_name
) )
def show_information(self, button):
model = next((element for element in list(window.model_manager.model_selector.get_popover().model_list_box) if element.get_name() == self.get_name()), None)
model_name = model.get_child().get_label()
window.model_detail_page.set_title(' ('.join(model_name.split(' (')[:-1]))
window.model_detail_page.set_description(' ('.join(model_name.split(' (')[-1:])[:-1])
window.model_detail_create_button.set_name(model_name)
window.model_detail_create_button.set_tooltip_text(_("Create Model Based on '{}'").format(model_name))
details_flow_box = Gtk.FlowBox(
valign=1,
hexpand=True,
vexpand=False,
selection_mode=0,
max_children_per_line=2,
min_children_per_line=1,
margin_top=12,
margin_bottom=12,
margin_start=12,
margin_end=12
)
translation_strings={
'modified_at': _('Modified At'),
'parent_model': _('Parent Model'),
'format': _('Format'),
'family': _('Family'),
'parameter_size': _('Parameter Size'),
'quantization_level': _('Quantization Level')
}
if 'modified_at' in model.data and model.data['modified_at']:
details_flow_box.append(information_bow(
title=translation_strings['modified_at'],
subtitle=datetime.datetime.strptime(':'.join(model.data['modified_at'].split(':')[:2]), '%Y-%m-%dT%H:%M').strftime('%Y-%m-%d %H:%M')
))
for name, value in model.data['details'].items():
if isinstance(value, str):
details_flow_box.append(information_bow(
title=translation_strings[name] if name in translation_strings else name.replace('_', ' ').title(),
subtitle=value
))
window.model_detail_page.set_child(details_flow_box)
window.navigation_view_manage_models.push_by_tag('model_information')
class local_model_list(Gtk.ListBox): class local_model_list(Gtk.ListBox):
__gtype_name__ = 'AlpacaLocalModelList' __gtype_name__ = 'AlpacaLocalModelList'
@ -411,7 +273,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)
GLib.idle_add(self.append, model) self.append(model)
if not self.get_visible(): if not self.get_visible():
self.set_visible(True) self.set_visible(True)
@ -431,9 +293,7 @@ class available_model(Gtk.ListBoxRow):
label="<b>{}</b> <small>by {}</small>".format(self.model_title, self.model_author), label="<b>{}</b> <small>by {}</small>".format(self.model_title, self.model_author),
hexpand=True, hexpand=True,
halign=1, halign=1,
use_markup=True, use_markup=True
wrap=True,
wrap_mode=0
) )
description_label = Gtk.Label( description_label = Gtk.Label(
css_classes=["subtitle"], css_classes=["subtitle"],
@ -599,7 +459,6 @@ class model_manager_container(Gtk.Box):
try: try:
response = window.ollama_instance.request("GET", "api/tags") response = window.ollama_instance.request("GET", "api/tags")
if response.status_code == 200: if response.status_code == 200:
self.model_selector.popover.model_list_box.remove_all()
self.local_list.remove_all() self.local_list.remove_all()
data = json.loads(response.text) data = json.loads(response.text)
if len(data['models']) == 0: if len(data['models']) == 0:
@ -607,14 +466,13 @@ 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']:
threading.Thread(target=self.add_local_model, args=(model['name'], )).start() self.add_local_model(model['name'])
else: else:
window.connection_error() window.connection_error()
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
window.connection_error() window.connection_error()
window.title_stack.set_visible_child_name('model_selector' if len(window.model_manager.get_model_list()) > 0 else 'no_models') window.title_stack.set_visible_child_name('model_selector')
#window.title_stack.set_visible_child_name('model_selector')
window.chat_list_box.update_welcome_screens(len(self.get_model_list()) > 0) window.chat_list_box.update_welcome_screens(len(self.get_model_list()) > 0)
#Should only be called when the app starts #Should only be called when the app starts
@ -626,18 +484,48 @@ 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.model_selector.get_popover().model_list_box.get_selected_row() selected = self.get_selected_model()
if selected and selected.image_recognition: if selected == None:
for name, content in window.attachments.items(): return False
if content['type'] == 'image':
content['button'].set_css_classes(["flat"]) # first try ollama show API.
if self.has_vision(selected):
return True return True
elif selected:
# 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
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

@ -7,12 +7,6 @@ import gi
gi.require_version('Gtk', '4.0') gi.require_version('Gtk', '4.0')
gi.require_version('Vte', '3.91') gi.require_version('Vte', '3.91')
from gi.repository import Gtk, Vte, GLib, Pango, GLib, Gdk from gi.repository import Gtk, Vte, GLib, Pango, GLib, Gdk
import logging, os, shutil, subprocess, re
from ..internal import data_dir
logger = logging.getLogger(__name__)
window = None
class terminal(Vte.Terminal): class terminal(Vte.Terminal):
__gtype_name__ = 'AlpacaTerminal' __gtype_name__ = 'AlpacaTerminal'
@ -25,6 +19,11 @@ 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,
@ -48,44 +47,3 @@ class terminal(Vte.Terminal):
self.copy_clipboard() self.copy_clipboard()
return True return True
return False return False
def show_terminal(script):
window.terminal_scroller.set_child(terminal(script))
window.terminal_dialog.present(window)
def run_terminal(script:str, language_name:str):
logger.info('Running: \n{}'.format(language_name))
if language_name == 'python3':
if not os.path.isdir(os.path.join(data_dir, 'pyenv')):
os.mkdir(os.path.join(data_dir, 'pyenv'))
with open(os.path.join(data_dir, 'pyenv', 'main.py'), 'w') as f:
f.write(script)
script = [
'echo "🐍 {}\n"'.format(_('Setting up Python environment...')),
'python3 -m venv "{}"'.format(os.path.join(data_dir, 'pyenv')),
'{} {}'.format(os.path.join(data_dir, 'pyenv', 'bin', 'python3').replace(' ', '\\ '), os.path.join(data_dir, 'pyenv', 'main.py').replace(' ', '\\ '))
]
if os.path.isfile(os.path.join(data_dir, 'pyenv', 'requirements.txt')):
script.insert(1, '{} install -r {} | grep -v "already satisfied"; clear'.format(os.path.join(data_dir, 'pyenv', 'bin', 'pip3'), os.path.join(data_dir, 'pyenv', 'requirements.txt')))
else:
with open(os.path.join(data_dir, 'pyenv', 'requirements.txt'), 'w') as f:
f.write('')
script = ';\n'.join(script)
script += '; echo "\n🦙 {}"'.format(_('Script exited'))
if language_name == 'bash':
script = re.sub(r'(?m)^\s*sudo', 'pkexec', script)
if shutil.which('flatpak-spawn') and language_name == 'bash':
sandbox = True
try:
process = subprocess.run(['flatpak-spawn', '--host', 'bash', '-c', 'echo "test"'], check=True)
sandbox = False
except Exception as e:
pass
if sandbox:
script = 'echo "🦙 {}\n";'.format(_('The script is contained inside Flatpak')) + script
show_terminal(['bash', '-c', script])
else:
show_terminal(['flatpak-spawn', '--host', 'bash', '-c', script])
else:
show_terminal(['bash', '-c', script])

474
src/dialogs.py Normal file
View File

@ -0,0 +1,474 @@
# dialogs.py
"""
Handles UI dialogs
"""
import os
import logging, requests, threading, shutil, subprocess, re
from pytube import YouTube
from html2text import html2text
from gi.repository import Adw, Gtk
from .internal import cache_dir, data_dir
logger = logging.getLogger(__name__)
# CLEAR CHAT | WORKS
def clear_chat_response(self, dialog, task):
if dialog.choose_finish(task) == "clear":
self.chat_list_box.get_current_chat().show_welcome_screen(len(self.model_manager.get_model_list()) > 0)
self.save_history(self.chat_list_box.get_current_chat())
def clear_chat(self):
if self.chat_list_box.get_current_chat().busy:
self.show_toast(_("Chat cannot be cleared while receiving a message"), self.main_overlay)
return
dialog = Adw.AlertDialog(
heading=_("Clear Chat?"),
body=_("Are you sure you want to clear the chat?"),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("clear", _("Clear"))
dialog.set_response_appearance("clear", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("clear")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task: clear_chat_response(self, dialog, task)
)
# DELETE CHAT | WORKS
def delete_chat_response(self, dialog, task, chat_name):
if dialog.choose_finish(task) == "delete":
self.chat_list_box.delete_chat(chat_name)
def delete_chat(self, chat_name):
dialog = Adw.AlertDialog(
heading=_("Delete Chat?"),
body=_("Are you sure you want to delete '{}'?").format(chat_name),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("delete", _("Delete"))
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("delete")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, chat_name=chat_name: delete_chat_response(self, dialog, task, chat_name)
)
# RENAME CHAT | WORKS
def rename_chat_response(self, dialog, task, old_chat_name, entry):
if not entry:
return
new_chat_name = entry.get_text()
if old_chat_name == new_chat_name:
return
if new_chat_name and (task is None or dialog.choose_finish(task) == "rename"):
self.chat_list_box.rename_chat(old_chat_name, new_chat_name)
def rename_chat(self, chat_name):
entry = Gtk.Entry()
dialog = Adw.AlertDialog(
heading=_("Rename Chat?"),
body=_("Renaming '{}'").format(chat_name),
extra_child=entry,
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("rename", _("Rename"))
dialog.set_response_appearance("rename", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("rename")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, old_chat_name=chat_name, entry=entry: rename_chat_response(self, dialog, task, old_chat_name, entry)
)
# NEW CHAT | WORKS | UNUSED REASON: The 'Add Chat' button now creates a chat without a name AKA "New Chat"
def new_chat_response(self, dialog, task, entry):
chat_name = _("New Chat")
if entry is not None and entry.get_text() != "":
chat_name = entry.get_text()
if chat_name and (task is None or dialog.choose_finish(task) == "create"):
self.new_chat(chat_name)
def new_chat(self):
entry = Gtk.Entry()
dialog = Adw.AlertDialog(
heading=_("Create Chat?"),
body=_("Enter name for new chat"),
extra_child=entry,
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("create", _("Create"))
dialog.set_response_appearance("create", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("create")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, entry=entry: new_chat_response(self, dialog, task, entry)
)
# STOP PULL MODEL | WORKS
def stop_pull_model_response(self, dialog, task, pulling_model):
if dialog.choose_finish(task) == "stop":
if len(list(pulling_model.get_parent())) == 1:
pulling_model.get_parent().set_visible(False)
pulling_model.get_parent().remove(pulling_model)
def stop_pull_model(self, pulling_model):
dialog = Adw.AlertDialog(
heading=_("Stop Download?"),
body=_("Are you sure you want to stop pulling '{}'?").format(self.convert_model_name(pulling_model.get_name(), 0)),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("stop", _("Stop"))
dialog.set_response_appearance("stop", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("stop")
dialog.choose(
parent = self.manage_models_dialog,
cancellable = None,
callback = lambda dialog, task, model=pulling_model: stop_pull_model_response(self, dialog, task, model)
)
# DELETE MODEL | WORKS
def delete_model_response(self, dialog, task, model_name):
if dialog.choose_finish(task) == "delete":
self.model_manager.remove_local_model(model_name)
def delete_model(self, model_name):
dialog = Adw.AlertDialog(
heading=_("Delete Model?"),
body=_("Are you sure you want to delete '{}'?").format(self.convert_model_name(model_name, 0)),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("delete", _("Delete"))
dialog.set_response_appearance("delete", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("delete")
dialog.choose(
parent = self.manage_models_dialog,
cancellable = None,
callback = lambda dialog, task, model_name = model_name: delete_model_response(self, dialog, task, model_name)
)
# REMOVE IMAGE | WORKS
def remove_attached_file_response(self, dialog, task, name):
if dialog.choose_finish(task) == 'remove':
self.file_preview_dialog.close()
self.remove_attached_file(name)
def remove_attached_file(self, name):
dialog = Adw.AlertDialog(
heading=_("Remove Attachment?"),
body=_("Are you sure you want to remove attachment?"),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("remove", _("Remove"))
dialog.set_response_appearance("remove", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("remove")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, name=name: remove_attached_file_response(self, dialog, task, name)
)
# RECONNECT REMOTE | WORKS
def reconnect_remote_response(self, dialog, task, url_entry, bearer_entry):
response = dialog.choose_finish(task)
if not task or response == "remote":
self.remote_connection_entry.set_text(url_entry.get_text())
self.remote_connection_switch.set_sensitive(url_entry.get_text())
self.remote_bearer_token_entry.set_text(bearer_entry.get_text())
self.remote_connection_switch.set_active(True)
self.model_manager.update_local_list()
elif response == "local":
self.ollama_instance.remote = False
self.ollama_instance.start()
self.model_manager.update_local_list()
elif response == "close":
self.destroy()
def reconnect_remote(self):
entry_url = Gtk.Entry(
css_classes = ["error"],
text = self.ollama_instance.remote_url,
placeholder_text = "URL"
)
entry_bearer_token = Gtk.Entry(
css_classes = ["error"] if self.ollama_instance.bearer_token else None,
text = self.ollama_instance.bearer_token,
placeholder_text = "Bearer Token (Optional)"
)
container = Gtk.Box(
orientation = 1,
spacing = 10
)
container.append(entry_url)
container.append(entry_bearer_token)
dialog = Adw.AlertDialog(
heading=_("Connection Error"),
body=_("The remote instance has disconnected"),
extra_child=container
)
dialog.add_response("close", _("Close Alpaca"))
if shutil.which('ollama'):
dialog.add_response("local", _("Use local instance"))
dialog.add_response("remote", _("Connect"))
dialog.set_response_appearance("remote", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("remote")
dialog.choose(
parent = self,
cancellable = None,
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
def create_model_from_existing_response(self, dialog, task, dropdown):
model = dropdown.get_selected_item().get_string()
if dialog.choose_finish(task) == 'accept' and model:
self.create_model(model, False)
def create_model_from_existing(self):
string_list = Gtk.StringList()
for model in self.model_manager.get_model_list():
string_list.append(self.convert_model_name(model, 0))
dropdown = Gtk.DropDown()
dropdown.set_model(string_list)
dialog = Adw.AlertDialog(
heading=_("Select Model"),
body=_("This model will be used as the base for the new model"),
extra_child=dropdown
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, dropdown=dropdown: create_model_from_existing_response(self, dialog, task, dropdown)
)
def create_model_from_file_response(self, file_dialog, result):
try:
file = file_dialog.open_finish(result)
try:
self.create_model(file.get_path(), True)
except Exception as e:
logger.error(e)
self.show_toast(_("An error occurred while creating the model"), self.main_overlay)
except Exception as e:
logger.error(e)
def create_model_from_file(self):
file_dialog = Gtk.FileDialog(default_filter=self.file_filter_gguf)
file_dialog.open(self, None, lambda file_dialog, result: create_model_from_file_response(self, file_dialog, result))
def create_model_from_name_response(self, dialog, task, entry):
model = entry.get_text().lower().strip()
if dialog.choose_finish(task) == 'accept' and model:
threading.Thread(target=self.model_manager.pull_model, kwargs={"model_name": model}).start()
def create_model_from_name(self):
entry = Gtk.Entry()
entry.get_delegate().connect("insert-text", lambda *_ : self.check_alphanumeric(*_, ['-', '.', ':', '_', '/']))
dialog = Adw.AlertDialog(
heading=_("Pull Model"),
body=_("Input the name of the model in this format\nname:tag"),
extra_child=entry
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, entry=entry: create_model_from_name_response(self, dialog, task, entry)
)
# FILE CHOOSER | WORKS
def attach_file_response(self, file_dialog, result):
file_types = {
"plain_text": ["txt", "md", "html", "css", "js", "py", "java", "json", "xml"],
"image": ["png", "jpeg", "jpg", "webp", "gif"],
"pdf": ["pdf"]
}
try:
file = file_dialog.open_finish(result)
except Exception as e:
logger.error(e)
return
extension = file.get_path().split(".")[-1]
file_type = next(key for key, value in file_types.items() if extension in value)
if not file_type:
return
if file_type == 'image' and not self.model_manager.verify_if_image_can_be_used():
self.show_toast(_("Image recognition is only available on specific models"), self.main_overlay)
return
self.attach_file(file.get_path(), file_type)
def attach_file(self, file_filter):
file_dialog = Gtk.FileDialog(default_filter=file_filter)
file_dialog.open(self, None, lambda file_dialog, result: attach_file_response(self, file_dialog, result))
# YouTube caption | WORKS
def youtube_caption_response(self, dialog, task, video_url, caption_drop_down):
if dialog.choose_finish(task) == "accept":
buffer = self.message_text_view.get_buffer()
text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).replace(video_url, "")
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), text, len(text))
yt = YouTube(video_url)
text = "{}\n{}\n{}\n\n".format(yt.title, yt.author, yt.watch_url)
selected_caption = caption_drop_down.get_selected_item().get_string()
for event in yt.captions[selected_caption.split('(')[-1][:-1]].json_captions['events']:
text += "{}\n".format(event['segs'][0]['utf8'].replace('\n', '\\n'))
if not os.path.exists(os.path.join(cache_dir, 'tmp/youtube')):
os.makedirs(os.path.join(cache_dir, 'tmp/youtube'))
file_path = os.path.join(os.path.join(cache_dir, 'tmp/youtube'), f'{yt.title} ({selected_caption.split(" (")[0]})')
with open(file_path, 'w+', encoding="utf-8") as f:
f.write(text)
self.attach_file(file_path, 'youtube')
def youtube_caption(self, video_url):
yt = YouTube(video_url)
video_title = yt.title
captions = yt.captions
if len(captions) == 0:
self.show_toast(_("This video does not have any transcriptions"), self.main_overlay)
return
caption_list = Gtk.StringList()
for caption in captions:
caption_list.append("{} ({})".format(caption.name.title(), caption.code))
caption_drop_down = Gtk.DropDown(
enable_search=len(captions) > 10,
model=caption_list
)
dialog = Adw.AlertDialog(
heading=_("Attach YouTube Video?"),
body=_("{}\n\nPlease select a transcript to include").format(video_title),
extra_child=caption_drop_down,
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, video_url = video_url, caption_drop_down = caption_drop_down: youtube_caption_response(self, dialog, task, video_url, caption_drop_down)
)
# Website extraction |
def attach_website_response(self, dialog, task, url):
if dialog.choose_finish(task) == "accept":
response = requests.get(url)
if response.status_code == 200:
html = response.text
md = html2text(html)
buffer = self.message_text_view.get_buffer()
textview_text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).replace(url, "")
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), textview_text, len(textview_text))
if not os.path.exists('/tmp/alpaca/websites/'):
os.makedirs('/tmp/alpaca/websites/')
md_name = self.generate_numbered_name('website.md', os.listdir('/tmp/alpaca/websites'))
file_path = os.path.join('/tmp/alpaca/websites/', md_name)
with open(file_path, 'w+', encoding="utf-8") as f:
f.write('{}\n\n{}'.format(url, md))
self.attach_file(file_path, 'website')
else:
self.show_toast(_("An error occurred while extracting text from the website"), self.main_overlay)
def attach_website(self, url):
dialog = Adw.AlertDialog(
heading=_("Attach Website? (Experimental)"),
body=_("Are you sure you want to attach\n'{}'?").format(url),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, url=url: attach_website_response(self, dialog, task, url)
)
# Run Script
def run_script_response(self, dialog, task, script, language_name):
if dialog.choose_finish(task) == "accept":
logger.info('Running: \n{}'.format(script))
if language_name == 'python3':
if not os.path.isdir(os.path.join(data_dir, 'pyenv')):
os.mkdir(os.path.join(data_dir, 'pyenv'))
with open(os.path.join(data_dir, 'pyenv', 'main.py'), 'w') as f:
f.write(script)
script = [
'echo "🐍 {}\n"'.format(_('Setting up Python environment...')),
'python3 -m venv "{}"'.format(os.path.join(data_dir, 'pyenv')),
'{} {}'.format(os.path.join(data_dir, 'pyenv', 'bin', 'python3').replace(' ', '\\ '), os.path.join(data_dir, 'pyenv', 'main.py').replace(' ', '\\ '))
]
if os.path.isfile(os.path.join(data_dir, 'pyenv', 'requirements.txt')):
script.insert(1, '{} install -r {} | grep -v "already satisfied"; clear'.format(os.path.join(data_dir, 'pyenv', 'bin', 'pip3'), os.path.join(data_dir, 'pyenv', 'requirements.txt')))
else:
with open(os.path.join(data_dir, 'pyenv', 'requirements.txt'), 'w') as f:
f.write('')
script = ';\n'.join(script)
script += '; echo "\n🦙 {}"'.format(_('Script exited'))
if language_name == 'bash':
script = re.sub(r'(?m)^\s*sudo', 'pkexec', script)
if shutil.which('flatpak-spawn') and language_name == 'bash':
sandbox = True
try:
process = subprocess.run(['flatpak-spawn', '--host', 'bash', '-c', 'echo "test"'], check=True)
sandbox = False
except Exception as e:
pass
if sandbox:
script = 'echo "🦙 {}\n";'.format(_('The script is contained inside Flatpak')) + script
self.run_terminal(['bash', '-c', script])
else:
self.run_terminal(['flatpak-spawn', '--host', 'bash', '-c', script])
else:
self.run_terminal(['bash', '-c', script])
def run_script(self, script:str, language_name:str):
dialog = Adw.AlertDialog(
heading=_("Run Script"),
body=_("Make sure you understand what this script does before running it, Alpaca is not responsible for any damages to your device or data"),
close_response="cancel"
)
dialog.add_response("cancel", _("Cancel"))
dialog.add_response("accept", _("Accept"))
dialog.set_response_appearance("accept", Adw.ResponseAppearance.SUGGESTED)
dialog.set_default_response("accept")
dialog.choose(
parent = self,
cancellable = None,
callback = lambda dialog, task, script=script, language_name=language_name: run_script_response(self, dialog, task, script, language_name)
)

View File

@ -1,83 +0,0 @@
#generic_actions.py
"""
Working on organizing the code
"""
import os, requests
from youtube_transcript_api import YouTubeTranscriptApi
from html2text import html2text
from .internal import cache_dir
window = None
def connect_remote(remote_url:str, bearer_token:str):
window.ollama_instance.remote_url=remote_url
window.ollama_instance.bearer_token=bearer_token
window.ollama_instance.remote = True
window.ollama_instance.stop()
window.model_manager.update_local_list()
window.save_server_config()
def attach_youtube(video_title:str, video_author:str, watch_url:str, video_url:str, video_id:str, caption_name:str):
buffer = window.message_text_view.get_buffer()
text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).replace(video_url, "")
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), text, len(text))
result_text = "{}\n{}\n{}\n\n".format(video_title, video_author, watch_url)
caption_name = caption_name.split(' (')[-1][:-1]
if caption_name.startswith('Translate:'):
available_captions = get_youtube_transcripts(video_id)
original_caption_name = available_captions[0].split(' (')[-1][:-1]
transcript = YouTubeTranscriptApi.list_transcripts(video_id).find_transcript([original_caption_name]).translate(caption_name.split(':')[-1]).fetch()
result_text += '(Auto translated from {})\n'.format(available_captions[0])
else:
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[caption_name])
result_text += '\n'.join([t['text'] for t in transcript])
if not os.path.exists(os.path.join(cache_dir, 'tmp/youtube')):
os.makedirs(os.path.join(cache_dir, 'tmp/youtube'))
file_path = os.path.join(os.path.join(cache_dir, 'tmp/youtube'), '{} ({})'.format(video_title.replace('/', ' '), caption_name))
with open(file_path, 'w+', encoding="utf-8") as f:
f.write(result_text)
window.attach_file(file_path, 'youtube')
def get_youtube_transcripts(video_id:str):
return ['{} ({})'.format(t.language, t.language_code) for t in YouTubeTranscriptApi.list_transcripts(video_id)]
def attach_website(url:str):
response = requests.get(url)
if response.status_code == 200:
html = response.text
md = html2text(html)
buffer = window.message_text_view.get_buffer()
textview_text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False).replace(url, "")
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), textview_text, len(textview_text))
if not os.path.exists('/tmp/alpaca/websites/'):
os.makedirs('/tmp/alpaca/websites/')
md_name = window.generate_numbered_name('website.md', os.listdir('/tmp/alpaca/websites'))
file_path = os.path.join('/tmp/alpaca/websites/', md_name)
with open(file_path, 'w+', encoding="utf-8") as f:
f.write('{}\n\n{}'.format(url, md))
window.attach_file(file_path, 'website')
else:
window.show_toast(_("An error occurred while extracting text from the website"), window.main_overlay)
def attach_file(file):
file_types = {
"plain_text": ["txt", "md", "html", "css", "js", "py", "java", "json", "xml"],
"image": ["png", "jpeg", "jpg", "webp", "gif"],
"pdf": ["pdf"]
}
extension = file.get_path().split(".")[-1]
file_type = next(key for key, value in file_types.items() if extension in value)
if not file_type:
return
if file_type == 'image' and not window.model_manager.verify_if_image_can_be_used():
window.show_toast(_("Image recognition is only available on specific models"), window.main_overlay)
return
window.attach_file(file.get_path(), file_type)

View File

@ -1,2 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 816 B

View File

@ -1,2 +0,0 @@
<?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 8 0 c -4.410156 0 -8 3.589844 -8 8 s 3.589844 8 8 8 s 8 -3.589844 8 -8 s -3.589844 -8 -8 -8 z m 0 2 c 3.332031 0 6 2.667969 6 6 s -2.667969 6 -6 6 s -6 -2.667969 -6 -6 s 2.667969 -6 6 -6 z m 0 1.875 c -0.621094 0 -1.125 0.503906 -1.125 1.125 s 0.503906 1.125 1.125 1.125 s 1.125 -0.503906 1.125 -1.125 s -0.503906 -1.125 -1.125 -1.125 z m -1.523438 3.125 c -0.265624 0.011719 -0.476562 0.230469 -0.476562 0.5 c 0 0.277344 0.222656 0.5 0.5 0.5 h 0.5 v 3 h -0.5 c -0.277344 0 -0.5 0.222656 -0.5 0.5 s 0.222656 0.5 0.5 0.5 h 3 c 0.277344 0 0.5 -0.222656 0.5 -0.5 s -0.222656 -0.5 -0.5 -0.5 h -0.5 v -4 h -2.5 c -0.007812 0 -0.015625 0 -0.023438 0 z m 0 0" fill="#222222"/></svg>

Before

Width:  |  Height:  |  Size: 813 B

View File

@ -40,7 +40,6 @@ translators = [
'Louis Chauvet-Villaret (French) https://github.com/loulou64490', 'Louis Chauvet-Villaret (French) https://github.com/loulou64490',
'Théo FORTIN (French) https://github.com/topiga', 'Théo FORTIN (French) https://github.com/topiga',
'Daimar Stein (Brazilian Portuguese) https://github.com/not-a-dev-stein', 'Daimar Stein (Brazilian Portuguese) https://github.com/not-a-dev-stein',
'Bruno Antunes (Brazilian Portuguese) https://github.com/antun3s',
'CounterFlow64 (Norwegian) https://github.com/CounterFlow64', 'CounterFlow64 (Norwegian) https://github.com/CounterFlow64',
'Aritra Saha (Bengali) https://github.com/olumolu', 'Aritra Saha (Bengali) https://github.com/olumolu',
'Yuehao Sui (Simplified Chinese) https://github.com/8ar10der', 'Yuehao Sui (Simplified Chinese) https://github.com/8ar10der',
@ -57,8 +56,7 @@ class AlpacaApplication(Adw.Application):
def __init__(self, version): def __init__(self, version):
super().__init__(application_id='com.jeffser.Alpaca', super().__init__(application_id='com.jeffser.Alpaca',
flags=Gio.ApplicationFlags.DEFAULT_FLAGS) flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
self.create_action('quit', lambda *_: self.props.active_window.closing_app(None), ['<primary>q']) self.create_action('quit', lambda *_: self.props.active_window.closing_app(None), ['<primary>w', '<primary>q'])
self.set_accels_for_action('app.delete_current_chat', ['<primary>w'])
self.create_action('preferences', lambda *_: self.props.active_window.preferences_dialog.present(self.props.active_window), ['<primary>comma']) self.create_action('preferences', lambda *_: self.props.active_window.preferences_dialog.present(self.props.active_window), ['<primary>comma'])
self.create_action('about', self.on_about_action) self.create_action('about', self.on_about_action)
self.set_accels_for_action("win.show-help-overlay", ['<primary>slash']) self.set_accels_for_action("win.show-help-overlay", ['<primary>slash'])

View File

@ -40,10 +40,10 @@ alpaca_sources = [
'main.py', 'main.py',
'window.py', 'window.py',
'connection_handler.py', 'connection_handler.py',
'dialogs.py',
'available_models.json', 'available_models.json',
'available_models_descriptions.py', 'available_models_descriptions.py',
'internal.py', 'internal.py'
'generic_actions.py'
] ]
custom_widgets = [ custom_widgets = [
@ -51,8 +51,7 @@ custom_widgets = [
'custom_widgets/message_widget.py', 'custom_widgets/message_widget.py',
'custom_widgets/chat_widget.py', 'custom_widgets/chat_widget.py',
'custom_widgets/model_widget.py', 'custom_widgets/model_widget.py',
'custom_widgets/terminal_widget.py', 'custom_widgets/terminal_widget.py'
'custom_widgets/dialog_widget.py'
] ]
install_data(alpaca_sources, install_dir: moduledir) install_data(alpaca_sources, install_dir: moduledir)

View File

@ -4,9 +4,6 @@
.chat_image_button { .chat_image_button {
padding: 0; padding: 0;
} }
.chat_image_button, .chat_image_button image {
border-radius: 10px;
}
.editing_message_textview { .editing_message_textview {
border-radius: 5px; border-radius: 5px;
padding: 5px; padding: 5px;

View File

@ -24,7 +24,6 @@ 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 pytube import YouTube
import gi import gi
gi.require_version('GtkSource', '5') gi.require_version('GtkSource', '5')
@ -32,8 +31,8 @@ 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
from . import connection_handler, generic_actions from . import dialogs, connection_handler
from .custom_widgets import message_widget, chat_widget, model_widget, terminal_widget, dialog_widget from .custom_widgets import message_widget, chat_widget, model_widget, terminal_widget
from .internal import config_dir, data_dir, cache_dir, source_dir from .internal import config_dir, data_dir, cache_dir, source_dir
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,7 +54,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
#Override elements #Override elements
overrides_group = Gtk.Template.Child() overrides_group = Gtk.Template.Child()
instance_page = Gtk.Template.Child()
#Elements #Elements
split_view_overlay = Gtk.Template.Child() split_view_overlay = Gtk.Template.Child()
@ -69,7 +67,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
preferences_dialog = Gtk.Template.Child() preferences_dialog = Gtk.Template.Child()
shortcut_window : Gtk.ShortcutsWindow = Gtk.Template.Child() shortcut_window : Gtk.ShortcutsWindow = Gtk.Template.Child()
file_preview_dialog = Gtk.Template.Child() file_preview_dialog = Gtk.Template.Child()
file_preview_text_label = Gtk.Template.Child() file_preview_text_view = Gtk.Template.Child()
file_preview_image = Gtk.Template.Child() file_preview_image = Gtk.Template.Child()
welcome_dialog = Gtk.Template.Child() welcome_dialog = Gtk.Template.Child()
welcome_carousel = Gtk.Template.Child() welcome_carousel = Gtk.Template.Child()
@ -102,9 +100,6 @@ class AlpacaWindow(Adw.ApplicationWindow):
title_stack = Gtk.Template.Child() title_stack = Gtk.Template.Child()
manage_models_dialog = Gtk.Template.Child() manage_models_dialog = Gtk.Template.Child()
model_scroller = Gtk.Template.Child() model_scroller = Gtk.Template.Child()
model_detail_page = Gtk.Template.Child()
model_detail_create_button = Gtk.Template.Child()
ollama_information_label = Gtk.Template.Child()
chat_list_container = Gtk.Template.Child() chat_list_container = Gtk.Template.Child()
chat_list_box = None chat_list_box = None
@ -116,6 +111,8 @@ class AlpacaWindow(Adw.ApplicationWindow):
background_switch = Gtk.Template.Child() background_switch = Gtk.Template.Child()
powersaver_warning_switch = Gtk.Template.Child() powersaver_warning_switch = Gtk.Template.Child()
remote_connection_switch = Gtk.Template.Child() remote_connection_switch = Gtk.Template.Child()
remote_connection_entry = Gtk.Template.Child()
remote_bearer_token_entry = Gtk.Template.Child()
banner = Gtk.Template.Child() banner = Gtk.Template.Child()
@ -214,6 +211,45 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.welcome_dialog.force_close() self.welcome_dialog.force_close()
self.powersaver_warning_switch.set_active(True) self.powersaver_warning_switch.set_active(True)
@Gtk.Template.Callback()
def change_remote_connection(self, switcher, *_):
logger.debug("Connection switched")
if self.remote_connection_switch.get_active() and not self.remote_connection_entry.get_text():
self.remote_connection_switch.set_active(False)
return
self.ollama_instance.remote = self.remote_connection_switch.get_active()
if self.ollama_instance.remote:
self.ollama_instance.stop()
else:
self.ollama_instance.start()
if self.model_manager:
self.model_manager.update_local_list()
self.save_server_config()
@Gtk.Template.Callback()
def change_remote_url(self, entry):
if entry.get_text() and not entry.get_text().startswith("http"):
entry.set_text("http://{}".format(entry.get_text()))
return
if entry.get_text() and entry.get_text() != entry.get_text().rstrip('/'):
entry.set_text(entry.get_text().rstrip('/'))
return
self.remote_connection_switch.set_sensitive(entry.get_text())
logger.debug(f"Changing remote url: {self.ollama_instance.remote_url}")
self.ollama_instance.remote_url = entry.get_text()
if not entry.get_text():
self.remote_connection_switch.set_active(False)
if self.ollama_instance.remote and self.model_manager and entry.get_text():
self.model_manager.update_local_list()
self.save_server_config()
@Gtk.Template.Callback()
def change_remote_bearer_token(self, entry):
self.ollama_instance.bearer_token = entry.get_text()
if self.ollama_instance.remote_url and self.ollama_instance.remote and self.model_manager:
self.model_manager.update_local_list()
self.save_server_config()
@Gtk.Template.Callback() @Gtk.Template.Callback()
def switch_run_on_background(self, switch, user_data): def switch_run_on_background(self, switch, user_data):
logger.debug("Switching run on background") logger.debug("Switching run on background")
@ -335,9 +371,9 @@ class AlpacaWindow(Adw.ApplicationWindow):
clipboard.read_text_async(None, self.cb_text_received) clipboard.read_text_async(None, self.cb_text_received)
clipboard.read_texture_async(None, self.cb_image_received) clipboard.read_texture_async(None, self.cb_image_received)
@Gtk.Template.Callback() def run_terminal(self, script:list):
def model_detail_create_button_clicked(self, button): self.terminal_scroller.set_child(terminal_widget.terminal(script))
self.create_model(button.get_name(), False) self.terminal_dialog.present(self)
def convert_model_name(self, name:str, mode:int) -> str: # mode=0 name:tag -> Name (tag) | mode=1 Name (tag) -> name:tag def convert_model_name(self, name:str, mode:int) -> str: # mode=0 name:tag -> Name (tag) | mode=1 Name (tag) -> name:tag
try: try:
@ -358,15 +394,20 @@ class AlpacaWindow(Adw.ApplicationWindow):
modelfile_buffer.delete(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter()) modelfile_buffer.delete(modelfile_buffer.get_start_iter(), modelfile_buffer.get_end_iter())
self.create_model_system.set_text('') self.create_model_system.set_text('')
if not file: if not file:
data = next((element for element in list(self.model_manager.model_selector.get_popover().model_list_box) if element.get_name() == self.convert_model_name(model, 1)), None).data response = self.ollama_instance.request("POST", "api/show", json.dumps({"name": self.convert_model_name(model, 1)}))
modelfile = [] if response.status_code == 200:
for line in data['modelfile'].split('\n'): data = json.loads(response.text)
if line.startswith('SYSTEM'): modelfile = []
self.create_model_system.set_text(line[len('SYSTEM'):].strip()) for line in data['modelfile'].split('\n'):
if not line.startswith('SYSTEM') and not line.startswith('FROM') and not line.startswith('#'): if line.startswith('SYSTEM'):
modelfile.append(line) self.create_model_system.set_text(line[len('SYSTEM'):].strip())
self.create_model_name.set_text(self.convert_model_name(model, 1).split(':')[0] + "-custom") if not line.startswith('SYSTEM') and not line.startswith('FROM') and not line.startswith('#'):
modelfile_buffer.insert(modelfile_buffer.get_start_iter(), '\n'.join(modelfile), len('\n'.join(modelfile).encode('utf-8'))) modelfile.append(line)
self.create_model_name.set_text(self.convert_model_name(model, 1).split(':')[0] + "-custom")
modelfile_buffer.insert(modelfile_buffer.get_start_iter(), '\n'.join(modelfile), len('\n'.join(modelfile).encode('utf-8')))
else:
##TODO ERROR MESSAGE
return
self.create_model_base.set_subtitle(self.convert_model_name(model, 1)) self.create_model_base.set_subtitle(self.convert_model_name(model, 1))
else: else:
self.create_model_name.set_text(os.path.splitext(os.path.basename(model))[0]) self.create_model_name.set_text(os.path.splitext(os.path.basename(model))[0])
@ -405,7 +446,7 @@ class AlpacaWindow(Adw.ApplicationWindow):
if content: if content:
if file_type == 'image': if file_type == 'image':
self.file_preview_image.set_visible(True) self.file_preview_image.set_visible(True)
self.file_preview_text_label.set_visible(False) self.file_preview_text_view.set_visible(False)
image_data = base64.b64decode(content) image_data = base64.b64decode(content)
loader = GdkPixbuf.PixbufLoader.new() loader = GdkPixbuf.PixbufLoader.new()
loader.write(image_data) loader.write(image_data)
@ -418,8 +459,10 @@ class AlpacaWindow(Adw.ApplicationWindow):
self.file_preview_open_button.set_name(file_path) self.file_preview_open_button.set_name(file_path)
else: else:
self.file_preview_image.set_visible(False) self.file_preview_image.set_visible(False)
self.file_preview_text_label.set_visible(True) self.file_preview_text_view.set_visible(True)
buffer = self.file_preview_text_label.set_label(content) buffer = self.file_preview_text_view.get_buffer()
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
buffer.insert(buffer.get_start_iter(), content, len(content.encode('utf-8')))
if file_type == 'youtube': if file_type == 'youtube':
self.file_preview_dialog.set_title(content.split('\n')[0]) self.file_preview_dialog.set_title(content.split('\n')[0])
self.file_preview_open_button.set_name(content.split('\n')[2]) self.file_preview_open_button.set_name(content.split('\n')[2])
@ -607,7 +650,6 @@ Generate a title following these rules:
self.chat_list_box.prepend_chat(_("New Chat")) self.chat_list_box.prepend_chat(_("New Chat"))
def generate_numbered_name(self, chat_name:str, compare_list:list) -> str: def generate_numbered_name(self, chat_name:str, compare_list:list) -> str:
if chat_name in compare_list: if chat_name in compare_list:
for i in range(len(compare_list)): for i in range(len(compare_list)):
@ -627,16 +669,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.ollama_instance.remote: if self.ollama_instance.remote:
options = { dialogs.reconnect_remote(self)
_("Close Alpaca"): {"callback": lambda *_: self.get_application().quit(), "appearance": "destructive"},
_("Use Local Instance"): {"callback": lambda *_: self.remote_connection_switch.set_active(False)},
_("Connect"): {"callback": lambda url, bearer: generic_actions.connect_remote(url,bearer), "appearance": "suggested"}
}
entries = [
{"text": self.ollama_instance.remote_url, "css": ['error'], "placeholder": _('Server URL')},
{"text": self.ollama_instance.bearer_token, "css": ['error'] if self.ollama_instance.bearer_token else None, "placeholder": _('Bearer Token (Optional)')}
]
dialog_widget.Entry(_('Connection Error'), _('The remote instance has disconnected'), list(options)[0], options, entries)
else: else:
self.ollama_instance.reset() self.ollama_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)
@ -681,8 +714,6 @@ Generate a title following these rules:
del self.attachments[name] del self.attachments[name]
if len(self.attachments) == 0: if len(self.attachments) == 0:
self.attachment_box.set_visible(False) self.attachment_box.set_visible(False)
if self.file_preview_dialog.get_visible():
self.file_preview_dialog.close()
def attach_file(self, file_path, file_type): def attach_file(self, file_path, file_type):
logger.debug(f"Attaching file: {file_path}") logger.debug(f"Attaching file: {file_path}")
@ -708,6 +739,7 @@ Generate a title following these rules:
child=button_content child=button_content
) )
self.attachments[file_name] = {"path": file_path, "type": file_type, "content": content, "button": button} self.attachments[file_name] = {"path": file_path, "type": file_type, "content": content, "button": button}
#button.connect("clicked", lambda button: dialogs.remove_attached_file(self, button))
button.connect("clicked", lambda button : self.preview_file(file_path, file_type, file_name)) button.connect("clicked", lambda button : self.preview_file(file_path, file_type, file_name))
self.attachment_container.append(button) self.attachment_container.append(button)
self.attachment_box.set_visible(True) self.attachment_box.set_visible(True)
@ -717,23 +749,11 @@ Generate a title following these rules:
chat_name = chat_row.label.get_label() chat_name = chat_row.label.get_label()
action_name = action.get_name() action_name = action.get_name()
if action_name in ('delete_chat', 'delete_current_chat'): if action_name in ('delete_chat', 'delete_current_chat'):
dialog_widget.simple( dialogs.delete_chat(self, chat_name)
_('Delete Chat?'),
_("Are you sure you want to delete '{}'?").format(chat_name),
lambda chat_name=chat_name, *_: self.chat_list_box.delete_chat(chat_name),
_('Delete'),
'destructive'
)
elif action_name in ('duplicate_chat', 'duplicate_current_chat'): elif action_name in ('duplicate_chat', 'duplicate_current_chat'):
self.chat_list_box.duplicate_chat(chat_name) self.chat_list_box.duplicate_chat(chat_name)
elif action_name in ('rename_chat', 'rename_current_chat'): elif action_name in ('rename_chat', 'rename_current_chat'):
dialog_widget.simple_entry( dialogs.rename_chat(self, chat_name)
_('Rename Chat?'),
_("Renaming '{}'").format(chat_name),
lambda new_chat_name, old_chat_name=chat_name, *_: self.chat_list_box.rename_chat(old_chat_name, new_chat_name),
{'placeholder': _('Chat name')},
_('Rename')
)
elif action_name in ('export_chat', 'export_current_chat'): elif action_name in ('export_chat', 'export_current_chat'):
self.chat_list_box.export_chat(chat_name) self.chat_list_box.export_chat(chat_name)
@ -741,36 +761,6 @@ Generate a title following these rules:
self.selected_chat_row = self.chat_list_box.get_selected_row() self.selected_chat_row = self.chat_list_box.get_selected_row()
self.chat_actions(action, user_data) self.chat_actions(action, user_data)
def youtube_detected(self, video_url):
try:
tries=0
while True:
try:
yt = YouTube(video_url)
video_title = yt.title
break
except Exception as e:
tries+=1
if tries == 4:
raise Exception(e)
transcriptions = generic_actions.get_youtube_transcripts(yt.video_id)
if len(transcriptions) == 0:
self.show_toast(_("This video does not have any transcriptions"), self.main_overlay)
return
if not any(filter(lambda x: '(en' in x and 'auto-generated' not in x and len(transcriptions) > 1, transcriptions)):
transcriptions.insert(1, 'English (translate:en)')
dialog_widget.simple_dropdown(
_('Attach YouTube Video?'),
_('{}\n\nPlease select a transcript to include').format(video_title),
lambda caption_name, yt=yt, video_url=video_url: generic_actions.attach_youtube(yt.title, yt.author, yt.watch_url, video_url, yt.video_id, caption_name),
transcriptions
)
except Exception as e:
logger.error(e)
self.show_toast(_("Error attaching video, please try again"), self.main_overlay)
def cb_text_received(self, clipboard, result): def cb_text_received(self, clipboard, result):
try: try:
text = clipboard.read_text_finish(result) text = clipboard.read_text_finish(result)
@ -786,13 +776,13 @@ Generate a title following these rules:
r'(?:/[^\\s]*)?' r'(?:/[^\\s]*)?'
) )
if youtube_regex.match(text): if youtube_regex.match(text):
self.youtube_detected(text) try:
dialogs.youtube_caption(self, text)
except Exception as e:
logger.error(e)
self.show_toast(_("This video is not available"), self.main_overlay)
elif url_regex.match(text): elif url_regex.match(text):
dialog_widget.simple( dialogs.attach_website(self, text)
_('Attach Website? (Experimental)'),
_("Are you sure you want to attach\n'{}'?").format(text),
lambda url=text: generic_actions.attach_website(url)
)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
@ -830,42 +820,6 @@ Generate a title following these rules:
def power_saver_toggled(self, monitor): def power_saver_toggled(self, monitor):
self.banner.set_revealed(monitor.get_power_saver_enabled() and self.powersaver_warning_switch.get_active()) self.banner.set_revealed(monitor.get_power_saver_enabled() and self.powersaver_warning_switch.get_active())
def remote_switched(self, switch, state):
def local_instance_process():
sensitive_elements = [switch, self.tweaks_group, self.instance_page, self.send_button, self.attachment_button]
[element.set_sensitive(False) for element in sensitive_elements]
self.get_application().lookup_action('manage_models').set_enabled(False)
self.title_stack.set_visible_child_name('loading')
self.ollama_instance.remote = False
self.ollama_instance.start()
self.model_manager.update_local_list()
self.save_server_config()
[element.set_sensitive(True) for element in sensitive_elements]
self.get_application().lookup_action('manage_models').set_enabled(True)
self.title_stack.set_visible_child_name('model_selector' if len(self.model_manager.get_model_list()) > 0 else 'no_models')
if state:
options = {
_("Cancel"): {"callback": lambda *_: self.remote_connection_switch.set_active(False)},
_("Connect"): {"callback": lambda url, bearer: generic_actions.connect_remote(url, bearer), "appearance": "suggested"}
}
entries = [
{"text": self.ollama_instance.remote_url, "placeholder": _('Server URL')},
{"text": self.ollama_instance.bearer_token, "placeholder": _('Bearer Token (Optional)')}
]
dialog_widget.Entry(
_('Connect Remote Instance'),
_('Enter instance information to continue'),
list(options)[0],
options,
entries
)
elif self.ollama_instance.remote:
threading.Thread(target=local_instance_process).start()
def prepare_alpaca(self, local_port:int, remote_url:str, remote:bool, tweaks:dict, overrides:dict, bearer_token:str, idle_timer_delay:int, save:bool): def prepare_alpaca(self, local_port:int, remote_url:str, remote:bool, tweaks:dict, overrides:dict, bearer_token:str, idle_timer_delay:int, save:bool):
#Model Manager #Model Manager
self.model_manager = model_widget.model_manager_container() self.model_manager = model_widget.model_manager_container()
@ -891,19 +845,19 @@ Generate a title following these rules:
element.set_text(self.ollama_instance.overrides[element.get_name()]) element.set_text(self.ollama_instance.overrides[element.get_name()])
self.set_hide_on_close(self.background_switch.get_active()) self.set_hide_on_close(self.background_switch.get_active())
self.instance_idle_timer.set_value(self.ollama_instance.idle_timer_delay) self.remote_connection_entry.set_text(self.ollama_instance.remote_url)
self.remote_connection_switch.set_sensitive(self.remote_connection_entry.get_text())
self.remote_bearer_token_entry.set_text(self.ollama_instance.bearer_token)
self.remote_connection_switch.set_active(self.ollama_instance.remote) self.remote_connection_switch.set_active(self.ollama_instance.remote)
self.remote_connection_switch.get_activatable_widget().connect('state-set', self.remote_switched) self.instance_idle_timer.set_value(self.ollama_instance.idle_timer_delay)
#Save preferences #Save preferences
if save: if save:
self.save_server_config() self.save_server_config()
self.send_button.set_sensitive(True) self.send_button.set_sensitive(True)
self.attachment_button.set_sensitive(True) self.attachment_button.set_sensitive(True)
self.remote_connection_switch.set_sensitive(True)
self.tweaks_group.set_sensitive(True)
self.instance_page.set_sensitive(True)
self.get_application().lookup_action('manage_models').set_enabled(True) self.get_application().lookup_action('manage_models').set_enabled(True)
self.get_application().lookup_action('preferences').set_enabled(True)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
@ -911,9 +865,6 @@ Generate a title following these rules:
message_widget.window = self message_widget.window = self
chat_widget.window = self chat_widget.window = self
model_widget.window = self model_widget.window = self
dialog_widget.window = self
terminal_widget.window = self
generic_actions.window = self
connection_handler.window = self connection_handler.window = self
drop_target = Gtk.DropTarget.new(Gdk.FileList, Gdk.DragAction.COPY) drop_target = Gtk.DropTarget.new(Gdk.FileList, Gdk.DragAction.COPY)
@ -933,11 +884,11 @@ Generate a title following these rules:
universal_actions = { universal_actions = {
'new_chat': [lambda *_: self.chat_list_box.new_chat(), ['<primary>n']], 'new_chat': [lambda *_: self.chat_list_box.new_chat(), ['<primary>n']],
'clear': [lambda *i: dialog_widget.simple(_('Clear Chat?'), _('Are you sure you want to clear the chat?'), self.chat_list_box.get_current_chat().clear_chat, _('Clear')), ['<primary>e']], 'clear': [lambda *_: dialogs.clear_chat(self), ['<primary>e']],
'import_chat': [lambda *_: self.chat_list_box.import_chat(), ['<primary>i']], 'import_chat': [lambda *_: self.chat_list_box.import_chat(), ['<primary>i']],
'create_model_from_existing': [lambda *i: dialog_widget.simple_dropdown(_('Select Model'), _('This model will be used as the base for the new model'), lambda model: self.create_model(model, False), [self.convert_model_name(model, 0) for model in self.model_manager.get_model_list()])], 'create_model_from_existing': [lambda *_: dialogs.create_model_from_existing(self)],
'create_model_from_file': [lambda *i, file_filter=self.file_filter_gguf: dialog_widget.simple_file(file_filter, lambda file: self.create_model(file.get_path(), True))], 'create_model_from_file': [lambda *_: dialogs.create_model_from_file(self)],
'create_model_from_name': [lambda *i: dialog_widget.simple_entry(_('Pull Model'), _('Input the name of the model in this format\nname:tag'), lambda model: threading.Thread(target=self.model_manager.pull_model, kwargs={"model_name": model}).start(), {'placeholder': 'llama3.2:latest'})], 'create_model_from_name': [lambda *_: dialogs.create_model_from_name(self)],
'duplicate_chat': [self.chat_actions], 'duplicate_chat': [self.chat_actions],
'duplicate_current_chat': [self.current_chat_actions], 'duplicate_current_chat': [self.current_chat_actions],
'delete_chat': [self.chat_actions], 'delete_chat': [self.chat_actions],
@ -955,13 +906,12 @@ Generate a title following these rules:
self.get_application().create_action(action_name, data[0], data[1] if len(data) > 1 else None) self.get_application().create_action(action_name, data[0], data[1] if len(data) > 1 else None)
self.get_application().lookup_action('manage_models').set_enabled(False) self.get_application().lookup_action('manage_models').set_enabled(False)
self.remote_connection_switch.set_sensitive(False) self.get_application().lookup_action('preferences').set_enabled(False)
self.tweaks_group.set_sensitive(False)
self.instance_page.set_sensitive(False)
self.file_preview_remove_button.connect('clicked', lambda button : dialog_widget.simple(_('Remove Attachment?'), _("Are you sure you want to remove attachment?"), lambda button=button: self.remove_attached_file(button.get_name()), _('Remove'), 'destructive')) self.file_preview_remove_button.connect('clicked', lambda button : dialogs.remove_attached_file(self, button.get_name()))
self.attachment_button.connect("clicked", lambda button, file_filter=self.file_filter_attachments: dialog_widget.simple_file(file_filter, generic_actions.attach_file)) self.attachment_button.connect("clicked", lambda button, file_filter=self.file_filter_attachments: dialogs.attach_file(self, file_filter))
self.create_model_name.get_delegate().connect("insert-text", lambda *_: self.check_alphanumeric(*_, ['-', '.', '_'])) self.create_model_name.get_delegate().connect("insert-text", lambda *_: self.check_alphanumeric(*_, ['-', '.', '_']))
self.remote_connection_entry.connect("entry-activated", lambda entry : entry.set_css_classes([]))
self.set_focus(self.message_text_view) self.set_focus(self.message_text_view)
if os.path.exists(os.path.join(config_dir, "server.json")): if os.path.exists(os.path.join(config_dir, "server.json")):
try: try:

View File

@ -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">400</property> <property name="width-request">400</property>
<property name="height-request">600</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>
@ -97,18 +97,6 @@
</property> </property>
</object> </object>
</child> </child>
<child>
<object class="GtkStackPage">
<property name="name">no_models</property>
<property name="child">
<object class="GtkButton">
<property name="label" translatable="yes">Manage Models</property>
<property name="tooltip-text" translatable="yes">Manage Models</property>
<property name="action-name">app.manage_models</property>
</object>
</property>
</object>
</child>
</object> </object>
</child> </child>
<child type="end"> <child type="end">
@ -224,7 +212,6 @@
<signal name="paste-clipboard" handler="on_clipboard_paste"/> <signal name="paste-clipboard" handler="on_clipboard_paste"/>
<style> <style>
<class name="message_text_view"/> <class name="message_text_view"/>
<class name="undershoot-bottom"/>
</style> </style>
<property name="wrap-mode">word</property> <property name="wrap-mode">word</property>
<property name="top-margin">10</property> <property name="top-margin">10</property>
@ -304,9 +291,24 @@
<object class="AdwPreferencesGroup"> <object class="AdwPreferencesGroup">
<child> <child>
<object class="AdwSwitchRow" id="remote_connection_switch"> <object class="AdwSwitchRow" id="remote_connection_switch">
<signal name="notify::active" handler="change_remote_connection"/>
<property name="title" translatable="yes">Use Remote Connection to Ollama</property> <property name="title" translatable="yes">Use Remote Connection to Ollama</property>
</object> </object>
</child> </child>
<child>
<object class="AdwEntryRow" id="remote_connection_entry">
<signal name="apply" handler="change_remote_url"/>
<property name="title" translatable="yes">URL of Remote Instance</property>
<property name="show-apply-button">true</property>
</object>
</child>
<child>
<object class="AdwEntryRow" id="remote_bearer_token_entry">
<signal name="apply" handler="change_remote_bearer_token"/>
<property name="title" translatable="yes">Bearer Token (Optional)</property>
<property name="show-apply-button">true</property>
</object>
</child>
</object> </object>
</child> </child>
<child> <child>
@ -470,21 +472,6 @@
</child> </child>
</object> </object>
</child> </child>
<child>
<object class="AdwPreferencesGroup">
<child>
<object class="GtkLabel" id="ollama_information_label">
<property name="wrap">true</property>
<property name="use-markup">true</property>
<property name="label" translatable="yes">Integrated Ollama instance is not running</property>
<property name="justify">2</property>
<style>
<class name="dim-label"/>
</style>
</object>
</child>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
@ -665,41 +652,6 @@
</property> </property>
</object> </object>
</child> </child>
<child>
<object class="AdwNavigationPage">
<property name="title" translatable="yes">Model Details</property>
<property name="tag">model_information</property>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkButton" id="model_detail_create_button">
<signal name="clicked" handler="model_detail_create_button_clicked"/>
<property name="icon-name">edit-copy-symbolic</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="AdwStatusPage" id="model_detail_page">
<property name="icon-name">brain-augemnted-symbolic</property>
<property name="description">text</property>
<style>
<class name="compact"/>
</style>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</child>
<child> <child>
<object class="AdwNavigationPage"> <object class="AdwNavigationPage">
<property name="title" translatable="yes">Create Model</property> <property name="title" translatable="yes">Create Model</property>
@ -707,7 +659,14 @@
<property name="child"> <property name="child">
<object class="AdwToolbarView"> <object class="AdwToolbarView">
<child type="top"> <child type="top">
<object class="AdwHeaderBar"/> <object class="AdwHeaderBar">
<child type="start">
<object class="GtkButton">
<signal name="clicked" handler="link_button_handler"/>
<property name="icon-name">globe-symbolic</property>
</object>
</child>
</object>
</child> </child>
<property name="content"> <property name="content">
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
@ -782,9 +741,6 @@
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="margin-start">10</property> <property name="margin-start">10</property>
<property name="margin-end">10</property> <property name="margin-end">10</property>
<style>
<class name="undershoot-bottom"/>
</style>
<child> <child>
<object class="GtkTextView" id="create_model_modelfile"> <object class="GtkTextView" id="create_model_modelfile">
<style> <style>
@ -884,12 +840,14 @@
<child> <child>
<object class="GtkBox"> <object class="GtkBox">
<child> <child>
<object class="GtkLabel" id="file_preview_text_label"> <object class="GtkTextView" id="file_preview_text_view">
<property name="margin-top">12</property> <property name="margin-top">12</property>
<property name="margin-bottom">12</property> <property name="margin-bottom">12</property>
<property name="margin-start">12</property> <property name="margin-start">12</property>
<property name="margin-end">12</property> <property name="margin-end">12</property>
<property name="selectable">true</property> <property name="hexpand">true</property>
<property name="vexpand">true</property>
<property name="editable">false</property>
</object> </object>
</child> </child>
<child> <child>
@ -1123,16 +1081,10 @@
<child> <child>
<object class="GtkShortcutsGroup"> <object class="GtkShortcutsGroup">
<property name="title" translatable="yes">General</property> <property name="title" translatable="yes">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;ctrl&gt;Q</property>
<property name="title" translatable="yes">Close application</property>
</object>
</child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;ctrl&gt;W</property> <property name="accelerator">&lt;ctrl&gt;W</property>
<property name="title" translatable="yes">Delete current chat</property> <property name="title" translatable="yes">Close application</property>
</object> </object>
</child> </child>
<child> <child>