import { useState, useEffect } from "react";
import { useNavigate, useParams } from "react-router-dom";
import slug from "slug";
import type { User, Project } from "../lib/api";
import {
projectApi,
droneApi,
chatSessionApi,
type DroneRegistration,
type ChatSession,
type AiProvider,
providerApi,
} from "../lib/api";
import { socketClient } from "../lib/socket";
interface ProjectManagerProps {
user: User | null;
}
function NewProjectForm({
onCancel,
onSuccess,
}: {
onCancel: () => void;
onSuccess: () => void;
}) {
const [name, setName] = useState("");
const [slug, setSlug] = useState("");
const [gitUrl, setGitUrl] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name || !slug) {
setError("Name and slug are required");
return;
}
setSubmitting(true);
setError("");
try {
await projectApi.create({ name, slug, gitUrl: gitUrl || undefined });
onSuccess();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create project");
} finally {
setSubmitting(false);
}
};
return (
);
}
interface EditProjectFormProps {
project: Project;
onCancel: () => void;
onSuccess: () => void;
}
function EditProjectForm({
project,
onCancel,
onSuccess,
}: EditProjectFormProps) {
const [name, setName] = useState(project.name);
const [slugValue, setSlugValue] = useState(project.slug);
const [gitUrl, setGitUrl] = useState(project.gitUrl || "");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name || !slugValue) {
setError("Name and slug are required");
return;
}
setSubmitting(true);
setError("");
try {
await projectApi.update(project._id, {
name,
slug: slug(slugValue),
gitUrl: gitUrl || undefined,
});
onSuccess();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to update project");
} finally {
setSubmitting(false);
}
};
return (
);
}
interface ProjectInspectorProps {
project: Project;
onDelete: () => void;
onUpdate: () => void;
}
function ProjectInspector({ project, onDelete, onUpdate }: ProjectInspectorProps) {
const [deleting, setDeleting] = useState(false);
const [editing, setEditing] = useState(false);
const handleDelete = async () => {
if (
!confirm(
"Are you sure you want to delete this project? This cannot be undone.",
)
) {
return;
}
setDeleting(true);
try {
await projectApi.delete(project._id);
onDelete();
} catch (err) {
console.error("Failed to delete project", err);
} finally {
setDeleting(false);
}
};
return (
Project Inspector
{editing ? (
setEditing(false)}
onSuccess={() => {
setEditing(false);
onUpdate();
}}
/>
) : (
<>
Git URL
{project.gitUrl || (
Not configured
)}
Created
{new Date(project.createdAt).toLocaleDateString()}
>
)}
);
}
interface RightSidebarProps {
project: Project;
selectedDrone: DroneRegistration | null;
onSelectDrone: (drone: DroneRegistration) => void;
onOpenChatSession: (sessionId: string) => void;
}
function RightSidebar({
project,
selectedDrone,
onSelectDrone,
onOpenChatSession,
}: RightSidebarProps) {
const [drones, setDrones] = useState([]);
const [chatSessions, setChatSessions] = useState([]);
const [providers, setProviders] = useState([]);
const [showNewChatModal, setShowNewChatModal] = useState(false);
const [loading, setLoading] = useState(true);
const [deletingSessions, setDeletingSessions] = useState>(
new Set(),
);
useEffect(() => {
loadData();
}, [project]);
const loadData = async () => {
setLoading(true);
try {
// Load available drones (backend filters offline)
const availableDrones = await droneApi.getAll();
setDrones(availableDrones);
// Load chat sessions for this project
const sessions = await chatSessionApi.getAll(project._id);
setChatSessions(sessions);
// Load providers for new chat session
const allProviders = await providerApi.getAll();
setProviders(allProviders.sort((a, b) => a.name.localeCompare(b.name)));
} catch (err) {
console.error("Failed to load sidebar data", err);
} finally {
setLoading(false);
}
};
const handleCreateChatSession = async (data: {
providerId: string;
selectedModel: string;
mode: string;
name?: string;
}) => {
try {
const session = await chatSessionApi.create({
projectId: project._id,
providerId: data.providerId,
selectedModel: data.selectedModel,
mode: data.mode as any,
name: data.name,
});
setShowNewChatModal(false);
// Lock the drone to this session BEFORE navigating
if (selectedDrone) {
const success = await socketClient.requestSessionLock(
selectedDrone,
project,
session,
);
if (!success) {
console.error("Failed to lock drone session");
}
}
onOpenChatSession(session._id);
await loadData(); // Refresh list
} catch (err) {
console.error("Failed to create chat session", err);
}
};
const handleDeleteChatSession = async (sessionId: string) => {
if (!confirm("Delete this chat session?")) return;
setDeletingSessions((prev) => new Set(prev).add(sessionId));
try {
await chatSessionApi.delete(sessionId);
await loadData();
} catch (err) {
console.error("Failed to delete chat session", err);
} finally {
setDeletingSessions((prev) => {
const next = new Set(prev);
next.delete(sessionId);
return next;
});
}
};
return (
<>
{/* New Chat Session Modal */}
{showNewChatModal && (
setShowNewChatModal(false)}
onCreate={handleCreateChatSession}
/>
)}
>
);
}
interface NewChatSessionModalProps {
providers: AiProvider[];
selectedDrone: DroneRegistration | null;
onCancel: () => void;
onCreate: (data: {
providerId: string;
selectedModel: string;
mode: string;
name?: string;
}) => void;
}
function NewChatSessionModal({
providers,
selectedDrone,
onCancel,
onCreate,
}: NewChatSessionModalProps) {
const [selectedProviderId, setSelectedProviderId] = useState("");
const [selectedModel, setSelectedModel] = useState("");
const [mode, setMode] = useState("build");
const [name, setName] = useState("");
const [creating, setCreating] = useState(false);
const selectedProvider = providers.find((p) => p._id === selectedProviderId);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedProviderId || !selectedModel) {
return;
}
setCreating(true);
try {
onCreate({
providerId: selectedProviderId,
selectedModel,
mode,
name: name || undefined,
});
} catch (err) {
console.error("Failed to create chat session", err);
} finally {
setCreating(false);
}
};
return (
New Chat Session
{selectedDrone && (
Selected Drone
{selectedDrone.hostname}
{selectedDrone.workspaceDir}
)}
);
}
export default function ProjectManager({ user }: ProjectManagerProps) {
const navigate = useNavigate();
const { slug } = useParams();
const [projects, setProjects] = useState([]);
const [selectedProject, setSelectedProject] = useState(null);
const [selectedDrone, setSelectedDrone] = useState(null);
const [loading, setLoading] = useState(true);
const [showNewForm, setShowNewForm] = useState(false);
useEffect(() => {
loadProjects();
}, []);
useEffect(() => {
if (slug && projects.length > 0) {
const found = projects.find((p) => p.slug === slug);
setSelectedProject(found || null);
} else if (!slug) {
setSelectedProject(null);
}
}, [slug, projects]);
const loadProjects = async () => {
try {
const data = await projectApi.getAll();
setProjects(data);
} catch (err) {
console.error("Failed to load projects", err);
} finally {
setLoading(false);
}
};
const handleSelectProject = (project: Project) => {
navigate(`/projects/${project.slug}`);
};
const handleProjectCreated = () => {
setShowNewForm(false);
loadProjects();
};
const handleProjectDeleted = () => {
setSelectedProject(null);
loadProjects();
navigate("/projects");
};
const handleProjectUpdated = () => {
loadProjects();
};
const handleSelectDrone = (drone: DroneRegistration) => {
setSelectedDrone(drone);
};
const handleOpenChatSession = async (sessionId: string) => {
if (!selectedProject || !selectedDrone) return;
try {
const session = await chatSessionApi.get(sessionId);
const success = await socketClient.requestSessionLock(
selectedDrone,
selectedProject,
session,
);
if (!success) {
console.error("Failed to lock drone session");
return;
}
localStorage.setItem('dtp_drone_registration', JSON.stringify(selectedDrone));
navigate(`/projects/${selectedProject._id}/chat-session/${sessionId}`);
} catch (err) {
console.error("Failed to open chat session", err);
}
};
if (!user) {
return (
Please sign in to view projects.
);
}
if (loading) {
return (
);
}
return (
{/* Left Sidebar - Project List */}
{/* Main Content Area */}
{showNewForm ? (
setShowNewForm(false)}
onSuccess={handleProjectCreated}
/>
) : selectedProject ? (
<>
{/* Center - Project Inspector */}
{/* Right Sidebar - Drones & Chat Sessions */}
>
) : (
Select a project to view details
or create a new project to get started
)}
);
}