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)
This commit is contained in:
Mario Lavoie
2026-01-28 15:31:12 +00:00
parent ca01c7a944
commit 571855823f

View File

@@ -158,7 +158,6 @@ class CADDocumenterGUI:
self.config = load_config() self.config = load_config()
self._create_widgets() self._create_widgets()
self._update_project_list()
def _create_widgets(self): def _create_widgets(self):
"""Create the main UI widgets.""" """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 = ctk.CTkFrame(self.root) if USE_CTK else ttk.Frame(self.root)
main_frame.pack(fill="both", expand=True, padx=10, pady=10) 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 = ctk.CTkFrame(main_frame) if USE_CTK else ttk.LabelFrame(main_frame, text="Project")
project_frame.pack(fill="x", pady=(0, 10)) project_frame.pack(fill="x", pady=(0, 10))
if USE_CTK: 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 = ctk.CTkFrame(project_frame) if USE_CTK else ttk.Frame(project_frame)
proj_row.pack(fill="x", padx=10, pady=(0, 10)) proj_row.pack(fill="x", padx=10, pady=(0, 10))
self.project_var = tk.StringVar() # Project path display (read-only)
self.project_combo = ctk.CTkComboBox( self.project_path_var = tk.StringVar(value="No project selected")
proj_row, if USE_CTK:
variable=self.project_var, self.project_path_label = ctk.CTkLabel(proj_row, textvariable=self.project_path_var,
values=[], anchor="w", width=450)
width=400, else:
command=self._on_project_selected self.project_path_label = ttk.Label(proj_row, textvariable=self.project_path_var)
) if USE_CTK else ttk.Combobox(proj_row, textvariable=self.project_var, width=50) self.project_path_label.pack(side="left", fill="x", expand=True)
self.project_combo.pack(side="left", padx=(0, 10))
if not USE_CTK: # Single "Select Project" button
self.project_combo.bind("<<ComboboxSelected>>", lambda e: self._on_project_selected(None)) 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) # Project info (read-only)
btn_new.pack(side="left", padx=5) self.project_info_var = tk.StringVar(value="Select a folder to create or open a project")
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")
if USE_CTK: if USE_CTK:
self.project_info = ctk.CTkLabel(project_frame, textvariable=self.project_info_var, text_color="gray") self.project_info = ctk.CTkLabel(project_frame, textvariable=self.project_info_var, text_color="gray")
else: else:
@@ -257,7 +251,7 @@ class CADDocumenterGUI:
cb_scene.pack(anchor="w", pady=2) cb_scene.pack(anchor="w", pady=2)
self.transcribe_var = tk.BooleanVar(value=True) 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) cb_trans.pack(anchor="w", pady=2)
# === Process Button === # === Process Button ===
@@ -274,7 +268,7 @@ class CADDocumenterGUI:
status_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.Frame(main_frame) status_frame = ctk.CTkFrame(main_frame) if USE_CTK else ttk.Frame(main_frame)
status_frame.pack(fill="x") 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 = 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) status_label.pack(side="left", padx=5)
@@ -287,19 +281,14 @@ class CADDocumenterGUI:
"""Get the next generation number for the current project.""" """Get the next generation number for the current project."""
if not self.current_project: if not self.current_project:
return 1 return 1
# Count existing videos as generations
return len(self.current_project.manifest.videos) + 1 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): def _update_project_info(self):
"""Update the project info display.""" """Update the project info display."""
if not self.current_project: 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.next_gen_var.set("")
self.project_path_var.set("No project selected")
return return
m = self.current_project.manifest m = self.current_project.manifest
@@ -307,58 +296,86 @@ class CADDocumenterGUI:
pending = len(self.current_project.get_pending_videos()) pending = len(self.current_project.get_pending_videos())
processed = total - pending 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) self.project_info_var.set(info)
next_gen = self._get_next_gen_number() next_gen = self._get_next_gen_number()
self.next_gen_var.set(f"Next: Gen {next_gen:03d}") self.next_gen_var.set(f"Next: Gen {next_gen:03d}")
def _on_project_selected(self, value): def _select_project(self):
"""Handle project selection.""" """Select a folder - creates project if empty/new, opens if existing."""
self._refresh_video_list() folder = filedialog.askdirectory(title="Select Project Folder")
self._update_project_info()
def _new_project(self):
"""Create a new project."""
folder = filedialog.askdirectory(title="Select folder for new project")
if not folder: if not folder:
return return
name = Path(folder).name folder_path = Path(folder)
project_json = folder_path / "project.json"
# Dialog for project name if project_json.exists():
if USE_CTK: # Existing project - just open it
dialog = ctk.CTkInputDialog( try:
text="Enter project name:", self.current_project = Project.load(folder_path)
title="New Project" 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="",
) )
name = dialog.get_input() or name
try: self.current_project = Project(folder_path)
self.current_project = Project.create(Path(folder), name, "") self.current_project.manifest = manifest
self.project_var.set(f"{name}") self.current_project.save()
self.status_var.set(f"✓ Created project: {name}")
self.status_var.set(f"✓ Created new project: {name}")
self._refresh_video_list() self._refresh_video_list()
self._update_project_info() 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: except Exception as e:
messagebox.showerror("Error", f"Failed to load project: {e}") messagebox.showerror("Error", f"Failed to create project:\n{e}")
def _refresh_video_list(self): def _refresh_video_list(self):
"""Refresh the video listbox.""" """Refresh the video listbox."""
@@ -382,7 +399,7 @@ class CADDocumenterGUI:
def _add_videos(self): def _add_videos(self):
"""Add videos to the project with topic dialog.""" """Add videos to the project with topic dialog."""
if not self.current_project: 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 return
files = filedialog.askopenfilenames( files = filedialog.askopenfilenames(
@@ -433,7 +450,7 @@ class CADDocumenterGUI:
if not selection: if not selection:
return 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 return
# Remove in reverse order to maintain indices # Remove in reverse order to maintain indices
@@ -453,17 +470,17 @@ class CADDocumenterGUI:
def _process_videos(self): def _process_videos(self):
"""Process videos in background thread.""" """Process videos in background thread."""
if not self.current_project: 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 return
pending = self.current_project.get_pending_videos() pending = self.current_project.get_pending_videos()
if not pending: 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 return
# Disable button during processing # Disable button during processing
self.btn_process.configure(state="disabled") 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: if USE_CTK:
self.progress.set(0) self.progress.set(0)