AIStory: fix textarea jump
This commit is contained in:
parent
277b315795
commit
e82eaed944
|
|
@ -22,7 +22,7 @@ Bun.serve({
|
|||
{
|
||||
production: url.searchParams.get('production') === 'true', // to debug production builds
|
||||
portable: url.searchParams.get('portable') === 'true', // to skip AssemblyScript compilation,
|
||||
mobile: detectedBrowser.mobile,
|
||||
mobile: detectedBrowser.mobile || url.searchParams.get('mobile') === 'true',
|
||||
}
|
||||
);
|
||||
if (html) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useRef } from "preact/hooks";
|
|||
import type { JSX } from "preact/jsx-runtime"
|
||||
|
||||
import { useIsVisible } from '@common/hooks/useIsVisible';
|
||||
import { DOMTools } from "../dom";
|
||||
|
||||
export const AutoTextarea = (props: JSX.HTMLAttributes<HTMLTextAreaElement>) => {
|
||||
const { value } = props;
|
||||
|
|
@ -12,12 +13,10 @@ export const AutoTextarea = (props: JSX.HTMLAttributes<HTMLTextAreaElement>) =>
|
|||
if (ref.current && isVisible) {
|
||||
const area = ref.current;
|
||||
|
||||
area.style.height = '0'; // reset
|
||||
area.style.height = `${area.scrollHeight}px`;
|
||||
const { height } = DOMTools.calculateNodeHeight(area);
|
||||
area.style.height = `${height}px`;
|
||||
}
|
||||
}, [value, isVisible]);
|
||||
|
||||
|
||||
|
||||
return <textarea {...props} ref={ref} />
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useEffect, useRef } from "preact/hooks";
|
||||
import { useCallback, useContext, useEffect, useRef } from "preact/hooks";
|
||||
import { StateContext } from "../contexts/state";
|
||||
import { Message } from "./message/message";
|
||||
import { MessageTools } from "../messages";
|
||||
|
|
@ -15,16 +15,19 @@ export const Chat = () => {
|
|||
const lastAssistantId = messages.findLastIndex(m => m.role === 'assistant');
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => DOMTools.scrollDown(chatRef.current, false), 100);
|
||||
DOMTools.scrollDown(chatRef.current);
|
||||
}, [messages.length, lastMessageContent]);
|
||||
|
||||
const handleScroll = useCallback(() => DOMTools.scrollDown(chatRef.current, false), []);
|
||||
|
||||
return (
|
||||
<div class="chat" ref={chatRef}>
|
||||
{messages.map((m, i) => (
|
||||
{messages.map((m, i, ms) => (
|
||||
<Message
|
||||
message={m}
|
||||
key={i} index={i}
|
||||
isLastUser={i === lastUserId} isLastAssistant={i === lastAssistantId}
|
||||
onNeedScroll={i === ms.length - 1 ? handleScroll : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useCallback, useContext, useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
||||
|
||||
import styles from './header.module.css';
|
||||
import { Connection, HORDE_ANON_KEY, isHordeConnection, isKoboldConnection, type IConnection, type IHordeModel } from '../../connection';
|
||||
import { Instruct, StateContext } from '../../contexts/state';
|
||||
import { Instruct } from '../../contexts/state';
|
||||
import { useInputState } from '@common/hooks/useInputState';
|
||||
import { useInputCallback } from '@common/hooks/useInputCallback';
|
||||
import { Huggingface } from '../../huggingface';
|
||||
|
|
@ -17,7 +17,7 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => {
|
|||
const [apiKey, setApiKey] = useInputState(HORDE_ANON_KEY);
|
||||
const [modelName, setModelName] = useInputState('');
|
||||
|
||||
const [modelTemplate, setModelTemplate] = useInputState(Instruct.CHATML);
|
||||
const [modelTemplate, setModelTemplate] = useInputState('');
|
||||
const [hordeModels, setHordeModels] = useState<IHordeModel[]>([]);
|
||||
const [contextLength, setContextLength] = useState<number>(0);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,18 +5,20 @@ import { DOMTools } from "../../dom";
|
|||
|
||||
import styles from './message.module.css';
|
||||
import { AutoTextarea } from "../autoTextarea";
|
||||
import { useInputState } from "@common/hooks/useInputState";
|
||||
|
||||
interface IProps {
|
||||
message: IMessage;
|
||||
index: number;
|
||||
isLastUser: boolean;
|
||||
isLastAssistant: boolean;
|
||||
onNeedScroll?: () => void;
|
||||
}
|
||||
|
||||
export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps) => {
|
||||
export const Message = ({ message, index, isLastUser, isLastAssistant, onNeedScroll }: IProps) => {
|
||||
const { messages, editMessage, editSummary, deleteMessage, setCurrentSwipe, setMessages } = useContext(StateContext);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [editedMessage, setEditedMessage] = useState('');
|
||||
const [editedMessage, setEditedMessage] = useInputState('');
|
||||
const textRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const swipe = useMemo(() => MessageTools.getSwipe(message), [message]);
|
||||
|
|
@ -25,10 +27,14 @@ export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps)
|
|||
const summary = swipe?.summary;
|
||||
const htmlContent = useMemo(() => MessageTools.format(content ?? ''), [content]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => onNeedScroll?.(), 50);
|
||||
}, [editedMessage, editing]);
|
||||
|
||||
const handleEnableEdit = useCallback(() => {
|
||||
setEditing(true);
|
||||
setEditedMessage(content ?? '');
|
||||
}, [content]);
|
||||
}, [content, onNeedScroll]);
|
||||
|
||||
const handleSaveEdit = useCallback(() => {
|
||||
editMessage(index, editedMessage.trim());
|
||||
|
|
@ -54,13 +60,6 @@ export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps)
|
|||
}
|
||||
}, [messages, setMessages, index]);
|
||||
|
||||
const handleEdit = useCallback((e: InputEvent) => {
|
||||
if (e.target instanceof HTMLTextAreaElement) {
|
||||
const newContent = e.target.value;
|
||||
setEditedMessage(newContent);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSwipeLeft = useCallback(() => {
|
||||
setCurrentSwipe(index, message.currentSwipe - 1);
|
||||
DOMTools.animate(textRef.current, 'swipe-from-left');
|
||||
|
|
@ -75,34 +74,32 @@ export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps)
|
|||
<div class={`${styles.message} ${styles[message.role]} ${isLastUser ? styles.lastUser : ''}`}>
|
||||
<div class={styles.content}>
|
||||
{editing
|
||||
? <AutoTextarea onInput={handleEdit} value={editedMessage} />
|
||||
? <AutoTextarea onInput={setEditedMessage} value={editedMessage} />
|
||||
: <>
|
||||
<div class={styles.text} dangerouslySetInnerHTML={{ __html: htmlContent }} ref={textRef} />
|
||||
{summary && <small class={styles.summary}>{summary}</small>}
|
||||
</>
|
||||
}
|
||||
{(isLastUser || message.role === 'assistant') &&
|
||||
<div class={styles.buttons}>
|
||||
{editing
|
||||
? <>
|
||||
<button class='icon' onClick={handleSaveEdit} title='Save'>✔</button>
|
||||
<button class='icon' onClick={handleDeleteMessage} title='Delete'>🗑️</button>
|
||||
<button class='icon' onClick={handleStopHere} title='Stop here'>⛔</button>
|
||||
<button class='icon' onClick={handleCancelEdit} title='Cancel'>❌</button>
|
||||
</>
|
||||
: <>
|
||||
{isLastAssistant &&
|
||||
<div class={styles.swipes}>
|
||||
<div onClick={handleSwipeLeft}>◀</div>
|
||||
<div>{message.currentSwipe + 1}/{message.swipes.length}</div>
|
||||
<div onClick={handleSwipeRight}>▶</div>
|
||||
</div>
|
||||
}
|
||||
<button class='icon' onClick={handleEnableEdit} title="Edit">🖊</button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class={styles.buttons}>
|
||||
{editing
|
||||
? <>
|
||||
<button class='icon' onClick={handleSaveEdit} title='Save'>✔</button>
|
||||
<button class='icon' onClick={handleDeleteMessage} title='Delete'>🗑️</button>
|
||||
<button class='icon' onClick={handleStopHere} title='Stop here'>⛔</button>
|
||||
<button class='icon' onClick={handleCancelEdit} title='Cancel'>❌</button>
|
||||
</>
|
||||
: <>
|
||||
{isLastAssistant &&
|
||||
<div class={styles.swipes}>
|
||||
<div onClick={handleSwipeLeft}>◀</div>
|
||||
<div>{message.currentSwipe + 1}/{message.swipes.length}</div>
|
||||
<div onClick={handleSwipeRight}>▶</div>
|
||||
</div>
|
||||
}
|
||||
<button class='icon' onClick={handleEnableEdit} title="Edit">🖊</button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,12 +79,12 @@ Continue the story forward.
|
|||
{%- endif %}
|
||||
|
||||
{% if prompt -%}
|
||||
What should happen next in your answer: {{ prompt | trim }}
|
||||
This is the description of What should happen next in your answer: {{ prompt | trim }}
|
||||
{% endif %}
|
||||
Remember that this story should be infinite and go forever.
|
||||
Make sure to follow the world description and rules exactly. Avoid cliffhangers and pauses, be creative.`,
|
||||
summarizePrompt: 'Shrink following text down to one paragraph, keeping all important details:\n\n{{ message }}\n\nAnswer with shortened text only.',
|
||||
summaryEnabled: false,
|
||||
summarizePrompt: 'Summarize following text in one paragraph:\n\n{{ message }}\n\nAnswer with shortened text only.',
|
||||
summaryEnabled: true,
|
||||
bannedWords: [],
|
||||
messages: [],
|
||||
triggerNext: false,
|
||||
|
|
|
|||
|
|
@ -20,4 +20,123 @@ export namespace DOMTools {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const HIDDEN_TEXTAREA_STYLE = `
|
||||
min-height:0 !important;
|
||||
max-height:none !important;
|
||||
height:0 !important;
|
||||
visibility:hidden !important;
|
||||
overflow:hidden !important;
|
||||
position:absolute !important;
|
||||
z-index:-1000 !important;
|
||||
top:0 !important;
|
||||
right:0 !important
|
||||
`;
|
||||
|
||||
const SIZING_STYLE = [
|
||||
'letter-spacing',
|
||||
'line-height',
|
||||
'padding-top',
|
||||
'padding-bottom',
|
||||
'font-family',
|
||||
'font-weight',
|
||||
'font-size',
|
||||
'text-rendering',
|
||||
'text-transform',
|
||||
'width',
|
||||
'text-indent',
|
||||
'padding-left',
|
||||
'padding-right',
|
||||
'border-width',
|
||||
'box-sizing'
|
||||
];
|
||||
|
||||
let hiddenTextarea: HTMLTextAreaElement;
|
||||
|
||||
export const calculateNodeHeight = (uiTextNode: HTMLTextAreaElement, minRows = null, maxRows = null) => {
|
||||
if (!hiddenTextarea) {
|
||||
hiddenTextarea = document.createElement('textarea');
|
||||
document.body.appendChild(hiddenTextarea);
|
||||
}
|
||||
|
||||
// Copy all CSS properties that have an impact on the height of the content in
|
||||
// the textbox
|
||||
let {
|
||||
paddingSize, borderSize,
|
||||
boxSizing, sizingStyle
|
||||
} = calculateNodeStyling(uiTextNode);
|
||||
|
||||
// Need to have the overflow attribute to hide the scrollbar otherwise
|
||||
// text-lines will not calculated properly as the shadow will technically be
|
||||
// narrower for content
|
||||
hiddenTextarea.setAttribute('style', sizingStyle + ';' + HIDDEN_TEXTAREA_STYLE);
|
||||
hiddenTextarea.value = uiTextNode.value || uiTextNode.placeholder || 'x';
|
||||
|
||||
let minHeight = -Infinity;
|
||||
let maxHeight = Infinity;
|
||||
let height = hiddenTextarea.scrollHeight;
|
||||
|
||||
if (boxSizing === 'border-box') {
|
||||
// border-box: add border, since height = content + padding + border
|
||||
height = height + borderSize;
|
||||
} else if (boxSizing === 'content-box') {
|
||||
// remove padding, since height = content
|
||||
height = height - paddingSize;
|
||||
}
|
||||
|
||||
if (minRows !== null || maxRows !== null) {
|
||||
// measure height of a textarea with a single row
|
||||
hiddenTextarea.value = 'x';
|
||||
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
|
||||
if (minRows !== null) {
|
||||
minHeight = singleRowHeight * minRows;
|
||||
if (boxSizing === 'border-box') {
|
||||
minHeight = minHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.max(minHeight, height);
|
||||
}
|
||||
if (maxRows !== null) {
|
||||
maxHeight = singleRowHeight * maxRows;
|
||||
if (boxSizing === 'border-box') {
|
||||
maxHeight = maxHeight + paddingSize + borderSize;
|
||||
}
|
||||
height = Math.min(maxHeight, height);
|
||||
}
|
||||
}
|
||||
return { height, minHeight, maxHeight };
|
||||
}
|
||||
|
||||
function calculateNodeStyling(node: HTMLElement) {
|
||||
let style = window.getComputedStyle(node);
|
||||
|
||||
let boxSizing = (
|
||||
style.getPropertyValue('box-sizing') ||
|
||||
style.getPropertyValue('-moz-box-sizing') ||
|
||||
style.getPropertyValue('-webkit-box-sizing')
|
||||
);
|
||||
|
||||
let paddingSize = (
|
||||
parseFloat(style.getPropertyValue('padding-bottom')) +
|
||||
parseFloat(style.getPropertyValue('padding-top'))
|
||||
);
|
||||
|
||||
let borderSize = (
|
||||
parseFloat(style.getPropertyValue('border-bottom-width')) +
|
||||
parseFloat(style.getPropertyValue('border-top-width'))
|
||||
);
|
||||
|
||||
let sizingStyle = SIZING_STYLE
|
||||
.map(name => `${name}:${style.getPropertyValue(name)}`)
|
||||
.join(';');
|
||||
|
||||
let nodeInfo = {
|
||||
sizingStyle,
|
||||
paddingSize,
|
||||
borderSize,
|
||||
boxSizing
|
||||
};
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue