OhMyDitzzy commited on
Commit
c0ee8b0
·
1 Parent(s): 83f446a
package.json CHANGED
@@ -41,10 +41,13 @@
41
  "cheerio": "^1.1.2",
42
  "class-variance-authority": "^0.7.1",
43
  "clsx": "^2.1.1",
 
44
  "express": "^5.2.1",
 
45
  "form-data": "^4.0.5",
46
  "framer-motion": "^12.26.2",
47
  "lucide-react": "^0.562.0",
 
48
  "prismjs": "^1.30.0",
49
  "react": "^19.2.3",
50
  "react-dom": "^19.2.3",
 
41
  "cheerio": "^1.1.2",
42
  "class-variance-authority": "^0.7.1",
43
  "clsx": "^2.1.1",
44
+ "crypto-js": "^4.2.0",
45
  "express": "^5.2.1",
46
+ "file-type": "^21.3.0",
47
  "form-data": "^4.0.5",
48
  "framer-motion": "^12.26.2",
49
  "lucide-react": "^0.562.0",
50
+ "multer": "^2.0.2",
51
  "prismjs": "^1.30.0",
52
  "react": "^19.2.3",
53
  "react-dom": "^19.2.3",
src/client/hooks/usePlugin.ts CHANGED
@@ -1,14 +1,22 @@
1
  import { useState, useEffect } from "react";
2
 
 
 
 
 
 
 
3
  export interface PluginParameter {
4
  name: string;
5
- type: "string" | "number" | "boolean" | "array" | "object";
6
  required: boolean;
7
  description: string;
8
  example?: any;
9
  default?: any;
10
  enum?: any[];
11
  pattern?: string;
 
 
12
  }
13
 
14
  export interface PluginResponse {
@@ -37,6 +45,10 @@ export interface PluginMetadata {
37
  responses?: {
38
  [statusCode: number]: PluginResponse;
39
  };
 
 
 
 
40
  }
41
 
42
  export interface ApiStats {
 
1
  import { useState, useEffect } from "react";
2
 
3
+ export interface FileConstraints {
4
+ maxSize?: number;
5
+ acceptedTypes?: string[];
6
+ acceptedExtensions?: string[];
7
+ }
8
+
9
  export interface PluginParameter {
10
  name: string;
11
+ type: "string" | "number" | "boolean" | "array" | "object" | "file";
12
  required: boolean;
13
  description: string;
14
  example?: any;
15
  default?: any;
16
  enum?: any[];
17
  pattern?: string;
18
+ fileConstraints?: FileConstraints;
19
+ acceptUrl?: boolean;
20
  }
21
 
22
  export interface PluginResponse {
 
45
  responses?: {
46
  [statusCode: number]: PluginResponse;
47
  };
48
+ disabled?: boolean;
49
+ deprecated?: boolean;
50
+ disabledReason?: string;
51
+ deprecatedReason?: string;
52
  }
53
 
54
  export interface ApiStats {
src/components/FileUpload.tsx ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useCallback } from "react";
2
+ import { Upload, X, File, AlertCircle, Link as LinkIcon } from "lucide-react";
3
+ import { Input } from "@/components/ui/input";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
6
+ import { FileConstraints } from "@/server/types/plugin";
7
+
8
+ interface FileUploadProps {
9
+ name: string;
10
+ description: string;
11
+ fileConstraints?: FileConstraints;
12
+ acceptUrl?: boolean;
13
+ onFileChange: (file: File | null) => void;
14
+ onUrlChange?: (url: string) => void;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export function FileUpload({
19
+ name,
20
+ description,
21
+ fileConstraints,
22
+ acceptUrl = false,
23
+ onFileChange,
24
+ onUrlChange,
25
+ disabled = false,
26
+ }: FileUploadProps) {
27
+ const [selectedFile, setSelectedFile] = useState<File | null>(null);
28
+ const [fileUrl, setFileUrl] = useState<string>("");
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [dragActive, setDragActive] = useState(false);
31
+ const [mode, setMode] = useState<"upload" | "url">("upload");
32
+ const fileInputRef = useRef<HTMLInputElement>(null);
33
+
34
+ const formatFileSize = (bytes: number): string => {
35
+ if (bytes === 0) return "0 Bytes";
36
+ const k = 1024;
37
+ const sizes = ["Bytes", "KB", "MB", "GB"];
38
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
39
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i];
40
+ };
41
+
42
+ const validateFile = (file: File): string | null => {
43
+ if (!fileConstraints) return null;
44
+
45
+ if (fileConstraints.maxSize && file.size > fileConstraints.maxSize) {
46
+ return `File size (${formatFileSize(file.size)}) exceeds maximum allowed size (${formatFileSize(fileConstraints.maxSize)})`;
47
+ }
48
+
49
+ if (fileConstraints.acceptedTypes && fileConstraints.acceptedTypes.length > 0) {
50
+ if (!fileConstraints.acceptedTypes.includes(file.type)) {
51
+ return `File type ${file.type} is not accepted. Allowed types: ${fileConstraints.acceptedTypes.join(", ")}`;
52
+ }
53
+ }
54
+
55
+ if (fileConstraints.acceptedExtensions && fileConstraints.acceptedExtensions.length > 0) {
56
+ const extension = "." + file.name.split(".").pop()?.toLowerCase();
57
+ if (!fileConstraints.acceptedExtensions.includes(extension)) {
58
+ return `File extension ${extension} is not accepted. Allowed extensions: ${fileConstraints.acceptedExtensions.join(", ")}`;
59
+ }
60
+ }
61
+
62
+ return null;
63
+ };
64
+
65
+ const handleFileSelect = useCallback((file: File) => {
66
+ const validationError = validateFile(file);
67
+
68
+ if (validationError) {
69
+ setError(validationError);
70
+ setSelectedFile(null);
71
+ onFileChange(null);
72
+ return;
73
+ }
74
+
75
+ setError(null);
76
+ setSelectedFile(file);
77
+ onFileChange(file);
78
+ }, [fileConstraints, onFileChange]);
79
+
80
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
81
+ const file = e.target.files?.[0];
82
+ if (file) {
83
+ handleFileSelect(file);
84
+ }
85
+ };
86
+
87
+ const handleDrag = (e: React.DragEvent) => {
88
+ e.preventDefault();
89
+ e.stopPropagation();
90
+ if (e.type === "dragenter" || e.type === "dragover") {
91
+ setDragActive(true);
92
+ } else if (e.type === "dragleave") {
93
+ setDragActive(false);
94
+ }
95
+ };
96
+
97
+ const handleDrop = (e: React.DragEvent) => {
98
+ e.preventDefault();
99
+ e.stopPropagation();
100
+ setDragActive(false);
101
+
102
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
103
+ handleFileSelect(e.dataTransfer.files[0]);
104
+ }
105
+ };
106
+
107
+ const handleRemoveFile = () => {
108
+ setSelectedFile(null);
109
+ setError(null);
110
+ onFileChange(null);
111
+ if (fileInputRef.current) {
112
+ fileInputRef.current.value = "";
113
+ }
114
+ };
115
+
116
+ const handleUrlChange = (url: string) => {
117
+ setFileUrl(url);
118
+ if (onUrlChange) {
119
+ onUrlChange(url);
120
+ }
121
+ };
122
+
123
+ const getAcceptAttribute = (): string | undefined => {
124
+ if (!fileConstraints) return undefined;
125
+
126
+ const types = [];
127
+ if (fileConstraints.acceptedTypes) {
128
+ types.push(...fileConstraints.acceptedTypes);
129
+ }
130
+ if (fileConstraints.acceptedExtensions) {
131
+ types.push(...fileConstraints.acceptedExtensions);
132
+ }
133
+
134
+ return types.length > 0 ? types.join(",") : undefined;
135
+ };
136
+
137
+ const renderContent = () => {
138
+ if (!acceptUrl) {
139
+ return (
140
+ <div className="space-y-3">
141
+ {renderUploadArea()}
142
+ {renderConstraints()}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <Tabs value={mode} onValueChange={(v) => setMode(v as "upload" | "url")} className="w-full">
149
+ <TabsList className="grid w-full grid-cols-2 bg-black/30">
150
+ <TabsTrigger value="upload" className="data-[state=active]:bg-purple-500/20">
151
+ <Upload className="w-4 h-4 mr-2" />
152
+ Upload File
153
+ </TabsTrigger>
154
+ <TabsTrigger value="url" className="data-[state=active]:bg-purple-500/20">
155
+ <LinkIcon className="w-4 h-4 mr-2" />
156
+ Use URL
157
+ </TabsTrigger>
158
+ </TabsList>
159
+
160
+ <TabsContent value="upload" className="mt-3 space-y-3">
161
+ {renderUploadArea()}
162
+ {renderConstraints()}
163
+ </TabsContent>
164
+
165
+ <TabsContent value="url" className="mt-3 space-y-3">
166
+ <Input
167
+ type="url"
168
+ placeholder="https://example.com/file.jpg"
169
+ value={fileUrl}
170
+ onChange={(e) => handleUrlChange(e.target.value)}
171
+ disabled={disabled}
172
+ className="bg-black/50 border-white/10 text-white focus:border-purple-500"
173
+ />
174
+ <p className="text-xs text-gray-500">
175
+ Enter the URL of the file you want to process
176
+ </p>
177
+ {renderConstraints()}
178
+ </TabsContent>
179
+ </Tabs>
180
+ );
181
+ };
182
+
183
+ const renderUploadArea = () => (
184
+ <>
185
+ <div
186
+ className={`relative border-2 border-dashed rounded-lg p-8 transition-all ${
187
+ dragActive
188
+ ? "border-purple-500 bg-purple-500/10"
189
+ : "border-white/20 bg-black/30"
190
+ } ${disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:border-purple-500/50"}`}
191
+ onDragEnter={handleDrag}
192
+ onDragLeave={handleDrag}
193
+ onDragOver={handleDrag}
194
+ onDrop={handleDrop}
195
+ onClick={() => !disabled && fileInputRef.current?.click()}
196
+ >
197
+ <input
198
+ ref={fileInputRef}
199
+ type="file"
200
+ onChange={handleFileChange}
201
+ accept={getAcceptAttribute()}
202
+ disabled={disabled}
203
+ className="hidden"
204
+ />
205
+
206
+ {selectedFile ? (
207
+ <div className="flex items-center justify-between">
208
+ <div className="flex items-center gap-3">
209
+ <div className="p-2 bg-purple-500/20 rounded">
210
+ <File className="w-6 h-6 text-purple-400" />
211
+ </div>
212
+ <div>
213
+ <p className="text-sm font-medium text-white">{selectedFile.name}</p>
214
+ <p className="text-xs text-gray-400">{formatFileSize(selectedFile.size)}</p>
215
+ </div>
216
+ </div>
217
+ <Button
218
+ type="button"
219
+ variant="ghost"
220
+ size="sm"
221
+ onClick={(e) => {
222
+ e.stopPropagation();
223
+ handleRemoveFile();
224
+ }}
225
+ disabled={disabled}
226
+ className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
227
+ >
228
+ <X className="w-4 h-4" />
229
+ </Button>
230
+ </div>
231
+ ) : (
232
+ <div className="text-center">
233
+ <Upload className="w-12 h-12 text-gray-400 mx-auto mb-3" />
234
+ <p className="text-sm text-gray-300 mb-1">
235
+ {dragActive ? "Drop file here" : "Click to upload or drag and drop"}
236
+ </p>
237
+ <p className="text-xs text-gray-500">{description}</p>
238
+ </div>
239
+ )}
240
+ </div>
241
+
242
+ {error && (
243
+ <div className="flex items-start gap-2 p-3 bg-red-500/10 border border-red-500/30 rounded-lg">
244
+ <AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
245
+ <p className="text-sm text-red-300">{error}</p>
246
+ </div>
247
+ )}
248
+ </>
249
+ );
250
+
251
+ const renderConstraints = () => {
252
+ if (!fileConstraints) return null;
253
+
254
+ return (
255
+ <div className="space-y-2">
256
+ {fileConstraints.maxSize && (
257
+ <p className="text-xs text-gray-500">
258
+ • Max file size: {formatFileSize(fileConstraints.maxSize)}
259
+ </p>
260
+ )}
261
+ {fileConstraints.acceptedTypes && fileConstraints.acceptedTypes.length > 0 && (
262
+ <p className="text-xs text-gray-500">
263
+ • Accepted types: {fileConstraints.acceptedTypes.join(", ")}
264
+ </p>
265
+ )}
266
+ {fileConstraints.acceptedExtensions && fileConstraints.acceptedExtensions.length > 0 && (
267
+ <p className="text-xs text-gray-500">
268
+ • Accepted extensions: {fileConstraints.acceptedExtensions.join(", ")}
269
+ </p>
270
+ )}
271
+ </div>
272
+ );
273
+ };
274
+
275
+ return (
276
+ <div className="space-y-2">
277
+ <label className="block text-sm text-gray-300">
278
+ {name}
279
+ <span className="text-xs text-gray-500 ml-2">(file)</span>
280
+ </label>
281
+ {renderContent()}
282
+ </div>
283
+ );
284
+ }
src/components/PluginCard.tsx CHANGED
@@ -8,6 +8,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
8
  import { PluginMetadata } from "@/client/hooks/usePlugin";
9
  import { Play, ChevronDown, ChevronUp, Copy, Check, AlertTriangle, XCircle } from "lucide-react";
10
  import { CodeBlock } from "@/components/CodeBlock";
 
11
  import { getApiUrl } from "@/lib/api-url";
12
 
13
  interface PluginCardProps {
@@ -24,6 +25,8 @@ const methodColors: Record<string, string> = {
24
 
25
  export function PluginCard({ plugin }: PluginCardProps) {
26
  const [paramValues, setParamValues] = useState<Record<string, string>>({});
 
 
27
  const [response, setResponse] = useState<any>(null);
28
  const [responseHeaders, setResponseHeaders] = useState<Record<string, string>>({});
29
  const [requestUrl, setRequestUrl] = useState<string>("");
@@ -39,6 +42,14 @@ export function PluginCard({ plugin }: PluginCardProps) {
39
  setParamValues((prev) => ({ ...prev, [paramName]: value }));
40
  };
41
 
 
 
 
 
 
 
 
 
42
  const handleExecute = async () => {
43
  if (isDisabled) {
44
  setResponse({
@@ -59,12 +70,23 @@ export function PluginCard({ plugin }: PluginCardProps) {
59
  let url = "/api" + plugin.endpoint;
60
  let fullUrl = getApiUrl(plugin.endpoint);
61
 
 
 
 
 
62
  if (plugin.method === "GET" && plugin.parameters?.query) {
63
  const queryParams = new URLSearchParams();
64
  plugin.parameters.query.forEach((param) => {
65
- const value = paramValues[param.name];
66
- if (value) {
67
- queryParams.append(param.name, value);
 
 
 
 
 
 
 
68
  }
69
  });
70
 
@@ -80,7 +102,34 @@ export function PluginCard({ plugin }: PluginCardProps) {
80
  method: plugin.method,
81
  };
82
 
83
- if (["POST", "PUT", "PATCH"].includes(plugin.method) && plugin.parameters?.body) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  const bodyData: Record<string, any> = {};
85
  plugin.parameters.body.forEach((param) => {
86
  const value = paramValues[param.name];
@@ -94,13 +143,53 @@ export function PluginCard({ plugin }: PluginCardProps) {
94
  };
95
  }
96
 
97
- const res = await fetch(url, fetchOptions);
98
- const data = await res.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  const headers: Record<string, string> = {};
101
- res.headers.forEach((value, key) => {
102
- headers[key] = value;
103
- });
 
 
 
 
104
 
105
  setResponseHeaders(headers);
106
  setResponse({
@@ -134,6 +223,20 @@ export function PluginCard({ plugin }: PluginCardProps) {
134
  };
135
 
136
  const renderParameterInput = (param: any) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  if (param.enum && Array.isArray(param.enum) && param.enum.length > 0) {
138
  return (
139
  <Select
@@ -157,18 +260,18 @@ export function PluginCard({ plugin }: PluginCardProps) {
157
  </SelectContent>
158
  </Select>
159
  );
160
- } else {
161
- return (
162
- <Input
163
- type="text"
164
- placeholder={param.example?.toString() || param.description}
165
- value={paramValues[param.name] || ""}
166
- onChange={(e) => handleParamChange(param.name, e.target.value)}
167
- disabled={isDisabled}
168
- className="bg-black/50 border-white/10 text-white focus:border-purple-500 disabled:opacity-50"
169
- />
170
- );
171
  }
 
 
 
 
 
 
 
 
 
 
 
172
  };
173
 
174
  const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0;
@@ -181,7 +284,12 @@ export function PluginCard({ plugin }: PluginCardProps) {
181
 
182
  if (hasQueryParams) {
183
  const exampleParams = plugin.parameters!.query!
184
- .map((p) => `${p.name}=${p.example || 'value'}`)
 
 
 
 
 
185
  .join('&');
186
  curl += `?${exampleParams}`;
187
  }
@@ -189,31 +297,80 @@ export function PluginCard({ plugin }: PluginCardProps) {
189
  curl += '"';
190
 
191
  if (hasBodyParams) {
192
- curl += ' \\\n -H "Content-Type: application/json" \\\n -d \'';
193
- const bodyExample: Record<string, any> = {};
194
- plugin.parameters!.body!.forEach((p) => {
195
- bodyExample[p.name] = p.example || 'value';
196
- });
197
- curl += JSON.stringify(bodyExample, null, 2);
198
- curl += "'";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  }
200
 
201
  return curl;
202
  };
203
 
204
  const generateNodeExample = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  let code = `const response = await fetch("${getApiUrl(plugin.endpoint)}`;
206
 
207
  if (hasQueryParams) {
208
  const exampleParams = plugin.parameters!.query!
209
- .map((p) => `${p.name}=${p.example || 'value'}`)
 
 
 
 
 
210
  .join('&');
211
  code += `?${exampleParams}`;
212
  }
213
 
214
  code += '", {\n method: "' + plugin.method + '"';
215
 
216
- if (hasBodyParams) {
217
  code += ',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify(';
218
  const bodyExample: Record<string, any> = {};
219
  plugin.parameters!.body!.forEach((p) => {
@@ -229,7 +386,7 @@ export function PluginCard({ plugin }: PluginCardProps) {
229
 
230
  return (
231
  <Card className={`bg-white/[0.02] border-white/10 overflow-hidden w-full ${isDisabled ? 'opacity-60' : ''}`}>
232
- {/* Collapsible Header */}
233
  <div
234
  className="p-4 border-b border-white/10 cursor-pointer hover:bg-white/[0.02] transition-colors"
235
  onClick={() => setIsExpanded(!isExpanded)}
@@ -240,7 +397,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
240
  </Badge>
241
  <code className="text-sm text-purple-400 font-mono flex-1 min-w-0 break-all">{plugin.endpoint}</code>
242
 
243
- {/* Status Badges */}
244
  {isDisabled && (
245
  <Badge className="bg-red-500/20 text-red-400 border-red-500/50 border flex items-center gap-1 flex-shrink-0">
246
  <XCircle className="w-3 h-3" />
@@ -290,7 +446,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
290
  <h3 className="text-xl font-bold text-white mb-2">{plugin.name}</h3>
291
  <p className="text-gray-400 text-sm leading-relaxed">{plugin.description || "No description provided"}</p>
292
 
293
- {/* Disabled/Deprecated Warnings */}
294
  {isDisabled && plugin.disabledReason && (
295
  <div className="mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-start gap-2">
296
  <XCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
@@ -311,7 +466,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
311
  </div>
312
  )}
313
 
314
- {/* Tags */}
315
  {plugin.tags && plugin.tags.length > 0 && (
316
  <div className="flex flex-wrap gap-2 mt-3">
317
  {plugin.tags.map((tag) => (
@@ -322,7 +476,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
322
  </div>
323
  )}
324
 
325
- {/* API URL Display */}
326
  <div className="mt-3 flex items-start gap-2">
327
  <span className="text-xs text-gray-500 flex-shrink-0">API URL:</span>
328
  <code className="text-xs text-gray-300 bg-black/30 px-2 py-1 rounded break-all flex-1">
@@ -352,7 +505,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
352
 
353
  {/* Documentation Tab */}
354
  <TabsContent value="documentation" className="p-6 space-y-6">
355
- {/* Parameters Table */}
356
  {hasAnyParams && (
357
  <div>
358
  <h4 className="text-purple-400 font-semibold mb-3">Parameters</h4>
@@ -367,30 +519,25 @@ export function PluginCard({ plugin }: PluginCardProps) {
367
  </tr>
368
  </thead>
369
  <tbody>
370
- {plugin.parameters?.path?.map((param) => (
371
  <tr key={param.name} className="border-b border-white/5">
372
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
373
  <td className="py-3 pr-4">
374
  <span className="text-blue-400 font-mono text-xs">{param.type}</span>
375
- {param.enum && (
376
- <span className="ml-2 text-xs text-gray-500">
377
- (options: {param.enum.join(', ')})
378
- </span>
 
 
 
 
 
 
 
 
 
379
  )}
380
- </td>
381
- <td className="py-3 pr-4">
382
- <span className={param.required ? "text-red-400" : "text-gray-500"}>
383
- {param.required ? "Yes" : "No"}
384
- </span>
385
- </td>
386
- <td className="py-3 text-gray-400">{param.description}</td>
387
- </tr>
388
- ))}
389
- {plugin.parameters?.query?.map((param) => (
390
- <tr key={param.name} className="border-b border-white/5">
391
- <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
392
- <td className="py-3 pr-4">
393
- <span className="text-blue-400 font-mono text-xs">{param.type}</span>
394
  {param.enum && (
395
  <span className="ml-2 text-xs text-gray-500">
396
  (options: {param.enum.join(', ')})
@@ -402,26 +549,14 @@ export function PluginCard({ plugin }: PluginCardProps) {
402
  {param.required ? "Yes" : "No"}
403
  </span>
404
  </td>
405
- <td className="py-3 text-gray-400">{param.description}</td>
406
- </tr>
407
- ))}
408
- {plugin.parameters?.body?.map((param) => (
409
- <tr key={param.name} className="border-b border-white/5">
410
- <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
411
- <td className="py-3 pr-4">
412
- <span className="text-blue-400 font-mono text-xs">{param.type}</span>
413
- {param.enum && (
414
- <span className="ml-2 text-xs text-gray-500">
415
- (options: {param.enum.join(', ')})
416
  </span>
417
  )}
418
  </td>
419
- <td className="py-3 pr-4">
420
- <span className={param.required ? "text-red-400" : "text-gray-500"}>
421
- {param.required ? "Yes" : "No"}
422
- </span>
423
- </td>
424
- <td className="py-3 text-gray-400">{param.description}</td>
425
  </tr>
426
  ))}
427
  </tbody>
@@ -430,7 +565,7 @@ export function PluginCard({ plugin }: PluginCardProps) {
430
  </div>
431
  )}
432
 
433
- {/* Responses */}
434
  {plugin.responses && Object.keys(plugin.responses).length > 0 && (
435
  <div>
436
  <h4 className="text-purple-400 font-semibold mb-3">Responses</h4>
@@ -487,40 +622,27 @@ export function PluginCard({ plugin }: PluginCardProps) {
487
 
488
  {/* Try It Out Tab */}
489
  <TabsContent value="try" className="p-6">
490
- {/* Parameters Input */}
491
  {hasAnyParams ? (
492
  <div className="space-y-4 mb-4">
493
- {plugin.parameters?.query?.map((param) => (
494
  <div key={param.name}>
495
- <label className="block text-sm text-gray-300 mb-2">
496
- {param.name}
497
- {param.required && <span className="text-red-400 ml-1">*</span>}
498
- <span className="text-xs text-gray-500 ml-2">({param.type})</span>
499
- {param.enum && (
500
- <span className="text-xs text-purple-400 ml-2">
501
- Select from options
502
- </span>
503
- )}
504
- </label>
505
- {renderParameterInput(param)}
506
- <p className="text-xs text-gray-500 mt-1">{param.description}</p>
507
- </div>
508
- ))}
509
-
510
- {plugin.parameters?.body?.map((param) => (
511
- <div key={param.name}>
512
- <label className="block text-sm text-gray-300 mb-2">
513
- {param.name}
514
- {param.required && <span className="text-red-400 ml-1">*</span>}
515
- <span className="text-xs text-gray-500 ml-2">({param.type})</span>
516
- {param.enum && (
517
- <span className="text-xs text-purple-400 ml-2">
518
- • Select from options
519
- </span>
520
- )}
521
- </label>
522
- {renderParameterInput(param)}
523
- <p className="text-xs text-gray-500 mt-1">{param.description}</p>
524
  </div>
525
  ))}
526
  </div>
@@ -528,7 +650,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
528
  <p className="text-sm text-gray-400 mb-4">No parameters required</p>
529
  )}
530
 
531
- {/* Execute Button */}
532
  <Button
533
  onClick={handleExecute}
534
  disabled={loading || isDisabled}
@@ -541,7 +662,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
541
  {/* Response Display */}
542
  {response && (
543
  <div className="mt-6 space-y-4">
544
- {/* Deprecation Warning in Response */}
545
  {isDeprecated && responseHeaders['x-plugin-deprecated'] && (
546
  <div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-start gap-2">
547
  <AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
@@ -554,7 +674,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
554
  </div>
555
  )}
556
 
557
- {/* Request URL */}
558
  {requestUrl && (
559
  <div>
560
  <div className="flex items-center justify-between mb-2">
@@ -577,7 +696,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
577
  </div>
578
  )}
579
 
580
- {/* Response Status */}
581
  <div className="flex items-center justify-between">
582
  <span className="text-sm text-gray-400">Response Status</span>
583
  <Badge className={`${response.status >= 200 && response.status < 300
@@ -588,7 +706,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
588
  </Badge>
589
  </div>
590
 
591
- {/* Response Headers */}
592
  {Object.keys(responseHeaders).length > 0 && (
593
  <div>
594
  <h5 className="text-sm text-gray-400 mb-2">Response Headers</h5>
@@ -603,7 +720,6 @@ export function PluginCard({ plugin }: PluginCardProps) {
603
  </div>
604
  )}
605
 
606
- {/* Response Body */}
607
  <div>
608
  <h5 className="text-sm text-gray-400 mb-2">Response Body</h5>
609
  <CodeBlock
 
8
  import { PluginMetadata } from "@/client/hooks/usePlugin";
9
  import { Play, ChevronDown, ChevronUp, Copy, Check, AlertTriangle, XCircle } from "lucide-react";
10
  import { CodeBlock } from "@/components/CodeBlock";
11
+ import { FileUpload } from "@/components/FileUpload";
12
  import { getApiUrl } from "@/lib/api-url";
13
 
14
  interface PluginCardProps {
 
25
 
26
  export function PluginCard({ plugin }: PluginCardProps) {
27
  const [paramValues, setParamValues] = useState<Record<string, string>>({});
28
+ const [fileParams, setFileParams] = useState<Record<string, File | null>>({});
29
+ const [urlParams, setUrlParams] = useState<Record<string, string>>({});
30
  const [response, setResponse] = useState<any>(null);
31
  const [responseHeaders, setResponseHeaders] = useState<Record<string, string>>({});
32
  const [requestUrl, setRequestUrl] = useState<string>("");
 
42
  setParamValues((prev) => ({ ...prev, [paramName]: value }));
43
  };
44
 
45
+ const handleFileChange = (paramName: string, file: File | null) => {
46
+ setFileParams((prev) => ({ ...prev, [paramName]: file }));
47
+ };
48
+
49
+ const handleUrlParamChange = (paramName: string, url: string) => {
50
+ setUrlParams((prev) => ({ ...prev, [paramName]: url }));
51
+ };
52
+
53
  const handleExecute = async () => {
54
  if (isDisabled) {
55
  setResponse({
 
70
  let url = "/api" + plugin.endpoint;
71
  let fullUrl = getApiUrl(plugin.endpoint);
72
 
73
+ // Check if we have any file parameters
74
+ const hasFiles = Object.values(fileParams).some(file => file !== null);
75
+ const hasUrls = Object.values(urlParams).some(url => url !== "");
76
+
77
  if (plugin.method === "GET" && plugin.parameters?.query) {
78
  const queryParams = new URLSearchParams();
79
  plugin.parameters.query.forEach((param) => {
80
+ if (param.type === "file" && param.acceptUrl) {
81
+ const urlValue = urlParams[param.name];
82
+ if (urlValue) {
83
+ queryParams.append(param.name, urlValue);
84
+ }
85
+ } else {
86
+ const value = paramValues[param.name];
87
+ if (value) {
88
+ queryParams.append(param.name, value);
89
+ }
90
  }
91
  });
92
 
 
102
  method: plugin.method,
103
  };
104
 
105
+ if (hasFiles || (hasUrls && plugin.method !== "GET")) {
106
+ const formData = new FormData();
107
+
108
+ Object.entries(fileParams).forEach(([name, file]) => {
109
+ if (file) {
110
+ formData.append(name, file);
111
+ }
112
+ });
113
+
114
+ Object.entries(urlParams).forEach(([name, url]) => {
115
+ if (url && !fileParams[name]) {
116
+ formData.append(name, url);
117
+ }
118
+ });
119
+
120
+ if (plugin.parameters?.body) {
121
+ plugin.parameters.body.forEach((param) => {
122
+ if (param.type !== "file") {
123
+ const value = paramValues[param.name];
124
+ if (value) {
125
+ formData.append(param.name, value);
126
+ }
127
+ }
128
+ });
129
+ }
130
+
131
+ fetchOptions.body = formData;
132
+ } else if (["POST", "PUT", "PATCH"].includes(plugin.method) && plugin.parameters?.body) {
133
  const bodyData: Record<string, any> = {};
134
  plugin.parameters.body.forEach((param) => {
135
  const value = paramValues[param.name];
 
143
  };
144
  }
145
 
146
+ // Add XMLHttpRequest fallback for FormData to avoid HTTP/2 issues
147
+ let res;
148
+ let data;
149
+
150
+ if (fetchOptions.body instanceof FormData) {
151
+ // Use XMLHttpRequest for FormData to ensure compatibility
152
+ const xhr = new XMLHttpRequest();
153
+
154
+ const xhrPromise = new Promise((resolve, reject) => {
155
+ xhr.onload = () => {
156
+ try {
157
+ const responseData = JSON.parse(xhr.responseText);
158
+ resolve({
159
+ status: xhr.status,
160
+ statusText: xhr.statusText,
161
+ headers: new Headers(),
162
+ json: async () => responseData
163
+ });
164
+ } catch (e) {
165
+ reject(new Error('Failed to parse response'));
166
+ }
167
+ };
168
+
169
+ xhr.onerror = () => reject(new Error('Network request failed'));
170
+ xhr.ontimeout = () => reject(new Error('Request timeout'));
171
+
172
+ xhr.open(plugin.method, url, true);
173
+ xhr.timeout = 60000; // 60 second timeout
174
+ xhr.send(fetchOptions.body as FormData);
175
+ });
176
+
177
+ res = await xhrPromise as any;
178
+ data = await res.json();
179
+ } else {
180
+ // Use fetch for non-FormData requests
181
+ res = await fetch(url, fetchOptions);
182
+ data = await res.json();
183
+ }
184
 
185
  const headers: Record<string, string> = {};
186
+
187
+ // Get headers from Response object
188
+ if (res.headers && typeof res.headers.forEach === 'function') {
189
+ res.headers.forEach((value: string, key: string) => {
190
+ headers[key] = value;
191
+ });
192
+ }
193
 
194
  setResponseHeaders(headers);
195
  setResponse({
 
223
  };
224
 
225
  const renderParameterInput = (param: any) => {
226
+ if (param.type === "file") {
227
+ return (
228
+ <FileUpload
229
+ name={param.name}
230
+ description={param.description}
231
+ fileConstraints={param.fileConstraints}
232
+ acceptUrl={param.acceptUrl}
233
+ onFileChange={(file) => handleFileChange(param.name, file)}
234
+ onUrlChange={param.acceptUrl ? (url) => handleUrlParamChange(param.name, url) : undefined}
235
+ disabled={isDisabled}
236
+ />
237
+ );
238
+ }
239
+
240
  if (param.enum && Array.isArray(param.enum) && param.enum.length > 0) {
241
  return (
242
  <Select
 
260
  </SelectContent>
261
  </Select>
262
  );
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
+
265
+ return (
266
+ <Input
267
+ type="text"
268
+ placeholder={param.example?.toString() || param.description}
269
+ value={paramValues[param.name] || ""}
270
+ onChange={(e) => handleParamChange(param.name, e.target.value)}
271
+ disabled={isDisabled}
272
+ className="bg-black/50 border-white/10 text-white focus:border-purple-500 disabled:opacity-50"
273
+ />
274
+ );
275
  };
276
 
277
  const hasQueryParams = plugin.parameters?.query && plugin.parameters.query.length > 0;
 
284
 
285
  if (hasQueryParams) {
286
  const exampleParams = plugin.parameters!.query!
287
+ .map((p) => {
288
+ if (p.type === "file" && p.acceptUrl) {
289
+ return `${p.name}=https://example.com/file.jpg`;
290
+ }
291
+ return `${p.name}=${p.example || 'value'}`;
292
+ })
293
  .join('&');
294
  curl += `?${exampleParams}`;
295
  }
 
297
  curl += '"';
298
 
299
  if (hasBodyParams) {
300
+ const hasFileParams = plugin.parameters!.body!.some(p => p.type === "file");
301
+
302
+ if (hasFileParams) {
303
+ curl += ' \\\n';
304
+ plugin.parameters!.body!.forEach((p) => {
305
+ if (p.type === "file") {
306
+ if (p.acceptUrl) {
307
+ curl += ` -F "${p.name}=https://example.com/file.jpg" \\\n`;
308
+ } else {
309
+ curl += ` -F "${p.name}=@/path/to/file" \\\n`;
310
+ }
311
+ } else {
312
+ curl += ` -F "${p.name}=${p.example || 'value'}" \\\n`;
313
+ }
314
+ });
315
+ curl = curl.slice(0, -3); // Remove last \\\n
316
+ } else {
317
+ curl += ' \\\n -H "Content-Type: application/json" \\\n -d \'';
318
+ const bodyExample: Record<string, any> = {};
319
+ plugin.parameters!.body!.forEach((p) => {
320
+ bodyExample[p.name] = p.example || 'value';
321
+ });
322
+ curl += JSON.stringify(bodyExample, null, 2);
323
+ curl += "'";
324
+ }
325
  }
326
 
327
  return curl;
328
  };
329
 
330
  const generateNodeExample = () => {
331
+ const hasFileParams = plugin.parameters?.body?.some(p => p.type === "file") || false;
332
+
333
+ if (hasFileParams) {
334
+ let code = 'const formData = new FormData();\n';
335
+
336
+ plugin.parameters!.body!.forEach((p) => {
337
+ if (p.type === "file") {
338
+ if (p.acceptUrl) {
339
+ code += `formData.append("${p.name}", "https://example.com/file.jpg");\n`;
340
+ } else {
341
+ code += `// For file upload from input: <input type="file" id="fileInput">\n`;
342
+ code += `const fileInput = document.getElementById("fileInput");\n`;
343
+ code += `formData.append("${p.name}", fileInput.files[0]);\n`;
344
+ }
345
+ } else {
346
+ code += `formData.append("${p.name}", "${p.example || 'value'}");\n`;
347
+ }
348
+ });
349
+
350
+ code += `\nconst response = await fetch("${getApiUrl(plugin.endpoint)}", {\n`;
351
+ code += ` method: "${plugin.method}",\n`;
352
+ code += ` body: formData\n`;
353
+ code += '});\n\nconst data = await response.json();\nconsole.log(data);';
354
+ return code;
355
+ }
356
+
357
  let code = `const response = await fetch("${getApiUrl(plugin.endpoint)}`;
358
 
359
  if (hasQueryParams) {
360
  const exampleParams = plugin.parameters!.query!
361
+ .map((p) => {
362
+ if (p.type === "file" && p.acceptUrl) {
363
+ return `${p.name}=https://example.com/file.jpg`;
364
+ }
365
+ return `${p.name}=${p.example || 'value'}`;
366
+ })
367
  .join('&');
368
  code += `?${exampleParams}`;
369
  }
370
 
371
  code += '", {\n method: "' + plugin.method + '"';
372
 
373
+ if (hasBodyParams && !hasFileParams) {
374
  code += ',\n headers: {\n "Content-Type": "application/json"\n },\n body: JSON.stringify(';
375
  const bodyExample: Record<string, any> = {};
376
  plugin.parameters!.body!.forEach((p) => {
 
386
 
387
  return (
388
  <Card className={`bg-white/[0.02] border-white/10 overflow-hidden w-full ${isDisabled ? 'opacity-60' : ''}`}>
389
+ {/* Header */}
390
  <div
391
  className="p-4 border-b border-white/10 cursor-pointer hover:bg-white/[0.02] transition-colors"
392
  onClick={() => setIsExpanded(!isExpanded)}
 
397
  </Badge>
398
  <code className="text-sm text-purple-400 font-mono flex-1 min-w-0 break-all">{plugin.endpoint}</code>
399
 
 
400
  {isDisabled && (
401
  <Badge className="bg-red-500/20 text-red-400 border-red-500/50 border flex items-center gap-1 flex-shrink-0">
402
  <XCircle className="w-3 h-3" />
 
446
  <h3 className="text-xl font-bold text-white mb-2">{plugin.name}</h3>
447
  <p className="text-gray-400 text-sm leading-relaxed">{plugin.description || "No description provided"}</p>
448
 
 
449
  {isDisabled && plugin.disabledReason && (
450
  <div className="mt-3 p-3 bg-red-500/10 border border-red-500/30 rounded-lg flex items-start gap-2">
451
  <XCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
 
466
  </div>
467
  )}
468
 
 
469
  {plugin.tags && plugin.tags.length > 0 && (
470
  <div className="flex flex-wrap gap-2 mt-3">
471
  {plugin.tags.map((tag) => (
 
476
  </div>
477
  )}
478
 
 
479
  <div className="mt-3 flex items-start gap-2">
480
  <span className="text-xs text-gray-500 flex-shrink-0">API URL:</span>
481
  <code className="text-xs text-gray-300 bg-black/30 px-2 py-1 rounded break-all flex-1">
 
505
 
506
  {/* Documentation Tab */}
507
  <TabsContent value="documentation" className="p-6 space-y-6">
 
508
  {hasAnyParams && (
509
  <div>
510
  <h4 className="text-purple-400 font-semibold mb-3">Parameters</h4>
 
519
  </tr>
520
  </thead>
521
  <tbody>
522
+ {[...(plugin.parameters?.path || []), ...(plugin.parameters?.query || []), ...(plugin.parameters?.body || [])].map((param) => (
523
  <tr key={param.name} className="border-b border-white/5">
524
  <td className="py-3 pr-4 text-white font-mono">{param.name}</td>
525
  <td className="py-3 pr-4">
526
  <span className="text-blue-400 font-mono text-xs">{param.type}</span>
527
+ {param.type === "file" && param.fileConstraints && (
528
+ <div className="mt-1 space-y-1">
529
+ {param.fileConstraints.maxSize && (
530
+ <div className="text-xs text-gray-500">
531
+ Max: {(param.fileConstraints.maxSize / 1024 / 1024).toFixed(1)}MB
532
+ </div>
533
+ )}
534
+ {param.fileConstraints.acceptedTypes && (
535
+ <div className="text-xs text-gray-500">
536
+ Types: {param.fileConstraints.acceptedTypes.join(', ')}
537
+ </div>
538
+ )}
539
+ </div>
540
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  {param.enum && (
542
  <span className="ml-2 text-xs text-gray-500">
543
  (options: {param.enum.join(', ')})
 
549
  {param.required ? "Yes" : "No"}
550
  </span>
551
  </td>
552
+ <td className="py-3 text-gray-400">
553
+ {param.description}
554
+ {param.type === "file" && param.acceptUrl && (
555
+ <span className="block mt-1 text-xs text-purple-400">
556
+ Can also accept URL
 
 
 
 
 
 
557
  </span>
558
  )}
559
  </td>
 
 
 
 
 
 
560
  </tr>
561
  ))}
562
  </tbody>
 
565
  </div>
566
  )}
567
 
568
+ {/* Responses section */}
569
  {plugin.responses && Object.keys(plugin.responses).length > 0 && (
570
  <div>
571
  <h4 className="text-purple-400 font-semibold mb-3">Responses</h4>
 
622
 
623
  {/* Try It Out Tab */}
624
  <TabsContent value="try" className="p-6">
 
625
  {hasAnyParams ? (
626
  <div className="space-y-4 mb-4">
627
+ {[...(plugin.parameters?.query || []), ...(plugin.parameters?.body || [])].map((param) => (
628
  <div key={param.name}>
629
+ {param.type !== "file" && (
630
+ <>
631
+ <label className="block text-sm text-gray-300 mb-2">
632
+ {param.name}
633
+ {param.required && <span className="text-red-400 ml-1">*</span>}
634
+ <span className="text-xs text-gray-500 ml-2">({param.type})</span>
635
+ {param.enum && (
636
+ <span className="text-xs text-purple-400 ml-2">
637
+ • Select from options
638
+ </span>
639
+ )}
640
+ </label>
641
+ {renderParameterInput(param)}
642
+ <p className="text-xs text-gray-500 mt-1">{param.description}</p>
643
+ </>
644
+ )}
645
+ {param.type === "file" && renderParameterInput(param)}
 
 
 
 
 
 
 
 
 
 
 
 
646
  </div>
647
  ))}
648
  </div>
 
650
  <p className="text-sm text-gray-400 mb-4">No parameters required</p>
651
  )}
652
 
 
653
  <Button
654
  onClick={handleExecute}
655
  disabled={loading || isDisabled}
 
662
  {/* Response Display */}
663
  {response && (
664
  <div className="mt-6 space-y-4">
 
665
  {isDeprecated && responseHeaders['x-plugin-deprecated'] && (
666
  <div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg flex items-start gap-2">
667
  <AlertTriangle className="w-5 h-5 text-yellow-400 flex-shrink-0 mt-0.5" />
 
674
  </div>
675
  )}
676
 
 
677
  {requestUrl && (
678
  <div>
679
  <div className="flex items-center justify-between mb-2">
 
696
  </div>
697
  )}
698
 
 
699
  <div className="flex items-center justify-between">
700
  <span className="text-sm text-gray-400">Response Status</span>
701
  <Badge className={`${response.status >= 200 && response.status < 300
 
706
  </Badge>
707
  </div>
708
 
 
709
  {Object.keys(responseHeaders).length > 0 && (
710
  <div>
711
  <h5 className="text-sm text-gray-400 mb-2">Response Headers</h5>
 
720
  </div>
721
  )}
722
 
 
723
  <div>
724
  <h5 className="text-sm text-gray-400 mb-2">Response Body</h5>
725
  <CodeBlock
src/server/index.ts CHANGED
@@ -19,10 +19,11 @@ app.use(
19
  verify: (req, _res, buf) => {
20
  req.rawBody = buf;
21
  },
 
22
  }),
23
  );
24
 
25
- app.use(express.urlencoded({ extended: false }));
26
 
27
  export function log(message: string, source = "express") {
28
  const formattedTime = new Date().toLocaleTimeString("id-ID", {
 
19
  verify: (req, _res, buf) => {
20
  req.rawBody = buf;
21
  },
22
+ limit: '10mb'
23
  }),
24
  );
25
 
26
+ app.use(express.urlencoded({ limit: '10mb', extended: false }));
27
 
28
  export function log(message: string, source = "express") {
29
  const formattedTime = new Date().toLocaleTimeString("id-ID", {
src/server/plugins/ai/ai_image_editor.js ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { sendSuccess, ErrorResponses } from "../../lib/response-helper.js";
2
+ import { AIEnhancer } from "./ai_image_utils.js";
3
+ import multer from "multer";
4
+ import axios from "axios";
5
+ import path from "path";
6
+
7
+ const storage = multer.memoryStorage();
8
+ const upload = multer({
9
+ storage: storage,
10
+ limits: {
11
+ fileSize: 5 * 1024 * 1024, // 5MB
12
+ },
13
+ fileFilter: (req, file, cb) => {
14
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
15
+ if (allowedTypes.includes(file.mimetype)) {
16
+ cb(null, true);
17
+ } else {
18
+ cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.'));
19
+ }
20
+ }
21
+ });
22
+
23
+ const ModelMap = {
24
+ nano_banana: 2,
25
+ seed_dream: 5,
26
+ flux: 8,
27
+ qwen_image: 9
28
+ };
29
+
30
+ /** @type {import("../../types/plugin.ts").ApiPluginHandler}*/
31
+ const handler = {
32
+ name: "AI Image Editor",
33
+ description: "Edit your images using AI",
34
+ method: "POST",
35
+ version: "1.0.0",
36
+ category: ["ai"],
37
+ alias: ["aiImageEditor"],
38
+ tags: ["ai", "image"],
39
+ parameters: {
40
+ body: [
41
+ {
42
+ name: "image",
43
+ type: "file",
44
+ required: true,
45
+ description: "Image file to edit (or URL)",
46
+ fileConstraints: {
47
+ maxSize: 5 * 1024 * 1024, // 5MB
48
+ acceptedTypes: ["image/jpeg", "image/png", "image/gif", "image/webp"],
49
+ acceptedExtensions: [".jpg", ".jpeg", ".png", ".gif", ".webp"]
50
+ },
51
+ acceptUrl: true
52
+ },
53
+ {
54
+ name: "model",
55
+ type: "string",
56
+ required: true,
57
+ description: "Select an AI model to edit your image.",
58
+ example: "nano_banana",
59
+ enum: ["nano_banana", "seed_dream", "flux", "qwen_image"],
60
+ default: "nano_banana"
61
+ },
62
+ {
63
+ name: "prompt",
64
+ type: "string",
65
+ required: true,
66
+ description: "Tell AI how your image will look edited",
67
+ example: "Put Cristiano Ronaldo next to that person"
68
+ },
69
+ {
70
+ name: "output_size",
71
+ type: "string",
72
+ required: true,
73
+ description: "Output size",
74
+ example: "2K",
75
+ enum: ["2K", "4K", "8K"],
76
+ default: "2K"
77
+ },
78
+ {
79
+ name: "disable_safety_checker",
80
+ type: "boolean",
81
+ required: false,
82
+ description: "If you want to disable safety checker for indecent images, set to true (default false)",
83
+ example: false,
84
+ default: false
85
+ }
86
+ ]
87
+ },
88
+ responses: {
89
+ 200: {
90
+ status: 200,
91
+ description: "Successfully retrieved data",
92
+ example: {
93
+ status: 200,
94
+ author: "Ditzzy",
95
+ note: "Thank you for using this API!",
96
+ results: {
97
+ image_url: "https://example.com/edited-image.jpg",
98
+ filename: "edited-image.jpg",
99
+ size: "2K"
100
+ }
101
+ }
102
+ },
103
+ 404: {
104
+ status: 404,
105
+ description: "Missing required parameter",
106
+ example: {
107
+ status: 404,
108
+ message: "Missing required parameter"
109
+ }
110
+ },
111
+ 400: {
112
+ status: 400,
113
+ description: "Invalid file or URL",
114
+ example: {
115
+ success: false,
116
+ message: "Invalid image file or URL"
117
+ }
118
+ },
119
+ 413: {
120
+ status: 413,
121
+ description: "File too large",
122
+ example: {
123
+ success: false,
124
+ message: "File size exceeds 5MB limit"
125
+ }
126
+ },
127
+ 500: {
128
+ status: 500,
129
+ description: "Server error or unavailable",
130
+ example: {
131
+ status: 500,
132
+ message: "An error occurred, please try again later."
133
+ }
134
+ }
135
+ },
136
+ exec: async (req, res) => {
137
+ console.log("🔵 [1] Request received at AI Image Editor");
138
+ console.log("🔵 [1] Headers:", req.headers);
139
+
140
+ upload.single('image')(req, res, async (err) => {
141
+ console.log("🔵 [2] Inside multer middleware");
142
+
143
+ if (err) {
144
+ console.error("❌ [2] Multer error:", err);
145
+ if (err instanceof multer.MulterError) {
146
+ if (err.code === 'LIMIT_FILE_SIZE') {
147
+ return res.status(413).json({
148
+ success: false,
149
+ message: "File size exceeds 5MB limit"
150
+ });
151
+ }
152
+ }
153
+ return res.status(400).json({
154
+ success: false,
155
+ message: err.message || "File upload error"
156
+ });
157
+ }
158
+
159
+ console.log("🔵 [3] Multer successful");
160
+ console.log("🔵 [3] Body:", req.body);
161
+ console.log("🔵 [3] File:", req.file ? `${req.file.originalname} (${req.file.size} bytes)` : "No file");
162
+
163
+ try {
164
+ const { prompt, output_size, disable_safety_checker, model } = req.body;
165
+
166
+ console.log("🔵 [4] Extracted params:", { prompt, output_size, model, disable_safety_checker });
167
+
168
+ // Validate required parameters
169
+ if (!prompt) {
170
+ console.log("❌ [4] Missing prompt");
171
+ return ErrorResponses.missingParameter(res, "prompt");
172
+ }
173
+ if (!output_size) {
174
+ console.log("❌ [4] Missing output_size");
175
+ return ErrorResponses.missingParameter(res, "output_size");
176
+ }
177
+ if (!model) {
178
+ console.log("❌ [4] Missing model");
179
+ return ErrorResponses.missingParameter(res, "model");
180
+ }
181
+
182
+ // Validate model
183
+ if (!ModelMap[model]) {
184
+ console.log("❌ [4] Invalid model:", model);
185
+ return res.status(400).json({
186
+ success: false,
187
+ message: `Invalid model. Must be one of: ${Object.keys(ModelMap).join(', ')}`
188
+ });
189
+ }
190
+
191
+ console.log("🔵 [5] Creating AIEnhancer instance");
192
+ const ai = new AIEnhancer();
193
+ console.log("✅ [5] AIEnhancer created");
194
+
195
+ let imageBuffer;
196
+ let filename;
197
+
198
+ // Handle uploaded file
199
+ if (req.file) {
200
+ console.log("🔵 [6] Using uploaded file");
201
+ imageBuffer = req.file.buffer;
202
+ filename = req.file.originalname;
203
+ }
204
+ // Handle URL
205
+ else if (req.body.image && typeof req.body.image === 'string') {
206
+ console.log("🔵 [6] Downloading from URL:", req.body.image);
207
+ const imageUrl = req.body.image;
208
+
209
+ // Validate URL
210
+ try {
211
+ new URL(imageUrl);
212
+ } catch {
213
+ console.log("❌ [6] Invalid URL");
214
+ return ErrorResponses.invalidUrl(res, "Invalid image URL");
215
+ }
216
+
217
+ // Download image from URL
218
+ const response = await axios.get(imageUrl, {
219
+ responseType: 'arraybuffer',
220
+ maxContentLength: 5 * 1024 * 1024,
221
+ timeout: 10000,
222
+ headers: {
223
+ 'User-Agent': 'Mozilla/5.0 (compatible; AIImageEditor/1.0)'
224
+ }
225
+ });
226
+
227
+ imageBuffer = Buffer.from(response.data);
228
+ filename = path.basename(new URL(imageUrl).pathname) || 'image';
229
+ console.log("✅ [6] Downloaded from URL:", filename);
230
+ } else {
231
+ console.log("❌ [6] No image provided");
232
+ return ErrorResponses.missingParameter(res, "image (file or URL)");
233
+ }
234
+
235
+ console.log("🔵 [7] Image buffer size:", imageBuffer.length);
236
+
237
+ // Get model ID from map
238
+ const modelId = ModelMap[model];
239
+ console.log("🔵 [8] Model ID:", modelId);
240
+
241
+ // Call AI Image Editor
242
+ console.log("🔵 [9] Calling ImageAIEditor...");
243
+ const edit = await ai.ImageAIEditor(imageBuffer, modelId, {
244
+ size: output_size,
245
+ aspect_ratio: "match_input_image",
246
+ go_fast: true,
247
+ prompt: prompt,
248
+ output_quality: 100,
249
+ disable_safety_checker: disable_safety_checker === 'true' || disable_safety_checker === true,
250
+ });
251
+
252
+ console.log("✅ [9] ImageAIEditor response:", edit);
253
+
254
+ return sendSuccess(res, edit.results || edit);
255
+ } catch (e) {
256
+ console.error("❌ [ERROR] Exception caught:", e);
257
+ console.error("❌ [ERROR] Stack trace:", e.stack);
258
+ return ErrorResponses.serverError(res, "An error occurred, try again later.");
259
+ }
260
+ });
261
+ }
262
+ }
263
+
264
+ export default handler;
src/server/plugins/ai/ai_image_utils.js ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const ModelMap = {
2
+ nano_banana: 2,
3
+ seed_dream: 5,
4
+ flux: 8,
5
+ qwen_image: 9
6
+ };
7
+
8
+ const IMAGE_TO_ANIME_PRESETS = {
9
+ manga: {
10
+ size: "2K",
11
+ aspect_ratio: "match_input_image",
12
+ output_format: "jpg",
13
+ sequential_image_generation: "disabled",
14
+ max_images: 1,
15
+ prompt: "Convert the provided image into a KOREAN-STYLE MANGA illustration. Apply strong stylization with clear and noticeable differences from the original image."
16
+ },
17
+
18
+ anime: {
19
+ size: "2K",
20
+ aspect_ratio: "match_input_image",
21
+ output_format: "jpg",
22
+ sequential_image_generation: "disabled",
23
+ max_images: 1,
24
+ prompt: "Convert the provided image into an ANIME-STYLE illustration. Apply strong stylization with clear and noticeable differences from the original image."
25
+ },
26
+
27
+ ghibli: {
28
+ size: "2K",
29
+ aspect_ratio: "match_input_image",
30
+ output_format: "jpg",
31
+ sequential_image_generation: "disabled",
32
+ max_images: 1,
33
+ prompt: "Convert the provided image into a STUDIO GHIBLI-STYLE illustration. Apply strong stylization with clear and noticeable differences from the original image."
34
+ },
35
+
36
+ cyberpunk: {
37
+ size: "2K",
38
+ aspect_ratio: "match_input_image",
39
+ output_format: "jpg",
40
+ sequential_image_generation: "disabled",
41
+ max_images: 1,
42
+ prompt: "Convert the provided image into a CYBERPUNK-STYLE illustration with neon colors, futuristic elements, and dark atmosphere."
43
+ },
44
+
45
+ watercolor: {
46
+ size: "2K",
47
+ aspect_ratio: "match_input_image",
48
+ output_format: "png",
49
+ sequential_image_generation: "disabled",
50
+ max_images: 1,
51
+ prompt: "Convert the provided image into a WATERCOLOR painting style with soft brush strokes and pastel colors."
52
+ },
53
+
54
+ pixelart: {
55
+ size: "2K",
56
+ aspect_ratio: "match_input_image",
57
+ output_format: "png",
58
+ sequential_image_generation: "disabled",
59
+ max_images: 1,
60
+ prompt: "Convert the provided image into PIXEL ART style with 8-bit retro gaming aesthetic."
61
+ },
62
+
63
+ sketch: {
64
+ size: "2K",
65
+ aspect_ratio: "match_input_image",
66
+ output_format: "jpg",
67
+ sequential_image_generation: "disabled",
68
+ max_images: 1,
69
+ prompt: "Convert the provided image into a detailed PENCIL SKETCH with realistic shading and artistic strokes."
70
+ },
71
+
72
+ oilpainting: {
73
+ size: "2K",
74
+ aspect_ratio: "match_input_image",
75
+ output_format: "jpg",
76
+ sequential_image_generation: "disabled",
77
+ max_images: 1,
78
+ prompt: "Convert the provided image into an OIL PAINTING style with thick brush strokes and rich colors."
79
+ }
80
+ };
81
+
82
+ const IMAGE_TO_ANIME_ENCRYPTED_PAYLOADS = {
83
+ manga: "L7p91uXhVyp5OOJthAyqjSqhlbM+RPZ8+h2Uq9tz6Y+4Agarugz8f4JjxjEycxEzuj/7+6Q0YY9jUvrfmqkucENhHAkMq1EOilzosQlw2msQpW2yRqV3C/WqvP/jrmSu3aUVAyeFhSbK3ARzowBzQYPVHtxwBbTWwlSR4tehnodUasnmftnY77c8gIFtL2ArNdzmPLx5H8O9un2U8WE4s0+xiFV3y4sbetHMN7rHh7DRIpuIQD4rKISR/vE+HeaHpRavXfsilr5P7Y6bsIo+RRFIPgX2ofbYYiATziqsjDeie4IlcOAVf1Pudqz8uk6YKM78CGxjF9iPLYQnkW+c6j96PNsg1Yk4Xz8/ZcdmHF4GGZe8ILYH/D0yyM1dsCkK1zY8ciL+6pAk4dHIZ/4k9A==",
84
+ ghibli: "L7p91uXhVyp5OOJthAyqjSqhlbM+RPZ8+h2Uq9tz6Y+4Agarugz8f4JjxjEycxEzuj/7+6Q0YY9jUvrfmqkucENhHAkMq1EOilzosQlw2msQpW2yRqV3C/WqvP/jrmSu3aUVAyeFhSbK3ARzowBzQYPVHtxwBbTWwlSR4tehnodUasnmftnY77c8gIFtL2ArNdzmPLx5H8O9un2U8WE4syzL5EYHGJWC1rlQM9xhNe1PViOsBSxmwHVwOdqtxZtcAJmGuzTgG7JVU7Hr9ZRwajhYK5yxQwSdJGwwR4jjS1yF9s9wKUQqgI+fYxaw7FZziLS+9JG5pTEjch4D0fpl+LO7vIynHN4cyu4DDeAUwNeYfbGMn2QQs+5OgMdViCAM1GkJk2jhlQm10rESTjDryw==",
85
+ anime: "L7p91uXhVyp5OOJthAyqjSqhlbM+RPZ8+h2Uq9tz6Y+4Agarugz8f4JjxjEycxEzuj/7+6Q0YY9jUvrfmqkucENhHAkMq1EOilzosQlw2msQpW2yRqV3C/WqvP/jrmSu3aUVAyeFhSbK3ARzowBzQYPVHtxwBbTWwlSR4tehnodUasnmftnY77c8gIFtL2ArNdzmPLx5H8O9un2U8WE4s7O2FxvQPCjt2uGmHPMOx1DsNSnLvzCKPVdz8Ob1cPHePmmquQZlsb/p+8gGv+cizSiOL4ts6GD2RxWN+K5MmpA/F3rQXanFUm4EL0g7qZCQbChRRQyaAyZuxtIdTKsmsMzkVKM5Sx96eV7bEjUAJ52j6NcP96INv2DhnWTP7gB6tltFQe8B8SPS2LuLRuPghA=="
86
+ };
87
+
88
+ export class AIEnhancer {
89
+ constructor() {
90
+ this.CREATED_BY = "Ditzzy";
91
+ this.NOTE = "Thank you for using this scrape, I hope you appreciate me for making this scrape by not deleting wm";
92
+
93
+ this.AES_KEY = "ai-enhancer-web__aes-key";
94
+ this.AES_IV = "aienhancer-aesiv";
95
+
96
+ this.HEADERS = {
97
+ "accept": "*/*",
98
+ "content-type": "application/json",
99
+ "Referer": "https://aienhancer.ai",
100
+ "Referrer-Policy": "strict-origin-when-cross-origin"
101
+ };
102
+
103
+ this.POLLING_INTERVAL = 2000;
104
+ this.MAX_POLLING_ATTEMPTS = 120;
105
+ }
106
+
107
+ encrypt(data) {
108
+ const plaintext = typeof data === "string" ? data : JSON.stringify(data);
109
+
110
+ return CryptoJS.AES.encrypt(
111
+ plaintext,
112
+ CryptoJS.enc.Utf8.parse(this.AES_KEY),
113
+ {
114
+ iv: CryptoJS.enc.Utf8.parse(this.AES_IV),
115
+ mode: CryptoJS.mode.CBC,
116
+ padding: CryptoJS.pad.Pkcs7
117
+ }
118
+ ).toString();
119
+ }
120
+
121
+ decrypt(encryptedData) {
122
+ const decrypted = CryptoJS.AES.decrypt(
123
+ encryptedData,
124
+ CryptoJS.enc.Utf8.parse(this.AES_KEY),
125
+ {
126
+ iv: CryptoJS.enc.Utf8.parse(this.AES_IV),
127
+ mode: CryptoJS.mode.CBC,
128
+ padding: CryptoJS.pad.Pkcs7
129
+ }
130
+ );
131
+
132
+ return decrypted.toString(CryptoJS.enc.Utf8);
133
+ }
134
+
135
+ decryptToJSON(encryptedData) {
136
+ const decrypted = this.decrypt(encryptedData);
137
+ return JSON.parse(decrypted);
138
+ }
139
+
140
+ wrapResponse(data) {
141
+ return {
142
+ created_by: this.CREATED_BY,
143
+ note: this.NOTE,
144
+ results: data
145
+ };
146
+ }
147
+
148
+ async sleep(ms) {
149
+ return new Promise(resolve => setTimeout(resolve, ms));
150
+ }
151
+
152
+ async processImage(image) {
153
+ let img;
154
+ let type;
155
+
156
+ if (typeof image === "string") {
157
+ img = readFileSync(image);
158
+ type = await fileTypeFromBuffer(img);
159
+ } else if (Buffer.isBuffer(image)) {
160
+ img = image;
161
+ type = await fileTypeFromBuffer(image);
162
+ } else {
163
+ throw new Error("Invalid image input: must be file path (string) or Buffer");
164
+ }
165
+
166
+ if (!type) {
167
+ throw new Error("Could not detect file type");
168
+ }
169
+
170
+ const allowedImageTypes = [
171
+ 'image/jpeg',
172
+ 'image/jpg',
173
+ 'image/png',
174
+ 'image/webp',
175
+ 'image/gif',
176
+ 'image/bmp'
177
+ ];
178
+
179
+ if (!allowedImageTypes.includes(type.mime)) {
180
+ throw new Error(
181
+ `Unsupported format: ${type.mime}. Allowed: jpeg, jpg, png, webp, gif, bmp`
182
+ );
183
+ }
184
+
185
+ const imgbase64 = img.toString("base64");
186
+ return {
187
+ base64: `data:${type.mime};base64,${imgbase64}`,
188
+ mime: type.mime
189
+ };
190
+ }
191
+
192
+ async createTask(apiUrl, model, image, config) {
193
+ const { base64 } = await this.processImage(image);
194
+
195
+ const settings = typeof config === "string"
196
+ ? config
197
+ : this.encrypt(config);
198
+
199
+ const requestConfig = {
200
+ url: apiUrl,
201
+ method: "POST",
202
+ headers: this.HEADERS,
203
+ data: {
204
+ model,
205
+ image: base64,
206
+ settings
207
+ }
208
+ };
209
+
210
+ const { data } = await axios.request(requestConfig);
211
+
212
+ if (data.code !== 100000 || !data.data.id) {
213
+ throw new Error(`Task creation failed: ${data.message}`);
214
+ }
215
+
216
+ return data.data.id;
217
+ }
218
+
219
+ async checkTaskStatus(resultUrl, taskId) {
220
+ const config = {
221
+ url: resultUrl,
222
+ method: "POST",
223
+ headers: this.HEADERS,
224
+ data: { task_id: taskId }
225
+ };
226
+
227
+ const { data } = await axios.request(config);
228
+ return data;
229
+ }
230
+
231
+ async pollTaskResult(resultUrl, taskId) {
232
+ let attempts = 0;
233
+
234
+ while (attempts < this.MAX_POLLING_ATTEMPTS) {
235
+ const response = await this.checkTaskStatus(resultUrl, taskId);
236
+
237
+ if (response.code !== 100000) {
238
+ throw new Error(`Status check failed: ${response.message}`);
239
+ }
240
+
241
+ const { status, error, output, input, completed_at } = response.data;
242
+
243
+ if ((status === "succeeded" || status === "success") && output && input) {
244
+ return {
245
+ id: taskId,
246
+ output,
247
+ input,
248
+ error,
249
+ status,
250
+ created_at: response.data.created_at,
251
+ started_at: response.data.started_at,
252
+ completed_at: completed_at
253
+ };
254
+ }
255
+
256
+ if (status === "failed" || status === "fail" || error) {
257
+ throw new Error(`Task failed: ${error || "Unknown error"}`);
258
+ }
259
+
260
+ console.log(`[${taskId} | ${status}] Polling attempt ${attempts + 1}/${this.MAX_POLLING_ATTEMPTS}`);
261
+
262
+ await this.sleep(this.POLLING_INTERVAL);
263
+ attempts++;
264
+ }
265
+
266
+ throw new Error(`Task timeout after ${this.MAX_POLLING_ATTEMPTS} attempts`);
267
+ }
268
+
269
+ async imageToAnime(image, preset = "anime") {
270
+ try {
271
+ const apiUrl = "https://aienhancer.ai/api/v1/r/image-enhance/create";
272
+ const resultUrl = "https://aienhancer.ai/api/v1/r/image-enhance/result";
273
+ const model = 5;
274
+
275
+ let config;
276
+
277
+ if (typeof preset === "string") {
278
+ if (IMAGE_TO_ANIME_PRESETS[preset]) {
279
+ config = IMAGE_TO_ANIME_PRESETS[preset];
280
+ }
281
+ else if (IMAGE_TO_ANIME_ENCRYPTED_PAYLOADS[preset]) {
282
+ config = IMAGE_TO_ANIME_ENCRYPTED_PAYLOADS[preset];
283
+ }
284
+ else {
285
+ config = preset;
286
+ }
287
+ } else {
288
+ config = preset;
289
+ }
290
+
291
+ const taskId = await this.createTask(apiUrl, model, image, config);
292
+ console.log(`✓ Task created: ${taskId}`);
293
+
294
+ const result = await this.pollTaskResult(resultUrl, taskId);
295
+ return this.wrapResponse(result);
296
+ } catch (error) {
297
+ throw new Error(`Image to anime conversion failed: ${error}`);
298
+ }
299
+ }
300
+
301
+ async ImageAIEditor(image, model, preset) {
302
+ try {
303
+ const apiUrl = "https://aienhancer.ai/api/v1/k/image-enhance/create";
304
+ const resultUrl = "https://aienhancer.ai/api/v1/k/image-enhance/result";
305
+
306
+ const modelId = ModelMap[model];
307
+ const taskId = await this.createTask(apiUrl, modelId, image, preset);
308
+ const result = await this.pollTaskResult(resultUrl, taskId);
309
+
310
+ return this.wrapResponse(result);
311
+ } catch (e) {
312
+ throw new Error("Image editor failed: " + e);
313
+ }
314
+ }
315
+
316
+ async AIImageRestoration(image, model, config) {
317
+ try {
318
+ const apiUrl = "https://aienhancer.ai/api/v1/r/image-enhance/create";
319
+ const resultUrl = "https://aienhancer.ai/api/v1/r/image-enhance/result";
320
+
321
+ const taskId = await this.createTask(apiUrl, model, image, config);
322
+ const result = await this.pollTaskResult(resultUrl, taskId);
323
+
324
+ return this.wrapResponse(result);
325
+ } catch (e) {
326
+ throw new Error(`An error occurred while upscaling the image: ${e}`);
327
+ }
328
+ }
329
+
330
+ async RemoveBackground(image, config = {}) {
331
+ try {
332
+ const apiUrl = "https://aienhancer.ai/api/v1/r/image-enhance/create";
333
+ const resultUrl = "https://aienhancer.ai/api/v1/r/image-enhance/result";
334
+ const model = 4;
335
+
336
+ const payloadConfig = config ? config : {
337
+ threshold: 0,
338
+ reverse: false,
339
+ background_type: "rgba",
340
+ format: "png",
341
+ };
342
+
343
+ const taskId = await this.createTask(apiUrl, model, image, payloadConfig);
344
+ const result = await this.pollTaskResult(resultUrl, taskId);
345
+
346
+ return this.wrapResponse(result);
347
+ } catch (e) {
348
+ throw new Error(`Remove background error: ${e}`);
349
+ }
350
+ }
351
+
352
+ decryptPayload(encryptedPayload) {
353
+ return this.decryptToJSON(encryptedPayload);
354
+ }
355
+
356
+ encryptConfig(config) {
357
+ return this.encrypt(config);
358
+ }
359
+ }
src/server/types/plugin.ts CHANGED
@@ -1,14 +1,22 @@
1
  import { Request, Response, NextFunction } from "express";
2
 
 
 
 
 
 
 
3
  export interface PluginParameter {
4
  name: string;
5
- type: "string" | "number" | "boolean" | "array" | "object";
6
  required: boolean;
7
  description: string;
8
  example?: any;
9
  default?: any;
10
  enum?: any[];
11
  pattern?: string;
 
 
12
  }
13
 
14
  export interface PluginResponse {
 
1
  import { Request, Response, NextFunction } from "express";
2
 
3
+ export interface FileConstraints {
4
+ maxSize?: number; // NOTE: in bytes, e.g., 5 * 1024 * 1024 for 5MB
5
+ acceptedTypes?: string[]; // NOTE: MIME types, e.g., ['image/jpeg', 'image/png']
6
+ acceptedExtensions?: string[]; // e.g., ['.jpg', '.png', '.pdf']
7
+ }
8
+
9
  export interface PluginParameter {
10
  name: string;
11
+ type: "string" | "number" | "boolean" | "array" | "object" | "file";
12
  required: boolean;
13
  description: string;
14
  example?: any;
15
  default?: any;
16
  enum?: any[];
17
  pattern?: string;
18
+ fileConstraints?: FileConstraints;
19
+ acceptUrl?: boolean;
20
  }
21
 
22
  export interface PluginResponse {