Created
February 5, 2025 12:41
-
-
Save Amerboss/20aa33f2dec84b1577acecb9db1f57b7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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