1
0
Fork 0

Compare commits

..

No commits in common. "7f18a66a8b7f525d354efbeda8a2d71afafb8926" and "2a711a6deadcb5fa8d037866eeb93f8485bbaa4f" have entirely different histories.

10 changed files with 65 additions and 64 deletions

View File

@ -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}
/>
);
};

View File

@ -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={

View File

@ -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',

View File

@ -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..."
/>

View File

@ -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}

View File

@ -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."
/>

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}

View File

@ -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)}