This commit is contained in:
GH Action - Upstream Sync 2023-08-04 01:21:59 +00:00
commit ecba3450b4
20 changed files with 169 additions and 133 deletions

View File

@ -14,10 +14,11 @@
padding: 4px 10px; padding: 4px 10px;
animation: slide-in ease 0.3s; animation: slide-in ease 0.3s;
box-shadow: var(--card-shadow); box-shadow: var(--card-shadow);
transition: all ease 0.3s; transition: width ease 0.3s;
align-items: center; align-items: center;
height: 16px; height: 16px;
width: var(--icon-width); width: var(--icon-width);
overflow: hidden;
&:not(:last-child) { &:not(:last-child) {
margin-right: 5px; margin-right: 5px;
@ -29,14 +30,16 @@
opacity: 0; opacity: 0;
transform: translateX(-5px); transform: translateX(-5px);
transition: all ease 0.3s; transition: all ease 0.3s;
transition-delay: 0.1s;
pointer-events: none; pointer-events: none;
} }
&:hover { &:hover {
--delay: 0.5s;
width: var(--full-width); width: var(--full-width);
transition-delay: var(--delay);
.text { .text {
transition-delay: var(--delay);
opacity: 1; opacity: 1;
transform: translate(0); transform: translate(0);
} }

View File

@ -74,7 +74,13 @@ import {
showToast, showToast,
} from "./ui-lib"; } from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant"; import {
CHAT_PAGE_SIZE,
LAST_INPUT_KEY,
MAX_RENDER_MSG_COUNT,
Path,
REQUEST_TIMEOUT_MS,
} from "../constant";
import { Avatar } from "./emoji"; import { Avatar } from "./emoji";
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
import { useMaskStore } from "../store/mask"; import { useMaskStore } from "../store/mask";
@ -371,23 +377,29 @@ function useScrollToBottom() {
// for auto-scroll // for auto-scroll
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true); const [autoScroll, setAutoScroll] = useState(true);
const scrollToBottom = useCallback(() => {
function scrollDomToBottom() {
const dom = scrollRef.current; const dom = scrollRef.current;
if (dom) { if (dom) {
requestAnimationFrame(() => dom.scrollTo(0, dom.scrollHeight)); requestAnimationFrame(() => {
setAutoScroll(true);
dom.scrollTo(0, dom.scrollHeight);
});
}
} }
}, []);
// auto scroll // auto scroll
useEffect(() => { useEffect(() => {
autoScroll && scrollToBottom(); if (autoScroll) {
scrollDomToBottom();
}
}); });
return { return {
scrollRef, scrollRef,
autoScroll, autoScroll,
setAutoScroll, setAutoScroll,
scrollToBottom, scrollDomToBottom,
}; };
} }
@ -504,6 +516,7 @@ export function ChatActions(props: {
{showModelSelector && ( {showModelSelector && (
<Selector <Selector
defaultSelectedValue={currentModel}
items={models.map((m) => ({ items={models.map((m) => ({
title: m, title: m,
value: m, value: m,
@ -531,7 +544,7 @@ export function EditMessageModal(props: { onClose: () => void }) {
return ( return (
<div className="modal-mask"> <div className="modal-mask">
<Modal <Modal
title={Locale.UI.Edit} title={Locale.Chat.EditMessage.Title}
onClose={props.onClose} onClose={props.onClose}
actions={[ actions={[
<IconButton <IconButton
@ -585,14 +598,11 @@ export function EditMessageModal(props: { onClose: () => void }) {
); );
} }
export function Chat() { function _Chat() {
type RenderMessage = ChatMessage & { preview?: boolean }; type RenderMessage = ChatMessage & { preview?: boolean };
const chatStore = useChatStore(); const chatStore = useChatStore();
const [session, sessionIndex] = useChatStore((state) => [ const session = chatStore.currentSession();
state.currentSession(),
state.currentSessionIndex,
]);
const config = useAppConfig(); const config = useAppConfig();
const fontSize = config.fontSize; const fontSize = config.fontSize;
@ -602,16 +612,11 @@ export function Chat() {
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, setAutoScroll, scrollToBottom } = useScrollToBottom(); const { scrollRef, setAutoScroll, scrollDomToBottom } = useScrollToBottom();
const [hitBottom, setHitBottom] = useState(true); const [hitBottom, setHitBottom] = useState(true);
const isMobileScreen = useMobileScreen(); const isMobileScreen = useMobileScreen();
const navigate = useNavigate(); const navigate = useNavigate();
const onChatBodyScroll = (e: HTMLElement) => {
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 10;
setHitBottom(isTouchBottom);
};
// prompt hints // prompt hints
const promptStore = usePromptStore(); const promptStore = usePromptStore();
const [promptHints, setPromptHints] = useState<RenderPompt[]>([]); const [promptHints, setPromptHints] = useState<RenderPompt[]>([]);
@ -853,10 +858,9 @@ export function Chat() {
}); });
}; };
const context: RenderMessage[] = session.mask.hideContext const context: RenderMessage[] = useMemo(() => {
? [] return session.mask.hideContext ? [] : session.mask.context.slice();
: session.mask.context.slice(); }, [session.mask.context, session.mask.hideContext]);
const accessStore = useAccessStore(); const accessStore = useAccessStore();
if ( if (
@ -870,14 +874,9 @@ export function Chat() {
context.push(copiedHello); context.push(copiedHello);
} }
// clear context index = context length + index in messages
const clearContextIndex =
(session.clearContextIndex ?? -1) >= 0
? session.clearContextIndex! + context.length
: -1;
// preview messages // preview messages
const messages = context const renderMessages = useMemo(() => {
return context
.concat(session.messages as RenderMessage[]) .concat(session.messages as RenderMessage[])
.concat( .concat(
isLoading isLoading
@ -905,15 +904,68 @@ export function Chat() {
] ]
: [], : [],
); );
}, [
config.sendPreviewBubble,
context,
isLoading,
session.messages,
userInput,
]);
const [msgRenderIndex, _setMsgRenderIndex] = useState(
Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
);
function setMsgRenderIndex(newIndex: number) {
newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
newIndex = Math.max(0, newIndex);
_setMsgRenderIndex(newIndex);
}
const messages = useMemo(() => {
const endRenderIndex = Math.min(
msgRenderIndex + 3 * CHAT_PAGE_SIZE,
renderMessages.length,
);
return renderMessages.slice(msgRenderIndex, endRenderIndex);
}, [msgRenderIndex, renderMessages]);
const onChatBodyScroll = (e: HTMLElement) => {
const bottomHeight = e.scrollTop + e.clientHeight;
const edgeThreshold = e.clientHeight;
const isTouchTopEdge = e.scrollTop <= edgeThreshold;
const isTouchBottomEdge = bottomHeight >= e.scrollHeight - edgeThreshold;
const isHitBottom = bottomHeight >= e.scrollHeight - 10;
const prevPageMsgIndex = msgRenderIndex - CHAT_PAGE_SIZE;
const nextPageMsgIndex = msgRenderIndex + CHAT_PAGE_SIZE;
if (isTouchTopEdge) {
setMsgRenderIndex(prevPageMsgIndex);
} else if (isTouchBottomEdge) {
setMsgRenderIndex(nextPageMsgIndex);
}
setHitBottom(isHitBottom);
setAutoScroll(isHitBottom);
};
function scrollToBottom() {
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
scrollDomToBottom();
}
// clear context index = context length + index in messages
const clearContextIndex =
(session.clearContextIndex ?? -1) >= 0
? session.clearContextIndex! + context.length - msgRenderIndex
: -1;
const [showPromptModal, setShowPromptModal] = useState(false); const [showPromptModal, setShowPromptModal] = useState(false);
const clientConfig = useMemo(() => getClientConfig(), []); const clientConfig = useMemo(() => getClientConfig(), []);
const location = useLocation(); const autoFocus = !isMobileScreen; // wont auto focus on mobile screen
const isChat = location.pathname === Path.Chat;
const autoFocus = !isMobileScreen || isChat; // only focus in chat page
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
useCommand({ useCommand({
@ -1035,7 +1087,6 @@ export function Chat() {
ref={scrollRef} ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)} onScroll={(e) => onChatBodyScroll(e.currentTarget)}
onMouseDown={() => inputRef.current?.blur()} onMouseDown={() => inputRef.current?.blur()}
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
onTouchStart={() => { onTouchStart={() => {
inputRef.current?.blur(); inputRef.current?.blur();
setAutoScroll(false); setAutoScroll(false);
@ -1053,7 +1104,7 @@ export function Chat() {
const shouldShowClearContextDivider = i === clearContextIndex - 1; const shouldShowClearContextDivider = i === clearContextIndex - 1;
return ( return (
<Fragment key={i}> <Fragment key={message.id}>
<div <div
className={ className={
isUser ? styles["chat-message-user"] : styles["chat-message"] isUser ? styles["chat-message-user"] : styles["chat-message"]
@ -1137,7 +1188,8 @@ export function Chat() {
<Markdown <Markdown
content={message.content} content={message.content}
loading={ loading={
(message.preview || message.content.length === 0) && (message.preview || message.streaming) &&
message.content.length === 0 &&
!isUser !isUser
} }
onContextMenu={(e) => onRightClick(e, message)} onContextMenu={(e) => onRightClick(e, message)}
@ -1147,7 +1199,7 @@ export function Chat() {
}} }}
fontSize={fontSize} fontSize={fontSize}
parentRef={scrollRef} parentRef={scrollRef}
defaultShow={i >= messages.length - 10} defaultShow={i >= messages.length - 6}
/> />
</div> </div>
@ -1191,8 +1243,8 @@ export function Chat() {
onInput={(e) => onInput(e.currentTarget.value)} onInput={(e) => onInput(e.currentTarget.value)}
value={userInput} value={userInput}
onKeyDown={onInputKeyDown} onKeyDown={onInputKeyDown}
onFocus={() => setAutoScroll(true)} onFocus={scrollToBottom}
onBlur={() => setAutoScroll(false)} onClick={scrollToBottom}
rows={inputRows} rows={inputRows}
autoFocus={autoFocus} autoFocus={autoFocus}
style={{ style={{
@ -1223,3 +1275,9 @@ export function Chat() {
</div> </div>
); );
} }
export function Chat() {
const chatStore = useChatStore();
const sessionIndex = chatStore.currentSessionIndex;
return <_Chat key={sessionIndex}></_Chat>;
}

View File

@ -146,70 +146,23 @@ export function Markdown(
} & React.DOMAttributes<HTMLDivElement>, } & React.DOMAttributes<HTMLDivElement>,
) { ) {
const mdRef = useRef<HTMLDivElement>(null); const mdRef = useRef<HTMLDivElement>(null);
const renderedHeight = useRef(0);
const renderedWidth = useRef(0);
const inView = useRef(!!props.defaultShow);
const [_, triggerRender] = useState(0);
const checkInView = useThrottledCallback(
() => {
const parent = props.parentRef?.current;
const md = mdRef.current;
if (parent && md && !props.defaultShow) {
const parentBounds = parent.getBoundingClientRect();
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
const mdBounds = md.getBoundingClientRect();
const parentTop = parentBounds.top - twoScreenHeight;
const parentBottom = parentBounds.bottom + twoScreenHeight;
const isOverlap =
Math.max(parentTop, mdBounds.top) <=
Math.min(parentBottom, mdBounds.bottom);
inView.current = isOverlap;
triggerRender(Date.now());
}
if (inView.current && md) {
const rect = md.getBoundingClientRect();
renderedHeight.current = Math.max(renderedHeight.current, rect.height);
renderedWidth.current = Math.max(renderedWidth.current, rect.width);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
300,
{
leading: true,
trailing: true,
},
);
useEffect(() => {
props.parentRef?.current?.addEventListener("scroll", checkInView);
checkInView();
return () =>
props.parentRef?.current?.removeEventListener("scroll", checkInView);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const getSize = (x: number) => (!inView.current && x > 0 ? x : "auto");
return ( return (
<div <div
className="markdown-body" className="markdown-body"
style={{ style={{
fontSize: `${props.fontSize ?? 14}px`, fontSize: `${props.fontSize ?? 14}px`,
height: getSize(renderedHeight.current),
width: getSize(renderedWidth.current),
direction: /[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr", direction: /[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr",
}} }}
ref={mdRef} ref={mdRef}
onContextMenu={props.onContextMenu} onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture} onDoubleClickCapture={props.onDoubleClickCapture}
> >
{inView.current && {props.loading ? (
(props.loading ? (
<LoadingIcon /> <LoadingIcon />
) : ( ) : (
<MarkdownContent content={props.content} /> <MarkdownContent content={props.content} />
))} )}
</div> </div>
); );
} }

View File

@ -76,7 +76,7 @@ export function ModelConfigList(props: {
<input <input
type="number" type="number"
min={100} min={100}
max={32000} max={100000}
value={props.modelConfig.max_tokens} value={props.modelConfig.max_tokens}
onChange={(e) => onChange={(e) =>
props.updateConfig( props.updateConfig(
@ -169,7 +169,7 @@ export function ModelConfigList(props: {
title={props.modelConfig.historyMessageCount.toString()} title={props.modelConfig.historyMessageCount.toString()}
value={props.modelConfig.historyMessageCount} value={props.modelConfig.historyMessageCount}
min="0" min="0"
max="32" max="64"
step="1" step="1"
onChange={(e) => onChange={(e) =>
props.updateConfig( props.updateConfig(

View File

@ -443,6 +443,7 @@ export function Selector<T>(props: {
subTitle?: string; subTitle?: string;
value: T; value: T;
}>; }>;
defaultSelectedValue?: T;
onSelection?: (selection: T[]) => void; onSelection?: (selection: T[]) => void;
onClose?: () => void; onClose?: () => void;
multiple?: boolean; multiple?: boolean;
@ -452,6 +453,7 @@ export function Selector<T>(props: {
<div className={styles["selector-content"]}> <div className={styles["selector-content"]}>
<List> <List>
{props.items.map((item, i) => { {props.items.map((item, i) => {
const selected = props.defaultSelectedValue === item.value;
return ( return (
<ListItem <ListItem
className={styles["selector-item"]} className={styles["selector-item"]}
@ -462,7 +464,20 @@ export function Selector<T>(props: {
props.onSelection?.([item.value]); props.onSelection?.([item.value]);
props.onClose?.(); props.onClose?.();
}} }}
></ListItem> >
{selected ? (
<div
style={{
height: 10,
width: 10,
backgroundColor: "var(--primary)",
borderRadius: 10,
}}
></div>
) : (
<></>
)}
</ListItem>
); );
})} })}
</List> </List>

View File

@ -109,3 +109,6 @@ export const DEFAULT_MODELS = [
available: true, available: true,
}, },
] as const; ] as const;
export const CHAT_PAGE_SIZE = 10;
export const MAX_RENDER_MSG_COUNT = 20;

View File

@ -19,6 +19,7 @@ const cn = {
Chat: { Chat: {
SubTitle: (count: number) => `${count} 条对话`, SubTitle: (count: number) => `${count} 条对话`,
EditMessage: { EditMessage: {
Title: "编辑消息记录",
Topic: { Topic: {
Title: "聊天主题", Title: "聊天主题",
SubTitle: "更改当前聊天主题", SubTitle: "更改当前聊天主题",
@ -274,7 +275,7 @@ const cn = {
Context: { Context: {
Toast: (x: any) => `包含 ${x} 条预设提示词`, Toast: (x: any) => `包含 ${x} 条预设提示词`,
Edit: "当前对话设置", Edit: "当前对话设置",
Add: "新增预设对话", Add: "新增一条对话",
Clear: "上下文已清除", Clear: "上下文已清除",
Revert: "恢复上下文", Revert: "恢复上下文",
}, },

View File

@ -5,7 +5,7 @@ const cs: PartialLocaleType = {
WIP: "V přípravě...", WIP: "V přípravě...",
Error: { Error: {
Unauthorized: Unauthorized:
"Neoprávněný přístup, zadejte přístupový kód na stránce nastavení.", "Neoprávněný přístup, zadejte přístupový kód na [stránce](/#/auth) nastavení.",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} zpráv`, ChatItemCount: (count: number) => `${count} zpráv`,

View File

@ -5,7 +5,7 @@ const de: PartialLocaleType = {
WIP: "In Bearbeitung...", WIP: "In Bearbeitung...",
Error: { Error: {
Unauthorized: Unauthorized:
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der Einstellungsseite ein.", "Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der [Einstellungsseite](/#/auth) ein.",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} Nachrichten`, ChatItemCount: (count: number) => `${count} Nachrichten`,

View File

@ -21,6 +21,7 @@ const en: LocaleType = {
Chat: { Chat: {
SubTitle: (count: number) => `${count} messages`, SubTitle: (count: number) => `${count} messages`,
EditMessage: { EditMessage: {
Title: "Edit All Messages",
Topic: { Topic: {
Title: "Topic", Title: "Topic",
SubTitle: "Change the current topic", SubTitle: "Change the current topic",

View File

@ -5,7 +5,7 @@ const es: PartialLocaleType = {
WIP: "En construcción...", WIP: "En construcción...",
Error: { Error: {
Unauthorized: Unauthorized:
"Acceso no autorizado, por favor ingrese el código de acceso en la página de configuración.", "Acceso no autorizado, por favor ingrese el código de acceso en la [página](/#/auth) de configuración.",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} mensajes`, ChatItemCount: (count: number) => `${count} mensajes`,

View File

@ -5,7 +5,7 @@ const fr: PartialLocaleType = {
WIP: "Prochainement...", WIP: "Prochainement...",
Error: { Error: {
Unauthorized: Unauthorized:
"Accès non autorisé, veuillez saisir le code d'accès dans la page des paramètres.", "Accès non autorisé, veuillez saisir le code d'accès dans la [page](/#/auth) des paramètres.",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} messages en total`, ChatItemCount: (count: number) => `${count} messages en total`,

View File

@ -5,7 +5,7 @@ const it: PartialLocaleType = {
WIP: "Work in progress...", WIP: "Work in progress...",
Error: { Error: {
Unauthorized: Unauthorized:
"Accesso non autorizzato, inserire il codice di accesso nella pagina delle impostazioni.", "Accesso non autorizzato, inserire il codice di accesso nella [pagina](/#/auth) delle impostazioni.",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} messaggi`, ChatItemCount: (count: number) => `${count} messaggi`,

View File

@ -5,7 +5,8 @@ import type { PartialLocaleType } from "./index";
const ko: PartialLocaleType = { const ko: PartialLocaleType = {
WIP: "곧 출시 예정...", WIP: "곧 출시 예정...",
Error: { Error: {
Unauthorized: "권한이 없습니다. 설정 페이지에서 액세스 코드를 입력하세요.", Unauthorized:
"권한이 없습니다. 설정 페이지에서 액세스 코드를 [입력하세요](/#/auth).",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count}개의 메시지`, ChatItemCount: (count: number) => `${count}개의 메시지`,

View File

@ -4,7 +4,8 @@ import type { PartialLocaleType } from "./index";
const no: PartialLocaleType = { const no: PartialLocaleType = {
WIP: "Arbeid pågår ...", WIP: "Arbeid pågår ...",
Error: { Error: {
Unauthorized: "Du har ikke tilgang. Vennlig oppgi tildelt adgangskode.", Unauthorized:
"Du har ikke tilgang. [Vennlig oppgi tildelt adgangskode](/#/auth).",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} meldinger`, ChatItemCount: (count: number) => `${count} meldinger`,

View File

@ -5,7 +5,7 @@ const ru: PartialLocaleType = {
WIP: "Скоро...", WIP: "Скоро...",
Error: { Error: {
Unauthorized: Unauthorized:
"Несанкционированный доступ. Пожалуйста, введите код доступа на странице настроек.", "Несанкционированный доступ. Пожалуйста, введите код доступа на [странице](/#/auth) настроек.",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} сообщений`, ChatItemCount: (count: number) => `${count} сообщений`,

View File

@ -5,7 +5,7 @@ const tr: PartialLocaleType = {
WIP: "Çalışma devam ediyor...", WIP: "Çalışma devam ediyor...",
Error: { Error: {
Unauthorized: Unauthorized:
"Yetkisiz erişim, lütfen erişim kodunu ayarlar sayfasından giriniz.", "Yetkisiz erişim, lütfen erişim kodunu ayarlar [sayfasından](/#/auth) giriniz.",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} mesaj`, ChatItemCount: (count: number) => `${count} mesaj`,

View File

@ -4,7 +4,7 @@ import type { PartialLocaleType } from "./index";
const tw: PartialLocaleType = { const tw: PartialLocaleType = {
WIP: "該功能仍在開發中……", WIP: "該功能仍在開發中……",
Error: { Error: {
Unauthorized: "目前您的狀態是未授權,請前往設定頁面輸入授權碼。", Unauthorized: "目前您的狀態是未授權,請前往[設定頁面](/#/auth)輸入授權碼。",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} 條對話`, ChatItemCount: (count: number) => `${count} 條對話`,

View File

@ -81,7 +81,7 @@ export const ModalConfigValidator = {
return x as ModelType; return x as ModelType;
}, },
max_tokens(x: number) { max_tokens(x: number) {
return limitNumber(x, 0, 32000, 2000); return limitNumber(x, 0, 100000, 2000);
}, },
presence_penalty(x: number) { presence_penalty(x: number) {
return limitNumber(x, -2, 2, 0); return limitNumber(x, -2, 2, 0);

View File

@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "ChatGPT Next Web", "productName": "ChatGPT Next Web",
"version": "2.9.1" "version": "2.9.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {