mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2025-10-28 12:45:55 +00:00
feat: Added multiturn messaging and tracking of scenario/description changes.
This commit is contained in:
323
src/app/page.tsx
323
src/app/page.tsx
@@ -35,6 +35,17 @@ interface Card {
|
||||
mes_example: string;
|
||||
scenario: string;
|
||||
avatarUrl?: string;
|
||||
hasVersions?: boolean;
|
||||
versionCount?: number;
|
||||
messageCount?: number;
|
||||
initialVersion?: {
|
||||
name: string;
|
||||
first_mes: string;
|
||||
description: string;
|
||||
personality: string;
|
||||
mes_example: string;
|
||||
scenario: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
@@ -48,6 +59,9 @@ export default function Home() {
|
||||
const [avatarPath, setAvatarPath] = useState("");
|
||||
const [isMetadataOpen, setIsMetadataOpen] = useState(false);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [changesDialogOpen, setChangesDialogOpen] = useState(false);
|
||||
const [selectedChanges, setSelectedChanges] = useState<any>(null);
|
||||
const [showFullText, setShowFullText] = useState(false);
|
||||
|
||||
const fetchCards = async () => {
|
||||
try {
|
||||
@@ -76,17 +90,74 @@ 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 element = document.createElement("a");
|
||||
const file = new Blob([JSON.stringify(card, null, 2)], {
|
||||
const file = new Blob([JSON.stringify(downloadData, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `${card.name.replace(/\s+/g, "_")}.json`;
|
||||
element.download = `${card.name.replace(/[^a-zA-Z0-9\-_]/g, '_')}.json`;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
const downloadChanges = async (card: Card) => {
|
||||
try {
|
||||
const response = await fetch(`/api/proxy?changes=true&cardId=${card.id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch changes');
|
||||
}
|
||||
|
||||
const changesData = await response.json();
|
||||
const element = document.createElement("a");
|
||||
const file = new Blob([JSON.stringify(changesData, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `${card.name.replace(/[^a-zA-Z0-9\-_]/g, '_')}_changes.json`;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
} catch (error) {
|
||||
console.error("Error downloading changes:", error);
|
||||
alert("Failed to download changes. The card may not have version history.");
|
||||
}
|
||||
};
|
||||
|
||||
const viewChanges = async (card: Card) => {
|
||||
try {
|
||||
const response = await fetch(`/api/proxy?changes=true&cardId=${card.id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch changes');
|
||||
}
|
||||
|
||||
const changesData = await response.json();
|
||||
setSelectedChanges(changesData);
|
||||
setShowFullText(false); // Reset to diff view by default
|
||||
setChangesDialogOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Error fetching changes:", error);
|
||||
alert("Failed to fetch changes. The card may not have version history.");
|
||||
}
|
||||
};
|
||||
|
||||
const downloadPng = async (card: Card, cardId: string) => {
|
||||
if (!card.avatarUrl) return;
|
||||
|
||||
@@ -115,18 +186,28 @@ export default function Home() {
|
||||
|
||||
const arrayBuffer = await pngBlob.arrayBuffer();
|
||||
|
||||
const cardData = JSON.stringify({
|
||||
// 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 cardData = JSON.stringify(pngData);
|
||||
|
||||
const newImageData = Png.Generate(arrayBuffer, cardData);
|
||||
const newFileName = `${
|
||||
card.name.replace(/\s+/g, "_") || "character"
|
||||
card.name.replace(/[^a-zA-Z0-9\-_]/g, '_') || "character"
|
||||
}.png`;
|
||||
const newFile = new File([newImageData], newFileName, {
|
||||
type: "image/png",
|
||||
@@ -191,9 +272,9 @@ export default function Home() {
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Sucker v1.9</h1>
|
||||
<h1 className="text-3xl font-bold">Sucker v2.0</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Handles the new prompt structure (again). See instructions below, you'll need it.
|
||||
Now with multimessage support! Tracks changes to character descriptions and scenarios across multiple extractions.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -210,10 +291,10 @@ 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">
|
||||
Heads-up.
|
||||
New: Multimessage Support
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Same instructions as 1.8 if you used it before, except this time you can send a dot to let sucker infer char name, or send anything else and that'll be used to name it.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,6 +358,12 @@ 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>
|
||||
@@ -295,23 +382,37 @@ export default function Home() {
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value={`card-${index}`}>
|
||||
<AccordionTrigger className="text-xl font-semibold">
|
||||
{card.name || "Unnamed Card"}
|
||||
<div className="flex items-center gap-2">
|
||||
{card.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">
|
||||
v{card.versionCount}
|
||||
</span>
|
||||
)}
|
||||
{card.messageCount && card.messageCount > 1 && (
|
||||
<span className="text-sm bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded-full">
|
||||
{card.messageCount} msgs
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div id={`card-${index}`} className="space-y-4 mt-4">
|
||||
{card.description && (
|
||||
{(card.initialVersion?.description || card.description) && (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="description">
|
||||
<AccordionTrigger>Description</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex justify-between">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.description}</pre>
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.initialVersion?.description || card.description}</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(card.description);
|
||||
copyToClipboard(card.initialVersion?.description || card.description);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
@@ -321,7 +422,7 @@ export default function Home() {
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
{card.first_mes && (
|
||||
{(card.initialVersion?.first_mes || card.first_mes) && (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="first-message">
|
||||
<AccordionTrigger>
|
||||
@@ -329,13 +430,13 @@ export default function Home() {
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex justify-between">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.first_mes}</pre>
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.initialVersion?.first_mes || card.first_mes}</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(card.first_mes);
|
||||
copyToClipboard(card.initialVersion?.first_mes || card.first_mes);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
@@ -345,19 +446,19 @@ export default function Home() {
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
{card.scenario && (
|
||||
{(card.initialVersion?.scenario || card.scenario) && (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="scenario">
|
||||
<AccordionTrigger>Scenario</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex justify-between">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.scenario}</pre>
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.initialVersion?.scenario || card.scenario}</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(card.scenario);
|
||||
copyToClipboard(card.initialVersion?.scenario || card.scenario);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
@@ -367,7 +468,7 @@ export default function Home() {
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
{card.mes_example && (
|
||||
{(card.initialVersion?.mes_example || card.mes_example) && (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="example-messages">
|
||||
<AccordionTrigger>
|
||||
@@ -375,13 +476,13 @@ export default function Home() {
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex justify-between">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.mes_example}</pre>
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.initialVersion?.mes_example || card.mes_example}</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(card.mes_example);
|
||||
copyToClipboard(card.initialVersion?.mes_example || card.mes_example);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
@@ -391,19 +492,19 @@ export default function Home() {
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
{card.personality && (
|
||||
{(card.initialVersion?.personality || card.personality) && (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="personality">
|
||||
<AccordionTrigger>Personality</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex justify-between">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.personality}</pre>
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm">{card.initialVersion?.personality || card.personality}</pre>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(card.personality);
|
||||
copyToClipboard(card.initialVersion?.personality || card.personality);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
@@ -424,6 +525,22 @@ export default function Home() {
|
||||
>
|
||||
Download JSON
|
||||
</Button>
|
||||
{card.hasVersions && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => viewChanges(card)}
|
||||
variant="outline"
|
||||
>
|
||||
View Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => downloadChanges(card)}
|
||||
variant="outline"
|
||||
>
|
||||
Download Changes
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{!card.avatarUrl ? (
|
||||
<Button
|
||||
onClick={() => handleOpenDialog(index)}
|
||||
@@ -495,6 +612,162 @@ export default function Home() {
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={changesDialogOpen} onOpenChange={setChangesDialogOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Change History: {selectedChanges?.cardName}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="flex items-center justify-between">
|
||||
<span>Version history showing changes to description and scenario fields</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowFullText(!showFullText)}
|
||||
>
|
||||
{showFullText ? 'Show Changes Only' : 'Show Full Text'}
|
||||
</Button>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{selectedChanges && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<strong>Total Versions:</strong> {selectedChanges.totalVersions}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Current Version:</strong> {selectedChanges.currentVersion}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Description Changes:</strong> {selectedChanges.summary.descriptionChanges}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Scenario Changes:</strong> {selectedChanges.summary.scenarioChanges}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">Version History</h3>
|
||||
{selectedChanges.versions.map((version: any, index: number) => (
|
||||
<div key={version.version} className="border rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="font-semibold">
|
||||
Version {version.version} ({version.changeType})
|
||||
</h4>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{new Date(version.timestamp).toLocaleString()}
|
||||
{version.messageCount && ` • Message ${version.messageCount}`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{version.changes.description && (
|
||||
<div className="mb-3">
|
||||
<h5 className="font-medium text-sm mb-1">Description Change:</h5>
|
||||
{version.changeType === 'initial' ? (
|
||||
<div className="bg-blue-50 dark:bg-blue-950 p-2 rounded text-sm">
|
||||
<strong>Initial Content:</strong> {version.changes.description.new}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{version.addedText?.description && (
|
||||
<div className="bg-green-50 dark:bg-green-950 p-2 rounded text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<strong>Added:</strong> {version.addedText.description}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(version.addedText.description);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{version.removedText?.description && (
|
||||
<div className="bg-red-50 dark:bg-red-950 p-2 rounded text-sm">
|
||||
<strong>Removed:</strong> {version.removedText.description}
|
||||
</div>
|
||||
)}
|
||||
{showFullText && (
|
||||
<div className="space-y-1 mt-2 pt-2 border-t">
|
||||
<div className="bg-gray-50 dark:bg-gray-950 p-2 rounded text-xs">
|
||||
<strong>Full Old:</strong> {version.changes.description.old}
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-950 p-2 rounded text-xs">
|
||||
<strong>Full New:</strong> {version.changes.description.new}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{version.changes.scenario && (
|
||||
<div>
|
||||
<h5 className="font-medium text-sm mb-1">Scenario Change:</h5>
|
||||
{version.changeType === 'initial' ? (
|
||||
<div className="bg-blue-50 dark:bg-blue-950 p-2 rounded text-sm">
|
||||
<strong>Initial Content:</strong> {version.changes.scenario.new}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{version.addedText?.scenario && (
|
||||
<div className="bg-green-50 dark:bg-green-950 p-2 rounded text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<strong>Added:</strong> {version.addedText.scenario}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-2 h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(version.addedText.scenario);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{version.removedText?.scenario && (
|
||||
<div className="bg-red-50 dark:bg-red-950 p-2 rounded text-sm">
|
||||
<strong>Removed:</strong> {version.removedText.scenario}
|
||||
</div>
|
||||
)}
|
||||
{showFullText && (
|
||||
<div className="space-y-1 mt-2 pt-2 border-t">
|
||||
<div className="bg-gray-50 dark:bg-gray-950 p-2 rounded text-xs">
|
||||
<strong>Full Old:</strong> {version.changes.scenario.old}
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-950 p-2 rounded text-xs">
|
||||
<strong>Full New:</strong> {version.changes.scenario.new}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user