Compare commits

..

11 Commits

Author SHA1 Message Date
Ema
06d8b2e36c Merge pull request #6 from severian-dev/docker-next-standalone
Docker next standalone
2025-12-10 22:37:39 -05:00
Ema
59acc534fa Cleaning readme. 2025-12-10 22:34:29 -05:00
Ema
fdd13085c3 Removing runtime env 2025-12-10 22:31:54 -05:00
Severian
8923bf3f63 chore: prod env, no sourcemaps 2025-12-11 08:42:20 +08:00
Ema P.
a02087915b Standalone Readme Editing 2025-12-10 12:38:10 -05:00
Ema P.
e6e230ab84 Building image from .next standalone server. 2025-12-10 10:55:35 -05:00
Ema P.
b3aece1e41 Adding next.config.js for standalone. 2025-12-10 10:52:36 -05:00
Ema
24441720d6 Merge pull request #5 from leri-a/master
Updating base image
2025-12-10 09:42:29 -05:00
Ema P.
2fc4e419b2 Updating base image 2025-12-10 09:39:17 -05:00
Severian
95f5a3e725 chore: 2.1 2025-12-10 08:27:22 +08:00
Severian
f99985ad6c chore: deps 2025-12-09 10:40:18 +08:00
11 changed files with 1468 additions and 920 deletions

47
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,47 @@
# Copilot Instructions for sucker.severian.dev
## Project Overview
- This is a Next.js project with a custom proxy API and UI components, using Tailwind CSS and PostCSS for styling.
- The main app logic is in `src/app/`, with global styles in `globals.css` and layout in `layout.tsx`.
- API routes are under `src/app/api/proxy/`, including image proxying (`image/route.ts`).
- UI components are in `src/components/ui/` and utility functions in `src/components/lib/`.
## Architecture & Data Flow
- The app uses Next.js routing and API routes for backend logic. The proxy API handles requests to external services, including image fetching and transformation.
- UI components follow a modular pattern, with reusable elements (e.g., `button.tsx`, `card.tsx`).
- Data flows from API routes to UI via React props and hooks. No global state management library is present.
## Developer Workflows
- **Build & Dev:** Use `npm run dev` to start the development server. Check `package.json` for other scripts.
- **Styling:** Tailwind CSS is configured via `tailwind.config.js` and PostCSS via `postcss.config.js`.
- **API:** Custom logic for proxying and image handling is in `src/app/api/proxy/`. Review these files for request/response patterns.
- **No test suite detected.** If adding tests, follow Next.js and React conventions.
## Project-Specific Conventions
- API routes use Next.js `route.ts` files, with custom logic for proxying and image manipulation.
- UI components are colocated in `src/components/ui/` and use Tailwind utility classes.
- Utility functions (e.g., PNG handling) are in `src/components/lib/`.
- Minimal documentation; refer to code for implementation details.
- Changelog is maintained in `README.md`.
## Integration Points & External Dependencies
- Relies on Next.js, React, Tailwind CSS, and PostCSS.
- External requests are proxied via custom API routes.
- Docker support via `docker-compose.yml` and `dockerfile` for containerization.
## Examples
- To add a new API route: create a `route.ts` under `src/app/api/yourroute/`.
- To add a new UI component: place a `.tsx` file in `src/components/ui/` and use Tailwind for styling.
- For image processing, review `src/app/api/proxy/image/route.ts` and `src/components/lib/png.ts`.
## Key Files & Directories
- `src/app/` — Main app logic and API routes
- `src/components/ui/` — UI components
- `src/components/lib/` — Utility functions
- `tailwind.config.js`, `postcss.config.js` — Styling configuration
- `docker-compose.yml`, `dockerfile` — Containerization
- `README.md` — Changelog and minimal project notes
---
If any section is unclear or missing important project-specific details, please provide feedback to improve these instructions.

View File

@@ -1,9 +1,14 @@
# Sucker # Sucker
Check package.json for commands, I can't be bothered. ### Usage
Pull this repostory and build with `npm run build`. You can start the server with `node ./.next/standalone/server.js`
You can also build and run Sucker as a Docker container with `docker compose build` and `docker compose up`.
### Changelog ### Changelog
- 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. - 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. - also 2.0: V2 charcard format and alternate greetings.
- 1.9: Not again. They changed stuff again. What is this? - 1.9: Not again. They changed stuff again. What is this?

View File

@@ -4,5 +4,3 @@ services:
image: sucker image: sucker
ports: ports:
- "3000:3000" - "3000:3000"
environment:
NODE_ENV: production

View File

@@ -1,7 +1,5 @@
FROM node:18-alpine AS base FROM node:22-alpine AS base
WORKDIR /app WORKDIR /app
FROM base AS deps FROM base AS deps
COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
RUN \ RUN \
@@ -12,23 +10,22 @@ RUN \
fi fi
FROM base AS builder FROM base AS builder
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
RUN npm run build RUN npm run build
FROM base AS runner FROM node:22-alpine AS runner
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
WORKDIR /app WORKDIR /app
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
USER nextjs USER nextjs
EXPOSE 3000 EXPOSE 3000
CMD ["npm", "start"] CMD ["node", "server.js"]

3
next-env.d.ts vendored
View File

@@ -1,5 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7
next.config.js Normal file
View File

@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
productionBrowserSourceMaps: false,
};
module.exports = nextConfig;

2045
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,8 @@
"@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.1.1",
"@types/react": "^18.2.39", "@types/react": "^19.2.7",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^19.2.3",
"axios": "^1.6.2", "axios": "^1.6.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -23,9 +23,9 @@
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"lucide-react": "^0.471.0", "lucide-react": "^0.471.0",
"next": "^14.0.3", "next": "^16.0.7",
"react": "^18.2.0", "react": "^19.2.1",
"react-dom": "^18.2.0", "react-dom": "^19.2.1",
"tailwind-merge": "^2.6.0", "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^5.3.2" "typescript": "^5.3.2"

View File

@@ -31,6 +31,7 @@ import {
Collapsible, Collapsible,
CollapsibleTrigger, CollapsibleTrigger,
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { CollapsibleInfobox } from "@/components/ui/collapsible-infobox";
import Script from "next/script"; import Script from "next/script";
interface CardDataV2 { interface CardDataV2 {
@@ -82,7 +83,9 @@ export default function Home() {
Record<string, number> Record<string, number>
>({}); >({});
const [proxyUrl, setProxyUrl] = useState("https://sucker.severian.dev/api/proxy"); const [proxyUrl, setProxyUrl] = useState(
"https://sucker.severian.dev/api/proxy"
);
const fetchCards = async () => { const fetchCards = async () => {
try { try {
@@ -321,7 +324,10 @@ export default function Home() {
return ( return (
<main className="min-h-screen bg-background text-foreground"> <main className="min-h-screen bg-background text-foreground">
<Script src="https://www.googletagmanager.com/gtag/js?id=G-YVD6QFSR71" strategy="afterInteractive" /> <Script
src="https://www.googletagmanager.com/gtag/js?id=G-YVD6QFSR71"
strategy="afterInteractive"
/>
<Script id="gtag-init" strategy="afterInteractive"> <Script id="gtag-init" strategy="afterInteractive">
{`window.dataLayer = window.dataLayer || []; {`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);} function gtag(){dataLayer.push(arguments);}
@@ -331,9 +337,9 @@ export default function Home() {
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<div> <div>
<h1 className="text-3xl font-bold">Sucker v2.0</h1> <h1 className="text-3xl font-bold">Sucker v2.1</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
A couple of updates, see below. Just some notes this time
</p> </p>
</div> </div>
<Button <Button
@@ -346,13 +352,34 @@ export default function Home() {
</div> </div>
<Separator className="my-4" /> <Separator className="my-4" />
<div className="mb-8"> {/* Collapsible infoboxes */}
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4"> {[
<div className="flex flex-col justify-between"> {
<span className="text-lg font-semibold text-blue-800 dark:text-blue-200"> title: "(Dec 2025) A note about fetching avatars",
V2 charcard format, multi-turn support for scripts/lorebooks, content: (
alternate greetings. <>
</span> <p className="text-sm text-muted-foreground">
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.
</p>
<p className="text-sm text-muted-foreground">
Sometimes (but not always), the avatar URL can still be
fetched after a day or two since the bot was published.
</p>
<p className="text-sm text-muted-foreground">
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.
</p>
</>
),
},
{
title:
"(Oct 2025) V2 charcard format, multi-turn support for scripts/lorebooks, alternate greetings.",
content: (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Sucker now tracks changes to character descriptions and Sucker now tracks changes to character descriptions and
scenarios across multiple messages. Cards with multiple versions scenarios across multiple messages. Cards with multiple versions
@@ -365,13 +392,79 @@ export default function Home() {
instead of the character name. instead of the character name.
<br /> <br />
Directions are updated below. Make sure you read 'em. Directions are updated below. Make sure you read 'em.
<br />
If you're interested in hosting your own sucker instance, lmk
via Discord: @lyseverian, I've made the GH repo private for now.
</p> </p>
</div> ),
</div> },
</div> {
title: "List of mirrors",
content: (
<>
<p className="text-sm text-muted-foreground">
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!):
</p>
<ul className="text-sm flex flex-col sm:flex-row list-none">
<li className="after:content-none sm:after:mx-2 sm:after:content-['•'] sm:last:after:content-none">
<a
className="text-yellow-600"
href="https://sucker.severian.dev"
>
severian.dev
</a>
</li>
<li className="after:content-none sm:after:mx-2 sm:after:content-['•'] sm:last:after:content-none">
<a
className="text-yellow-600"
href="https://sucker.trashpanda.land"
>
trashpanda.land
</a>
</li>
<li className="after:content-none sm:after:mx-2 sm:after:content-['•'] sm:last:after:content-none">
<a
className="text-yellow-600"
href="https://sucker.hitani.me"
>
hitani.me
</a>
</li>
<li className="after:content-none sm:after:mx-2 sm:after:content-['•'] sm:last:after:content-none">
<a
className="text-yellow-600"
href="https://succ.portalnexus.link"
>
portalnexus.link
</a>
</li>
<li className="after:content-none sm:after:mx-2 sm:after:content-['•'] sm:last:after:content-none">
<a
className="text-yellow-600"
href="https://sucker.lemuria.dev"
>
lemuria.dev
</a>
</li>
</ul>
<p className="text-sm text-muted-foreground">
<br />
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.
</p>
</>
),
},
].map((infobox, idx) => (
<CollapsibleInfobox
key={infobox.title}
title={infobox.title}
defaultOpen={false}
>
{infobox.content}
</CollapsibleInfobox>
))}
<Collapsible <Collapsible
open={isInstructionsOpen} open={isInstructionsOpen}
@@ -399,16 +492,18 @@ export default function Home() {
</p> </p>
<ol className="list-decimal list-inside"> <ol className="list-decimal list-inside">
<li className="mb-2"> <li className="mb-2">
Put <code style={{ color: "#fff0b9" }}>{proxyUrl}</code> in your Put <code style={{ color: "#fff0b9" }}>{proxyUrl}</code> in
API settings, any value for model and key. your API settings, any value for model and key.
</li> </li>
<li className="mb-2"> <li className="mb-2">
REQUIRED: Set your custom prompt to <code style={{ color: "#fff0b9" }}>&lt;.&gt;</code> REQUIRED: Set your custom prompt to{" "}
<code style={{ color: "#fff0b9" }}>&lt;.&gt;</code>
</li> </li>
<li className="mb-2"> <li className="mb-2">
REQUIRED: Set your persona (or create a new one) with the name{" "} REQUIRED: Set your persona (or create a new one) with the name{" "}
<code style={{ color: "#fff0b9" }}>&#123;user&#125;</code> and the description should only <code style={{ color: "#fff0b9" }}>&#123;user&#125;</code> and
have <code style={{ color: "#fff0b9" }}>.</code> in it. the description should only have{" "}
<code style={{ color: "#fff0b9" }}>.</code> in it.
</li> </li>
<li className="mb-2"> <li className="mb-2">
Save settings and refresh the page. Not this page. <i>That</i>{" "} Save settings and refresh the page. Not this page. <i>That</i>{" "}
@@ -416,7 +511,10 @@ export default function Home() {
</li> </li>
<li className="mb-2">Start a new chat with a character.</li> <li className="mb-2">Start a new chat with a character.</li>
<li className="mb-2"> <li className="mb-2">
Char name inference is implemented: if you send just a dot: <code style={{ color: "#fff0b9" }}>.</code>, sucker will use the inferred name from the persona tag, or you can send the character name yourself. Char name inference is implemented: if you send just a dot:{" "}
<code style={{ color: "#fff0b9" }}>.</code>, sucker will use
the inferred name from the persona tag, or you can send the
character name yourself.
</li> </li>
<li className="mb-2"> <li className="mb-2">
Hit the Refresh button here, and the cards should appear here. Hit the Refresh button here, and the cards should appear here.
@@ -425,12 +523,20 @@ export default function Home() {
If you're interested in capturing alternate greetings, start a If you're interested in capturing alternate greetings, start a
new chat and send the conversation ID as first message instead new chat and send the conversation ID as first message instead
of the character name. The format is{" "} of the character name. The format is{" "}
<code style={{ color: "#fff0b9" }}>[sucker:conv=conversationId]</code> which you'll be <code style={{ color: "#fff0b9" }}>
given when creating a new card. [sucker:conv=conversationId]
</code>{" "}
which you'll be given when creating a new card.
</li> </li>
<li className="mb-2"> <li className="mb-2">
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 You can also send more messages with possible keywords to
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. 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.
</li> </li>
<li className="mb-2"> <li className="mb-2">
Download the JSON files or go through a little more effort to Download the JSON files or go through a little more effort to

View File

@@ -0,0 +1,52 @@
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 (
<div
className={`bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4 ${className}`}
>
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => setOpen((v) => !v)}
>
<span className="text-lg font-semibold text-blue-800 dark:text-blue-200">
{title}
</span>
<Button
variant="ghost"
size="sm"
className="w-9 p-0"
tabIndex={-1}
type="button"
onClick={(e) => {
e.stopPropagation();
setOpen((v) => !v);
}}
>
{open ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
<span className="sr-only">Toggle {title}</span>
</Button>
</div>
{open && <div className="mt-2">{children}</div>}
</div>
);
}

View File

@@ -2,8 +2,14 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"types": ["node"], "dom",
"dom.iterable",
"esnext"
],
"types": [
"node"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
@@ -13,7 +19,7 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true, "incremental": true,
"plugins": [ "plugins": [
{ {
@@ -21,9 +27,19 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": [
"./src/*"
]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": [
"exclude": ["node_modules"] "next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }