2024-05-12 18:18:25 -06:00
# window.py
#
2024-06-04 12:07:15 -06:00
# Copyright 2024 Jeffser
2024-05-12 18:18:25 -06:00
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
2024-08-04 21:20:47 -06:00
"""
Handles the main window
"""
2024-09-03 21:52:14 -06:00
import json , threading , os , re , base64 , gettext , uuid , shutil , logging , time
2024-05-17 00:35:34 -06:00
from io import BytesIO
from PIL import Image
2024-06-21 17:16:59 -06:00
from pypdf import PdfReader
2024-05-12 18:18:25 -06:00
from datetime import datetime
2024-10-10 22:14:08 -06:00
from pytube import YouTube
2024-08-04 21:27:12 -06:00
import gi
gi . require_version ( ' GtkSource ' , ' 5 ' )
gi . require_version ( ' GdkPixbuf ' , ' 2.0 ' )
from gi . repository import Adw , Gtk , Gdk , GLib , GtkSource , Gio , GdkPixbuf
2024-10-11 13:45:19 -06:00
from . import connection_handler , generic_actions
2024-10-10 22:14:08 -06:00
from . custom_widgets import message_widget , chat_widget , model_widget , terminal_widget , dialog_widget
2024-08-04 04:07:14 +08:00
from . internal import config_dir , data_dir , cache_dir , source_dir
2024-07-17 21:49:55 -05:00
logger = logging . getLogger ( __name__ )
2024-05-12 18:18:25 -06:00
@Gtk.Template ( resource_path = ' /com/jeffser/Alpaca/window.ui ' )
class AlpacaWindow ( Adw . ApplicationWindow ) :
2024-05-22 18:01:16 -06:00
app_dir = os . getenv ( " FLATPAK_DEST " )
2024-05-12 18:18:25 -06:00
__gtype_name__ = ' AlpacaWindow '
2024-05-19 19:06:11 -06:00
2024-08-04 04:07:14 +08:00
localedir = os . path . join ( source_dir , ' locale ' )
2024-05-19 19:06:11 -06:00
gettext . bindtextdomain ( ' com.jeffser.Alpaca ' , localedir )
gettext . textdomain ( ' com.jeffser.Alpaca ' )
_ = gettext . gettext
2024-05-12 18:18:25 -06:00
#Variables
2024-06-04 12:07:15 -06:00
attachments = { }
2024-05-12 18:18:25 -06:00
2024-06-03 17:01:27 -06:00
#Override elements
2024-08-31 17:14:39 -06:00
overrides_group = Gtk . Template . Child ( )
2024-10-11 15:01:13 -06:00
instance_page = Gtk . Template . Child ( )
2024-06-03 17:01:27 -06:00
2024-05-12 18:18:25 -06:00
#Elements
2024-08-04 17:50:52 -06:00
split_view_overlay = Gtk . Template . Child ( )
2024-08-02 23:42:35 -06:00
regenerate_button : Gtk . Button = None
2024-08-04 22:09:37 -06:00
selected_chat_row : Gtk . ListBoxRow = None
2024-06-01 00:07:34 -06:00
create_model_base = Gtk . Template . Child ( )
create_model_name = Gtk . Template . Child ( )
create_model_system = Gtk . Template . Child ( )
2024-08-02 20:47:04 -06:00
create_model_modelfile = Gtk . Template . Child ( )
2024-08-31 17:14:39 -06:00
tweaks_group = Gtk . Template . Child ( )
2024-05-21 17:58:51 -06:00
preferences_dialog = Gtk . Template . Child ( )
2024-05-19 12:10:14 -06:00
shortcut_window : Gtk . ShortcutsWindow = Gtk . Template . Child ( )
2024-06-04 12:07:15 -06:00
file_preview_dialog = Gtk . Template . Child ( )
file_preview_text_view = Gtk . Template . Child ( )
2024-07-08 13:33:02 -06:00
file_preview_image = Gtk . Template . Child ( )
2024-05-21 15:36:24 -06:00
welcome_dialog = Gtk . Template . Child ( )
2024-05-24 12:43:25 -06:00
welcome_carousel = Gtk . Template . Child ( )
welcome_previous_button = Gtk . Template . Child ( )
welcome_next_button = Gtk . Template . Child ( )
2024-05-14 00:27:02 -06:00
main_overlay = Gtk . Template . Child ( )
manage_models_overlay = Gtk . Template . Child ( )
2024-08-26 13:16:55 -06:00
chat_stack = Gtk . Template . Child ( )
2024-05-17 00:35:34 -06:00
message_text_view = Gtk . Template . Child ( )
2024-05-12 18:18:25 -06:00
send_button = Gtk . Template . Child ( )
2024-05-29 14:32:57 -06:00
stop_button = Gtk . Template . Child ( )
2024-06-04 12:07:15 -06:00
attachment_container = Gtk . Template . Child ( )
attachment_box = Gtk . Template . Child ( )
file_filter_tar = Gtk . Template . Child ( )
2024-06-01 00:07:34 -06:00
file_filter_gguf = Gtk . Template . Child ( )
2024-06-21 17:16:59 -06:00
file_filter_attachments = Gtk . Template . Child ( )
attachment_button = Gtk . Template . Child ( )
2024-06-24 23:13:17 -06:00
chat_right_click_menu = Gtk . Template . Child ( )
2024-06-25 23:32:09 -06:00
model_tag_list_box = Gtk . Template . Child ( )
2024-06-27 22:48:02 -06:00
navigation_view_manage_models = Gtk . Template . Child ( )
2024-06-26 16:03:44 -06:00
file_preview_open_button = Gtk . Template . Child ( )
2024-07-08 13:08:10 -06:00
file_preview_remove_button = Gtk . Template . Child ( )
2024-06-28 16:42:58 -06:00
secondary_menu_button = Gtk . Template . Child ( )
2024-06-29 11:10:31 -06:00
model_searchbar = Gtk . Template . Child ( )
2024-09-21 15:50:38 -06:00
message_searchbar = Gtk . Template . Child ( )
message_search_button = Gtk . Template . Child ( )
searchentry_messages = Gtk . Template . Child ( )
2024-06-30 18:31:10 -06:00
no_results_page = Gtk . Template . Child ( )
2024-06-30 13:28:08 -06:00
model_link_button = Gtk . Template . Child ( )
2024-09-17 21:14:58 -06:00
title_stack = Gtk . Template . Child ( )
2024-05-12 18:18:25 -06:00
manage_models_dialog = Gtk . Template . Child ( )
2024-08-27 23:25:58 -06:00
model_scroller = Gtk . Template . Child ( )
2024-05-12 18:18:25 -06:00
2024-08-26 13:16:55 -06:00
chat_list_container = Gtk . Template . Child ( )
chat_list_box = None
2024-08-31 17:14:39 -06:00
ollama_instance = None
model_manager = None
2024-05-19 00:17:00 -06:00
add_chat_button = Gtk . Template . Child ( )
2024-09-02 02:38:03 -06:00
instance_idle_timer = Gtk . Template . Child ( )
2024-05-19 00:17:00 -06:00
2024-05-21 23:15:32 -06:00
background_switch = Gtk . Template . Child ( )
2024-09-04 23:05:16 +02:00
powersaver_warning_switch = Gtk . Template . Child ( )
2024-05-21 17:58:51 -06:00
remote_connection_switch = Gtk . Template . Child ( )
2024-10-11 14:40:49 -06:00
remote_connection_switch_handler = None
2024-05-21 17:58:51 -06:00
2024-08-30 20:29:23 -06:00
banner = Gtk . Template . Child ( )
2024-05-25 23:07:51 +02:00
style_manager = Adw . StyleManager ( )
2024-10-07 02:02:27 -06:00
terminal_scroller = Gtk . Template . Child ( )
terminal_dialog = Gtk . Template . Child ( )
2024-05-25 23:13:25 -06:00
@Gtk.Template.Callback ( )
2024-05-29 14:32:57 -06:00
def stop_message ( self , button = None ) :
2024-08-26 13:16:55 -06:00
self . chat_list_box . get_current_chat ( ) . stop_message ( )
2024-05-25 23:13:25 -06:00
2024-05-29 14:32:57 -06:00
@Gtk.Template.Callback ( )
def send_message ( self , button = None ) :
2024-08-08 10:56:45 -06:00
if button and not button . get_visible ( ) :
return
2024-08-04 21:43:23 -06:00
if not self . message_text_view . get_buffer ( ) . get_text ( self . message_text_view . get_buffer ( ) . get_start_iter ( ) , self . message_text_view . get_buffer ( ) . get_end_iter ( ) , False ) :
return
2024-08-26 13:16:55 -06:00
current_chat = self . chat_list_box . get_current_chat ( )
if current_chat . busy == True :
return
self . chat_list_box . send_tab_to_top ( self . chat_list_box . get_selected_row ( ) )
2024-08-28 11:51:12 -06:00
current_model = self . model_manager . get_selected_model ( )
2024-05-29 14:32:57 -06:00
if current_model is None :
2024-07-07 20:24:29 -06:00
self . show_toast ( _ ( " Please select a model before chatting " ) , self . main_overlay )
2024-05-29 14:32:57 -06:00
return
2024-08-04 21:43:23 -06:00
message_id = self . generate_uuid ( )
2024-06-04 12:07:15 -06:00
attached_images = [ ]
attached_files = { }
for name , content in self . attachments . items ( ) :
2024-08-26 13:16:55 -06:00
if content [ " type " ] == ' image ' :
2024-08-30 12:42:48 -06:00
if self . model_manager . verify_if_image_can_be_used ( ) :
2024-08-31 17:14:39 -06:00
attached_images . append ( os . path . join ( data_dir , " chats " , current_chat . get_name ( ) , message_id , name ) )
2024-06-24 00:18:55 -06:00
else :
2024-08-31 17:14:39 -06:00
attached_files [ os . path . join ( data_dir , " chats " , current_chat . get_name ( ) , message_id , name ) ] = content [ ' type ' ]
if not os . path . exists ( os . path . join ( data_dir , " chats " , current_chat . get_name ( ) , message_id ) ) :
os . makedirs ( os . path . join ( data_dir , " chats " , current_chat . get_name ( ) , message_id ) )
shutil . copy ( content [ ' path ' ] , os . path . join ( data_dir , " chats " , current_chat . get_name ( ) , message_id , name ) )
2024-06-04 12:07:15 -06:00
content [ " button " ] . get_parent ( ) . remove ( content [ " button " ] )
self . attachments = { }
2024-06-04 14:14:09 -06:00
self . attachment_box . set_visible ( False )
2024-08-26 13:16:55 -06:00
raw_message = self . message_text_view . get_buffer ( ) . get_text ( self . message_text_view . get_buffer ( ) . get_start_iter ( ) , self . message_text_view . get_buffer ( ) . get_end_iter ( ) , False )
current_chat . add_message ( message_id , None )
m_element = current_chat . messages [ message_id ]
2024-06-04 12:07:15 -06:00
2024-08-26 13:16:55 -06:00
if len ( attached_files ) > 0 :
m_element . add_attachments ( attached_files )
2024-06-04 12:07:15 -06:00
if len ( attached_images ) > 0 :
2024-08-26 13:16:55 -06:00
m_element . add_images ( attached_images )
m_element . set_text ( raw_message )
m_element . add_footer ( datetime . now ( ) )
m_element . add_action_buttons ( )
2024-05-29 14:32:57 -06:00
data = {
2024-06-23 17:34:41 -06:00
" model " : current_model ,
2024-08-26 13:16:55 -06:00
" messages " : self . convert_history_to_ollama ( current_chat ) ,
2024-08-31 17:14:39 -06:00
" options " : { " temperature " : self . ollama_instance . tweaks [ " temperature " ] , " seed " : self . ollama_instance . tweaks [ " seed " ] } ,
2024-08-31 22:00:42 -06:00
" keep_alive " : f " { self . ollama_instance . tweaks [ ' keep_alive ' ] } m " ,
" stream " : True
2024-05-29 14:32:57 -06:00
}
self . message_text_view . get_buffer ( ) . set_text ( " " , 0 )
2024-08-07 20:39:46 -06:00
2024-08-26 13:16:55 -06:00
bot_id = self . generate_uuid ( )
current_chat . add_message ( bot_id , current_model )
m_element_bot = current_chat . messages [ bot_id ]
m_element_bot . set_text ( )
2024-08-31 22:00:42 -06:00
threading . Thread ( target = self . run_message , args = ( data , m_element_bot , current_chat ) ) . start ( )
2024-05-25 23:13:25 -06:00
@Gtk.Template.Callback ( )
def welcome_carousel_page_changed ( self , carousel , index ) :
2024-07-17 21:49:55 -05:00
logger . debug ( " Showing welcome carousel " )
2024-08-04 21:43:23 -06:00
if index == 0 :
self . welcome_previous_button . set_sensitive ( False )
else :
self . welcome_previous_button . set_sensitive ( True )
2024-07-08 11:41:20 -06:00
if index == carousel . get_n_pages ( ) - 1 :
self . welcome_next_button . set_label ( _ ( " Close " ) )
self . welcome_next_button . set_tooltip_text ( _ ( " Close " ) )
else :
self . welcome_next_button . set_label ( _ ( " Next " ) )
self . welcome_next_button . set_tooltip_text ( _ ( " Next " ) )
2024-05-25 23:13:25 -06:00
@Gtk.Template.Callback ( )
def welcome_previous_button_activate ( self , button ) :
self . welcome_carousel . scroll_to ( self . welcome_carousel . get_nth_page ( self . welcome_carousel . get_position ( ) - 1 ) , True )
@Gtk.Template.Callback ( )
def welcome_next_button_activate ( self , button ) :
2024-08-04 21:43:23 -06:00
if button . get_label ( ) == " Next " :
self . welcome_carousel . scroll_to ( self . welcome_carousel . get_nth_page ( self . welcome_carousel . get_position ( ) + 1 ) , True )
2024-05-25 23:13:25 -06:00
else :
self . welcome_dialog . force_close ( )
2024-09-11 22:31:18 -06:00
self . powersaver_warning_switch . set_active ( True )
2024-08-31 17:14:39 -06:00
@Gtk.Template.Callback ( )
2024-09-02 18:18:49 -06:00
def switch_run_on_background ( self , switch , user_data ) :
2024-08-31 17:14:39 -06:00
logger . debug ( " Switching run on background " )
2024-09-02 18:18:49 -06:00
self . set_hide_on_close ( switch . get_active ( ) )
2024-06-28 21:29:36 -06:00
self . save_server_config ( )
2024-09-04 23:05:16 +02:00
@Gtk.Template.Callback ( )
def switch_powersaver_warning ( self , switch , user_data ) :
logger . debug ( " Switching powersaver warning banner " )
if switch . get_active ( ) :
self . banner . set_revealed ( Gio . PowerProfileMonitor . dup_default ( ) . get_power_saver_enabled ( ) )
else :
self . banner . set_revealed ( False )
self . save_server_config ( )
2024-06-28 21:29:36 -06:00
2024-05-28 11:24:50 -06:00
@Gtk.Template.Callback ( )
def closing_app ( self , user_data ) :
2024-08-26 13:16:55 -06:00
with open ( os . path . join ( data_dir , " chats " , " selected_chat.txt " ) , ' w ' ) as f :
f . write ( self . chat_list_box . get_selected_row ( ) . chat_window . get_name ( ) )
2024-05-28 11:24:50 -06:00
if self . get_hide_on_close ( ) :
2024-07-17 21:49:55 -05:00
logger . info ( " Hiding app... " )
2024-05-28 11:24:50 -06:00
else :
2024-07-17 21:49:55 -05:00
logger . info ( " Closing app... " )
2024-08-31 17:14:39 -06:00
self . ollama_instance . stop ( )
2024-08-11 13:23:34 -06:00
self . get_application ( ) . quit ( )
2024-05-26 14:10:20 -06:00
2024-05-29 14:24:30 -06:00
@Gtk.Template.Callback ( )
def model_spin_changed ( self , spin ) :
value = spin . get_value ( )
2024-08-04 21:43:23 -06:00
if spin . get_name ( ) != " temperature " :
value = round ( value )
else :
value = round ( value , 1 )
2024-08-31 17:14:39 -06:00
if self . ollama_instance . tweaks [ spin . get_name ( ) ] != value :
self . ollama_instance . tweaks [ spin . get_name ( ) ] = value
2024-05-29 14:24:30 -06:00
self . save_server_config ( )
2024-09-02 02:38:03 -06:00
@Gtk.Template.Callback ( )
def instance_idle_timer_changed ( self , spin ) :
self . ollama_instance . idle_timer_delay = round ( spin . get_value ( ) )
self . save_server_config ( )
2024-06-01 00:07:34 -06:00
@Gtk.Template.Callback ( )
def create_model_start ( self , button ) :
2024-08-02 20:47:04 -06:00
name = self . create_model_name . get_text ( ) . lower ( ) . replace ( " : " , " " )
modelfile_buffer = self . create_model_modelfile . get_buffer ( )
modelfile_raw = modelfile_buffer . get_text ( modelfile_buffer . get_start_iter ( ) , modelfile_buffer . get_end_iter ( ) , False )
modelfile = [ " FROM {} " . format ( self . create_model_base . get_subtitle ( ) ) , " SYSTEM {} " . format ( self . create_model_system . get_text ( ) ) ]
for line in modelfile_raw . split ( ' \n ' ) :
if not line . startswith ( ' SYSTEM ' ) and not line . startswith ( ' FROM ' ) :
modelfile . append ( line )
2024-08-31 17:14:39 -06:00
threading . Thread ( target = self . model_manager . pull_model , kwargs = { " model_name " : name , " modelfile " : ' \n ' . join ( modelfile ) } ) . start ( )
2024-08-02 20:47:04 -06:00
self . navigation_view_manage_models . pop ( )
2024-06-01 00:07:34 -06:00
2024-06-03 16:31:03 -06:00
@Gtk.Template.Callback ( )
2024-06-04 12:55:31 -06:00
def override_changed ( self , entry ) :
name = entry . get_name ( )
value = entry . get_text ( )
2024-08-31 17:14:39 -06:00
if self . ollama_instance :
if value :
self . ollama_instance . overrides [ name ] = value
elif name in self . ollama_instance . overrides :
del self . ollama_instance . overrides [ name ]
if not self . ollama_instance . remote :
self . ollama_instance . reset ( )
self . save_server_config ( )
2024-06-01 00:07:34 -06:00
2024-06-03 17:01:27 -06:00
@Gtk.Template.Callback ( )
def link_button_handler ( self , button ) :
2024-08-26 13:16:55 -06:00
os . system ( f ' xdg-open " { button . get_name ( ) } " ' . replace ( " {selected_chat} " , self . chat_list_box . get_current_chat ( ) . get_name ( ) ) )
2024-06-03 17:01:27 -06:00
2024-06-29 11:10:31 -06:00
@Gtk.Template.Callback ( )
def model_search_toggle ( self , button ) :
self . model_searchbar . set_search_mode ( button . get_active ( ) )
2024-08-27 23:25:58 -06:00
self . model_manager . pulling_list . set_visible ( not button . get_active ( ) and len ( list ( self . model_manager . pulling_list ) ) > 0 )
self . model_manager . local_list . set_visible ( not button . get_active ( ) and len ( list ( self . model_manager . local_list ) ) > 0 )
2024-06-29 11:10:31 -06:00
2024-09-21 15:50:38 -06:00
@Gtk.Template.Callback ( )
def message_search_toggle ( self , button ) :
self . message_searchbar . set_search_mode ( button . get_active ( ) )
2024-06-29 11:10:31 -06:00
@Gtk.Template.Callback ( )
def model_search_changed ( self , entry ) :
2024-06-30 18:31:10 -06:00
results = 0
2024-09-11 22:33:11 -06:00
if self . model_manager :
for model in list ( self . model_manager . available_list ) :
model . set_visible ( re . search ( entry . get_text ( ) , ' {} {} {} {} {} ' . format ( model . get_name ( ) , model . model_title , model . model_author , model . model_description , ( _ ( ' image ' ) if model . image_recognition else ' ' ) ) , re . IGNORECASE ) )
if model . get_visible ( ) :
results + = 1
if entry . get_text ( ) and results == 0 :
self . no_results_page . set_visible ( True )
self . model_scroller . set_visible ( False )
else :
self . model_scroller . set_visible ( True )
self . no_results_page . set_visible ( False )
2024-06-30 18:31:10 -06:00
2024-09-21 15:50:38 -06:00
@Gtk.Template.Callback ( )
def message_search_changed ( self , entry , current_chat = None ) :
search_term = entry . get_text ( )
results = 0
if not current_chat :
current_chat = self . chat_list_box . get_current_chat ( )
if current_chat :
for key , message in current_chat . messages . items ( ) :
2024-10-06 21:42:08 -06:00
if message and message . text :
message . set_visible ( re . search ( search_term , message . text , re . IGNORECASE ) )
for block in message . content_children :
if isinstance ( block , message_widget . text_block ) :
if search_term :
highlighted_text = re . sub ( f " ( { re . escape ( search_term ) } ) " , r " <span background= ' yellow ' bgalpha= ' 30 % ' > \ 1</span> " , block . get_text ( ) , flags = re . IGNORECASE )
block . set_markup ( highlighted_text )
else :
block . set_markup ( block . get_text ( ) )
2024-09-21 15:50:38 -06:00
2024-08-31 19:09:46 -06:00
@Gtk.Template.Callback ( )
def on_clipboard_paste ( self , textview ) :
logger . debug ( " Pasting from clipboard " )
clipboard = Gdk . Display . get_default ( ) . get_clipboard ( )
clipboard . read_text_async ( None , self . cb_text_received )
clipboard . read_texture_async ( None , self . cb_image_received )
2024-08-02 16:00:47 -06:00
def convert_model_name ( self , name : str , mode : int ) - > str : # mode=0 name:tag -> Name (tag) | mode=1 Name (tag) -> name:tag
2024-08-05 23:12:22 -06:00
try :
if mode == 0 :
return " {} ( {} ) " . format ( name . split ( " : " ) [ 0 ] . replace ( " - " , " " ) . title ( ) , name . split ( " : " ) [ 1 ] )
if mode == 1 :
return " {} : {} " . format ( name . split ( " ( " ) [ 0 ] . replace ( " " , " - " ) . lower ( ) , name . split ( " ( " ) [ 1 ] [ : - 1 ] )
except Exception as e :
pass
2024-08-07 19:41:34 -06:00
def check_alphanumeric ( self , editable , text , length , position , allowed_chars ) :
new_text = ' ' . join ( [ char for char in text if char . isalnum ( ) or char in allowed_chars ] )
2024-08-04 21:43:23 -06:00
if new_text != text :
editable . stop_emission_by_name ( " insert-text " )
2024-06-01 00:07:34 -06:00
def create_model ( self , model : str , file : bool ) :
2024-08-02 20:47:04 -06:00
modelfile_buffer = self . create_model_modelfile . get_buffer ( )
modelfile_buffer . delete ( modelfile_buffer . get_start_iter ( ) , modelfile_buffer . get_end_iter ( ) )
self . create_model_system . set_text ( ' ' )
2024-06-01 00:07:34 -06:00
if not file :
2024-08-31 17:14:39 -06:00
response = self . ollama_instance . request ( " POST " , " api/show " , json . dumps ( { " name " : self . convert_model_name ( model , 1 ) } ) )
2024-07-07 20:41:25 -06:00
if response . status_code == 200 :
data = json . loads ( response . text )
2024-08-02 20:47:04 -06:00
modelfile = [ ]
2024-06-01 00:07:34 -06:00
for line in data [ ' modelfile ' ] . split ( ' \n ' ) :
if line . startswith ( ' SYSTEM ' ) :
2024-08-02 20:47:04 -06:00
self . create_model_system . set_text ( line [ len ( ' SYSTEM ' ) : ] . strip ( ) )
if not line . startswith ( ' SYSTEM ' ) and not line . startswith ( ' FROM ' ) and not line . startswith ( ' # ' ) :
modelfile . append ( line )
self . create_model_name . set_text ( self . convert_model_name ( model , 1 ) . split ( ' : ' ) [ 0 ] + " -custom " )
modelfile_buffer . insert ( modelfile_buffer . get_start_iter ( ) , ' \n ' . join ( modelfile ) , len ( ' \n ' . join ( modelfile ) . encode ( ' utf-8 ' ) ) )
else :
##TODO ERROR MESSAGE
return
2024-08-04 22:23:02 -06:00
self . create_model_base . set_subtitle ( self . convert_model_name ( model , 1 ) )
2024-06-01 00:07:34 -06:00
else :
2024-08-04 22:23:02 -06:00
self . create_model_name . set_text ( os . path . splitext ( os . path . basename ( model ) ) [ 0 ] )
self . create_model_base . set_subtitle ( model )
2024-08-02 20:47:04 -06:00
self . navigation_view_manage_models . push_by_tag ( ' model_create_page ' )
2024-06-01 00:07:34 -06:00
2024-07-07 20:24:29 -06:00
def show_toast ( self , message : str , overlay ) :
2024-07-17 21:49:55 -05:00
logger . info ( message )
2024-05-12 18:18:25 -06:00
toast = Adw . Toast (
2024-07-07 20:24:29 -06:00
title = message ,
2024-05-12 18:18:25 -06:00
timeout = 2
)
2024-05-14 00:27:02 -06:00
overlay . add_toast ( toast )
2024-05-12 18:18:25 -06:00
2024-06-30 16:56:28 -06:00
def show_notification ( self , title : str , body : str , icon : Gio . ThemedIcon = None ) :
if not self . is_active ( ) :
2024-07-17 21:49:55 -05:00
logger . info ( f " { title } , { body } " )
2024-05-18 17:28:10 -06:00
notification = Gio . Notification . new ( title )
notification . set_body ( body )
2024-08-04 21:43:23 -06:00
if icon :
notification . set_icon ( icon )
2024-05-18 17:28:10 -06:00
self . get_application ( ) . send_notification ( None , notification )
2024-05-18 16:46:20 -06:00
2024-07-08 13:08:10 -06:00
def preview_file ( self , file_path , file_type , presend_name ) :
2024-07-17 21:49:55 -05:00
logger . debug ( f " Previewing file: { file_path } " )
2024-08-26 13:16:55 -06:00
file_path = file_path . replace ( " {selected_chat} " , self . chat_list_box . get_current_chat ( ) . get_name ( ) )
if not os . path . isfile ( file_path ) :
self . show_toast ( _ ( " Missing file " ) , self . main_overlay )
return
2024-06-04 12:07:15 -06:00
content = self . get_content_of_file ( file_path , file_type )
2024-07-08 13:08:10 -06:00
if presend_name :
self . file_preview_remove_button . set_visible ( True )
2024-07-08 16:31:39 -06:00
self . file_preview_remove_button . set_name ( presend_name )
2024-07-08 13:08:10 -06:00
else :
self . file_preview_remove_button . set_visible ( False )
2024-06-04 18:53:41 -06:00
if content :
2024-07-08 13:33:02 -06:00
if file_type == ' image ' :
self . file_preview_image . set_visible ( True )
self . file_preview_text_view . set_visible ( False )
image_data = base64 . b64decode ( content )
loader = GdkPixbuf . PixbufLoader . new ( )
loader . write ( image_data )
loader . close ( )
pixbuf = loader . get_pixbuf ( )
texture = Gdk . Texture . new_for_pixbuf ( pixbuf )
self . file_preview_image . set_from_paintable ( texture )
self . file_preview_image . set_size_request ( 240 , 240 )
2024-06-24 00:18:55 -06:00
self . file_preview_dialog . set_title ( os . path . basename ( file_path ) )
2024-06-26 16:03:44 -06:00
self . file_preview_open_button . set_name ( file_path )
2024-07-08 13:33:02 -06:00
else :
self . file_preview_image . set_visible ( False )
self . file_preview_text_view . set_visible ( True )
buffer = self . file_preview_text_view . get_buffer ( )
buffer . delete ( buffer . get_start_iter ( ) , buffer . get_end_iter ( ) )
2024-08-07 20:39:46 -06:00
buffer . insert ( buffer . get_start_iter ( ) , content , len ( content . encode ( ' utf-8 ' ) ) )
2024-07-08 13:33:02 -06:00
if file_type == ' youtube ' :
self . file_preview_dialog . set_title ( content . split ( ' \n ' ) [ 0 ] )
self . file_preview_open_button . set_name ( content . split ( ' \n ' ) [ 2 ] )
elif file_type == ' website ' :
self . file_preview_open_button . set_name ( content . split ( ' \n ' ) [ 0 ] )
else :
self . file_preview_dialog . set_title ( os . path . basename ( file_path ) )
self . file_preview_open_button . set_name ( file_path )
2024-06-04 18:53:41 -06:00
self . file_preview_dialog . present ( self )
2024-06-04 12:07:15 -06:00
2024-08-26 13:16:55 -06:00
def convert_history_to_ollama ( self , chat ) :
2024-06-04 12:07:15 -06:00
messages = [ ]
2024-08-26 13:16:55 -06:00
for message_id , message in chat . messages_to_dict ( ) . items ( ) :
2024-06-04 12:07:15 -06:00
new_message = message . copy ( )
2024-08-31 22:00:42 -06:00
if ' model ' in new_message :
del new_message [ ' model ' ]
if ' date ' in new_message :
del new_message [ ' date ' ]
2024-06-04 12:07:15 -06:00
if ' files ' in message and len ( message [ ' files ' ] ) > 0 :
del new_message [ ' files ' ]
new_message [ ' content ' ] = ' '
for name , file_type in message [ ' files ' ] . items ( ) :
2024-08-31 17:14:39 -06:00
file_path = os . path . join ( data_dir , " chats " , chat . get_name ( ) , message_id , name )
2024-06-04 18:53:41 -06:00
file_data = self . get_content_of_file ( file_path , file_type )
2024-08-04 21:43:23 -06:00
if file_data :
new_message [ ' content ' ] + = f " ```[ { name } ] \n { file_data } \n ``` "
2024-06-04 12:07:15 -06:00
new_message [ ' content ' ] + = message [ ' content ' ]
if ' images ' in message and len ( message [ ' images ' ] ) > 0 :
new_message [ ' images ' ] = [ ]
for name in message [ ' images ' ] :
2024-08-31 17:14:39 -06:00
file_path = os . path . join ( data_dir , " chats " , chat . get_name ( ) , message_id , name )
2024-06-04 18:53:41 -06:00
image_data = self . get_content_of_file ( file_path , ' image ' )
2024-08-04 21:43:23 -06:00
if image_data :
new_message [ ' images ' ] . append ( image_data )
2024-06-04 12:07:15 -06:00
messages . append ( new_message )
return messages
2024-08-26 13:16:55 -06:00
def generate_chat_title ( self , message , old_chat_name ) :
2024-07-17 21:49:55 -05:00
logger . debug ( " Generating chat title " )
2024-06-28 16:30:47 -06:00
prompt = f """
Generate a title following these rules :
- The title should be based on the prompt at the end
- Keep it in the same language as the prompt
2024-06-30 23:24:29 -06:00
- The title needs to be less than 30 characters
2024-06-28 16:36:04 -06:00
- Use only alphanumeric characters and spaces
2024-06-30 17:11:40 -06:00
- Just write the title , NOTHING ELSE
2024-06-28 16:30:47 -06:00
` ` ` PROMPT
2024-06-30 16:13:36 -06:00
{ message [ ' content ' ] }
2024-06-28 16:30:47 -06:00
` ` ` """
2024-08-28 11:51:12 -06:00
current_model = self . model_manager . get_selected_model ( )
2024-06-30 15:06:02 -06:00
data = { " model " : current_model , " prompt " : prompt , " stream " : False }
2024-08-04 21:43:23 -06:00
if ' images ' in message :
data [ " images " ] = message [ ' images ' ]
2024-08-31 17:14:39 -06:00
response = self . ollama_instance . request ( " POST " , " api/generate " , json . dumps ( data ) )
2024-08-19 23:01:40 -06:00
if response . status_code == 200 :
new_chat_name = json . loads ( response . text ) [ " response " ] . strip ( ) . removeprefix ( " Title: " ) . removeprefix ( " title: " ) . strip ( ' \' " ' ) . replace ( ' \n ' , ' ' ) . title ( ) . replace ( ' \' S ' , ' \' s ' )
new_chat_name = new_chat_name [ : 50 ] + ( new_chat_name [ 50 : ] and ' ... ' )
2024-08-26 13:16:55 -06:00
self . chat_list_box . rename_chat ( old_chat_name , new_chat_name )
2024-05-12 18:18:25 -06:00
2024-05-29 14:24:30 -06:00
def save_server_config ( self ) :
2024-09-04 15:16:46 -06:00
if self . ollama_instance :
with open ( os . path . join ( config_dir , " server.json " ) , " w+ " , encoding = " utf-8 " ) as f :
data = {
' remote_url ' : self . ollama_instance . remote_url ,
' remote_bearer_token ' : self . ollama_instance . bearer_token ,
' run_remote ' : self . ollama_instance . remote ,
' local_port ' : self . ollama_instance . local_port ,
' run_on_background ' : self . background_switch . get_active ( ) ,
' powersaver_warning ' : self . powersaver_warning_switch . get_active ( ) ,
' model_tweaks ' : self . ollama_instance . tweaks ,
' ollama_overrides ' : self . ollama_instance . overrides ,
' idle_timer ' : self . ollama_instance . idle_timer_delay
}
json . dump ( data , f , indent = 6 )
2024-05-29 14:24:30 -06:00
2024-05-13 13:25:46 -06:00
def verify_connection ( self ) :
2024-07-31 21:17:31 -06:00
try :
2024-08-31 17:14:39 -06:00
response = self . ollama_instance . request ( " GET " , " api/tags " )
2024-07-31 21:17:31 -06:00
if response . status_code == 200 :
self . save_server_config ( )
2024-08-27 23:25:58 -06:00
#self.update_list_local_models()
2024-07-31 21:17:31 -06:00
return response . status_code == 200
except Exception as e :
logger . error ( e )
return False
2024-05-12 18:18:25 -06:00
2024-05-25 23:07:51 +02:00
def on_theme_changed ( self , manager , dark , buffer ) :
2024-07-17 21:49:55 -05:00
logger . debug ( " Theme changed " )
2024-05-25 23:07:51 +02:00
if manager . get_dark ( ) :
source_style = GtkSource . StyleSchemeManager . get_default ( ) . get_scheme ( ' Adwaita-dark ' )
else :
source_style = GtkSource . StyleSchemeManager . get_default ( ) . get_scheme ( ' Adwaita ' )
buffer . set_style_scheme ( source_style )
2024-08-07 20:39:46 -06:00
def switch_send_stop_button ( self , send : bool ) :
self . stop_button . set_visible ( not send )
self . send_button . set_visible ( send )
2024-05-29 14:32:57 -06:00
2024-08-31 22:00:42 -06:00
def run_message ( self , data : dict , message_element : message_widget . message , chat : chat_widget . chat ) :
2024-07-17 21:49:55 -05:00
logger . debug ( " Running message " )
2024-09-02 17:00:35 -06:00
self . save_history ( chat )
2024-08-26 13:16:55 -06:00
chat . busy = True
2024-08-31 22:00:42 -06:00
self . chat_list_box . get_tab_by_name ( chat . get_name ( ) ) . spinner . set_visible ( True )
if len ( data [ ' messages ' ] ) == 1 and chat . get_name ( ) . startswith ( _ ( " New Chat " ) ) :
threading . Thread ( target = self . generate_chat_title , args = ( data [ ' messages ' ] [ 0 ] . copy ( ) , chat . get_name ( ) ) ) . start ( )
2024-08-26 13:16:55 -06:00
if chat . welcome_screen :
chat . welcome_screen . set_visible ( False )
2024-08-26 14:05:42 -06:00
if chat . regenerate_button :
chat . container . remove ( chat . regenerate_button )
2024-08-26 13:16:55 -06:00
self . switch_send_stop_button ( False )
2024-08-02 23:42:35 -06:00
if self . regenerate_button :
2024-08-26 13:16:55 -06:00
GLib . idle_add ( self . chat_list_box . get_current_chat ( ) . remove , self . regenerate_button )
2024-08-02 23:42:35 -06:00
try :
2024-09-17 19:46:28 -06:00
response = self . ollama_instance . request ( " POST " , " api/chat " , json . dumps ( data ) , lambda data , message_element = message_element : message_element . update_message ( data ) )
2024-08-04 21:43:23 -06:00
if response . status_code != 200 :
raise Exception ( ' Network Error ' )
2024-08-02 23:42:35 -06:00
except Exception as e :
2024-09-29 16:07:12 -06:00
logger . error ( e )
2024-09-16 18:30:48 -06:00
self . chat_list_box . get_tab_by_name ( chat . get_name ( ) ) . spinner . set_visible ( False )
2024-08-26 14:05:42 -06:00
chat . busy = False
GLib . idle_add ( message_element . add_action_buttons )
2024-09-16 18:30:48 -06:00
if message_element . spinner :
GLib . idle_add ( message_element . container . remove , message_element . spinner )
message_element . spinner = None
2024-08-26 14:05:42 -06:00
GLib . idle_add ( chat . show_regenerate_button , message_element )
2024-05-21 18:52:56 -06:00
GLib . idle_add ( self . connection_error )
2024-05-12 18:18:25 -06:00
2024-08-26 13:16:55 -06:00
def save_history ( self , chat : chat_widget . chat = None ) :
2024-09-11 10:51:04 -06:00
logger . info ( " Saving history " )
2024-08-26 13:16:55 -06:00
history = None
2024-08-31 17:14:39 -06:00
if chat and os . path . exists ( os . path . join ( data_dir , " chats " , " chats.json " ) ) :
2024-08-26 13:16:55 -06:00
history = { ' chats ' : { chat . get_name ( ) : { ' messages ' : chat . messages_to_dict ( ) } } }
try :
2024-08-31 17:14:39 -06:00
with open ( os . path . join ( data_dir , " chats " , " chats.json " ) , " r " , encoding = " utf-8 " ) as f :
2024-08-26 13:16:55 -06:00
data = json . load ( f )
for chat_tab in self . chat_list_box . tab_list :
if chat_tab . chat_window . get_name ( ) != chat . get_name ( ) :
history [ ' chats ' ] [ chat_tab . chat_window . get_name ( ) ] = data [ ' chats ' ] [ chat_tab . chat_window . get_name ( ) ]
history [ ' chats ' ] [ chat . get_name ( ) ] = { ' messages ' : chat . messages_to_dict ( ) }
except Exception as e :
logger . error ( e )
history = None
if not history :
history = { ' chats ' : { } }
for chat_tab in self . chat_list_box . tab_list :
history [ ' chats ' ] [ chat_tab . chat_window . get_name ( ) ] = { ' messages ' : chat_tab . chat_window . messages_to_dict ( ) }
2024-08-07 20:39:46 -06:00
2024-08-31 17:14:39 -06:00
with open ( os . path . join ( data_dir , " chats " , " chats.json " ) , " w+ " , encoding = " utf-8 " ) as f :
2024-08-26 13:16:55 -06:00
json . dump ( history , f , indent = 4 )
2024-05-19 00:17:00 -06:00
2024-05-13 13:25:46 -06:00
def load_history ( self ) :
2024-07-17 21:49:55 -05:00
logger . debug ( " Loading history " )
2024-08-31 17:14:39 -06:00
if os . path . exists ( os . path . join ( data_dir , " chats " , " chats.json " ) ) :
2024-05-13 13:25:46 -06:00
try :
2024-08-31 17:14:39 -06:00
with open ( os . path . join ( data_dir , " chats " , " chats.json " ) , " r " , encoding = " utf-8 " ) as f :
2024-08-26 13:16:55 -06:00
data = json . load ( f )
selected_chat = None
if len ( list ( data ) ) == 0 :
data [ ' chats ' ] [ _ ( " New Chat " ) ] = { " messages " : { } }
2024-08-31 17:14:39 -06:00
if os . path . exists ( os . path . join ( data_dir , " chats " , " selected_chat.txt " ) ) :
with open ( os . path . join ( data_dir , " chats " , " selected_chat.txt " ) , ' r ' ) as scf :
2024-08-26 13:16:55 -06:00
selected_chat = scf . read ( )
elif ' selected_chat ' in data and data [ ' selected_chat ' ] in data [ ' chats ' ] :
selected_chat = data [ ' selected_chat ' ]
if not selected_chat or selected_chat not in data [ ' chats ' ] :
selected_chat = list ( data [ ' chats ' ] ) [ 0 ]
if len ( data [ ' chats ' ] [ selected_chat ] [ ' messages ' ] . keys ( ) ) > 0 :
last_model_used = data [ ' chats ' ] [ selected_chat ] [ ' messages ' ] [ list ( data [ " chats " ] [ selected_chat ] [ " messages " ] ) [ - 1 ] ] [ " model " ]
2024-08-27 23:25:58 -06:00
self . model_manager . change_model ( last_model_used )
2024-08-26 13:16:55 -06:00
for chat_name in data [ ' chats ' ] :
self . chat_list_box . append_chat ( chat_name )
chat_container = self . chat_list_box . get_chat_by_name ( chat_name )
if chat_name == selected_chat :
self . chat_list_box . select_row ( self . chat_list_box . tab_list [ - 1 ] )
chat_container . load_chat_messages ( data [ ' chats ' ] [ chat_name ] [ ' messages ' ] )
2024-08-12 22:12:09 -06:00
2024-05-13 13:25:46 -06:00
except Exception as e :
2024-07-22 22:03:44 -06:00
logger . error ( e )
2024-08-26 13:16:55 -06:00
self . chat_list_box . prepend_chat ( _ ( " New Chat " ) )
2024-06-30 17:11:40 -06:00
else :
2024-08-26 13:16:55 -06:00
self . chat_list_box . prepend_chat ( _ ( " New Chat " ) )
2024-06-30 17:11:40 -06:00
2024-05-12 18:18:25 -06:00
2024-06-04 12:07:15 -06:00
def generate_numbered_name ( self , chat_name : str , compare_list : list ) - > str :
if chat_name in compare_list :
for i in range ( len ( compare_list ) ) :
if " . " in chat_name :
if f " { ' . ' . join ( chat_name . split ( ' . ' ) [ : - 1 ] ) } { i + 1 } . { chat_name . split ( ' . ' ) [ - 1 ] } " not in compare_list :
chat_name = f " { ' . ' . join ( chat_name . split ( ' . ' ) [ : - 1 ] ) } { i + 1 } . { chat_name . split ( ' . ' ) [ - 1 ] } "
break
2024-05-17 00:35:34 -06:00
else :
2024-08-04 21:43:23 -06:00
if f " { chat_name } { i + 1 } " not in compare_list :
2024-06-04 12:07:15 -06:00
chat_name = f " { chat_name } { i + 1 } "
break
2024-05-25 23:03:26 -06:00
return chat_name
2024-06-04 12:07:15 -06:00
def generate_uuid ( self ) - > str :
return f " { datetime . today ( ) . strftime ( ' % Y % m %d % H % M % S %f ' ) } { uuid . uuid4 ( ) . hex } "
2024-05-21 18:52:56 -06:00
def connection_error ( self ) :
2024-07-17 21:49:55 -05:00
logger . error ( " Connection error " )
2024-08-31 17:14:39 -06:00
if self . ollama_instance . remote :
2024-10-10 22:14:08 -06:00
options = {
2024-10-10 22:34:14 -06:00
_ ( " 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 " }
2024-10-10 22:14:08 -06:00
}
entries = [
2024-10-10 22:34:14 -06:00
{ " 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) ' ) }
2024-10-10 22:14:08 -06:00
]
2024-10-10 22:30:52 -06:00
dialog_widget . Entry ( _ ( ' Connection Error ' ) , _ ( ' The remote instance has disconnected ' ) , list ( options ) [ 0 ] , options , entries )
2024-05-21 17:58:51 -06:00
else :
2024-08-31 17:14:39 -06:00
self . ollama_instance . reset ( )
2024-07-07 20:24:29 -06:00
self . show_toast ( _ ( " There was an error with the local Ollama instance, so it has been reset " ) , self . main_overlay )
2024-05-21 17:58:51 -06:00
2024-06-04 12:07:15 -06:00
def get_content_of_file ( self , file_path , file_type ) :
2024-06-26 14:26:41 -06:00
if not os . path . exists ( file_path ) : return None
2024-06-04 12:07:15 -06:00
if file_type == ' image ' :
try :
with Image . open ( file_path ) as img :
width , height = img . size
max_size = 240
if width > height :
new_width = max_size
new_height = int ( ( max_size / width ) * height )
else :
new_height = max_size
new_width = int ( ( max_size / height ) * width )
resized_img = img . resize ( ( new_width , new_height ) , Image . LANCZOS )
with BytesIO ( ) as output :
resized_img . save ( output , format = " PNG " )
image_data = output . getvalue ( )
return base64 . b64encode ( image_data ) . decode ( " utf-8 " )
except Exception as e :
2024-07-22 22:03:44 -06:00
logger . error ( e )
2024-07-07 20:24:29 -06:00
self . show_toast ( _ ( " Cannot open image " ) , self . main_overlay )
2024-06-30 23:24:29 -06:00
elif file_type == ' plain_text ' or file_type == ' youtube ' or file_type == ' website ' :
2024-08-04 21:11:00 -06:00
with open ( file_path , ' r ' , encoding = " utf-8 " ) as f :
2024-06-04 12:07:15 -06:00
return f . read ( )
2024-06-21 17:16:59 -06:00
elif file_type == ' pdf ' :
reader = PdfReader ( file_path )
2024-08-04 21:43:23 -06:00
if len ( reader . pages ) == 0 :
return None
2024-06-21 17:16:59 -06:00
text = " "
for i , page in enumerate ( reader . pages ) :
2024-07-10 16:20:40 -06:00
text + = f " \n - Page { i } \n { page . extract_text ( extraction_mode = ' layout ' , layout_mode_space_vertically = False ) } \n "
2024-06-21 17:16:59 -06:00
return text
2024-06-04 12:07:15 -06:00
2024-07-08 13:08:10 -06:00
def remove_attached_file ( self , name ) :
2024-07-17 21:49:55 -05:00
logger . debug ( " Removing attached file " )
2024-07-08 13:08:10 -06:00
button = self . attachments [ name ] [ ' button ' ]
2024-06-04 12:07:15 -06:00
button . get_parent ( ) . remove ( button )
2024-07-08 13:08:10 -06:00
del self . attachments [ name ]
2024-08-04 21:43:23 -06:00
if len ( self . attachments ) == 0 :
self . attachment_box . set_visible ( False )
2024-10-10 22:14:08 -06:00
if self . file_preview_dialog . get_visible ( ) :
self . file_preview_dialog . close ( )
2024-06-04 12:07:15 -06:00
def attach_file ( self , file_path , file_type ) :
2024-07-17 21:49:55 -05:00
logger . debug ( f " Attaching file: { file_path } " )
2024-07-08 13:08:10 -06:00
file_name = self . generate_numbered_name ( os . path . basename ( file_path ) , self . attachments . keys ( ) )
2024-06-04 12:07:15 -06:00
content = self . get_content_of_file ( file_path , file_type )
2024-06-04 18:53:41 -06:00
if content :
button_content = Adw . ButtonContent (
2024-07-08 13:08:10 -06:00
label = file_name ,
2024-06-21 17:16:59 -06:00
icon_name = {
" image " : " image-x-generic-symbolic " ,
" plain_text " : " document-text-symbolic " ,
" pdf " : " document-text-symbolic " ,
2024-06-30 23:24:29 -06:00
" youtube " : " play-symbolic " ,
" website " : " globe-symbolic "
2024-06-21 17:16:59 -06:00
} [ file_type ]
2024-06-04 18:53:41 -06:00
)
button = Gtk . Button (
vexpand = True ,
valign = 3 ,
2024-07-08 13:08:10 -06:00
name = file_name ,
2024-06-04 18:53:41 -06:00
css_classes = [ " flat " ] ,
2024-07-08 13:08:10 -06:00
tooltip_text = file_name ,
2024-06-04 18:53:41 -06:00
child = button_content
)
2024-07-08 13:08:10 -06:00
self . attachments [ file_name ] = { " path " : file_path , " type " : file_type , " content " : content , " button " : button }
button . connect ( " clicked " , lambda button : self . preview_file ( file_path , file_type , file_name ) )
2024-06-04 18:53:41 -06:00
self . attachment_container . append ( button )
self . attachment_box . set_visible ( True )
2024-06-04 12:07:15 -06:00
2024-06-23 20:07:27 -06:00
def chat_actions ( self , action , user_data ) :
2024-06-28 15:39:26 -06:00
chat_row = self . selected_chat_row
2024-08-31 22:00:42 -06:00
chat_name = chat_row . label . get_label ( )
2024-06-28 15:39:26 -06:00
action_name = action . get_name ( )
2024-08-05 13:50:42 -06:00
if action_name in ( ' delete_chat ' , ' delete_current_chat ' ) :
2024-10-10 22:23:53 -06:00
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 '
)
2024-08-11 22:11:17 -06:00
elif action_name in ( ' duplicate_chat ' , ' duplicate_current_chat ' ) :
2024-08-26 13:16:55 -06:00
self . chat_list_box . duplicate_chat ( chat_name )
2024-06-28 15:39:26 -06:00
elif action_name in ( ' rename_chat ' , ' rename_current_chat ' ) :
2024-10-10 22:23:53 -06:00
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 ' )
)
2024-06-28 15:39:26 -06:00
elif action_name in ( ' export_chat ' , ' export_current_chat ' ) :
2024-08-26 13:16:55 -06:00
self . chat_list_box . export_chat ( chat_name )
2024-06-23 20:07:27 -06:00
2024-06-28 15:39:26 -06:00
def current_chat_actions ( self , action , user_data ) :
self . selected_chat_row = self . chat_list_box . get_selected_row ( )
self . chat_actions ( action , user_data )
2024-06-30 17:45:03 -06:00
def cb_text_received ( self , clipboard , result ) :
try :
text = clipboard . read_text_finish ( result )
#Check if text is a Youtube URL
youtube_regex = re . compile (
r ' (https?://)?(www \ .)?(youtube|youtu|youtube-nocookie) \ .(com|be)/ '
r ' (watch \ ?v=|embed/|v/|.+ \ ?v=)?([^&= % \ ?] {11} ) ' )
2024-06-30 23:24:29 -06:00
url_regex = re . compile (
r ' http[s]?:// '
r ' (?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!* \\ ( \\ ),]| '
r ' (?: % [0-9a-fA-F][0-9a-fA-F]))+ '
r ' (?: \\ :[0-9] { 1,5})? '
r ' (?:/[^ \\ s]*)? '
)
2024-06-30 17:45:03 -06:00
if youtube_regex . match ( text ) :
try :
2024-10-10 22:14:08 -06:00
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 \n Please 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 ]
)
2024-06-30 17:45:03 -06:00
except Exception as e :
2024-07-22 22:03:44 -06:00
logger . error ( e )
2024-07-07 20:24:29 -06:00
self . show_toast ( _ ( " This video is not available " ) , self . main_overlay )
2024-06-30 23:24:29 -06:00
elif url_regex . match ( text ) :
2024-10-10 22:14:08 -06:00
dialog_widget . simple (
_ ( ' Attach Website? (Experimental) ' ) ,
_ ( " Are you sure you want to attach \n ' {} ' ? " ) . format ( text ) ,
lambda url = text : generic_actions . attach_website ( url )
)
2024-07-08 16:55:04 -06:00
except Exception as e :
2024-07-22 22:03:44 -06:00
logger . error ( e )
2024-06-30 17:45:03 -06:00
def cb_image_received ( self , clipboard , result ) :
try :
texture = clipboard . read_texture_finish ( result )
if texture :
2024-08-30 12:42:48 -06:00
if self . model_manager . verify_if_image_can_be_used ( ) :
2024-06-30 22:12:46 -06:00
pixbuf = Gdk . pixbuf_get_from_texture ( texture )
2024-08-31 17:14:39 -06:00
if not os . path . exists ( os . path . join ( cache_dir , ' tmp/images/ ' ) ) :
os . makedirs ( os . path . join ( cache_dir , ' tmp/images/ ' ) )
image_name = self . generate_numbered_name ( ' image.png ' , os . listdir ( os . path . join ( cache_dir , os . path . join ( cache_dir , ' tmp/images ' ) ) ) )
pixbuf . savev ( os . path . join ( cache_dir , ' tmp/images/ {} ' . format ( image_name ) ) , " png " , [ ] , [ ] )
self . attach_file ( os . path . join ( cache_dir , ' tmp/images/ {} ' . format ( image_name ) ) , ' image ' )
2024-06-30 22:12:46 -06:00
else :
2024-07-07 20:24:29 -06:00
self . show_toast ( _ ( " Image recognition is only available on specific models " ) , self . main_overlay )
2024-08-05 16:07:49 -06:00
except Exception as e :
pass
2024-06-24 00:18:55 -06:00
2024-08-04 22:23:02 -06:00
def handle_enter_key ( self ) :
2024-08-30 20:34:05 -06:00
self . send_message ( )
2024-08-04 22:23:02 -06:00
return True
2024-08-30 21:48:50 -06:00
def on_file_drop ( self , drop_target , value , x , y ) :
files = value . get_files ( )
for file in files :
extension = os . path . splitext ( file . get_path ( ) ) [ 1 ] [ 1 : ]
if extension in ( ' png ' , ' jpeg ' , ' jpg ' , ' webp ' , ' gif ' ) :
self . attach_file ( file . get_path ( ) , ' image ' )
elif extension in ( ' txt ' , ' md ' , ' html ' , ' css ' , ' js ' , ' py ' , ' java ' , ' json ' , ' xml ' ) :
self . attach_file ( file . get_path ( ) , ' plain_text ' )
elif extension == ' pdf ' :
self . attach_file ( file . get_path ( ) , ' pdf ' )
2024-09-04 23:05:16 +02:00
def power_saver_toggled ( self , monitor ) :
self . banner . set_revealed ( monitor . get_power_saver_enabled ( ) and self . powersaver_warning_switch . get_active ( ) )
2024-10-11 14:40:49 -06:00
def remote_switched ( self , switch , state ) :
def local_instance_process ( ) :
2024-10-11 14:46:05 -06:00
switch . set_sensitive ( False )
2024-10-11 15:01:13 -06:00
self . tweaks_group . set_sensitive ( False )
self . instance_page . set_sensitive ( False )
self . get_application ( ) . lookup_action ( ' manage_models ' ) . set_enabled ( False )
self . title_stack . set_visible_child_name ( ' loading ' )
2024-10-11 14:40:49 -06:00
self . ollama_instance . remote = False
self . ollama_instance . start ( )
self . model_manager . update_local_list ( )
self . save_server_config ( )
2024-10-11 15:01:13 -06:00
self . title_stack . set_visible_child_name ( ' model_selector ' )
self . get_application ( ) . lookup_action ( ' manage_models ' ) . set_enabled ( True )
self . tweaks_group . set_sensitive ( True )
self . instance_page . set_sensitive ( True )
2024-10-11 14:46:05 -06:00
switch . set_sensitive ( True )
2024-10-11 14:40:49 -06:00
if state :
options = {
_ ( " Cancel " ) : { " callback " : lambda * _ : self . remote_connection_switch . set_active ( False ) } ,
_ ( " Connect " ) : { " callback " : lambda url , bearer : generic_actions . connect_remote ( url , bearer ) , " appearance " : " suggested " }
}
entries = [
{ " text " : self . ollama_instance . remote_url , " placeholder " : _ ( ' Server URL ' ) } ,
{ " text " : self . ollama_instance . bearer_token , " placeholder " : _ ( ' Bearer Token (Optional) ' ) }
]
2024-10-11 14:46:05 -06:00
dialog_widget . Entry (
_ ( ' Connect Remote Instance ' ) ,
_ ( ' Enter instance information to continue ' ) ,
list ( options ) [ 0 ] ,
options ,
entries
)
elif self . ollama_instance . remote :
2024-10-11 14:40:49 -06:00
threading . Thread ( target = local_instance_process ) . start ( )
2024-09-17 21:14:58 -06:00
def prepare_alpaca ( self , local_port : int , remote_url : str , remote : bool , tweaks : dict , overrides : dict , bearer_token : str , idle_timer_delay : int , save : bool ) :
2024-10-06 21:42:08 -06:00
#Model Manager
self . model_manager = model_widget . model_manager_container ( )
self . model_scroller . set_child ( self . model_manager )
#Chat History
self . load_history ( )
2024-08-31 19:09:46 -06:00
#Instance
2024-09-02 02:38:03 -06:00
self . ollama_instance = connection_handler . instance ( local_port , remote_url , remote , tweaks , overrides , bearer_token , idle_timer_delay )
2024-08-31 18:24:53 -06:00
2024-10-06 21:42:08 -06:00
#Model Manager P.2
self . model_manager . update_available_list ( )
self . model_manager . update_local_list ( )
2024-08-31 19:09:46 -06:00
#User Preferences
2024-08-31 18:24:53 -06:00
for element in list ( list ( list ( list ( self . tweaks_group ) [ 0 ] ) [ 1 ] ) [ 0 ] ) :
if element . get_name ( ) in self . ollama_instance . tweaks :
element . set_value ( self . ollama_instance . tweaks [ element . get_name ( ) ] )
for element in list ( list ( list ( list ( self . overrides_group ) [ 0 ] ) [ 1 ] ) [ 0 ] ) :
if element . get_name ( ) in self . ollama_instance . overrides :
element . set_text ( self . ollama_instance . overrides [ element . get_name ( ) ] )
self . set_hide_on_close ( self . background_switch . get_active ( ) )
2024-10-11 14:40:49 -06:00
self . remote_connection_switch . get_activatable_widget ( ) . handler_block ( self . remote_connection_switch_handler )
2024-08-31 18:24:53 -06:00
self . remote_connection_switch . set_active ( self . ollama_instance . remote )
2024-10-11 14:40:49 -06:00
self . remote_connection_switch . get_activatable_widget ( ) . handler_unblock ( self . remote_connection_switch_handler )
2024-09-02 02:38:03 -06:00
self . instance_idle_timer . set_value ( self . ollama_instance . idle_timer_delay )
2024-08-31 19:09:46 -06:00
#Save preferences
2024-08-31 18:24:53 -06:00
if save :
self . save_server_config ( )
2024-09-17 21:14:58 -06:00
self . send_button . set_sensitive ( True )
2024-10-06 21:42:08 -06:00
self . attachment_button . set_sensitive ( True )
2024-10-11 15:01:13 -06:00
self . remote_connection_switch . set_sensitive ( True )
self . tweaks_group . set_sensitive ( True )
self . instance_page . set_sensitive ( True )
2024-10-06 21:42:08 -06:00
self . get_application ( ) . lookup_action ( ' manage_models ' ) . set_enabled ( True )
2024-08-31 18:24:53 -06:00
2024-05-12 18:18:25 -06:00
def __init__ ( self , * * kwargs ) :
super ( ) . __init__ ( * * kwargs )
2024-09-21 15:50:38 -06:00
self . message_searchbar . connect ( ' notify::search-mode-enabled ' , lambda * _ : self . message_search_button . set_active ( self . message_searchbar . get_search_mode ( ) ) )
2024-08-26 13:16:55 -06:00
message_widget . window = self
chat_widget . window = self
model_widget . window = self
2024-10-10 22:14:08 -06:00
dialog_widget . window = self
terminal_widget . window = self
generic_actions . window = self
2024-09-02 02:55:02 -06:00
connection_handler . window = self
2024-08-26 13:16:55 -06:00
2024-08-30 21:48:50 -06:00
drop_target = Gtk . DropTarget . new ( Gdk . FileList , Gdk . DragAction . COPY )
drop_target . connect ( ' drop ' , self . on_file_drop )
self . message_text_view . add_controller ( drop_target )
2024-08-26 13:16:55 -06:00
self . chat_list_box = chat_widget . chat_list ( )
self . chat_list_container . set_child ( self . chat_list_box )
2024-05-16 20:13:18 -06:00
GtkSource . init ( )
2024-08-31 17:14:39 -06:00
if not os . path . exists ( os . path . join ( data_dir , " chats " ) ) :
os . makedirs ( os . path . join ( data_dir , " chats " ) )
2024-08-11 21:48:31 -06:00
enter_key_controller = Gtk . EventControllerKey . new ( )
enter_key_controller . connect ( " key-pressed " , lambda controller , keyval , keycode , state : self . handle_enter_key ( ) if keyval == Gdk . KEY_Return and not ( state & Gdk . ModifierType . SHIFT_MASK ) else None )
self . message_text_view . add_controller ( enter_key_controller )
2024-05-19 12:10:14 -06:00
self . set_help_overlay ( self . shortcut_window )
self . get_application ( ) . set_accels_for_action ( " win.show-help-overlay " , [ ' <primary>slash ' ] )
2024-08-26 13:16:55 -06:00
universal_actions = {
' new_chat ' : [ lambda * _ : self . chat_list_box . new_chat ( ) , [ ' <primary>n ' ] ] ,
2024-10-10 22:14:08 -06:00
' 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 ' ] ] ,
2024-08-26 13:16:55 -06:00
' import_chat ' : [ lambda * _ : self . chat_list_box . import_chat ( ) , [ ' <primary>i ' ] ] ,
2024-10-11 15:11:09 -06:00
' create_model_from_existing ' : [ lambda * i : dialog_widget . simple_dropdown ( _ ( ' Select Model ' ) , _ ( ' This model will be used as the base for the new model ' ) , lambda model : self . create_model ( model , False ) , [ self . convert_model_name ( model , 0 ) for model in self . model_manager . get_model_list ( ) ] ) ] ,
2024-10-10 22:14:08 -06:00
' 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 * i : dialog_widget . simple_entry ( _ ( ' Pull Model ' ) , _ ( ' Input the name of the model in this format \n name:tag ' ) , lambda model : threading . Thread ( target = self . model_manager . pull_model , kwargs = { " model_name " : model } ) . start ( ) , { ' placeholder ' : ' llama3.2:latest ' } ) ] ,
2024-08-26 13:16:55 -06:00
' duplicate_chat ' : [ self . chat_actions ] ,
' duplicate_current_chat ' : [ self . current_chat_actions ] ,
' delete_chat ' : [ self . chat_actions ] ,
' delete_current_chat ' : [ self . current_chat_actions ] ,
' rename_chat ' : [ self . chat_actions ] ,
' rename_current_chat ' : [ self . current_chat_actions , [ ' F2 ' ] ] ,
' export_chat ' : [ self . chat_actions ] ,
' export_current_chat ' : [ self . current_chat_actions ] ,
' toggle_sidebar ' : [ lambda * _ : self . split_view_overlay . set_show_sidebar ( not self . split_view_overlay . get_show_sidebar ( ) ) , [ ' F9 ' ] ] ,
2024-09-21 15:50:38 -06:00
' manage_models ' : [ lambda * _ : self . manage_models_dialog . present ( self ) , [ ' <primary>m ' ] ] ,
' search_messages ' : [ lambda * _ : self . message_searchbar . set_search_mode ( not self . message_searchbar . get_search_mode ( ) ) , [ ' <primary>f ' ] ]
2024-08-26 13:16:55 -06:00
}
for action_name , data in universal_actions . items ( ) :
self . get_application ( ) . create_action ( action_name , data [ 0 ] , data [ 1 ] if len ( data ) > 1 else None )
2024-10-06 21:42:08 -06:00
self . get_application ( ) . lookup_action ( ' manage_models ' ) . set_enabled ( False )
2024-10-11 15:01:13 -06:00
self . remote_connection_switch . set_sensitive ( False )
self . tweaks_group . set_sensitive ( False )
self . instance_page . set_sensitive ( False )
2024-10-11 14:40:49 -06:00
self . remote_connection_switch_handler = self . remote_connection_switch . get_activatable_widget ( ) . connect ( ' state-set ' , self . remote_switched )
2024-08-26 13:16:55 -06:00
2024-10-10 22:14:08 -06:00
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 : dialog_widget . simple_file ( file_filter , generic_actions . attach_file ) )
2024-08-31 17:14:39 -06:00
self . create_model_name . get_delegate ( ) . connect ( " insert-text " , lambda * _ : self . check_alphanumeric ( * _ , [ ' - ' , ' . ' , ' _ ' ] ) )
2024-08-07 21:21:26 -06:00
self . set_focus ( self . message_text_view )
2024-08-31 17:14:39 -06:00
if os . path . exists ( os . path . join ( config_dir , " server.json " ) ) :
try :
with open ( os . path . join ( config_dir , " server.json " ) , " r " , encoding = " utf-8 " ) as f :
data = json . load ( f )
2024-09-04 23:05:16 +02:00
self . background_switch . set_active ( data [ ' run_on_background ' ] )
2024-09-02 02:38:03 -06:00
if ' idle_timer ' not in data :
data [ ' idle_timer ' ] = 0
2024-09-04 23:05:16 +02:00
if ' powersaver_warning ' not in data :
data [ ' powersaver_warning ' ] = True
self . powersaver_warning_switch . set_active ( data [ ' powersaver_warning ' ] )
2024-09-17 21:14:58 -06:00
threading . Thread ( target = self . prepare_alpaca , args = ( data [ ' local_port ' ] , data [ ' remote_url ' ] , data [ ' run_remote ' ] , data [ ' model_tweaks ' ] , data [ ' ollama_overrides ' ] , data [ ' remote_bearer_token ' ] , round ( data [ ' idle_timer ' ] ) , False ) ) . start ( )
2024-08-31 17:14:39 -06:00
except Exception as e :
logger . error ( e )
2024-09-17 21:14:58 -06:00
threading . Thread ( target = self . prepare_alpaca , args = ( 11435 , ' ' , False , { ' temperature ' : 0.7 , ' seed ' : 0 , ' keep_alive ' : 5 } , { } , ' ' , 0 , True ) ) . start ( )
2024-09-04 23:05:16 +02:00
self . powersaver_warning_switch . set_active ( True )
2024-08-31 18:24:53 -06:00
else :
2024-09-17 21:14:58 -06:00
if shutil . which ( ' ollama ' ) :
threading . Thread ( target = self . prepare_alpaca , args = ( 11435 , ' ' , False , { ' temperature ' : 0.7 , ' seed ' : 0 , ' keep_alive ' : 5 } , { } , ' ' , 0 , True ) ) . start ( )
else :
threading . Thread ( target = self . prepare_alpaca , args = ( 11435 , ' http://0.0.0.0:11434 ' , True , { ' temperature ' : 0.7 , ' seed ' : 0 , ' keep_alive ' : 5 } , { } , ' ' , 0 , True ) ) . start ( )
2024-05-21 15:36:24 -06:00
self . welcome_dialog . present ( self )
2024-09-04 23:05:16 +02:00
if self . powersaver_warning_switch . get_active ( ) :
self . banner . set_revealed ( Gio . PowerProfileMonitor . dup_default ( ) . get_power_saver_enabled ( ) )
Gio . PowerProfileMonitor . dup_default ( ) . connect ( " notify::power-saver-enabled " , lambda monitor , * _ : self . power_saver_toggled ( monitor ) )
self . banner . connect ( ' button-clicked ' , lambda * _ : self . banner . set_revealed ( False ) )