fork download
  1. import sys
  2. import json
  3. import time
  4. import pprint
  5. import platform
  6. import webbrowser
  7.  
  8. import urllib.parse
  9. import urllib.request
  10.  
  11. from threading import Thread
  12. from typing import Optional, List, Generator
  13.  
  14. try:
  15. import tkinter as tk
  16. from tkinter import ttk, font, messagebox, filedialog
  17.  
  18. except (ModuleNotFoundError, ImportError):
  19. print(
  20. "Your Python installation does not include the Tk library. \n"
  21. "Please refer to https://g...content-available-to-author-only...b.com/chyok/ollama-gui?tab=readme-ov-file#-qa")
  22. sys.exit(0)
  23.  
  24. __version__ = "1.2.2"
  25.  
  26.  
  27. def _system_check(root: tk.Tk) -> Optional[str]:
  28. """
  29. Detected some system and software compatibility issues,
  30. and returned the information in the form of a string to alert the user
  31.  
  32. :param root: Tk instance
  33. :return: None or message string
  34. """
  35.  
  36. def _version_tuple(v):
  37. """A lazy way to avoid importing third-party libraries"""
  38. filled = []
  39. for point in v.split("."):
  40. filled.append(point.zfill(8))
  41. return tuple(filled)
  42.  
  43. # Tcl and macOS issue: https://g...content-available-to-author-only...b.com/python/cpython/issues/110218
  44. if platform.system().lower() == "darwin":
  45. version = platform.mac_ver()[0]
  46. if version and 14 <= float(version) < 15:
  47. tcl_version = root.tk.call("info", "patchlevel")
  48. if _version_tuple(tcl_version) <= _version_tuple("8.6.12"):
  49. return (
  50. "Warning: Tkinter Responsiveness Issue Detected\n\n"
  51. "You may experience unresponsive GUI elements when "
  52. "your cursor is inside the window during startup. "
  53. "This is a known issue with Tcl/Tk versions 8.6.12 "
  54. "and older on macOS Sonoma.\n\nTo resolve this:\n"
  55. "Update to Python 3.11.7+ or 3.12+\n"
  56. "Or install Tcl/Tk 8.6.13 or newer separately\n\n"
  57. "Temporary workaround: Move your cursor out of "
  58. "the window and back in if elements become unresponsive.\n\n"
  59. "For more information, visit: https://g...content-available-to-author-only...b.com/python/cpython/issues/110218"
  60. )
  61.  
  62.  
  63. class OllamaInterface:
  64. chat_box: tk.Text
  65. user_input: tk.Text
  66. host_input: ttk.Entry
  67. progress: ttk.Progressbar
  68. stop_button: ttk.Button
  69. send_button: ttk.Button
  70. refresh_button: ttk.Button
  71. download_button: ttk.Button
  72. delete_button: ttk.Button
  73. model_select: ttk.Combobox
  74. log_textbox: tk.Text
  75. models_list: tk.Listbox
  76.  
  77. def __init__(self, root: tk.Tk):
  78. self.root: tk.Tk = root
  79. self.api_url: str = "http://127.0.0.1:11434"
  80. self.chat_history: List[dict] = []
  81. self.label_widgets: List[tk.Label] = []
  82. self.default_font: str = font.nametofont("TkTextFont").actual()["family"]
  83.  
  84. self.layout = LayoutManager(self)
  85. self.layout.init_layout()
  86.  
  87. self.root.after(200, self.check_system)
  88. self.refresh_models()
  89.  
  90. def copy_text(self, text: str):
  91. if text:
  92. self.chat_box.clipboard_clear()
  93. self.chat_box.clipboard_append(text)
  94.  
  95. def copy_all(self):
  96. self.copy_text(pprint.pformat(self.chat_history))
  97.  
  98. @staticmethod
  99. def open_homepage():
  100. webbrowser.open("https://g...content-available-to-author-only...b.com/chyok/ollama-gui")
  101.  
  102. def show_help(self):
  103. info = ("Project: Ollama GUI\n"
  104. f"Version: {__version__}\n"
  105. "Author: chyok\n"
  106. "Github: https://g...content-available-to-author-only...b.com/chyok/ollama-gui\n\n"
  107. "<Enter>: send\n"
  108. "<Shift+Enter>: new line\n"
  109. "<Double click dialog>: edit dialog\n")
  110. messagebox.showinfo("About", info, parent=self.root)
  111.  
  112. def check_system(self):
  113. message = _system_check(self.root)
  114. if message is not None:
  115. messagebox.showwarning("Warning", message, parent=self.root)
  116.  
  117. def append_text_to_chat(self,
  118. text: str,
  119. *args,
  120. use_label: bool = False):
  121. self.chat_box.config(state=tk.NORMAL)
  122. if use_label:
  123. cur_label_widget = self.label_widgets[-1]
  124. cur_label_widget.config(text=cur_label_widget.cget("text") + text)
  125. else:
  126. self.chat_box.insert(tk.END, text, *args)
  127. self.chat_box.see(tk.END)
  128. self.chat_box.config(state=tk.DISABLED)
  129.  
  130. def append_log_to_inner_textbox(self,
  131. message: Optional[str] = None,
  132. clear: bool = False):
  133. if self.log_textbox.winfo_exists():
  134. self.log_textbox.config(state=tk.NORMAL)
  135. if clear:
  136. self.log_textbox.delete(1.0, tk.END)
  137. elif message:
  138. self.log_textbox.insert(tk.END, message + "\n")
  139. self.log_textbox.config(state=tk.DISABLED)
  140. self.log_textbox.see(tk.END)
  141.  
  142. def resize_inner_text_widget(self, event: tk.Event):
  143. for i in self.label_widgets:
  144. current_width = event.widget.winfo_width()
  145. max_width = int(current_width) * 0.7
  146. i.config(wraplength=max_width)
  147.  
  148. def show_error(self, text):
  149. self.model_select.set(text)
  150. self.model_select.config(foreground="red")
  151. self.model_select["values"] = []
  152. self.send_button.state(["disabled"])
  153.  
  154. def show_process_bar(self):
  155. self.progress.grid(row=0, column=0, sticky="nsew")
  156. self.stop_button.grid(row=0, column=1, padx=20)
  157. self.progress.start(5)
  158.  
  159. def hide_process_bar(self):
  160. self.progress.stop()
  161. self.stop_button.grid_remove()
  162. self.progress.grid_remove()
  163.  
  164. def handle_key_press(self, event: tk.Event):
  165. if event.keysym == "Return":
  166. if event.state & 0x1 == 0x1: # Shift key is pressed
  167. self.user_input.insert("end", "\n")
  168. elif "disabled" not in self.send_button.state():
  169. self.on_send_button(event)
  170. return "break"
  171.  
  172. def refresh_models(self):
  173. self.update_host()
  174. self.model_select.config(foreground="black")
  175. self.model_select.set("Waiting...")
  176. self.send_button.state(["disabled"])
  177. self.refresh_button.state(["disabled"])
  178. Thread(target=self.update_model_select, daemon=True).start()
  179.  
  180. def update_host(self):
  181. self.api_url = self.host_input.get()
  182.  
  183. def update_model_select(self):
  184. try:
  185. models = self.fetch_models()
  186. self.model_select["values"] = models
  187. if models:
  188. self.model_select.set(models[0])
  189. self.send_button.state(["!disabled"])
  190. else:
  191. self.show_error("You need download a model!")
  192. except Exception: # noqa
  193. self.show_error("Error! Please check the host.")
  194. finally:
  195. self.refresh_button.state(["!disabled"])
  196.  
  197. def update_model_list(self):
  198. if self.models_list.winfo_exists():
  199. self.models_list.delete(0, tk.END)
  200. try:
  201. models = self.fetch_models()
  202. for model in models:
  203. self.models_list.insert(tk.END, model)
  204. except Exception: # noqa
  205. self.append_log_to_inner_textbox("Error! Please check the Ollama host.")
  206.  
  207. def on_send_button(self, _=None):
  208. message = self.user_input.get("1.0", "end-1c")
  209. if message:
  210. self.layout.create_inner_label(on_right_side=True)
  211. self.append_text_to_chat(f"{message}", use_label=True)
  212. self.append_text_to_chat(f"\n\n")
  213. self.user_input.delete("1.0", "end")
  214. self.chat_history.append({"role": "user", "content": message})
  215.  
  216. Thread(
  217. target=self.generate_ai_response,
  218. daemon=True,
  219. ).start()
  220.  
  221. def generate_ai_response(self):
  222. self.show_process_bar()
  223. self.send_button.state(["disabled"])
  224. self.refresh_button.state(["disabled"])
  225.  
  226. try:
  227. self.append_text_to_chat(f"{self.model_select.get()}\n", ("Bold",))
  228. ai_message = ""
  229. self.layout.create_inner_label()
  230. for i in self.fetch_chat_stream_result():
  231. self.append_text_to_chat(f"{i}", use_label=True)
  232. ai_message += i
  233. self.chat_history.append({"role": "assistant", "content": ai_message})
  234. self.append_text_to_chat("\n\n")
  235. except Exception: # noqa
  236. self.append_text_to_chat(tk.END, f"\nAI error!\n\n", ("Error",))
  237. finally:
  238. self.hide_process_bar()
  239. self.send_button.state(["!disabled"])
  240. self.refresh_button.state(["!disabled"])
  241. self.stop_button.state(["!disabled"])
  242.  
  243. def fetch_models(self) -> List[str]:
  244. with urllib.request.urlopen(
  245. urllib.parse.urljoin(self.api_url, "/api/tags")
  246. ) as response:
  247. data = json.load(response)
  248. models = [model["name"] for model in data["models"]]
  249. return models
  250.  
  251. def fetch_chat_stream_result(self) -> Generator:
  252. request = urllib.request.Request(
  253. urllib.parse.urljoin(self.api_url, "/api/chat"),
  254. data=json.dumps(
  255. {
  256. "model": self.model_select.get(),
  257. "messages": self.chat_history,
  258. "stream": True,
  259. }
  260. ).encode("utf-8"),
  261. headers={"Content-Type": "application/json"},
  262. method="POST",
  263. )
  264.  
  265. with urllib.request.urlopen(request) as resp:
  266. for line in resp:
  267. if "disabled" in self.stop_button.state(): # stop
  268. break
  269. data = json.loads(line.decode("utf-8"))
  270. if "message" in data:
  271. time.sleep(0.01)
  272. yield data["message"]["content"]
  273.  
  274. def delete_model(self, model_name: str):
  275. self.append_log_to_inner_textbox(clear=True)
  276. if not model_name:
  277. return
  278.  
  279. req = urllib.request.Request(
  280. urllib.parse.urljoin(self.api_url, "/api/delete"),
  281. data=json.dumps({"name": model_name}).encode("utf-8"),
  282. method="DELETE",
  283. )
  284. try:
  285. with urllib.request.urlopen(req) as response:
  286. if response.status == 200:
  287. self.append_log_to_inner_textbox("Model deleted successfully.")
  288. elif response.status == 404:
  289. self.append_log_to_inner_textbox("Model not found.")
  290. except Exception as e:
  291. self.append_log_to_inner_textbox(f"Failed to delete model: {e}")
  292. finally:
  293. self.update_model_list()
  294. self.update_model_select()
  295.  
  296. def download_model(self, model_name: str, insecure: bool = False):
  297. self.append_log_to_inner_textbox(clear=True)
  298. if not model_name:
  299. return
  300.  
  301. self.download_button.state(["disabled"])
  302.  
  303. req = urllib.request.Request(
  304. urllib.parse.urljoin(self.api_url, "/api/pull"),
  305. data=json.dumps(
  306. {"name": model_name, "insecure": insecure, "stream": True}
  307. ).encode("utf-8"),
  308. method="POST",
  309. )
  310. try:
  311. with urllib.request.urlopen(req) as response:
  312. for line in response:
  313. data = json.loads(line.decode("utf-8"))
  314. log = data.get("error") or data.get("status") or "No response"
  315. if "status" in data:
  316. total = data.get("total")
  317. completed = data.get("completed", 0)
  318. if total:
  319. log += f" [{completed}/{total}]"
  320. self.append_log_to_inner_textbox(log)
  321. except Exception as e:
  322. self.append_log_to_inner_textbox(f"Failed to download model: {e}")
  323. finally:
  324. self.update_model_list()
  325. self.update_model_select()
  326. if self.download_button.winfo_exists():
  327. self.download_button.state(["!disabled"])
  328.  
  329. def clear_chat(self):
  330. for i in self.label_widgets:
  331. i.destroy()
  332. self.label_widgets.clear()
  333. self.chat_box.config(state=tk.NORMAL)
  334. self.chat_box.delete(1.0, tk.END)
  335. self.chat_box.config(state=tk.DISABLED)
  336. self.chat_history.clear()
  337.  
  338. def save_chat(self):
  339. filepath = filedialog.asksaveasfilename(
  340. defaultextension=".json",
  341. filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
  342. title="Save Chat As"
  343. )
  344. if filepath:
  345. with open(filepath, 'w', encoding='utf-8') as f:
  346. json.dump(self.chat_history, f, indent=2)
  347.  
  348. def load_chat(self):
  349. filepath = filedialog.askopenfilename(
  350. filetypes=[("JSON files", "*.json"), ("All files", "*.*")],
  351. title="Load Chat"
  352. )
  353. if not filepath:
  354. return
  355.  
  356. with open(filepath, 'r', encoding='utf-8') as f:
  357. try:
  358. history = json.load(f)
  359. if isinstance(history, list) and all("role" in item and "content" in item for item in history):
  360. self.clear_chat()
  361. self.chat_history = history
  362.  
  363. # Rebuild chat display
  364. for message in self.chat_history:
  365. if message["role"] != "user":
  366. self.append_text_to_chat(f"{self.model_select.get()}\n", ("Bold",))
  367. self.layout.create_inner_label(on_right_side=False)
  368. else:
  369. self.layout.create_inner_label(on_right_side=True)
  370.  
  371. self.append_text_to_chat(message["content"], use_label=True)
  372. self.append_text_to_chat("\n\n")
  373.  
  374. else:
  375. messagebox.showerror("Error", "Invalid chat file format.", parent=self.root)
  376. except json.JSONDecodeError:
  377. messagebox.showerror("Error", "Could not decode JSON from file.", parent=self.root)
  378.  
  379.  
  380. class LayoutManager:
  381. """
  382. Manages the layout and arrangement of the OllamaInterface.
  383.  
  384. The LayoutManager is responsible for the visual organization and positioning
  385. of the various components within the OllamaInterface, such as the header,
  386. chat container, progress bar, and input fields. It handles the sizing,
  387. spacing, and alignment of these elements to create a cohesive and
  388. user-friendly layout.
  389. """
  390.  
  391. def __init__(self, interface: OllamaInterface):
  392. self.interface: OllamaInterface = interface
  393. self.management_window: Optional[tk.Toplevel] = None
  394. self.editor_window: Optional[tk.Toplevel] = None
  395.  
  396. def init_layout(self):
  397. self._header_frame()
  398. self._chat_container_frame()
  399. self._processbar_frame()
  400. self._input_frame()
  401.  
  402. def _header_frame(self):
  403. header_frame = ttk.Frame(self.interface.root)
  404. header_frame.grid(row=0, column=0, sticky="ew", padx=20, pady=20)
  405. header_frame.grid_columnconfigure(3, weight=1)
  406.  
  407. model_select = ttk.Combobox(header_frame, state="readonly", width=30)
  408. model_select.grid(row=0, column=0)
  409.  
  410. settings_button = ttk.Button(
  411. header_frame, text="⚙️", command=self.show_model_management_window, width=3
  412. )
  413. settings_button.grid(row=0, column=1, padx=(5, 0))
  414.  
  415. refresh_button = ttk.Button(header_frame, text="Refresh", command=self.interface.refresh_models)
  416. refresh_button.grid(row=0, column=2, padx=(5, 0))
  417.  
  418. ttk.Label(header_frame, text="Host:").grid(row=0, column=4, padx=(10, 0))
  419.  
  420. host_input = ttk.Entry(header_frame, width=24)
  421. host_input.grid(row=0, column=5, padx=(5, 15))
  422. host_input.insert(0, self.interface.api_url)
  423.  
  424. self.interface.model_select = model_select
  425. self.interface.refresh_button = refresh_button
  426. self.interface.host_input = host_input
  427.  
  428. def _chat_container_frame(self):
  429. chat_frame = ttk.Frame(self.interface.root)
  430. chat_frame.grid(row=1, column=0, sticky="nsew", padx=20)
  431. chat_frame.grid_columnconfigure(0, weight=1)
  432. chat_frame.grid_rowconfigure(0, weight=1)
  433.  
  434. chat_box = tk.Text(
  435. chat_frame,
  436. wrap=tk.WORD,
  437. state=tk.DISABLED,
  438. font=(self.interface.default_font, 12),
  439. spacing1=5,
  440. highlightthickness=0,
  441. )
  442. chat_box.grid(row=0, column=0, sticky="nsew")
  443.  
  444. scrollbar = ttk.Scrollbar(chat_frame, orient="vertical", command=chat_box.yview)
  445. scrollbar.grid(row=0, column=1, sticky="ns")
  446.  
  447. chat_box.configure(yscrollcommand=scrollbar.set)
  448.  
  449. chat_box_menu = tk.Menu(chat_box, tearoff=0)
  450. chat_box_menu.add_command(label="Copy All", command=self.interface.copy_all)
  451. chat_box_menu.add_separator()
  452. chat_box_menu.add_command(label="Clear Chat", command=self.interface.clear_chat)
  453. chat_box.bind("<Configure>", self.interface.resize_inner_text_widget)
  454.  
  455. _right_click = (
  456. "<Button-2>" if platform.system().lower() == "darwin" else "<Button-3>"
  457. )
  458. chat_box.bind(_right_click, lambda e: chat_box_menu.post(e.x_root, e.y_root))
  459.  
  460. self.interface.chat_box = chat_box
  461.  
  462. def _processbar_frame(self):
  463. process_frame = ttk.Frame(self.interface.root, height=28)
  464. process_frame.grid(row=2, column=0, sticky="ew", padx=20, pady=10)
  465.  
  466. progress = ttk.Progressbar(
  467. process_frame,
  468. mode="indeterminate",
  469. style="LoadingBar.Horizontal.TProgressbar",
  470. )
  471.  
  472. stop_button = ttk.Button(
  473. process_frame,
  474. width=5,
  475. text="Stop",
  476. command=lambda: stop_button.state(["disabled"]),
  477. )
  478.  
  479. self.interface.progress = progress
  480. self.interface.stop_button = stop_button
  481.  
  482. def _input_frame(self):
  483. input_frame = ttk.Frame(self.interface.root)
  484. input_frame.grid(row=3, column=0, sticky="ew", padx=20, pady=(0, 20))
  485. input_frame.grid_columnconfigure(0, weight=1)
  486.  
  487. user_input = tk.Text(
  488. input_frame, font=(self.interface.default_font, 12), height=4, wrap=tk.WORD
  489. )
  490. user_input.grid(row=0, column=0, sticky="ew", padx=(0, 10))
  491. user_input.bind("<Key>", self.interface.handle_key_press)
  492.  
  493. send_button = ttk.Button(
  494. input_frame,
  495. text="Send",
  496. command=self.interface.on_send_button,
  497. )
  498. send_button.grid(row=0, column=1)
  499. send_button.state(["disabled"])
  500.  
  501. menubar = tk.Menu(self.interface.root)
  502. self.interface.root.config(menu=menubar)
  503.  
  504. file_menu = tk.Menu(menubar, tearoff=0)
  505. menubar.add_cascade(label="File", menu=file_menu)
  506. file_menu.add_command(label="Save Chat", command=self.interface.save_chat)
  507. file_menu.add_command(label="Load Chat", command=self.interface.load_chat)
  508. file_menu.add_separator()
  509. file_menu.add_command(label="Model Management", command=self.show_model_management_window)
  510. file_menu.add_command(label="Exit", command=self.interface.root.quit)
  511.  
  512. edit_menu = tk.Menu(menubar, tearoff=0)
  513. menubar.add_cascade(label="Edit", menu=edit_menu)
  514.  
  515. edit_menu.add_command(label="Copy All", command=self.interface.copy_all)
  516. edit_menu.add_command(label="Clear Chat", command=self.interface.clear_chat)
  517.  
  518. help_menu = tk.Menu(menubar, tearoff=0)
  519. menubar.add_cascade(label="Help", menu=help_menu)
  520. help_menu.add_command(label="Source Code", command=self.interface.open_homepage)
  521. help_menu.add_command(label="Help", command=self.interface.show_help)
  522.  
  523. self.interface.user_input = user_input
  524. self.interface.send_button = send_button
  525.  
  526. def show_model_management_window(self):
  527. self.interface.update_host()
  528.  
  529. if self.management_window and self.management_window.winfo_exists():
  530. self.management_window.lift()
  531. return
  532.  
  533. management_window = tk.Toplevel(self.interface.root)
  534. management_window.title("Model Management")
  535. screen_width = self.interface.root.winfo_screenwidth()
  536. screen_height = self.interface.root.winfo_screenheight()
  537. x = int((screen_width / 2) - (400 / 2))
  538. y = int((screen_height / 2) - (500 / 2))
  539.  
  540. management_window.geometry(f"{400}x{500}+{x}+{y}")
  541.  
  542. management_window.grid_columnconfigure(0, weight=1)
  543. management_window.grid_rowconfigure(3, weight=1)
  544.  
  545. frame = ttk.Frame(management_window)
  546. frame.grid(row=0, column=0, sticky="ew", padx=10, pady=10)
  547. frame.grid_columnconfigure(0, weight=1)
  548.  
  549. model_name_input = ttk.Entry(frame)
  550. model_name_input.grid(row=0, column=0, sticky="ew", padx=(0, 5))
  551.  
  552. def _download():
  553. arg = model_name_input.get().strip()
  554. if arg.startswith("ollama run "):
  555. arg = arg[11:]
  556. Thread(
  557. target=self.interface.download_model, daemon=True, args=(arg,)
  558. ).start()
  559.  
  560. def _delete():
  561. arg = models_list.get(tk.ACTIVE).strip()
  562. Thread(target=self.interface.delete_model, daemon=True, args=(arg,)).start()
  563.  
  564. download_button = ttk.Button(frame, text="Download", command=_download)
  565. download_button.grid(row=0, column=1, sticky="ew")
  566.  
  567. tips = tk.Label(
  568. frame,
  569. text="find models: https://o...content-available-to-author-only...a.com/library",
  570. fg="blue",
  571. cursor="hand2",
  572. )
  573. tips.bind("<Button-1>", lambda e: webbrowser.open("https://o...content-available-to-author-only...a.com/library"))
  574. tips.grid(row=1, column=0, sticky="W", padx=(0, 5), pady=5)
  575.  
  576. list_action_frame = ttk.Frame(management_window)
  577. list_action_frame.grid(row=2, column=0, sticky="nsew", padx=10, pady=(0, 10))
  578. list_action_frame.grid_columnconfigure(0, weight=1)
  579. list_action_frame.grid_rowconfigure(0, weight=1)
  580.  
  581. models_list = tk.Listbox(list_action_frame)
  582. models_list.grid(row=0, column=0, sticky="nsew")
  583.  
  584. scrollbar = ttk.Scrollbar(
  585. list_action_frame, orient="vertical", command=models_list.yview
  586. )
  587. scrollbar.grid(row=0, column=1, sticky="ns")
  588. models_list.config(yscrollcommand=scrollbar.set)
  589.  
  590. delete_button = ttk.Button(list_action_frame, text="Delete", command=_delete)
  591. delete_button.grid(row=0, column=2, sticky="ew", padx=(5, 0))
  592.  
  593. log_textbox = tk.Text(management_window)
  594. log_textbox.grid(row=3, column=0, sticky="nsew", padx=10, pady=(0, 10))
  595. log_textbox.config(state="disabled")
  596.  
  597. self.management_window = management_window
  598.  
  599. self.interface.log_textbox = log_textbox
  600. self.interface.download_button = download_button
  601. self.interface.delete_button = delete_button
  602. self.interface.models_list = models_list
  603. Thread(
  604. target=self.interface.update_model_list, daemon=True,
  605. ).start()
  606.  
  607. def show_editor_window(self, _, inner_label):
  608. if self.editor_window and self.editor_window.winfo_exists():
  609. self.editor_window.lift()
  610. return
  611.  
  612. editor_window = tk.Toplevel(self.interface.root)
  613. editor_window.title("Chat Editor")
  614.  
  615. screen_width = self.interface.root.winfo_screenwidth()
  616. screen_height = self.interface.root.winfo_screenheight()
  617.  
  618. x = int((screen_width / 2) - (400 / 2))
  619. y = int((screen_height / 2) - (300 / 2))
  620.  
  621. editor_window.geometry(f"{400}x{300}+{x}+{y}")
  622.  
  623. chat_editor = tk.Text(editor_window)
  624. chat_editor.grid(row=0, column=0, columnspan=2, sticky="nsew", padx=5, pady=5)
  625. chat_editor.insert(tk.END, inner_label.cget("text"))
  626.  
  627. editor_window.grid_rowconfigure(0, weight=1)
  628. editor_window.grid_columnconfigure(0, weight=1)
  629. editor_window.grid_columnconfigure(1, weight=1)
  630.  
  631. def _save():
  632. idx = self.interface.label_widgets.index(inner_label)
  633. if len(self.interface.chat_history) > idx:
  634. self.interface.chat_history[idx]["content"] = chat_editor.get("1.0", "end-1c")
  635. inner_label.config(text=chat_editor.get("1.0", "end-1c"))
  636.  
  637. editor_window.destroy()
  638.  
  639. save_button = tk.Button(editor_window, text="Save", command=_save)
  640. save_button.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
  641.  
  642. cancel_button = tk.Button(
  643. editor_window, text="Cancel", command=editor_window.destroy
  644. )
  645. cancel_button.grid(row=1, column=1, sticky="ew", padx=5, pady=5)
  646.  
  647. editor_window.grid_columnconfigure(0, weight=1, uniform="btn")
  648. editor_window.grid_columnconfigure(1, weight=1, uniform="btn")
  649.  
  650. self.editor_window = editor_window
  651.  
  652. def create_inner_label(self, on_right_side: bool = False):
  653. background = "#48a4f2" if on_right_side else "#eaeaea"
  654. foreground = "white" if on_right_side else "black"
  655. max_width = int(self.interface.chat_box.winfo_reqwidth()) * 0.7
  656. inner_label = tk.Label(
  657. self.interface.chat_box,
  658. justify=tk.LEFT,
  659. wraplength=max_width,
  660. background=background,
  661. highlightthickness=0,
  662. highlightbackground=background,
  663. foreground=foreground,
  664. padx=8,
  665. pady=8,
  666. font=(self.interface.default_font, 12),
  667. borderwidth=0,
  668. )
  669. self.interface.label_widgets.append(inner_label)
  670.  
  671. inner_label.bind(
  672. "<MouseWheel>",
  673. lambda e:
  674. self.interface.chat_box.yview_scroll(int(-1 * (e.delta / 120)), "units")
  675. )
  676. inner_label.bind("<Double-1>", lambda e: self.show_editor_window(e, inner_label))
  677.  
  678. _right_menu = tk.Menu(inner_label, tearoff=0)
  679. _right_menu.add_command(
  680. label="Edit", command=lambda: self.show_editor_window(None, inner_label)
  681. )
  682. _right_menu.add_command(
  683. label="Copy This", command=lambda: self.interface.copy_text(inner_label.cget("text"))
  684. )
  685. _right_menu.add_separator()
  686. _right_menu.add_command(label="Clear Chat", command=self.interface.clear_chat)
  687. _right_click = (
  688. "<Button-2>" if platform.system().lower() == "darwin" else "<Button-3>"
  689. )
  690. inner_label.bind(_right_click, lambda e: _right_menu.post(e.x_root, e.y_root))
  691. self.interface.chat_box.window_create(tk.END, window=inner_label)
  692. if on_right_side:
  693. idx = self.interface.chat_box.index("end-1c").split(".")[0]
  694. self.interface.chat_box.tag_add("Right", f"{idx}.0", f"{idx}.end")
  695.  
  696.  
  697. def run():
  698. root = tk.Tk()
  699.  
  700. root.title("Ollama GUI")
  701. screen_width = root.winfo_screenwidth()
  702. screen_height = root.winfo_screenheight()
  703. root.geometry(f"800x600+{(screen_width - 800) // 2}+{(screen_height - 600) // 2}")
  704.  
  705. root.grid_columnconfigure(0, weight=1)
  706. root.grid_rowconfigure(1, weight=1)
  707. root.grid_rowconfigure(2, weight=0)
  708. root.grid_rowconfigure(3, weight=0)
  709.  
  710. app = OllamaInterface(root)
  711.  
  712. app.chat_box.tag_configure(
  713. "Bold", foreground="#ff007b", font=(app.default_font, 10, "bold")
  714. )
  715. app.chat_box.tag_configure("Error", foreground="red")
  716. app.chat_box.tag_configure("Right", justify="right")
  717.  
  718. root.mainloop()
  719.  
  720.  
  721. if __name__ == "__main__":
  722. run()
  723.  
  724. lab.config(text=str(sel))
  725.  
  726. lab.pack()
  727. F2.pack(side=Tkinter.TOP)
  728.  
  729. poll()
  730. Tkinter.mainloop()
Success #stdin #stdout 0.46s 27884KB
stdin
Standard input is empty
stdout
Your Python installation does not include the Tk library. 
Please refer to https://g...content-available-to-author-only...b.com/chyok/ollama-gui?tab=readme-ov-file#-qa