open, interoperable sandbox platform for agents and humans 📦 ✨
pocketenv.io
claude-code
atproto
sandbox
openclaw
agent
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;