Merge pull request #7 from chill-protocol/webp-link

Option for webp file name or character image URL inputs
This commit is contained in:
Severian
2025-12-21 12:41:20 +08:00
committed by GitHub
3 changed files with 195 additions and 159 deletions

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

9
package-lock.json generated
View File

@@ -1296,6 +1296,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -1305,6 +1306,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -1464,6 +1466,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -2075,6 +2078,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -2372,6 +2376,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -2540,6 +2545,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -2549,6 +2555,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -2844,6 +2851,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@@ -2953,6 +2961,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },

View File

@@ -258,12 +258,11 @@ export default function Home() {
const cardData = JSON.stringify(pngData); const cardData = JSON.stringify(pngData);
const newImageData = Png.Generate(arrayBuffer, cardData); const newImageData = Png.Generate(arrayBuffer, cardData);
const newFileName = `${ const newFileName = `${(card.initialVersion?.name || card.data.name).replace(
(card.initialVersion?.name || card.data.name).replace( /[^a-zA-Z0-9\-_]/g,
/[^a-zA-Z0-9\-_]/g, "_"
"_" ) || "character"
) || "character" }.png`;
}.png`;
const newFile = new File([new Uint8Array(newImageData)], newFileName, { const newFile = new File([new Uint8Array(newImageData)], newFileName, {
type: "image/png", type: "image/png",
}); });
@@ -290,14 +289,44 @@ export default function Home() {
}; };
const handleOpenMetadata = () => { const handleOpenMetadata = () => {
const match = characterUrl.match(/characters\/([\w-]+)/); // Check if the input is a character metadata URL (janitorai.com/characters/...)
if (match && match[1]) { const isCharacterUrl = /janitorai\.com\/characters\//.test(characterUrl);
const characterId = match[1].split("_")[0];
window.open( if (isCharacterUrl) {
`https://janitorai.com/hampter/characters/${characterId}`, // Extract character ID and open metadata page, then show second input
"_blank" const match = characterUrl.match(/characters\/([\w-]+)/);
); if (match && match[1]) {
setIsMetadataOpen(true); const characterId = match[1].split("_")[0];
window.open(
`https://janitorai.com/hampter/characters/${characterId}`,
"_blank"
);
setIsMetadataOpen(true);
}
return;
}
// Check if the input is a direct image link (webp filename or full image URL)
const isImagePath = /\.(webp|png|jpg|jpeg|gif)(\?.*)?$/i.test(characterUrl);
const isFullImageUrl = (characterUrl.startsWith("http://") || characterUrl.startsWith("https://")) && isImagePath;
const isWebpFilename = /^[\w-]+\.(webp|png|jpg|jpeg|gif)$/i.test(characterUrl);
if (isFullImageUrl || isWebpFilename) {
// Directly set the avatar URL without opening metadata
if (selectedCardIndex === null) return;
const avatarUrl = isFullImageUrl
? characterUrl
: `https://ella.janitorai.com/bot-avatars/${characterUrl}`;
const updatedCards = [...cards];
updatedCards[selectedCardIndex] = {
...updatedCards[selectedCardIndex],
avatarUrl,
};
setCards(updatedCards);
setDialogOpen(false);
return;
} }
}; };
@@ -588,71 +617,70 @@ export default function Home() {
<div id={`card-${index}`} className="space-y-4 mt-4"> <div id={`card-${index}`} className="space-y-4 mt-4">
{(card.initialVersion?.description || {(card.initialVersion?.description ||
card.data?.description) && ( card.data?.description) && (
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<AccordionItem value="description"> <AccordionItem value="description">
<AccordionTrigger>Description</AccordionTrigger> <AccordionTrigger>Description</AccordionTrigger>
<AccordionContent> <AccordionContent>
<div className="flex justify-between"> <div className="flex justify-between">
<pre className="whitespace-pre-wrap font-sans text-sm"> <pre className="whitespace-pre-wrap font-sans text-sm">
{card.initialVersion?.description || {card.initialVersion?.description ||
card.data.description} card.data.description}
</pre> </pre>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
copyToClipboard( copyToClipboard(
card.initialVersion?.description || card.initialVersion?.description ||
card.data.description card.data.description
); );
}} }}
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
</div> </div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
)} )}
{(card.initialVersion?.first_mes || {(card.initialVersion?.first_mes ||
card.data?.first_mes) && ( card.data?.first_mes) && (
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<AccordionItem value="first-message"> <AccordionItem value="first-message">
<AccordionTrigger> <AccordionTrigger>
First Message First Message
</AccordionTrigger> </AccordionTrigger>
<AccordionContent> <AccordionContent>
<div className="flex justify-between"> <div className="flex justify-between">
<pre className="whitespace-pre-wrap font-sans text-sm"> <pre className="whitespace-pre-wrap font-sans text-sm">
{card.initialVersion?.first_mes || {card.initialVersion?.first_mes ||
card.data.first_mes} card.data.first_mes}
</pre> </pre>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
copyToClipboard( copyToClipboard(
card.initialVersion?.first_mes || card.initialVersion?.first_mes ||
card.data.first_mes card.data.first_mes
); );
}} }}
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
</div> </div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
)} )}
{card.alternate_greetings && {card.alternate_greetings &&
card.alternate_greetings.length > 0 && ( card.alternate_greetings.length > 0 && (
<div className="mt-4"> <div className="mt-4">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h4 className="font-medium">{`Alternate Greetings (${ <h4 className="font-medium">{`Alternate Greetings (${card.alternate_greetings?.length || 0
card.alternate_greetings?.length || 0 })`}</h4>
})`}</h4>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="ghost" variant="ghost"
@@ -721,93 +749,93 @@ export default function Home() {
)} )}
{(card.initialVersion?.scenario || {(card.initialVersion?.scenario ||
card.data?.scenario) && ( card.data?.scenario) && (
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<AccordionItem value="scenario"> <AccordionItem value="scenario">
<AccordionTrigger>Scenario</AccordionTrigger> <AccordionTrigger>Scenario</AccordionTrigger>
<AccordionContent> <AccordionContent>
<div className="flex justify-between"> <div className="flex justify-between">
<pre className="whitespace-pre-wrap font-sans text-sm"> <pre className="whitespace-pre-wrap font-sans text-sm">
{card.initialVersion?.scenario || {card.initialVersion?.scenario ||
card.data.scenario} card.data.scenario}
</pre> </pre>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
copyToClipboard( copyToClipboard(
card.initialVersion?.scenario || card.initialVersion?.scenario ||
card.data.scenario card.data.scenario
); );
}} }}
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
</div> </div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
)} )}
{(card.initialVersion?.mes_example || {(card.initialVersion?.mes_example ||
card.data?.mes_example) && ( card.data?.mes_example) && (
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<AccordionItem value="example-messages"> <AccordionItem value="example-messages">
<AccordionTrigger> <AccordionTrigger>
Example Messages Example Messages
</AccordionTrigger> </AccordionTrigger>
<AccordionContent> <AccordionContent>
<div className="flex justify-between"> <div className="flex justify-between">
<pre className="whitespace-pre-wrap font-sans text-sm"> <pre className="whitespace-pre-wrap font-sans text-sm">
{card.initialVersion?.mes_example || {card.initialVersion?.mes_example ||
card.data.mes_example} card.data.mes_example}
</pre> </pre>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
copyToClipboard( copyToClipboard(
card.initialVersion?.mes_example || card.initialVersion?.mes_example ||
card.data.mes_example card.data.mes_example
); );
}} }}
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
</div> </div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
)} )}
{(card.initialVersion?.personality || {(card.initialVersion?.personality ||
card.data?.personality) && ( card.data?.personality) && (
<Accordion type="single" collapsible> <Accordion type="single" collapsible>
<AccordionItem value="personality"> <AccordionItem value="personality">
<AccordionTrigger>Personality</AccordionTrigger> <AccordionTrigger>Personality</AccordionTrigger>
<AccordionContent> <AccordionContent>
<div className="flex justify-between"> <div className="flex justify-between">
<pre className="whitespace-pre-wrap font-sans text-sm"> <pre className="whitespace-pre-wrap font-sans text-sm">
{card.initialVersion?.personality || {card.initialVersion?.personality ||
card.data.personality} card.data.personality}
</pre> </pre>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
copyToClipboard( copyToClipboard(
card.initialVersion?.personality || card.initialVersion?.personality ||
card.data.personality card.data.personality
); );
}} }}
> >
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
</Button> </Button>
</div> </div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
)} )}
</div> </div>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
@@ -863,12 +891,12 @@ export default function Home() {
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{isMetadataOpen ? "Enter Avatar Path" : "Enter Character URL"} {isMetadataOpen ? "Enter Avatar Path" : "Fetch Avatar"}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{isMetadataOpen {isMetadataOpen
? "Look for the avatar field in the opened tab and paste the value here." ? "Look for the avatar field in the opened tab and paste the value here."
: "Enter the Janitor character URL (https://janitorai.com/characters/...)."} : "Enter a character URL (janitorai.com/characters/...) to open metadata, or paste an image filename (id.webp) or full image URL directly."}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -888,19 +916,18 @@ export default function Home() {
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<Input <Input
placeholder="https://janitorai.com/characters/..." placeholder="URL or id.webp"
value={characterUrl} value={characterUrl}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCharacterUrl(e.target.value) setCharacterUrl(e.target.value)
} }
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Upon clicking this button, a new tab will open with the For character URLs, a new tab will open with metadata. For image
character's metadata. Look for the avatar field and copy the filenames or full image URLs, the avatar will be set directly.
value before returning to this page.
</p> </p>
<Button onClick={handleOpenMetadata} className="w-full"> <Button onClick={handleOpenMetadata} className="w-full">
Open Metadata Fetch Avatar
</Button> </Button>
</div> </div>
)} )}