From 571855823fec487bc665f9448f19b70139c2ac68 Mon Sep 17 00:00:00 2001 From: Mario Lavoie Date: Wed, 28 Jan 2026 15:31:12 +0000 Subject: [PATCH] fix(gui): Simplified project selection workflow Fixes: - Single 'Select Project Folder' button replaces confusing New/Open - Auto-creates project if folder is empty or has no project.json - Auto-opens project if project.json exists - Removed editable project name field (was confusing) - Fixed 'directory not empty' error - now handles existing folders gracefully - Better error messages and confirmations - Project path shown as read-only label The flow is now: 1. Click 'Select Project Folder' 2. Pick any folder 3. If new: project created automatically If existing: project opened automatically TODO: Multilingual transcription support (Whisper handles mixed FR/EN poorly) --- src/cad_documenter/gui.py | 175 +++++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 79 deletions(-) diff --git a/src/cad_documenter/gui.py b/src/cad_documenter/gui.py index 788e223..afcd281 100644 --- a/src/cad_documenter/gui.py +++ b/src/cad_documenter/gui.py @@ -158,7 +158,6 @@ class CADDocumenterGUI: self.config = load_config() self._create_widgets() - self._update_project_list() def _create_widgets(self): """Create the main UI widgets.""" @@ -166,37 +165,32 @@ class CADDocumenterGUI: main_frame = ctk.CTkFrame(self.root) if USE_CTK else ttk.Frame(self.root) main_frame.pack(fill="both", expand=True, padx=10, pady=10) - # === Project Section === + # === Project Section (Simplified) === project_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Project") project_frame.pack(fill="x", pady=(0, 10)) if USE_CTK: - ctk.CTkLabel(project_frame, text="Project:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5)) + ctk.CTkLabel(project_frame, text="Project Folder:", font=("", 14, "bold")).pack(anchor="w", padx=10, pady=(10, 5)) proj_row = ctk.CTkFrame(project_frame) if USE_CTK else ttk.Frame(project_frame) proj_row.pack(fill="x", padx=10, pady=(0, 10)) - self.project_var = tk.StringVar() - self.project_combo = ctk.CTkComboBox( - proj_row, - variable=self.project_var, - values=[], - width=400, - command=self._on_project_selected - ) if USE_CTK else ttk.Combobox(proj_row, textvariable=self.project_var, width=50) - self.project_combo.pack(side="left", padx=(0, 10)) + # Project path display (read-only) + self.project_path_var = tk.StringVar(value="No project selected") + if USE_CTK: + self.project_path_label = ctk.CTkLabel(proj_row, textvariable=self.project_path_var, + anchor="w", width=450) + else: + self.project_path_label = ttk.Label(proj_row, textvariable=self.project_path_var) + self.project_path_label.pack(side="left", fill="x", expand=True) - if not USE_CTK: - self.project_combo.bind("<>", lambda e: self._on_project_selected(None)) + # Single "Select Project" button + btn_select = ctk.CTkButton(proj_row, text="📁 Select Project Folder", width=180, + command=self._select_project) if USE_CTK else ttk.Button(proj_row, text="Select Folder", command=self._select_project) + btn_select.pack(side="right", padx=(10, 0)) - btn_new = ctk.CTkButton(proj_row, text="+ New", width=80, command=self._new_project) if USE_CTK else ttk.Button(proj_row, text="+ New", command=self._new_project) - btn_new.pack(side="left", padx=5) - - btn_open = ctk.CTkButton(proj_row, text="Open", width=80, command=self._open_project) if USE_CTK else ttk.Button(proj_row, text="Open", command=self._open_project) - btn_open.pack(side="left", padx=5) - - # Project info - self.project_info_var = tk.StringVar(value="No project loaded") + # Project info (read-only) + self.project_info_var = tk.StringVar(value="Select a folder to create or open a project") if USE_CTK: self.project_info = ctk.CTkLabel(project_frame, textvariable=self.project_info_var, text_color="gray") else: @@ -257,7 +251,7 @@ class CADDocumenterGUI: cb_scene.pack(anchor="w", pady=2) self.transcribe_var = tk.BooleanVar(value=True) - cb_trans = ctk.CTkCheckBox(opts_inner, text="Whisper transcription (local GPU)", variable=self.transcribe_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Whisper transcription", variable=self.transcribe_var) + cb_trans = ctk.CTkCheckBox(opts_inner, text="Whisper transcription (local GPU, auto-detect language)", variable=self.transcribe_var) if USE_CTK else ttk.Checkbutton(opts_inner, text="Whisper transcription", variable=self.transcribe_var) cb_trans.pack(anchor="w", pady=2) # === Process Button === @@ -274,7 +268,7 @@ class CADDocumenterGUI: status_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.Frame(main_frame) status_frame.pack(fill="x") - self.status_var = tk.StringVar(value="Ready — Open or create a project to start") + self.status_var = tk.StringVar(value="Ready — Select a project folder to start") status_label = ctk.CTkLabel(status_frame, textvariable=self.status_var) if USE_CTK else ttk.Label(status_frame, textvariable=self.status_var) status_label.pack(side="left", padx=5) @@ -287,19 +281,14 @@ class CADDocumenterGUI: """Get the next generation number for the current project.""" if not self.current_project: return 1 - - # Count existing videos as generations return len(self.current_project.manifest.videos) + 1 - def _update_project_list(self): - """Update the project dropdown with recent projects.""" - pass # Could load from config/history - def _update_project_info(self): """Update the project info display.""" if not self.current_project: - self.project_info_var.set("No project loaded") + self.project_info_var.set("Select a folder to create or open a project") self.next_gen_var.set("") + self.project_path_var.set("No project selected") return m = self.current_project.manifest @@ -307,58 +296,86 @@ class CADDocumenterGUI: pending = len(self.current_project.get_pending_videos()) processed = total - pending - info = f"📁 {m.name} — {total} videos ({processed} processed, {pending} pending)" + self.project_path_var.set(str(self.current_project.project_dir)) + + if total == 0: + info = f"✓ Project: {m.name} — No videos yet, add one!" + else: + info = f"✓ Project: {m.name} — {total} videos ({processed} processed, {pending} pending)" self.project_info_var.set(info) next_gen = self._get_next_gen_number() self.next_gen_var.set(f"Next: Gen {next_gen:03d}") - def _on_project_selected(self, value): - """Handle project selection.""" - self._refresh_video_list() - self._update_project_info() - - def _new_project(self): - """Create a new project.""" - folder = filedialog.askdirectory(title="Select folder for new project") + def _select_project(self): + """Select a folder - creates project if empty/new, opens if existing.""" + folder = filedialog.askdirectory(title="Select Project Folder") if not folder: return - name = Path(folder).name + folder_path = Path(folder) + project_json = folder_path / "project.json" - # Dialog for project name - if USE_CTK: - dialog = ctk.CTkInputDialog( - text="Enter project name:", - title="New Project" - ) - name = dialog.get_input() or name - - try: - self.current_project = Project.create(Path(folder), name, "") - self.project_var.set(f"{name}") - self.status_var.set(f"✓ Created project: {name}") - self._refresh_video_list() - self._update_project_info() - except Exception as e: - messagebox.showerror("Error", str(e)) - - def _open_project(self): - """Open an existing project.""" - folder = filedialog.askdirectory(title="Select project folder") - if not folder: - return - - try: - self.current_project = Project.load(Path(folder)) - self.project_var.set(f"{self.current_project.manifest.name}") - self.status_var.set(f"✓ Loaded: {self.current_project.manifest.name}") - self._refresh_video_list() - self._update_project_info() - except FileNotFoundError: - messagebox.showerror("Error", f"No project.json found in {folder}\n\nUse '+ New' to create a new project.") - except Exception as e: - messagebox.showerror("Error", f"Failed to load project: {e}") + if project_json.exists(): + # Existing project - just open it + try: + self.current_project = Project.load(folder_path) + self.status_var.set(f"✓ Opened: {self.current_project.manifest.name}") + self._refresh_video_list() + self._update_project_info() + except Exception as e: + messagebox.showerror("Error", f"Failed to load project:\n{e}") + else: + # New project - create it + # Use folder name as project name + name = folder_path.name + + # Check if folder has other files (not empty but no project.json) + existing_files = list(folder_path.iterdir()) if folder_path.exists() else [] + + if existing_files: + # Folder has files but no project.json - ask to initialize here + if not messagebox.askyesno( + "Initialize Project?", + f"Folder '{name}' exists but has no project.\n\n" + f"Create a new CAD Documenter project here?\n\n" + f"(Existing files will not be deleted)" + ): + return + + try: + # Create project (handles existing folder gracefully) + folder_path.mkdir(parents=True, exist_ok=True) + + # Manually create structure without the "not empty" check + (folder_path / "videos").mkdir(exist_ok=True) + (folder_path / "knowledge").mkdir(exist_ok=True) + (folder_path / "knowledge" / "transcripts").mkdir(exist_ok=True) + (folder_path / "frames").mkdir(exist_ok=True) + (folder_path / "output").mkdir(exist_ok=True) + (folder_path / "context").mkdir(exist_ok=True) + (folder_path / "clawdbot_export").mkdir(exist_ok=True) + + # Create manifest + from .project import ProjectManifest + now = datetime.now().isoformat() + manifest = ProjectManifest( + name=name, + created_at=now, + updated_at=now, + description="", + ) + + self.current_project = Project(folder_path) + self.current_project.manifest = manifest + self.current_project.save() + + self.status_var.set(f"✓ Created new project: {name}") + self._refresh_video_list() + self._update_project_info() + + except Exception as e: + messagebox.showerror("Error", f"Failed to create project:\n{e}") def _refresh_video_list(self): """Refresh the video listbox.""" @@ -382,7 +399,7 @@ class CADDocumenterGUI: def _add_videos(self): """Add videos to the project with topic dialog.""" if not self.current_project: - messagebox.showwarning("Warning", "Please create or open a project first") + messagebox.showwarning("Warning", "Please select a project folder first") return files = filedialog.askopenfilenames( @@ -433,7 +450,7 @@ class CADDocumenterGUI: if not selection: return - if not messagebox.askyesno("Confirm", "Remove selected video(s) from project?"): + if not messagebox.askyesno("Confirm", "Remove selected video(s) from project?\n\n(Video files will be deleted)"): return # Remove in reverse order to maintain indices @@ -453,17 +470,17 @@ class CADDocumenterGUI: def _process_videos(self): """Process videos in background thread.""" if not self.current_project: - messagebox.showwarning("Warning", "Please create or open a project first") + messagebox.showwarning("Warning", "Please select a project folder first") return pending = self.current_project.get_pending_videos() if not pending: - messagebox.showinfo("Info", "No pending videos to process") + messagebox.showinfo("Info", "No pending videos to process.\n\nAdd a video first!") return # Disable button during processing self.btn_process.configure(state="disabled") - self.status_var.set(f"Processing {len(pending)} video(s)...") + self.status_var.set(f"Processing {len(pending)} video(s)... (this may take a few minutes)") if USE_CTK: self.progress.set(0)