Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 40 additions & 87 deletions app/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useDebouncedCallback } from "use-debounce";
import React, {
Fragment,
RefObject,
useCallback,
useEffect,
useMemo,
Expand Down Expand Up @@ -450,53 +449,12 @@ export function ChatAction(props: {
);
}

function useScrollToBottom(
scrollRef: RefObject<HTMLDivElement>,
detach: boolean = false,
messages: ChatMessage[],
) {
// for auto-scroll
const [autoScroll, setAutoScroll] = useState(true);
const scrollDomToBottom = useCallback(() => {
const dom = scrollRef.current;
if (dom) {
requestAnimationFrame(() => {
setAutoScroll(true);
dom.scrollTo(0, dom.scrollHeight);
});
}
}, [scrollRef]);

// auto scroll
useEffect(() => {
if (autoScroll && !detach) {
scrollDomToBottom();
}
});

// auto scroll when messages length changes
const lastMessagesLength = useRef(messages.length);
useEffect(() => {
if (messages.length > lastMessagesLength.current && !detach) {
scrollDomToBottom();
}
lastMessagesLength.current = messages.length;
}, [messages.length, detach, scrollDomToBottom]);

return {
scrollRef,
autoScroll,
setAutoScroll,
scrollDomToBottom,
};
}

export function ChatActions(props: {
uploadImage: () => void;
setAttachImages: (images: string[]) => void;
setUploading: (uploading: boolean) => void;
showPromptModal: () => void;
scrollToBottom: () => void;
scrollChatToBottom: () => void;
showPromptHints: () => void;
hitBottom: boolean;
uploading: boolean;
Expand Down Expand Up @@ -608,7 +566,7 @@ export function ChatActions(props: {
)}
{!props.hitBottom && (
<ChatAction
onClick={props.scrollToBottom}
onClick={props.scrollChatToBottom}
text={Locale.Chat.InputActions.ToBottom}
icon={<BottomIcon />}
/>
Expand Down Expand Up @@ -997,37 +955,12 @@ function _Chat() {

const [showExport, setShowExport] = useState(false);

const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler();
const scrollRef = useRef<HTMLDivElement>(null);
const isScrolledToBottom = scrollRef?.current
? Math.abs(
scrollRef.current.scrollHeight -
(scrollRef.current.scrollTop + scrollRef.current.clientHeight),
) <= 1
: false;
const isAttachWithTop = useMemo(() => {
const lastMessage = scrollRef.current?.lastElementChild as HTMLElement;
// if scrolllRef is not ready or no message, return false
if (!scrollRef?.current || !lastMessage) return false;
const topDistance =
lastMessage!.getBoundingClientRect().top -
scrollRef.current.getBoundingClientRect().top;
// leave some space for user question
return topDistance < 100;
}, [scrollRef?.current?.scrollHeight]);

const isTyping = userInput !== "";

// if user is typing, should auto scroll to bottom
// if user is not typing, should auto scroll to bottom only if already at bottom
const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(
scrollRef,
(isScrolledToBottom || isAttachWithTop) && !isTyping,
session.messages,
);

const [hitBottom, setHitBottom] = useState(true);
const isMobileScreen = useMobileScreen();
const navigate = useNavigate();
Expand Down Expand Up @@ -1104,23 +1037,27 @@ function _Chat() {

const doSubmit = (userInput: string) => {
if (userInput.trim() === "" && isEmpty(attachImages)) return;

const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) {
setUserInput("");
setPromptHints([]);
matchCommand.invoke();
return;
}

setIsLoading(true);
chatStore
.onUserInput(userInput, attachImages)
.then(() => setIsLoading(false));

chatStore.onUserInput(userInput, attachImages).then(() => {
setIsLoading(false);
autoScrollChatToBottom();
});

setAttachImages([]);
chatStore.setLastInput(userInput);
setUserInput("");
setPromptHints([]);
if (!isMobileScreen) inputRef.current?.focus();
setAutoScroll(true);
autoScrollChatToBottom();
};

const onPromptSelect = (prompt: RenderPrompt) => {
Expand Down Expand Up @@ -1420,14 +1357,33 @@ function _Chat() {
}

setHitBottom(isHitBottom);
setAutoScroll(isHitBottom);
};

function scrollToBottom() {
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
scrollDomToBottom();
function scrollChatToBottom() {
const dom = scrollRef.current;
if (dom) {
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
requestAnimationFrame(() => {
dom.scrollTo(0, dom.scrollHeight);
});
}
}

// scroll if auto-scroll is enabled in the settings
function autoScrollChatToBottom() {
if (config.enableAutoScroll) scrollChatToBottom();
}

// scroll to the bottom on mount
useEffect(() => {
scrollChatToBottom();
}, []);

// keep scroll the chat as it gets longer, but only if the chat is already scrolled to the bottom (sticky bottom)
useEffect(() => {
if (hitBottom) scrollChatToBottom();
});

// clear context index = context length + index in messages
const clearContextIndex =
(session.clearContextIndex ?? -1) >= 0
Expand Down Expand Up @@ -1775,10 +1731,7 @@ function _Chat() {
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
onMouseDown={() => inputRef.current?.blur()}
onTouchStart={() => {
inputRef.current?.blur();
setAutoScroll(false);
}}
onTouchStart={() => inputRef.current?.blur()}
>
{messages
// TODO
Expand Down Expand Up @@ -2050,7 +2003,7 @@ function _Chat() {
setAttachImages={setAttachImages}
setUploading={setUploading}
showPromptModal={() => setShowPromptModal(true)}
scrollToBottom={scrollToBottom}
scrollChatToBottom={scrollChatToBottom}
hitBottom={hitBottom}
uploading={uploading}
showPromptHints={() => {
Expand Down Expand Up @@ -2083,8 +2036,8 @@ function _Chat() {
onInput={(e) => onInput(e.currentTarget.value)}
value={userInput}
onKeyDown={onInputKeyDown}
onFocus={scrollToBottom}
onClick={scrollToBottom}
onFocus={autoScrollChatToBottom}
onClick={autoScrollChatToBottom}
onPaste={handlePaste}
rows={inputRows}
autoFocus={autoFocus}
Expand Down
17 changes: 17 additions & 0 deletions app/components/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,23 @@ export function Settings() {
}
></input>
</ListItem>
<ListItem
title={Locale.Settings.AutoScroll.Title}
subTitle={Locale.Settings.AutoScroll.SubTitle}
>
<input
aria-label={Locale.Settings.AutoScroll.Title}
type="checkbox"
checked={config.enableAutoScroll}
data-testid="enable-auto-scroll-checkbox"
onChange={(e) =>
updateConfig(
(config) =>
(config.enableAutoScroll = e.currentTarget.checked),
)
}
></input>
</ListItem>
</List>

<SyncItems />
Expand Down
5 changes: 5 additions & 0 deletions app/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ const ar: PartialLocaleType = {
Title: "توليد العنوان تلقائيًا",
SubTitle: "توليد عنوان مناسب بناءً على محتوى الدردشة",
},
AutoScroll: {
Title: "تفعيل التمرير التلقائي",
SubTitle:
"التمرير التلقائي للدردشة إلى الأسفل عند التركيز على منطقة النص أو إرسال الرسالة",
},
Sync: {
CloudState: "بيانات السحابة",
NotSyncYet: "لم يتم التزامن بعد",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/bn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ const bn: PartialLocaleType = {
Title: "স্বয়ংক্রিয় শিরোনাম জেনারেশন",
SubTitle: "চ্যাট কনটেন্টের ভিত্তিতে উপযুক্ত শিরোনাম তৈরি করুন",
},
AutoScroll: {
Title: "অটো স্ক্রল সক্ষম করুন",
SubTitle:
"টেক্সট এরিয়া ফোকাস বা মেসেজ সাবমিটে স্বয়ংক্রিয়ভাবে চ্যাট নিচে স্ক্রল করুন",
},
Sync: {
CloudState: "ক্লাউড ডেটা",
NotSyncYet: "এখনো সিঙ্ক করা হয়নি",
Expand Down
4 changes: 4 additions & 0 deletions app/locales/cn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ const cn = {
Title: "自动生成标题",
SubTitle: "根据对话内容生成合适的标题",
},
AutoScroll: {
Title: "启用自动滚动",
SubTitle: "在文本区域聚焦或提交消息时自动滚动聊天到底部",
},
Sync: {
CloudState: "云端数据",
NotSyncYet: "还没有进行过同步",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ const cs: PartialLocaleType = {
Title: "Automatické generování názvu",
SubTitle: "Generovat vhodný název na základě obsahu konverzace",
},
AutoScroll: {
Title: "Povolit automatické posouvání",
SubTitle:
"Automaticky posunout chat dolů při zaměření na textové pole nebo odeslání zprávy",
},
Sync: {
CloudState: "Data na cloudu",
NotSyncYet: "Ještě nebylo synchronizováno",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ const da: PartialLocaleType = {
Title: "Lav titel automatisk",
SubTitle: "Foreslå en titel ud fra chatten",
},
AutoScroll: {
Title: "Aktivér automatisk rulning",
SubTitle:
"Rul automatisk chatten til bunden ved fokus på tekstfelt eller afsendelse af besked",
},
Sync: {
CloudState: "Seneste opdatering",
NotSyncYet: "Endnu ikke synkroniseret",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ const de: PartialLocaleType = {
SubTitle:
"Basierend auf dem Chat-Inhalt einen passenden Titel generieren",
},
AutoScroll: {
Title: "Automatisches Scrollen aktivieren",
SubTitle:
"Chat automatisch nach unten scrollen bei Fokus auf Texteingabe oder Nachrichtensenden",
},
Sync: {
CloudState: "Cloud-Daten",
NotSyncYet: "Noch nicht synchronisiert",
Expand Down
7 changes: 6 additions & 1 deletion app/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ const en: LocaleType = {
Title: "Auto Generate Title",
SubTitle: "Generate a suitable title based on the conversation content",
},
AutoScroll: {
Title: "Enable Auto Scroll",
SubTitle:
"Automatically scroll chat to bottom on text area focus or message submit",
},
Sync: {
CloudState: "Last Update",
NotSyncYet: "Not sync yet",
Expand Down Expand Up @@ -756,7 +761,7 @@ const en: LocaleType = {
},
Artifacts: {
Title: "Enable Artifacts",
SubTitle: "Can render HTML page when enable artifacts.",
SubTitle: "Can render HTML page when enable artifacts",
},
CodeFold: {
Title: "Enable CodeFold",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ const es: PartialLocaleType = {
Title: "Generar título automáticamente",
SubTitle: "Generar un título adecuado basado en el contenido del chat",
},
AutoScroll: {
Title: "Habilitar desplazamiento automático",
SubTitle:
"Desplazar el chat automáticamente hacia abajo al enfocar el área de texto o enviar un mensaje",
},
Sync: {
CloudState: "Datos en la nube",
NotSyncYet: "Aún no se ha sincronizado",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ const fr: PartialLocaleType = {
SubTitle:
"Générer un titre approprié en fonction du contenu de la discussion",
},
AutoScroll: {
Title: "Activer le défilement automatique",
SubTitle:
"Faire défiler automatiquement le chat vers le bas lors du focus sur la zone de texte ou de l'envoi d'un message",
},
Sync: {
CloudState: "Données cloud",
NotSyncYet: "Pas encore synchronisé",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ const id: PartialLocaleType = {
Title: "Otomatis Membuat Judul",
SubTitle: "Membuat judul yang sesuai berdasarkan konten obrolan",
},
AutoScroll: {
Title: "Aktifkan Gulir Otomatis",
SubTitle:
"Secara otomatis gulir obrolan ke bawah saat area teks difokuskan atau pesan dikirim",
},
Sync: {
CloudState: "Data Cloud",
NotSyncYet: "Belum disinkronkan",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ const it: PartialLocaleType = {
SubTitle:
"Genera un titolo appropriato in base al contenuto della conversazione",
},
AutoScroll: {
Title: "Abilita lo scorrimento automatico",
SubTitle:
"Scorri automaticamente la chat in basso quando si seleziona l'area di testo o si invia un messaggio",
},
Sync: {
CloudState: "Dati cloud",
NotSyncYet: "Non è ancora avvenuta alcuna sincronizzazione",
Expand Down
5 changes: 5 additions & 0 deletions app/locales/jp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,11 @@ const jp: PartialLocaleType = {
Title: "自動タイトル生成",
SubTitle: "チャット内容に基づいて適切なタイトルを生成",
},
AutoScroll: {
Title: "自動スクロールを有効にする",
SubTitle:
"テキストエリアにフォーカスするか、メッセージを送信するとチャットが自動で下にスクロールされます",
},
Sync: {
CloudState: "クラウドデータ",
NotSyncYet: "まだ同期されていません",
Expand Down
Loading
Loading