diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 4aaa8437..4173fc3a 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -54,7 +54,7 @@ import styles from "./home.module.scss"; import chatStyle from "./chat.module.scss"; import { ListItem, Modal, showModal } from "./ui-lib"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { Path } from "../constant"; import { Avatar } from "./emoji"; import { MaskAvatar, MaskConfig } from "./mask"; @@ -224,15 +224,63 @@ export function PromptHints(props: { prompts: Prompt[]; onPromptSelect: (prompt: Prompt) => void; }) { - if (props.prompts.length === 0) return null; + const noPrompts = props.prompts.length === 0; + const [selectIndex, setSelectIndex] = useState(0); + const selectedRef = useRef(null); + useEffect(() => { + setSelectIndex(0); + }, [props.prompts.length]); + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (noPrompts) return; + + // arrow up / down to select prompt + const changeIndex = (delta: number) => { + e.stopPropagation(); + e.preventDefault(); + const nextIndex = Math.max( + 0, + Math.min(props.prompts.length - 1, selectIndex + delta), + ); + setSelectIndex(nextIndex); + selectedRef.current?.scrollIntoView({ + block: "center", + }); + }; + + if (e.key === "ArrowUp") { + changeIndex(1); + } else if (e.key === "ArrowDown") { + changeIndex(-1); + } else if (e.key === "Enter") { + const selectedPrompt = props.prompts.at(selectIndex); + if (selectedPrompt) { + props.onPromptSelect(selectedPrompt); + } + } + }; + + window.addEventListener("keydown", onKeyDown); + + return () => window.removeEventListener("keydown", onKeyDown); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [noPrompts, selectIndex]); + + if (noPrompts) return null; return (
{props.prompts.map((prompt, i) => (
props.onPromptSelect(prompt)} + onMouseEnter={() => setSelectIndex(i)} >
{prompt.title}
{prompt.content}
@@ -370,7 +418,7 @@ export function Chat() { const navigate = useNavigate(); const onChatBodyScroll = (e: HTMLElement) => { - const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20; + const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 100; setHitBottom(isTouchBottom); }; @@ -397,7 +445,7 @@ export function Chat() { () => { const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1; const inputRows = Math.min( - 5, + 20, Math.max(2 + Number(!isMobileScreen), rows), ); setInputRows(inputRows); @@ -566,12 +614,9 @@ export function Chat() { } }; - // Auto focus - useEffect(() => { - if (isMobileScreen) return; - inputRef.current?.focus(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const location = useLocation(); + const isChat = location.pathname === Path.Chat; + const autoFocus = !isMobileScreen || isChat; // only focus in chat page return (
@@ -762,16 +807,9 @@ export function Chat() { value={userInput} onKeyDown={onInputKeyDown} onFocus={() => setAutoScroll(true)} - onBlur={() => { - setTimeout(() => { - if (document.activeElement !== inputRef.current) { - setAutoScroll(false); - setPromptHints([]); - } - }, 100); - }} - autoFocus + onBlur={() => setAutoScroll(false)} rows={inputRows} + autoFocus={autoFocus} /> } diff --git a/app/locales/index.ts b/app/locales/index.ts index 2ce59261..40f0a1ad 100644 --- a/app/locales/index.ts +++ b/app/locales/index.ts @@ -22,6 +22,7 @@ export const AllLangs = [ export type Lang = (typeof AllLangs)[number]; const LANG_KEY = "lang"; +const DEFAULT_LANG = "en"; function getItem(key: string) { try { @@ -41,7 +42,8 @@ function getLanguage() { try { return navigator.language.toLowerCase(); } catch { - return "cn"; + console.log("[Lang] failed to detect user lang."); + return DEFAULT_LANG; } } @@ -60,7 +62,7 @@ export function getLang(): Lang { } } - return "en"; + return DEFAULT_LANG; } export function changeLang(lang: Lang) {