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

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>
)}