Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bc9f79f99 | ||
|
|
8bfa0830c1 | ||
|
|
ffefe2b141 | ||
|
|
c8679d6fa5 | ||
|
|
8fd8d920e6 | ||
|
|
202da99fa7 | ||
|
|
f084d6e447 | ||
|
|
9ab0084e18 | ||
|
|
e42eec3e31 | ||
|
|
e553215bf1 | ||
|
|
50eedc5326 | ||
|
|
f6975f1b6d | ||
|
|
35dac564b0 | ||
|
|
7e1a3713b5 | ||
|
|
a99d1f11c2 | ||
|
|
19523ba37a | ||
|
|
9bec816965 | ||
|
|
cbb7605851 | ||
|
|
c856b49268 | ||
|
|
fb04e4cb4f | ||
|
|
00527a6271 | ||
|
|
462657b7bb | ||
|
|
12fb88f3fd | ||
|
|
22af279548 | ||
|
|
00fc442348 | ||
|
|
32df119c60 | ||
|
|
ef8ec59977 | ||
|
|
3156c70260 |
@@ -91,6 +91,7 @@ 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!
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,14 @@
|
|||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://jeffser.com/images/alpaca/screenie4.png</image>
|
<image>https://jeffser.com/images/alpaca/screenie4.png</image>
|
||||||
<caption>A conversation involving a YouTube video transcript</caption>
|
<caption>A Python script running inside integrated terminal</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://jeffser.com/images/alpaca/screenie5.png</image>
|
<image>https://jeffser.com/images/alpaca/screenie5.png</image>
|
||||||
|
<caption>A conversation involving a YouTube video transcript</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://jeffser.com/images/alpaca/screenie6.png</image>
|
||||||
<caption>Multiple models being downloaded</caption>
|
<caption>Multiple models being downloaded</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
@@ -78,6 +82,22 @@
|
|||||||
<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.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">
|
<release version="2.5.1" date="2024-10-09">
|
||||||
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.5.1</url>
|
<url type="details">https://github.com/Jeffser/Alpaca/releases/tag/2.5.1</url>
|
||||||
<description>
|
<description>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
project('Alpaca', 'c',
|
project('Alpaca', 'c',
|
||||||
version: '2.5.1',
|
version: '2.6.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', ],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ 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
|
||||||
1332
po/alpaca.pot
1332
po/alpaca.pot
File diff suppressed because it is too large
Load Diff
1338
po/nb_NO.po
1338
po/nb_NO.po
File diff suppressed because it is too large
Load Diff
1349
po/pt_BR.po
1349
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
1364
po/zh_Hans.po
1364
po/zh_Hans.po
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ def log_output(pipe):
|
|||||||
with pipe:
|
with pipe:
|
||||||
try:
|
try:
|
||||||
for line in iter(pipe.readline, ''):
|
for line in iter(pipe.readline, ''):
|
||||||
print(line, end='')
|
#print(line, end='')
|
||||||
f.write(line)
|
f.write(line)
|
||||||
f.flush()
|
f.flush()
|
||||||
except:
|
except:
|
||||||
@@ -118,8 +118,6 @@ class instance():
|
|||||||
self.start_timer()
|
self.start_timer()
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ 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)
|
||||||
@@ -102,7 +104,9 @@ 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
|
||||||
self.clear_chat()
|
if len(list(self.container)) > 0:
|
||||||
|
self.clear_chat()
|
||||||
|
return
|
||||||
button_container = Gtk.Box(
|
button_container = Gtk.Box(
|
||||||
orientation=1,
|
orientation=1,
|
||||||
spacing=10,
|
spacing=10,
|
||||||
@@ -333,6 +337,8 @@ 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])
|
||||||
|
|||||||
173
src/custom_widgets/dialog_widget.py
Normal file
173
src/custom_widgets/dialog_widget.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#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)
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ 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 dialogs
|
from . import dialog_widget, terminal_widget
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -180,7 +180,13 @@ 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()
|
||||||
dialogs.run_script(window, self.buffer.get_text(start, end, False), language_name)
|
dialog_widget.simple(
|
||||||
|
_('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'
|
||||||
@@ -288,6 +294,7 @@ 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):
|
||||||
@@ -569,7 +576,7 @@ 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[pos:]):
|
if pos < len(self.text):
|
||||||
normal_text = self.text[pos:]
|
normal_text = self.text[pos:]
|
||||||
if normal_text.strip():
|
if normal_text.strip():
|
||||||
parts.append({"type": "normal", "text": normal_text.strip()})
|
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ 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, glob
|
||||||
from ..internal import config_dir, data_dir, cache_dir, source_dir
|
from ..internal import config_dir, data_dir, cache_dir, source_dir
|
||||||
from .. import available_models_descriptions, dialogs
|
from .. import available_models_descriptions
|
||||||
|
from . import dialog_widget
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -178,7 +179,13 @@ 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 *_: dialogs.stop_pull_model(window, self))
|
stop_button.connect('clicked', lambda *i: dialog_widget.simple(
|
||||||
|
_('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,
|
||||||
@@ -201,6 +208,11 @@ class pulling_model(Gtk.ListBoxRow):
|
|||||||
self.error = None
|
self.error = None
|
||||||
self.digests = []
|
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:
|
if 'digest' in data and data['digest'] not in self.digests:
|
||||||
self.digests.append(data['digest'].replace(':', '-'))
|
self.digests.append(data['digest'].replace(':', '-'))
|
||||||
@@ -270,7 +282,14 @@ 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,
|
||||||
@@ -488,6 +507,7 @@ 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:
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ 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'
|
||||||
@@ -42,3 +48,44 @@ 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
474
src/dialogs.py
@@ -1,474 +0,0 @@
|
|||||||
# 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)
|
|
||||||
)
|
|
||||||
72
src/generic_actions.py
Normal file
72
src/generic_actions.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#generic_actions.py
|
||||||
|
"""
|
||||||
|
Working on organizing the code
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, requests
|
||||||
|
from pytube import YouTube
|
||||||
|
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_url: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))
|
||||||
|
|
||||||
|
yt = YouTube(video_url)
|
||||||
|
text = "{}\n{}\n{}\n\n".format(yt.title, yt.author, yt.watch_url)
|
||||||
|
|
||||||
|
for event in yt.captions[caption_name.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} ({caption_name.split(" (")[0]})')
|
||||||
|
with open(file_path, 'w+', encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
window.attach_file(file_path, 'youtube')
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -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,7 +51,8 @@ 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)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
.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;
|
||||||
|
|||||||
166
src/window.py
166
src/window.py
@@ -24,6 +24,7 @@ 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')
|
||||||
@@ -31,8 +32,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 dialogs, connection_handler
|
from . import connection_handler, generic_actions
|
||||||
from .custom_widgets import message_widget, chat_widget, model_widget, terminal_widget
|
from .custom_widgets import message_widget, chat_widget, model_widget, terminal_widget, dialog_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__)
|
||||||
@@ -54,6 +55,7 @@ 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()
|
||||||
@@ -111,8 +113,6 @@ 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()
|
||||||
|
|
||||||
@@ -211,45 +211,6 @@ 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")
|
||||||
@@ -371,10 +332,6 @@ 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)
|
||||||
|
|
||||||
def run_terminal(self, script:list):
|
|
||||||
self.terminal_scroller.set_child(terminal_widget.terminal(script))
|
|
||||||
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:
|
||||||
if mode == 0:
|
if mode == 0:
|
||||||
@@ -669,7 +626,16 @@ 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:
|
||||||
dialogs.reconnect_remote(self)
|
options = {
|
||||||
|
_("Close Alpaca"): {"callback": lambda *_: self.get_application().quit(), "appearance": "destructive"},
|
||||||
|
_("Use Local Instance"): {"callback": lambda *_: window.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)
|
||||||
@@ -714,6 +680,8 @@ 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}")
|
||||||
@@ -739,7 +707,6 @@ 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)
|
||||||
@@ -749,11 +716,23 @@ 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'):
|
||||||
dialogs.delete_chat(self, chat_name)
|
dialog_widget.simple(
|
||||||
|
_('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'):
|
||||||
dialogs.rename_chat(self, chat_name)
|
dialog_widget.simple_entry(
|
||||||
|
_('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)
|
||||||
|
|
||||||
@@ -777,12 +756,27 @@ Generate a title following these rules:
|
|||||||
)
|
)
|
||||||
if youtube_regex.match(text):
|
if youtube_regex.match(text):
|
||||||
try:
|
try:
|
||||||
dialogs.youtube_caption(self, text)
|
yt = YouTube(text)
|
||||||
|
captions = yt.captions
|
||||||
|
if len(captions) == 0:
|
||||||
|
self.show_toast(_("This video does not have any transcriptions"), self.main_overlay)
|
||||||
|
return
|
||||||
|
video_title = yt.title
|
||||||
|
dialog_widget.simple_dropdown(
|
||||||
|
_('Attach YouTube Video?'),
|
||||||
|
_('{}\n\nPlease select a transcript to include').format(video_title),
|
||||||
|
lambda caption_name, video_url=text: generic_actions.attach_youtube(video_url, caption_name),
|
||||||
|
["{} ({})".format(caption.name.title(), caption.code) for caption in captions]
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
self.show_toast(_("This video is not available"), self.main_overlay)
|
self.show_toast(_("This video is not available"), self.main_overlay)
|
||||||
elif url_regex.match(text):
|
elif url_regex.match(text):
|
||||||
dialogs.attach_website(self, text)
|
dialog_widget.simple(
|
||||||
|
_('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)
|
||||||
|
|
||||||
@@ -820,6 +814,42 @@ 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 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()
|
||||||
@@ -845,19 +875,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.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.instance_idle_timer.set_value(self.ollama_instance.idle_timer_delay)
|
self.instance_idle_timer.set_value(self.ollama_instance.idle_timer_delay)
|
||||||
|
self.remote_connection_switch.set_active(self.ollama_instance.remote)
|
||||||
|
self.remote_connection_switch.get_activatable_widget().connect('state-set', self.remote_switched)
|
||||||
|
|
||||||
#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)
|
||||||
@@ -865,6 +895,9 @@ 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)
|
||||||
@@ -884,11 +917,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 *_: dialogs.clear_chat(self), ['<primary>e']],
|
'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']],
|
||||||
'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 *_: dialogs.create_model_from_existing(self)],
|
'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_file': [lambda *_: dialogs.create_model_from_file(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_name': [lambda *_: dialogs.create_model_from_name(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'})],
|
||||||
'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],
|
||||||
@@ -906,12 +939,13 @@ 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.get_application().lookup_action('preferences').set_enabled(False)
|
self.remote_connection_switch.set_sensitive(False)
|
||||||
|
self.tweaks_group.set_sensitive(False)
|
||||||
|
self.instance_page.set_sensitive(False)
|
||||||
|
|
||||||
self.file_preview_remove_button.connect('clicked', lambda button : dialogs.remove_attached_file(self, button.get_name()))
|
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.attachment_button.connect("clicked", lambda button, file_filter=self.file_filter_attachments: dialogs.attach_file(self, file_filter))
|
self.attachment_button.connect("clicked", lambda button, file_filter=self.file_filter_attachments: dialog_widget.simple_file(file_filter, generic_actions.attach_file))
|
||||||
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:
|
||||||
|
|||||||
@@ -291,24 +291,9 @@
|
|||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user