mirror of
				https://github.com/severian-dev/sucker.severian.dev.git
				synced 2025-11-04 08:05:40 +00:00 
			
		
		
		
	2.0
This commit is contained in:
		@@ -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<string, unknown>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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=<conversationId>]`;
 | 
			
		||||
        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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										288
									
								
								src/app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										288
									
								
								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<string, unknown>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<any>(null);
 | 
			
		||||
  const [showFullText, setShowFullText] = useState(false);
 | 
			
		||||
  const [altGreetingIndexById, setAltGreetingIndexById] = useState<
 | 
			
		||||
    Record<string, number>
 | 
			
		||||
  >({});
 | 
			
		||||
 | 
			
		||||
  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() {
 | 
			
		||||
          <div>
 | 
			
		||||
            <h1 className="text-3xl font-bold">Sucker v2.0</h1>
 | 
			
		||||
            <p className="text-sm text-muted-foreground">
 | 
			
		||||
              Now with multimessage support! Tracks changes to character
 | 
			
		||||
              descriptions and scenarios across multiple extractions.
 | 
			
		||||
              A couple of updates, see below.
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Button
 | 
			
		||||
@@ -301,13 +333,24 @@ export default function Home() {
 | 
			
		||||
          <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">
 | 
			
		||||
                New: Multimessage Support
 | 
			
		||||
                V2 charcard format, multi-turn support for scripts/lorebooks,
 | 
			
		||||
                alternate greetings.
 | 
			
		||||
              </span>
 | 
			
		||||
              <p className="text-sm text-muted-foreground">
 | 
			
		||||
                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.
 | 
			
		||||
                <br />
 | 
			
		||||
                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.
 | 
			
		||||
                <br />
 | 
			
		||||
                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 privated the GH repo for now.
 | 
			
		||||
              </p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -354,17 +397,25 @@ export default function Home() {
 | 
			
		||||
                  Save settings and refresh the page. Not this page. <i>That</i>{" "}
 | 
			
		||||
                  page.
 | 
			
		||||
                </li>
 | 
			
		||||
                <li className="mb-2">Start a new chat with a character.</li>
 | 
			
		||||
                <li className="mb-2">
 | 
			
		||||
                  Start a new chat with a character or multiple.
 | 
			
		||||
                </li>
 | 
			
		||||
                <li className="mb-2">
 | 
			
		||||
                  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.
 | 
			
		||||
                </li>
 | 
			
		||||
                <li className="mb-2">
 | 
			
		||||
                  Hit the Refresh button here, and the cards should appear here.
 | 
			
		||||
                </li>
 | 
			
		||||
                <li className="mb-2">
 | 
			
		||||
                  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{" "}
 | 
			
		||||
                  <code>[sucker:conv=conversationId]</code> which you'll be
 | 
			
		||||
                  given when creating a new card.
 | 
			
		||||
                </li>
 | 
			
		||||
                <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
 | 
			
		||||
                  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 className="mb-2">
 | 
			
		||||
                  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.
 | 
			
		||||
              </p>
 | 
			
		||||
              <p className="mb-2">
 | 
			
		||||
                <strong>New:</strong> 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.
 | 
			
		||||
              </p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </CollapsibleContent>
 | 
			
		||||
        </Collapsible>
 | 
			
		||||
@@ -401,7 +445,9 @@ export default function Home() {
 | 
			
		||||
                    <AccordionItem value={`card-${index}`}>
 | 
			
		||||
                      <AccordionTrigger className="text-xl font-semibold">
 | 
			
		||||
                        <div className="flex items-center gap-2">
 | 
			
		||||
                          {card.name || "Unnamed Card"}
 | 
			
		||||
                          {card.initialVersion?.name ||
 | 
			
		||||
                            card.data?.name ||
 | 
			
		||||
                            "Unnamed Card"}
 | 
			
		||||
                          <div className="flex gap-1">
 | 
			
		||||
                            {card.hasVersions && (
 | 
			
		||||
                              <span className="text-sm bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded-full">
 | 
			
		||||
@@ -419,7 +465,7 @@ export default function Home() {
 | 
			
		||||
                      <AccordionContent>
 | 
			
		||||
                        <div id={`card-${index}`} className="space-y-4 mt-4">
 | 
			
		||||
                          {(card.initialVersion?.description ||
 | 
			
		||||
                            card.description) && (
 | 
			
		||||
                            card.data?.description) && (
 | 
			
		||||
                            <Accordion type="single" collapsible>
 | 
			
		||||
                              <AccordionItem value="description">
 | 
			
		||||
                                <AccordionTrigger>Description</AccordionTrigger>
 | 
			
		||||
@@ -427,7 +473,7 @@ export default function Home() {
 | 
			
		||||
                                  <div className="flex justify-between">
 | 
			
		||||
                                    <pre className="whitespace-pre-wrap font-sans text-sm">
 | 
			
		||||
                                      {card.initialVersion?.description ||
 | 
			
		||||
                                        card.description}
 | 
			
		||||
                                        card.data.description}
 | 
			
		||||
                                    </pre>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                      variant="ghost"
 | 
			
		||||
@@ -436,7 +482,7 @@ export default function Home() {
 | 
			
		||||
                                        e.stopPropagation();
 | 
			
		||||
                                        copyToClipboard(
 | 
			
		||||
                                          card.initialVersion?.description ||
 | 
			
		||||
                                            card.description
 | 
			
		||||
                                            card.data.description
 | 
			
		||||
                                        );
 | 
			
		||||
                                      }}
 | 
			
		||||
                                    >
 | 
			
		||||
@@ -448,7 +494,7 @@ export default function Home() {
 | 
			
		||||
                            </Accordion>
 | 
			
		||||
                          )}
 | 
			
		||||
                          {(card.initialVersion?.first_mes ||
 | 
			
		||||
                            card.first_mes) && (
 | 
			
		||||
                            card.data?.first_mes) && (
 | 
			
		||||
                            <Accordion type="single" collapsible>
 | 
			
		||||
                              <AccordionItem value="first-message">
 | 
			
		||||
                                <AccordionTrigger>
 | 
			
		||||
@@ -458,7 +504,7 @@ export default function Home() {
 | 
			
		||||
                                  <div className="flex justify-between">
 | 
			
		||||
                                    <pre className="whitespace-pre-wrap font-sans text-sm">
 | 
			
		||||
                                      {card.initialVersion?.first_mes ||
 | 
			
		||||
                                        card.first_mes}
 | 
			
		||||
                                        card.data.first_mes}
 | 
			
		||||
                                    </pre>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                      variant="ghost"
 | 
			
		||||
@@ -467,7 +513,7 @@ export default function Home() {
 | 
			
		||||
                                        e.stopPropagation();
 | 
			
		||||
                                        copyToClipboard(
 | 
			
		||||
                                          card.initialVersion?.first_mes ||
 | 
			
		||||
                                            card.first_mes
 | 
			
		||||
                                            card.data.first_mes
 | 
			
		||||
                                        );
 | 
			
		||||
                                      }}
 | 
			
		||||
                                    >
 | 
			
		||||
@@ -478,7 +524,81 @@ export default function Home() {
 | 
			
		||||
                              </AccordionItem>
 | 
			
		||||
                            </Accordion>
 | 
			
		||||
                          )}
 | 
			
		||||
                          {(card.initialVersion?.scenario || card.scenario) && (
 | 
			
		||||
                          {card.alternate_greetings &&
 | 
			
		||||
                            card.alternate_greetings.length > 0 && (
 | 
			
		||||
                              <div className="mt-4">
 | 
			
		||||
                                <div className="flex items-center justify-between mb-2">
 | 
			
		||||
                                  <h4 className="font-medium">{`Alternate Greetings (${
 | 
			
		||||
                                    card.alternate_greetings?.length || 0
 | 
			
		||||
                                  })`}</h4>
 | 
			
		||||
                                  <div className="flex items-center gap-2">
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                      variant="ghost"
 | 
			
		||||
                                      size="icon"
 | 
			
		||||
                                      onClick={() => {
 | 
			
		||||
                                        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 };
 | 
			
		||||
                                        });
 | 
			
		||||
                                      }}
 | 
			
		||||
                                    >
 | 
			
		||||
                                      <ChevronLeft className="h-4 w-4" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                      variant="ghost"
 | 
			
		||||
                                      size="icon"
 | 
			
		||||
                                      onClick={() => {
 | 
			
		||||
                                        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 };
 | 
			
		||||
                                        });
 | 
			
		||||
                                      }}
 | 
			
		||||
                                    >
 | 
			
		||||
                                      <ChevronRight className="h-4 w-4" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                  </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {(() => {
 | 
			
		||||
                                  const greetings =
 | 
			
		||||
                                    card.alternate_greetings || [];
 | 
			
		||||
                                  const index =
 | 
			
		||||
                                    altGreetingIndexById[card.id] ?? 0;
 | 
			
		||||
                                  const current = greetings.length
 | 
			
		||||
                                    ? greetings[index % greetings.length]
 | 
			
		||||
                                    : "";
 | 
			
		||||
                                  return (
 | 
			
		||||
                                    <div className="flex justify-between">
 | 
			
		||||
                                      <pre className="whitespace-pre-wrap font-sans text-sm">
 | 
			
		||||
                                        {current}
 | 
			
		||||
                                      </pre>
 | 
			
		||||
                                      <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        size="icon"
 | 
			
		||||
                                        onClick={() => {
 | 
			
		||||
                                          if (!current) return;
 | 
			
		||||
                                          copyToClipboard(current);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                      >
 | 
			
		||||
                                        <Copy className="h-4 w-4" />
 | 
			
		||||
                                      </Button>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                  );
 | 
			
		||||
                                })()}
 | 
			
		||||
                              </div>
 | 
			
		||||
                            )}
 | 
			
		||||
                          {(card.initialVersion?.scenario ||
 | 
			
		||||
                            card.data?.scenario) && (
 | 
			
		||||
                            <Accordion type="single" collapsible>
 | 
			
		||||
                              <AccordionItem value="scenario">
 | 
			
		||||
                                <AccordionTrigger>Scenario</AccordionTrigger>
 | 
			
		||||
@@ -486,7 +606,7 @@ export default function Home() {
 | 
			
		||||
                                  <div className="flex justify-between">
 | 
			
		||||
                                    <pre className="whitespace-pre-wrap font-sans text-sm">
 | 
			
		||||
                                      {card.initialVersion?.scenario ||
 | 
			
		||||
                                        card.scenario}
 | 
			
		||||
                                        card.data.scenario}
 | 
			
		||||
                                    </pre>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                      variant="ghost"
 | 
			
		||||
@@ -495,7 +615,7 @@ export default function Home() {
 | 
			
		||||
                                        e.stopPropagation();
 | 
			
		||||
                                        copyToClipboard(
 | 
			
		||||
                                          card.initialVersion?.scenario ||
 | 
			
		||||
                                            card.scenario
 | 
			
		||||
                                            card.data.scenario
 | 
			
		||||
                                        );
 | 
			
		||||
                                      }}
 | 
			
		||||
                                    >
 | 
			
		||||
@@ -507,7 +627,7 @@ export default function Home() {
 | 
			
		||||
                            </Accordion>
 | 
			
		||||
                          )}
 | 
			
		||||
                          {(card.initialVersion?.mes_example ||
 | 
			
		||||
                            card.mes_example) && (
 | 
			
		||||
                            card.data?.mes_example) && (
 | 
			
		||||
                            <Accordion type="single" collapsible>
 | 
			
		||||
                              <AccordionItem value="example-messages">
 | 
			
		||||
                                <AccordionTrigger>
 | 
			
		||||
@@ -517,7 +637,7 @@ export default function Home() {
 | 
			
		||||
                                  <div className="flex justify-between">
 | 
			
		||||
                                    <pre className="whitespace-pre-wrap font-sans text-sm">
 | 
			
		||||
                                      {card.initialVersion?.mes_example ||
 | 
			
		||||
                                        card.mes_example}
 | 
			
		||||
                                        card.data.mes_example}
 | 
			
		||||
                                    </pre>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                      variant="ghost"
 | 
			
		||||
@@ -526,7 +646,7 @@ export default function Home() {
 | 
			
		||||
                                        e.stopPropagation();
 | 
			
		||||
                                        copyToClipboard(
 | 
			
		||||
                                          card.initialVersion?.mes_example ||
 | 
			
		||||
                                            card.mes_example
 | 
			
		||||
                                            card.data.mes_example
 | 
			
		||||
                                        );
 | 
			
		||||
                                      }}
 | 
			
		||||
                                    >
 | 
			
		||||
@@ -538,7 +658,7 @@ export default function Home() {
 | 
			
		||||
                            </Accordion>
 | 
			
		||||
                          )}
 | 
			
		||||
                          {(card.initialVersion?.personality ||
 | 
			
		||||
                            card.personality) && (
 | 
			
		||||
                            card.data?.personality) && (
 | 
			
		||||
                            <Accordion type="single" collapsible>
 | 
			
		||||
                              <AccordionItem value="personality">
 | 
			
		||||
                                <AccordionTrigger>Personality</AccordionTrigger>
 | 
			
		||||
@@ -546,7 +666,7 @@ export default function Home() {
 | 
			
		||||
                                  <div className="flex justify-between">
 | 
			
		||||
                                    <pre className="whitespace-pre-wrap font-sans text-sm">
 | 
			
		||||
                                      {card.initialVersion?.personality ||
 | 
			
		||||
                                        card.personality}
 | 
			
		||||
                                        card.data.personality}
 | 
			
		||||
                                    </pre>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                      variant="ghost"
 | 
			
		||||
@@ -555,7 +675,7 @@ export default function Home() {
 | 
			
		||||
                                        e.stopPropagation();
 | 
			
		||||
                                        copyToClipboard(
 | 
			
		||||
                                          card.initialVersion?.personality ||
 | 
			
		||||
                                            card.personality
 | 
			
		||||
                                            card.data.personality
 | 
			
		||||
                                        );
 | 
			
		||||
                                      }}
 | 
			
		||||
                                    >
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user