tech-envision
Add macOS GUI client and usage docs
1f8a981
raw
history blame
5.05 kB
from __future__ import annotations
import threading
import queue
from pathlib import Path
from tkinter import (
Tk,
Text,
Entry,
Button,
Scrollbar,
Frame,
Label,
StringVar,
END,
filedialog,
)
from .api_client import APIClient
class ChatApp:
"""Tkinter GUI for interacting with the LLM backend."""
def __init__(self, root: Tk) -> None:
self.root = root
self.root.title("LLM Backend Chat")
self.server_var = StringVar(value="http://localhost:8000")
self.api_key_var = StringVar(value="")
self.user_var = StringVar(value="default")
self.session_var = StringVar(value="default")
self._client = APIClient()
self._queue: queue.Queue[tuple[str, str]] = queue.Queue()
self._build_ui()
self.root.after(100, self._process_queue)
# ------------------------------------------------------------------
# UI construction
# ------------------------------------------------------------------
def _build_ui(self) -> None:
top = Frame(self.root)
top.pack(fill="x")
Label(top, text="Server:").grid(row=0, column=0, sticky="w")
Entry(top, textvariable=self.server_var, width=30).grid(row=0, column=1, sticky="ew")
Label(top, text="API Key:").grid(row=0, column=2, sticky="w")
Entry(top, textvariable=self.api_key_var, width=20).grid(row=0, column=3, sticky="ew")
Label(top, text="User:").grid(row=1, column=0, sticky="w")
Entry(top, textvariable=self.user_var, width=15).grid(row=1, column=1, sticky="ew")
Label(top, text="Session:").grid(row=1, column=2, sticky="w")
Entry(top, textvariable=self.session_var, width=15).grid(row=1, column=3, sticky="ew")
self.chat_display = Text(self.root, wrap="word", height=20)
self.chat_display.pack(fill="both", expand=True)
scroll = Scrollbar(self.chat_display)
scroll.pack(side="right", fill="y")
self.chat_display.config(yscrollcommand=scroll.set)
scroll.config(command=self.chat_display.yview)
bottom = Frame(self.root)
bottom.pack(fill="x")
self.msg_entry = Entry(bottom)
self.msg_entry.pack(side="left", fill="x", expand=True)
self.msg_entry.bind("<Return>", lambda _: self.send_message())
Button(bottom, text="Send", command=self.send_message).pack(side="left")
Button(bottom, text="Upload", command=self.upload_file).pack(side="left")
# ------------------------------------------------------------------
# Event handlers
# ------------------------------------------------------------------
def _update_client(self) -> None:
self._client = APIClient(self.server_var.get(), self.api_key_var.get() or None)
def send_message(self) -> None:
prompt = self.msg_entry.get().strip()
if not prompt:
return
self.msg_entry.delete(0, END)
self.chat_display.insert(END, f"You: {prompt}\n")
self.chat_display.see(END)
self._update_client()
thread = threading.Thread(target=self._stream_prompt, args=(prompt,), daemon=True)
thread.start()
def _stream_prompt(self, prompt: str) -> None:
try:
for part in self._client.stream_chat(
self.user_var.get(), self.session_var.get(), prompt
):
self._queue.put(("assistant", part))
except Exception as exc: # pragma: no cover - runtime errors
self._queue.put(("error", str(exc)))
def upload_file(self) -> None:
path = filedialog.askopenfilename()
if path:
self._update_client()
thread = threading.Thread(target=self._upload_file, args=(Path(path),), daemon=True)
thread.start()
def _upload_file(self, path: Path) -> None:
try:
vm_path = self._client.upload_document(
self.user_var.get(), self.session_var.get(), str(path)
)
self._queue.put(("info", f"Uploaded {path.name} -> {vm_path}"))
except Exception as exc: # pragma: no cover - runtime errors
self._queue.put(("error", str(exc)))
# ------------------------------------------------------------------
# Queue processing
# ------------------------------------------------------------------
def _process_queue(self) -> None:
while True:
try:
kind, msg = self._queue.get_nowait()
except queue.Empty:
break
if kind == "assistant":
self.chat_display.insert(END, msg)
else:
prefix = "INFO" if kind == "info" else "ERROR"
self.chat_display.insert(END, f"[{prefix}] {msg}\n")
self.chat_display.see(END)
self.root.after(100, self._process_queue)
def main() -> None:
root = Tk()
ChatApp(root)
root.mainloop()
if __name__ == "__main__": # pragma: no cover - manual execution
main()