Rewrite of Chat / Message / Model selector systems
This commit is contained in:
parent
8026550f7a
commit
4545f5a1b2
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id" : "com.jeffser.Alpaca",
|
"id" : "com.jeffser.Alpaca",
|
||||||
"runtime" : "org.gnome.Platform",
|
"runtime" : "org.gnome.Platform",
|
||||||
"runtime-version" : "46",
|
"runtime-version" : "master",
|
||||||
"sdk" : "org.gnome.Sdk",
|
"sdk" : "org.gnome.Sdk",
|
||||||
"command" : "alpaca",
|
"command" : "alpaca",
|
||||||
"finish-args" : [
|
"finish-args" : [
|
||||||
|
390
src/custom_widgets/chat_widget.py
Normal file
390
src/custom_widgets/chat_widget.py
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
#chat_widget.py
|
||||||
|
"""
|
||||||
|
Handles the chat widget (testing)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '4.0')
|
||||||
|
gi.require_version('GtkSource', '5')
|
||||||
|
from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk
|
||||||
|
import logging, os, datetime, re, shutil, random, tempfile, tarfile, json
|
||||||
|
from ..internal import config_dir, data_dir, cache_dir, source_dir
|
||||||
|
from .message_widget import message
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
window = None
|
||||||
|
|
||||||
|
possible_prompts = [
|
||||||
|
"What can you do?",
|
||||||
|
"Give me a pancake recipe",
|
||||||
|
"Why is the sky blue?",
|
||||||
|
"Can you tell me a joke?",
|
||||||
|
"Give me a healthy breakfast recipe",
|
||||||
|
"How to make a pizza",
|
||||||
|
"Can you write a poem?",
|
||||||
|
"Can you write a story?",
|
||||||
|
"What is GNU-Linux?",
|
||||||
|
"Which is the best Linux distro?",
|
||||||
|
"Why is Pluto not a planet?",
|
||||||
|
"What is a black-hole?",
|
||||||
|
"Tell me how to stay fit",
|
||||||
|
"Write a conversation between sun and Earth",
|
||||||
|
"Why is the grass green?",
|
||||||
|
"Write an Haïku about AI",
|
||||||
|
"What is the meaning of life?",
|
||||||
|
"Explain quantum physics in simple terms",
|
||||||
|
"Explain the theory of relativity",
|
||||||
|
"Explain how photosynthesis works",
|
||||||
|
"Recommend a film about nature",
|
||||||
|
"What is nostalgia?"
|
||||||
|
]
|
||||||
|
|
||||||
|
class chat(Gtk.ScrolledWindow):
|
||||||
|
__gtype_name__ = 'AlpacaChat'
|
||||||
|
|
||||||
|
def __init__(self, name:str):
|
||||||
|
self.container = Gtk.Box(
|
||||||
|
orientation=1,
|
||||||
|
hexpand=True,
|
||||||
|
vexpand=True,
|
||||||
|
spacing=12,
|
||||||
|
margin_top=12,
|
||||||
|
margin_bottom=12,
|
||||||
|
margin_start=12,
|
||||||
|
margin_end=12
|
||||||
|
)
|
||||||
|
self.clamp = Adw.Clamp(
|
||||||
|
maximum_size=1000,
|
||||||
|
tightening_threshold=800,
|
||||||
|
child=self.container
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
child=self.clamp,
|
||||||
|
propagate_natural_height=True,
|
||||||
|
kinetic_scrolling=True,
|
||||||
|
vexpand=True,
|
||||||
|
hexpand=True,
|
||||||
|
css_classes=["undershoot-bottom"],
|
||||||
|
name=name
|
||||||
|
)
|
||||||
|
self.messages = {}
|
||||||
|
self.welcome_screen = None
|
||||||
|
self.busy = False
|
||||||
|
|
||||||
|
def stop_message(self):
|
||||||
|
self.busy = False
|
||||||
|
window.switch_send_stop_button(True)
|
||||||
|
|
||||||
|
def clear_chat(self):
|
||||||
|
if self.busy:
|
||||||
|
self.stop_message()
|
||||||
|
self.message = {}
|
||||||
|
self.stop_message()
|
||||||
|
for widget in list(self.container):
|
||||||
|
self.container.remove(widget)
|
||||||
|
|
||||||
|
def add_message(self, message_id:str, model:str=None):
|
||||||
|
msg = message(message_id, model)
|
||||||
|
self.messages[message_id] = msg
|
||||||
|
self.container.append(msg)
|
||||||
|
|
||||||
|
def send_sample_prompt(self, prompt):
|
||||||
|
buffer = window.message_text_view.get_buffer()
|
||||||
|
buffer.delete(buffer.get_start_iter(), buffer.get_end_iter())
|
||||||
|
buffer.insert(buffer.get_start_iter(), prompt, len(prompt.encode('utf-8')))
|
||||||
|
window.send_message()
|
||||||
|
|
||||||
|
def show_welcome_screen(self, show_prompts:bool):
|
||||||
|
if self.welcome_screen:
|
||||||
|
self.container.remove(self.welcome_screen)
|
||||||
|
self.welcome_screen = None
|
||||||
|
self.clear_chat()
|
||||||
|
button_container = Gtk.Box(
|
||||||
|
orientation=1,
|
||||||
|
spacing=10,
|
||||||
|
halign=3
|
||||||
|
)
|
||||||
|
if show_prompts:
|
||||||
|
for prompt in random.sample(possible_prompts, 3):
|
||||||
|
prompt_button = Gtk.Button(
|
||||||
|
label=prompt,
|
||||||
|
tooltip_text=_("Send prompt: '{}'").format(prompt)
|
||||||
|
)
|
||||||
|
prompt_button.connect('clicked', lambda *_, prompt=prompt : self.send_sample_prompt(prompt))
|
||||||
|
button_container.append(prompt_button)
|
||||||
|
else:
|
||||||
|
button = Gtk.Button(
|
||||||
|
label=_("Open Model Manager"),
|
||||||
|
tooltip_text=_("Open Model Manager"),
|
||||||
|
css_classes=["suggested-action", "pill"]
|
||||||
|
)
|
||||||
|
button.connect('clicked', lambda *_ : window.manage_models_dialog.present(window))
|
||||||
|
button_container.append(button)
|
||||||
|
|
||||||
|
self.welcome_screen = Adw.StatusPage(
|
||||||
|
icon_name="com.jeffser.Alpaca",
|
||||||
|
title="Alpaca",
|
||||||
|
description=_("Try one of these prompts") if show_prompts else _("It looks like you don't have any models downloaded yet. Download models to get started!"),
|
||||||
|
child=button_container,
|
||||||
|
vexpand=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.container.append(self.welcome_screen)
|
||||||
|
|
||||||
|
def load_chat_messages(self, messages:dict):
|
||||||
|
if len(messages.keys()) > 0:
|
||||||
|
if self.welcome_screen:
|
||||||
|
self.container.remove(self.welcome_screen)
|
||||||
|
self.welcome_screen = None
|
||||||
|
for message_id, message_data in messages.items():
|
||||||
|
if message_data['content']:
|
||||||
|
self.add_message(message_id, message_data['model'] if message_data['role'] == 'assistant' else None)
|
||||||
|
message_element = self.messages[message_id]
|
||||||
|
if 'images' in message_data:
|
||||||
|
images=[]
|
||||||
|
for image in message_data['images']:
|
||||||
|
images.append(os.path.join(data_dir, "chats", self.get_name(), message_id, image))
|
||||||
|
message_element.add_images(images)
|
||||||
|
if 'files' in message_data:
|
||||||
|
files={}
|
||||||
|
for file_name, file_type in message_data['files'].items():
|
||||||
|
files[os.path.join(data_dir, "chats", self.get_name(), message_id, file_name)] = file_type
|
||||||
|
message_element.add_attachments(files)
|
||||||
|
message_element.set_text(message_data['content'])
|
||||||
|
message_element.add_footer(datetime.datetime.strptime(message_data['date'] + (":00" if message_data['date'].count(":") == 1 else ""), '%Y/%m/%d %H:%M:%S'))
|
||||||
|
else:
|
||||||
|
self.show_welcome_screen(len(window.model_selector.get_model_list()) > 0)
|
||||||
|
|
||||||
|
def messages_to_dict(self) -> dict:
|
||||||
|
messages_dict = {}
|
||||||
|
for message_id, message_element in self.messages.items():
|
||||||
|
if message_element.text and message_element.dt:
|
||||||
|
messages_dict[message_id] = {
|
||||||
|
'role': 'assistant' if message_element.bot else 'user',
|
||||||
|
'model': message_element.model,
|
||||||
|
'date': message_element.dt.strftime("%Y/%m/%d %H:%M:%S"),
|
||||||
|
'content': message_element.text
|
||||||
|
}
|
||||||
|
|
||||||
|
if message_element.image_c:
|
||||||
|
images = []
|
||||||
|
for file in message_element.image_c.files:
|
||||||
|
images.append(file.image_name)
|
||||||
|
messages_dict[message_id]['images'] = images
|
||||||
|
|
||||||
|
if message_element.attachment_c:
|
||||||
|
files = {}
|
||||||
|
for file in message_element.attachment_c.files:
|
||||||
|
files[file.file_name] = file.file_type
|
||||||
|
messages_dict[message_id]['files'] = files
|
||||||
|
return messages_dict
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class chat_tab(Gtk.ListBoxRow):
|
||||||
|
__gtype_name__ = 'AlpacaChatTab'
|
||||||
|
|
||||||
|
def __init__(self, chat_window:chat):
|
||||||
|
self.chat_window=chat_window
|
||||||
|
self.label = Gtk.Label(
|
||||||
|
label=self.chat_window.get_name(),
|
||||||
|
tooltip_text=self.chat_window.get_name(),
|
||||||
|
hexpand=True,
|
||||||
|
halign=0,
|
||||||
|
wrap=True,
|
||||||
|
ellipsize=3,
|
||||||
|
wrap_mode=2,
|
||||||
|
xalign=0
|
||||||
|
)
|
||||||
|
super().__init__(
|
||||||
|
css_classes = ["chat_row"],
|
||||||
|
height_request = 45,
|
||||||
|
child = self.label
|
||||||
|
)
|
||||||
|
|
||||||
|
self.gesture = Gtk.GestureClick(button=3)
|
||||||
|
self.gesture.connect("released", window.chat_click_handler)
|
||||||
|
self.add_controller(self.gesture)
|
||||||
|
|
||||||
|
class chat_list(Gtk.ListBox):
|
||||||
|
__gtype_name__ = 'AlpacaChatList'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
selection_mode=1,
|
||||||
|
css_classes=["navigation-sidebar"]
|
||||||
|
)
|
||||||
|
self.connect("row-selected", lambda listbox, row: self.chat_changed(row))
|
||||||
|
self.tab_list = []
|
||||||
|
|
||||||
|
def get_tab_by_name(self, chat_name:str) -> chat_tab:
|
||||||
|
for tab in self.tab_list:
|
||||||
|
if tab.chat_window.get_name() == chat_name:
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def get_chat_by_name(self, chat_name:str) -> chat:
|
||||||
|
tab = self.get_tab_by_name(chat_name)
|
||||||
|
if tab:
|
||||||
|
return tab.chat_window
|
||||||
|
|
||||||
|
def get_current_chat(self) -> chat:
|
||||||
|
row = self.get_selected_row()
|
||||||
|
if row:
|
||||||
|
return self.get_selected_row().chat_window
|
||||||
|
|
||||||
|
def send_tab_to_top(self, tab:chat_tab):
|
||||||
|
self.unselect_all()
|
||||||
|
self.tab_list.remove(tab)
|
||||||
|
self.tab_list.insert(0, tab)
|
||||||
|
self.remove(tab)
|
||||||
|
self.prepend(tab)
|
||||||
|
self.select_row(tab)
|
||||||
|
|
||||||
|
def append_chat(self, chat_name:str) -> chat:
|
||||||
|
chat_name = window.generate_numbered_name(chat_name, [tab.chat_window.get_name() for tab in self.tab_list])
|
||||||
|
chat_window = chat(chat_name)
|
||||||
|
tab = chat_tab(chat_window)
|
||||||
|
self.append(tab)
|
||||||
|
self.tab_list.append(tab)
|
||||||
|
window.chat_stack.add_child(chat_window)
|
||||||
|
return chat_window
|
||||||
|
|
||||||
|
def prepend_chat(self, chat_name:str) -> chat:
|
||||||
|
chat_name = window.generate_numbered_name(chat_name, [tab.chat_window.get_name() for tab in self.tab_list])
|
||||||
|
chat_window = chat(chat_name)
|
||||||
|
tab = chat_tab(chat_window)
|
||||||
|
self.prepend(tab)
|
||||||
|
self.tab_list.insert(0, tab)
|
||||||
|
chat_window.show_welcome_screen(len(window.model_selector.get_model_list()) > 0)
|
||||||
|
window.chat_stack.add_child(chat_window)
|
||||||
|
window.chat_list_box.select_row(tab)
|
||||||
|
return chat_window
|
||||||
|
|
||||||
|
def new_chat(self):
|
||||||
|
window.save_history(self.prepend_chat(_("New Chat")))
|
||||||
|
|
||||||
|
def delete_chat(self, chat_name:str):
|
||||||
|
chat_tab = None
|
||||||
|
for c in self.tab_list:
|
||||||
|
if c.chat_window.get_name() == chat_name:
|
||||||
|
chat_tab = c
|
||||||
|
if chat_tab:
|
||||||
|
chat_tab.chat_window.stop_message()
|
||||||
|
window.chat_stack.remove(chat_tab.chat_window)
|
||||||
|
self.tab_list.remove(chat_tab)
|
||||||
|
self.remove(chat_tab)
|
||||||
|
if os.path.exists(os.path.join(data_dir, "chats", chat_name)):
|
||||||
|
shutil.rmtree(os.path.join(data_dir, "chats", chat_name))
|
||||||
|
if len(self.tab_list) == 0:
|
||||||
|
self.new_chat()
|
||||||
|
if not self.get_current_chat() or self.get_current_chat() == chat_tab.chat_window:
|
||||||
|
self.select_row(self.get_row_at_index(0))
|
||||||
|
window.save_history()
|
||||||
|
|
||||||
|
def rename_chat(self, old_chat_name:str, new_chat_name:str):
|
||||||
|
tab = self.get_tab_by_name(old_chat_name)
|
||||||
|
if tab:
|
||||||
|
new_chat_name = window.generate_numbered_name(new_chat_name, [tab.chat_window.get_name() for tab in self.tab_list])
|
||||||
|
tab.get_child().set_label(new_chat_name)
|
||||||
|
tab.get_child().set_tooltip_text(new_chat_name)
|
||||||
|
tab.chat_window.set_name(new_chat_name)
|
||||||
|
|
||||||
|
def duplicate_chat(self, chat_name:str):
|
||||||
|
new_chat_name = window.generate_numbered_name(_("Copy of {}").format(chat_name), [tab.chat_window.get_name() for tab in self.tab_list])
|
||||||
|
try:
|
||||||
|
shutil.copytree(os.path.join(data_dir, "chats", chat_name), os.path.join(data_dir, "chats", new_chat_name))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
self.prepend_chat(new_chat_name)
|
||||||
|
self.get_tab_by_name(new_chat_name).chat_window.load_chat_messages(self.get_tab_by_name(chat_name).chat_window.messages_to_dict())
|
||||||
|
|
||||||
|
def on_replace_contents(self, file, result):
|
||||||
|
file.replace_contents_finish(result)
|
||||||
|
window.show_toast(_("Chat exported successfully"), window.main_overlay)
|
||||||
|
|
||||||
|
def on_export_chat(self, file_dialog, result, chat_name):
|
||||||
|
file = file_dialog.save_finish(result)
|
||||||
|
if not file:
|
||||||
|
return
|
||||||
|
json_data = json.dumps({chat_name: self.get_chat_by_name(chat_name).messages_to_dict()}, indent=4).encode("UTF-8")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
json_path = os.path.join(temp_dir, "data.json")
|
||||||
|
with open(json_path, "wb") as json_file:
|
||||||
|
json_file.write(json_data)
|
||||||
|
|
||||||
|
tar_path = os.path.join(temp_dir, chat_name)
|
||||||
|
with tarfile.open(tar_path, "w") as tar:
|
||||||
|
tar.add(json_path, arcname="data.json")
|
||||||
|
directory = os.path.join(data_dir, "chats", chat_name)
|
||||||
|
if os.path.exists(directory) and os.path.isdir(directory):
|
||||||
|
tar.add(directory, arcname=os.path.basename(directory))
|
||||||
|
|
||||||
|
with open(tar_path, "rb") as tar:
|
||||||
|
tar_content = tar.read()
|
||||||
|
|
||||||
|
file.replace_contents_async(
|
||||||
|
tar_content,
|
||||||
|
etag=None,
|
||||||
|
make_backup=False,
|
||||||
|
flags=Gio.FileCreateFlags.NONE,
|
||||||
|
cancellable=None,
|
||||||
|
callback=self.on_replace_contents
|
||||||
|
)
|
||||||
|
|
||||||
|
def export_chat(self, chat_name:str):
|
||||||
|
logger.info("Exporting chat")
|
||||||
|
file_dialog = Gtk.FileDialog(initial_name=f"{chat_name}.tar")
|
||||||
|
file_dialog.save(parent=window, cancellable=None, callback=lambda file_dialog, result, chat_name=chat_name: self.on_export_chat(file_dialog, result, chat_name))
|
||||||
|
|
||||||
|
def on_chat_imported(self, file_dialog, result):
|
||||||
|
file = file_dialog.open_finish(result)
|
||||||
|
if not file:
|
||||||
|
return
|
||||||
|
stream = file.read(None)
|
||||||
|
data_stream = Gio.DataInputStream.new(stream)
|
||||||
|
tar_content = data_stream.read_bytes(1024 * 1024, None)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
tar_filename = os.path.join(temp_dir, "imported_chat.tar")
|
||||||
|
|
||||||
|
with open(tar_filename, "wb") as tar_file:
|
||||||
|
tar_file.write(tar_content.get_data())
|
||||||
|
|
||||||
|
with tarfile.open(tar_filename, "r") as tar:
|
||||||
|
tar.extractall(path=temp_dir)
|
||||||
|
chat_name = None
|
||||||
|
chat_content = None
|
||||||
|
for member in tar.getmembers():
|
||||||
|
if member.name == "data.json":
|
||||||
|
json_filepath = os.path.join(temp_dir, member.name)
|
||||||
|
with open(json_filepath, "r", encoding="utf-8") as json_file:
|
||||||
|
data = json.load(json_file)
|
||||||
|
for chat_name, chat_content in data.items():
|
||||||
|
new_chat_name = window.generate_numbered_name(chat_name, [tab.chat_window.get_name() for tab in self.tab_list])
|
||||||
|
src_path = os.path.join(temp_dir, chat_name)
|
||||||
|
dest_path = os.path.join(data_dir, "chats", new_chat_name)
|
||||||
|
if os.path.exists(src_path) and os.path.isdir(src_path) and not os.path.exists(dest_path):
|
||||||
|
shutil.copytree(src_path, dest_path)
|
||||||
|
|
||||||
|
self.prepend_chat(new_chat_name)
|
||||||
|
self.get_chat_by_name(new_chat_name).load_chat_messages(chat_content['messages'])
|
||||||
|
window.show_toast(_("Chat imported successfully"), window.main_overlay)
|
||||||
|
|
||||||
|
def import_chat(self):
|
||||||
|
logger.info("Importing chat")
|
||||||
|
file_dialog = Gtk.FileDialog(default_filter=window.file_filter_tar)
|
||||||
|
file_dialog.open(window, None, self.on_chat_imported)
|
||||||
|
|
||||||
|
def chat_changed(self, row):
|
||||||
|
if row:
|
||||||
|
current_tab_i = next((i for i, t in enumerate(self.tab_list) if t.chat_window == window.chat_stack.get_visible_child()), -1)
|
||||||
|
if self.tab_list.index(row) != current_tab_i:
|
||||||
|
window.chat_stack.set_transition_type(4 if self.tab_list.index(row) > current_tab_i else 5)
|
||||||
|
window.chat_stack.set_visible_child(row.chat_window)
|
||||||
|
window.switch_send_stop_button(not row.chat_window.busy)
|
||||||
|
if len(row.chat_window.messages) > 0:
|
||||||
|
last_model_used = row.chat_window.messages[list(row.chat_window.messages)[-1]].model
|
||||||
|
window.model_selector.change_model(last_model_used)
|
||||||
|
|
531
src/custom_widgets/message_widget.py
Normal file
531
src/custom_widgets/message_widget.py
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
#message_widget.py
|
||||||
|
"""
|
||||||
|
Handles the message widget (testing)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '4.0')
|
||||||
|
gi.require_version('GtkSource', '5')
|
||||||
|
from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk
|
||||||
|
import logging, os, datetime, re, shutil, threading
|
||||||
|
from ..internal import config_dir, data_dir, cache_dir, source_dir
|
||||||
|
from .table_widget import TableWidget
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
window = None
|
||||||
|
|
||||||
|
class edit_text_block(Gtk.TextView):
|
||||||
|
__gtype_name__ = 'AlpacaEditTextBlock'
|
||||||
|
|
||||||
|
def __init__(self, text:str):
|
||||||
|
super().__init__(
|
||||||
|
hexpand=True,
|
||||||
|
halign=0,
|
||||||
|
margin_top=5,
|
||||||
|
margin_bottom=5,
|
||||||
|
margin_start=5,
|
||||||
|
margin_end=5,
|
||||||
|
css_classes=["view", "editing_message_textview"]
|
||||||
|
)
|
||||||
|
self.get_buffer().insert(self.get_buffer().get_start_iter(), text, len(text.encode('utf-8')))
|
||||||
|
enter_key_controller = Gtk.EventControllerKey.new()
|
||||||
|
enter_key_controller.connect("key-pressed", lambda controller, keyval, keycode, state: self.edit_message() if keyval==Gdk.KEY_Return and not (state & Gdk.ModifierType.SHIFT_MASK) else None)
|
||||||
|
self.add_controller(enter_key_controller)
|
||||||
|
|
||||||
|
def edit_message(self):
|
||||||
|
self.get_parent().get_parent().action_buttons.set_visible(True)
|
||||||
|
self.get_parent().get_parent().set_text(self.get_buffer().get_text(self.get_buffer().get_start_iter(), self.get_buffer().get_end_iter(), False))
|
||||||
|
self.get_parent().get_parent().add_footer(self.get_parent().get_parent().dt)
|
||||||
|
window.save_history(self.get_parent().get_parent().get_parent().get_parent().get_parent().get_parent())
|
||||||
|
self.get_parent().remove(self)
|
||||||
|
window.show_toast(_("Message edited successfully"), window.main_overlay)
|
||||||
|
return True
|
||||||
|
|
||||||
|
class text_block(Gtk.Label):
|
||||||
|
__gtype_name__ = 'AlpacaTextBlock'
|
||||||
|
|
||||||
|
def __init__(self, bot:bool):
|
||||||
|
super().__init__(
|
||||||
|
hexpand=True,
|
||||||
|
halign=0,
|
||||||
|
wrap=True,
|
||||||
|
wrap_mode=0,
|
||||||
|
xalign=0,
|
||||||
|
margin_top=5,
|
||||||
|
margin_start=5,
|
||||||
|
margin_end=5,
|
||||||
|
focusable=True,
|
||||||
|
selectable=True
|
||||||
|
)
|
||||||
|
self.update_property([4, 7], [_("Response message") if bot else _("User message"), False])
|
||||||
|
|
||||||
|
def insert_at_end(self, text:str, markdown:bool):
|
||||||
|
if markdown:
|
||||||
|
self.set_markup(self.get_text() + text)
|
||||||
|
else:
|
||||||
|
self.set_text(self.get_text() + text)
|
||||||
|
self.update_property([1], [self.get_text()])
|
||||||
|
|
||||||
|
def clear_text(self):
|
||||||
|
self.buffer.delete(self.textbuffer.get_start_iter(), self.textbuffer.get_end_iter())
|
||||||
|
self.update_property([1], [""])
|
||||||
|
|
||||||
|
class code_block(Gtk.Box):
|
||||||
|
__gtype_name__ = 'AlpacaCodeBlock'
|
||||||
|
|
||||||
|
def __init__(self, text:str, language_name:str=None):
|
||||||
|
super().__init__(
|
||||||
|
css_classes=["card", "code_block"],
|
||||||
|
orientation=1,
|
||||||
|
overflow=1,
|
||||||
|
margin_start=5,
|
||||||
|
margin_end=5
|
||||||
|
)
|
||||||
|
|
||||||
|
self.language = None
|
||||||
|
if language_name:
|
||||||
|
self.language = GtkSource.LanguageManager.get_default().get_language(language_name)
|
||||||
|
if self.language:
|
||||||
|
self.buffer = GtkSource.Buffer.new_with_language(self.language)
|
||||||
|
else:
|
||||||
|
self.buffer = GtkSource.Buffer()
|
||||||
|
self.buffer.set_style_scheme(GtkSource.StyleSchemeManager.get_default().get_scheme('Adwaita-dark'))
|
||||||
|
self.source_view = GtkSource.View(
|
||||||
|
auto_indent=True, indent_width=4, buffer=self.buffer, show_line_numbers=True, editable=None,
|
||||||
|
top_margin=6, bottom_margin=6, left_margin=12, right_margin=12, css_classes=["code_block"]
|
||||||
|
)
|
||||||
|
self.source_view.update_property([4], [_("{}Code Block").format('{} '.format(self.language.get_name()) if self.language else "")])
|
||||||
|
|
||||||
|
title_box = Gtk.Box(margin_start=12, margin_top=3, margin_bottom=3, margin_end=3)
|
||||||
|
title_box.append(Gtk.Label(label=self.language.get_name() if self.language else _("Code Block"), hexpand=True, xalign=0))
|
||||||
|
copy_button = Gtk.Button(icon_name="edit-copy-symbolic", css_classes=["flat", "circular"], tooltip_text=_("Copy Message"))
|
||||||
|
copy_button.connect("clicked", self.on_copy)
|
||||||
|
title_box.append(copy_button)
|
||||||
|
self.append(title_box)
|
||||||
|
self.append(Gtk.Separator())
|
||||||
|
self.append(self.source_view)
|
||||||
|
self.buffer.set_text(text)
|
||||||
|
|
||||||
|
def on_copy(self, *_):
|
||||||
|
logger.debug("Copying code")
|
||||||
|
clipboard = Gdk.Display().get_default().get_clipboard()
|
||||||
|
start = self.buffer.get_start_iter()
|
||||||
|
end = self.buffer.get_end_iter()
|
||||||
|
text = self.buffer.get_text(start, end, False)
|
||||||
|
clipboard.set(text)
|
||||||
|
window.show_toast(_("Code copied to the clipboard"), window.main_overlay)
|
||||||
|
|
||||||
|
class attachment(Gtk.Button):
|
||||||
|
__gtype_name__ = 'AlpacaAttachment'
|
||||||
|
|
||||||
|
def __init__(self, file_name:str, file_path:str, file_type:str):
|
||||||
|
self.file_name = file_name
|
||||||
|
self.file_path = file_path
|
||||||
|
self.file_type = file_type
|
||||||
|
|
||||||
|
directory, file_name = os.path.split(self.file_path)
|
||||||
|
head, last_dir = os.path.split(directory)
|
||||||
|
head, second_last_dir = os.path.split(head)
|
||||||
|
self.file_path = os.path.join(head, '{selected_chat}', last_dir, file_name)
|
||||||
|
|
||||||
|
button_content = Adw.ButtonContent(
|
||||||
|
label=self.file_name,
|
||||||
|
icon_name={
|
||||||
|
"plain_text": "document-text-symbolic",
|
||||||
|
"pdf": "document-text-symbolic",
|
||||||
|
"youtube": "play-symbolic",
|
||||||
|
"website": "globe-symbolic"
|
||||||
|
}[self.file_type]
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
vexpand=False,
|
||||||
|
valign=3,
|
||||||
|
name=self.file_name,
|
||||||
|
css_classes=["flat"],
|
||||||
|
tooltip_text=self.file_name,
|
||||||
|
child=button_content
|
||||||
|
)
|
||||||
|
|
||||||
|
self.connect("clicked", lambda button, file_path=self.file_path, file_type=self.file_type: window.preview_file(file_path, file_type, None))
|
||||||
|
|
||||||
|
class attachment_container(Gtk.ScrolledWindow):
|
||||||
|
__gtype_name__ = 'AlpacaAttachmentContainer'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.files = []
|
||||||
|
|
||||||
|
self.container = Gtk.Box(
|
||||||
|
orientation=0,
|
||||||
|
spacing=12
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
margin_top=10,
|
||||||
|
margin_start=10,
|
||||||
|
margin_end=10,
|
||||||
|
hexpand=True,
|
||||||
|
child=self.container
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_file(self, file:attachment):
|
||||||
|
self.container.append(file)
|
||||||
|
self.files.append(file)
|
||||||
|
|
||||||
|
class image(Gtk.Button):
|
||||||
|
__gtype_name__ = 'AlpacaImage'
|
||||||
|
|
||||||
|
def __init__(self, image_path:str):
|
||||||
|
self.image_path = image_path
|
||||||
|
self.image_name = os.path.basename(self.image_path)
|
||||||
|
|
||||||
|
directory, file_name = os.path.split(self.image_path)
|
||||||
|
head, last_dir = os.path.split(directory)
|
||||||
|
head, second_last_dir = os.path.split(head)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.isfile(self.image_path):
|
||||||
|
raise FileNotFoundError("'{}' was not found or is a directory".format(self.image_path))
|
||||||
|
image = Gtk.Image.new_from_file(self.image_path)
|
||||||
|
image.set_size_request(240, 240)
|
||||||
|
super().__init__(
|
||||||
|
child=image,
|
||||||
|
css_classes=["flat", "chat_image_button"],
|
||||||
|
name=self.image_name,
|
||||||
|
tooltip_text=_("Image")
|
||||||
|
)
|
||||||
|
image.update_property([4], [_("Image")])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
image_texture = Gtk.Image.new_from_icon_name("image-missing-symbolic")
|
||||||
|
image_texture.set_icon_size(2)
|
||||||
|
image_texture.set_vexpand(True)
|
||||||
|
image_texture.set_pixel_size(120)
|
||||||
|
image_label = Gtk.Label(
|
||||||
|
label=_("Missing Image"),
|
||||||
|
)
|
||||||
|
image_box = Gtk.Box(
|
||||||
|
spacing=10,
|
||||||
|
orientation=1,
|
||||||
|
margin_top=10,
|
||||||
|
margin_bottom=10,
|
||||||
|
margin_start=10,
|
||||||
|
margin_end=10
|
||||||
|
)
|
||||||
|
image_box.append(image_texture)
|
||||||
|
image_box.append(image_label)
|
||||||
|
image_box.set_size_request(220, 220)
|
||||||
|
super().__init__(
|
||||||
|
child=image_box,
|
||||||
|
css_classes=["flat", "chat_image_button"],
|
||||||
|
tooltip_text=_("Missing Image")
|
||||||
|
)
|
||||||
|
image_texture.update_property([4], [_("Missing image")])
|
||||||
|
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):
|
||||||
|
__gtype_name__ = 'AlpacaImageContainer'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.files = []
|
||||||
|
|
||||||
|
self.container = Gtk.Box(
|
||||||
|
orientation=0,
|
||||||
|
spacing=12
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
margin_top=10,
|
||||||
|
margin_start=10,
|
||||||
|
margin_end=10,
|
||||||
|
hexpand=True,
|
||||||
|
height_request = 240,
|
||||||
|
child=self.container
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_image(self, img:image):
|
||||||
|
self.container.append(img)
|
||||||
|
self.files.append(img)
|
||||||
|
|
||||||
|
class footer(Gtk.Label):
|
||||||
|
__gtype_name__ = 'AlpacaMessageFooter'
|
||||||
|
|
||||||
|
def __init__(self, dt:datetime.datetime, model:str=None):
|
||||||
|
super().__init__(
|
||||||
|
hexpand=False,
|
||||||
|
halign=0,
|
||||||
|
wrap=True,
|
||||||
|
ellipsize=3,
|
||||||
|
wrap_mode=2,
|
||||||
|
xalign=0,
|
||||||
|
margin_bottom=5,
|
||||||
|
margin_start=5,
|
||||||
|
focusable=True
|
||||||
|
)
|
||||||
|
self.set_markup("{}<small>{}</small>".format((window.convert_model_name(model, 0) + "\n") if model else "", GLib.markup_escape_text(self.format_datetime(dt))))
|
||||||
|
|
||||||
|
def format_datetime(self, dt:datetime) -> str:
|
||||||
|
date = GLib.DateTime.new(GLib.DateTime.new_now_local().get_timezone(), dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
|
||||||
|
current_date = GLib.DateTime.new_now_local()
|
||||||
|
if date.format("%Y/%m/%d") == current_date.format("%Y/%m/%d"):
|
||||||
|
return date.format("%H:%M %p")
|
||||||
|
if date.format("%Y") == current_date.format("%Y"):
|
||||||
|
return date.format("%b %d, %H:%M %p")
|
||||||
|
return date.format("%b %d %Y, %H:%M %p")
|
||||||
|
|
||||||
|
class action_buttons(Gtk.Box):
|
||||||
|
__gtype_name__ = 'AlpacaActionButtonContainer'
|
||||||
|
|
||||||
|
def __init__(self, bot:bool):
|
||||||
|
super().__init__(
|
||||||
|
orientation=0,
|
||||||
|
spacing=6,
|
||||||
|
margin_end=6,
|
||||||
|
margin_bottom=6,
|
||||||
|
valign="end",
|
||||||
|
halign="end"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.delete_button = Gtk.Button(
|
||||||
|
icon_name = "user-trash-symbolic",
|
||||||
|
css_classes = ["flat", "circular"],
|
||||||
|
tooltip_text = _("Remove Message")
|
||||||
|
)
|
||||||
|
self.delete_button.connect('clicked', lambda *_: self.delete_message())
|
||||||
|
self.append(self.delete_button)
|
||||||
|
|
||||||
|
self.copy_button = Gtk.Button(
|
||||||
|
icon_name = "edit-copy-symbolic",
|
||||||
|
css_classes = ["flat", "circular"],
|
||||||
|
tooltip_text = _("Copy Message")
|
||||||
|
)
|
||||||
|
self.copy_button.connect('clicked', lambda *_: self.copy_message())
|
||||||
|
self.append(self.copy_button)
|
||||||
|
|
||||||
|
self.regenerate_button = Gtk.Button(
|
||||||
|
icon_name = "update-symbolic",
|
||||||
|
css_classes = ["flat", "circular"],
|
||||||
|
tooltip_text = _("Regenerate Message")
|
||||||
|
)
|
||||||
|
self.regenerate_button.connect('clicked', lambda *_: self.regenerate_message())
|
||||||
|
|
||||||
|
self.edit_button = Gtk.Button(
|
||||||
|
icon_name = "edit-symbolic",
|
||||||
|
css_classes = ["flat", "circular"],
|
||||||
|
tooltip_text = _("Edit Message")
|
||||||
|
)
|
||||||
|
self.edit_button.connect('clicked', lambda *_: self.edit_message())
|
||||||
|
|
||||||
|
self.append(self.regenerate_button if bot else self.edit_button)
|
||||||
|
|
||||||
|
def delete_message(self):
|
||||||
|
logger.debug("Deleting message")
|
||||||
|
chat = self.get_parent().get_parent().get_parent().get_parent().get_parent()
|
||||||
|
message_id = self.get_parent().message_id
|
||||||
|
self.get_parent().get_parent().remove(self.get_parent())
|
||||||
|
if os.path.exists(os.path.join(data_dir, "chats", window.chat_list_box.get_current_chat().get_name(), self.get_parent().message_id)):
|
||||||
|
shutil.rmtree(os.path.join(data_dir, "chats", window.chat_list_box.get_current_chat().get_name(), self.get_parent().message_id))
|
||||||
|
del chat.messages[message_id]
|
||||||
|
window.save_history(chat)
|
||||||
|
if len(chat.messages) == 0:
|
||||||
|
chat.show_welcome_screen(len(window.model_selector.get_model_list()) > 0)
|
||||||
|
|
||||||
|
def copy_message(self):
|
||||||
|
logger.debug("Copying message")
|
||||||
|
clipboard = Gdk.Display().get_default().get_clipboard()
|
||||||
|
clipboard.set(self.get_parent().text)
|
||||||
|
window.show_toast(_("Message copied to the clipboard"), window.main_overlay)
|
||||||
|
|
||||||
|
def regenerate_message(self):
|
||||||
|
chat = self.get_parent().get_parent().get_parent().get_parent().get_parent()
|
||||||
|
message_element = self.get_parent()
|
||||||
|
if not chat.busy:
|
||||||
|
message_element.set_text()
|
||||||
|
message_element.container.remove(message_element.footer)
|
||||||
|
message_element.remove_overlay(self)
|
||||||
|
message_element.action_buttons = None
|
||||||
|
history = window.convert_history_to_ollama(chat)[:list(chat.messages).index(message_element.message_id)]
|
||||||
|
data = {
|
||||||
|
"model": window.get_current_model(1),
|
||||||
|
"messages": history,
|
||||||
|
"options": {"temperature": window.model_tweaks["temperature"], "seed": window.model_tweaks["seed"]},
|
||||||
|
"keep_alive": f"{window.model_tweaks['keep_alive']}m"
|
||||||
|
}
|
||||||
|
thread = threading.Thread(target=window.run_message, args=(data, message_element))
|
||||||
|
thread.start()
|
||||||
|
else:
|
||||||
|
window.show_toast(_("Message cannot be regenerated while receiving a response"), window.main_overlay)
|
||||||
|
|
||||||
|
def edit_message(self):
|
||||||
|
logger.debug("Editing message")
|
||||||
|
self.get_parent().action_buttons.set_visible(False)
|
||||||
|
for child in self.get_parent().content_children:
|
||||||
|
self.get_parent().container.remove(child)
|
||||||
|
self.get_parent().content_children = []
|
||||||
|
self.get_parent().container.remove(self.get_parent().footer)
|
||||||
|
self.get_parent().footer = None
|
||||||
|
edit_text_b = edit_text_block(self.get_parent().text)
|
||||||
|
self.get_parent().container.append(edit_text_b)
|
||||||
|
window.set_focus(edit_text_b)
|
||||||
|
|
||||||
|
|
||||||
|
class message(Gtk.Overlay):
|
||||||
|
__gtype_name__ = 'AlpacaMessage'
|
||||||
|
|
||||||
|
def __init__(self, message_id:str, model:str=None):
|
||||||
|
self.message_id = message_id
|
||||||
|
self.bot = model != None
|
||||||
|
self.dt = None
|
||||||
|
self.model = model
|
||||||
|
self.action_buttons = None
|
||||||
|
self.content_children = [] #These are the code blocks, text blocks and tables
|
||||||
|
self.footer = None
|
||||||
|
self.image_c = None
|
||||||
|
self.attachment_c = None
|
||||||
|
self.spinner = None
|
||||||
|
self.text = None
|
||||||
|
|
||||||
|
self.container = Gtk.Box(
|
||||||
|
orientation=1,
|
||||||
|
halign='fill',
|
||||||
|
css_classes=["response_message"] if self.bot else ["card", "user_message"],
|
||||||
|
spacing=12
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(css_classes=["message"], name=message_id)
|
||||||
|
self.set_child(self.container)
|
||||||
|
|
||||||
|
def add_attachments(self, attachments:dict):
|
||||||
|
self.attachment_c = attachment_container()
|
||||||
|
self.container.append(self.attachment_c)
|
||||||
|
for file_path, file_type in attachments.items():
|
||||||
|
file = attachment(os.path.basename(file_path), file_path, file_type)
|
||||||
|
self.attachment_c.add_file(file)
|
||||||
|
|
||||||
|
def add_images(self, images:list):
|
||||||
|
self.image_c = image_container()
|
||||||
|
self.container.append(self.image_c)
|
||||||
|
for image_path in images:
|
||||||
|
image_element = image(image_path)
|
||||||
|
self.image_c.add_image(image_element)
|
||||||
|
|
||||||
|
def add_footer(self, dt:datetime.datetime):
|
||||||
|
self.dt = dt
|
||||||
|
self.footer = footer(self.dt, self.model)
|
||||||
|
self.container.append(self.footer)
|
||||||
|
|
||||||
|
def add_action_buttons(self):
|
||||||
|
if not self.action_buttons:
|
||||||
|
self.action_buttons = action_buttons(self.bot)
|
||||||
|
self.add_overlay(self.action_buttons)
|
||||||
|
|
||||||
|
def update_message(self, data:dict):
|
||||||
|
chat = self.get_parent().get_parent().get_parent().get_parent()
|
||||||
|
if chat.busy:
|
||||||
|
if self.spinner:
|
||||||
|
if not window.chat_list_box.get_sensitive():
|
||||||
|
window.chat_list_box.set_sensitive(True)
|
||||||
|
self.container.remove(self.spinner)
|
||||||
|
self.spinner = None
|
||||||
|
self.content_children[-1].set_visible(True)
|
||||||
|
self.content_children[-1].insert_at_end(data['message']['content'], False)
|
||||||
|
if 'done' in data and data['done']:
|
||||||
|
if chat.welcome_screen:
|
||||||
|
chat.container.remove(chat.welcome_screen)
|
||||||
|
chat.welcome_screen = None
|
||||||
|
chat.stop_message()
|
||||||
|
self.set_text(self.content_children[-1].get_label())
|
||||||
|
self.dt = datetime.datetime.now()
|
||||||
|
self.add_footer(self.dt)
|
||||||
|
window.save_history(chat)
|
||||||
|
|
||||||
|
def set_text(self, text:str=None):
|
||||||
|
self.text = text
|
||||||
|
for child in self.content_children:
|
||||||
|
self.container.remove(child)
|
||||||
|
self.content_children = []
|
||||||
|
if text:
|
||||||
|
self.content_children = []
|
||||||
|
code_block_pattern = re.compile(r'```(\w+)\n(.*?)\n```', re.DOTALL)
|
||||||
|
no_lang_code_block_pattern = re.compile(r'`\n(.*?)\n`', re.DOTALL)
|
||||||
|
table_pattern = re.compile(r'((\r?\n){2}|^)([^\r\n]*\|[^\r\n]*(\r?\n)?)+(?=(\r?\n){2}|$)', re.MULTILINE)
|
||||||
|
bold_pattern = re.compile(r'\*\*(.*?)\*\*') #"**text**"
|
||||||
|
code_pattern = re.compile(r'`([^`\n]*?)`') #"`text`"
|
||||||
|
h1_pattern = re.compile(r'^#\s(.*)$') #"# text"
|
||||||
|
h2_pattern = re.compile(r'^##\s(.*)$') #"## text"
|
||||||
|
markup_pattern = re.compile(r'<(b|u|tt|span.*)>(.*?)<\/(b|u|tt|span)>') #heh butt span, I'm so funny
|
||||||
|
parts = []
|
||||||
|
pos = 0
|
||||||
|
# Code blocks
|
||||||
|
for match in code_block_pattern.finditer(self.text):
|
||||||
|
start, end = match.span()
|
||||||
|
if pos < start:
|
||||||
|
normal_text = self.text[pos:start]
|
||||||
|
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||||
|
language = match.group(1)
|
||||||
|
code_text = match.group(2)
|
||||||
|
parts.append({"type": "code", "text": code_text, "language": 'python3' if language == 'python' else language})
|
||||||
|
pos = end
|
||||||
|
# Code blocks (No language)
|
||||||
|
for match in no_lang_code_block_pattern.finditer(self.text):
|
||||||
|
start, end = match.span()
|
||||||
|
if pos < start:
|
||||||
|
normal_text = self.text[pos:start]
|
||||||
|
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||||
|
code_text = match.group(1)
|
||||||
|
parts.append({"type": "code", "text": code_text, "language": None})
|
||||||
|
pos = end
|
||||||
|
# Tables
|
||||||
|
for match in table_pattern.finditer(self.text):
|
||||||
|
start, end = match.span()
|
||||||
|
if pos < start:
|
||||||
|
normal_text = self.text[pos:start]
|
||||||
|
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||||
|
table_text = match.group(0)
|
||||||
|
parts.append({"type": "table", "text": table_text})
|
||||||
|
pos = end
|
||||||
|
# Text blocks
|
||||||
|
if pos < len(text):
|
||||||
|
normal_text = text[pos:]
|
||||||
|
if normal_text.strip():
|
||||||
|
parts.append({"type": "normal", "text": normal_text.strip()})
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
if part['type'] == 'normal':
|
||||||
|
text_b = text_block(self.bot)
|
||||||
|
part['text'] = part['text'].replace("\n* ", "\n• ")
|
||||||
|
part['text'] = code_pattern.sub(r'<tt>\1</tt>', part['text'])
|
||||||
|
part['text'] = bold_pattern.sub(r'<b>\1</b>', part['text'])
|
||||||
|
part['text'] = h1_pattern.sub(r'<span size="x-large">\1</span>', part['text'])
|
||||||
|
part['text'] = h2_pattern.sub(r'<span size="large">\1</span>', part['text'])
|
||||||
|
pos = 0
|
||||||
|
for match in markup_pattern.finditer(part['text']):
|
||||||
|
start, end = match.span()
|
||||||
|
if pos < start:
|
||||||
|
text_b.insert_at_end(part['text'][pos:start], False)
|
||||||
|
text_b.insert_at_end(match.group(0), True)
|
||||||
|
pos = end
|
||||||
|
|
||||||
|
if pos < len(part['text']):
|
||||||
|
text_b.insert_at_end(part['text'][pos:], False)
|
||||||
|
self.content_children.append(text_b)
|
||||||
|
self.container.append(text_b)
|
||||||
|
elif part['type'] == 'code':
|
||||||
|
code_b = code_block(part['text'], part['language'])
|
||||||
|
self.content_children.append(code_b)
|
||||||
|
self.container.append(code_b)
|
||||||
|
elif part['type'] == 'table':
|
||||||
|
table_w = TableWidget(part['text'])
|
||||||
|
self.content_children.append(table_w)
|
||||||
|
self.container.append(table_w)
|
||||||
|
self.add_action_buttons()
|
||||||
|
else:
|
||||||
|
text_b = text_block(self.bot)
|
||||||
|
text_b.set_visible(False)
|
||||||
|
self.content_children.append(text_b)
|
||||||
|
self.spinner = Gtk.Spinner(spinning=True, margin_top=12, margin_bottom=12, hexpand=True)
|
||||||
|
self.container.append(self.spinner)
|
||||||
|
self.container.append(text_b)
|
||||||
|
self.container.queue_draw()
|
||||||
|
|
110
src/custom_widgets/model_widget.py
Normal file
110
src/custom_widgets/model_widget.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#model_widget.py
|
||||||
|
"""
|
||||||
|
Handles the model widget (testing)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '4.0')
|
||||||
|
gi.require_version('GtkSource', '5')
|
||||||
|
from gi.repository import Gtk, GObject, Gio, Adw, GtkSource, GLib, Gdk
|
||||||
|
import logging, os, datetime, re, shutil, threading
|
||||||
|
from ..internal import config_dir, data_dir, cache_dir, source_dir
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
window = None
|
||||||
|
|
||||||
|
class model_selector_popup(Gtk.Popover):
|
||||||
|
__gtype_name__ = 'AlpacaModelSelectorPopup'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
manage_models_button = Gtk.Button(
|
||||||
|
tooltip_text=_('Model Manager'),
|
||||||
|
child=Gtk.Label(label=_('Model Manager')),
|
||||||
|
hexpand=True,
|
||||||
|
css_classes=['manage_models_button', 'flat']
|
||||||
|
)
|
||||||
|
manage_models_button.set_action_name("app.manage_models")
|
||||||
|
manage_models_button.connect("clicked", lambda *_: self.hide())
|
||||||
|
self.model_list_box = Gtk.ListBox(
|
||||||
|
css_classes=['navigation-sidebar', 'model_list_box'],
|
||||||
|
height_request=0
|
||||||
|
)
|
||||||
|
container = Gtk.Box(
|
||||||
|
orientation=1,
|
||||||
|
spacing=5
|
||||||
|
)
|
||||||
|
container.append(self.model_list_box)
|
||||||
|
container.append(Gtk.Separator())
|
||||||
|
container.append(manage_models_button)
|
||||||
|
|
||||||
|
scroller = Gtk.ScrolledWindow(
|
||||||
|
max_content_height=300,
|
||||||
|
propagate_natural_width=True,
|
||||||
|
propagate_natural_height=True,
|
||||||
|
child=container
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
css_classes=['model_popover'],
|
||||||
|
has_arrow=False,
|
||||||
|
child=scroller
|
||||||
|
)
|
||||||
|
|
||||||
|
class model_selector_button(Gtk.MenuButton):
|
||||||
|
__gtype_name__ = 'AlpacaModelSelectorButton'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.popover = model_selector_popup()
|
||||||
|
self.popover.model_list_box.connect('selected-rows-changed', self.model_changed)
|
||||||
|
self.popover.model_list_box.connect('row-activated', lambda *_: self.get_popover().hide())
|
||||||
|
super().__init__(
|
||||||
|
tooltip_text=_('Select a Model'),
|
||||||
|
child=Adw.ButtonContent(
|
||||||
|
label=_('Select a model'),
|
||||||
|
icon_name='down-symbolic'
|
||||||
|
),
|
||||||
|
popover=self.popover
|
||||||
|
)
|
||||||
|
|
||||||
|
def change_model(self, model_name:str):
|
||||||
|
for model_row in list(self.get_popover().model_list_box):
|
||||||
|
if model_name == model_row.get_name():
|
||||||
|
self.get_popover().model_list_box.select_row(model_row)
|
||||||
|
break
|
||||||
|
|
||||||
|
def model_changed(self, listbox:Gtk.ListBox):
|
||||||
|
row = listbox.get_selected_row()
|
||||||
|
if row:
|
||||||
|
model_name = row.get_name()
|
||||||
|
self.get_child().set_label(window.convert_model_name(model_name, 0))
|
||||||
|
self.set_tooltip_text(window.convert_model_name(model_name, 0))
|
||||||
|
elif len(list(listbox)) == 0:
|
||||||
|
self.get_child().set_label(_("Select a model"))
|
||||||
|
self.set_tooltip_text(_("Select a Model"))
|
||||||
|
|
||||||
|
def get_model(self) -> str:
|
||||||
|
row = self.get_popover().model_list_box.get_selected_row()
|
||||||
|
if row:
|
||||||
|
return row.get_name()
|
||||||
|
|
||||||
|
def add_model(self, model_name:str):
|
||||||
|
model_row = Gtk.ListBoxRow(
|
||||||
|
child = Gtk.Label(
|
||||||
|
label=window.convert_model_name(model_name, 0),
|
||||||
|
halign=1,
|
||||||
|
hexpand=True
|
||||||
|
),
|
||||||
|
halign=0,
|
||||||
|
hexpand=True,
|
||||||
|
name=model_name,
|
||||||
|
tooltip_text=window.convert_model_name(model_name, 0)
|
||||||
|
)
|
||||||
|
self.get_popover().model_list_box.append(model_row)
|
||||||
|
self.change_model(model_name)
|
||||||
|
|
||||||
|
def get_model_list(self) -> list:
|
||||||
|
return [model.get_name() for model in list(self.get_popover().model_list_box)]
|
||||||
|
|
||||||
|
def clear_list(self):
|
||||||
|
self.get_popover().model_list_box.remove_all()
|
@ -37,7 +37,8 @@ class TableWidget(Gtk.Frame):
|
|||||||
|
|
||||||
def __init__(self, markdown):
|
def __init__(self, markdown):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.set_margin_start(5)
|
||||||
|
self.set_margin_end(5)
|
||||||
self.table = MarkdownTable()
|
self.table = MarkdownTable()
|
||||||
|
|
||||||
self.set_halign(Gtk.Align.START)
|
self.set_halign(Gtk.Align.START)
|
||||||
|
@ -14,7 +14,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def clear_chat_response(self, dialog, task):
|
def clear_chat_response(self, dialog, task):
|
||||||
if dialog.choose_finish(task) == "clear":
|
if dialog.choose_finish(task) == "clear":
|
||||||
self.clear_chat()
|
self.chat_list_box.get_current_chat().clear_chat()
|
||||||
|
|
||||||
def clear_chat(self):
|
def clear_chat(self):
|
||||||
if self.bot_message is not None:
|
if self.bot_message is not None:
|
||||||
@ -39,7 +39,7 @@ def clear_chat(self):
|
|||||||
|
|
||||||
def delete_chat_response(self, dialog, task, chat_name):
|
def delete_chat_response(self, dialog, task, chat_name):
|
||||||
if dialog.choose_finish(task) == "delete":
|
if dialog.choose_finish(task) == "delete":
|
||||||
self.delete_chat(chat_name)
|
self.chat_list_box.delete_chat(chat_name)
|
||||||
|
|
||||||
def delete_chat(self, chat_name):
|
def delete_chat(self, chat_name):
|
||||||
dialog = Adw.AlertDialog(
|
dialog = Adw.AlertDialog(
|
||||||
@ -59,16 +59,16 @@ def delete_chat(self, chat_name):
|
|||||||
|
|
||||||
# RENAME CHAT | WORKS
|
# RENAME CHAT | WORKS
|
||||||
|
|
||||||
def rename_chat_response(self, dialog, task, old_chat_name, entry, label_element):
|
def rename_chat_response(self, dialog, task, old_chat_name, entry):
|
||||||
if not entry:
|
if not entry:
|
||||||
return
|
return
|
||||||
new_chat_name = entry.get_text()
|
new_chat_name = entry.get_text()
|
||||||
if old_chat_name == new_chat_name:
|
if old_chat_name == new_chat_name:
|
||||||
return
|
return
|
||||||
if new_chat_name and (task is None or dialog.choose_finish(task) == "rename"):
|
if new_chat_name and (task is None or dialog.choose_finish(task) == "rename"):
|
||||||
self.rename_chat(old_chat_name, new_chat_name, label_element)
|
self.chat_list_box.rename_chat(old_chat_name, new_chat_name)
|
||||||
|
|
||||||
def rename_chat(self, chat_name, label_element):
|
def rename_chat(self, chat_name):
|
||||||
entry = Gtk.Entry()
|
entry = Gtk.Entry()
|
||||||
dialog = Adw.AlertDialog(
|
dialog = Adw.AlertDialog(
|
||||||
heading=_("Rename Chat?"),
|
heading=_("Rename Chat?"),
|
||||||
@ -83,7 +83,7 @@ def rename_chat(self, chat_name, label_element):
|
|||||||
dialog.choose(
|
dialog.choose(
|
||||||
parent = self,
|
parent = self,
|
||||||
cancellable = None,
|
cancellable = None,
|
||||||
callback = lambda dialog, task, old_chat_name=chat_name, entry=entry, label_element=label_element: rename_chat_response(self, dialog, task, old_chat_name, entry, label_element)
|
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"
|
# NEW CHAT | WORKS | UNUSED REASON: The 'Add Chat' button now creates a chat without a name AKA "New Chat"
|
||||||
|
@ -48,7 +48,10 @@ alpaca_sources = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
custom_widgets = [
|
custom_widgets = [
|
||||||
'custom_widgets/table_widget.py'
|
'custom_widgets/table_widget.py',
|
||||||
|
'custom_widgets/message_widget.py',
|
||||||
|
'custom_widgets/chat_widget.py',
|
||||||
|
'custom_widgets/model_widget.py'
|
||||||
]
|
]
|
||||||
|
|
||||||
install_data(alpaca_sources, install_dir: moduledir)
|
install_data(alpaca_sources, install_dir: moduledir)
|
||||||
|
@ -18,13 +18,16 @@
|
|||||||
.model_list_box > * {
|
.model_list_box > * {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
.user_message, .response_message {
|
.user_message > label, .response_message > label {
|
||||||
padding: 12px;
|
padding: 7px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.user_message:focus, .response_message:focus, .editing_message_textview:focus, .code_block:focus {
|
.user_message label:focus, .response_message label:focus, .editing_message_textview:focus, .code_block:focus {
|
||||||
box-shadow: 0 0 1px 2px mix(@accent_color, @window_bg_color, 0.5);
|
box-shadow: 0 0 1px 2px mix(@accent_color, @window_bg_color, 0.5);
|
||||||
}
|
}
|
||||||
.model_popover {
|
.model_popover {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
stacksidebar {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
1029
src/window.py
1029
src/window.py
File diff suppressed because it is too large
Load Diff
132
src/window.ui
132
src/window.ui
@ -26,6 +26,7 @@
|
|||||||
<object class="AdwHeaderBar">
|
<object class="AdwHeaderBar">
|
||||||
<child type="start">
|
<child type="start">
|
||||||
<object class="GtkButton" id="add_chat_button">
|
<object class="GtkButton" id="add_chat_button">
|
||||||
|
<property name="action-name">app.new_chat</property>
|
||||||
<property name="tooltip-text" translatable="yes">New Chat</property>
|
<property name="tooltip-text" translatable="yes">New Chat</property>
|
||||||
<property name="icon-name">chat-message-new-symbolic</property>
|
<property name="icon-name">chat-message-new-symbolic</property>
|
||||||
<style>
|
<style>
|
||||||
@ -44,18 +45,9 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<property name="content">
|
<property name="content">
|
||||||
<object class="GtkScrolledWindow">
|
<object class="GtkScrolledWindow" id="chat_list_container">
|
||||||
<property name="vexpand">true</property>
|
<property name="vexpand">true</property>
|
||||||
<property name="hexpand">true</property>
|
<property name="hexpand">true</property>
|
||||||
<child>
|
|
||||||
<object class="GtkListBox" id="chat_list_box">
|
|
||||||
<signal name="row-selected" handler="chat_changed"/>
|
|
||||||
<property name="selection-mode">single</property>
|
|
||||||
<style>
|
|
||||||
<class name="navigation-sidebar"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
@ -71,91 +63,6 @@
|
|||||||
<property name="active" bind-source="split_view_overlay" bind-property="show-sidebar" bind-flags="sync-create"/>
|
<property name="active" bind-source="split_view_overlay" bind-property="show-sidebar" bind-flags="sync-create"/>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<property name="title-widget">
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">0</property>
|
|
||||||
<property name="spacing">12</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkMenuButton" id="model_selector_button">
|
|
||||||
<property name="tooltip-text" translatable="yes">Select Model</property>
|
|
||||||
<property name="child">
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="spacing">10</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="label" translatable="yes">Select a Model</property>
|
|
||||||
<property name="ellipsize">2</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkImage">
|
|
||||||
<property name="icon-name">down-symbolic</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
<property name="halign">1</property>
|
|
||||||
<style>
|
|
||||||
<class name="raised"/>
|
|
||||||
</style>
|
|
||||||
<property name="popover">
|
|
||||||
<object class="GtkPopover" id="model_popover">
|
|
||||||
<style>
|
|
||||||
<class name="model_popover"/>
|
|
||||||
</style>
|
|
||||||
<property name="has-arrow">false</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox">
|
|
||||||
<property name="orientation">1</property>
|
|
||||||
<property name="spacing">5</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkButton">
|
|
||||||
<property name="child">
|
|
||||||
<object class="GtkLabel">
|
|
||||||
<property name="label" translatable="yes">Manage Models</property>
|
|
||||||
<property name="justify">left</property>
|
|
||||||
<property name="halign">1</property>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
<property name="hexpand">true</property>
|
|
||||||
<property name="tooltip-text" translatable="yes">Manage Models</property>
|
|
||||||
<property name="action-name">app.manage_models</property>
|
|
||||||
<signal name="clicked" handler="close_model_popup"/>
|
|
||||||
<style>
|
|
||||||
<class name="flat"/>
|
|
||||||
<class name="manage_models_button"/>
|
|
||||||
</style>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkSeparator"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkScrolledWindow">
|
|
||||||
<property name="max-content-height">300</property>
|
|
||||||
<property name="propagate-natural-width">true</property>
|
|
||||||
<property name="propagate-natural-height">true</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkListBox" id="model_list_box">
|
|
||||||
<property name="hexpand">true</property>
|
|
||||||
<style>
|
|
||||||
<class name="navigation-sidebar"/>
|
|
||||||
<class name="model_list_box"/>
|
|
||||||
</style>
|
|
||||||
<signal name="row-selected" handler="change_model"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</property>
|
|
||||||
<child type="end">
|
<child type="end">
|
||||||
<object class="GtkMenuButton" id="secondary_menu_button">
|
<object class="GtkMenuButton" id="secondary_menu_button">
|
||||||
<property name="primary">False</property>
|
<property name="primary">False</property>
|
||||||
@ -174,32 +81,10 @@
|
|||||||
<child>
|
<child>
|
||||||
<object class="AdwToastOverlay" id="main_overlay">
|
<object class="AdwToastOverlay" id="main_overlay">
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="chat_window">
|
<object class="GtkStack" id="chat_stack">
|
||||||
<property name="propagate-natural-height">true</property>
|
<property name="hexpand">true</property>
|
||||||
<property name="kinetic-scrolling">true</property>
|
|
||||||
<property name="vexpand">true</property>
|
<property name="vexpand">true</property>
|
||||||
<style>
|
<property name="hhomogeneous">true</property>
|
||||||
<class name="undershoot-bottom"/>
|
|
||||||
</style>
|
|
||||||
<child>
|
|
||||||
<object class="AdwClamp">
|
|
||||||
<property name="maximum-size">1000</property>
|
|
||||||
<property name="tightening-threshold">800</property>
|
|
||||||
<child>
|
|
||||||
<object class="GtkBox" id="chat_container">
|
|
||||||
<property name="orientation">1</property>
|
|
||||||
<property name="homogeneous">false</property>
|
|
||||||
<property name="hexpand">true</property>
|
|
||||||
<property name="vexpand">true</property>
|
|
||||||
<property name="spacing">12</property>
|
|
||||||
<property name="margin-top">12</property>
|
|
||||||
<property name="margin-bottom">12</property>
|
|
||||||
<property name="margin-start">12</property>
|
|
||||||
<property name="margin-end">12</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
@ -1139,6 +1024,12 @@
|
|||||||
<property name="title" translatable="yes">Toggle sidebar</property>
|
<property name="title" translatable="yes">Toggle sidebar</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkShortcutsShortcut">
|
||||||
|
<property name="accelerator">F2</property>
|
||||||
|
<property name="title" translatable="yes">Rename chat</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
@ -1175,3 +1066,4 @@
|
|||||||
</object>
|
</object>
|
||||||
</interface>
|
</interface>
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user