Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
102 changes: 78 additions & 24 deletions packages/client/src/components/lorebooks/LorebookFormFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
// and LorebookEntryRow (the per-entry inline drawer).
// Extracted from LorebookEditor.tsx so styling stays consistent.
// ──────────────────────────────────────────────
import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { FileText, Maximize2, ToggleLeft, ToggleRight, X } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { FileText, Maximize2, Sparkles, ToggleLeft, ToggleRight, X } from "lucide-react";
import { cn } from "../../lib/utils";
import { HelpTooltip } from "../ui/HelpTooltip";
import { MagicRewritePanel } from "../ui/MagicRewritePanel";

export function FieldGroup({
label,
Expand Down Expand Up @@ -233,6 +234,8 @@ export function ExpandedContentModal({
placeholder?: string;
}) {
const [local, setLocal] = useState(value);
const [magicRewriteMode, setMagicRewriteMode] = useState(false);
const [magicRewriteResult, setMagicRewriteResult] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);

useEffect(() => {
Expand All @@ -241,52 +244,103 @@ export function ExpandedContentModal({

useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (e.key === "Escape" && !magicRewriteMode) {
onChange(local);
onCommit?.();
onClose();
}
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [onClose, onChange, onCommit, local]);
}, [onClose, onChange, onCommit, local, magicRewriteMode]);

const handleClose = () => {
onChange(local);
onCommit?.();
onClose();
};

const handleMagicRewriteBack = () => {
setMagicRewriteMode(false);
setMagicRewriteResult("");
};

const handleMagicRewriteApply = () => {
if (!magicRewriteResult) return;
setLocal(magicRewriteResult);
setMagicRewriteMode(false);
setMagicRewriteResult("");
window.setTimeout(() => textareaRef.current?.focus(), 100);
};

const handleMagicRewriteResultChange = useCallback((next: string) => {
setMagicRewriteResult(next);
}, []);

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-6 max-md:pt-[max(1.5rem,env(safe-area-inset-top))]">
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={handleClose} />
<div className="relative flex h-[80vh] w-full max-w-3xl flex-col rounded-2xl border border-[var(--border)] bg-[var(--card)] shadow-2xl shadow-black/50">
<div className="relative flex h-[80vh] w-full max-w-5xl flex-col rounded-2xl border border-[var(--border)] bg-[var(--card)] shadow-2xl shadow-black/50">
<div className="flex items-center justify-between border-b border-[var(--border)] px-4 py-3">
<h3 className="text-sm font-semibold">{title}</h3>
<button onClick={handleClose} className="rounded-lg p-1.5 hover:bg-[var(--accent)]">
<X size="1rem" />
</button>
<h3 className="text-sm font-semibold">{magicRewriteMode ? "✨ Magic Rewrite" : title}</h3>
<div className="flex items-center gap-2">
<button onClick={handleClose} className="rounded-lg p-1.5 hover:bg-[var(--accent)]">
<X size="1rem" />
</button>
</div>
</div>
<div className="flex-1 overflow-hidden p-4">
<textarea
ref={textareaRef}
value={local}
onChange={(e) => setLocal(e.target.value)}
onKeyDown={(e) => handleTextareaTabKeyDown(e, local, setLocal)}
className="h-full w-full resize-none rounded-lg bg-[var(--secondary)] p-4 text-sm text-[var(--foreground)] ring-1 ring-[var(--border)] placeholder:text-[var(--muted-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
placeholder={placeholder}
/>
{magicRewriteMode ? (
<MagicRewritePanel value={local} onResultChange={handleMagicRewriteResultChange} />
) : (
<textarea
ref={textareaRef}
value={local}
onChange={(e) => setLocal(e.target.value)}
onKeyDown={(e) => handleTextareaTabKeyDown(e, local, setLocal)}
className="h-full w-full resize-none rounded-lg bg-[var(--secondary)] p-4 text-sm text-[var(--foreground)] ring-1 ring-[var(--border)] placeholder:text-[var(--muted-foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
placeholder={placeholder}
/>
)}
</div>
<div className="flex items-center justify-between border-t border-[var(--border)] px-4 py-2.5">
<p className="text-[0.625rem] text-[var(--muted-foreground)]">
Changes auto-save on close. Press Escape to close.
{magicRewriteMode ? "Back returns to the editor without applying. Apply moves the preview into the editor." : "Changes auto-save on close. Press Escape to close."}
</p>
<button
onClick={handleClose}
className="rounded-xl bg-gradient-to-r from-amber-400 to-orange-500 px-4 py-1.5 text-xs font-medium text-white shadow-md hover:shadow-lg active:scale-[0.98]"
>
Done
</button>
{magicRewriteMode ? (
<div className="flex items-center gap-2">
<button
onClick={handleMagicRewriteBack}
className="rounded-xl border border-[var(--border)] px-4 py-1.5 text-xs font-medium text-[var(--foreground)] hover:bg-[var(--accent)] active:scale-[0.98]"
>
Back
</button>
<button
onClick={handleMagicRewriteApply}
disabled={!magicRewriteResult}
className="rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 px-4 py-1.5 text-xs font-medium text-white shadow-md hover:shadow-lg active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40"
>
Apply
</button>
</div>
) : (
<div className="flex items-center gap-2">
<button
onClick={() => setMagicRewriteMode(true)}
className="inline-flex items-center gap-1.5 rounded-lg border border-violet-400/30 bg-violet-500/10 px-3 py-1.5 text-xs font-medium text-violet-200 hover:bg-violet-500/20"
title="Open Magic Rewrite"
>
<Sparkles size="0.875rem" />
Rewrite
</button>
<button
onClick={handleClose}
className="rounded-xl bg-gradient-to-r from-amber-400 to-orange-500 px-4 py-1.5 text-xs font-medium text-white shadow-md hover:shadow-lg active:scale-[0.98]"
>
Done
</button>
</div>
)}
</div>
</div>
</div>
Expand Down
101 changes: 84 additions & 17 deletions packages/client/src/components/ui/ExpandedTextarea.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// ──────────────────────────────────────────────
// Expanded Textarea — Fullscreen editing overlay
// ──────────────────────────────────────────────
import { useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { motion, AnimatePresence } from "framer-motion";
import { Minimize2 } from "lucide-react";
import { Minimize2, Sparkles } from "lucide-react";
import { MagicRewritePanel } from "./MagicRewritePanel";

interface ExpandedTextareaProps {
open: boolean;
Expand All @@ -17,22 +18,57 @@ interface ExpandedTextareaProps {

export function ExpandedTextarea({ open, onClose, title, value, onChange, placeholder }: ExpandedTextareaProps) {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [local, setLocal] = useState(value);
const [magicRewriteMode, setMagicRewriteMode] = useState(false);
const [magicRewriteResult, setMagicRewriteResult] = useState("");

// Sync parent value into local when prop changes; reset rewrite state on close
useEffect(() => {
if (!open) {
setMagicRewriteMode(false);
setMagicRewriteResult("");
}
setLocal(value);
}, [value, open]);

useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
if (e.key === "Escape" && !magicRewriteMode) onClose();
};
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, [open, onClose]);
}, [open, onClose, magicRewriteMode]);

// Focus textarea when opened
useEffect(() => {
if (open) {
if (open && !magicRewriteMode) {
requestAnimationFrame(() => textareaRef.current?.focus());
}
}, [open]);
}, [open, magicRewriteMode]);

const handleClose = () => {
onChange(local);
onClose();
};

const handleMagicRewriteResultChange = useCallback((next: string) => {
setMagicRewriteResult(next);
}, []);

const handleMagicRewriteApply = () => {
if (!magicRewriteResult) return;
setLocal(magicRewriteResult);
setMagicRewriteMode(false);
setMagicRewriteResult("");
onChange(magicRewriteResult);
requestAnimationFrame(() => textareaRef.current?.focus());
};

const handleMagicRewriteBack = () => {
setMagicRewriteMode(false);
setMagicRewriteResult("");
};

return createPortal(
<AnimatePresence>
Expand All @@ -46,11 +82,38 @@ export function ExpandedTextarea({ open, onClose, title, value, onChange, placeh
>
{/* Header */}
<div className="flex shrink-0 items-center justify-between border-b border-[var(--border)] px-5 py-3">
<h2 className="text-sm font-semibold">{title}</h2>
<h2 className="text-sm font-semibold">{magicRewriteMode ? "✨ Magic Rewrite" : title}</h2>
<div className="flex items-center gap-2">
<span className="text-[0.625rem] text-[var(--muted-foreground)]">{value.length} characters</span>
{!magicRewriteMode && (
<button
onClick={() => setMagicRewriteMode(true)}
className="flex items-center gap-1.5 rounded-lg border border-violet-400/30 bg-violet-500/10 px-2.5 py-1.5 text-xs font-medium text-violet-200 hover:bg-violet-500/20"
title="Open Magic Rewrite"
>
<Sparkles size="0.875rem" />
Rewrite
</button>
)}
{magicRewriteMode && (
<button
onClick={handleMagicRewriteBack}
className="flex items-center gap-1.5 rounded-lg border border-[var(--border)] px-2.5 py-1.5 text-xs font-medium text-[var(--foreground)] hover:bg-[var(--accent)]"
>
Back
</button>
)}
{magicRewriteMode && (
<button
onClick={handleMagicRewriteApply}
disabled={!magicRewriteResult}
className="rounded-xl bg-gradient-to-r from-violet-500 to-fuchsia-500 px-3 py-1.5 text-xs font-medium text-white disabled:cursor-not-allowed disabled:opacity-40"
>
Apply
</button>
)}
<span className="text-[0.625rem] text-[var(--muted-foreground)]">{local.length} characters</span>
<button
onClick={onClose}
onClick={handleClose}
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs text-[var(--muted-foreground)] transition-colors hover:bg-[var(--accent)] hover:text-[var(--foreground)]"
>
<Minimize2 size="0.875rem" />
Expand All @@ -59,15 +122,19 @@ export function ExpandedTextarea({ open, onClose, title, value, onChange, placeh
</div>
</div>

{/* Textarea */}
{/* Content */}
<div className="flex-1 overflow-hidden p-4 md:p-6">
<textarea
ref={textareaRef}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
className="h-full w-full resize-none rounded-xl border border-[var(--border)] bg-[var(--secondary)] p-5 text-sm leading-relaxed outline-none transition-colors placeholder:text-[var(--muted-foreground)]/40 focus:border-[var(--primary)]/40 focus:ring-1 focus:ring-[var(--primary)]/20"
/>
{magicRewriteMode ? (
<MagicRewritePanel value={local} onResultChange={handleMagicRewriteResultChange} />
) : (
<textarea
ref={textareaRef}
value={local}
onChange={(e) => setLocal(e.target.value)}
placeholder={placeholder}
className="h-full w-full resize-none rounded-xl border border-[var(--border)] bg-[var(--secondary)] p-5 text-sm leading-relaxed outline-none transition-colors placeholder:text-[var(--muted-foreground)]/40 focus:border-[var(--primary)]/40 focus:ring-1 focus:ring-[var(--primary)]/20"
/>
)}
</div>
</motion.div>
)}
Expand Down
Loading