diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts
index 7b76e02..de5c248 100644
--- a/src/app/api/proxy/route.ts
+++ b/src/app/api/proxy/route.ts
@@ -20,13 +20,34 @@ interface CardVersion {
};
}
-interface StoredCard extends CardData {
+interface CardDataV2 {
+ name: string;
+ first_mes: string;
+ alternate_greetings: string[];
+ description: string;
+ personality: string;
+ mes_example: string;
+ scenario: string;
+ creator: string;
+ creator_notes: string;
+ system_prompt: string;
+ post_history_instructions: string;
+ tags: string[];
+ character_version: string;
+ extensions: Record;
+}
+
+interface StoredCard {
timestamp: number;
id: string;
versions: CardVersion[];
currentVersion: number;
messageCount: number;
conversationId: string;
+ trackingName: string;
+ data: CardDataV2;
+ spec: "chara_card_v2";
+ spec_version: "2.0";
}
let extractedCards: StoredCard[] = [];
@@ -47,14 +68,10 @@ interface Message {
content: string;
}
-interface CardData {
- name: string;
+// Extracted shape used during POST handling
+interface ExtractedCard {
trackingName: string;
- first_mes: string;
- description: string;
- personality: string;
- mes_example: string;
- scenario: string;
+ data: CardDataV2;
}
function extractPersonaName(content: string): string | null {
@@ -65,6 +82,16 @@ function extractPersonaName(content: string): string | null {
return null;
}
+function parseConversationToken(content: string | undefined | null): string | null {
+ if (!content) return null;
+ const trimmed = content.trim();
+ const match = trimmed.match(/^\[sucker:conv=([a-z0-9]+)\]$/i);
+ if (match) {
+ return match[1];
+ }
+ return null;
+}
+
function removePersonaTags(content: string): string {
let result = content;
const openingMatch = result.match(/<[^<>]+?\s*'s\s+Persona>/i);
@@ -80,15 +107,18 @@ function removePersonaTags(content: string): string {
return result;
}
-function extractCardData(messages: Message[]): CardData {
+function extractCardData(messages: Message[]): ExtractedCard {
const first_mes = messages[2].content.replace(/{user}/g, "{{user}}");
const nameContent = messages[3].content;
+ // If the name slot is actually a token, ignore it for naming purposes
+ const tokenInNameSlot = parseConversationToken(nameContent);
const lastColonIndex = nameContent.lastIndexOf(": ");
- const nameFromUser =
- lastColonIndex !== -1
- ? nameContent.substring(lastColonIndex + 2).trim()
- : "";
+ const nameFromUser = tokenInNameSlot
+ ? ""
+ : lastColonIndex !== -1
+ ? nameContent.substring(lastColonIndex + 2).trim()
+ : "";
let content = messages[0].content.replace(/{user}/g, "{{user}}");
const inferredName = extractPersonaName(content);
@@ -143,36 +173,33 @@ function extractCardData(messages: Message[]): CardData {
const description = content.trim();
- return {
+ const data: CardDataV2 = {
name: displayName,
- trackingName: cleanTrackingName,
first_mes,
+ alternate_greetings: [],
description,
personality: "",
mes_example,
scenario,
+ creator: "",
+ creator_notes: "",
+ system_prompt: "",
+ post_history_instructions: "",
+ tags: [],
+ character_version: "1",
+ extensions: {},
+ };
+
+ return {
+ trackingName: cleanTrackingName,
+ data,
};
}
-function generateConversationId(messages: Message[]): string {
- // Create a simple hash from the character name in the persona tag to identify conversations
- const content = messages[0]?.content || "";
- const personaMatch = content.match(/<([^<>]+?)\s*'s\s+Persona>/i);
- if (personaMatch) {
- return personaMatch[1]
- .trim()
- .toLowerCase()
- .replace(/[^a-zA-Z0-9]/g, "");
- }
- // Fallback to content-based ID
- return content
- .substring(0, 50)
- .replace(/[^a-zA-Z0-9]/g, "")
- .toLowerCase();
-}
+// conversationId is now an opaque random ID generated via generateId() on creation
function detectChanges(
- newCard: CardData,
+ newData: CardDataV2,
existingCard: StoredCard
): {
description?: { old: string; new: string };
@@ -184,16 +211,16 @@ function detectChanges(
} = {};
let hasChanges = false;
- if (newCard.description.trim() !== existingCard.description.trim()) {
+ if (newData.description.trim() !== existingCard.data.description.trim()) {
changes.description = {
- old: existingCard.description,
- new: newCard.description,
+ old: existingCard.data.description,
+ new: newData.description,
};
hasChanges = true;
}
- if (newCard.scenario.trim() !== existingCard.scenario.trim()) {
- changes.scenario = { old: existingCard.scenario, new: newCard.scenario };
+ if (newData.scenario.trim() !== existingCard.data.scenario.trim()) {
+ changes.scenario = { old: existingCard.data.scenario, new: newData.scenario };
hasChanges = true;
}
@@ -207,9 +234,13 @@ function findExistingCard(trackingName: string): StoredCard | null {
);
}
+function findExistingCardByConversationId(conversationId: string): StoredCard | null {
+ return extractedCards.find((card) => card.conversationId === conversationId) || null;
+}
+
function updateCardWithVersion(
existingCard: StoredCard,
- newCard: CardData,
+ newData: CardDataV2,
changes: {
description?: { old: string; new: string };
scenario?: { old: string; new: string };
@@ -259,10 +290,10 @@ function updateCardWithVersion(
// Update the main card data
if (changes.description) {
- existingCard.description = changes.description.new;
+ existingCard.data.description = changes.description.new;
}
if (changes.scenario) {
- existingCard.scenario = changes.scenario.new;
+ existingCard.data.scenario = changes.scenario.new;
}
}
@@ -303,9 +334,62 @@ export async function POST(request: NextRequest) {
);
}
- const cardData = extractCardData(body.messages);
- const conversationId = generateConversationId(body.messages);
- const existingCard = findExistingCard(cardData.trackingName);
+ // Parse potential token from messages[3]
+ const tokenCandidate: string | undefined = body.messages?.[3]?.content;
+ const providedConversationId = parseConversationToken(tokenCandidate || undefined);
+
+ let existingCard: StoredCard | null = null;
+ let linkedByToken = false;
+ if (providedConversationId) {
+ const byToken = findExistingCardByConversationId(providedConversationId);
+ if (!byToken) {
+ const notFoundMessage = `Conversation ID not found. Please provide a valid token or the character name to create a new one: [sucker:conv=]`;
+ if (isStreamingRequest) {
+ return createSSEErrorResponse(notFoundMessage);
+ }
+ return NextResponse.json(
+ {
+ id: `chatcmpl-${generateId()}`,
+ object: "chat.completion",
+ created: Math.floor(Date.now() / 1000),
+ model: "sucker-v2",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: notFoundMessage,
+ },
+ finish_reason: "stop",
+ },
+ ],
+ usage: {
+ prompt_tokens: 0,
+ completion_tokens: notFoundMessage.split(" ").length,
+ total_tokens: notFoundMessage.split(" ").length,
+ },
+ },
+ {
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Content-Type": "application/json",
+ },
+ }
+ );
+ }
+ existingCard = byToken;
+ linkedByToken = true;
+ }
+
+ const extracted = extractCardData(body.messages);
+ if (!existingCard) {
+ existingCard = findExistingCard(extracted.trackingName);
+ }
+
+ // Prefer existing card's conversationId; otherwise generate a new random one for creation
+ const conversationId = existingCard
+ ? existingCard.conversationId
+ : providedConversationId || generateId();
console.log(`Conversation ID: ${conversationId}`);
@@ -313,13 +397,13 @@ export async function POST(request: NextRequest) {
let changesSummary = "";
console.log(
- `Processing card: "${cardData.name}" (tracking: "${cardData.trackingName}"), ConversationID: ${conversationId}`
+ `Processing card: "${extracted.data.name}" (tracking: "${extracted.trackingName}"), ConversationID: ${conversationId}`
);
console.log(
`Existing cards: ${extractedCards
.map(
(c) =>
- `"${c.name}" (tracking: "${c.trackingName}", v${c.currentVersion})`
+ `"${c.data.name}" (tracking: "${c.trackingName}", v${c.currentVersion})`
)
.join(", ")}`
);
@@ -330,7 +414,21 @@ export async function POST(request: NextRequest) {
);
if (existingCard) {
- const changes = detectChanges(cardData, existingCard);
+ let alternateGreetingRecorded = false;
+ // Capture alternate greeting if applicable (no version bump for greetings-only)
+ const normalizedGreeting = extracted.data.first_mes.trim();
+ if (
+ normalizedGreeting &&
+ normalizedGreeting !== existingCard.data.first_mes &&
+ !(existingCard.data.alternate_greetings || []).includes(normalizedGreeting)
+ ) {
+ if (!existingCard.data.alternate_greetings) existingCard.data.alternate_greetings = [];
+ existingCard.data.alternate_greetings.push(normalizedGreeting);
+ existingCard.timestamp = Date.now();
+ alternateGreetingRecorded = true;
+ }
+
+ const changes = detectChanges(extracted.data, existingCard);
console.log(`Changes detected:`, changes ? "YES" : "NO");
if (changes) {
console.log(
@@ -338,10 +436,10 @@ export async function POST(request: NextRequest) {
existingCard.currentVersion + 1
}`
);
- updateCardWithVersion(existingCard, cardData, changes);
+ updateCardWithVersion(existingCard, extracted.data, changes);
// Keep the original display name (don't update it)
- // existingCard.name stays the same
+ // existingCard.data.name stays the same
// Create a summary of changes for the response
const changeTypes = [];
@@ -353,13 +451,19 @@ export async function POST(request: NextRequest) {
} else {
existingCard.messageCount += 1;
// Keep the original display name (don't update it)
- // existingCard.name stays the same
+ // existingCard.data.name stays the same
responseMessage = `Character data unchanged (v${existingCard.currentVersion}, message ${existingCard.messageCount}).`;
}
+ if (alternateGreetingRecorded) {
+ responseMessage += ` Alternate greeting recorded.`;
+ }
+ if (linkedByToken) {
+ responseMessage += ` Conversation linked via provided ID.`;
+ }
} else {
// Create new card with initial version
const newCard: StoredCard = {
- ...cardData,
+ data: extracted.data,
timestamp: Date.now(),
id: generateId(),
conversationId,
@@ -369,17 +473,21 @@ export async function POST(request: NextRequest) {
version: 1,
timestamp: Date.now(),
changes: {
- description: { old: "", new: cardData.description },
- scenario: { old: "", new: cardData.scenario },
+ description: { old: "", new: extracted.data.description },
+ scenario: { old: "", new: extracted.data.scenario },
},
changeType: "initial",
messageCount: 1,
},
],
currentVersion: 1,
+ trackingName: extracted.trackingName,
+ spec: "chara_card_v2",
+ spec_version: "2.0",
};
extractedCards.push(newCard);
- responseMessage = `New character "${cardData.trackingName}" created (v1).`;
+ const tokenNote = ` This is the conversation ID you can use to start off a new chat when capturing alternate greetings, use it instead of the character name: [sucker:conv=${conversationId}]`;
+ responseMessage = `New character "${extracted.trackingName}" created (v1).${tokenNote}`;
}
cleanupExpiredCards();
@@ -466,7 +574,7 @@ export async function POST(request: NextRequest) {
}
}
-function getInitialCardVersion(card: StoredCard): CardData {
+function getInitialCardVersion(card: StoredCard): CardDataV2 {
// Get the initial version (v1) of the card
const initialVersion = card.versions.find((v) => v.version === 1);
if (
@@ -475,24 +583,38 @@ function getInitialCardVersion(card: StoredCard): CardData {
initialVersion.changes.scenario
) {
return {
- name: card.name,
- trackingName: card.trackingName,
- first_mes: card.first_mes,
+ name: card.data.name,
+ first_mes: card.data.first_mes,
+ alternate_greetings: card.data.alternate_greetings || [],
description: initialVersion.changes.description.new,
- personality: card.personality,
- mes_example: card.mes_example,
+ personality: card.data.personality,
+ mes_example: card.data.mes_example,
scenario: initialVersion.changes.scenario.new,
+ creator: card.data.creator,
+ creator_notes: card.data.creator_notes,
+ system_prompt: card.data.system_prompt,
+ post_history_instructions: card.data.post_history_instructions,
+ tags: card.data.tags,
+ character_version: card.data.character_version,
+ extensions: card.data.extensions,
};
}
// Fallback to current version if initial not found
return {
- name: card.name,
- trackingName: card.trackingName,
- first_mes: card.first_mes,
- description: card.description,
- personality: card.personality,
- mes_example: card.mes_example,
- scenario: card.scenario,
+ name: card.data.name,
+ first_mes: card.data.first_mes,
+ alternate_greetings: card.data.alternate_greetings || [],
+ description: card.data.description,
+ personality: card.data.personality,
+ mes_example: card.data.mes_example,
+ scenario: card.data.scenario,
+ creator: card.data.creator,
+ creator_notes: card.data.creator_notes,
+ system_prompt: card.data.system_prompt,
+ post_history_instructions: card.data.post_history_instructions,
+ tags: card.data.tags,
+ character_version: card.data.character_version,
+ extensions: card.data.extensions,
};
}
@@ -519,7 +641,7 @@ export async function GET(request: NextRequest) {
}
const changesReport = {
- cardName: card.name,
+ cardName: card.data.name,
cardId: card.id,
totalVersions: card.versions.length,
currentVersion: card.currentVersion,
@@ -545,7 +667,7 @@ export async function GET(request: NextRequest) {
};
// Sanitize filename for download
- const sanitizedName = card.name.replace(/[^a-zA-Z0-9\-_]/g, "_");
+ const sanitizedName = card.data.name.replace(/[^a-zA-Z0-9\-_]/g, "_");
return NextResponse.json(changesReport, {
headers: {
@@ -559,10 +681,12 @@ export async function GET(request: NextRequest) {
{
status: "online",
cards: extractedCards.map((card) => {
- const { timestamp, versions, ...cardData } = card;
+ const { timestamp, versions, ...rest } = card;
const initialVersion = getInitialCardVersion(card);
return {
- ...cardData,
+ ...rest,
+ data: card.data,
+ alternate_greetings: card.data.alternate_greetings || [],
hasVersions: versions && versions.length > 1,
versionCount: versions ? versions.length : 0,
messageCount: card.messageCount || 1,
diff --git a/src/app/page.tsx b/src/app/page.tsx
index bc1773d..434b4ce 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -19,33 +19,48 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Png } from "@/lib/png";
-import { ChevronUp, ChevronDown, Copy } from "lucide-react";
+import {
+ ChevronUp,
+ ChevronDown,
+ Copy,
+ ChevronLeft,
+ ChevronRight,
+} from "lucide-react";
import {
CollapsibleContent,
Collapsible,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
-interface Card {
- id: string;
+interface CardDataV2 {
name: string;
first_mes: string;
+ alternate_greetings?: string[];
description: string;
personality: string;
mes_example: string;
scenario: string;
+ creator?: string;
+ creator_notes?: string;
+ system_prompt?: string;
+ post_history_instructions?: string;
+ tags?: string[];
+ character_version?: string;
+ extensions?: Record;
+}
+
+interface Card {
+ id: string;
+ data: CardDataV2;
+ trackingName?: string;
+ spec?: string;
+ spec_version?: string;
avatarUrl?: string;
hasVersions?: boolean;
versionCount?: number;
messageCount?: number;
- initialVersion?: {
- name: string;
- first_mes: string;
- description: string;
- personality: string;
- mes_example: string;
- scenario: string;
- };
+ alternate_greetings?: string[];
+ initialVersion?: CardDataV2;
}
export default function Home() {
@@ -62,6 +77,9 @@ export default function Home() {
const [changesDialogOpen, setChangesDialogOpen] = useState(false);
const [selectedChanges, setSelectedChanges] = useState(null);
const [showFullText, setShowFullText] = useState(false);
+ const [altGreetingIndexById, setAltGreetingIndexById] = useState<
+ Record
+ >({});
const fetchCards = async () => {
try {
@@ -91,30 +109,38 @@ export default function Home() {
const downloadJson = (card: Card) => {
// Use initial version for download, or current version if no initial version available
- const downloadData = card.initialVersion
- ? {
- name: card.initialVersion.name,
- first_mes: card.initialVersion.first_mes,
- description: card.initialVersion.description,
- personality: card.initialVersion.personality,
- mes_example: card.initialVersion.mes_example,
- scenario: card.initialVersion.scenario,
- }
- : {
- name: card.name,
- first_mes: card.first_mes,
- description: card.description,
- personality: card.personality,
- mes_example: card.mes_example,
- scenario: card.scenario,
- };
+ const chosen = card.initialVersion || card.data;
+ const downloadData = {
+ data: {
+ name: chosen.name,
+ first_mes: chosen.first_mes,
+ alternate_greetings: chosen.alternate_greetings || [],
+ description: chosen.description,
+ personality: chosen.personality,
+ mes_example: chosen.mes_example,
+ scenario: chosen.scenario,
+ creator: (chosen as any).creator || "",
+ creator_notes: (chosen as any).creator_notes || "",
+ system_prompt: (chosen as any).system_prompt || "",
+ post_history_instructions:
+ (chosen as any).post_history_instructions || "",
+ tags: (chosen as any).tags || [],
+ character_version: (chosen as any).character_version || "1",
+ extensions: (chosen as any).extensions || {},
+ },
+ spec: card.spec || "chara_card_v2",
+ spec_version: card.spec_version || "2.0",
+ };
const element = document.createElement("a");
const file = new Blob([JSON.stringify(downloadData, null, 2)], {
type: "application/json",
});
element.href = URL.createObjectURL(file);
- element.download = `${card.name.replace(/[^a-zA-Z0-9\-_]/g, "_")}.json`;
+ element.download = `${(card.initialVersion?.name || card.data.name).replace(
+ /[^a-zA-Z0-9\-_]/g,
+ "_"
+ )}.json`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
@@ -133,10 +159,9 @@ export default function Home() {
type: "application/json",
});
element.href = URL.createObjectURL(file);
- element.download = `${card.name.replace(
- /[^a-zA-Z0-9\-_]/g,
- "_"
- )}_changes.json`;
+ element.download = `${(
+ card.initialVersion?.name || card.data.name
+ ).replace(/[^a-zA-Z0-9\-_]/g, "_")}_changes.json`;
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
@@ -194,31 +219,39 @@ export default function Home() {
const arrayBuffer = await pngBlob.arrayBuffer();
// Use initial version for PNG embedding, or current version if no initial version available
- const pngData = card.initialVersion
- ? {
- name: card.initialVersion.name,
- first_mes: card.initialVersion.first_mes,
- description: card.initialVersion.description,
- personality: card.initialVersion.personality,
- mes_example: card.initialVersion.mes_example,
- scenario: card.initialVersion.scenario,
- }
- : {
- name: card.name,
- first_mes: card.first_mes,
- description: card.description,
- personality: card.personality,
- mes_example: card.mes_example,
- scenario: card.scenario,
- };
+ const chosen = card.initialVersion || card.data;
+ const pngData = {
+ data: {
+ name: chosen.name,
+ first_mes: chosen.first_mes,
+ alternate_greetings: chosen.alternate_greetings || [],
+ description: chosen.description,
+ personality: chosen.personality,
+ mes_example: chosen.mes_example,
+ scenario: chosen.scenario,
+ creator: (chosen as any).creator || "",
+ creator_notes: (chosen as any).creator_notes || "",
+ system_prompt: (chosen as any).system_prompt || "",
+ post_history_instructions:
+ (chosen as any).post_history_instructions || "",
+ tags: (chosen as any).tags || [],
+ character_version: (chosen as any).character_version || "1",
+ extensions: (chosen as any).extensions || {},
+ },
+ spec: card.spec || "chara_card_v2",
+ spec_version: card.spec_version || "2.0",
+ };
const cardData = JSON.stringify(pngData);
const newImageData = Png.Generate(arrayBuffer, cardData);
const newFileName = `${
- card.name.replace(/[^a-zA-Z0-9\-_]/g, "_") || "character"
+ (card.initialVersion?.name || card.data.name).replace(
+ /[^a-zA-Z0-9\-_]/g,
+ "_"
+ ) || "character"
}.png`;
- const newFile = new File([newImageData], newFileName, {
+ const newFile = new File([new Uint8Array(newImageData)], newFileName, {
type: "image/png",
});
@@ -283,8 +316,7 @@ export default function Home() {
Sucker v2.0
- Now with multimessage support! Tracks changes to character
- descriptions and scenarios across multiple extractions.
+ A couple of updates, see below.
- New: Multimessage Support
+ V2 charcard format, multi-turn support for scripts/lorebooks,
+ alternate greetings.
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.
+
+ If you're interested in hosting your own sucker instance, lmk
+ via Discord: @lyseverian, I've privated the GH repo for now.
@@ -354,17 +397,25 @@ export default function Home() {
Save settings and refresh the page. Not this page. That {" "}
page.
+ Start a new chat with a character.
- Start a new chat with a character or multiple.
-
-
- You can either send a dot to let sucker make a best guess
- about the char name, or send the char name yourself and it'll
- be used instead.
+ Char name inference is implemented, but the prompt structure
+ changed. You'll need to send the character name yourself.
Hit the Refresh button here, and the cards should appear here.
+
+ 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.
+
+
+ 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.
+
Download the JSON files or go through a little more effort to
get PNGs instead.
@@ -375,13 +426,6 @@ export default function Home() {
discarded. Reloading the page will remove any attached avatars.
I'm not storing shit.
-
- New: If you send multiple messages with the
- same character name, 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.
-
@@ -401,7 +445,9 @@ export default function Home() {
- {card.name || "Unnamed Card"}
+ {card.initialVersion?.name ||
+ card.data?.name ||
+ "Unnamed Card"}
{card.hasVersions && (
@@ -419,7 +465,7 @@ export default function Home() {
{(card.initialVersion?.description ||
- card.description) && (
+ card.data?.description) && (
Description
@@ -427,7 +473,7 @@ export default function Home() {
{card.initialVersion?.description ||
- card.description}
+ card.data.description}
@@ -448,7 +494,7 @@ export default function Home() {
)}
{(card.initialVersion?.first_mes ||
- card.first_mes) && (
+ card.data?.first_mes) && (
@@ -458,7 +504,7 @@ export default function Home() {
{card.initialVersion?.first_mes ||
- card.first_mes}
+ card.data.first_mes}
@@ -478,7 +524,81 @@ export default function Home() {
)}
- {(card.initialVersion?.scenario || card.scenario) && (
+ {card.alternate_greetings &&
+ card.alternate_greetings.length > 0 && (
+
+
+
{`Alternate Greetings (${
+ card.alternate_greetings?.length || 0
+ })`}
+
+ {
+ const greetings =
+ card.alternate_greetings || [];
+ if (greetings.length === 0) return;
+ setAltGreetingIndexById((prev) => {
+ const current = prev[card.id] ?? 0;
+ const next =
+ (current - 1 + greetings.length) %
+ greetings.length;
+ return { ...prev, [card.id]: next };
+ });
+ }}
+ >
+
+
+ {
+ const greetings =
+ card.alternate_greetings || [];
+ if (greetings.length === 0) return;
+ setAltGreetingIndexById((prev) => {
+ const current = prev[card.id] ?? 0;
+ const next =
+ (current + 1) % greetings.length;
+ return { ...prev, [card.id]: next };
+ });
+ }}
+ >
+
+
+
+
+ {(() => {
+ const greetings =
+ card.alternate_greetings || [];
+ const index =
+ altGreetingIndexById[card.id] ?? 0;
+ const current = greetings.length
+ ? greetings[index % greetings.length]
+ : "";
+ return (
+
+
+ {current}
+
+
{
+ if (!current) return;
+ copyToClipboard(current);
+ }}
+ >
+
+
+
+ );
+ })()}
+
+ )}
+ {(card.initialVersion?.scenario ||
+ card.data?.scenario) && (
Scenario
@@ -486,7 +606,7 @@ export default function Home() {
{card.initialVersion?.scenario ||
- card.scenario}
+ card.data.scenario}
@@ -507,7 +627,7 @@ export default function Home() {
)}
{(card.initialVersion?.mes_example ||
- card.mes_example) && (
+ card.data?.mes_example) && (
@@ -517,7 +637,7 @@ export default function Home() {
{card.initialVersion?.mes_example ||
- card.mes_example}
+ card.data.mes_example}
@@ -538,7 +658,7 @@ export default function Home() {
)}
{(card.initialVersion?.personality ||
- card.personality) && (
+ card.data?.personality) && (
Personality
@@ -546,7 +666,7 @@ export default function Home() {
{card.initialVersion?.personality ||
- card.personality}
+ card.data.personality}