Compare commits
No commits in common. "7f18a66a8b7f525d354efbeda8a2d71afafb8926" and "2a711a6deadcb5fa8d037866eeb93f8485bbaa4f" have entirely different histories.
7f18a66a8b
...
2a711a6dea
|
|
@ -1,36 +1,22 @@
|
|||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import type { JSX } from "preact";
|
||||
import clsx from "clsx";
|
||||
import styles from "../assets/content-editable.module.css";
|
||||
import { highlight } from "../highlight";
|
||||
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'value' | 'onInput'> & {
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
autoLines?: boolean;
|
||||
highlight?: boolean;
|
||||
onInput?: JSX.EventHandler<JSX.TargetedInputEvent<HTMLDivElement>>;
|
||||
};
|
||||
|
||||
const parseLines = (html: string): string => (
|
||||
html
|
||||
.replace(/\n?<div><br\/?><\/div>/g, '\n\n')
|
||||
.replace(/\n?<div[^>]*>/, '\n')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
);
|
||||
|
||||
function getCaretOffset(el: HTMLElement): number {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.rangeCount === 0) return 0;
|
||||
|
||||
const range = sel.getRangeAt(0).cloneRange();
|
||||
range.selectNodeContents(el);
|
||||
range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
|
||||
const contents = range.cloneContents();
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(contents);
|
||||
const text = parseLines(div.innerHTML);
|
||||
return text.length;
|
||||
return range.toString().length;
|
||||
}
|
||||
|
||||
function setCaretOffset(el: HTMLElement, offset: number) {
|
||||
|
|
@ -41,8 +27,7 @@ function setCaretOffset(el: HTMLElement, offset: number) {
|
|||
|
||||
function traverse(node: Node): boolean {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent || '';
|
||||
const len = text.length;
|
||||
const len = node.textContent?.length ?? 0;
|
||||
if (remaining <= len) {
|
||||
range.setStart(node, remaining);
|
||||
range.collapse(true);
|
||||
|
|
@ -71,46 +56,61 @@ function resizeToContent(el: HTMLElement) {
|
|||
el.style.height = el.scrollHeight + 'px';
|
||||
}
|
||||
|
||||
export const ContentEditable = ({
|
||||
value,
|
||||
placeholder,
|
||||
autoLines,
|
||||
highlight: enableHighlight,
|
||||
onInput,
|
||||
class: externalClass,
|
||||
...props
|
||||
}: Props) => {
|
||||
export const ContentEditable = ({ value, placeholder, autoLines, onInput, class: externalClass, ...props }: Props) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
if (!el || el.innerHTML === value) return;
|
||||
|
||||
const offset = document.activeElement === el ? getCaretOffset(el) : null;
|
||||
const newValue = enableHighlight ? highlight(value) : value;
|
||||
el.innerHTML = newValue;
|
||||
(el as any).value = value;
|
||||
el.innerHTML = value;
|
||||
if (offset !== null) setCaretOffset(el, offset);
|
||||
if (autoLines) resizeToContent(el);
|
||||
}, [value, enableHighlight]);
|
||||
}, [value]);
|
||||
|
||||
const handleKeyDown: JSX.KeyboardEventHandler<HTMLDivElement> = (e) => {
|
||||
if (e.key !== 'Enter' || !ref.current) return;
|
||||
e.preventDefault();
|
||||
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.rangeCount === 0) return;
|
||||
|
||||
const range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
|
||||
const endsWithNewline = ref.current.textContent?.endsWith('\n');
|
||||
const caretAtEnd = getCaretOffset(ref.current) === (ref.current.textContent?.length ?? 0);
|
||||
|
||||
const newline = document.createTextNode('\n'.repeat((endsWithNewline || !caretAtEnd) ? 1 : 2));
|
||||
range.insertNode(newline);
|
||||
range.setStartAfter(newline);
|
||||
range.collapse(true);
|
||||
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
(ref.current as any).value = ref.current.textContent;
|
||||
ref.current?.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
||||
};
|
||||
|
||||
const handleInput: JSX.EventHandler<JSX.TargetedInputEvent<HTMLDivElement>> = (e) => {
|
||||
if (!ref.current) return;
|
||||
if (autoLines) resizeToContent(ref.current);
|
||||
const text = parseLines(ref.current.innerHTML);
|
||||
// add value prop to work with useInputState/useInputCallback
|
||||
(ref.current as any).value = text;
|
||||
if (autoLines && ref.current) resizeToContent(ref.current);
|
||||
if (ref.current) {
|
||||
(ref.current as any).value = ref.current.textContent;
|
||||
}
|
||||
onInput?.(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
contentEditable
|
||||
onKeyDown={handleKeyDown}
|
||||
onInput={handleInput}
|
||||
data-placeholder={value.replaceAll('\n', '').length ? undefined : placeholder}
|
||||
class={clsx(styles.root, autoLines && styles.autoLines, externalClass)}
|
||||
onInput={handleInput}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -515,9 +515,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
|||
<div class={styles.editContainer}>
|
||||
<ContentEditable
|
||||
autoLines
|
||||
highlight
|
||||
class={styles.editTextarea}
|
||||
value={editingContent}
|
||||
value={highlight(editingContent)}
|
||||
onInput={setEditingContent}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && e.ctrlKey) {
|
||||
|
|
@ -571,9 +570,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => {
|
|||
<div class={styles.inputWrapper}>
|
||||
<ContentEditable
|
||||
autoLines
|
||||
highlight
|
||||
class={styles.input}
|
||||
value={input}
|
||||
value={highlight(input)}
|
||||
onInput={setInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import styles from "../../assets/chapters-editor.module.css";
|
||||
import { useAppState } from "../../contexts/state";
|
||||
|
|
@ -46,9 +47,8 @@ export const ChaptersEditor = ({ visible }: { visible: boolean }) => {
|
|||
<div class={styles.chunkPreview}>{body}</div>
|
||||
<ContentEditable
|
||||
autoLines
|
||||
highlight
|
||||
class={styles.summaryEditable}
|
||||
value={summary ?? ''}
|
||||
value={highlight(summary ?? '')}
|
||||
placeholder="Not summarized yet..."
|
||||
onInput={(e) => dispatch({
|
||||
type: 'STORE_CHAPTER_SUMMARY',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import styles from "../../assets/editor.module.css";
|
||||
import { useAppState } from "../../contexts/state";
|
||||
|
||||
|
|
@ -16,15 +18,16 @@ export const ScratchpadEditor = ({ visible }: { visible: boolean }) => {
|
|||
});
|
||||
}, [currentStory?.id, currentWorld?.id]);
|
||||
|
||||
const value = useMemo(() => highlight(currentStory?.scratchpad || ''), [currentStory?.scratchpad]);
|
||||
|
||||
if (!currentWorld || !currentStory || !visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentEditable
|
||||
highlight
|
||||
class={styles.editable}
|
||||
value={currentStory?.scratchpad ?? ''}
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
placeholder="Notes, ideas, outlines — anything you don't want in the story..."
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import styles from "../../assets/editor.module.css";
|
||||
|
|
@ -19,15 +20,12 @@ export const StoryEditor = ({ visible }: { visible: boolean }) => {
|
|||
|
||||
const value = useMemo(() => {
|
||||
if (!currentStory) return '';
|
||||
|
||||
const { text, lastEditedText } = currentStory;
|
||||
if (!lastEditedText) return text;
|
||||
|
||||
if (!lastEditedText) return highlight(text);
|
||||
const idx = text.lastIndexOf(lastEditedText);
|
||||
if (idx === -1) return text;
|
||||
|
||||
if (idx === -1) return highlight(text);
|
||||
const marked = text.slice(0, idx) + '<mark>' + lastEditedText + '</mark>' + text.slice(idx + lastEditedText.length);
|
||||
return marked;
|
||||
return highlight(marked);
|
||||
}, [currentStory?.text, currentStory?.lastEditedText]);
|
||||
|
||||
|
||||
|
|
@ -37,7 +35,6 @@ export const StoryEditor = ({ visible }: { visible: boolean }) => {
|
|||
|
||||
return (
|
||||
<ContentEditable
|
||||
highlight
|
||||
class={styles.editable}
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import { useMemo } from "preact/hooks";
|
||||
import styles from "../../assets/editor.module.css";
|
||||
import { useAppState } from "../../contexts/state";
|
||||
|
||||
|
|
@ -15,15 +17,16 @@ export const SystemEditor = ({ visible }: { visible: boolean }) => {
|
|||
});
|
||||
}, [currentWorld?.id]);
|
||||
|
||||
const value = useMemo(() => highlight(currentWorld?.systemInstructionOverride || ''), [currentWorld?.systemInstructionOverride]);
|
||||
|
||||
if (!currentWorld || !visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentEditable
|
||||
highlight
|
||||
class={styles.editable}
|
||||
value={currentWorld?.systemInstructionOverride || ''}
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
placeholder="Override the global system instruction for this world. Leave empty to use the global setting."
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import clsx from "clsx";
|
||||
import styles from "../../assets/settings-modal.module.css";
|
||||
|
|
@ -16,8 +17,7 @@ export const ChatSystemInstructionSettings = () => {
|
|||
<div class={clsx(styles.formGroup, styles.formGroupFill)}>
|
||||
<label class={styles.label}>Chat System Instruction</label>
|
||||
<ContentEditable
|
||||
highlight
|
||||
value={chatSystemInstruction}
|
||||
value={highlight(chatSystemInstruction)}
|
||||
onInput={setInstructionValue}
|
||||
placeholder="Enter default system instruction for chat/roleplay worlds ({{char}}, {{user}} supported)..."
|
||||
class={clsx(styles.input, styles.textarea)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import clsx from "clsx";
|
||||
import styles from "../../assets/settings-modal.module.css";
|
||||
|
|
@ -16,8 +17,7 @@ export const ContinuePromptSettings = () => {
|
|||
<div class={clsx(styles.formGroup, styles.formGroupFill)}>
|
||||
<label class={styles.label}>Continue Prompt</label>
|
||||
<ContentEditable
|
||||
highlight
|
||||
value={continuePrompt}
|
||||
value={highlight(continuePrompt)}
|
||||
onInput={setPromptValue}
|
||||
placeholder="Enter the prompt prepended when continuing the story..."
|
||||
class={clsx(styles.input, styles.textarea)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import clsx from "clsx";
|
||||
import styles from "../../assets/settings-modal.module.css";
|
||||
|
|
@ -16,8 +17,7 @@ export const SystemInstructionSettings = () => {
|
|||
<div class={clsx(styles.formGroup, styles.formGroupFill)}>
|
||||
<label class={styles.label}>System Instruction</label>
|
||||
<ContentEditable
|
||||
highlight
|
||||
value={systemInstruction}
|
||||
value={highlight(systemInstruction)}
|
||||
onInput={setInstructionValue}
|
||||
placeholder="Enter system instruction for the AI assistant..."
|
||||
class={clsx(styles.input, styles.textarea)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ContentEditable } from "@common/components/ContentEditable";
|
||||
import { highlight } from "@common/highlight";
|
||||
import { useInputCallback } from "@common/hooks/useInputCallback";
|
||||
import { useInputState } from "@common/hooks/useInputState";
|
||||
import clsx from "clsx";
|
||||
|
|
@ -37,8 +38,7 @@ export const UserSettings = () => {
|
|||
<div class={clsx(styles.formGroup, styles.formGroupFill)}>
|
||||
<label class={styles.label}>Your Description</label>
|
||||
<ContentEditable
|
||||
highlight
|
||||
value={userDescription}
|
||||
value={highlight(userDescription)}
|
||||
onInput={setDescription}
|
||||
placeholder="Describe yourself for the AI..."
|
||||
class={clsx(styles.input, styles.textarea)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue