ChBysk's picture
Upload folder using huggingface_hub
61b6204 verified
Raw
History Blame Contribute Delete
8.25 kB
import { useState } from "react";
import type { SiteConfig } from "@/lib/grabber/sites";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
type Props = {
open: boolean;
onOpenChange: (v: boolean) => void;
onAdd: (site: SiteConfig) => void;
};
const DEFAULT: SiteConfig = {
id: "danbooru",
name: "Danbooru",
baseUrl: "https://danbooru.donmai.us",
searchPath: "/posts.json",
params: { tags: "tags", page: "page", limit: "limit" },
postsPath: "",
fields: {
id: "id",
previewUrl: "preview_file_url",
fileUrl: "file_url",
ext: "file_ext",
tags: "tag_string_general",
source: "source",
rating: "rating",
score: "score",
sampleUrl: "large_file_url",
fileSize: "file_size",
width: "image_width",
height: "image_height",
md5: "md5",
uploader: "uploader_name",
createdAt: "created_at",
},
};
export function AddSiteDialog({ open, onOpenChange, onAdd }: Props) {
const [s, setS] = useState<SiteConfig>(DEFAULT);
const [headersText, setHeadersText] = useState("");
const [pasteSchemaText, setPasteSchemaText] = useState("");
const [parseError, setParseError] = useState("");
const set = <K extends keyof SiteConfig>(k: K, v: SiteConfig[K]) =>
setS((p) => ({ ...p, [k]: v }));
const setField = (k: keyof SiteConfig["fields"], v: string) =>
setS((p) => ({ ...p, fields: { ...p.fields, [k]: v } }));
const setParam = (k: keyof SiteConfig["params"], v: string) =>
setS((p) => ({ ...p, params: { ...p.params, [k]: v } }));
const handlePasteSchema = () => {
setParseError("");
if (!pasteSchemaText.trim()) return;
try {
const parsed = JSON.parse(pasteSchemaText);
if (typeof parsed !== "object" || parsed === null) {
throw new Error("Schema must be an object");
}
const merged: SiteConfig = {
id: parsed.id || s.id,
name: parsed.name || s.name,
baseUrl: parsed.baseUrl || s.baseUrl,
searchPath: parsed.searchPath || s.searchPath,
params: {
tags: parsed.params?.tags || s.params.tags,
page: parsed.params?.page || s.params.page,
limit: parsed.params?.limit || s.params.limit,
},
postsPath: typeof parsed.postsPath === "string" ? parsed.postsPath : s.postsPath,
fields: {
id: parsed.fields?.id || s.fields.id,
previewUrl: parsed.fields?.previewUrl || s.fields.previewUrl,
fileUrl: parsed.fields?.fileUrl || s.fields.fileUrl,
ext: parsed.fields?.ext || s.fields.ext,
tags: parsed.fields?.tags || s.fields.tags,
source: parsed.fields?.source || s.fields.source,
rating: parsed.fields?.rating || s.fields.rating,
score: parsed.fields?.score || s.fields.score,
sampleUrl: parsed.fields?.sampleUrl || s.fields.sampleUrl,
fileSize: parsed.fields?.fileSize || s.fields.fileSize,
width: parsed.fields?.width || s.fields.width,
height: parsed.fields?.height || s.fields.height,
md5: parsed.fields?.md5 || s.fields.md5,
uploader: parsed.fields?.uploader || s.fields.uploader,
createdAt: parsed.fields?.createdAt || s.fields.createdAt,
},
};
if (parsed.headers && typeof parsed.headers === "object") {
setHeadersText(JSON.stringify(parsed.headers, null, 2));
}
setS(merged);
setPasteSchemaText("");
} catch (e: unknown) {
if (e instanceof Error) {
setParseError(e.message);
} else {
setParseError("Failed to parse JSON");
}
}
};
const submit = () => {
let headers: Record<string, string> | undefined;
if (headersText.trim()) {
try {
headers = JSON.parse(headersText);
} catch {
alert("Headers must be valid JSON");
return;
}
}
onAdd({ ...s, headers });
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Add Site (schema)</DialogTitle>
</DialogHeader>
{/* New Paste Schema Input Box */}
<div className="border p-3 rounded-md bg-muted/30 mb-4">
<Label className="text-xs font-semibold mb-1 block">
Paste Site Configuration JSON (API Support)
</Label>
<div className="flex gap-2">
<Textarea
rows={3}
value={pasteSchemaText}
onChange={(e) => setPasteSchemaText(e.target.value)}
placeholder={`Paste JSON schema here, e.g.:\n{\n "id": "danbooru",\n "name": "Danbooru",\n "baseUrl": "https://danbooru.donmai.us",\n "fields": { "fileUrl": "file_url" }\n}`}
className="font-mono text-xs flex-1"
/>
<Button variant="secondary" size="sm" onClick={handlePasteSchema} className="self-end">
Apply Schema
</Button>
</div>
{parseError && (
<div className="text-xs text-destructive mt-1 font-semibold">{parseError}</div>
)}
</div>
<div className="grid grid-cols-2 gap-3">
<Field label="ID (unique)">
<Input value={s.id} onChange={(e) => set("id", e.target.value)} />
</Field>
<Field label="Name">
<Input value={s.name} onChange={(e) => set("name", e.target.value)} />
</Field>
<Field label="Base URL">
<Input value={s.baseUrl} onChange={(e) => set("baseUrl", e.target.value)} />
</Field>
<Field label="Search Path">
<Input value={s.searchPath} onChange={(e) => set("searchPath", e.target.value)} />
</Field>
<Field label="tags param">
<Input value={s.params.tags} onChange={(e) => setParam("tags", e.target.value)} />
</Field>
<Field label="page param">
<Input value={s.params.page} onChange={(e) => setParam("page", e.target.value)} />
</Field>
<Field label="limit param">
<Input value={s.params.limit} onChange={(e) => setParam("limit", e.target.value)} />
</Field>
<Field label="posts JSON path (empty = root)">
<Input value={s.postsPath} onChange={(e) => set("postsPath", e.target.value)} />
</Field>
<div className="col-span-2 border-t pt-3 mt-1 text-sm font-semibold">
Field mappings (dotted paths)
</div>
{(
[
"id",
"previewUrl",
"fileUrl",
"ext",
"tags",
"source",
"rating",
"score",
"sampleUrl",
"fileSize",
"width",
"height",
"md5",
"uploader",
"createdAt",
] as const
).map((k) => (
<Field key={k} label={k}>
<Input value={s.fields[k] ?? ""} onChange={(e) => setField(k, e.target.value)} />
</Field>
))}
<Field label="Extra headers (JSON, optional)" full>
<Textarea
rows={3}
value={headersText}
onChange={(e) => setHeadersText(e.target.value)}
placeholder='{"User-Agent":"MyGrabber/1.0"}'
/>
</Field>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={submit}>Add site</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function Field({
label,
children,
full,
}: {
label: string;
children: React.ReactNode;
full?: boolean;
}) {
return (
<div className={full ? "col-span-2" : ""}>
<Label className="text-xs mb-1 block">{label}</Label>
{children}
</div>
);
}