open, interoperable sandbox platform for agents and humans 📦 ✨ pocketenv.io
claude-code atproto sandbox openclaw agent
at main 233 lines 7.2 kB view raw
1import { useEffect, useState } from "react"; 2import { createPortal } from "react-dom"; 3import { useForm } from "react-hook-form"; 4import { z } from "zod"; 5import { zodResolver } from "@hookform/resolvers/zod"; 6import { 7 useAddFileMutation, 8 useFileQuery, 9 useUpdateFileMutation, 10} from "../../../hooks/useFile"; 11import { useSodium } from "../../../hooks/useSodium"; 12import { PUBLIC_KEY } from "../../../consts"; 13import { useNotyf } from "../../../hooks/useNotyf"; 14 15const schema = z.object({ 16 path: z.string().min(1, "Path is required"), 17 content: z.string().min(1, "Content is required"), 18}); 19 20type FormValues = z.infer<typeof schema>; 21 22export type AddFileModalProps = { 23 isOpen: boolean; 24 onClose: () => void; 25 sandboxId: string; 26 fileId?: string; 27}; 28 29function AddFileModal({ 30 isOpen, 31 onClose, 32 sandboxId, 33 fileId, 34}: AddFileModalProps) { 35 const sodium = useSodium(); 36 const notyf = useNotyf(); 37 const [isLoading, setIsLoading] = useState(false); 38 const { mutateAsync: addFile } = useAddFileMutation(); 39 const { data } = useFileQuery(fileId!); 40 const { mutateAsync: updateFile } = useUpdateFileMutation(); 41 const { 42 register, 43 handleSubmit, 44 reset, 45 formState: { errors }, 46 } = useForm<FormValues>({ 47 resolver: zodResolver(schema), 48 }); 49 50 useEffect(() => { 51 if (data) { 52 reset({ path: data.file.path }); 53 } 54 }, [data, reset]); 55 56 useEffect(() => { 57 const handleEscapeKey = (event: KeyboardEvent) => { 58 if (event.key === "Escape" && isOpen) { 59 reset(); 60 onClose(); 61 } 62 }; 63 64 document.addEventListener("keydown", handleEscapeKey); 65 return () => { 66 document.removeEventListener("keydown", handleEscapeKey); 67 }; 68 }, [isOpen, onClose, reset]); 69 70 const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => { 71 e.stopPropagation(); 72 if (e.target === e.currentTarget) { 73 reset(); 74 onClose(); 75 } 76 }; 77 78 const handleContentClick = (e: React.MouseEvent<HTMLDivElement>) => { 79 e.stopPropagation(); 80 }; 81 82 const handleCloseButton = (e: React.MouseEvent<HTMLButtonElement>) => { 83 reset(); 84 e.stopPropagation(); 85 onClose(); 86 }; 87 88 const onSubmit = async (data: FormValues) => { 89 setIsLoading(true); 90 const sealed = sodium.cryptoBoxSeal( 91 sodium.fromString(data.content), 92 sodium.fromHex(PUBLIC_KEY), 93 ); 94 try { 95 if (fileId) { 96 await updateFile({ 97 id: fileId, 98 path: data.path, 99 content: sodium.toBase64( 100 sealed, 101 sodium.base64Variants.URLSAFE_NO_PADDING, 102 ), 103 }); 104 setIsLoading(false); 105 reset(); 106 onClose(); 107 notyf.open("primary", "File updated successfully!"); 108 return; 109 } 110 await addFile({ 111 sandboxId, 112 path: data.path, 113 content: sodium.toBase64( 114 sealed, 115 sodium.base64Variants.URLSAFE_NO_PADDING, 116 ), 117 }); 118 setIsLoading(false); 119 reset(); 120 onClose(); 121 notyf.open("primary", "File added successfully!"); 122 } catch { 123 notyf.open("error", "Failed to add file!"); 124 setIsLoading(false); 125 reset(); 126 onClose(); 127 return; 128 } 129 }; 130 131 if (!isOpen) return null; 132 133 return createPortal( 134 <> 135 <div 136 className="overlay modal modal-middle overlay-open:opacity-100 overlay-open:duration-300 open opened" 137 role="dialog" 138 style={{ outline: "none", zIndex: 80 }} 139 onClick={handleBackdropClick} 140 onMouseDown={handleBackdropClick} 141 > 142 <div 143 className={`overlay-animation-target modal-dialog overlay-open:duration-300 transition-all ease-out modal-dialog-lg overlay-open:mt-4 mt-12`} 144 onClick={handleContentClick} 145 onMouseDown={handleContentClick} 146 > 147 <div className="modal-content"> 148 <div className="modal-header"> 149 <div className="flex-1">{fileId ? "Edit File" : "Add File"}</div> 150 <button 151 type="button" 152 className="btn btn-text btn-circle btn-sm absolute end-3 top-3" 153 aria-label="Close" 154 onClick={handleCloseButton} 155 onMouseDown={(e) => e.stopPropagation()} 156 > 157 <span className="icon-[tabler--x] size-4"></span> 158 </button> 159 </div> 160 <form onSubmit={handleSubmit(onSubmit)}> 161 <div className="modal-body"> 162 <label className="label"> 163 <span className="label-text font-bold mb-1 text-[14px]"> 164 Path 165 </span> 166 </label> 167 <div className="input input-bordered w-full input-lg text-[15px] font-semibold bg-transparent"> 168 <input 169 type="text" 170 placeholder="File Mount Path, e.g /root/.openclaw/openclaw.json" 171 className={`grow`} 172 autoComplete="off" 173 data-1p-ignore 174 data-lpignore="true" 175 data-form-type="other" 176 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 177 {...register("path")} 178 /> 179 </div> 180 {errors.path && ( 181 <span className="text-error text-[12px] mt-1"> 182 {errors.path.message} 183 </span> 184 )} 185 <div className="mt-5"> 186 <label className="label"> 187 <span className="label-text font-bold mb-1 text-[14px]"> 188 Content 189 </span> 190 </label> 191 <textarea 192 className={`textarea max-w-full h-[250px] text-[14px] font-semibold`} 193 aria-label="Textarea" 194 placeholder="File Content" 195 style={{ fontFamily: "CaskaydiaNerdFontMonoRegular" }} 196 {...register("content")} 197 ></textarea> 198 {errors.content && ( 199 <span className="text-error text-[12px] mt-1 block"> 200 {errors.content.message} 201 </span> 202 )} 203 </div> 204 </div> 205 <div className="modal-footer"> 206 <button 207 type="submit" 208 className="btn btn-primary w-36 font-semibold" 209 > 210 {isLoading && ( 211 <span className="loading loading-spinner loading-xs mr-1.5"></span> 212 )} 213 {fileId ? "Save Changes" : "Add File"} 214 </button> 215 </div> 216 </form> 217 </div> 218 </div> 219 </div> 220 221 <div 222 data-overlay-backdrop-template="" 223 style={{ zIndex: 79 }} 224 className="overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto opacity-75" 225 onClick={handleBackdropClick} 226 onMouseDown={(e) => e.stopPropagation()} 227 ></div> 228 </>, 229 document.body, 230 ); 231} 232 233export default AddFileModal;