33import json
44import customtkinter as ctk
55import os
6+ import threading as thr
67
78APP_PATH : str = os .path .dirname (os .path .realpath (__file__ ))
89THEME_PATH : str = os .path .join (APP_PATH , "theme" , "custom-theme.json" )
@@ -119,7 +120,6 @@ def setup_ui(self):
119120 self .chat_font_var .set (value = "14" )
120121 self .chat_output = ctk .CTkTextbox (self , height = 600 , cursor = "arrow" )
121122 self .chat_output .grid (row = 1 , columnspan = 2 , padx = 5 , pady = (5 , 2 ), sticky = "nsew" ,)
122- #self.chat_output.insert("1.0", error_log)
123123 self .chat_output .configure (wrap = "word" , state = "disabled" , font = ("" , int (self .chat_font_var .get ())))
124124
125125 mid_frame = ctk .CTkFrame (self )
@@ -159,19 +159,19 @@ def setup_ui(self):
159159 self .progress_bar = ctk .CTkProgressBar (mid_frame2 , mode = "determinate" ,)
160160 self .progress_bar .configure (width = 150 ,)
161161
162- self .stop_button = ctk .CTkButton (mid_frame2 , text = "🛑 " , command = self .stop_response )
162+ self .stop_button = ctk .CTkButton (mid_frame2 , text = "Stop " , command = self .stop_response )
163163
164164 # lower panel
165165 self .input_field = ctk .CTkTextbox (self , height = 100 )
166166 self .input_field .grid (row = 3 , column = 0 , padx = 5 , pady = (5 ,5 ), sticky = "nswe" ,)
167167 self .input_field .configure (wrap = "word" ,)
168168 self .input_field .focus_set ()
169169
170- self .send_button = ctk .CTkButton (self , text = "📤" , command = self .send_message )
170+ self .send_button = ctk .CTkButton (self , text = "📤" , command = self .send_message_thread )
171171 self .send_button .grid (row = 3 , column = 1 , sticky = "we" , padx = (0 ,5 ), pady = (5 ,5 ))
172172 self .send_button .configure (height = 100 , width = 10 , corner_radius = 5 , border_width = 2 )
173173
174- self .copyright_label = ctk .CTkLabel (self , text = "Copyright © 2025 by Kamil Wiśniewski | Ver. 1.5 " )
174+ self .copyright_label = ctk .CTkLabel (self , text = "Copyright © 2025 by Kamil Wiśniewski | Ver. 1.6.0 " )
175175 self .copyright_label .grid (sticky = "se" , row = 4 , column = 0 , columnspan = 2 , padx = 5 )
176176 self .copyright_label .configure (font = ("" , 10 ))
177177
@@ -181,15 +181,15 @@ def setup_ui(self):
181181 self .custom_model_name .bind ("<Return>" , self .custom_model )
182182 self .input_field .bind ("<Control-a>" , self .keybinds )
183183 self .input_field .bind ("<Return>" , self .keybinds )
184-
184+ self . host_url . bind ( "<Return>" , self . check_existing_models )
185185
186186 self .check_existing_models ()
187187
188188 # functions
189189 def keybinds (self , event ):
190190 if event .keysym == "Return" and not (event .state & 0x1 ): # Enter
191191 if self .send_button .cget ("state" ) == "normal" :
192- self .send_message ()
192+ self .send_message_thread ()
193193 return "break"
194194
195195 elif event .keysym == "Return" and (event .state & 0x1 ): # Shift+Enter
@@ -220,7 +220,7 @@ def insert_text(self, content):
220220 self .chat_output .see ("end" )
221221
222222 except Exception as e :
223- error_log .append (f"Error : { e } " )
223+ error_log .append (f"Autoscroll error : { e } " )
224224 self .open_error_logs ()
225225
226226 def on_model_change (self , * args ):
@@ -234,6 +234,7 @@ def custom_model(self, event):
234234 return
235235
236236 self .custom_model_name .delete ("0" , "end" )
237+ self .check_existing_models ()
237238 model_list .append (cModel )
238239 self .refresh_model_list ()
239240 self .model_menu_var .set (cModel )
@@ -301,10 +302,9 @@ def stop_response(self):
301302 last_key = len (self .messages ) - 1
302303
303304 if hasattr (self , "response" ) and self .response :
304- self .response .close ()
305- del self .response
305+ self .response = None
306306 self .messages .pop (last_key )
307- self .insert_text ("\n \n [AI] : Chat stopped. \n \n " )
307+ self .insert_text ("\n \n [AI] : Response canceled. " )
308308
309309 self .hide_progress ()
310310 self .send_button .configure (state = "normal" )
@@ -324,7 +324,8 @@ def hide_progress(self):
324324 self .stop_button .configure (state = "disabled" )
325325
326326 # checks for installed LLMs.
327- def check_existing_models (self ) -> None :
327+ def check_existing_models (self , * args ) -> None :
328+ model_list .clear ()
328329 try :
329330 api_url = self .host_url .get ().rstrip ('/api/chat' )
330331 response = requests .get (
@@ -337,22 +338,31 @@ def check_existing_models(self) -> None:
337338 self .refresh_model_list ()
338339
339340 except requests .exceptions .RequestException as e :
340- error_log .append (f"def check_existing_models: { e } " )
341+ model_list .clear ()
342+ self .model_menu_var .set ("" )
343+ error_log .append (f"Failed to fetch models: { e } " )
341344 self .open_error_logs ()
342345
343- def send_message (self ):
346+ # threading for app to not freeze on first message.
347+ # It used to freeze for a while when Ollama was setting up a server
348+ def send_message_thread (self ):
344349 question = self .input_field .get ("1.0" , "end" ).strip ()
345350 if not question :
346351 return
347352
348- model_name = self .model_menu_var .get ()
349-
350353 self .insert_text (f"[You] :\n { question } \n \n " )
351354 self .input_field .delete ("1.0" , "end" ) # clear input field
352355
353356 self .messages .append ({"role" : "user" , "content" : question })
354357 self .send_button .configure (state = "disabled" ) # disable send button
355358
359+ t1 = thr .Thread (target = self .send_message )
360+ t1 .start ()
361+
362+ self .show_progress ()
363+
364+ def send_message (self ):
365+ model_name = self .model_menu_var .get ()
356366
357367 payload = {
358368 "model" : model_name ,
@@ -368,15 +378,16 @@ def send_message(self):
368378
369379 if response .status_code == 200 :
370380 self .insert_text ("[AI] :\n " )
371- self .show_progress ()
372- #self.update()
373381
374382 # full_reply "", needed for AI to remember the context of messages.
375383 # Without this, every new message is considered as new chat or whatever.
376384 full_reply = ""
377385 try :
378386 for line in response .iter_lines (decode_unicode = True ):
379- if line .strip ():
387+ if self .response is None : # stop response
388+ break
389+
390+ elif line .strip ():
380391 try :
381392 data = json .loads (line )
382393 content = data .get ("message" , {}).get ("content" , "" )
@@ -385,28 +396,28 @@ def send_message(self):
385396 self .update ()
386397
387398 except json .JSONDecodeError :
388- error_log .append (f"def send_message : { line } " )
399+ error_log .append (f"Failed to Decode line : { line } " )
389400 self .open_error_logs ()
390401 continue
391402
392403 self .messages .append ({"role" : "assistant" , "content" : full_reply })
393404 self .insert_text ("\n \n " )
394405
395- # YES, IT WILL SCREAM AN ERROR EACH TIME YOU CANCEL THE CHAT.
396406 except Exception as e :
397- error_log .append (f"def send_message : { e } " )
398- # self.open_error_logs() # Yes, thats why this is commented out. How would you feel if someone rips your tongue off mid-sentence?
407+ error_log .append (f"Failed to get response : { e } " )
408+ self .open_error_logs ()
399409
400410 else :
401- error_log .append (f"def send_message : { response .status_code } " )
411+ error_log .append (f"Failed to send message, Status Code : { response .status_code } " )
402412 self .open_error_logs ()
403413
404414 except requests .exceptions .RequestException as e :
405- error_log .append (f"def send_message : { e } " )
415+ error_log .append (f"Request error : { e } " )
406416 self .open_error_logs ()
407417
408- self .send_button .configure (state = "normal" )
409- self .hide_progress ()
418+ finally :
419+ self .send_button .configure (state = "normal" )
420+ self .hide_progress ()
410421
411422ollama_gui = OllamaGUIChat ()
412423ollama_gui .mainloop ()
0 commit comments