From 3bc20ea40d2ac45cfa2fe6267355a42a2f0d15b4 Mon Sep 17 00:00:00 2001 From: SoniaNvm Date: Mon, 2 Feb 2026 02:27:09 -0800 Subject: [PATCH] attempt at metadata grabbing --- .env.example => .env | 2 +- .gitignore | 7 +- src/app/page.tsx | 1739 +++++++++++++++++++++--------------------- 3 files changed, 876 insertions(+), 872 deletions(-) rename .env.example => .env (76%) diff --git a/.env.example b/.env similarity index 76% rename from .env.example rename to .env index f5ef863..f1b54f8 100644 --- a/.env.example +++ b/.env @@ -2,4 +2,4 @@ # Disable Discord community banner (optional) # Uncomment the line below to hide the Discord banner -# NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true +NEXT_PUBLIC_DISABLE_DISCORD_BANNER=true diff --git a/.gitignore b/.gitignore index be1ad58..af48c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,9 @@ +.idea/ +bun.lock +.env + node_modules .next .env.local -.env*.local \ No newline at end of file +.env*.local +.env diff --git a/src/app/page.tsx b/src/app/page.tsx index bdba3ee..66aa2a7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -69,7 +69,7 @@ export default function Home() { const [cards, setCards] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [selectedCardIndex, setSelectedCardIndex] = useState( - null + null ); const [characterUrl, setCharacterUrl] = useState(""); const [avatarPath, setAvatarPath] = useState(""); @@ -79,16 +79,19 @@ export default function Home() { const [selectedChanges, setSelectedChanges] = useState(null); const [showFullText, setShowFullText] = useState(false); const [altGreetingIndexById, setAltGreetingIndexById] = useState< - Record + Record >({}); const [mirrorsDialogOpen, setMirrorsDialogOpen] = useState(false); const [changelogDialogOpen, setChangelogDialogOpen] = useState(false); const [howToUseDialogOpen, setHowToUseDialogOpen] = useState(false); const [proxyUrl, setProxyUrl] = useState( - "https://sucker.severian.dev/api/proxy" + "https://sucker.severian.dev/api/proxy" ); + const [pageSource, setPageSource] = useState(""); + const [metadataJson, setMetadataJson] = useState(null); + const fetchCards = async () => { try { setIsRefreshing(true); @@ -99,8 +102,8 @@ export default function Home() { return data.cards.map((newCard: Card) => ({ ...newCard, avatarUrl: - prevCards.find((c) => c.id === newCard.id)?.avatarUrl || - newCard.avatarUrl, + prevCards.find((c) => c.id === newCard.id)?.avatarUrl || + newCard.avatarUrl, })); }); } @@ -138,7 +141,7 @@ export default function Home() { creator_notes: (chosen as any).creator_notes || "", system_prompt: (chosen as any).system_prompt || "", post_history_instructions: - (chosen as any).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 || {}, @@ -153,8 +156,8 @@ export default function Home() { }); element.href = URL.createObjectURL(file); element.download = `${(card.initialVersion?.name || card.data.name).replace( - /[^a-zA-Z0-9\-_]/g, - "_" + /[^a-zA-Z0-9\-_]/g, + "_" )}.json`; document.body.appendChild(element); element.click(); @@ -175,7 +178,7 @@ export default function Home() { }); element.href = URL.createObjectURL(file); element.download = `${( - card.initialVersion?.name || card.data.name + card.initialVersion?.name || card.data.name ).replace(/[^a-zA-Z0-9\-_]/g, "_")}_changes.json`; document.body.appendChild(element); element.click(); @@ -183,7 +186,7 @@ export default function Home() { } catch (error) { console.error("Error downloading changes:", error); alert( - "Failed to download changes. The card may not have version history." + "Failed to download changes. The card may not have version history." ); } }; @@ -233,6 +236,15 @@ export default function Home() { const arrayBuffer = await pngBlob.arrayBuffer(); + // Fallback if metadataJson hasn't been fetched for this card + const safeMetadata = metadataJson || {}; + const creator = (safeMetadata.creator_name || (card.data as any).creator || "Unknown") + (safeMetadata.creator_verified ? " ✅" : ""); + + // Tag Parsing + const normalTags = (safeMetadata.tags || (card.data as any).tags || []).map((t: any) => t.name); + const customTags = safeMetadata.custom_tags || []; + const allTagsArray = [...normalTags, ...customTags]; + // Use initial version for PNG embedding, or current version if no initial version available const chosen = card.initialVersion || card.data; const pngData = { @@ -244,13 +256,13 @@ export default function Home() { personality: chosen.personality, mes_example: chosen.mes_example, scenario: chosen.scenario, - creator: (chosen as any).creator || "", - creator_notes: (chosen as any).creator_notes || "", + creator: creator, + creator_notes: safeMetadata.description || (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", + (chosen as any).post_history_instructions || "", + tags: allTagsArray, + character_version: safeMetadata.name || (chosen as any).character_version || "1", extensions: (chosen as any).extensions || {}, }, spec: card.spec || "chara_card_v2", @@ -261,10 +273,10 @@ export default function Home() { const newImageData = Png.Generate(arrayBuffer, cardData); const newFileName = `${(card.initialVersion?.name || card.data.name).replace( - /[^a-zA-Z0-9\-_]/g, - "_" + /[^a-zA-Z0-9\-_]/g, + "_" ) || "character" - }.png`; + }.png`; const newFile = new File([new Uint8Array(newImageData)], newFileName, { type: "image/png", }); @@ -295,39 +307,9 @@ export default function Home() { const isCharacterUrl = /janitorai\.com\/characters\//.test(characterUrl); if (isCharacterUrl) { - // Extract character ID and open metadata page, then show second input - const match = characterUrl.match(/characters\/([\w-]+)/); - if (match && match[1]) { - const characterId = match[1].split("_")[0]; - window.open( - `https://janitorai.com/hampter/characters/${characterId}`, - "_blank" - ); - setIsMetadataOpen(true); - } - return; - } - - // Check if the input is a direct image link (webp filename or full image URL) - const isImagePath = /\.(webp|png|jpg|jpeg|gif)(\?.*)?$/i.test(characterUrl); - const isFullImageUrl = (characterUrl.startsWith("http://") || characterUrl.startsWith("https://")) && isImagePath; - const isWebpFilename = /^[\w-]+\.(webp|png|jpg|jpeg|gif)$/i.test(characterUrl); - - if (isFullImageUrl || isWebpFilename) { - // Directly set the avatar URL without opening metadata - if (selectedCardIndex === null) return; - - const avatarUrl = isFullImageUrl - ? characterUrl - : `https://ella.janitorai.com/bot-avatars/${characterUrl}`; - - const updatedCards = [...cards]; - updatedCards[selectedCardIndex] = { - ...updatedCards[selectedCardIndex], - avatarUrl, - }; - setCards(updatedCards); - setDialogOpen(false); + // Open html source, then show second input + window.open(`view-source:${characterUrl}`, "_blank"); + setIsMetadataOpen(true); return; } }; @@ -336,7 +318,42 @@ export default function Home() { if (selectedCardIndex === null) return; try { - const avatarUrl = `https://ella.janitorai.com/bot-avatars/${avatarPath}`; + const storeKey = "Sk--a:a-a--characterStore"; + + // Define the anchor points we are looking for + const prefix = 'window.mbxM.push(JSON.parse("'; + const suffix = '"));'; + + // Locate the script content + const startIndex = pageSource.indexOf(prefix); + if (startIndex === -1) { + throw new Error("Could not find character data in the page source."); + } + + // Move index to the start of the actual JSON string + const jsonStartIndex = startIndex + prefix.length; + + // Find the end of the statement + const jsonEndIndex = pageSource.indexOf(suffix, jsonStartIndex); + if (jsonEndIndex === -1) { + throw new Error("Could not find closing tag for JSON content."); + } + + // Extract the escaped string + const escapedJsonString = pageSource.substring(jsonStartIndex, jsonEndIndex); + + // Wrap it in quotes so it gets treated as a string containing JSON + const rawJsonString = JSON.parse(`"${escapedJsonString}"`); + console.log(rawJsonString); + + // Parse the actual JSON data into an object + const data = JSON.parse(rawJsonString); + + // Return the specific character data + const char = data[storeKey].character; + setMetadataJson(char); + + const avatarUrl = `https://ella.janitorai.com/bot-avatars/${char.avatar}`; const updatedCards = [...cards]; updatedCards[selectedCardIndex] = { ...updatedCards[selectedCardIndex], @@ -354,869 +371,851 @@ export default function Home() { }; return ( -
- -
- {/* Mobile Layout */} -
- {/* Row 1: Title on left, Hamburger on right */} -
-
-

Sucker v2.2

-

- Consider joining Avalon! -

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

Sucker v2.2

+

+ Consider joining Avalon! +

+
+ + + + + + setHowToUseDialogOpen(true)}> + How to Use + + setMirrorsDialogOpen(true)}> + Mirrors + + setChangelogDialogOpen(true)}> + Changelog + + +
- - - - - - setHowToUseDialogOpen(true)}> - How to Use - - setMirrorsDialogOpen(true)}> - Mirrors - - setChangelogDialogOpen(true)}> - Changelog - - - -
- {/* Row 2: Full-width Refresh button */} - -
- - {/* Desktop Layout */} -
-
-

Sucker v2.2

-

- Consider joining Avalon! -

-
-
+ {/* Row 2: Full-width Refresh button */} - - -
-
- + {/* Desktop Layout */} +
+
+

Sucker v2.2

+

+ Consider joining Avalon! +

+
+
+ + + + +
+
- {/* Discord Banner - Disabled if NEXT_PUBLIC_DISABLE_DISCORD_BANNER is set */} - {process.env.NEXT_PUBLIC_DISABLE_DISCORD_BANNER !== "true" && ( - - )} + -
- {cards.length === 0 ? ( - - -

No extractions yet.

-
-
- ) : ( - cards.map((card, index) => ( - - - - - -
- {card.initialVersion?.name || - card.data?.name || - "Unnamed Card"} -
- {card.hasVersions && ( - + {/* Discord Banner - Disabled if NEXT_PUBLIC_DISABLE_DISCORD_BANNER is set */} + {process.env.NEXT_PUBLIC_DISABLE_DISCORD_BANNER !== "true" && ( + + )} + +
+ {cards.length === 0 ? ( + + +

No extractions yet.

+
+
+ ) : ( + cards.map((card, index) => ( + + + + + +
+ {card.initialVersion?.name || + card.data?.name || + "Unnamed Card"} +
+ {card.hasVersions && ( + v{card.versionCount} - )} - {card.messageCount && card.messageCount > 1 && ( - + )} + {card.messageCount && card.messageCount > 1 && ( + {card.messageCount} msgs - )} -
-
-
- -
- {(card.initialVersion?.description || - card.data?.description) && ( - - - Description - -
+ )} +
+
+ + +
+ {(card.initialVersion?.description || + card.data?.description) && ( + + + Description + +
                                         {card.initialVersion?.description ||
-                                          card.data.description}
+                                            card.data.description}
                                       
- -
-
-
-
- )} - {(card.initialVersion?.first_mes || - card.data?.first_mes) && ( - - - - First Message - - -
+ +
+
+
+
+ )} + {(card.initialVersion?.first_mes || + card.data?.first_mes) && ( + + + + First Message + + +
                                         {card.initialVersion?.first_mes ||
-                                          card.data.first_mes}
+                                            card.data.first_mes}
                                       
- -
-
-
-
- )} - {card.alternate_greetings && - card.alternate_greetings.length > 0 && ( -
-
-

{`Alternate Greetings (${card.alternate_greetings?.length || 0 - })`}

-
- - -
-
- {(() => { - const greetings = - card.alternate_greetings || []; - const index = - altGreetingIndexById[card.id] ?? 0; - const current = greetings.length - ? greetings[index % greetings.length] - : ""; - return ( -
+ +
+ + + + )} + {card.alternate_greetings && + card.alternate_greetings.length > 0 && ( +
+
+

{`Alternate Greetings (${card.alternate_greetings?.length || 0 + })`}

+
+ + +
+
+ {(() => { + const greetings = + card.alternate_greetings || []; + const index = + altGreetingIndexById[card.id] ?? 0; + const current = greetings.length + ? greetings[index % greetings.length] + : ""; + return ( +
                                         {current}
                                       
- -
- ); - })()} -
- )} - {(card.initialVersion?.scenario || - card.data?.scenario) && ( - - - Scenario - -
+ +
+ ); + })()} +
+ )} + {(card.initialVersion?.scenario || + card.data?.scenario) && ( + + + Scenario + +
                                         {card.initialVersion?.scenario ||
-                                          card.data.scenario}
+                                            card.data.scenario}
                                       
- -
-
-
-
- )} - {(card.initialVersion?.mes_example || - card.data?.mes_example) && ( - - - - Example Messages - - -
+ +
+
+
+
+ )} + {(card.initialVersion?.mes_example || + card.data?.mes_example) && ( + + + + Example Messages + + +
                                         {card.initialVersion?.mes_example ||
-                                          card.data.mes_example}
+                                            card.data.mes_example}
                                       
- -
-
-
-
- )} - {(card.initialVersion?.personality || - card.data?.personality) && ( - - - Personality - -
+ +
+
+
+
+ )} + {(card.initialVersion?.personality || + card.data?.personality) && ( + + + Personality + +
                                         {card.initialVersion?.personality ||
-                                          card.data.personality}
+                                            card.data.personality}
                                       
- -
-
-
-
- )} + +
+
+
+
+ )} +
+ + + +
+ + {card.hasVersions && ( + <> + + + + )} + {!card.avatarUrl ? ( + + ) : ( + + )}
- - - -
- - {card.hasVersions && ( - <> - - - - )} - {!card.avatarUrl ? ( - - ) : ( - - )} -
- - - )) - )} + + + )) + )} +
-
- {/* How to Use Dialog */} - - - - How to Use - - Follow every instruction here to the letter because it's all you - need to know and I have no intent of helping you further. - - -
-
    -
  1. - Put {proxyUrl} in - your API settings, any value for model and key. -
  2. -
  3. - REQUIRED: Set your custom prompt to{" "} - <.> -
  4. -
  5. - REQUIRED: Set your persona (or create a new one) with the name{" "} - {user} and - the description should only have{" "} - . in it. -
  6. -
  7. - Save settings and refresh the page. Not this page. That{" "} - page. -
  8. -
  9. Start a new chat with a character.
  10. -
  11. - Char name inference is implemented: if you send just a dot:{" "} - ., sucker will use - the inferred name from the persona tag, or you can send the - character name yourself. -
  12. -
  13. - Hit the Refresh button here, and the cards should appear here. -
  14. -
  15. - If you're interested in capturing alternate greetings, start a - new chat and send the conversation ID as first message instead - of the character name. The format is{" "} - - [sucker:conv=conversationId] - {" "} - which you'll be given when creating a new card. -
  16. -
  17. - You can also send more messages with possible keywords to - trigger scripts/lorebooks. Sucker will track changes to the - description and scenario fields. Cards with multiple versions - will show a version badge and offer a "Download Changes" - button to get a detailed change history with timestamps. - Unfortunately, lorebook creation is out of scope at the - moment, but you can use the changes detected to modify the - character card yourself post-export. -
  18. -
  19. - Download the JSON files or go through a little more effort to - get PNGs instead. -
  20. -
-

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

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

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

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

Jan 2026 - Direct image input

-
-

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

-

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

-

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

-
-
- -
-

Dec 2025 - A note about fetching avatars

-
-

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

-

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

-

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

-
-
- -
-

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

-
-

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

-

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

-

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

-
-
-
-
-
- - - - - - {isMetadataOpen ? "Enter Avatar Path" : "Fetch Avatar"} - - - {isMetadataOpen - ? "Look for the avatar field in the opened tab and paste the value here." - : "Enter a character URL (janitorai.com/characters/...) to open metadata, or paste an image filename (id.webp) or full image URL directly."} - - - - {isMetadataOpen ? ( -
- ) => - setAvatarPath(e.target.value) - } - /> - -
- ) : ( -
- ) => - setCharacterUrl(e.target.value) - } - /> -

- For character URLs, a new tab will open with metadata. For image - filenames or full image URLs, the avatar will be set directly. + {/* How to Use Dialog */} +

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

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

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

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

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

Jan 2026 - Direct image input

+
+

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

+

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

+

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

+
+
+ +
+

Dec 2025 - A note about fetching avatars

+
+

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

+

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

+

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

+
+
+ +
+

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

+
+

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

+

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

+

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

+
+
+
+
+
+ + + + + + {isMetadataOpen ? "Enter Page Source Code" : "Fetch Metadata"} + + + {isMetadataOpen ? "Paste the entire source code here." : "Enter a character URL (janitorai.com/characters/...) to open page source."} + + + + {isMetadataOpen ? ( +
+ ) => setPageSource(e.target.value)} /> + +
+ ) : ( +
+ ) => setCharacterUrl(e.target.value)} /> + +
+ )} +
+
+ + + + + + Change History: {selectedChanges?.cardName} + + Version history showing changes to description and scenario fields - - - + +
+
- {selectedChanges && ( -
-
-
- Total Versions:{" "} - {selectedChanges.totalVersions} -
-
- Current Version:{" "} - {selectedChanges.currentVersion} -
-
- Description Changes:{" "} - {selectedChanges.summary.descriptionChanges} -
-
- Scenario Changes:{" "} - {selectedChanges.summary.scenarioChanges} -
-
- - - -
-

Version History

- {selectedChanges.versions.map((version: any, index: number) => ( -
-
-

- Version {version.version} ({version.changeType}) -

-
- {new Date(version.timestamp).toLocaleString()} - {version.messageCount && - ` • Message ${version.messageCount}`} -
+ {selectedChanges && ( +
+
+
+ Total Versions:{" "} + {selectedChanges.totalVersions} +
+
+ Current Version:{" "} + {selectedChanges.currentVersion} +
+
+ Description Changes:{" "} + {selectedChanges.summary.descriptionChanges} +
+
+ Scenario Changes:{" "} + {selectedChanges.summary.scenarioChanges}
- - {version.changes.description && ( -
-
- Description Change: -
- {version.changeType === "initial" ? ( -
- Initial Content:{" "} - {version.changes.description.new} -
- ) : ( -
- {version.addedText?.description && ( -
-
-
- Added:{" "} - {version.addedText.description} -
- -
-
- )} - {version.removedText?.description && ( -
- Removed:{" "} - {version.removedText.description} -
- )} - {showFullText && ( -
-
- Full Old:{" "} - {version.changes.description.old} -
-
- Full New:{" "} - {version.changes.description.new} -
-
- )} -
- )} -
- )} - - {version.changes.scenario && ( -
-
- Scenario Change: -
- {version.changeType === "initial" ? ( -
- Initial Content:{" "} - {version.changes.scenario.new} -
- ) : ( -
- {version.addedText?.scenario && ( -
-
-
- Added:{" "} - {version.addedText.scenario} -
- -
-
- )} - {version.removedText?.scenario && ( -
- Removed:{" "} - {version.removedText.scenario} -
- )} - {showFullText && ( -
-
- Full Old:{" "} - {version.changes.scenario.old} -
-
- Full New:{" "} - {version.changes.scenario.new} -
-
- )} -
- )} -
- )}
- ))} -
-
- )} - -
-
+ + + +
+

Version History

+ {selectedChanges.versions.map((version: any, index: number) => ( +
+
+

+ Version {version.version} ({version.changeType}) +

+
+ {new Date(version.timestamp).toLocaleString()} + {version.messageCount && + ` • Message ${version.messageCount}`} +
+
+ + {version.changes.description && ( +
+
+ Description Change: +
+ {version.changeType === "initial" ? ( +
+ Initial Content:{" "} + {version.changes.description.new} +
+ ) : ( +
+ {version.addedText?.description && ( +
+
+
+ Added:{" "} + {version.addedText.description} +
+ +
+
+ )} + {version.removedText?.description && ( +
+ Removed:{" "} + {version.removedText.description} +
+ )} + {showFullText && ( +
+
+ Full Old:{" "} + {version.changes.description.old} +
+
+ Full New:{" "} + {version.changes.description.new} +
+
+ )} +
+ )} +
+ )} + + {version.changes.scenario && ( +
+
+ Scenario Change: +
+ {version.changeType === "initial" ? ( +
+ Initial Content:{" "} + {version.changes.scenario.new} +
+ ) : ( +
+ {version.addedText?.scenario && ( +
+
+
+ Added:{" "} + {version.addedText.scenario} +
+ +
+
+ )} + {version.removedText?.scenario && ( +
+ Removed:{" "} + {version.removedText.scenario} +
+ )} + {showFullText && ( +
+
+ Full Old:{" "} + {version.changes.scenario.old} +
+
+ Full New:{" "} + {version.changes.scenario.new} +
+
+ )} +
+ )} +
+ )} +
+ ))} +
+ + )} + + + ); -} +} \ No newline at end of file