Commit 9f6573c0 authored by Kantz's avatar Kantz
Browse files

error with parial loss of message fixed

parent 3f8118b0
......@@ -8,7 +8,7 @@ type ChatWindowProps = {
messages: ChatMessage[];
draft: string;
onDraftChange: (value: string) => void;
onSend: () => void;
onSend: (value: string) => void;
onTip?: () => void;
onRevealSolution?: () => void;
isSending?: boolean;
......
import { useEffect, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { EditPencil, Send, Upload } from "iconoir-react";
import { t } from "../../i18n";
import "mathquill/build/mathquill.css";
......@@ -6,7 +6,7 @@ import "mathquill/build/mathquill.css";
type MessageInputProps = {
value: string;
onChange: (value: string) => void;
onSend: () => void;
onSend: (value: string) => void;
onTip?: () => void;
onRevealSolution?: () => void;
isSending?: boolean;
......@@ -42,6 +42,23 @@ type MathQuillGlobal = {
getInterface: (version: number) => MathQuillInterface;
};
type TextPart = {
type: "text";
value: string;
};
type MathPart = {
type: "math";
latex: string;
};
type Part = TextPart | MathPart;
type CaretPosition = {
index: number;
offset: number;
};
declare global {
interface Window {
$?: unknown;
......@@ -50,13 +67,11 @@ declare global {
}
}
const INLINE_MATH_SELECTOR = ".inline-math, .inline-math-render";
const ZERO_WIDTH_SPACE = "\u200b";
const MATH_SEGMENT_PATTERN = /(\$[^$\n]+\$)/g;
let mathQuillLoader: Promise<MathQuillInterface> | null = null;
const activeMathFields = new WeakMap<HTMLElement, MathFieldLike>();
const emptyParts = (): Part[] => [{ type: "text", value: "" }];
const loadMathQuill = async (): Promise<MathQuillInterface> => {
if (!mathQuillLoader) {
......@@ -79,161 +94,155 @@ const loadMathQuill = async (): Promise<MathQuillInterface> => {
return mathQuillLoader;
};
const isSelectionInside = (container: HTMLElement, selection: Selection | null) => {
if (!selection || selection.rangeCount === 0) {
return false;
}
return container.contains(selection.anchorNode);
};
const stringifyParts = (parts: Part[]) =>
parts
.map((part) => (part.type === "math" ? `$${part.latex}$` : part.value))
.join("");
const placeCaretAfter = (node: Node) => {
const selection = window.getSelection();
if (!selection) {
return;
const normalizeParts = (parts: Part[]): Part[] => {
if (!parts.length) {
return emptyParts();
}
const range = document.createRange();
range.setStartAfter(node);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
};
const placeCaretAtStart = (container: HTMLElement) => {
const selection = window.getSelection();
if (!selection) {
return;
}
const range = document.createRange();
range.selectNodeContents(container);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
};
const normalized: Part[] = [];
const insertLineBreakAtCaret = (editor: HTMLElement) => {
const selection = window.getSelection();
if (!selection) {
return;
}
if (!isSelectionInside(editor, selection)) {
editor.focus();
}
const pushText = (value: string) => {
const last = normalized[normalized.length - 1];
if (last?.type === "text") {
last.value += value;
return;
}
normalized.push({ type: "text", value });
};
const activeSelection = window.getSelection();
if (!activeSelection || activeSelection.rangeCount === 0) {
return;
}
parts.forEach((part, partIndex) => {
if (part.type === "text") {
pushText(part.value);
return;
}
const range = activeSelection.getRangeAt(0);
range.deleteContents();
const previous = normalized[normalized.length - 1];
if (!previous || previous.type !== "text") {
normalized.push({ type: "text", value: "" });
}
const br = document.createElement("br");
const spacer = document.createTextNode(ZERO_WIDTH_SPACE);
range.insertNode(spacer);
range.insertNode(br);
normalized.push(part);
const nextRange = document.createRange();
nextRange.setStart(spacer, spacer.textContent?.length ?? 0);
nextRange.collapse(true);
activeSelection.removeAllRanges();
activeSelection.addRange(nextRange);
};
const nextInput = parts[partIndex + 1];
if (!nextInput || nextInput.type !== "text") {
normalized.push({ type: "text", value: "" });
}
});
const isCaretAtStart = (editor: HTMLElement) => {
const selection = window.getSelection();
if (!selection || !selection.isCollapsed || !isSelectionInside(editor, selection)) {
return false;
if (!normalized.length) {
return emptyParts();
}
const range = selection.getRangeAt(0).cloneRange();
const prefixRange = document.createRange();
prefixRange.selectNodeContents(editor);
prefixRange.setEnd(range.startContainer, range.startOffset);
if (normalized[0].type !== "text") {
normalized.unshift({ type: "text", value: "" });
}
if (normalized[normalized.length - 1].type !== "text") {
normalized.push({ type: "text", value: "" });
}
const fragment = prefixRange.cloneContents();
if (fragment.querySelector(INLINE_MATH_SELECTOR)) {
return false;
return normalized;
};
const partsFromValue = (value: string): Part[] => {
const parts: Part[] = [];
let lastIndex = 0;
for (const match of value.matchAll(MATH_SEGMENT_PATTERN)) {
const fullMatch = match[0];
const index = match.index ?? 0;
parts.push({ type: "text", value: value.slice(lastIndex, index) });
parts.push({ type: "math", latex: fullMatch.slice(1, -1) });
lastIndex = index + fullMatch.length;
}
return prefixRange.toString().replaceAll(ZERO_WIDTH_SPACE, "").length === 0;
parts.push({ type: "text", value: value.slice(lastIndex) });
return normalizeParts(parts);
};
const serializeEditor = (root: HTMLElement) => {
const parts: string[] = [];
const getSelectionOffsets = (element: HTMLElement) => {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || !element.contains(selection.anchorNode)) {
const length = element.textContent?.length ?? 0;
return { start: length, end: length };
}
const walk = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
parts.push((node.textContent || "").replaceAll(ZERO_WIDTH_SPACE, ""));
return;
}
const range = selection.getRangeAt(0);
const startRange = range.cloneRange();
startRange.selectNodeContents(element);
startRange.setEnd(range.startContainer, range.startOffset);
if (!(node instanceof HTMLElement)) {
return;
}
const endRange = range.cloneRange();
endRange.selectNodeContents(element);
endRange.setEnd(range.endContainer, range.endOffset);
if (node.matches(".inline-math-render")) {
parts.push(`$${node.dataset.latex || ""}$`);
return;
}
return {
start: startRange.toString().length,
end: endRange.toString().length,
};
};
if (node.matches(".inline-math")) {
const field = activeMathFields.get(node);
const latex = field?.latex() || node.dataset.latex || "";
parts.push(`$${latex}$`);
return;
}
const setCaretOffset = (element: HTMLElement, offset: number) => {
const selection = window.getSelection();
if (!selection) {
return;
}
if (node.tagName === "BR") {
parts.push("\n");
const range = document.createRange();
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
let remaining = offset;
let currentNode = walker.nextNode();
while (currentNode) {
const textLength = currentNode.textContent?.length ?? 0;
if (remaining <= textLength) {
range.setStart(currentNode, remaining);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
return;
}
remaining -= textLength;
currentNode = walker.nextNode();
}
Array.from(node.childNodes).forEach(walk);
};
Array.from(root.childNodes).forEach(walk);
return parts.join("");
range.selectNodeContents(element);
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
};
const createRenderedMathNode = (MQ: MathQuillInterface, latex: string) => {
const rendered = document.createElement("span");
rendered.className = "inline-math-render";
rendered.dataset.latex = latex;
rendered.tabIndex = 0;
rendered.textContent = latex || "\\square";
MQ.StaticMath(rendered);
return rendered;
const findNearestTextIndex = (parts: Part[], start: number, direction: -1 | 1) => {
let index = start;
while (index >= 0 && index < parts.length) {
if (parts[index]?.type === "text") {
return index;
}
index += direction;
}
return direction > 0 ? parts.length - 1 : 0;
};
const renderValueIntoEditor = (editor: HTMLElement, MQ: MathQuillInterface, value: string) => {
editor.innerHTML = "";
const appendText = (text: string) => {
const lines = text.split("\n");
lines.forEach((line, lineIndex) => {
if (line) {
editor.appendChild(document.createTextNode(line));
}
if (lineIndex < lines.length - 1) {
editor.appendChild(document.createElement("br"));
}
});
const clampCaret = (parts: Part[], caret: CaretPosition): CaretPosition => {
const index = findNearestTextIndex(parts, caret.index, caret.index >= parts.length ? -1 : 1);
const part = parts[index];
const maxOffset = part?.type === "text" ? part.value.length : 0;
return {
index,
offset: Math.max(0, Math.min(caret.offset, maxOffset)),
};
let lastIndex = 0;
for (const match of value.matchAll(MATH_SEGMENT_PATTERN)) {
const fullMatch = match[0];
const index = match.index ?? 0;
appendText(value.slice(lastIndex, index));
editor.appendChild(createRenderedMathNode(MQ, fullMatch.slice(1, -1)));
lastIndex = index + fullMatch.length;
}
appendText(value.slice(lastIndex));
};
const exitMathField = (editor: HTMLElement, node: Node) => {
editor.focus();
placeCaretAfter(node);
const isCaretAtStart = (parts: Part[], index: number, element: HTMLElement) => {
const { start, end } = getSelectionOffsets(element);
if (start !== 0 || end !== 0) {
return false;
}
return stringifyParts(parts.slice(0, index)).length === 0;
};
export default function MessageInput({
......@@ -247,194 +256,233 @@ export default function MessageInput({
onToggleCanvas,
onUploadSolution,
}: MessageInputProps) {
const canSend = value.trim().length > 0;
const uploadInputRef = useRef<HTMLInputElement | null>(null);
const editorRef = useRef<HTMLDivElement | null>(null);
const textPartRefs = useRef(new Map<number, HTMLSpanElement>());
const staticMathRefs = useRef(new Map<number, HTMLSpanElement>());
const activeMathHostRef = useRef<HTMLSpanElement | null>(null);
const activeFieldRef = useRef<MathFieldLike | null>(null);
const mqRef = useRef<MathQuillInterface | null>(null);
const wasSendingRef = useRef(isSending);
const shouldRestoreFocusRef = useRef(false);
const sendButtonStartedFromEditorRef = useRef(false);
const currentValueRef = useRef(value);
const mqRef = useRef<MathQuillInterface | null>(null);
const [isMathBoxActive, setIsMathBoxActive] = useState(false);
const pendingCaretRef = useRef<CaretPosition | null>(null);
const selectionRef = useRef<CaretPosition>({ index: 0, offset: 0 });
const activeMathIndexRef = useRef<number | null>(null);
const partsRef = useRef<Part[]>(partsFromValue(value));
const [parts, setParts] = useState<Part[]>(partsRef.current);
const [activeMathIndex, setActiveMathIndex] = useState<number | null>(null);
const message = stringifyParts(parts);
const canSend = message.trim().length > 0;
const commitParts = (
nextPartsInput: Part[],
options: {
emit?: boolean;
caret?: CaretPosition | null;
activeMath?: number | null;
} = {}
) => {
const previousMessage = stringifyParts(partsRef.current);
const nextParts = normalizeParts(nextPartsInput);
const nextMessage = stringifyParts(nextParts);
partsRef.current = nextParts;
setParts(nextParts);
if (options.caret) {
pendingCaretRef.current = clampCaret(nextParts, options.caret);
}
const syncEditorToValue = async (nextValue: string) => {
const editor = editorRef.current;
if (!editor) {
if (options.activeMath !== undefined) {
activeMathIndexRef.current = options.activeMath;
setActiveMathIndex(options.activeMath);
}
if (options.emit !== false && nextMessage !== previousMessage) {
onChange(nextMessage);
}
};
const finalizeActiveMathField = () => {
const field = activeFieldRef.current;
const index = activeMathIndexRef.current;
if (!field || index === null) {
return;
}
const MQ = mqRef.current ?? (await loadMathQuill());
mqRef.current = MQ;
renderValueIntoEditor(editor, MQ, nextValue);
const latex = field.latex();
const nextParts = [...partsRef.current];
nextParts[index] = latex ? { type: "math", latex } : { type: "text", value: "" };
const caret = latex ? { index: index + 1, offset: 0 } : { index, offset: 0 };
activeFieldRef.current = null;
commitParts(nextParts, { caret, activeMath: null });
};
const commitEditorValue = () => {
useEffect(() => {
activeMathIndexRef.current = activeMathIndex;
}, [activeMathIndex]);
useEffect(() => {
partsRef.current = parts;
}, [parts]);
useEffect(() => {
const editor = editorRef.current;
if (!editor) {
return;
}
const nextValue = serializeEditor(editor);
if (nextValue === currentValueRef.current) {
if (value === stringifyParts(partsRef.current)) {
return;
}
currentValueRef.current = nextValue;
onChange(nextValue);
};
const wireMathField = async (element: HTMLElement, latex: string) => {
const editor = editorRef.current;
if (!editor) {
return null;
if (activeMathIndexRef.current !== null || editor.contains(document.activeElement)) {
return;
}
const MQ = mqRef.current ?? (await loadMathQuill());
mqRef.current = MQ;
const nextParts = partsFromValue(value);
partsRef.current = nextParts;
setParts(nextParts);
}, [value]);
let field: MathFieldLike | null = null;
const finalizeMathField = () => {
if (!field) {
return null;
}
const fieldElement = field.el();
if (!fieldElement.isConnected) {
return null;
useEffect(() => {
let cancelled = false;
const renderStaticMath = async () => {
const MQ = mqRef.current ?? (await loadMathQuill());
if (cancelled) {
return;
}
const rendered = createRenderedMathNode(MQ, field.latex());
rendered.addEventListener("click", () => {
void activateRenderedMath(rendered);
});
rendered.addEventListener("keydown", (event) => {
if (event.key !== "Enter" && event.key !== " ") {
mqRef.current = MQ;
parts.forEach((part, index) => {
if (part.type !== "math" || index === activeMathIndex) {
return;
}
event.preventDefault();
void activateRenderedMath(rendered);
const node = staticMathRefs.current.get(index);
if (!node || node.dataset.renderedLatex === part.latex) {
return;
}
node.dataset.latex = part.latex;
node.dataset.renderedLatex = part.latex;
node.textContent = part.latex || "\\square";
MQ.StaticMath(node);
});
fieldElement.replaceWith(rendered);
activeMathFields.delete(fieldElement);
field.revert();
setIsMathBoxActive(false);
commitEditorValue();
return rendered;
};
field = MQ.MathField(element, {
handlers: {
edit: (mathField) => {
mathField.el().dataset.empty = mathField.latex() ? "false" : "true";
commitEditorValue();
},
enter: () => {
const rendered = finalizeMathField();
if (rendered) {
exitMathField(editor, rendered);
}
},
},
});
void renderStaticMath();
activeMathFields.set(element, field);
field.latex(latex);
element.dataset.latex = latex;
element.dataset.empty = latex ? "false" : "true";
return () => {
cancelled = true;
};
}, [parts, activeMathIndex]);
const root = field.el();
root.addEventListener("focusin", () => {
setIsMathBoxActive(true);
});
root.addEventListener("keydown", (event) => {
if (event.key !== "Enter") {
return;
}
event.preventDefault();
const rendered = finalizeMathField();
if (rendered) {
exitMathField(editor, rendered);
}
});
root.addEventListener("focusout", () => {
useEffect(() => {
if (activeMathIndex === null) {
return;
}
const host = activeMathHostRef.current;
if (!host) {
return;
}
let disposed = false;
let field: MathFieldLike | null = null;
const handleFocusOut = () => {
requestAnimationFrame(() => {
if (root.contains(document.activeElement)) {
if (host.contains(document.activeElement)) {
return;
}
setIsMathBoxActive(false);
finalizeMathField();
finalizeActiveMathField();
});
});
return field;
};
const activateRenderedMath = async (rendered: HTMLElement) => {
const editor = editorRef.current;
if (!editor) {
return null;
}
const mathfield = document.createElement("span");
mathfield.className = "inline-math";
mathfield.dataset.latex = rendered.dataset.latex || "";
rendered.replaceWith(mathfield);
const field = await wireMathField(mathfield, rendered.dataset.latex || "");
requestAnimationFrame(() => field?.focus());
return field;
};
};
const insertMathFieldAtCaret = async () => {
const editor = editorRef.current;
const selection = window.getSelection();
if (!editor || !selection) {
return null;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== "Enter" && event.key !== "$") {
return;
}
event.preventDefault();
event.stopPropagation();
finalizeActiveMathField();
};
if (!isSelectionInside(editor, selection)) {
editor.focus();
placeCaretAtStart(editor);
}
void (async () => {
const MQ = mqRef.current ?? (await loadMathQuill());
if (disposed) {
return;
}
const activeSelection = window.getSelection();
if (!activeSelection || activeSelection.rangeCount === 0) {
return null;
}
mqRef.current = MQ;
field = MQ.MathField(host, {
handlers: {
edit: (mathField) => {
const nextLatex = mathField.latex();
mathField.el().dataset.empty = nextLatex ? "false" : "true";
mathField.el().dataset.latex = nextLatex;
},
enter: () => {
finalizeActiveMathField();
},
},
});
const range = activeSelection.getRangeAt(0);
range.deleteContents();
activeFieldRef.current = field;
const mathfield = document.createElement("span");
mathfield.className = "inline-math";
const spacer = document.createTextNode("\u00a0");
range.insertNode(spacer);
range.insertNode(mathfield);
const part = partsRef.current[activeMathIndex];
const latex = part?.type === "math" ? part.latex : "";
field.latex(latex);
host.dataset.empty = latex ? "false" : "true";
host.dataset.latex = latex;
host.addEventListener("focusout", handleFocusOut);
host.addEventListener("keydown", handleKeyDown);
requestAnimationFrame(() => field?.focus());
})();
const field = await wireMathField(mathfield, "");
commitEditorValue();
requestAnimationFrame(() => field?.focus());
return field;
};
return () => {
disposed = true;
activeFieldRef.current = null;
if (field) {
host.removeEventListener("focusout", handleFocusOut);
host.removeEventListener("keydown", handleKeyDown);
field.revert();
}
};
}, [activeMathIndex]);
useEffect(() => {
currentValueRef.current = value;
}, [value]);
useLayoutEffect(() => {
if (activeMathIndex !== null) {
return;
}
useEffect(() => {
const editor = editorRef.current;
if (!editor) {
const pendingCaret = pendingCaretRef.current;
if (!pendingCaret) {
return;
}
const currentSerialized = serializeEditor(editor);
if (currentSerialized === value) {
const target = textPartRefs.current.get(pendingCaret.index);
if (!target) {
return;
}
void syncEditorToValue(value);
}, [value]);
target.focus();
setCaretOffset(target, pendingCaret.offset);
selectionRef.current = pendingCaret;
pendingCaretRef.current = null;
}, [parts, activeMathIndex]);
useEffect(() => {
if (wasSendingRef.current && !isSending) {
if (shouldRestoreFocusRef.current) {
editorRef.current?.focus({ preventScroll: true });
const target = textPartRefs.current.get(selectionRef.current.index);
target?.focus({ preventScroll: true });
}
shouldRestoreFocusRef.current = false;
}
......@@ -455,9 +503,82 @@ export default function MessageInput({
await onUploadSolution?.(file);
};
const handleSend = (restoreFocus = document.activeElement === editorRef.current) => {
shouldRestoreFocusRef.current = restoreFocus;
onSend();
const updateSelection = (index: number, element: HTMLElement) => {
const { end } = getSelectionOffsets(element);
selectionRef.current = { index, offset: end };
};
const handleTextInput = (index: number, element: HTMLElement) => {
const nextParts = [...partsRef.current];
const current = nextParts[index];
if (!current || current.type !== "text") {
return;
}
const { end } = getSelectionOffsets(element);
current.value = element.textContent ?? "";
commitParts(nextParts, { caret: { index, offset: end } });
};
const replaceTextSelection = (index: number, text: string) => {
const element = textPartRefs.current.get(index);
const current = partsRef.current[index];
if (!element || !current || current.type !== "text") {
return;
}
const { start, end } = getSelectionOffsets(element);
const nextValue = `${current.value.slice(0, start)}${text}${current.value.slice(end)}`;
const nextParts = [...partsRef.current];
nextParts[index] = { type: "text", value: nextValue };
commitParts(nextParts, { caret: { index, offset: start + text.length } });
};
const insertMathAtSelection = (index?: number) => {
const fallbackIndex = findNearestTextIndex(partsRef.current, partsRef.current.length - 1, -1);
const targetIndex = index ?? selectionRef.current.index ?? fallbackIndex;
const element = textPartRefs.current.get(targetIndex);
const current = partsRef.current[targetIndex];
if (!current || current.type !== "text") {
return;
}
const selection = element ? getSelectionOffsets(element) : { start: current.value.length, end: current.value.length };
const before = current.value.slice(0, selection.start);
const after = current.value.slice(selection.end);
const nextParts = [
...partsRef.current.slice(0, targetIndex),
{ type: "text", value: before } as TextPart,
{ type: "math", latex: "" } as MathPart,
{ type: "text", value: after } as TextPart,
...partsRef.current.slice(targetIndex + 1),
];
commitParts(nextParts, { activeMath: targetIndex + 1, emit: false });
};
const activateMathPart = (index: number) => {
if (isSending) {
return;
}
commitParts(partsRef.current, { activeMath: index, emit: false });
};
const handleSend = () => {
finalizeActiveMathField();
const nextMessage = stringifyParts(partsRef.current);
if (!nextMessage.trim()) {
return;
}
shouldRestoreFocusRef.current = editorRef.current?.contains(document.activeElement) ?? false;
onSend(nextMessage);
commitParts(emptyParts(), {
caret: { index: 0, offset: 0 },
emit: true,
activeMath: null,
});
};
return (
......@@ -514,12 +635,12 @@ export default function MessageInput({
{t("revealSolution")}
</button>
) : null}
{!isMathBoxActive ? (
{activeMathIndex === null ? (
<button
className="btn btn-formula"
type="button"
onClick={() => {
void insertMathFieldAtCaret();
insertMathAtSelection();
}}
disabled={isSending}
aria-label={t("formula")}
......@@ -532,10 +653,10 @@ export default function MessageInput({
className="btn primary"
type="button"
onPointerDown={() => {
sendButtonStartedFromEditorRef.current = document.activeElement === editorRef.current;
sendButtonStartedFromEditorRef.current = editorRef.current?.contains(document.activeElement) ?? false;
}}
onClick={() => {
handleSend(sendButtonStartedFromEditorRef.current);
handleSend();
sendButtonStartedFromEditorRef.current = false;
}}
disabled={!canSend || isSending}
......@@ -556,54 +677,125 @@ export default function MessageInput({
<div
ref={editorRef}
className={`composer-input composer-rich-input${isSending ? " composer-input-disabled" : ""}`}
contentEditable={!isSending}
suppressContentEditableWarning
data-placeholder={t("typeQuestionOrLatex")}
data-empty={message.length === 0 ? "true" : "false"}
aria-label={t("typeQuestionOrLatex")}
role="textbox"
aria-multiline="true"
onInput={() => {
commitEditorValue();
}}
onKeyDown={(event) => {
if (isSending) {
event.preventDefault();
return;
}
if (event.key === "$") {
event.preventDefault();
void insertMathFieldAtCaret();
return;
>
{parts.map((part, index) => {
if (part.type === "text") {
return (
<span
key={`text-${index}`}
ref={(node) => {
if (node) {
textPartRefs.current.set(index, node);
return;
}
textPartRefs.current.delete(index);
}}
className="composer-text-part"
contentEditable={!isSending && activeMathIndex === null}
suppressContentEditableWarning
data-part-index={index}
spellCheck={false}
onFocus={(event) => {
updateSelection(index, event.currentTarget);
}}
onMouseUp={(event) => {
updateSelection(index, event.currentTarget);
}}
onKeyUp={(event) => {
updateSelection(index, event.currentTarget);
}}
onInput={(event) => {
handleTextInput(index, event.currentTarget);
}}
onKeyDown={(event) => {
if (isSending) {
event.preventDefault();
return;
}
if (event.key === "$") {
event.preventDefault();
insertMathAtSelection(index);
return;
}
if (event.key === "ArrowUp" && isCaretAtStart(partsRef.current, index, event.currentTarget)) {
if (onHistoryNavigate?.("older")) {
event.preventDefault();
return;
}
}
if (event.key === "ArrowDown" && isCaretAtStart(partsRef.current, index, event.currentTarget)) {
if (onHistoryNavigate?.("newer")) {
event.preventDefault();
return;
}
}
if (event.key === "Enter" && event.shiftKey) {
event.preventDefault();
replaceTextSelection(index, "\n");
return;
}
if (event.key === "Enter") {
event.preventDefault();
handleSend();
}
}}
>
{part.value}
</span>
);
}
if (event.key === "ArrowUp" && isCaretAtStart(event.currentTarget)) {
if (onHistoryNavigate?.("older")) {
event.preventDefault();
return;
}
if (index === activeMathIndex) {
return (
<span
key={`math-active-${index}`}
ref={activeMathHostRef}
className="inline-math"
data-empty={part.latex ? "false" : "true"}
data-latex={part.latex}
contentEditable={false}
/>
);
}
if (event.key === "ArrowDown" && isCaretAtStart(event.currentTarget)) {
if (onHistoryNavigate?.("newer")) {
event.preventDefault();
return;
}
}
if (event.key === "Enter" && event.shiftKey) {
event.preventDefault();
insertLineBreakAtCaret(event.currentTarget);
commitEditorValue();
return;
}
if (event.key === "Enter") {
event.preventDefault();
handleSend(true);
}
}}
/>
return (
<span
key={`math-${index}`}
ref={(node) => {
if (node) {
staticMathRefs.current.set(index, node);
return;
}
staticMathRefs.current.delete(index);
}}
className="inline-math-render"
data-latex={part.latex}
contentEditable={false}
tabIndex={isSending ? -1 : 0}
onClick={() => {
activateMathPart(index);
}}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
event.preventDefault();
activateMathPart(index);
}}
/>
);
})}
</div>
</div>
);
}
......@@ -835,9 +835,9 @@ export default function ChatPage() {
setRetrievalLoading(false);
};
const handleSend = async () => {
const handleSend = async (value: string) => {
await sendMessage({
text: draft,
text: value,
clearDraft: true,
});
};
......
......@@ -586,6 +586,7 @@ body {
}
.composer-rich-input {
position: relative;
min-height: 92px;
line-height: 1.6;
white-space: pre-wrap;
......@@ -594,9 +595,20 @@ body {
cursor: text;
}
.composer-rich-input[data-empty="true"]::before,
.composer-rich-input:empty::before {
content: attr(data-placeholder);
color: #8b857b;
pointer-events: none;
}
.composer-text-part {
display: inline;
outline: none;
}
.composer-text-part:empty::before {
content: "\200b";
}
.composer-rich-input:focus {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment