import React, { useRef, useState } from 'react'; import { ArrowLeft, ArrowRight, CheckCircle2, FileText, Link2, Paperclip, PlusCircle, RefreshCw, StickyNote, Trash2 } from 'lucide-react'; import { motion } from 'framer-motion'; import { supabase } from '../services/supabase'; import { useAuth } from '../context/useAuth'; import type { UiMode } from '../services/uiMode'; import { getApiUrl } from '../services/runtimeConfig'; type ProjectSource = | { id: string; kind: 'link'; label: string; url: string; } | { id: string; kind: 'note'; label: string; content: string; } | { id: string; kind: 'file'; label: string; fileName: string; mimeType: string; size: number; content?: string; extracted: boolean; }; const supportedTextMimeTypes = new Set([ 'text/plain', 'text/markdown', 'text/csv', 'application/json', ]); const formatFileSize = (size: number) => { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`; return `${(size / (1024 * 1024)).toFixed(1)} MB`; }; const buildContextPayload = (baseContext: string, sources: ProjectSource[]) => { const sections: string[] = []; const trimmedContext = baseContext.trim(); if (trimmedContext) { sections.push(trimmedContext); } if (sources.length) { const sourceLines = sources.flatMap((source, index) => { if (source.kind === 'link') { return [`${index + 1}. [${source.label}](${source.url})`]; } if (source.kind === 'note') { return [ `${index + 1}. ${source.label}`, source.content, ]; } const metadata = `${source.fileName} (${source.mimeType || 'unknown'}, ${formatFileSize(source.size)})`; if (source.extracted && source.content) { return [ `${index + 1}. ${source.label} - ${metadata}`, source.content, ]; } return [`${index + 1}. ${source.label} - ${metadata}`]; }); sections.push(`Project Sources:\n${sourceLines.join('\n\n')}`); } return sections.join('\n\n').trim(); }; const FieldHelp: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children }) => ( ); const wizardSteps = [ { title: 'Basics', description: 'Name the workspace and describe the business outcome. Agents use this to understand what success looks like.' }, { title: 'Context', description: 'Add constraints, acceptance criteria, tone, risks, and assumptions. Good context reduces generic task plans.' }, { title: 'Sources', description: 'Attach links, notes, or files that should influence planning. This step is optional when the description is enough.' }, { title: 'Review', description: 'Check the setup before creating the project. You will generate tasks from the project page after creation.' }, { title: 'Magic Generation', description: 'Describe your project in natural language and attach reference docs. AI will pre-configure the workspace for you.' } ]; const expertAccessStep = { title: 'Workspace', description: 'Decide whether this project is personal or belongs to a team workspace.' }; const NewProject: React.FC<{ uiMode: UiMode; initialData?: any; onCreated?: () => void }> = ({ uiMode, initialData, onCreated }) => { const { user } = useAuth(); const fileInputRef = useRef(null); const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [context, setContext] = useState(''); const [sourceLabel, setSourceLabel] = useState(''); const [sourceUrl, setSourceUrl] = useState(''); const [noteLabel, setNoteLabel] = useState(''); const [noteContent, setNoteContent] = useState(''); const [sources, setSources] = useState([]); const [isPublic, setIsPublic] = useState(false); const [teams, setTeams] = useState<{ id: string; name: string }[]>([]); const [selectedTeamId, setSelectedTeamId] = useState(null); const [showAdvancedSources, setShowAdvancedSources] = useState(uiMode === 'expert'); const [wizardStep, setWizardStep] = useState(0); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(null); const [aiPrompt, setAiPrompt] = useState(''); const [isGenerating, setIsGenerating] = useState(false); const [generationFiles, setGenerationFiles] = useState([]); React.useEffect(() => { if (uiMode === 'expert') { fetchTeams(); } }, [uiMode]); // Hydrate from Magic Bar / external data React.useEffect(() => { if (initialData) { if (initialData.name) setName(initialData.name); if (initialData.description) setDescription(initialData.description); if (initialData.context) setContext(initialData.context); if (initialData.sources && Array.isArray(initialData.sources)) { const aiSources: ProjectSource[] = initialData.sources.map((s: any) => ({ id: crypto.randomUUID(), ...s })); setSources(aiSources); } // If we have initial data, jump to step 0 of the wizard (Basics) // but ensure we are in the wizard view setWizardStep(0); } }, [initialData]); const handleAiGenerate = async () => { if (!aiPrompt.trim()) return; setIsGenerating(true); setMessage('AI is analyzing your request and documents...'); try { const formData = new FormData(); formData.append('prompt', aiPrompt); generationFiles.forEach(file => { formData.append('files', file); }); const response = await fetch(`${getApiUrl()}/generator/generate-project`, { method: 'POST', body: formData }); if (!response.ok) throw new Error('AI generation failed'); const data = await response.json(); setName(data.name || ''); setDescription(data.description || ''); setContext(data.context || ''); if (data.sources && Array.isArray(data.sources)) { const aiSources: ProjectSource[] = data.sources.map((s: any) => ({ id: crypto.randomUUID(), ...s })); setSources(prev => [...prev, ...aiSources]); } setMessage('Success! AI has drafted your project. Review the fields in the next steps.'); setWizardStep(1); } catch (err: any) { console.error('AI Generation Error:', err); setMessage(`AI Error: ${err.message}`); } finally { setIsGenerating(false); } }; const fetchTeams = async () => { try { const { data, error } = await supabase.from('teams').select('id, name'); if (error) throw error; setTeams(data || []); } catch (err) { console.error('Failed to fetch teams:', err); } }; const isWizard = true; const projectWizardSteps = uiMode === 'expert' ? [wizardSteps[4], wizardSteps[0], wizardSteps[1], wizardSteps[2], expertAccessStep, wizardSteps[3]] : [wizardSteps[4], wizardSteps[0], wizardSteps[1], wizardSteps[2], wizardSteps[3]]; const reviewStepIndex = projectWizardSteps.length - 1; const accessStepIndex = uiMode === 'expert' ? 4 : -1; const currentWizardStep = projectWizardSteps[wizardStep] ?? projectWizardSteps[0]; const isFirstWizardStep = wizardStep === 0; const isLastWizardStep = wizardStep === reviewStepIndex; const appendSource = (source: ProjectSource) => { setSources((current) => [...current, source]); }; const handleAddLink = () => { if (!sourceUrl.trim()) { setMessage('Add a valid link before saving it.'); return; } appendSource({ id: crypto.randomUUID(), kind: 'link', label: sourceLabel.trim() || sourceUrl.trim(), url: sourceUrl.trim(), }); setSourceLabel(''); setSourceUrl(''); setMessage(null); }; const handleAddNote = () => { if (!noteContent.trim()) { setMessage('Write some note content before saving it.'); return; } appendSource({ id: crypto.randomUUID(), kind: 'note', label: noteLabel.trim() || 'Inline note', content: noteContent.trim(), }); setNoteLabel(''); setNoteContent(''); setMessage(null); }; const handleFileSelection = async (event: React.ChangeEvent) => { const files = Array.from(event.target.files ?? []); const nextSources: ProjectSource[] = []; for (const file of files) { const canExtractText = supportedTextMimeTypes.has(file.type) || /\.(md|txt|csv|json)$/i.test(file.name); let content: string | undefined; if (canExtractText) { content = (await file.text()).slice(0, 12000); } nextSources.push({ id: crypto.randomUUID(), kind: 'file', label: file.name, fileName: file.name, mimeType: file.type, size: file.size, content, extracted: Boolean(content), }); } if (nextSources.length) { setSources((current) => [...current, ...nextSources]); setMessage(null); } event.target.value = ''; }; const removeSource = (id: string) => { setSources((current) => current.filter((source) => source.id !== id)); }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!user) { setMessage('You must be signed in to create a project.'); return; } setSaving(true); setMessage(null); const contextPayload = buildContextPayload(context, sources); const { error } = await supabase.from('projects').insert({ name, description, context: contextPayload, owner_id: user.id, team_id: selectedTeamId, is_public: isPublic, status: 'active' }); if (error) { setMessage(error.message); } else { setName(''); setDescription(''); setContext(''); setSourceLabel(''); setSourceUrl(''); setNoteLabel(''); setNoteContent(''); setSources([]); setIsPublic(false); setWizardStep(0); setMessage('Project created successfully.'); window.setTimeout(() => onCreated?.(), 500); } setSaving(false); }; return (

Create Project

{uiMode === 'guided' ? 'Describe the goal, add relevant context, and create the workspace.' : 'Start a workspace for agents, tasks, context, and reviews.'}

{isWizard && (
{projectWizardSteps.map((step, index) => ( ))}
{currentWizardStep.title}

{currentWizardStep.description}

)} {wizardStep === 0 && (