This commit is contained in:
sz.kgt 2025-04-25 04:50:57 +02:00 committed by GitHub
commit 0fa2c3a24b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 157 additions and 87 deletions

View File

@ -1,7 +1,6 @@
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import React, { import React, {
Fragment, Fragment,
RefObject,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@ -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: { export function ChatActions(props: {
uploadImage: () => void; uploadImage: () => void;
setAttachImages: (images: string[]) => void; setAttachImages: (images: string[]) => void;
setUploading: (uploading: boolean) => void; setUploading: (uploading: boolean) => void;
showPromptModal: () => void; showPromptModal: () => void;
scrollToBottom: () => void; scrollChatToBottom: () => void;
showPromptHints: () => void; showPromptHints: () => void;
hitBottom: boolean; hitBottom: boolean;
uploading: boolean; uploading: boolean;
@ -608,7 +566,7 @@ export function ChatActions(props: {
)} )}
{!props.hitBottom && ( {!props.hitBottom && (
<ChatAction <ChatAction
onClick={props.scrollToBottom} onClick={props.scrollChatToBottom}
text={Locale.Chat.InputActions.ToBottom} text={Locale.Chat.InputActions.ToBottom}
icon={<BottomIcon />} icon={<BottomIcon />}
/> />
@ -997,37 +955,12 @@ function _Chat() {
const [showExport, setShowExport] = useState(false); const [showExport, setShowExport] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const [userInput, setUserInput] = useState(""); const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { submitKey, shouldSubmit } = useSubmitHandler(); 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 [hitBottom, setHitBottom] = useState(true);
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const navigate = useNavigate(); const navigate = useNavigate();
@ -1104,6 +1037,7 @@ function _Chat() {
const doSubmit = (userInput: string) => { const doSubmit = (userInput: string) => {
if (userInput.trim() === "" && isEmpty(attachImages)) return; if (userInput.trim() === "" && isEmpty(attachImages)) return;
const matchCommand = chatCommands.match(userInput); const matchCommand = chatCommands.match(userInput);
if (matchCommand.matched) { if (matchCommand.matched) {
setUserInput(""); setUserInput("");
@ -1111,16 +1045,19 @@ function _Chat() {
matchCommand.invoke(); matchCommand.invoke();
return; return;
} }
setIsLoading(true); setIsLoading(true);
chatStore
.onUserInput(userInput, attachImages) chatStore.onUserInput(userInput, attachImages).then(() => {
.then(() => setIsLoading(false)); setIsLoading(false);
autoScrollChatToBottom();
});
setAttachImages([]); setAttachImages([]);
chatStore.setLastInput(userInput); chatStore.setLastInput(userInput);
setUserInput(""); setUserInput("");
setPromptHints([]); setPromptHints([]);
if (!isMobileScreen) inputRef.current?.focus(); autoScrollChatToBottom();
setAutoScroll(true);
}; };
const onPromptSelect = (prompt: RenderPrompt) => { const onPromptSelect = (prompt: RenderPrompt) => {
@ -1420,13 +1357,32 @@ function _Chat() {
} }
setHitBottom(isHitBottom); setHitBottom(isHitBottom);
setAutoScroll(isHitBottom);
}; };
function scrollToBottom() { function scrollChatToBottom() {
const dom = scrollRef.current;
if (dom) {
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE); setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
scrollDomToBottom(); 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 // clear context index = context length + index in messages
const clearContextIndex = const clearContextIndex =
@ -1775,10 +1731,7 @@ function _Chat() {
ref={scrollRef} ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)} onScroll={(e) => onChatBodyScroll(e.currentTarget)}
onMouseDown={() => inputRef.current?.blur()} onMouseDown={() => inputRef.current?.blur()}
onTouchStart={() => { onTouchStart={() => inputRef.current?.blur()}
inputRef.current?.blur();
setAutoScroll(false);
}}
> >
{messages {messages
// TODO // TODO
@ -2050,7 +2003,7 @@ function _Chat() {
setAttachImages={setAttachImages} setAttachImages={setAttachImages}
setUploading={setUploading} setUploading={setUploading}
showPromptModal={() => setShowPromptModal(true)} showPromptModal={() => setShowPromptModal(true)}
scrollToBottom={scrollToBottom} scrollChatToBottom={scrollChatToBottom}
hitBottom={hitBottom} hitBottom={hitBottom}
uploading={uploading} uploading={uploading}
showPromptHints={() => { showPromptHints={() => {
@ -2083,8 +2036,8 @@ function _Chat() {
onInput={(e) => onInput(e.currentTarget.value)} onInput={(e) => onInput(e.currentTarget.value)}
value={userInput} value={userInput}
onKeyDown={onInputKeyDown} onKeyDown={onInputKeyDown}
onFocus={scrollToBottom} onFocus={autoScrollChatToBottom}
onClick={scrollToBottom} onClick={autoScrollChatToBottom}
onPaste={handlePaste} onPaste={handlePaste}
rows={inputRows} rows={inputRows}
autoFocus={autoFocus} autoFocus={autoFocus}

View File

@ -1699,6 +1699,23 @@ export function Settings() {
} }
></input> ></input>
</ListItem> </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> </List>
<SyncItems /> <SyncItems />

View File

@ -199,6 +199,11 @@ const ar: PartialLocaleType = {
Title: "توليد العنوان تلقائيًا", Title: "توليد العنوان تلقائيًا",
SubTitle: "توليد عنوان مناسب بناءً على محتوى الدردشة", SubTitle: "توليد عنوان مناسب بناءً على محتوى الدردشة",
}, },
AutoScroll: {
Title: "تفعيل التمرير التلقائي",
SubTitle:
"التمرير التلقائي للدردشة إلى الأسفل عند التركيز على منطقة النص أو إرسال الرسالة",
},
Sync: { Sync: {
CloudState: "بيانات السحابة", CloudState: "بيانات السحابة",
NotSyncYet: "لم يتم التزامن بعد", NotSyncYet: "لم يتم التزامن بعد",

View File

@ -200,6 +200,11 @@ const bn: PartialLocaleType = {
Title: "স্বয়ংক্রিয় শিরোনাম জেনারেশন", Title: "স্বয়ংক্রিয় শিরোনাম জেনারেশন",
SubTitle: "চ্যাট কনটেন্টের ভিত্তিতে উপযুক্ত শিরোনাম তৈরি করুন", SubTitle: "চ্যাট কনটেন্টের ভিত্তিতে উপযুক্ত শিরোনাম তৈরি করুন",
}, },
AutoScroll: {
Title: "অটো স্ক্রল সক্ষম করুন",
SubTitle:
"টেক্সট এরিয়া ফোকাস বা মেসেজ সাবমিটে স্বয়ংক্রিয়ভাবে চ্যাট নিচে স্ক্রল করুন",
},
Sync: { Sync: {
CloudState: "ক্লাউড ডেটা", CloudState: "ক্লাউড ডেটা",
NotSyncYet: "এখনো সিঙ্ক করা হয়নি", NotSyncYet: "এখনো সিঙ্ক করা হয়নি",

View File

@ -220,6 +220,10 @@ const cn = {
Title: "自动生成标题", Title: "自动生成标题",
SubTitle: "根据对话内容生成合适的标题", SubTitle: "根据对话内容生成合适的标题",
}, },
AutoScroll: {
Title: "启用自动滚动",
SubTitle: "在文本区域聚焦或提交消息时自动滚动聊天到底部",
},
Sync: { Sync: {
CloudState: "云端数据", CloudState: "云端数据",
NotSyncYet: "还没有进行过同步", NotSyncYet: "还没有进行过同步",

View File

@ -200,6 +200,11 @@ const cs: PartialLocaleType = {
Title: "Automatické generování názvu", Title: "Automatické generování názvu",
SubTitle: "Generovat vhodný název na základě obsahu konverzace", 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: { Sync: {
CloudState: "Data na cloudu", CloudState: "Data na cloudu",
NotSyncYet: "Ještě nebylo synchronizováno", NotSyncYet: "Ještě nebylo synchronizováno",

View File

@ -219,6 +219,11 @@ const da: PartialLocaleType = {
Title: "Lav titel automatisk", Title: "Lav titel automatisk",
SubTitle: "Foreslå en titel ud fra chatten", 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: { Sync: {
CloudState: "Seneste opdatering", CloudState: "Seneste opdatering",
NotSyncYet: "Endnu ikke synkroniseret", NotSyncYet: "Endnu ikke synkroniseret",

View File

@ -205,6 +205,11 @@ const de: PartialLocaleType = {
SubTitle: SubTitle:
"Basierend auf dem Chat-Inhalt einen passenden Titel generieren", "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: { Sync: {
CloudState: "Cloud-Daten", CloudState: "Cloud-Daten",
NotSyncYet: "Noch nicht synchronisiert", NotSyncYet: "Noch nicht synchronisiert",

View File

@ -222,6 +222,11 @@ const en: LocaleType = {
Title: "Auto Generate Title", Title: "Auto Generate Title",
SubTitle: "Generate a suitable title based on the conversation content", 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: { Sync: {
CloudState: "Last Update", CloudState: "Last Update",
NotSyncYet: "Not sync yet", NotSyncYet: "Not sync yet",
@ -756,7 +761,7 @@ const en: LocaleType = {
}, },
Artifacts: { Artifacts: {
Title: "Enable Artifacts", Title: "Enable Artifacts",
SubTitle: "Can render HTML page when enable artifacts.", SubTitle: "Can render HTML page when enable artifacts",
}, },
CodeFold: { CodeFold: {
Title: "Enable CodeFold", Title: "Enable CodeFold",

View File

@ -208,6 +208,11 @@ const es: PartialLocaleType = {
Title: "Generar título automáticamente", Title: "Generar título automáticamente",
SubTitle: "Generar un título adecuado basado en el contenido del chat", 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: { Sync: {
CloudState: "Datos en la nube", CloudState: "Datos en la nube",
NotSyncYet: "Aún no se ha sincronizado", NotSyncYet: "Aún no se ha sincronizado",

View File

@ -207,6 +207,11 @@ const fr: PartialLocaleType = {
SubTitle: SubTitle:
"Générer un titre approprié en fonction du contenu de la discussion", "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: { Sync: {
CloudState: "Données cloud", CloudState: "Données cloud",
NotSyncYet: "Pas encore synchronisé", NotSyncYet: "Pas encore synchronisé",

View File

@ -201,6 +201,11 @@ const id: PartialLocaleType = {
Title: "Otomatis Membuat Judul", Title: "Otomatis Membuat Judul",
SubTitle: "Membuat judul yang sesuai berdasarkan konten obrolan", 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: { Sync: {
CloudState: "Data Cloud", CloudState: "Data Cloud",
NotSyncYet: "Belum disinkronkan", NotSyncYet: "Belum disinkronkan",

View File

@ -209,6 +209,11 @@ const it: PartialLocaleType = {
SubTitle: SubTitle:
"Genera un titolo appropriato in base al contenuto della conversazione", "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: { Sync: {
CloudState: "Dati cloud", CloudState: "Dati cloud",
NotSyncYet: "Non è ancora avvenuta alcuna sincronizzazione", NotSyncYet: "Non è ancora avvenuta alcuna sincronizzazione",

View File

@ -200,6 +200,11 @@ const jp: PartialLocaleType = {
Title: "自動タイトル生成", Title: "自動タイトル生成",
SubTitle: "チャット内容に基づいて適切なタイトルを生成", SubTitle: "チャット内容に基づいて適切なタイトルを生成",
}, },
AutoScroll: {
Title: "自動スクロールを有効にする",
SubTitle:
"テキストエリアにフォーカスするか、メッセージを送信するとチャットが自動で下にスクロールされます",
},
Sync: { Sync: {
CloudState: "クラウドデータ", CloudState: "クラウドデータ",
NotSyncYet: "まだ同期されていません", NotSyncYet: "まだ同期されていません",

View File

@ -199,6 +199,11 @@ const ko: PartialLocaleType = {
Title: "제목 자동 생성", Title: "제목 자동 생성",
SubTitle: "대화 내용에 따라 적절한 제목 생성", SubTitle: "대화 내용에 따라 적절한 제목 생성",
}, },
AutoScroll: {
Title: "자동 스크롤 활성화",
SubTitle:
"텍스트 영역에 포커스하거나 메시지를 전송하면 채팅이 자동으로 아래로 스크롤됩니다",
},
Sync: { Sync: {
CloudState: "클라우드 데이터", CloudState: "클라우드 데이터",
NotSyncYet: "아직 동기화되지 않았습니다.", NotSyncYet: "아직 동기화되지 않았습니다.",

View File

@ -206,6 +206,11 @@ const no: PartialLocaleType = {
Title: "Automatisk generere tittel", Title: "Automatisk generere tittel",
SubTitle: "Generer en passende tittel basert på samtaleinnholdet", SubTitle: "Generer en passende tittel basert på samtaleinnholdet",
}, },
AutoScroll: {
Title: "Aktiver automatisk rulling",
SubTitle:
"Rull automatisk chatten til bunnen ved fokus på tekstfelt eller sending av melding",
},
Sync: { Sync: {
CloudState: "Skydatasynkronisering", CloudState: "Skydatasynkronisering",
NotSyncYet: "Har ikke blitt synkronisert ennå", NotSyncYet: "Har ikke blitt synkronisert ennå",

View File

@ -199,6 +199,11 @@ const pt: PartialLocaleType = {
Title: "Gerar Título Automaticamente", Title: "Gerar Título Automaticamente",
SubTitle: "Gerar um título adequado baseado no conteúdo da conversa", SubTitle: "Gerar um título adequado baseado no conteúdo da conversa",
}, },
AutoScroll: {
Title: "Ativar rolagem automática",
SubTitle:
"Rolar automaticamente o chat para baixo ao focar na área de texto ou enviar uma mensagem",
},
Sync: { Sync: {
CloudState: "Última Atualização", CloudState: "Última Atualização",
NotSyncYet: "Ainda não sincronizado", NotSyncYet: "Ainda não sincronizado",

View File

@ -202,6 +202,11 @@ const ru: PartialLocaleType = {
Title: "Автоматическое создание заголовка", Title: "Автоматическое создание заголовка",
SubTitle: "Создание подходящего заголовка на основе содержания беседы", SubTitle: "Создание подходящего заголовка на основе содержания беседы",
}, },
AutoScroll: {
Title: "Включить автопрокрутку",
SubTitle:
"Автоматически прокручивать чат вниз при фокусе на текстовом поле или отправке сообщения",
},
Sync: { Sync: {
CloudState: "Облачные данные", CloudState: "Облачные данные",
NotSyncYet: "Синхронизация еще не проводилась", NotSyncYet: "Синхронизация еще не проводилась",

View File

@ -200,6 +200,11 @@ const sk: PartialLocaleType = {
Title: "Automaticky generovať názov", Title: "Automaticky generovať názov",
SubTitle: "Generovať vhodný názov na základe obsahu konverzácie", SubTitle: "Generovať vhodný názov na základe obsahu konverzácie",
}, },
AutoScroll: {
Title: "Povoliť automatické posúvanie",
SubTitle:
"Automaticky posunúť chat nadol pri zameraní na textové pole alebo odoslaní správy",
},
Sync: { Sync: {
CloudState: "Posledná aktualizácia", CloudState: "Posledná aktualizácia",
NotSyncYet: "Zatiaľ nesynchronizované", NotSyncYet: "Zatiaľ nesynchronizované",

View File

@ -200,6 +200,11 @@ const tr: PartialLocaleType = {
Title: "Başlığı Otomatik Oluştur", Title: "Başlığı Otomatik Oluştur",
SubTitle: "Sohbet içeriğine göre uygun başlık oluştur", SubTitle: "Sohbet içeriğine göre uygun başlık oluştur",
}, },
AutoScroll: {
Title: "Otomatik Kaydırmayı Etkinleştir",
SubTitle:
"Metin alanına odaklanıldığında veya mesaj gönderildiğinde sohbeti otomatik olarak aşağı kaydır",
},
Sync: { Sync: {
CloudState: "Bulut Verisi", CloudState: "Bulut Verisi",
NotSyncYet: "Henüz senkronize edilmedi", NotSyncYet: "Henüz senkronize edilmedi",

View File

@ -207,6 +207,10 @@ const tw = {
Title: "自動產生標題", Title: "自動產生標題",
SubTitle: "根據對話內容產生合適的標題", SubTitle: "根據對話內容產生合適的標題",
}, },
AutoScroll: {
Title: "啟用自動捲動",
SubTitle: "在文字區域聚焦或送出訊息時,自動將聊天捲動至底部",
},
Sync: { Sync: {
CloudState: "雲端資料", CloudState: "雲端資料",
NotSyncYet: "還沒有進行過同步", NotSyncYet: "還沒有進行過同步",

View File

@ -200,6 +200,11 @@ const vi: PartialLocaleType = {
Title: "Tự động tạo tiêu đề", Title: "Tự động tạo tiêu đề",
SubTitle: "Tạo tiêu đề phù hợp dựa trên nội dung cuộc trò chuyện", SubTitle: "Tạo tiêu đề phù hợp dựa trên nội dung cuộc trò chuyện",
}, },
AutoScroll: {
Title: "Bật Tự động Cuộn",
SubTitle:
"Tự động cuộn cuộc trò chuyện xuống dưới khi tập trung vào vùng văn bản hoặc gửi tin nhắn",
},
Sync: { Sync: {
CloudState: "Dữ liệu đám mây", CloudState: "Dữ liệu đám mây",
NotSyncYet: "Chưa thực hiện đồng bộ", NotSyncYet: "Chưa thực hiện đồng bộ",

View File

@ -55,6 +55,8 @@ export const DEFAULT_CONFIG = {
enableCodeFold: true, // code fold config enableCodeFold: true, // code fold config
enableAutoScroll: true, // auto scroll config
disablePromptHint: false, disablePromptHint: false,
dontShowMaskSplashScreen: false, // dont show splash screen when create chat dontShowMaskSplashScreen: false, // dont show splash screen when create chat