"use client"; import { useState, useEffect } from "react"; import { Separator } from "@/components/ui/separator"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Png } from "@/lib/png"; import { ChevronUp, ChevronDown } from "lucide-react"; import { CollapsibleContent, Collapsible, CollapsibleTrigger, } from "@/components/ui/collapsible"; interface Card { id: string; name: string; first_mes: string; description: string; personality: string; mes_example: string; scenario: string; avatarUrl?: string; } export default function Home() { const [isInstructionsOpen, setIsInstructionsOpen] = useState(false); const [cards, setCards] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [selectedCardIndex, setSelectedCardIndex] = useState( null ); const [characterUrl, setCharacterUrl] = useState(""); const [avatarPath, setAvatarPath] = useState(""); const [isMetadataOpen, setIsMetadataOpen] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const fetchCards = async () => { try { setIsRefreshing(true); const response = await fetch("/api/proxy"); const data = await response.json(); if (data.cards) { setCards((prevCards) => { return data.cards.map((newCard: Card) => ({ ...newCard, avatarUrl: prevCards.find((c) => c.id === newCard.id)?.avatarUrl || newCard.avatarUrl, })); }); } } catch (error) { console.error("Error fetching cards:", error); } finally { setIsRefreshing(false); } }; useEffect(() => { fetchCards(); }, []); const downloadJson = (card: Card) => { const element = document.createElement("a"); const file = new Blob([JSON.stringify(card, null, 2)], { type: "application/json", }); element.href = URL.createObjectURL(file); element.download = `${card.name.replace(/\s+/g, "_")}.json`; document.body.appendChild(element); element.click(); document.body.removeChild(element); }; const downloadPng = async (card: Card, cardId: string) => { if (!card.avatarUrl) return; try { // Use proxy directly instead of attempting CORS const img = new Image(); img.src = `/api/proxy/image?url=${encodeURIComponent(card.avatarUrl)}`; await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; }); // Create a canvas to convert WebP to PNG const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not get canvas context"); // Draw the image and convert to PNG ctx.drawImage(img, 0, 0); const pngBlob = await new Promise((resolve) => { canvas.toBlob((blob) => { if (blob) resolve(blob); else throw new Error("Could not convert to PNG"); }, "image/png"); }); // Convert blob to array buffer for PNG embedding const arrayBuffer = await pngBlob.arrayBuffer(); // Prepare card data for embedding const cardData = JSON.stringify({ name: card.name, first_mes: card.first_mes, description: card.description, personality: card.personality, mes_example: card.mes_example, scenario: card.scenario, }); // Generate PNG with embedded card data const newImageData = Png.Generate(arrayBuffer, cardData); const newFileName = `${ card.name.replace(/\s+/g, "_") || "character" }.png`; const newFile = new File([newImageData], newFileName, { type: "image/png", }); // Download the file const link = URL.createObjectURL(newFile); const a = document.createElement("a"); a.download = newFileName; a.href = link; a.click(); // Cleanup URL.revokeObjectURL(link); } catch (error) { console.error("Error generating PNG:", error); alert("Couldn't export this character card, sorry."); } }; const handleOpenDialog = (index: number) => { setSelectedCardIndex(index); setDialogOpen(true); setCharacterUrl(""); setAvatarPath(""); setIsMetadataOpen(false); }; 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); } }; const handleFetchAvatar = async () => { if (selectedCardIndex === null) return; try { const avatarUrl = `https://ella.janitorai.com/bot-avatars/${avatarPath}`; const updatedCards = [...cards]; updatedCards[selectedCardIndex] = { ...updatedCards[selectedCardIndex], avatarUrl, }; setCards(updatedCards); setDialogOpen(false); } catch (error) { console.error("Error fetching avatar:", error); } }; return (

Sucker

How to Use

Follow every instruction here to the letter because it's all you need to know and I have no intent of helping you further.

  1. Put https://sucker.severian.dev/api/proxy in your API settings, any value for model and key.
  2. Remove your custom prompt - otherwise, it'll get inserted into cards, on the example message section.
  3. Save settings and refresh the page. Not this page. That{" "} page.
  4. Start a new chat with a character or multiple. Send any message.
  5. Hit the Refresh button here, and the cards should appear here.
  6. Download the JSON files or go through a little more effort to get PNGs instead.

Extractions will only last for 10 minutes, after which they're discarded. Reloading the page will remove any attached avatars. I'm not storing shit.

{cards.length === 0 ? (

No extractions yet.

) : ( cards.map((card, index) => ( {card.name || "Unnamed Card"}
{card.description && ( Description {card.description} )} {card.first_mes && ( First Message {card.first_mes} )} {card.scenario && ( Scenario {card.scenario} )} {card.mes_example && ( Example Messages {card.mes_example} )}
{!card.avatarUrl ? ( ) : ( )}
)) )}
{isMetadataOpen ? "Enter Avatar Path" : "Enter Character URL"} {isMetadataOpen ? "Look for the avatar field in the opened tab and paste the value here." : "Enter the Janitor character URL (https://janitorai.com/characters/...)."} {isMetadataOpen ? (
) => setAvatarPath(e.target.value) } />
) : (
) => setCharacterUrl(e.target.value) } />

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.

)}
); }