summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes/files.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/routes/files.tsx')
-rw-r--r--makima/frontend/src/routes/files.tsx221
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>
);
}