diff options
Diffstat (limited to 'makima/frontend/src/routes/files.tsx')
| -rw-r--r-- | makima/frontend/src/routes/files.tsx | 221 |
1 files changed, 194 insertions, 27 deletions
diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx index 3ba2d52..6cfb3ca 100644 --- a/makima/frontend/src/routes/files.tsx +++ b/makima/frontend/src/routes/files.tsx @@ -12,8 +12,8 @@ import { useFileSubscription, type FileUpdateEvent, } from "../hooks/useFileSubscription"; -import type { FileDetail as FileDetailType, BodyElement, Task } from "../lib/api"; -import { createTask } from "../lib/api"; +import type { FileDetail as FileDetailType, BodyElement, Task, ContractSummary } from "../lib/api"; +import { createTask, listContracts } from "../lib/api"; import { useAuth } from "../contexts/AuthContext"; export default function FilesPage() { @@ -59,6 +59,14 @@ function FilesPageContent() { const [focusedElement, setFocusedElement] = useState<FocusedElement | null>(null); const [suggestedPrompt, setSuggestedPrompt] = useState<string | null>(null); const [createdTask, setCreatedTask] = useState<Task | null>(null); + // Contract selection modal state for task creation + const [showContractModal, setShowContractModal] = useState(false); + const [contracts, setContracts] = useState<ContractSummary[]>([]); + const [contractsLoading, setContractsLoading] = useState(false); + const [pendingTaskData, setPendingTaskData] = useState<{ name: string; plan: string } | null>(null); + // Contract selection modal state for file creation + const [showFileContractModal, setShowFileContractModal] = useState(false); + const [pendingFileData, setPendingFileData] = useState<{ name: string; body?: BodyElement[] } | null>(null); const pendingUpdateRef = useRef(false); // Track the last version we sent to detect our own updates const lastSentVersionRef = useRef<number | null>(null); @@ -548,10 +556,10 @@ function FilesPageContent() { [fileDetail] ); - // Create a mesh task from an element + // Create a mesh task from an element - shows contract selection modal const handleCreateTaskFromElement = useCallback( async (index: number, selectedText?: string) => { - if (!fileDetail) return; + if (!fileDetail || contractsLoading) return; const element = fileDetail.body[index]; if (!element) return; @@ -578,57 +586,98 @@ function FilesPageContent() { // Create a task name from the content const name = content.slice(0, 60) + (content.length > 60 ? "..." : ""); + // Store pending task data and show contract selection modal + setPendingTaskData({ name, plan: content }); + setContractsLoading(true); + try { + const response = await listContracts(); + setContracts(response.contracts); + setShowContractModal(true); + } catch (e) { + console.error("Failed to load contracts:", e); + } finally { + setContractsLoading(false); + } + }, + [fileDetail, contractsLoading] + ); + + // Create task with selected contract + const handleCreateTaskWithContract = useCallback( + async (contractId: string) => { + if (!pendingTaskData || !fileDetail) return; + setShowContractModal(false); try { const task = await createTask({ - name, - plan: content, + contractId, + name: pendingTaskData.name, + plan: pendingTaskData.plan, description: `Created from ${fileDetail.name}`, }); setCreatedTask(task); + setPendingTaskData(null); } catch (err) { console.error("Failed to create task:", err); } }, - [fileDetail] + [pendingTaskData, fileDetail] ); + // Open contract selection modal for file creation const handleCreate = useCallback(async () => { - if (creating) return; + if (creating || contractsLoading) return; + setContractsLoading(true); + try { + const response = await listContracts(); + setContracts(response.contracts); + setPendingFileData({ name: `Untitled ${new Date().toLocaleDateString()}` }); + setShowFileContractModal(true); + } catch (e) { + console.error("Failed to load contracts:", e); + } finally { + setContractsLoading(false); + } + }, [creating, contractsLoading]); + + // Create file with selected contract + const handleCreateFileWithContract = useCallback(async (contractId: string) => { + if (creating || !pendingFileData) return; + setShowFileContractModal(false); setCreating(true); try { const newFile = await saveFile({ - name: `Untitled ${new Date().toLocaleDateString()}`, + contractId, + name: pendingFileData.name, + body: pendingFileData.body, transcript: [], }); if (newFile) { + // If there's body content, update it + if (pendingFileData.body && pendingFileData.body.length > 0) { + await editFile(newFile.id, { body: pendingFileData.body, version: newFile.version }); + } navigate(`/files/${newFile.id}`); } } finally { setCreating(false); + setPendingFileData(null); } - }, [creating, saveFile, navigate]); + }, [creating, pendingFileData, saveFile, editFile, navigate]); const handleUploadMarkdown = useCallback(async (name: string, body: BodyElement[]) => { - if (creating) return; - setCreating(true); + if (creating || contractsLoading) return; + setContractsLoading(true); try { - const newFile = await saveFile({ - name, - transcript: [], - }); - if (newFile) { - // Update with the parsed body - const updated = await editFile(newFile.id, { body, version: newFile.version }); - if (updated) { - navigate(`/files/${updated.id}`); - } else { - navigate(`/files/${newFile.id}`); - } - } + const response = await listContracts(); + setContracts(response.contracts); + setPendingFileData({ name, body }); + setShowFileContractModal(true); + } catch (e) { + console.error("Failed to load contracts:", e); } finally { - setCreating(false); + setContractsLoading(false); } - }, [creating, saveFile, editFile, navigate]); + }, [creating, contractsLoading]); // Conflict resolution handlers const handleConflictReload = useCallback(async () => { @@ -808,6 +857,124 @@ function FilesPageContent() { </div> </div> )} + + {/* Contract Selection Modal for Task Creation */} + {showContractModal && ( + <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> + <div className="bg-[#0d1117] border border-[#30363d] rounded-lg max-w-md w-full mx-4 max-h-[80vh] flex flex-col"> + <div className="p-4 border-b border-[#30363d] flex justify-between items-center"> + <h2 className="text-lg font-semibold text-white">Select Contract for Task</h2> + <button + onClick={() => { + setShowContractModal(false); + setPendingTaskData(null); + }} + className="text-[#8b949e] hover:text-white" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 overflow-y-auto flex-1"> + {contracts.length === 0 ? ( + <div className="text-center py-8"> + <p className="text-[#8b949e] mb-4">No contracts found. Create a contract first.</p> + <button + onClick={() => { + setShowContractModal(false); + setPendingTaskData(null); + navigate("/contracts"); + }} + className="px-4 py-2 bg-[#238636] hover:bg-[#2ea043] text-white rounded-md text-sm" + > + Create Contract + </button> + </div> + ) : ( + <div className="space-y-2"> + {contracts.map((contract) => ( + <button + key={contract.id} + onClick={() => handleCreateTaskWithContract(contract.id)} + className="w-full text-left p-3 rounded-md border border-[#30363d] hover:border-[#58a6ff] hover:bg-[#161b22] transition-colors" + > + <div className="flex items-center justify-between"> + <span className="text-white font-medium">{contract.name}</span> + <span className="text-xs px-2 py-0.5 rounded bg-[#21262d] text-[#8b949e]"> + {contract.phase} + </span> + </div> + {contract.description && ( + <p className="text-sm text-[#8b949e] mt-1 line-clamp-2">{contract.description}</p> + )} + </button> + ))} + </div> + )} + </div> + </div> + </div> + )} + + {/* Contract Selection Modal for File Creation */} + {showFileContractModal && ( + <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50"> + <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] max-w-md w-full mx-4 max-h-[80vh] flex flex-col"> + <div className="p-4 border-b border-[rgba(117,170,252,0.15)] flex justify-between items-center"> + <h2 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff]">Select Contract for File</h2> + <button + onClick={() => { + setShowFileContractModal(false); + setPendingFileData(null); + }} + className="text-[#7788aa] hover:text-[#9bc3ff] transition-colors" + > + <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + <div className="p-4 overflow-y-auto flex-1"> + {contracts.length === 0 ? ( + <div className="text-center py-8"> + <p className="text-[#7788aa] font-mono text-xs mb-4">No contracts found. Create a contract first.</p> + <button + onClick={() => { + setShowFileContractModal(false); + setPendingFileData(null); + navigate("/contracts"); + }} + className="px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] transition-colors" + > + Create Contract + </button> + </div> + ) : ( + <div className="space-y-2"> + {contracts.map((contract) => ( + <button + key={contract.id} + onClick={() => handleCreateFileWithContract(contract.id)} + className="w-full text-left p-3 border border-[rgba(117,170,252,0.15)] bg-[#0a1525] hover:border-[rgba(117,170,252,0.35)] hover:bg-[#0d1b2d] transition-colors" + > + <div className="flex items-center justify-between"> + <span className="text-[#9bc3ff] font-mono text-xs">{contract.name}</span> + <span className="text-[10px] font-mono uppercase px-2 py-0.5 border border-[rgba(117,170,252,0.25)] text-[#75aafc]"> + {contract.phase} + </span> + </div> + {contract.description && ( + <p className="text-[10px] font-mono text-[#7788aa] mt-1 line-clamp-2">{contract.description}</p> + )} + </button> + ))} + </div> + )} + </div> + </div> + </div> + )} </div> ); } |
