"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(null); const [convertedData, setConvertedData] = useState(null); const [originalFileName, setOriginalFileName] = useState(""); const [editingEntry, setEditingEntry] = useState(null); const [editDialogOpen, setEditDialogOpen] = useState(false); const [editingEntryData, setEditingEntryData] = useState(null); const [pasteDialogOpen, setPasteDialogOpen] = useState(false); const [pasteText, setPasteText] = useState(""); const fileInputRef = useRef(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 (

Lorebook Converter

Convert Janitor AI lorebook format to SillyTavern format with editing capabilities

How to Use

  1. Upload your Janitor AI lorebook JSON file using the upload area below, or paste JSON data directly
  2. The file will be automatically converted to SillyTavern format
  3. Review and edit entries as needed using the edit buttons
  4. Add new entries or delete existing ones if desired
  5. Use "Clear & Upload New" to start over with a different lorebook
  6. Download the converted lorebook when you're satisfied with the changes
{!convertedData ? (
fileInputRef.current?.click()} >

Upload Lorebook File

Click to upload or drag & drop your Janitor AI lorebook JSON file

{ const file = e.target.files?.[0]; if (file) handleFileUpload(file); }} />
) : (

{Object.keys(convertedData.entries).length} Entries Converted

From: {originalFileName}.json

{Object.entries(convertedData.entries).map(([entryId, entry]) => (
{entry.comment}
UID: {entry.uid} • Order: {entry.order} • {entry.key.length > 0 ? ` Keys: ${entry.key.join(', ')}` : ' No keys'}
{entry.key.length > 0 && (

Primary Keywords

{entry.key.map((keyword, idx) => ( {keyword} ))}
)} {entry.group && (

Group

{entry.group}

)}

Content

                                {entry.content}
                              
Constant: {entry.constant ? 'Yes' : 'No'}
Selective: {entry.selective ? 'Yes' : 'No'}
Disabled: {entry.disable ? 'Yes' : 'No'}
Probability: {entry.probability}%
))}
)}
Edit Entry Modify the lorebook entry details below {editingEntryData && (
{/* Basic Info */}
setEditingEntryData({ ...editingEntryData, comment: e.target.value })} />
setEditingEntryData({ ...editingEntryData, order: parseInt(e.target.value) || 0 })} />
{/* Keywords */}
setEditingEntryData({ ...editingEntryData, key: e.target.value.split(',').map(k => k.trim()).filter(k => k) })} />
setEditingEntryData({ ...editingEntryData, keysecondary: e.target.value.split(',').map(k => k.trim()).filter(k => k) })} />
{/* Logic and Filters */}
setEditingEntryData({ ...editingEntryData, scanDepth: e.target.value ? parseInt(e.target.value) : null })} />
{/* Boolean Settings Row 1 */}
{/* Automation ID */}
setEditingEntryData({ ...editingEntryData, automationId: e.target.value })} />
{/* Content */}