Added the option to directly input webp file name or character image link when creating PNG for character cards

This commit is contained in:
2025-12-21 09:44:53 +13:00
parent 06d8b2e36c
commit bdde78475e
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/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// 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",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1305,6 +1306,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -1464,6 +1466,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2075,6 +2078,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "bin/jiti.js"
}
@@ -2372,6 +2376,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -2540,6 +2545,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -2549,6 +2555,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2844,6 +2851,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -2953,6 +2961,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},

View File

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