From 9eaa85dc376f039661718dd6770498abd67c8903 Mon Sep 17 00:00:00 2001 From: DJP Date: Fri, 10 Apr 2026 10:52:51 -0400 Subject: [PATCH] =?UTF-8?q?3-step=20upload=20flow:=20select=20files=20?= =?UTF-8?q?=E2=86=92=20set=20tiers=20=E2=86=92=20extract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step 1: Select files (shown with gold bullet points after selection) - Step 2: Tier mapping is REQUIRED - must click "None" or a preset before extraction is enabled. Red asterisk indicates required. - Step 3: Choose Normal/Deep extraction mode, then click "Extract Assets" - Upload no longer auto-triggers extraction on file select - Uploaded filenames shown persistently on the Upload tab - Tier confirmation state tracked (tierConfirmed) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/ProjectView.css | 36 +++++++ frontend/src/pages/ProjectView.tsx | 156 +++++++++++++++-------------- 2 files changed, 119 insertions(+), 73 deletions(-) diff --git a/frontend/src/pages/ProjectView.css b/frontend/src/pages/ProjectView.css index 32e075a..8553f3f 100644 --- a/frontend/src/pages/ProjectView.css +++ b/frontend/src/pages/ProjectView.css @@ -145,6 +145,42 @@ cursor: pointer; } +.uploaded-files { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 12px 16px; + margin-top: 12px; +} + +.uploaded-files-label { + font-size: 11px; + font-weight: 600; + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 6px; +} + +.uploaded-file-item { + font-size: 13px; + color: var(--color-text); + padding: 3px 0; + padding-left: 14px; + position: relative; +} + +.uploaded-file-item::before { + content: ''; + position: absolute; + left: 0; + top: 10px; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-primary); +} + .upload-timer { font-size: 12px; color: var(--color-text-muted); diff --git a/frontend/src/pages/ProjectView.tsx b/frontend/src/pages/ProjectView.tsx index 1afe8d9..c72cec3 100644 --- a/frontend/src/pages/ProjectView.tsx +++ b/frontend/src/pages/ProjectView.tsx @@ -59,6 +59,10 @@ export default function ProjectView() { const [uploadStage, setUploadStage] = useState(''); const [extractionMode, setExtractionMode] = useState<'normal' | 'deep'>('normal'); const [uploadTimer, setUploadTimer] = useState(0); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [filesReady, setFilesReady] = useState(false); + const [pendingFiles, setPendingFiles] = useState(null); + const [tierConfirmed, setTierConfirmed] = useState(false); const [refineInput, setRefineInput] = useState(''); const [refining, setRefining] = useState(false); const [refineLog, setRefineLog] = useState([]); @@ -86,6 +90,9 @@ export default function ProjectView() { ]); setProject(projRes.data); setAssets(assetsRes.data); + if (projRes.data.source_filename) { + setUploadedFiles(projRes.data.source_filename.split(', ').filter(Boolean)); + } if (assetsRes.data.length > 0) { const matchRes = await api.get(`/projects/${id}/matches`); @@ -144,19 +151,31 @@ export default function ProjectView() { loadEfficiency(); }, []); - async function handleUpload(e: React.ChangeEvent) { + function handleFileSelect(e: React.ChangeEvent) { const files = e.target.files; if (!files || files.length === 0) return; + setPendingFiles(files); + setFilesReady(true); + setUploadedFiles(Array.from(files).map(f => f.name)); + } + + async function handleExtract() { + if (!pendingFiles || pendingFiles.length === 0) return; + if (!tierConfirmed) { + alert('Please select a tier mapping (or "None") before extracting.'); + return; + } + setUploading(true); setUploadTimer(0); const timerInterval = setInterval(() => setUploadTimer(t => t + 1), 1000); - const names = Array.from(files).map(f => f.name).join(', '); - setUploadStage(`Uploading ${files.length} file(s): ${names}...`); + const names = Array.from(pendingFiles).map(f => f.name).join(', '); + setUploadStage(`Uploading ${pendingFiles.length} file(s): ${names}...`); try { const form = new FormData(); - for (let i = 0; i < files.length; i++) { - form.append('files', files[i]); + for (let i = 0; i < pendingFiles.length; i++) { + form.append('files', pendingFiles[i]); } await api.post(`/projects/${id}/upload?mode=${extractionMode}`, form); } catch (err: any) { @@ -179,6 +198,8 @@ export default function ProjectView() { clearInterval(timerInterval); setUploading(false); setUploadStage(''); + setPendingFiles(null); + setFilesReady(false); await loadProject(); setTab('matches'); } @@ -498,6 +519,7 @@ export default function ProjectView() { {tab === 'upload' && (
+ {/* Step 1: File Selection */}
{uploading ? ( <> @@ -512,100 +534,88 @@ export default function ProjectView() {
-

Upload Client Document

-

Word (.docx) or Excel (.xlsx) file with the client's asset brief

- -
- - -
-

- {extractionMode === 'normal' - ? 'Fast extraction — best for clean asset lists and simple briefs.' - : 'Two-pass AI analysis — best for complex spreadsheets with tiers, merged cells, and mixed data. Takes longer, costs more (~$0.15-0.30).'} -

- -
- {/* Tier Mapping - set BEFORE matching */} -
+ {/* Show uploaded/selected files */} + {(uploadedFiles.length > 0 || filesReady) && !uploading && ( +
+
Selected Documents:
+ {uploadedFiles.map((name, i) => ( +
{name}
+ ))} +
+ )} + + {/* Step 2: Tier Mapping (required selection) */} + {!uploading && ( +
- Client Tier Mapping (optional): + Step 2: Client Tier Mapping *
- - - - - - + + + + + +
{tierMapping.tiers.length > 0 && (
{tierMapping.tiers.map((t, i) => (
- { - const updated = [...tierMapping.tiers]; - updated[i] = { ...updated[i], label: e.target.value }; - saveTierMapping(updated); - }} - placeholder="Label (e.g. Tier A)" - /> + { const u = [...tierMapping.tiers]; u[i] = { ...u[i], label: e.target.value }; saveTierMapping(u); }} + placeholder="Label (e.g. Tier A)" /> - { const u = [...tierMapping.tiers]; u[i] = { ...u[i], complexity: e.target.value }; saveTierMapping(u); }}> - +
))} -
)} -
- Set tiers before running AI matching. The AI will extract tier labels from the client document and match each to the correct GMAL complexity variant. +
+ )} + + {/* Step 3: Extraction Mode + Extract Button */} + {filesReady && tierConfirmed && !uploading && ( +
+
+
+ Step 3: Extraction Mode +
+ + +
+
+

+ {extractionMode === 'normal' + ? 'Fast single-pass extraction — best for clean asset lists.' + : 'Two-pass AI analysis — best for complex spreadsheets. Takes longer (~$0.15-0.30).'} +

+
+ )} {assets.length > 0 && (