Skip to content

Instantly share code, notes, and snippets.

@Amerboss
Created February 5, 2025 12:42
Show Gist options
  • Save Amerboss/def32d75e48ace2676940550a7fb9fb0 to your computer and use it in GitHub Desktop.
Save Amerboss/def32d75e48ace2676940550a7fb9fb0 to your computer and use it in GitHub Desktop.
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog, filedialog
import sqlite3
from datetime import datetime
import pytz
from ctypes import windll
from tkinter import font as tkFont
from PIL import Image, ImageTk
import os
# Enable DPI awareness for better text rendering
windll.shcore.SetProcessDpiAwareness(1)
class ChatApp:
def __init__(self, root):
self.root = root
# Remove native title bar
self.root.overrideredirect(True)
self.root.geometry("1200x800")
self.is_fullscreen = False
# Bind <Map> event to reapply override-redirect when needed
self.root.bind("<Map>", self.on_map)
# Create our custom title bar
self.create_title_bar()
# Main container for UI (placed below title bar)
self.container = tk.Frame(self.root, bg="#1e1e1e")
self.container.pack(side="top", fill="both", expand=True)
# Setup resize grip at bottom-right
self.create_resize_grip()
self.current_chat_id = None
self.waiting_for_response = False
# Setup database (including tables for notebooks/notes)
self.setup_db()
self.setup_ui()
self.load_chat_history()
self.setup_context_menu()
self.setup_nb_context_menu()
# For storing notebook images so they persist:
self.nb_images = {}
# ----------------------------
# General Functions
# ----------------------------
def on_map(self, event):
if not self.is_fullscreen:
self.root.overrideredirect(True)
def create_title_bar(self):
title_bg = "#333333"
self.title_bar = tk.Frame(self.root, bg=title_bg, relief='raised', bd=0, height=30)
self.title_bar.pack(side="top", fill="x")
self.title_font = tkFont.Font(family="Segoe UI", size=16, weight="bold")
self.normal_font = tkFont.Font(family="Segoe UI", size=14)
self.title_label = tk.Label(self.title_bar, text=" Local Chat Assistant ", bg=title_bg,
fg="#007acc", font=self.title_font)
self.title_label.pack(side="left", padx=10)
self.title_label.bind("<Double-Button-1>", self.toggle_fullscreen)
self.title_bar.bind("<Double-Button-1>", self.toggle_fullscreen)
btn_frame = tk.Frame(self.title_bar, bg=title_bg)
btn_frame.pack(side="right", padx=5)
fs_button = tk.Button(btn_frame, text="⛶", command=self.toggle_fullscreen,
bg=title_bg, fg="#007acc", borderwidth=0, font=self.normal_font)
fs_button.pack(side="right", padx=2)
min_button = tk.Button(btn_frame, text="➖", command=self.minimize_window,
bg=title_bg, fg="#007acc", borderwidth=0, font=self.normal_font)
min_button.pack(side="right", padx=2)
close_button = tk.Button(btn_frame, text="✖", command=self.root.destroy,
bg=title_bg, fg="#007acc", borderwidth=0, font=self.normal_font)
close_button.pack(side="right", padx=2)
for widget in (self.title_bar, self.title_label):
widget.bind("<ButtonPress-1>", self.start_move)
widget.bind("<ButtonRelease-1>", self.stop_move)
widget.bind("<B1-Motion>", self.on_move)
def minimize_window(self):
self.root.overrideredirect(False)
self.root.iconify()
def toggle_fullscreen(self, event=None):
self.is_fullscreen = not self.is_fullscreen
if self.is_fullscreen:
self.root.overrideredirect(False)
self.root.attributes("-fullscreen", True)
else:
self.root.attributes("-fullscreen", False)
self.root.overrideredirect(True)
self.root.attributes("-alpha", 1.0)
self.container.config(bg="#1e1e1e")
def start_move(self, event):
self.x = event.x
self.y = event.y
def stop_move(self, event):
self.x = None
self.y = None
def on_move(self, event):
deltax = event.x - self.x
deltay = event.y - self.y
new_x = self.root.winfo_x() + deltax
new_y = self.root.winfo_y() + deltay
self.root.geometry(f"+{new_x}+{new_y}")
def create_resize_grip(self):
self.grip = tk.Label(self.container, bg="#1e1e1e", cursor="size_nw_se")
self.grip.place(relx=1.0, rely=1.0, anchor="se")
self.grip.bind("<ButtonPress-1>", self.start_resize)
self.grip.bind("<B1-Motion>", self.do_resize)
def start_resize(self, event):
self.win_width = self.root.winfo_width()
self.win_height = self.root.winfo_height()
self.start_x = event.x_root
self.start_y = event.y_root
def do_resize(self, event):
dx = event.x_root - self.start_x
dy = event.y_root - self.start_y
new_width = max(self.win_width + dx, 400)
new_height = max(self.win_height + dy, 300)
self.root.geometry(f"{new_width}x{new_height}")
def setup_db(self):
self.conn = sqlite3.connect('chat_data.db')
self.c = self.conn.cursor()
# Chats and messages tables
self.c.execute('''CREATE TABLE IF NOT EXISTS chats
(id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
created_at TIMESTAMP)''')
self.c.execute('''CREATE TABLE IF NOT EXISTS messages
(id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER,
content TEXT,
sender TEXT,
timestamp TIMESTAMP,
FOREIGN KEY(chat_id) REFERENCES chats(id))''')
# Notebook tables
self.c.execute('''CREATE TABLE IF NOT EXISTS notebooks
(id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
created_at TIMESTAMP)''')
self.c.execute('''CREATE TABLE IF NOT EXISTS notes
(id INTEGER PRIMARY KEY AUTOINCREMENT,
notebook_id INTEGER,
content TEXT,
note_type TEXT,
timestamp TIMESTAMP,
FOREIGN KEY(notebook_id) REFERENCES notebooks(id))''')
self.conn.commit()
def validate_numeric(self, value_if_allowed):
return value_if_allowed == "" or value_if_allowed.isdigit()
# ----------------------------
# UI Setup
# ----------------------------
def setup_ui(self):
# Colors and fonts
chat_bg = "#1e1e1e" # unified background for main content
history_bg = "#252526" # for the history panel
button_bg = "#007acc"
entry_bg = "#333333"
self.user_bg = "#2d2d30"
self.ai_bg = "#3f3f46"
self.chat_font = tkFont.Font(family="Segoe UI", size=14)
self.bold_font = tkFont.Font(family="Segoe UI", size=14, weight="bold")
self.container.grid_columnconfigure(0, weight=0, minsize=50) # Left icon bar
self.container.grid_columnconfigure(1, weight=0) # Chat history panel
self.container.grid_columnconfigure(2, weight=1) # Main content (tabs)
self.container.grid_rowconfigure(0, weight=1)
# ----------------------------
# Left Icon Bar
# ----------------------------
icon_bar_bg = "#333333"
self.icon_bar = tk.Frame(self.container, bg=icon_bar_bg, width=50)
self.icon_bar.grid(row=0, column=0, sticky="ns", padx=0, pady=0)
self.top_icon_frame = tk.Frame(self.icon_bar, bg=icon_bar_bg)
self.top_icon_frame.pack(side="top", fill="x", pady=(10, 0))
# Chat icon
chat_icon_btn = tk.Button(self.top_icon_frame, text="💬", bg=icon_bar_bg, fg=button_bg,
activebackground="#404040", activeforeground="#005999",
borderwidth=0, command=self.show_chat_tab, font=self.title_font)
chat_icon_btn.pack(pady=10, padx=5)
# Config icon
config_icon_btn = tk.Button(self.top_icon_frame, text="🧠", bg=icon_bar_bg, fg=button_bg,
activebackground="#404040", activeforeground="#005999",
borderwidth=0, command=self.show_config_tab, font=self.title_font)
config_icon_btn.pack(pady=10, padx=5)
# Features icon
features_icon_btn = tk.Button(self.top_icon_frame, text="🔌", bg=icon_bar_bg, fg=button_bg,
activebackground="#404040", activeforeground="#005999",
borderwidth=0, command=self.show_features_tab, font=self.title_font)
features_icon_btn.pack(pady=10, padx=5)
# Models icon
models_icon_btn = tk.Button(self.top_icon_frame, text="🖥", bg=icon_bar_bg, fg=button_bg,
activebackground="#404040", activeforeground="#005999",
borderwidth=0, command=self.show_models_tab, font=self.title_font)
models_icon_btn.pack(pady=10, padx=5)
# Notebook icon
notebook_icon_btn = tk.Button(self.top_icon_frame, text="📝", bg=icon_bar_bg, fg=button_bg,
activebackground="#404040", activeforeground="#005999",
borderwidth=0, command=self.show_notebook_tab, font=self.title_font)
notebook_icon_btn.pack(pady=10, padx=5)
# Settings icon (bottom)
settings_icon_btn = tk.Button(self.icon_bar, text="⚙", bg=icon_bar_bg, fg=button_bg,
activebackground="#404040", activeforeground="#005999",
borderwidth=0, command=self.show_settings_tab, font=self.title_font)
settings_icon_btn.pack(side="bottom", pady=10, padx=5)
# ----------------------------
# Chat History Panel
# ----------------------------
self.history_frame = tk.Frame(self.container, bg=history_bg, width=250)
self.history_frame.grid(row=0, column=1, sticky="ns", padx=5, pady=5)
self.history_list_frame = tk.Frame(self.history_frame, bg=history_bg)
self.history_list_frame.pack(fill="both", expand=True, pady=(0, 5))
self.history_left = tk.Frame(self.history_list_frame, bg=history_bg)
self.history_left.pack(side="left", fill="both", expand=True)
self.history_right = tk.Frame(self.history_list_frame, bg=history_bg, width=15)
self.history_right.pack(side="right", fill="y")
self.history_list = tk.Listbox(self.history_left, bg=history_bg, fg="#e0e0e0",
selectbackground=button_bg, selectforeground="#ffffff",
borderwidth=0, font=self.bold_font, highlightthickness=0,
activestyle='none', relief='flat', selectmode='single', height=20)
self.history_list.pack(fill="both", expand=True)
self.history_scrollbar = ttk.Scrollbar(self.history_right, orient="vertical",
command=self.history_list.yview)
self.history_list.config(yscrollcommand=self.history_scrollbar.set)
self.history_right.bind("<Enter>", lambda e: self.history_scrollbar.pack(side="top", fill="y", expand=True))
self.history_right.bind("<Leave>", lambda e: self.history_scrollbar.pack_forget())
self.history_list.bind('<<ListboxSelect>>', self.load_selected_chat)
new_chat_btn = tk.Button(self.history_frame, text="+ New Chat", bg=button_bg, fg="#e0e0e0",
activebackground="#005999", borderwidth=0, font=self.title_font,
command=self.new_chat)
new_chat_btn.pack(fill='x', padx=5, pady=5)
# ----------------------------
# Main Content / Tabs Area
# ----------------------------
# Chat Tab
self.main_frame = tk.Frame(self.container, bg=chat_bg)
self.main_frame.grid(row=0, column=2, sticky="nsew", padx=5, pady=5)
self.main_frame.grid_rowconfigure(0, weight=1)
self.main_frame.grid_columnconfigure(0, weight=1)
self.chat_display_frame = tk.Frame(self.main_frame, bg=chat_bg)
self.chat_display_frame.grid(row=0, column=0, sticky="nsew")
self.chat_left = tk.Frame(self.chat_display_frame, bg=chat_bg)
self.chat_left.pack(side="left", fill="both", expand=True)
self.chat_right = tk.Frame(self.chat_display_frame, bg=chat_bg, width=15)
self.chat_right.pack(side="right", fill="y")
self.chat_display = tk.Text(self.chat_left, bg=chat_bg, fg="#e0e0e0", wrap=tk.WORD,
state='disabled', font=self.chat_font, padx=10, pady=10)
self.chat_display.pack(fill="both", expand=True)
self.chat_scrollbar = ttk.Scrollbar(self.chat_right, orient="vertical",
command=self.chat_display.yview)
self.chat_display.config(yscrollcommand=self.chat_scrollbar.set)
self.chat_right.bind("<Enter>", lambda e: self.chat_scrollbar.pack(side="top", fill="y", expand=True))
self.chat_right.bind("<Leave>", lambda e: self.chat_scrollbar.pack_forget())
self.input_frame = tk.Frame(self.main_frame, bg=chat_bg)
self.input_frame.grid(row=1, column=0, sticky="ew", pady=(0,5))
self.input_frame.columnconfigure(0, weight=1)
self.input_frame.columnconfigure(1, weight=0)
self.msg_entry = tk.Text(self.input_frame, bg=entry_bg, fg="#e0e0e0",
insertbackground="#e0e0e0", font=self.bold_font,
wrap=tk.WORD, height=3, borderwidth=0, relief="flat")
self.msg_entry.grid(row=0, column=0, sticky="ew", padx=(0,5))
self.msg_entry.bind("<Return>", self.handle_enter)
self.msg_entry.bind("<Shift-Return>", self.send_message)
self.msg_entry.bind("<Control-Return>", self.send_message)
self.send_btn = tk.Button(self.input_frame, text="↑", bg=button_bg, fg="#e0e0e0",
activebackground="#005999", borderwidth=0, font=self.title_font,
command=self.send_message)
self.send_btn.grid(row=0, column=1, sticky="e", padx=(0,5))
# ----------------------------
# Configuration Tab
# ----------------------------
self.config_frame = tk.Frame(self.container, bg=chat_bg)
self.config_frame.grid(row=0, column=1, columnspan=2, sticky="nsew")
self.config_frame.grid_remove()
config_label = tk.Label(self.config_frame, text="AI Parameters Configuration",
fg="#e0e0e0", bg=chat_bg, font=self.title_font)
config_label.pack(pady=20)
# ----------------------------
# Features Tab
# ----------------------------
self.features_frame = tk.Frame(self.container, bg=chat_bg)
self.features_frame.grid(row=0, column=1, columnspan=2, sticky="nsew")
self.features_frame.grid_remove()
features_label = tk.Label(self.features_frame, text="Toggle AI Features",
fg="#e0e0e0", bg=chat_bg, font=self.title_font)
features_label.pack(pady=20)
self.features_vars = {}
for feat in ["Feature A", "Feature B", "Feature C"]:
feat_frame = tk.Frame(self.features_frame, bg=chat_bg)
feat_frame.pack(fill="x", padx=20, pady=5)
feat_label = tk.Label(feat_frame, text=feat, bg=chat_bg, fg="#e0e0e0", font=self.bold_font)
feat_label.grid(row=0, column=0, sticky="w")
var = tk.BooleanVar(value=False)
chk = tk.Checkbutton(feat_frame, variable=var,
bg=chat_bg, fg="#e0e0e0", selectcolor=button_bg,
activebackground=chat_bg, activeforeground="#ffffff",
font=self.bold_font,
command=lambda f=feat, v=var: print(f"{f} set to {v.get()}"))
chk.grid(row=0, column=1, padx=10)
self.features_vars[feat] = var
# ----------------------------
# Models Tab
# ----------------------------
self.models_frame = tk.Frame(self.container, bg=chat_bg)
self.models_frame.grid(row=0, column=1, columnspan=2, sticky="nsew")
self.models_frame.grid_remove()
models_label = tk.Label(self.models_frame, text="Model Management",
fg="#e0e0e0", bg=chat_bg, font=self.title_font)
models_label.pack(pady=20)
model_sel_frame = tk.Frame(self.models_frame, bg=chat_bg)
model_sel_frame.pack(pady=10, padx=20, fill="x")
sel_label = tk.Label(model_sel_frame, text="Select Model:", bg=chat_bg, fg="#e0e0e0", font=self.bold_font)
sel_label.grid(row=0, column=0, sticky="w")
self.model_var = tk.StringVar(value="Model A")
model_options = ["Model A", "Model B", "Model C"]
model_menu = ttk.OptionMenu(model_sel_frame, self.model_var, self.model_var.get(), *model_options)
model_menu.grid(row=0, column=1, padx=10, sticky="w")
model_btn_frame = tk.Frame(self.models_frame, bg=chat_bg)
model_btn_frame.pack(pady=10, padx=20, fill="x")
load_btn = tk.Button(model_btn_frame, text="Load Model", bg=button_bg, fg="#e0e0e0",
font=self.bold_font, command=lambda: print("Load model:", self.model_var.get()))
load_btn.grid(row=0, column=0, padx=5)
unload_btn = tk.Button(model_btn_frame, text="Unload Model", bg=button_bg, fg="#e0e0e0",
font=self.bold_font, command=lambda: print("Unload model"))
unload_btn.grid(row=0, column=1, padx=5)
refresh_btn = tk.Button(model_btn_frame, text="Refresh List", bg=button_bg, fg="#e0e0e0",
font=self.bold_font, command=lambda: print("Refresh model list"))
refresh_btn.grid(row=0, column=2, padx=5)
model_opts_frame = tk.Frame(self.models_frame, bg=chat_bg)
model_opts_frame.pack(pady=10, padx=20, fill="x")
self.flash_var = tk.BooleanVar(value=False)
flash_chk = tk.Checkbutton(model_opts_frame, text="Flash Attention", variable=self.flash_var,
bg=chat_bg, fg="#e0e0e0", selectcolor=button_bg,
activebackground=chat_bg, activeforeground="#ffffff",
font=self.bold_font, command=lambda: print("Flash Attention:", self.flash_var.get()))
flash_chk.grid(row=0, column=0, padx=5, sticky="w")
self.tensor_var = tk.BooleanVar(value=False)
tensor_chk = tk.Checkbutton(model_opts_frame, text="Tensor Cores", variable=self.tensor_var,
bg=chat_bg, fg="#e0e0e0", selectcolor=button_bg,
activebackground=chat_bg, activeforeground="#ffffff",
font=self.bold_font, command=lambda: print("Tensor Cores:", self.tensor_var.get()))
tensor_chk.grid(row=0, column=1, padx=5, sticky="w")
ctx_label = tk.Label(model_opts_frame, text="Context Length:", bg=chat_bg, fg="#e0e0e0", font=self.bold_font)
ctx_label.grid(row=1, column=0, sticky="w", padx=5, pady=(10,0))
vcmd = (self.root.register(self.validate_numeric), '%P')
self.ctx_length = tk.Entry(model_opts_frame, font=self.bold_font, width=10, validate='key', validatecommand=vcmd)
self.ctx_length.insert(0, "512")
self.ctx_length.grid(row=1, column=1, padx=5, pady=(10,0), sticky="w")
gpu_label = tk.Label(model_opts_frame, text="GPU Layers:", bg=chat_bg, fg="#e0e0e0", font=self.bold_font)
gpu_label.grid(row=2, column=0, sticky="w", padx=5, pady=(10,0))
self.gpu_layers = tk.Entry(model_opts_frame, font=self.bold_font, width=10, validate='key', validatecommand=vcmd)
self.gpu_layers.insert(0, "0")
self.gpu_layers.grid(row=2, column=1, padx=5, pady=(10,0), sticky="w")
# ----------------------------
# Notebook Tab
# ----------------------------
self.notebook_frame = tk.Frame(self.container, bg=chat_bg)
self.notebook_frame.grid(row=0, column=1, columnspan=2, sticky="nsew")
self.notebook_frame.grid_remove()
self.notebook_frame.grid_rowconfigure(0, weight=1)
self.notebook_frame.grid_columnconfigure(0, weight=1)
nb_label = tk.Label(self.notebook_frame, text="Notebook", fg="#e0e0e0", bg=chat_bg, font=self.title_font)
nb_label.pack(pady=5)
# Notebook history panel (left)
nb_history_frame = tk.Frame(self.notebook_frame, bg=history_bg, width=250)
nb_history_frame.pack(side="left", fill="y", padx=5, pady=5)
self.nb_list = tk.Listbox(nb_history_frame, bg=history_bg, fg="#e0e0e0",
selectbackground=button_bg, selectforeground="#ffffff",
borderwidth=0, font=self.bold_font, highlightthickness=0,
activestyle='none', relief='flat', selectmode='single')
self.nb_list.pack(fill="both", expand=True)
self.nb_list.bind('<<ListboxSelect>>', self.load_selected_note)
self.nb_list.bind("<Button-3>", self.show_nb_context_menu)
new_note_btn = tk.Button(nb_history_frame, text="+ New Note", bg=button_bg, fg="#e0e0e0",
activebackground="#005999", borderwidth=0, font=self.title_font,
command=self.new_note)
new_note_btn.pack(fill='x', padx=5, pady=5)
# Notebook main area
nb_main_frame = tk.Frame(self.notebook_frame, bg=chat_bg)
nb_main_frame.pack(side="left", fill="both", expand=True, padx=5, pady=5)
self.nb_display = tk.Text(nb_main_frame, bg=chat_bg, fg="#e0e0e0", wrap=tk.WORD,
state='disabled', font=self.chat_font, padx=10, pady=10)
self.nb_display.pack(fill="both", expand=True)
# Notebook input area with grid so that entry and icon buttons always remain visible
nb_input_frame = tk.Frame(nb_main_frame, bg=chat_bg)
nb_input_frame.pack(fill="x", pady=(0,5))
# Set a fixed width for icon columns to ensure they are visible even in narrow windows.
nb_input_frame.columnconfigure(0, weight=1)
nb_input_frame.columnconfigure(1, weight=0, minsize=40)
nb_input_frame.columnconfigure(2, weight=0, minsize=40)
self.nb_entry = tk.Text(nb_input_frame, bg=entry_bg, fg="#e0e0e0",
insertbackground="#e0e0e0", font=self.bold_font,
wrap=tk.WORD, height=3, borderwidth=0, relief="flat")
self.nb_entry.grid(row=0, column=0, sticky="ew", padx=(0,5))
self.nb_entry.bind("<Return>", self.handle_nb_enter)
self.nb_entry.bind("<Shift-Return>", self.handle_nb_enter)
self.nb_entry.bind("<Control-Return>", self.handle_nb_enter)
nb_send_btn = tk.Button(nb_input_frame, text="↑", bg=button_bg, fg="#e0e0e0",
activebackground="#005999", borderwidth=0, font=self.title_font,
command=self.add_note)
nb_send_btn.grid(row=0, column=1, sticky="ns", padx=(0,5))
nb_upload_btn = tk.Button(nb_input_frame, text="📎", bg=button_bg, fg="#e0e0e0",
activebackground="#005999", borderwidth=0, font=self.title_font,
command=self.upload_image)
nb_upload_btn.grid(row=0, column=2, sticky="ns", padx=(0,5))
# ----------------------------
# Settings Tab
# ----------------------------
self.settings_frame = tk.Frame(self.container, bg=chat_bg)
self.settings_frame.grid(row=0, column=1, columnspan=2, sticky="nsew")
self.settings_frame.grid_remove()
settings_label = tk.Label(self.settings_frame, text="General Settings",
fg="#e0e0e0", bg=chat_bg, font=self.title_font)
settings_label.pack(pady=20)
# ----------------------------
# Tab Display Methods
# ----------------------------
def show_chat_tab(self):
self.history_frame.grid()
self.main_frame.grid()
self.config_frame.grid_remove()
self.features_frame.grid_remove()
self.models_frame.grid_remove()
self.notebook_frame.grid_remove()
self.settings_frame.grid_remove()
def show_config_tab(self):
self.history_frame.grid_remove()
self.main_frame.grid_remove()
self.config_frame.grid()
self.features_frame.grid_remove()
self.models_frame.grid_remove()
self.notebook_frame.grid_remove()
self.settings_frame.grid_remove()
def show_features_tab(self):
self.history_frame.grid_remove()
self.main_frame.grid_remove()
self.config_frame.grid_remove()
self.features_frame.grid()
self.models_frame.grid_remove()
self.notebook_frame.grid_remove()
self.settings_frame.grid_remove()
def show_models_tab(self):
self.history_frame.grid_remove()
self.main_frame.grid_remove()
self.config_frame.grid_remove()
self.features_frame.grid_remove()
self.models_frame.grid()
self.notebook_frame.grid_remove()
self.settings_frame.grid_remove()
def show_notebook_tab(self):
self.history_frame.grid_remove()
self.main_frame.grid_remove()
self.config_frame.grid_remove()
self.features_frame.grid_remove()
self.models_frame.grid_remove()
self.notebook_frame.grid()
self.settings_frame.grid_remove()
self.load_notebook_history()
def show_settings_tab(self):
self.history_frame.grid_remove()
self.main_frame.grid_remove()
self.settings_frame.grid()
self.config_frame.grid_remove()
self.features_frame.grid_remove()
self.models_frame.grid_remove()
self.notebook_frame.grid_remove()
# ----------------------------
# Context Menu for Chats
# ----------------------------
def setup_context_menu(self):
self.context_menu = tk.Menu(self.root, tearoff=0)
self.context_menu.add_command(label="Rename", command=self.rename_chat)
self.context_menu.add_command(label="Delete", command=self.delete_chat)
self.history_list.bind("<Button-3>", self.show_context_menu)
def show_context_menu(self, event):
try:
self.context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.context_menu.grab_release()
# ----------------------------
# Notebook List Context Menu
# ----------------------------
def setup_nb_context_menu(self):
self.nb_context_menu = tk.Menu(self.root, tearoff=0)
self.nb_context_menu.add_command(label="Rename", command=self.rename_notebook)
self.nb_context_menu.add_command(label="Delete", command=self.delete_notebook)
self.nb_list.bind("<Button-3>", self.show_nb_context_menu)
def show_nb_context_menu(self, event):
try:
self.nb_context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.nb_context_menu.grab_release()
def rename_notebook(self):
selection = self.nb_list.curselection()
if not selection:
return
index = selection[0]
old_title = self.nb_list.get(index)
new_title = simpledialog.askstring("Rename Notebook", "New Notebook Title:", parent=self.root)
if new_title and new_title.strip():
nb_id = self.get_notebook_id_at_index(index)
self.c.execute("UPDATE notebooks SET title = ? WHERE id = ?", (new_title, nb_id))
self.conn.commit()
self.load_notebook_history()
def delete_notebook(self):
selection = self.nb_list.curselection()
if not selection:
return
index = selection[0]
nb_id = self.get_notebook_id_at_index(index)
if messagebox.askyesno("Delete Notebook", "Are you sure you want to delete this notebook? This will delete all associated notes."):
self.c.execute("DELETE FROM notes WHERE notebook_id = ?", (nb_id,))
self.c.execute("DELETE FROM notebooks WHERE id = ?", (nb_id,))
self.conn.commit()
self.load_notebook_history()
# ----------------------------
# Chat Functions
# ----------------------------
def delete_chat(self):
selection = self.history_list.curselection()
if not selection:
return
index = selection[0]
chat_id = self.get_chat_id_at_index(index)
if messagebox.askyesno("Delete Chat", "Are you sure you want to delete this chat?"):
self.c.execute("DELETE FROM messages WHERE chat_id = ?", (chat_id,))
self.c.execute("DELETE FROM chats WHERE id = ?", (chat_id,))
self.conn.commit()
self.history_list.delete(index)
del self.chat_ids[index]
if self.current_chat_id == chat_id:
self.current_chat_id = None
self.clear_chat_display()
self.toggle_input_state(True)
def rename_chat(self):
selection = self.history_list.curselection()
if not selection:
return
index = selection[0]
old_title = self.history_list.get(index)
new_title = simpledialog.askstring("Rename Chat", "New chat title:", parent=self.root)
if new_title and new_title.strip() != old_title.strip():
chat_id = self.get_chat_id_at_index(index)
self.c.execute("UPDATE chats SET title = ? WHERE id = ?", (new_title, chat_id))
self.conn.commit()
self.history_list.delete(index)
self.history_list.insert(index, f" {new_title} ")
self.history_list.selection_set(index)
def handle_enter(self, event):
self.msg_entry.insert(tk.INSERT, "\n")
return "break"
def load_selected_chat(self, event=None):
selection = self.history_list.curselection()
if not selection:
return
index = selection[0]
chat_id = self.get_chat_id_at_index(index)
if chat_id:
self.current_chat_id = chat_id
self.clear_chat_display()
self.load_chat_messages(chat_id)
self.toggle_input_state(True)
def load_chat_messages(self, chat_id):
self.c.execute("SELECT content, sender FROM messages WHERE chat_id = ? ORDER BY timestamp", (chat_id,))
messages = self.c.fetchall()
for msg in messages:
self.update_chat_display(msg[0], msg[1])
def new_chat(self):
self.current_chat_id = None
self.clear_chat_display()
self.msg_entry.delete("1.0", tk.END)
self.toggle_input_state(True)
self.refresh_chat_history()
def refresh_chat_history(self):
self.history_list.delete(0, tk.END)
self.load_chat_history()
def toggle_input_state(self, enabled):
state = 'normal' if enabled else 'disabled'
self.msg_entry.config(state=state)
self.send_btn.config(state=state)
self.waiting_for_response = not enabled
def clear_chat_display(self):
self.chat_display.config(state='normal')
self.chat_display.delete('1.0', tk.END)
self.chat_display.config(state='disabled')
def send_message(self, event=None):
if self.waiting_for_response:
return
msg = self.msg_entry.get("1.0", tk.END).strip()
if not msg:
return
self.msg_entry.delete("1.0", tk.END)
self.toggle_input_state(False)
timestamp = self.get_baghdad_time()
if not self.current_chat_id:
self.c.execute("INSERT INTO chats (title, created_at) VALUES (?, ?)",
(f"Chat {timestamp}", timestamp))
self.current_chat_id = self.c.lastrowid
self.conn.commit()
self.refresh_chat_history()
self.save_message(msg, 'user', timestamp)
self.update_chat_display(msg, "user")
self.root.after(1000, lambda: self.add_dummy_reply(timestamp))
def add_dummy_reply(self, original_timestamp):
reply = f"This is a dummy response. (Generated at {self.get_baghdad_time().split()[1]})"
timestamp = self.get_baghdad_time()
self.save_message(reply, 'assistant', timestamp)
self.update_chat_display(reply, "assistant")
self.toggle_input_state(True)
def save_message(self, content, sender, timestamp):
self.c.execute("INSERT INTO messages (chat_id, content, sender, timestamp) VALUES (?, ?, ?, ?)",
(self.current_chat_id, content, sender, timestamp))
self.conn.commit()
# ----------------------------
# Notebook Functions
# ----------------------------
def load_notebook_history(self):
self.nb_list.delete(0, tk.END)
self.notebook_ids = []
self.c.execute("SELECT id, title FROM notebooks ORDER BY created_at DESC")
notebooks = self.c.fetchall()
for nb in notebooks:
self.notebook_ids.append(nb[0])
self.nb_list.insert(tk.END, f" {nb[1]} ")
def get_notebook_id_at_index(self, index):
try:
return self.notebook_ids[index]
except IndexError:
return None
def new_note(self):
title = simpledialog.askstring("New Notebook", "Notebook Title:", parent=self.root)
if not title:
return
timestamp = self.get_baghdad_time()
self.c.execute("INSERT INTO notebooks (title, created_at) VALUES (?, ?)", (title, timestamp))
nb_id = self.c.lastrowid
self.conn.commit()
self.load_notebook_history()
index = self.notebook_ids.index(nb_id)
self.nb_list.selection_clear(0, tk.END)
self.nb_list.selection_set(index)
self.current_notebook_id = nb_id
self.nb_display.config(state="normal")
self.nb_display.delete("1.0", tk.END)
self.nb_display.config(state="disabled")
def load_selected_note(self, event=None):
selection = self.nb_list.curselection()
if not selection:
return
index = selection[0]
nb_id = self.get_notebook_id_at_index(index)
if nb_id:
self.current_notebook_id = nb_id
self.nb_display.config(state="normal")
self.nb_display.delete("1.0", tk.END)
self.nb_images.clear() # Clear previously loaded images
self.c.execute("SELECT content, note_type FROM notes WHERE notebook_id = ? ORDER BY timestamp", (nb_id,))
notes = self.c.fetchall()
for note in notes:
if note[1] == "text":
self.nb_display.insert(tk.END, f"{note[0]}\n\n")
elif note[1] == "image":
if os.path.exists(note[0]):
try:
img = Image.open(note[0])
max_width = 450 # increased size (1.5x of 300)
if img.width > max_width:
ratio = max_width / img.width
new_size = (max_width, int(img.height * ratio))
img = img.resize(new_size, Image.ANTIALIAS)
photo = ImageTk.PhotoImage(img)
self.nb_images[note[0]] = photo
self.nb_display.image_create(tk.END, image=photo)
self.nb_display.insert(tk.END, "\n\n")
except Exception as e:
self.nb_display.insert(tk.END, f"[Error loading image: {note[0]}]\n\n")
else:
self.nb_display.insert(tk.END, f"[Image not found: {note[0]}]\n\n")
self.nb_display.config(state="disabled")
def add_note(self, event=None):
if not hasattr(self, "current_notebook_id"):
messagebox.showwarning("No Notebook Selected", "Please create or select a notebook first.")
return "break"
note_text = self.nb_entry.get("1.0", tk.END).strip()
if not note_text:
return "break"
timestamp = self.get_baghdad_time()
self.c.execute("INSERT INTO notes (notebook_id, content, note_type, timestamp) VALUES (?, ?, ?, ?)",
(self.current_notebook_id, note_text, "text", timestamp))
self.conn.commit()
self.nb_entry.delete("1.0", tk.END)
self.load_selected_note()
return "break"
def handle_nb_enter(self, event):
self.add_note()
return "break"
def upload_image(self):
if not hasattr(self, "current_notebook_id"):
messagebox.showwarning("No Notebook Selected", "Please create or select a notebook first.")
return
filepath = filedialog.askopenfilename(title="Select Image", filetypes=[("Image Files", "*.png;*.jpg;*.jpeg;*.gif")])
if not filepath:
return
timestamp = self.get_baghdad_time()
self.c.execute("INSERT INTO notes (notebook_id, content, note_type, timestamp) VALUES (?, ?, ?, ?)",
(self.current_notebook_id, filepath, "image", timestamp))
self.conn.commit()
self.load_selected_note()
# ----------------------------
# Utility Functions
# ----------------------------
def get_baghdad_time(self):
baghdad_tz = pytz.timezone('Asia/Baghdad')
return datetime.now(baghdad_tz).strftime("%Y-%m-%d %H:%M:%S")
def update_chat_display(self, message, sender):
self.chat_display.config(state='normal')
tag = "user" if sender == "user" else "assistant"
text_color = "#e0e0e0"
bg_color = self.user_bg if sender == "user" else self.ai_bg
self.chat_display.tag_config(tag,
foreground=text_color,
background=bg_color,
lmargin1=10,
lmargin2=10,
rmargin=10,
spacing1=5,
spacing3=5,
font=self.bold_font,
relief="flat",
borderwidth=5)
self.chat_display.insert(tk.END, f" {message} \n", tag)
self.chat_display.insert(tk.END, "\n")
self.chat_display.config(state='disabled')
self.chat_display.see(tk.END)
def load_chat_history(self):
self.history_list.delete(0, tk.END)
self.chat_ids = []
self.c.execute("SELECT id, title FROM chats ORDER BY created_at DESC")
chats = self.c.fetchall()
for chat in chats:
self.chat_ids.append(chat[0])
self.history_list.insert(tk.END, f" {chat[1]} ")
def get_chat_id_at_index(self, index):
try:
return self.chat_ids[index]
except IndexError:
return None
def __del__(self):
self.conn.close()
if __name__ == "__main__":
root = tk.Tk()
app = ChatApp(root)
root.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment