Rewrote a whole new dialog system cause I was bored
This commit is contained in:
parent
7f042a906d
commit
3156c70260
@ -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'
|
||||||
|
@ -10,6 +10,7 @@ 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, dialogs
|
||||||
|
from . import dialog_widget
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -178,7 +179,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 *_: 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 +202,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 +276,8 @@ 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,
|
||||||
|
@ -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
|
||||||
|
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_local():
|
||||||
|
window.remote_connection_switch.set_active(False)
|
||||||
|
|
||||||
|
def connect_remote(url:str, bearer:str):
|
||||||
|
window.remote_connection_entry.set_text(url)
|
||||||
|
window.remote_bearer_token_entry.set_text(bearer)
|
||||||
|
window.remote_connection_switch.set_active(True)
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -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 dialogs, 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__)
|
||||||
@ -371,10 +372,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 +666,34 @@ 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)
|
#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)')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
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 +738,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 +765,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 +774,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'):
|
||||||
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 +802,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)
|
||||||
|
|
||||||
@ -865,6 +905,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 +927,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.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],
|
||||||
@ -908,8 +951,8 @@ Generate a title following these rules:
|
|||||||
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.get_application().lookup_action('preferences').set_enabled(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.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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user