Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/chat/input.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { CSSProperties, useEffect, useMemo, useRef, useState } from "react";6import { useIntl } from "react-intl";7import { useDebouncedCallback } from "use-debounce";8import { CSS, redux, useIsMountedRef } from "@cocalc/frontend/app-framework";9import MarkdownInput from "@cocalc/frontend/editors/markdown-input/multimode";10import { IS_MOBILE } from "@cocalc/frontend/feature";11import { SAVE_DEBOUNCE_MS } from "@cocalc/frontend/frame-editors/code-editor/const";12import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context";13import type { SyncDB } from "@cocalc/sync/editor/db";14import { SubmitMentionsRef } from "./types";1516interface Props {17on_send: (value: string) => void;18onChange: (string) => void;19syncdb: SyncDB | undefined;20// date:21// - ms since epoch of when this message was first sent22// - set to 0 for editing new message23// - set to -time (negative time) to respond to thread, where time is the time of ROOT message of the the thread.24date: number;25input?: string;26on_paste?: (e) => void;27height?: string;28submitMentionsRef?: SubmitMentionsRef;29fontSize?: number;30hideHelp?: boolean;31style?: CSSProperties;32cacheId?: string;33onFocus?: () => void;34onBlur?: () => void;35editBarStyle?: CSS;36placeholder?: string;37autoFocus?: boolean;38moveCursorToEndOfLine?: boolean;39}4041export default function ChatInput({42autoFocus,43cacheId,44date,45editBarStyle,46fontSize,47height,48hideHelp,49input: propsInput,50on_send,51onBlur,52onChange,53onFocus,54placeholder,55style,56submitMentionsRef,57syncdb,58moveCursorToEndOfLine,59}: Props) {60const intl = useIntl();61const onSendRef = useRef<Function>(on_send);62useEffect(() => {63onSendRef.current = on_send;64}, [on_send]);65const { project_id } = useFrameContext();66const sender_id = useMemo(67() => redux.getStore("account").get_account_id(),68[],69);70const controlRef = useRef<any>(null);71const [input, setInput] = useState<string>("");72useEffect(() => {73const dbInput = syncdb74?.get_one({75event: "draft",76sender_id,77date,78})79?.get("input");80// take version from syncdb if it is there; otherwise, version from input prop.81// the db version is used when you refresh your browser while editing, or scroll up and down82// thus unmounting and remounting the currently editing message (due to virtualization).83// See https://github.com/sagemathinc/cocalc/issues/641584const input = dbInput ?? propsInput;85setInput(input);86if (input?.trim() && moveCursorToEndOfLine) {87// have to wait until it's all rendered -- i hate code like this...88for (const n of [1, 10, 50]) {89setTimeout(() => {90controlRef.current?.moveCursorToEndOfLine();91}, n);92}93}94}, [date, sender_id, propsInput]);9596const currentInputRef = useRef<string>(input);97const saveOnUnmountRef = useRef<boolean>(true);98const isMountedRef = useIsMountedRef();99const lastSavedRef = useRef<string>(input);100const saveChat = useDebouncedCallback(101(input) => {102if (103syncdb == null ||104(!isMountedRef.current && !saveOnUnmountRef.current)105) {106return;107}108onChange(input);109lastSavedRef.current = input;110// also save to syncdb, so we have undo, etc.111// but definitely don't save (thus updating active) if112// the input didn't really change, since we use active for113// showing that a user is writing to other users.114const input0 = syncdb115.get_one({116event: "draft",117sender_id,118date,119})120?.get("input");121if (input0 != input) {122if (input0 == null && !input) {123// DO NOT save if you haven't written a draft before, and124// the draft we would save here would be empty, since that125// would lead to what humans would consider false notifications.126return;127}128syncdb.set({129event: "draft",130sender_id,131input,132date, // it's a primary key so can't use this to represent when user last edited this; use other date for editing past chats.133active: Date.now(),134});135syncdb.commit();136}137},138SAVE_DEBOUNCE_MS,139{140leading: true,141},142);143144useEffect(() => {145return () => {146if (!isMountedRef.current && !saveOnUnmountRef.current) {147return;148}149// save before unmounting. This is very important since if a new reply comes in,150// then the input component gets unmounted, then remounted BELOW the reply.151// Note: it is still slightly annoying, due to loss of focus... however, data152// loss is NOT ok, whereas loss of focus is.153const input = currentInputRef.current;154if (!input || syncdb == null) {155return;156}157if (158syncdb.get_one({159event: "draft",160sender_id,161date,162}) == null163) {164return;165}166syncdb.set({167event: "draft",168sender_id,169input,170date, // it's a primary key so can't use this to represent when user last edited this; use other date for editing past chats.171active: Date.now(),172});173syncdb.commit();174};175}, []);176177useEffect(() => {178if (syncdb == null) return;179const onSyncdbChange = () => {180const sender_id = redux.getStore("account").get_account_id();181const x = syncdb.get_one({182event: "draft",183sender_id,184date,185});186const input = x?.get("input") ?? "";187if (input != lastSavedRef.current) {188setInput(input);189currentInputRef.current = input;190lastSavedRef.current = input;191}192};193syncdb.on("change", onSyncdbChange);194return () => {195syncdb.removeListener("change", onSyncdbChange);196};197}, [syncdb]);198199function getPlaceholder(): string {200if (placeholder != null) return placeholder;201const have_llm = redux202.getStore("projects")203.hasLanguageModelEnabled(project_id);204return intl.formatMessage(205{206id: "chat.input.placeholder",207defaultMessage:208"Type a new message ({have_llm, select, true {chat with AI or } other {}}notify a collaborator by typing @)...",209},210{211have_llm,212},213);214}215216return (217<MarkdownInput218autoFocus={autoFocus}219saveDebounceMs={0}220onFocus={onFocus}221onBlur={onBlur}222cacheId={cacheId}223value={input}224controlRef={controlRef}225enableUpload={true}226enableMentions={true}227submitMentionsRef={submitMentionsRef}228onChange={(input) => {229currentInputRef.current = input;230/* BUG: in Markdown mode this stops getting231called after you paste in an image. It works232fine in Slate/Text mode. See233https://github.com/sagemathinc/cocalc/issues/7728234*/235setInput(input);236saveChat(input);237}}238onShiftEnter={(input) => {239setInput("");240saveChat("");241on_send(input);242}}243height={height}244placeholder={getPlaceholder()}245extraHelp={246IS_MOBILE247? "Click the date to edit chats."248: "Double click to edit chats."249}250fontSize={fontSize}251hideHelp={hideHelp}252style={style}253onUndo={() => {254saveChat.cancel();255syncdb?.undo();256}}257onRedo={() => {258saveChat.cancel();259syncdb?.redo();260}}261editBarStyle={editBarStyle}262overflowEllipsis={true}263/>264);265}266267268