-
Notifications
You must be signed in to change notification settings - Fork 13
agent: @U0AJM7X8FBR Chat - /files page - Drag-n-Drop Files. #1594
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: test
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,75 @@ | ||
| "use client"; | ||
|
|
||
| import { useState, useCallback } from "react"; | ||
| import { FileTree } from "@/components/ai-elements/file-tree"; | ||
| import FileNodeComponent from "./FileNodeComponent"; | ||
| import SandboxFilePreview from "./SandboxFilePreview"; | ||
| import useSandboxes from "@/hooks/useSandboxes"; | ||
| import useSandboxFileContent from "@/hooks/useSandboxFileContent"; | ||
| import { Loader } from "lucide-react"; | ||
| import useUploadSandboxFiles from "@/hooks/useUploadSandboxFiles"; | ||
| import { useDragAndDrop } from "@/hooks/useDragAndDrop"; | ||
| import { toast } from "sonner"; | ||
| import { Loader, Upload } from "lucide-react"; | ||
| import type { FileNode } from "@/lib/sandboxes/parseFileTree"; | ||
|
|
||
| function getFirstFolderPath(nodes: FileNode[]): string { | ||
| for (const node of nodes) { | ||
| if (node.type === "folder") return node.path; | ||
| } | ||
| return ""; | ||
| } | ||
|
|
||
| function getParentFolder(filePath: string): string { | ||
| const parts = filePath.split("/"); | ||
| parts.pop(); | ||
| return parts.join("/"); | ||
| } | ||
|
|
||
| export default function SandboxFileTree() { | ||
| const { filetree, isLoading, error, refetch } = useSandboxes(); | ||
| const fileContent = useSandboxFileContent(); | ||
| const { upload, isUploading } = useUploadSandboxFiles(); | ||
| const [dropFolder, setDropFolder] = useState<string>(""); | ||
|
|
||
| const resolvedDropFolder = | ||
| dropFolder || | ||
| (fileContent.selectedPath ? getParentFolder(fileContent.selectedPath) : "") || | ||
| getFirstFolderPath(filetree); | ||
|
|
||
| const handleDrop = useCallback( | ||
| async (files: File[]) => { | ||
| if (!resolvedDropFolder) { | ||
| toast.error("No folder selected. Select a file or folder first."); | ||
| return; | ||
| } | ||
|
|
||
| toast.loading(`Uploading ${files.length} file${files.length > 1 ? "s" : ""}…`, { | ||
| id: "sandbox-upload", | ||
| }); | ||
|
|
||
| const results = await upload(files, resolvedDropFolder); | ||
|
|
||
| const succeeded = results.filter((r) => r.success).length; | ||
| const failed = results.filter((r) => !r.success); | ||
|
|
||
| toast.dismiss("sandbox-upload"); | ||
|
|
||
| if (succeeded > 0) { | ||
| toast.success(`${succeeded} file${succeeded > 1 ? "s" : ""} uploaded successfully`); | ||
| refetch(); | ||
| } | ||
|
|
||
| for (const f of failed) { | ||
| toast.error(`Failed to upload ${f.path}: ${f.error ?? "unknown error"}`); | ||
| } | ||
| }, | ||
| [resolvedDropFolder, upload, refetch], | ||
| ); | ||
|
|
||
| const { getRootProps, getInputProps, isDragging } = useDragAndDrop({ | ||
| onDrop: handleDrop, | ||
| disabled: isUploading, | ||
| }); | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
|
|
@@ -24,10 +84,7 @@ export default function SandboxFileTree() { | |
| return ( | ||
| <div className="text-destructive"> | ||
| <p>Failed to load files</p> | ||
| <button | ||
| onClick={() => refetch()} | ||
| className="text-sm underline hover:no-underline" | ||
| > | ||
| <button onClick={() => refetch()} className="text-sm underline hover:no-underline"> | ||
| Try again | ||
| </button> | ||
| </div> | ||
|
|
@@ -36,7 +93,8 @@ export default function SandboxFileTree() { | |
|
|
||
| if (filetree.length === 0) { | ||
| return ( | ||
| <div className="w-full max-w-md"> | ||
| <div {...getRootProps()} className="w-full max-w-md"> | ||
| <input {...getInputProps()} /> | ||
| <p className="text-sm text-muted-foreground">No files yet.</p> | ||
| </div> | ||
| ); | ||
|
|
@@ -45,12 +103,70 @@ export default function SandboxFileTree() { | |
| return ( | ||
| <div className="flex w-full flex-col gap-4 lg:flex-row"> | ||
| <div className="w-full lg:max-w-md lg:shrink-0"> | ||
| <h2 className="mb-2 text-lg font-medium">Repository Files</h2> | ||
| <FileTree selectedPath={fileContent.selectedPath} onSelect={fileContent.select}> | ||
| {filetree.map((node) => ( | ||
| <FileNodeComponent key={node.path} node={node} /> | ||
| ))} | ||
| </FileTree> | ||
| <div | ||
| {...getRootProps()} | ||
| className={`relative rounded-lg border border-dashed transition-colors ${ | ||
| isDragging | ||
| ? "border-primary bg-primary/5" | ||
| : "border-transparent hover:border-muted-foreground/30" | ||
| }`} | ||
| > | ||
| <input {...getInputProps()} /> | ||
|
|
||
| {isDragging && ( | ||
| <div className="pointer-events-none absolute inset-0 z-10 flex flex-col items-center justify-center gap-2 rounded-lg bg-background/80"> | ||
| <Upload className="h-8 w-8 text-primary" /> | ||
| <p className="text-sm font-medium text-primary"> | ||
| Drop files into{" "} | ||
| <span className="font-mono text-xs"> | ||
| {resolvedDropFolder || "repository root"} | ||
| </span> | ||
| </p> | ||
| </div> | ||
| )} | ||
|
|
||
| <div className="p-1"> | ||
| <div className="mb-2 flex items-center justify-between"> | ||
| <h2 className="text-lg font-medium">Repository Files</h2> | ||
| {isUploading && ( | ||
| <span className="flex items-center gap-1 text-xs text-muted-foreground"> | ||
| <Loader className="h-3 w-3 animate-spin" /> | ||
| Uploading… | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {resolvedDropFolder && ( | ||
| <div className="mb-2 flex items-center gap-1"> | ||
| <Upload className="h-3 w-3 text-muted-foreground" /> | ||
| <p className="truncate font-mono text-xs text-muted-foreground"> | ||
| Drop target:{" "} | ||
| <button | ||
| className="underline decoration-dotted hover:text-foreground" | ||
| onClick={() => setDropFolder("")} | ||
| title="Reset to default" | ||
| > | ||
| {resolvedDropFolder} | ||
| </button> | ||
| </p> | ||
| </div> | ||
| )} | ||
|
|
||
| <FileTree | ||
| selectedPath={fileContent.selectedPath} | ||
| onSelect={(path) => { | ||
| fileContent.select(path); | ||
| // Update drop folder to parent of selected file | ||
| const parent = getParentFolder(path); | ||
| if (parent) setDropFolder(parent); | ||
|
Comment on lines
+160
to
+161
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| }} | ||
| > | ||
| {filetree.map((node) => ( | ||
| <FileNodeComponent key={node.path} node={node} /> | ||
| ))} | ||
| </FileTree> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| {fileContent.selectedPath && ( | ||
| <SandboxFilePreview | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| "use client"; | ||
|
|
||
| import { useState } from "react"; | ||
| import { usePrivy } from "@privy-io/react-auth"; | ||
| import { uploadFilesToSandbox } from "@/lib/sandboxes/uploadFilesToSandbox"; | ||
| import type { UploadFileResult } from "@/lib/sandboxes/uploadFilesToSandbox"; | ||
|
|
||
| interface UseUploadSandboxFilesReturn { | ||
| upload: (files: File[], folder: string) => Promise<UploadFileResult[]>; | ||
| isUploading: boolean; | ||
| error: string | null; | ||
| } | ||
|
|
||
| export default function useUploadSandboxFiles(): UseUploadSandboxFilesReturn { | ||
| const { getAccessToken } = usePrivy(); | ||
| const [isUploading, setIsUploading] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
|
|
||
| const upload = async (files: File[], folder: string): Promise<UploadFileResult[]> => { | ||
| setIsUploading(true); | ||
| setError(null); | ||
| try { | ||
| const accessToken = await getAccessToken(); | ||
| if (!accessToken) { | ||
| throw new Error("Please sign in to upload files"); | ||
| } | ||
| const response = await uploadFilesToSandbox(accessToken, files, folder); | ||
| if (response.status === "error") { | ||
| throw new Error(response.error ?? "Upload failed"); | ||
| } | ||
| return response.uploaded ?? []; | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : "Upload failed"; | ||
| setError(message); | ||
| return []; | ||
|
Comment on lines
+32
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } finally { | ||
| setIsUploading(false); | ||
| } | ||
| }; | ||
|
|
||
| return { upload, isUploading, error }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { NEW_API_BASE_URL } from "@/lib/consts"; | ||
|
|
||
| export interface UploadFileResult { | ||
| path: string; | ||
| success: boolean; | ||
| error?: string; | ||
| } | ||
|
|
||
| export interface UploadFilesResponse { | ||
| status: "success" | "error"; | ||
| uploaded?: UploadFileResult[]; | ||
| error?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Uploads files to the account's GitHub org submodule via the sandbox files API. | ||
| * | ||
| * @param accessToken - Privy access token for authentication | ||
| * @param files - Array of File objects to upload | ||
| * @param folder - Target folder path within the repo (e.g. ".openclaw/workspace/orgs/myorg") | ||
| * @returns Upload results per file | ||
| */ | ||
| export async function uploadFilesToSandbox( | ||
| accessToken: string, | ||
| files: File[], | ||
| folder: string, | ||
| ): Promise<UploadFilesResponse> { | ||
| const formData = new FormData(); | ||
| for (const file of files) { | ||
| formData.append("files", file); | ||
| } | ||
| formData.append("folder", folder); | ||
|
|
||
| const response = await fetch(`${NEW_API_BASE_URL}/api/sandboxes/files`, { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${accessToken}`, | ||
| }, | ||
| body: formData, | ||
| }); | ||
|
|
||
| const data: UploadFilesResponse = await response.json(); | ||
| return data; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This guard makes the new empty-state drop zone unusable. In the
filetree.length === 0branch we still mountgetRootProps()(components/Sandboxes/SandboxFileTree.tsx:94-99), butresolvedDropFoldercan only come fromdropFolder,selectedPath, orgetFirstFolderPath(filetree). When the tree is empty all three are"", so every drop immediately returns here with"No folder selected". That means a brand-new org repo cannot be populated via drag-and-drop at all.Useful? React with 👍 / 👎.