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.'}