mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2026-02-24 18:01:54 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bc20ea40d |
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
# Disable Discord community banner (optional)
|
# Disable Discord community banner (optional)
|
||||||
# Uncomment the line below to hide the Discord banner
|
# Uncomment the line below to hide the Discord banner
|
||||||
# NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true
|
NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,9 @@
|
|||||||
|
.idea/
|
||||||
|
bun.lock
|
||||||
|
.env
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
.next
|
.next
|
||||||
.env.local
|
.env.local
|
||||||
.env*.local
|
.env*.local
|
||||||
|
.env
|
||||||
|
|||||||
@@ -2,6 +2,5 @@ services:
|
|||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: sucker
|
image: sucker
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
125
src/app/page.tsx
125
src/app/page.tsx
@@ -89,6 +89,9 @@ export default function Home() {
|
|||||||
"https://sucker.severian.dev/api/proxy"
|
"https://sucker.severian.dev/api/proxy"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [pageSource, setPageSource] = useState("");
|
||||||
|
const [metadataJson, setMetadataJson] = useState<any>(null);
|
||||||
|
|
||||||
const fetchCards = async () => {
|
const fetchCards = async () => {
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
@@ -233,6 +236,15 @@ export default function Home() {
|
|||||||
|
|
||||||
const arrayBuffer = await pngBlob.arrayBuffer();
|
const arrayBuffer = await pngBlob.arrayBuffer();
|
||||||
|
|
||||||
|
// Fallback if metadataJson hasn't been fetched for this card
|
||||||
|
const safeMetadata = metadataJson || {};
|
||||||
|
const creator = (safeMetadata.creator_name || (card.data as any).creator || "Unknown") + (safeMetadata.creator_verified ? " ✅" : "");
|
||||||
|
|
||||||
|
// Tag Parsing
|
||||||
|
const normalTags = (safeMetadata.tags || (card.data as any).tags || []).map((t: any) => t.name);
|
||||||
|
const customTags = safeMetadata.custom_tags || [];
|
||||||
|
const allTagsArray = [...normalTags, ...customTags];
|
||||||
|
|
||||||
// Use initial version for PNG embedding, or current version if no initial version available
|
// Use initial version for PNG embedding, or current version if no initial version available
|
||||||
const chosen = card.initialVersion || card.data;
|
const chosen = card.initialVersion || card.data;
|
||||||
const pngData = {
|
const pngData = {
|
||||||
@@ -244,13 +256,13 @@ export default function Home() {
|
|||||||
personality: chosen.personality,
|
personality: chosen.personality,
|
||||||
mes_example: chosen.mes_example,
|
mes_example: chosen.mes_example,
|
||||||
scenario: chosen.scenario,
|
scenario: chosen.scenario,
|
||||||
creator: (chosen as any).creator || "",
|
creator: creator,
|
||||||
creator_notes: (chosen as any).creator_notes || "",
|
creator_notes: safeMetadata.description || (chosen as any).creator_notes || "",
|
||||||
system_prompt: (chosen as any).system_prompt || "",
|
system_prompt: (chosen as any).system_prompt || "",
|
||||||
post_history_instructions:
|
post_history_instructions:
|
||||||
(chosen as any).post_history_instructions || "",
|
(chosen as any).post_history_instructions || "",
|
||||||
tags: (chosen as any).tags || [],
|
tags: allTagsArray,
|
||||||
character_version: (chosen as any).character_version || "1",
|
character_version: safeMetadata.name || (chosen as any).character_version || "1",
|
||||||
extensions: (chosen as any).extensions || {},
|
extensions: (chosen as any).extensions || {},
|
||||||
},
|
},
|
||||||
spec: card.spec || "chara_card_v2",
|
spec: card.spec || "chara_card_v2",
|
||||||
@@ -295,39 +307,9 @@ export default function Home() {
|
|||||||
const isCharacterUrl = /janitorai\.com\/characters\//.test(characterUrl);
|
const isCharacterUrl = /janitorai\.com\/characters\//.test(characterUrl);
|
||||||
|
|
||||||
if (isCharacterUrl) {
|
if (isCharacterUrl) {
|
||||||
// Extract character ID and open metadata page, then show second input
|
// Open html source, then show second input
|
||||||
const match = characterUrl.match(/characters\/([\w-]+)/);
|
window.open(`view-source:${characterUrl}`, "_blank");
|
||||||
if (match && match[1]) {
|
|
||||||
const characterId = match[1].split("_")[0];
|
|
||||||
window.open(
|
|
||||||
`https://janitorai.com/hampter/characters/${characterId}`,
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
setIsMetadataOpen(true);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -336,7 +318,42 @@ export default function Home() {
|
|||||||
if (selectedCardIndex === null) return;
|
if (selectedCardIndex === null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const avatarUrl = `https://ella.janitorai.com/bot-avatars/${avatarPath}`;
|
const storeKey = "Sk--a:a-a--characterStore";
|
||||||
|
|
||||||
|
// Define the anchor points we are looking for
|
||||||
|
const prefix = 'window.mbxM.push(JSON.parse("';
|
||||||
|
const suffix = '"));';
|
||||||
|
|
||||||
|
// Locate the script content
|
||||||
|
const startIndex = pageSource.indexOf(prefix);
|
||||||
|
if (startIndex === -1) {
|
||||||
|
throw new Error("Could not find character data in the page source.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move index to the start of the actual JSON string
|
||||||
|
const jsonStartIndex = startIndex + prefix.length;
|
||||||
|
|
||||||
|
// Find the end of the statement
|
||||||
|
const jsonEndIndex = pageSource.indexOf(suffix, jsonStartIndex);
|
||||||
|
if (jsonEndIndex === -1) {
|
||||||
|
throw new Error("Could not find closing tag for JSON content.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the escaped string
|
||||||
|
const escapedJsonString = pageSource.substring(jsonStartIndex, jsonEndIndex);
|
||||||
|
|
||||||
|
// Wrap it in quotes so it gets treated as a string containing JSON
|
||||||
|
const rawJsonString = JSON.parse(`"${escapedJsonString}"`);
|
||||||
|
console.log(rawJsonString);
|
||||||
|
|
||||||
|
// Parse the actual JSON data into an object
|
||||||
|
const data = JSON.parse(rawJsonString);
|
||||||
|
|
||||||
|
// Return the specific character data
|
||||||
|
const char = data[storeKey].character;
|
||||||
|
setMetadataJson(char);
|
||||||
|
|
||||||
|
const avatarUrl = `https://ella.janitorai.com/bot-avatars/${char.avatar}`;
|
||||||
const updatedCards = [...cards];
|
const updatedCards = [...cards];
|
||||||
updatedCards[selectedCardIndex] = {
|
updatedCards[selectedCardIndex] = {
|
||||||
...updatedCards[selectedCardIndex],
|
...updatedCards[selectedCardIndex],
|
||||||
@@ -742,7 +759,7 @@ export default function Home() {
|
|||||||
onClick={() => handleOpenDialog(index)}
|
onClick={() => handleOpenDialog(index)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Fetch Avatar (required for PNG)
|
Fetch Metadata
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
@@ -750,7 +767,7 @@ export default function Home() {
|
|||||||
variant="default"
|
variant="default"
|
||||||
disabled={!card.avatarUrl}
|
disabled={!card.avatarUrl}
|
||||||
>
|
>
|
||||||
Download PNG
|
Download Complete Card
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -993,43 +1010,25 @@ export default function Home() {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{isMetadataOpen ? "Enter Avatar Path" : "Fetch Avatar"}
|
{isMetadataOpen ? "Enter Page Source Code" : "Fetch Metadata"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{isMetadataOpen
|
{isMetadataOpen ? "Paste the entire source code here." : "Enter a character URL (janitorai.com/characters/...) to open page source."}
|
||||||
? "Look for the avatar field in the opened tab and paste the value here."
|
|
||||||
: "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>
|
||||||
|
|
||||||
{isMetadataOpen ? (
|
{isMetadataOpen ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input placeholder="<!DOCTYPE html><html..." value={pageSource} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setPageSource(e.target.value)} />
|
||||||
placeholder="id.webp"
|
|
||||||
value={avatarPath}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setAvatarPath(e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button onClick={handleFetchAvatar} className="w-full">
|
<Button onClick={handleFetchAvatar} className="w-full">
|
||||||
Fetch Avatar
|
Fetch Metadata
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input placeholder="URL" value={characterUrl} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCharacterUrl(e.target.value)} />
|
||||||
placeholder="URL or id.webp"
|
|
||||||
value={characterUrl}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setCharacterUrl(e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
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">
|
<Button onClick={handleOpenMetadata} className="w-full">
|
||||||
Fetch Avatar
|
Fetch Metadata
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user