44import customtkinter as ctk
55import os
66
7+
78APP_PATH : str = os .path .dirname (os .path .realpath (__file__ ))
89THEME_PATH : str = os .path .join (APP_PATH , "theme" , "custom-theme.json" )
9- ctk .set_default_color_theme (THEME_PATH )
10-
11- url = "http://localhost:11434/api/chat"
12-
13- model_list = [
14- "llama3.1" ,
15- "llama3.2" ,
16- "llama3.3" ,
17- "gemma3" ,
18- "qwq" ,
19- "deepseek-r1" ,
20- "phi4" ,
21- "phi4-mini" ,
22- "mistral" ,
23- "moondream" ,
24- "starling-lm" ,
25- "codellama" ,
26- "llama2-uncensored" ,
27- "llava" ,
28- "granite3.2" ,
29- ]
3010
3111class GPLLicense (ctk .CTkToplevel ):
3212 def __init__ (self ):
@@ -53,16 +33,28 @@ def __init__(self):
5333You should have received a copy of the GNU General Public License
5434along with this program; if not, see <https://www.gnu.org/licenses/gpl-3.0.en.html>
5535 """ ,)
56- self .label .grid (row = 0 , column = 0 , sticky = "nsew" , padx = 10 , pady = 10 )
36+ self .label .grid (row = 0 , column = 0 , sticky = "nsew" , padx = 10 )
37+
5738
39+ error_log = []
40+ model_list = []
5841
5942class OllamaGUIChat (ctk .CTk ):
6043 def __init__ (self ):
44+
45+ try :
46+ ctk .set_default_color_theme (THEME_PATH )
47+ except Exception as e :
48+ ctk .set_default_color_theme ("blue" )
49+ print (f"Error: { e } " )
50+ error_log .append (f"Error: { e } " )
51+
6152 super ().__init__ ()
6253
6354 ctk .set_appearance_mode ("light" )
6455
6556 self .messages = []
57+ self .full_reply = ""
6658 self .gpl_opened = None
6759
6860 self .title ("Ollama GUI Chat" )
@@ -75,60 +67,96 @@ def __init__(self):
7567
7668 def setup_ui (self ):
7769
70+
7871 # top panel buttons
7972 top_frame = ctk .CTkFrame (self )
80- top_frame .grid (padx = 5 ,pady = 2 , sticky = "nswe" )
73+ top_frame .grid (row = 0 , padx = 5 ,pady = 2 , sticky = "nswe" )
8174 top_frame .configure (fg_color = "transparent" )
8275
83- #self.theme_button = ctk.CTkButton(top_frame, text="🌗Toogle theme",)
84- #self.theme_button.grid(row=0, column=1, padx=(0,3))
85- #self.theme_button.configure(height=20, font=("", 12), width=10)
86-
8776 self .gpl_button = ctk .CTkButton (top_frame , text = "GPL License" , command = self .open_gpl )
88- self .gpl_button .grid (row = 0 , column = 2 , padx = (0 ,3 ))
89- self .gpl_button .configure (height = 20 , font = ("" , 13 ), width = 10 ,)
77+ self .gpl_button .grid (row = 0 , column = 0 , padx = (0 ,3 ))
78+ self .gpl_button .configure (height = 20 , font = ("" , 13 ), width = 10 , border_width = 0 )
9079
9180 self .model_menu_var = ctk .StringVar ()
92- self .model_menu_var .set (model_list [0 ])
81+ # self.model_menu_var.set(model_list[0])
9382 self .model_menu_var .trace_add ("write" , self .on_model_change )
9483 self .model_menu = ctk .CTkOptionMenu (top_frame , variable = self .model_menu_var , values = model_list )
95- self .model_menu .grid (row = 0 , column = 3 , padx = (0 ,3 ), pady = (1 ,0 ))
84+ self .model_menu .grid (row = 0 , column = 1 , padx = (0 ,3 ), pady = (1 ,0 ))
9685 self .model_menu .configure (height = 23 , font = ("" , 12 ), dynamic_resizing = True ,)
9786
9887 self .custom_model_name = ctk .CTkEntry (top_frame ,)
99- self .custom_model_name .grid (row = 0 , column = 4 , padx = (0 ,3 ),)
88+ self .custom_model_name .grid (row = 0 , column = 2 , padx = (0 ,3 ),)
10089 self .custom_model_name .configure (height = 20 , font = ("" , 12 ), placeholder_text = "Custom model..." )
10190
91+ top_frame2 = ctk .CTkFrame (self )
92+ top_frame2 .grid (row = 0 , columnspan = 2 , padx = 5 , pady = 2 , sticky = "e" )
93+ top_frame2 .configure (fg_color = "transparent" )
94+
95+ self .host_url = ctk .CTkEntry (top_frame2 )
96+ self .host_url .grid (row = 0 , column = 0 , padx = (20 ,3 ), sticky = "e" )
97+ self .host_url .configure (height = 20 , width = 230 , font = ("" , 12 ), placeholder_text = "Host URL..." )
98+ self .host_url .insert (0 ,"http://localhost:11434" )
99+
102100 self .theme_var = ctk .StringVar (value = "off" )
103- self .theme_switch = ctk .CTkSwitch (self , text = "light/dark mode" , variable = self .theme_var , onvalue = "on" , offvalue = "off" , command = self .theme_modes )
104- self .theme_switch .grid (row = 0 , columnspan = 2 , padx = (0 ,5 ), sticky = "e" )
101+ self .theme_switch = ctk .CTkSwitch (top_frame2 , text = "light/dark mode" , variable = self .theme_var , onvalue = "on" , offvalue = "off" , command = self .theme_modes , )
102+ self .theme_switch .grid (row = 0 , column = 1 , padx = (0 ,5 ), sticky = "e" )
105103
106104 # mid panel
107- self .chat_output = ctk .CTkTextbox (self , height = 600 )
108- self .chat_output .grid (row = 1 , columnspan = 2 , padx = 5 , pady = (5 ,2 ), sticky = "nsew" ,)
109- self .chat_output .configure (wrap = "word" , state = "disabled" ,
110- font = ("" , 14 )
111- )
105+ self .chat_font_var = ctk .StringVar ()
106+ self .chat_font_var .set (value = "14" )
107+ self .chat_output = ctk .CTkTextbox (self , height = 600 , cursor = "arrow" )
108+ self .chat_output .grid (row = 1 , columnspan = 2 , padx = 5 , pady = (5 , 2 ), sticky = "nsew" ,)
109+ self .chat_output .insert ("1.0" , error_log )
110+ self .chat_output .configure (wrap = "word" , state = "disabled" , font = ("" , int (self .chat_font_var .get ())))
111+
112+ mid_frame = ctk .CTkFrame (self )
113+ mid_frame .grid (row = 2 , columnspan = 2 , sticky = "e" )
114+ mid_frame .configure (fg_color = "transparent" )
115+
116+ mid_frame2 = ctk .CTkFrame (self )
117+ mid_frame2 .grid (row = 2 , sticky = "w" )
118+ mid_frame2 .configure (fg_color = "transparent" )
119+
120+ self .save_button = ctk .CTkButton (mid_frame , text = "Save" , command = self .save_chat )
121+ self .save_button .grid (row = 2 , column = 0 , padx = 5 ,)
122+ self .save_button .configure (height = 10 , width = 15 , corner_radius = 5 , hover_color = "#024f04" , border_width = 2 )
123+
124+ self .load_button = ctk .CTkButton (mid_frame , text = "Load" , command = self .load_chat )
125+ self .load_button .grid (row = 2 , column = 1 , padx = (0 ,5 ),)
126+ self .load_button .configure (height = 10 , width = 15 , corner_radius = 5 , hover_color = "#1f538d" , border_width = 2 )
127+
128+ self .clear_button = ctk .CTkButton (mid_frame , text = "Clear" , command = self .clear_chat )
129+ self .clear_button .grid (row = 2 , column = 2 , padx = (0 ,5 ),)
130+ self .clear_button .configure (height = 10 , width = 10 , corner_radius = 5 , hover_color = "#3b0103" , border_width = 2 ,)
131+
132+ self .autoscroll_var = ctk .StringVar (value = "on" )
133+ self .autoscroll_box = ctk .CTkCheckBox (mid_frame2 , text = "Autoscroll" , variable = self .autoscroll_var , onvalue = "on" , offvalue = "off" ,)
134+ self .autoscroll_box .grid (row = 2 , column = 0 , padx = (5 ,0 ),)
135+ self .autoscroll_box .configure (font = ("" , 12 ), checkbox_width = 18 , checkbox_height = 18 )
136+
137+ self .increase_font = ctk .CTkButton (mid_frame2 , text = " + " , command = lambda : self .change_font_size (1 ))
138+ self .increase_font .grid (row = 2 ,column = 1 ,)
139+ self .increase_font .configure (width = 10 , height = 10 , corner_radius = 5 , border_width = 2 )
140+
141+ self .decrease_font = ctk .CTkButton (mid_frame2 , text = " - " , command = lambda : self .change_font_size (- 1 ))
142+ self .decrease_font .grid (row = 2 ,column = 2 , padx = (0 ,5 ))
143+ self .decrease_font .configure (width = 10 , height = 10 , corner_radius = 5 , border_width = 2 )
112144
113- self .save_button = ctk .CTkButton (self , text = "Save" , command = self .save_chat )
114- self .save_button .grid (row = 2 , column = 0 , sticky = "e" , padx = 5 )
115- self .save_button .configure (height = 10 , width = 10 , corner_radius = 5 , hover_color = "#024f04" , anchor = "center" , border_width = 2 )
116145
117- self .clear_button = ctk .CTkButton (self , text = "Clear" , command = self .clear_chat )
118- self .clear_button .grid (row = 2 , column = 1 , sticky = "e" , padx = (0 ,5 ))
119- self .clear_button .configure (height = 10 , width = 10 , corner_radius = 5 , hover_color = "#3b0103" , anchor = "center" , border_width = 2 )
146+ self .progress_bar = ctk .CTkProgressBar (mid_frame2 , mode = "determinate" ,)
147+ self .progress_bar .configure (width = 150 ,)
120148
121149 # lower panel
122150 self .input_field = ctk .CTkTextbox (self , height = 100 )
123151 self .input_field .grid (row = 3 , column = 0 , padx = 5 , pady = (5 ,5 ), sticky = "nswe" ,)
124152 self .input_field .configure (wrap = "word" ,)
125153 self .input_field .focus_set ()
126154
127- self .send_button = ctk .CTkButton (self , text = "📤 " , command = self .send_message )
155+ self .send_button = ctk .CTkButton (self , text = "Send " , command = self .send_message )
128156 self .send_button .grid (row = 3 , column = 1 , sticky = "we" , padx = (0 ,5 ), pady = (5 ,5 ))
129- self .send_button .configure (height = 100 , width = 10 , corner_radius = 5 , anchor = "center" , border_width = 2 )
157+ self .send_button .configure (height = 100 , width = 10 , corner_radius = 5 , border_width = 2 )
130158
131- self .copyright_label = ctk .CTkLabel (self , text = "Copyright (c) 2025 by Kamil Wiśniewski" )
159+ self .copyright_label = ctk .CTkLabel (self , text = "Copyright © 2025 by Kamil Wiśniewski | Ver. 1.5 " )
132160 self .copyright_label .grid (sticky = "se" , row = 4 , column = 0 , columnspan = 2 , padx = 5 )
133161 self .copyright_label .configure (font = ("" , 10 ))
134162
@@ -140,11 +168,14 @@ def setup_ui(self):
140168 self .input_field .bind ("<Return>" , self .keybinds )
141169
142170
171+ self .check_existing_models ()
172+
143173 # functions
144174 def keybinds (self , event ):
145175 if event .keysym == "Return" and not (event .state & 0x1 ): # Enter
146- self .send_message ()
147- return "break"
176+ if self .send_button .cget ("state" ) == "normal" :
177+ self .send_message ()
178+ return "break"
148179
149180 elif event .keysym == "Return" and (event .state & 0x1 ): # Shift+Enter
150181 self .input_field .insert ("end" , "\n " )
@@ -154,11 +185,28 @@ def keybinds(self, event):
154185 self .input_field .tag_add ("sel" , "1.0" , "end" )
155186 return "break"
156187
188+ def change_font_size (self , increment ):
189+ font_size = int (self .chat_font_var .get ()) + increment
190+ if font_size < 6 :
191+ font_size = 6
192+ elif font_size > 48 :
193+ font_size = 48
194+
195+ self .chat_font_var .set (value = f"{ font_size } " )
196+ self .chat_output .configure (font = ("" , font_size ))
197+
157198 def insert_text (self , content ):
158199 self .chat_output .configure (state = "normal" )
159200 self .chat_output .insert ("end" , content )
160201 self .chat_output .configure (state = "disabled" )
161202
203+ try :
204+ if self .autoscroll_var .get () == "on" :
205+ self .chat_output .see ("end" )
206+
207+ except Exception as e :
208+ print (f"Error: { e } " )
209+
162210 def on_model_change (self , * args ):
163211 self .messages = []
164212 self .insert_text (f"\n Model changed to: { self .model_menu .get ()} \n \n " )
@@ -202,7 +250,39 @@ def save_chat(self):
202250
203251 if file_path :
204252 with open (file_path , "w" ) as f :
205- f .write (self .chat_output .get ("1.0" , "end" ))
253+ for message in self .messages :
254+ f .write (f"{ message ['role' ]} : { message ['content' ]} \n " )
255+
256+ def load_chat (self ):
257+ file_path = filedialog .askopenfilename (filetypes = [("Text files" , "*.txt" ), ("All files" , "*.*" )])
258+ with open (file_path , "r" ) as f :
259+ for line in f .readlines ():
260+ self .messages .append ({"role" : "assistant" , "content" : line })
261+
262+ def show_progress (self ):
263+ self .progress_bar .grid (row = 2 , column = 3 , sticky = "we" )
264+ self .progress_bar .start ()
265+
266+ def hide_progress (self ):
267+ self .progress_bar .stop ()
268+ self .progress_bar .grid_forget ()
269+
270+ # checks for installed LLMs.
271+ def check_existing_models (self ) -> None :
272+ try :
273+ api_url = self .host_url .get ().rstrip ('/api/chat' )
274+ response = requests .get (
275+ url = api_url + "/api/tags"
276+ )
277+ response .raise_for_status ()
278+ data = response .json ()
279+ model_list .extend ([model ["name" ] for model in data ["models" ]])
280+ self .model_menu_var .set (model_list [0 ])
281+ self .refresh_model_list ()
282+
283+ except requests .exceptions .RequestException as e :
284+ print (f"Error while checking models: { e } " )
285+ self .insert_text (f"Error while checking models: { e } \n \n " )
206286
207287 def send_message (self ):
208288 question = self .input_field .get ("1.0" , "end" ).strip ()
@@ -211,34 +291,38 @@ def send_message(self):
211291
212292 model_name = self .model_menu_var .get ()
213293
214- self .insert_text (f"You: { question } \n \n " )
294+ self .insert_text (f"[ You] : \n { question } \n \n " )
215295 self .input_field .delete ("1.0" , "end" ) # clear input field
216296
217297 self .messages .append ({"role" : "user" , "content" : question })
218298 self .send_button .configure (state = "disabled" ) # disable send button
219299
300+
220301 payload = {
221302 "model" : model_name ,
222303 "messages" : self .messages ,
223304 "stream" : True
224305 }
225306
307+ url = f"{ self .host_url .get ().rstrip ('/api/chat' )} /api/chat"
308+
226309 try :
227310 response = requests .post (url , json = payload , stream = True )
228311
229312 if response .status_code == 200 :
230- self .insert_text ("AI: " )
231- self .update ()
313+ self .insert_text ("[AI] : \n " )
314+ # self.update()
232315
233316 # full_reply "", needed for AI to remember the context of messages.
234317 # Without this, every new message is considered as new chat or whatever.
235- full_reply = ""
318+ full_reply = ""
236319 for line in response .iter_lines (decode_unicode = True ):
237320 if line .strip ():
238321 try :
239322 data = json .loads (line )
240323 content = data .get ("message" , {}).get ("content" , "" )
241324 full_reply += content
325+ self .show_progress ()
242326 self .insert_text (content )
243327 self .update ()
244328
@@ -255,6 +339,7 @@ def send_message(self):
255339 self .insert_text (f"\n Error sending request: { e } \n \n " )
256340
257341 self .send_button .configure (state = "normal" )
342+ self .hide_progress ()
258343
259344ollama_gui = OllamaGUIChat ()
260345ollama_gui .mainloop ()
0 commit comments