← Commit history

Delete tour/narration.py

John Lauer ·8444cb3e0c ·29d ago ·parent 93828cc
1 file changed −348
tour/narration.pydeleted−348
@@ -1,348 +0,0 @@-"""Narration panel for the KiCad tour.--Styled to match the Adom Desktop Tauri app exactly:-  Colors: desktop/src/style.css CSS custom properties-  Fonts:  Segoe UI (body) + Cascadia Code (mono)-  Layout: bg-primary page, bg-secondary panels, bg-tertiary hover--Runs standalone with an HTTP server so Claude can push updates:-    python narration.py [--port 8799]--HTTP API:-    POST /update   {"step":1,"total":19,"title":"...","description":"...",-                     "act":"...","status":"...","status_color":"#3fb950",-                     "data":"...","data_color":"#3fb950"}-    POST /quit     Close the panel-    GET  /health   {"status":"ok"}-"""--import argparse-import ctypes-import json-import sys-import threading-import tkinter as tk-from http.server import HTTPServer, BaseHTTPRequestHandler-from tkinter import ttk---# ── Colors — exact match to desktop/src/style.css :root vars ─────-BG_PRIMARY = "#0d1117"       # --bg-primary  (page background)-BG_SECONDARY = "#161b22"     # --bg-secondary (panels, toolbars)-BG_TERTIARY = "#21262d"      # --bg-tertiary  (hover, elevated)-BORDER = "#30363d"           # --border-TEXT_PRIMARY = "#e6edf3"     # --text-primary-TEXT_SECONDARY = "#8b949e"   # --text-secondary-TEXT_MUTED = "#6e7681"       # --text-muted-ACCENT = "#58a6ff"           # --accent-GREEN = "#3fb950"            # --green-RED = "#f85149"              # --red-YELLOW = "#d29922"           # --yellow-ORANGE = "#db6d28"           # --orange--# Brand accent-TEAL = "#00b8b1"-TEAL_BRIGHT = "#00e6dc"-PURPLE = "#8C6BF7"-PURPLE_LIGHT = "#C5B3FF"-BLUE = ACCENT-BLUE_LIGHT = "#64ABFF"--# Backward-compat aliases-FG = TEXT_PRIMARY-FG_DIM = TEXT_MUTED-FG_SECONDARY = TEXT_SECONDARY--# ── Typography ──────────────────────────────────────────────────-FONT_TITLE = ("Segoe UI", 18, "bold")-FONT_H2 = ("Segoe UI", 15, "bold")-FONT_ACT = ("Segoe UI", 11, "bold")-FONT_DESC = ("Segoe UI", 13)-FONT_MONO = ("Cascadia Code", 12)-FONT_STATUS = ("Segoe UI", 13)-FONT_BTN = ("Segoe UI", 13)-FONT_SMALL = ("Segoe UI", 12)-FONT_TAG = ("Segoe UI", 11, "bold")---class NarrationPanel:-    """Always-on-top narration panel — styled identical to Adom Desktop."""--    def __init__(self, width: int = 480, on_quit=None):-        self.width = width-        self._on_quit = on_quit--        self.root = tk.Tk()-        self.root.title("Gallia Tour")-        self.root.configure(bg=BG_PRIMARY)-        self.root.attributes("-topmost", True)--        # Position on the right side of the screen, accounting for taskbar.-        # Use tkinter's own screen dimensions (logical pixels) so geometry-        # values are consistent regardless of DPI awareness.-        screen_w = self.root.winfo_screenwidth()-        screen_h = self.root.winfo_screenheight()-        # Subtract space for the taskbar (~50 logical px) and titlebar (~32 px)-        # to guarantee the Quit button is fully visible.-        panel_h = screen_h - 80-        x = screen_w - width-        self.root.geometry(f"{width}x{panel_h}+{x}+0")-        self.root.resizable(False, False)--        self.root.protocol("WM_DELETE_WINDOW", self._quit)--        self._build_ui()--        # Apply dark mode to the native Windows titlebar via DWM API.-        # This gives us a dark titlebar that matches the app without-        # removing it (which caused the invisible-window bug).-        self.root.update_idletasks()-        self._apply_dark_titlebar()--    def _apply_dark_titlebar(self):-        """Use DWM API to render the native titlebar in dark mode (Win10 1809+)."""-        try:-            hwnd = ctypes.windll.user32.GetParent(self.root.winfo_id())-            # DWMWA_USE_IMMERSIVE_DARK_MODE = 20 (Windows 10 1809+)-            DWMWA_USE_IMMERSIVE_DARK_MODE = 20-            value = ctypes.c_int(1)  # TRUE = dark mode-            ctypes.windll.dwmapi.DwmSetWindowAttribute(-                hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,-                ctypes.byref(value), ctypes.sizeof(value),-            )-        except Exception:-            pass  # Fall back to default titlebar if DWM not available--    def _build_ui(self):-        # ── Status bar (matches #top-bar status group) ───-        status_bar = tk.Frame(self.root, bg=BG_SECONDARY, padx=16, pady=8)-        status_bar.pack(fill="x")--        # Green dot + step counter (like the Connected indicator)-        dot_canvas = tk.Canvas(-            status_bar, width=14, height=14,-            bg=BG_SECONDARY, highlightthickness=0,-        )-        dot_canvas.pack(side="left", padx=(0, 8))-        self._dot = dot_canvas-        self._dot_id = dot_canvas.create_oval(2, 2, 12, 12, fill=GREEN, outline="")--        self._step_label = tk.Label(-            status_bar, text="Gallia Feature Tour", font=FONT_SMALL,-            bg=BG_SECONDARY, fg=TEXT_SECONDARY,-        )-        self._step_label.pack(side="left")--        self._act_tag = tk.Label(-            status_bar, text="", font=FONT_TAG,-            bg="#1a1a33", fg="#a78bfa",-            padx=6, pady=1,-        )-        self._act_tag.pack(side="right")--        # Border-        tk.Frame(self.root, bg=BORDER, height=1).pack(fill="x")--        # ── Progress bar ─────────────────────────────-        prog_frame = tk.Frame(self.root, bg=BG_PRIMARY, padx=16)-        prog_frame.pack(fill="x", pady=(8, 0))--        style = ttk.Style()-        style.theme_use("default")-        style.configure(-            "Tour.Horizontal.TProgressbar",-            troughcolor=BG_TERTIARY,-            background=TEAL,-            thickness=4,-        )-        self._progress = ttk.Progressbar(-            prog_frame, style="Tour.Horizontal.TProgressbar",-            length=self.width - 32, mode="determinate",-        )-        self._progress.pack(fill="x")--        # ── Content area ─────────────────────────────-        content_frame = tk.Frame(self.root, bg=BG_PRIMARY, padx=16, pady=12)-        content_frame.pack(fill="x")--        self._section_label = tk.Label(-            content_frame, text="FEATURE TOUR", font=FONT_H2,-            bg=BG_PRIMARY, fg=TEXT_SECONDARY,-        )-        self._section_label.pack(anchor="w")--        self._title_label = tk.Label(-            content_frame, text="Waiting for Claude...", font=FONT_TITLE,-            bg=BG_PRIMARY, fg=TEXT_PRIMARY, anchor="w",-            wraplength=self.width - 32, justify="left",-        )-        self._title_label.pack(fill="x", pady=(8, 0))--        tk.Frame(self.root, bg=BORDER, height=1).pack(fill="x", padx=16, pady=8)--        self._desc_label = tk.Label(-            self.root, text="The tour will begin shortly.\nClaude is analyzing your setup...",-            font=FONT_DESC,-            bg=BG_PRIMARY, fg=TEXT_SECONDARY, padx=16, anchor="nw",-            wraplength=self.width - 32, justify="left",-        )-        self._desc_label.pack(fill="x")--        # ── Status line ──────────────────────────────-        self._status_label = tk.Label(-            self.root, text="", font=FONT_STATUS,-            bg=BG_PRIMARY, fg=GREEN, padx=16, anchor="w",-        )-        self._status_label.pack(fill="x", pady=(8, 0))--        # ── Data area (matches #activity-log) ────────-        data_outer = tk.Frame(self.root, bg=BG_PRIMARY, padx=16, pady=8)-        data_outer.pack(fill="both", expand=True)--        data_border = tk.Frame(data_outer, bg=BORDER, padx=1, pady=1)-        data_border.pack(fill="both", expand=True)--        self._data_text = tk.Text(-            data_border, font=FONT_MONO,-            bg=BG_SECONDARY, fg=TEXT_PRIMARY,-            insertbackground=GREEN,-            selectbackground=BG_TERTIARY,-            relief="flat", wrap="word",-            padx=8, pady=8,-            borderwidth=0, highlightthickness=0,-        )-        self._data_text.pack(fill="both", expand=True)-        self._data_text.config(state="disabled")--        # ── Button bar (matches #stats-footer) ───────-        tk.Frame(self.root, bg=BORDER, height=1).pack(fill="x", side="bottom")--        btn_frame = tk.Frame(self.root, bg=BG_SECONDARY, pady=8, padx=16)-        btn_frame.pack(fill="x", side="bottom")--        tk.Button(-            btn_frame, text="Quit Tour", command=self._quit,-            font=FONT_BTN, bg="#da3633", fg="#ffffff",-            activebackground="#b62324", activeforeground="#ffffff",-            relief="flat", padx=14, pady=6, cursor="hand2",-            borderwidth=1, highlightthickness=0,-        ).pack(side="right")--        # "Powered by Claude" label-        tk.Label(-            btn_frame, text="Powered by Claude",-            font=FONT_SMALL, bg=BG_SECONDARY, fg=TEXT_MUTED,-        ).pack(side="left")--    # ── Public API (thread-safe via root.after) ───────────────────--    def set_step(self, number: int, total: int, title: str, description: str,-                 act: str = ""):-        self._step_label.config(text=f"Step {number} of {total}")-        self._progress["maximum"] = total-        self._progress["value"] = number-        if act:-            self._act_tag.config(text=act.upper())-        self._title_label.config(text=title)-        self._desc_label.config(text=description)-        self._status_label.config(text="")-        self._data_text.config(state="normal")-        self._data_text.delete("1.0", "end")-        self._data_text.config(state="disabled")--    def set_status(self, text: str, color: str = GREEN):-        self._status_label.config(text=text, fg=color)--    def set_data(self, text: str):-        self._data_text.config(state="normal")-        self._data_text.delete("1.0", "end")-        self._data_text.insert("1.0", text)-        self._data_text.config(state="disabled")--    def set_data_color(self, color: str):-        self._data_text.config(fg=color)--    def apply_update(self, data: dict):-        """Apply a dict of updates from the HTTP API."""-        if "step" in data and "total" in data:-            self.set_step(-                data["step"], data["total"],-                data.get("title", ""), data.get("description", ""),-                data.get("act", ""),-            )-        else:-            if "title" in data:-                self._title_label.config(text=data["title"])-            if "description" in data:-                self._desc_label.config(text=data["description"])-            if "act" in data:-                self._act_tag.config(text=data["act"].upper())-        if "status" in data:-            color = data.get("status_color", GREEN)-            self.set_status(data["status"], color)-        if "data" in data:-            self.set_data(data["data"])-        if "data_color" in data:-            self.set_data_color(data["data_color"])--    # ── HTTP server for Claude to push updates ────────────────────--    def start_server(self, port: int = 8799):-        """Start the HTTP control server on a background thread."""-        panel = self--        class Handler(BaseHTTPRequestHandler):-            def do_POST(self):-                if self.path == "/update":-                    length = int(self.headers.get("Content-Length", 0))-                    body = json.loads(self.rfile.read(length)) if length else {}-                    panel.root.after(0, lambda: panel.apply_update(body))-                    self._respond(200, {"ok": True})-                elif self.path == "/quit":-                    self._respond(200, {"ok": True})-                    panel.root.after(100, panel.root.destroy)-                else:-                    self._respond(404, {"error": "not found"})--            def do_GET(self):-                if self.path == "/health":-                    self._respond(200, {"status": "ok", "port": port})-                else:-                    self._respond(404, {"error": "not found"})--            def _respond(self, code, data):-                self.send_response(code)-                self.send_header("Content-Type", "application/json")-                self.end_headers()-                self.wfile.write(json.dumps(data).encode())--            def log_message(self, fmt, *args):-                pass  # Suppress request logging--        server = HTTPServer(("127.0.0.1", port), Handler)-        t = threading.Thread(target=server.serve_forever, daemon=True)-        t.start()-        return server--    # ── Internal ──────────────────────────────────────────────────--    def _quit(self):-        if self._on_quit:-            self._on_quit()-        self.root.destroy()---# ── Standalone mode ──────────────────────────────────────────────--if __name__ == "__main__":-    parser = argparse.ArgumentParser(description="Gallia Tour narration panel")-    parser.add_argument("--port", type=int, default=8799,-                        help="HTTP server port (default: 8799)")-    parser.add_argument("--width", type=int, default=480,-                        help="Panel width in pixels (default: 480)")-    args = parser.parse_args()--    panel = NarrationPanel(width=args.width)-    panel.start_server(port=args.port)-    print(f"Panel server listening on http://127.0.0.1:{args.port}")-    panel.root.mainloop()