1053 lines
39 KiB
TypeScript
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>
|
|
);
|
|
} |