Files
lorebook.tui-bird.com/src/app/page.tsx
2025-10-03 14:08:46 +13:00

1053 lines
39 KiB
TypeScript

"use client";
import { useState, useRef } from "react";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ChevronUp, ChevronDown, Upload, Download, Edit, Save, X, Copy, Trash2, RotateCcw, Clipboard } from "lucide-react";
import {
CollapsibleContent,
Collapsible,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
interface LorebookEntry {
uid: number;
key: string[];
keysecondary: string[];
comment: string;
content: string;
constant: boolean;
vectorized: boolean;
selective: boolean;
selectiveLogic: number;
addMemo: boolean;
order: number;
position: number;
disable: boolean;
excludeRecursion: boolean;
preventRecursion: boolean;
matchPersonaDescription: boolean;
matchCharacterDescription: boolean;
matchCharacterPersonality: boolean;
matchCharacterDepthPrompt: boolean;
matchScenario: boolean;
matchCreatorNotes: boolean;
delayUntilRecursion: boolean;
probability: number;
useProbability: boolean;
depth: number;
group: string;
groupOverride: boolean;
groupWeight: number;
scanDepth: number | null;
caseSensitive: boolean;
matchWholeWords: boolean;
useGroupScoring: boolean;
automationId: string;
role: null;
sticky: number;
cooldown: number;
delay: number;
displayIndex: number;
// Additional fields for comprehensive editing
filterToCharacters: string;
filterToGenerationTriggers: string;
excludeFromGeneration: boolean;
}
interface ConvertedLorebook {
entries: { [key: string]: LorebookEntry };
}
export default function Home() {
const [isInstructionsOpen, setIsInstructionsOpen] = useState(false);
const [fileData, setFileData] = useState<any>(null);
const [convertedData, setConvertedData] = useState<ConvertedLorebook | null>(null);
const [originalFileName, setOriginalFileName] = useState("");
const [editingEntry, setEditingEntry] = useState<string | null>(null);
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingEntryData, setEditingEntryData] = useState<LorebookEntry | null>(null);
const [pasteDialogOpen, setPasteDialogOpen] = useState(false);
const [pasteText, setPasteText] = useState("");
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = (file: File) => {
if (!file.name.endsWith('.json')) {
alert('Please upload a JSON file');
return;
}
setOriginalFileName(file.name.replace('.json', ''));
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
setFileData(data);
const converted = convertJanitorToSillyTavern(data);
setConvertedData(converted);
} catch (err) {
alert('Invalid JSON file');
setFileData(null);
setConvertedData(null);
}
};
reader.readAsText(file);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileUpload(files[0]);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const convertJanitorToSillyTavern = (janitorData: any[]): ConvertedLorebook => {
const entries: { [key: string]: LorebookEntry } = {};
janitorData.forEach((entry, uid) => {
entries[uid.toString()] = {
uid: uid,
key: entry.key || [],
keysecondary: entry.keysecondary || [],
comment: entry.name || "Entry",
content: entry.content || "",
constant: entry.constant || false,
vectorized: false,
selective: true,
selectiveLogic: 0,
addMemo: true,
order: entry.insertion_order || 100,
position: 0,
disable: !entry.enabled,
excludeRecursion: true,
preventRecursion: true,
matchPersonaDescription: false,
matchCharacterDescription: false,
matchCharacterPersonality: false,
matchCharacterDepthPrompt: false,
matchScenario: false,
matchCreatorNotes: false,
delayUntilRecursion: false,
probability: entry.probability || 100,
useProbability: true,
depth: 4,
group: entry.inclusionGroupRaw || entry.category || "",
groupOverride: false,
groupWeight: entry.groupWeight || 100,
scanDepth: null,
caseSensitive: entry.case_sensitive || false,
matchWholeWords: entry.matchWholeWords !== undefined ? entry.matchWholeWords : true,
useGroupScoring: false,
automationId: "",
role: null,
sticky: 0,
cooldown: 0,
delay: 0,
displayIndex: uid,
filterToCharacters: "",
filterToGenerationTriggers: "",
excludeFromGeneration: false
};
});
return { entries };
};
const downloadLorebook = () => {
if (!convertedData) return;
const blob = new Blob([JSON.stringify(convertedData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${originalFileName}_ST.json`;
a.click();
URL.revokeObjectURL(url);
};
const openEditDialog = (entryId: string) => {
const entry = convertedData?.entries[entryId];
if (entry) {
setEditingEntryData({ ...entry });
setEditingEntry(entryId);
setEditDialogOpen(true);
}
};
const saveEntry = () => {
if (!editingEntryData || !editingEntry || !convertedData) return;
setConvertedData({
...convertedData,
entries: {
...convertedData.entries,
[editingEntry]: editingEntryData
}
});
setEditDialogOpen(false);
setEditingEntry(null);
setEditingEntryData(null);
};
const deleteEntry = (entryId: string) => {
if (!convertedData) return;
const newEntries = { ...convertedData.entries };
delete newEntries[entryId];
setConvertedData({
...convertedData,
entries: newEntries
});
};
const addNewEntry = () => {
if (!convertedData) return;
const newUid = Math.max(...Object.keys(convertedData.entries).map(Number)) + 1;
const newEntry: LorebookEntry = {
uid: newUid,
key: [],
keysecondary: [],
comment: "New Entry",
content: "",
constant: false,
vectorized: false,
selective: true,
selectiveLogic: 0,
addMemo: true,
order: 100,
position: 0,
disable: false,
excludeRecursion: true,
preventRecursion: true,
matchPersonaDescription: false,
matchCharacterDescription: false,
matchCharacterPersonality: false,
matchCharacterDepthPrompt: false,
matchScenario: false,
matchCreatorNotes: false,
delayUntilRecursion: false,
probability: 100,
useProbability: true,
depth: 4,
group: "",
groupOverride: false,
groupWeight: 100,
scanDepth: null,
caseSensitive: false,
matchWholeWords: true,
useGroupScoring: false,
automationId: "",
role: null,
sticky: 0,
cooldown: 0,
delay: 0,
displayIndex: newUid,
filterToCharacters: "",
filterToGenerationTriggers: "",
excludeFromGeneration: false
};
setConvertedData({
...convertedData,
entries: {
...convertedData.entries,
[newUid.toString()]: newEntry
}
});
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const clearLorebook = () => {
setFileData(null);
setConvertedData(null);
setOriginalFileName("");
setEditingEntry(null);
setEditingEntryData(null);
setEditDialogOpen(false);
setPasteDialogOpen(false);
setPasteText("");
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handlePasteData = () => {
try {
const data = JSON.parse(pasteText);
setFileData(data);
const converted = convertJanitorToSillyTavern(data);
setConvertedData(converted);
setOriginalFileName("pasted_lorebook");
setPasteDialogOpen(false);
setPasteText("");
} catch (err) {
alert('Invalid JSON data. Please check your paste content.');
}
};
const openPasteDialog = () => {
setPasteText("");
setPasteDialogOpen(true);
};
return (
<main className="min-h-screen bg-background text-foreground">
<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">Lorebook Converter</h1>
<p className="text-sm text-muted-foreground">
Convert Janitor AI lorebook format to SillyTavern format with editing capabilities
</p>
</div>
</div>
<Separator className="my-4" />
<Collapsible
open={isInstructionsOpen}
onOpenChange={setIsInstructionsOpen}
className="w-full mb-8"
>
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold">How to Use</h2>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="w-9 p-0">
{isInstructionsOpen ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
<span className="sr-only">Toggle instructions</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="mt-4">
<div className="prose dark:prose-invert max-w-none">
<ol className="list-decimal list-inside">
<li className="mb-2">
Upload your Janitor AI lorebook JSON file using the upload area below, or paste JSON data directly
</li>
<li className="mb-2">
The file will be automatically converted to SillyTavern format
</li>
<li className="mb-2">
Review and edit entries as needed using the edit buttons
</li>
<li className="mb-2">
Add new entries or delete existing ones if desired
</li>
<li className="mb-2">
Use "Clear & Upload New" to start over with a different lorebook
</li>
<li className="mb-2">
Download the converted lorebook when you're satisfied with the changes
</li>
</ol>
</div>
</CollapsibleContent>
</Collapsible>
{!convertedData ? (
<Card>
<CardContent className="pt-6">
<div
className="border-2 border-dashed border-primary/50 rounded-lg p-12 text-center cursor-pointer hover:border-primary transition-colors"
onDrop={handleDrop}
onDragOver={handleDragOver}
onClick={() => fileInputRef.current?.click()}
>
<Upload className="mx-auto h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Upload Lorebook File</h3>
<p className="text-muted-foreground mb-4">
Click to upload or drag & drop your Janitor AI lorebook JSON file
</p>
<div className="flex gap-2 justify-center">
<Button variant="outline">
Choose File
</Button>
<Button variant="outline" onClick={(e) => {
e.stopPropagation();
openPasteDialog();
}}>
<Clipboard className="h-4 w-4 mr-2" />
Paste JSON
</Button>
</div>
<input
ref={fileInputRef}
type="file"
accept=".json"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileUpload(file);
}}
/>
</div>
</CardContent>
</Card>
) : (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-semibold">
{Object.keys(convertedData.entries).length} Entries Converted
</h3>
<p className="text-sm text-muted-foreground">
From: {originalFileName}.json
</p>
</div>
<div className="flex gap-2">
<Button onClick={clearLorebook} variant="outline">
<RotateCcw className="h-4 w-4 mr-2" />
Clear & Upload New
</Button>
<Button onClick={addNewEntry} variant="outline">
Add Entry
</Button>
<Button onClick={downloadLorebook}>
<Download className="h-4 w-4 mr-2" />
Download JSON
</Button>
</div>
</div>
</CardContent>
</Card>
<div className="space-y-4">
{Object.entries(convertedData.entries).map(([entryId, entry]) => (
<Card key={entryId}>
<CardContent className="pt-6">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value={`entry-${entryId}`}>
<AccordionTrigger className="text-left">
<div className="flex items-center justify-between w-full mr-4">
<div>
<div className="font-semibold">{entry.comment}</div>
<div className="text-sm text-muted-foreground">
UID: {entry.uid} • Order: {entry.order} •
{entry.key.length > 0 ? ` Keys: ${entry.key.join(', ')}` : ' No keys'}
</div>
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
openEditDialog(entryId);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
deleteEntry(entryId);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-4 mt-4">
{entry.key.length > 0 && (
<div>
<h4 className="font-medium mb-2">Primary Keywords</h4>
<div className="flex flex-wrap gap-2">
{entry.key.map((keyword, idx) => (
<span
key={idx}
className="bg-primary/20 text-primary px-2 py-1 rounded text-sm"
>
{keyword}
</span>
))}
</div>
</div>
)}
{entry.group && (
<div>
<h4 className="font-medium mb-2">Group</h4>
<p className="text-sm bg-muted p-2 rounded">{entry.group}</p>
</div>
)}
<div>
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium">Content</h4>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(entry.content)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
<pre className="text-sm bg-muted p-3 rounded whitespace-pre-wrap">
{entry.content}
</pre>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="font-medium">Constant:</span> {entry.constant ? 'Yes' : 'No'}
</div>
<div>
<span className="font-medium">Selective:</span> {entry.selective ? 'Yes' : 'No'}
</div>
<div>
<span className="font-medium">Disabled:</span> {entry.disable ? 'Yes' : 'No'}
</div>
<div>
<span className="font-medium">Probability:</span> {entry.probability}%
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent>
</Card>
))}
</div>
</div>
)}
</div>
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Entry</DialogTitle>
<DialogDescription>
Modify the lorebook entry details below
</DialogDescription>
</DialogHeader>
{editingEntryData && (
<div className="space-y-6">
{/* Basic Info */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">Name/Comment</label>
<Input
value={editingEntryData.comment}
onChange={(e) => setEditingEntryData({
...editingEntryData,
comment: e.target.value
})}
/>
</div>
<div>
<label className="text-sm font-medium">Order</label>
<Input
type="number"
value={editingEntryData.order}
onChange={(e) => setEditingEntryData({
...editingEntryData,
order: parseInt(e.target.value) || 0
})}
/>
</div>
</div>
{/* Keywords */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Primary Keywords (comma-separated)</label>
<Input
placeholder="keyword1, keyword2, keyword3"
value={editingEntryData.key.join(', ')}
onChange={(e) => setEditingEntryData({
...editingEntryData,
key: e.target.value.split(',').map(k => k.trim()).filter(k => k)
})}
/>
</div>
<div>
<label className="text-sm font-medium">Secondary Keywords (comma-separated)</label>
<Input
placeholder="optional, secondary, keywords"
value={editingEntryData.keysecondary.join(', ')}
onChange={(e) => setEditingEntryData({
...editingEntryData,
keysecondary: e.target.value.split(',').map(k => k.trim()).filter(k => k)
})}
/>
</div>
</div>
{/* Logic and Filters */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">Logic</label>
<Select
value={editingEntryData.selectiveLogic.toString()}
onValueChange={(value) => setEditingEntryData({
...editingEntryData,
selectiveLogic: parseInt(value)
})}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">AND ANY</SelectItem>
<SelectItem value="1">AND ALL</SelectItem>
<SelectItem value="2">NOT ANY</SelectItem>
<SelectItem value="3">NOT ALL</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Scan Depth</label>
<Input
type="number"
placeholder="Use global setting"
value={editingEntryData.scanDepth || ""}
onChange={(e) => setEditingEntryData({
...editingEntryData,
scanDepth: e.target.value ? parseInt(e.target.value) : null
})}
/>
</div>
</div>
{/* Boolean Settings Row 1 */}
<div className="grid grid-cols-3 gap-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.caseSensitive}
onChange={(e) => setEditingEntryData({
...editingEntryData,
caseSensitive: e.target.checked
})}
/>
<span className="text-sm">Case-Sensitive</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.matchWholeWords}
onChange={(e) => setEditingEntryData({
...editingEntryData,
matchWholeWords: e.target.checked
})}
/>
<span className="text-sm">Whole Words</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.useGroupScoring}
onChange={(e) => setEditingEntryData({
...editingEntryData,
useGroupScoring: e.target.checked
})}
/>
<span className="text-sm">Group Scoring</span>
</label>
</div>
{/* Automation ID */}
<div>
<label className="text-sm font-medium">Automation ID</label>
<Input
placeholder="None"
value={editingEntryData.automationId}
onChange={(e) => setEditingEntryData({
...editingEntryData,
automationId: e.target.value
})}
/>
</div>
{/* Content */}
<div>
<label className="text-sm font-medium">Content</label>
<Textarea
className="min-h-[120px] resize-none"
value={editingEntryData.content}
onChange={(e) => setEditingEntryData({
...editingEntryData,
content: e.target.value
})}
/>
</div>
{/* Recursion Settings */}
<div className="grid grid-cols-4 gap-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={!editingEntryData.excludeRecursion}
onChange={(e) => setEditingEntryData({
...editingEntryData,
excludeRecursion: !e.target.checked
})}
/>
<span className="text-sm">Non-recursable</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={!editingEntryData.preventRecursion}
onChange={(e) => setEditingEntryData({
...editingEntryData,
preventRecursion: !e.target.checked
})}
/>
<span className="text-sm">Prevent further recursion</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.delayUntilRecursion}
onChange={(e) => setEditingEntryData({
...editingEntryData,
delayUntilRecursion: e.target.checked
})}
/>
<span className="text-sm">Delay until recursion</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.disable}
onChange={(e) => setEditingEntryData({
...editingEntryData,
disable: e.target.checked
})}
/>
<span className="text-sm">Ignore budget</span>
</label>
</div>
{/* Group Settings */}
<div className="space-y-4">
<div>
<label className="text-sm font-medium">Inclusion Group</label>
<Input
placeholder="Only one entry with the same label"
value={editingEntryData.group}
onChange={(e) => setEditingEntryData({
...editingEntryData,
group: e.target.value
})}
/>
</div>
<div className="grid grid-cols-4 gap-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.groupOverride}
onChange={(e) => setEditingEntryData({
...editingEntryData,
groupOverride: e.target.checked
})}
/>
<span className="text-sm">Prioritize</span>
</label>
<div>
<label className="text-sm font-medium">Group Weight</label>
<Input
type="number"
value={editingEntryData.groupWeight}
onChange={(e) => setEditingEntryData({
...editingEntryData,
groupWeight: parseInt(e.target.value) || 0
})}
/>
</div>
<div>
<label className="text-sm font-medium">Sticky</label>
<Input
type="number"
value={editingEntryData.sticky}
onChange={(e) => setEditingEntryData({
...editingEntryData,
sticky: parseInt(e.target.value) || 0
})}
/>
</div>
<div>
<label className="text-sm font-medium">Cooldown</label>
<Input
type="number"
value={editingEntryData.cooldown}
onChange={(e) => setEditingEntryData({
...editingEntryData,
cooldown: parseInt(e.target.value) || 0
})}
/>
</div>
</div>
<div>
<label className="text-sm font-medium">Delay</label>
<Input
type="number"
value={editingEntryData.delay}
onChange={(e) => setEditingEntryData({
...editingEntryData,
delay: parseInt(e.target.value) || 0
})}
/>
</div>
</div>
{/* Filter Settings */}
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm font-medium">Filter to Characters or Tags</label>
<Input
placeholder="Tie this entry to specific characters or characters with"
value={editingEntryData.filterToCharacters}
onChange={(e) => setEditingEntryData({
...editingEntryData,
filterToCharacters: e.target.value
})}
/>
</div>
<div>
<label className="text-sm font-medium">Filter to Generation Triggers</label>
<Input
placeholder="All types (default)"
value={editingEntryData.filterToGenerationTriggers}
onChange={(e) => setEditingEntryData({
...editingEntryData,
filterToGenerationTriggers: e.target.value
})}
/>
</div>
</div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.excludeFromGeneration}
onChange={(e) => setEditingEntryData({
...editingEntryData,
excludeFromGeneration: e.target.checked
})}
/>
<span className="text-sm">Exclude</span>
</label>
</div>
{/* Additional Matching Sources */}
<div>
<h4 className="text-sm font-medium mb-3">Additional Matching Sources</h4>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.matchCharacterDescription}
onChange={(e) => setEditingEntryData({
...editingEntryData,
matchCharacterDescription: e.target.checked
})}
/>
<span className="text-sm">Character Description</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.matchCharacterPersonality}
onChange={(e) => setEditingEntryData({
...editingEntryData,
matchCharacterPersonality: e.target.checked
})}
/>
<span className="text-sm">Character Personality</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.matchScenario}
onChange={(e) => setEditingEntryData({
...editingEntryData,
matchScenario: e.target.checked
})}
/>
<span className="text-sm">Scenario</span>
</label>
</div>
<div className="space-y-2">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.matchPersonaDescription}
onChange={(e) => setEditingEntryData({
...editingEntryData,
matchPersonaDescription: e.target.checked
})}
/>
<span className="text-sm">Persona Description</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.matchCharacterDepthPrompt}
onChange={(e) => setEditingEntryData({
...editingEntryData,
matchCharacterDepthPrompt: e.target.checked
})}
/>
<span className="text-sm">Character's Note</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.matchCreatorNotes}
onChange={(e) => setEditingEntryData({
...editingEntryData,
matchCreatorNotes: e.target.checked
})}
/>
<span className="text-sm">Creator's Notes</span>
</label>
</div>
</div>
</div>
{/* Main Settings */}
<div className="grid grid-cols-3 gap-4">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.constant}
onChange={(e) => setEditingEntryData({
...editingEntryData,
constant: e.target.checked
})}
/>
<span className="text-sm">Constant</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={editingEntryData.selective}
onChange={(e) => setEditingEntryData({
...editingEntryData,
selective: e.target.checked
})}
/>
<span className="text-sm">Selective</span>
</label>
<div>
<label className="text-sm font-medium">Probability (%)</label>
<Input
type="number"
min="0"
max="100"
value={editingEntryData.probability}
onChange={(e) => setEditingEntryData({
...editingEntryData,
probability: parseInt(e.target.value) || 0
})}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
Cancel
</Button>
<Button onClick={saveEntry}>
<Save className="h-4 w-4 mr-2" />
Save Changes
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
<Dialog open={pasteDialogOpen} onOpenChange={setPasteDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Paste Lorebook JSON</DialogTitle>
<DialogDescription>
Paste your Janitor AI lorebook JSON data below. This should be an array of lorebook entries.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="text-sm font-medium">JSON Data</label>
<Textarea
className="min-h-[300px] font-mono text-sm"
placeholder='[{"category": "general", "content": "...", "enabled": true, ...}, ...]'
value={pasteText}
onChange={(e) => setPasteText(e.target.value)}
/>
</div>
<div className="bg-muted p-3 rounded text-sm">
<p className="font-medium mb-2">Expected format:</p>
<p>An array of objects with fields like: category, content, enabled, key, name, etc.</p>
<p className="mt-1 text-muted-foreground">
Example: The data you provided shows the correct format with entries containing category, content, enabled, key arrays, etc.
</p>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setPasteDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handlePasteData} disabled={!pasteText.trim()}>
<Clipboard className="h-4 w-4 mr-2" />
Process JSON
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</main>
);
}