diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f5ef863 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Environment Variables for Sucker + +# Disable Discord community banner (optional) +# Uncomment the line below to hide the Discord banner +# NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true diff --git a/.gitignore b/.gitignore index 1380c2e..be1ad58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -.next \ No newline at end of file +.next +.env.local +.env*.local \ No newline at end of file diff --git a/README.md b/README.md index 7df0aad..9a2aeb9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # Sucker +### Environment Variables + +Add these to your `.env.local` file (optional): + +```bash +# Disable Discord community banner (defaults to showing banner) +NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true +``` + ### Usage Pull this repostory and build with `npm run build`. You can start the server with `node ./.next/standalone/server.js` @@ -8,6 +17,7 @@ You can also build and run Sucker as a Docker container with `docker compose bui ### Changelog +- 2.2: Added support for directly inputting webp file names or character image links when creating PNG character cards - 2.1: updated deps, note about image fetching, list of mirrors - 2.0: from Tui: Multimessage support! Tracks changes to character descriptions and scenarios across multiple extractions. Shows version badges, message counts, and provides detailed change history viewer. - also 2.0: V2 charcard format and alternate greetings. diff --git a/package-lock.json b/package-lock.json index 62a491f..62637fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@types/react": "^19.2.7", @@ -59,6 +60,44 @@ "tslib": "^2.4.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -766,6 +805,29 @@ } } }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", @@ -966,6 +1028,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -1024,6 +1115,96 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", @@ -1113,6 +1294,37 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", @@ -1262,6 +1474,48 @@ } } }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1296,7 +1550,6 @@ "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" } @@ -1306,7 +1559,6 @@ "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" } @@ -1466,7 +1718,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2078,7 +2329,6 @@ "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" } @@ -2376,7 +2626,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2545,7 +2794,6 @@ "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" } @@ -2555,7 +2803,6 @@ "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" }, @@ -2851,7 +3098,6 @@ "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", @@ -2961,7 +3207,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 40aad4e..470e35d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@types/react": "^19.2.7", diff --git a/public/avalon/avalon-pfp.png b/public/avalon/avalon-pfp.png new file mode 100644 index 0000000..1d02025 Binary files /dev/null and b/public/avalon/avalon-pfp.png differ diff --git a/public/avalon/avalon-pfp.webp b/public/avalon/avalon-pfp.webp new file mode 100644 index 0000000..855da07 Binary files /dev/null and b/public/avalon/avalon-pfp.webp differ diff --git a/public/avalon/bg.webp b/public/avalon/bg.webp new file mode 100644 index 0000000..4a9b23b Binary files /dev/null and b/public/avalon/bg.webp differ diff --git a/public/avalon/morgana-thumb.png b/public/avalon/morgana-thumb.png new file mode 100644 index 0000000..5e0534c Binary files /dev/null and b/public/avalon/morgana-thumb.png differ diff --git a/public/avalon/morgana-thumb.webp b/public/avalon/morgana-thumb.webp new file mode 100644 index 0000000..f87aaac Binary files /dev/null and b/public/avalon/morgana-thumb.webp differ diff --git a/public/avalon/trashpanda.webp b/public/avalon/trashpanda.webp new file mode 100644 index 0000000..cd0756e Binary files /dev/null and b/public/avalon/trashpanda.webp differ diff --git a/src/app/page.tsx b/src/app/page.tsx index 45d09e8..bdba3ee 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -20,18 +20,18 @@ import { import { Input } from "@/components/ui/input"; import { Png } from "@/lib/png"; import { - ChevronUp, - ChevronDown, Copy, ChevronLeft, ChevronRight, + Menu, } from "lucide-react"; import { - CollapsibleContent, - Collapsible, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { CollapsibleInfobox } from "@/components/ui/collapsible-infobox"; + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; +import { DiscordBannerPermanent } from "@/components/ui/discord-banner-permanent"; import Script from "next/script"; interface CardDataV2 { @@ -66,7 +66,6 @@ interface Card { } export default function Home() { - const [isInstructionsOpen, setIsInstructionsOpen] = useState(false); const [cards, setCards] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [selectedCardIndex, setSelectedCardIndex] = useState( @@ -82,6 +81,9 @@ export default function Home() { const [altGreetingIndexById, setAltGreetingIndexById] = useState< Record >({}); + const [mirrorsDialogOpen, setMirrorsDialogOpen] = useState(false); + const [changelogDialogOpen, setChangelogDialogOpen] = useState(false); + const [howToUseDialogOpen, setHowToUseDialogOpen] = useState(false); const [proxyUrl, setProxyUrl] = useState( "https://sucker.severian.dev/api/proxy" @@ -364,222 +366,94 @@ export default function Home() { gtag('config', 'G-YVD6QFSR71');`}
-
-
-

Sucker v2.1

-

- Just some notes this time -

+ {/* Mobile Layout */} +
+ {/* Row 1: Title on left, Hamburger on right */} +
+
+

Sucker v2.2

+

+ Consider joining Avalon! +

+
+ + + + + + setHowToUseDialogOpen(true)}> + How to Use + + setMirrorsDialogOpen(true)}> + Mirrors + + setChangelogDialogOpen(true)}> + Changelog + + +
+ {/* Row 2: Full-width Refresh button */}
- - {/* Collapsible infoboxes */} - {[ - { - title: "(Dec 2025) A note about fetching avatars", - content: ( - <> -

- The platform you suck from has implemented limited visibility - of metadata for certain content with a particular 'obscenity - rating'. This means that in some cases, the Fetch Avatar flow - here will show a 404 - character not found error at the end. -

-

- Sometimes (but not always), the avatar URL can still be - fetched after a day or two since the bot was published. -

-

- As of this moment, can't really find a fix for it, so you'll - have to download the image yourself and just add the image to - the card someplace else. -

- - ), - }, - { - title: - "(Oct 2025) V2 charcard format, multi-turn support for scripts/lorebooks, alternate greetings.", - content: ( -

- Sucker now tracks changes to character descriptions and - scenarios across multiple messages. Cards with multiple versions - show a version badge and offer a "Download Changes" button to - get the change history. -
- Alternate greetings are also supported. Sucker will provide you - with a conversation ID that you can use to start off a new chat - when capturing alternate greetings, send it as first message - instead of the character name. -
- Directions are updated below. Make sure you read 'em. -

- ), - }, - { - title: "List of mirrors", - content: ( - <> -

- Sucker goes down sometimes on severian.dev because I use the - server for other stuff. Here's a full list of existing sucker - instances (thanks to those who signed up for it!): -

- -

-
- If you're interested in hosting your own sucker instance, lmk - via Discord: @lyseverian, I've made the GH repo private for - now. Or send me a message if there's anything you think that - could be added here, open to suggestions. -

- - ), - }, - ].map((infobox, idx) => ( - - {infobox.content} - - ))} - - -
-

How to Use

- - - + {/* Desktop Layout */} +
+
+

Sucker v2.2

+

+ Consider joining Avalon! +

- -
-

- 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 {proxyUrl} in - your API settings, any value for model and key. -
  2. -
  3. - REQUIRED: Set your custom prompt to{" "} - <.> -
  4. -
  5. - REQUIRED: Set your persona (or create a new one) with the name{" "} - {user} and - the description should only have{" "} - . in it. -
  6. -
  7. - Save settings and refresh the page. Not this page. That{" "} - page. -
  8. -
  9. Start a new chat with a character.
  10. -
  11. - Char name inference is implemented: if you send just a dot:{" "} - ., sucker will use - the inferred name from the persona tag, or you can send the - character name yourself. -
  12. -
  13. - Hit the Refresh button here, and the cards should appear here. -
  14. -
  15. - If you're interested in capturing alternate greetings, start a - new chat and send the conversation ID as first message instead - of the character name. The format is{" "} - - [sucker:conv=conversationId] - {" "} - which you'll be given when creating a new card. -
  16. -
  17. - You can also send more messages with possible keywords to - trigger scripts/lorebooks. Sucker will track changes to the - description and scenario fields. Cards with multiple versions - will show a version badge and offer a "Download Changes" - button to get a detailed change history with timestamps. - Unfortunately, lorebook creation is out of scope at the - moment, but you can use the changes detected to modify the - character card yourself post-export. -
  18. -
  19. - Download the JSON files or go through a little more effort to - get PNGs instead. -
  20. -
-

- 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. -

-
-
- +
+ + + + +
+
+ + + + {/* Discord Banner - Disabled if NEXT_PUBLIC_DISABLE_DISCORD_BANNER is set */} + {process.env.NEXT_PUBLIC_DISABLE_DISCORD_BANNER !== "true" && ( + + )}
{cards.length === 0 ? ( @@ -887,6 +761,234 @@ export default function Home() {
+ {/* How to Use Dialog */} + + + + 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 {proxyUrl} in + your API settings, any value for model and key. +
  2. +
  3. + REQUIRED: Set your custom prompt to{" "} + <.> +
  4. +
  5. + REQUIRED: Set your persona (or create a new one) with the name{" "} + {user} and + the description should only have{" "} + . in it. +
  6. +
  7. + Save settings and refresh the page. Not this page. That{" "} + page. +
  8. +
  9. Start a new chat with a character.
  10. +
  11. + Char name inference is implemented: if you send just a dot:{" "} + ., sucker will use + the inferred name from the persona tag, or you can send the + character name yourself. +
  12. +
  13. + Hit the Refresh button here, and the cards should appear here. +
  14. +
  15. + If you're interested in capturing alternate greetings, start a + new chat and send the conversation ID as first message instead + of the character name. The format is{" "} + + [sucker:conv=conversationId] + {" "} + which you'll be given when creating a new card. +
  16. +
  17. + You can also send more messages with possible keywords to + trigger scripts/lorebooks. Sucker will track changes to the + description and scenario fields. Cards with multiple versions + will show a version badge and offer a "Download Changes" + button to get a detailed change history with timestamps. + Unfortunately, lorebook creation is out of scope at the + moment, but you can use the changes detected to modify the + character card yourself post-export. +
  18. +
  19. + Download the JSON files or go through a little more effort to + get PNGs instead. +
  20. +
+

+ 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. +

+
+
+
+ + {/* Mirrors Dialog */} + + + + Sucker Mirrors + + Sucker goes down sometimes on severian.dev because I use the + server for other stuff. Here's a full list of existing sucker + instances (thanks to those who signed up for it!): + + +
+ +

+ If you're interested in hosting your own sucker instance, lmk + via Discord: @lyseverian, I've made the GH repo private for + now. Or send me a message if there's anything you think that + could be added here, open to suggestions. +

+
+
+
+ + {/* Changelog Dialog */} + + + + Changelog + + Recent updates and changes to Sucker + + +
+
+

Jan 2026 - Direct image input

+
+

+ You can now paste webp filenames (like id.webp) or full image URLs + directly into the avatar field without having to open the metadata tab first. +

+

+ Makes grabbing avatars way faster when you already know the image path. +

+

+ You should also consider joining our new thing, a Discord community server for botmakers:{" "} + + discord.gg/5jQKkCfHP3 + +

+
+
+ +
+

Dec 2025 - A note about fetching avatars

+
+

+ The platform you suck from has implemented limited visibility + of metadata for certain content with a particular 'obscenity + rating'. This means that in some cases, the Fetch Avatar flow + here will show a 404 - character not found error at the end. +

+

+ Sometimes (but not always), the avatar URL can still be + fetched after a day or two since the bot was published. +

+

+ As of this moment, can't really find a fix for it, so you'll + have to download the image yourself and just add the image to + the card someplace else. +

+
+
+ +
+

+ Oct 2025 - V2 charcard format, multi-turn support +

+
+

+ Sucker now tracks changes to character descriptions and + scenarios across multiple messages. Cards with multiple versions + show a version badge and offer a "Download Changes" button to + get the change history. +

+

+ Alternate greetings are also supported. Sucker will provide you + with a conversation ID that you can use to start off a new chat + when capturing alternate greetings, send it as first message + instead of the character name. +

+

+ Directions are updated below. Make sure you read 'em. +

+
+
+
+
+
+ diff --git a/src/components/ui/collapsible-infobox.tsx b/src/components/ui/collapsible-infobox.tsx deleted file mode 100644 index 814cd92..0000000 --- a/src/components/ui/collapsible-infobox.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useState } from "react"; -import { ChevronDown, ChevronUp } from "lucide-react"; -import { Button } from "@/components/ui/button"; - -export interface CollapsibleInfoboxProps { - title: string; - children: React.ReactNode; - defaultOpen?: boolean; - className?: string; -} - -export function CollapsibleInfobox({ - title, - children, - defaultOpen = false, - className = "", -}: CollapsibleInfoboxProps) { - const [open, setOpen] = useState(defaultOpen); - return ( -
-
setOpen((v) => !v)} - > - - {title} - - -
- {open &&
{children}
} -
- ); -} diff --git a/src/components/ui/discord-banner-permanent.tsx b/src/components/ui/discord-banner-permanent.tsx new file mode 100644 index 0000000..bb33335 --- /dev/null +++ b/src/components/ui/discord-banner-permanent.tsx @@ -0,0 +1,110 @@ +"use client"; + +import Image from "next/image"; + +interface DiscordBannerPermanentProps { + inviteLink: string; + serverName?: string; + description?: string; + ctaText?: string; +} + +export function DiscordBannerPermanent({ + inviteLink, + serverName = "Avalon", + description = "Looking for somewhere more... interesting? Avalon's an enchanted collective of botmakers who refuse to be boring 🌿 Come play with us~ (18+ only, darling)", + ctaText = "Join Server", +}: DiscordBannerPermanentProps) { + return ( +
+ {/* Background Image */} +
+ Background +
+ + {/* Content */} +
+ {/* Chat Message Style */} +
+ {/* Morgana Avatar */} +
+ Morgana +
+ + {/* Message Content */} +
+
+ Morgana + Today at {new Date().toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })} +
+
+ {description} +
+ + {/* Server Invite Embed */} +
+
+
+ You've been invited to join a server +
+
+ Avalon +
+
{serverName}
+
+
67 members
+ + from-avalon.com + +
+
+ + {ctaText} + +
+
+
+
+
+ + {/* Trash Panda Icon - Bottom Right */} +
+ +
+
+
+ ); +} diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..32f28d2 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,41 @@ +"use client"; + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className = "", sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className = "", inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem };