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:
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user