mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-26 10:39:21 +08:00
Merge branch 'Yidadaa:main' into main
This commit is contained in:
@@ -12,14 +12,7 @@ import BotIcon from "../icons/bot.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
|
||||
import {
|
||||
Message,
|
||||
SubmitKey,
|
||||
useChatStore,
|
||||
ChatSession,
|
||||
BOT_HELLO,
|
||||
ROLES,
|
||||
} from "../store";
|
||||
import { Message, SubmitKey, useChatStore, BOT_HELLO, ROLES } from "../store";
|
||||
|
||||
import {
|
||||
copyToClipboard,
|
||||
@@ -377,7 +370,8 @@ export function Chat(props: {
|
||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
inputRef.current?.focus();
|
||||
if (!isMobileScreen()) inputRef.current?.focus();
|
||||
setAutoScroll(true);
|
||||
};
|
||||
|
||||
// stop response
|
||||
@@ -514,7 +508,10 @@ export function Chat(props: {
|
||||
bordered
|
||||
title={Locale.Chat.Actions.Export}
|
||||
onClick={() => {
|
||||
exportMessages(session.messages, session.topic);
|
||||
exportMessages(
|
||||
session.messages.filter((msg) => !msg.isError),
|
||||
session.topic,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -531,7 +528,11 @@ export function Chat(props: {
|
||||
className={styles["chat-body"]}
|
||||
ref={scrollRef}
|
||||
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
||||
onTouchStart={() => inputRef.current?.blur()}
|
||||
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
|
||||
onTouchStart={() => {
|
||||
inputRef.current?.blur();
|
||||
setAutoScroll(false);
|
||||
}}
|
||||
>
|
||||
{messages.map((message, i) => {
|
||||
const isUser = message.role === "user";
|
||||
@@ -592,7 +593,6 @@ export function Chat(props: {
|
||||
if (!isMobileScreen()) return;
|
||||
setUserInput(message.content);
|
||||
}}
|
||||
onMouseOver={() => inputRef.current?.blur()}
|
||||
>
|
||||
<Markdown content={message.content} />
|
||||
</div>
|
||||
@@ -627,9 +627,6 @@ export function Chat(props: {
|
||||
setAutoScroll(false);
|
||||
setTimeout(() => setPromptHints([]), 500);
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
autoFocus={!props?.sideBarShowing}
|
||||
/>
|
||||
<IconButton
|
||||
|
||||
47
app/components/error.tsx
Normal file
47
app/components/error.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from "react";
|
||||
import { IconButton } from "./button";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
import { ISSUE_URL } from "../constant";
|
||||
|
||||
interface IErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
info: React.ErrorInfo | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, info: null };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||
// Update state with error details
|
||||
this.setState({ hasError: true, error, info });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Render error message
|
||||
return (
|
||||
<div className="error">
|
||||
<h2>Oops, something went wrong!</h2>
|
||||
<pre>
|
||||
<code>{this.state.error?.toString()}</code>
|
||||
<code>{this.state.info?.componentStack}</code>
|
||||
</pre>
|
||||
|
||||
<a href={ISSUE_URL} className="report">
|
||||
<IconButton
|
||||
text="Report This Error"
|
||||
icon={<GithubIcon />}
|
||||
bordered
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// if no error occurred, render children
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||
require("../polyfill");
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
import styles from "./home.module.scss";
|
||||
@@ -14,25 +16,15 @@ import AddIcon from "../icons/add.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
|
||||
import {
|
||||
Message,
|
||||
SubmitKey,
|
||||
useChatStore,
|
||||
ChatSession,
|
||||
BOT_HELLO,
|
||||
} from "../store";
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
isMobileScreen,
|
||||
selectOrCopy,
|
||||
} from "../utils";
|
||||
import { useChatStore } from "../store";
|
||||
import { isMobileScreen } from "../utils";
|
||||
import Locale from "../locales";
|
||||
import { ChatList } from "./chat-list";
|
||||
import { Chat } from "./chat";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { REPO_URL } from "../constant";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
@@ -60,11 +52,23 @@ function useSwitchTheme() {
|
||||
document.body.classList.add("light");
|
||||
}
|
||||
|
||||
const themeColor = getComputedStyle(document.body)
|
||||
.getPropertyValue("--theme-color")
|
||||
.trim();
|
||||
const metaDescription = document.querySelector('meta[name="theme-color"]');
|
||||
metaDescription?.setAttribute("content", themeColor);
|
||||
const metaDescriptionDark = document.querySelector(
|
||||
'meta[name="theme-color"][media]',
|
||||
);
|
||||
const metaDescriptionLight = document.querySelector(
|
||||
'meta[name="theme-color"]:not([media])',
|
||||
);
|
||||
|
||||
if (config.theme === "auto") {
|
||||
metaDescriptionDark?.setAttribute("content", "#151515");
|
||||
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
||||
} else {
|
||||
const themeColor = getComputedStyle(document.body)
|
||||
.getPropertyValue("--theme-color")
|
||||
.trim();
|
||||
metaDescriptionDark?.setAttribute("content", themeColor);
|
||||
metaDescriptionLight?.setAttribute("content", themeColor);
|
||||
}
|
||||
}, [config.theme]);
|
||||
}
|
||||
|
||||
@@ -78,7 +82,7 @@ const useHasHydrated = () => {
|
||||
return hasHydrated;
|
||||
};
|
||||
|
||||
export function Home() {
|
||||
function _Home() {
|
||||
const [createNewSession, currentIndex, removeSession] = useChatStore(
|
||||
(state) => [
|
||||
state.newSession,
|
||||
@@ -191,3 +195,11 @@ export function Home() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<_Home></_Home>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export function Markdown(props: { content: string }) {
|
||||
components={{
|
||||
pre: PreCode,
|
||||
}}
|
||||
linkTarget={'_blank'}
|
||||
>
|
||||
{props.content}
|
||||
</ReactMarkdown>
|
||||
|
||||
@@ -18,3 +18,12 @@
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.password-eye {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo, HTMLProps } from "react";
|
||||
|
||||
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
|
||||
|
||||
@@ -8,6 +8,8 @@ import ResetIcon from "../icons/reload.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import ClearIcon from "../icons/clear.svg";
|
||||
import EditIcon from "../icons/edit.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import EyeOffIcon from "../icons/eye-off.svg";
|
||||
|
||||
import { List, ListItem, Popover, showToast } from "./ui-lib";
|
||||
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
ALL_MODELS,
|
||||
useUpdateStore,
|
||||
useAccessStore,
|
||||
ModalConfigValidator,
|
||||
} from "../store";
|
||||
import { Avatar } from "./chat";
|
||||
|
||||
@@ -28,6 +31,7 @@ import Link from "next/link";
|
||||
import { UPDATE_URL } from "../constant";
|
||||
import { SearchService, usePromptStore } from "../store/prompt";
|
||||
import { requestUsage } from "../requests";
|
||||
import { ErrorBoundary } from "./error";
|
||||
|
||||
function SettingItem(props: {
|
||||
title: string;
|
||||
@@ -47,6 +51,25 @@ function SettingItem(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function PasswordInput(props: HTMLProps<HTMLInputElement>) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
function changeVisibility() {
|
||||
setVisible(!visible);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles["password-input"]}>
|
||||
<IconButton
|
||||
icon={visible ? <EyeIcon /> : <EyeOffIcon />}
|
||||
onClick={changeVisibility}
|
||||
className={styles["password-eye"]}
|
||||
/>
|
||||
<input {...props} type={visible ? "text" : "password"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Settings(props: { closeSettings: () => void }) {
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
|
||||
@@ -91,11 +114,13 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
useEffect(() => {
|
||||
checkUpdate();
|
||||
checkUsage();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const enabledAccessControl = useMemo(
|
||||
() => accessStore.enabledAccessControl(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -103,8 +128,15 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
const builtinCount = SearchService.count.builtin;
|
||||
const customCount = promptStore.prompts.size ?? 0;
|
||||
|
||||
const showUsage = accessStore.token !== "";
|
||||
useEffect(() => {
|
||||
if (showUsage) {
|
||||
checkUsage();
|
||||
}
|
||||
}, [showUsage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<div className={styles["window-header"]}>
|
||||
<div className={styles["window-header-title"]}>
|
||||
<div className={styles["window-header-main-title"]}>
|
||||
@@ -327,14 +359,14 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
title={Locale.Settings.AccessCode.Title}
|
||||
subTitle={Locale.Settings.AccessCode.SubTitle}
|
||||
>
|
||||
<input
|
||||
<PasswordInput
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.AccessCode.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateCode(e.currentTarget.value);
|
||||
}}
|
||||
></input>
|
||||
/>
|
||||
</SettingItem>
|
||||
) : (
|
||||
<></>
|
||||
@@ -344,25 +376,27 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
title={Locale.Settings.Token.Title}
|
||||
subTitle={Locale.Settings.Token.SubTitle}
|
||||
>
|
||||
<input
|
||||
<PasswordInput
|
||||
value={accessStore.token}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Token.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateToken(e.currentTarget.value);
|
||||
}}
|
||||
></input>
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
title={Locale.Settings.Usage.Title}
|
||||
subTitle={
|
||||
loadingUsage
|
||||
? Locale.Settings.Usage.IsChecking
|
||||
: Locale.Settings.Usage.SubTitle(usage?.used ?? "[?]")
|
||||
showUsage
|
||||
? loadingUsage
|
||||
? Locale.Settings.Usage.IsChecking
|
||||
: Locale.Settings.Usage.SubTitle(usage?.used ?? "[?]")
|
||||
: Locale.Settings.Usage.NoAccess
|
||||
}
|
||||
>
|
||||
{loadingUsage ? (
|
||||
{!showUsage || loadingUsage ? (
|
||||
<div />
|
||||
) : (
|
||||
<IconButton
|
||||
@@ -420,7 +454,9 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
onChange={(e) => {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.modelConfig.model = e.currentTarget.value),
|
||||
(config.modelConfig.model = ModalConfigValidator.model(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -437,7 +473,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
value={config.modelConfig.temperature.toFixed(1)}
|
||||
value={config.modelConfig.temperature?.toFixed(1)}
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
@@ -445,7 +481,9 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.modelConfig.temperature =
|
||||
e.currentTarget.valueAsNumber),
|
||||
ModalConfigValidator.temperature(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
@@ -457,13 +495,15 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
<input
|
||||
type="number"
|
||||
min={100}
|
||||
max={4096}
|
||||
max={32000}
|
||||
value={config.modelConfig.max_tokens}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.modelConfig.max_tokens =
|
||||
e.currentTarget.valueAsNumber),
|
||||
ModalConfigValidator.max_tokens(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
@@ -474,7 +514,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
value={config.modelConfig.presence_penalty.toFixed(1)}
|
||||
value={config.modelConfig.presence_penalty?.toFixed(1)}
|
||||
min="-2"
|
||||
max="2"
|
||||
step="0.5"
|
||||
@@ -482,13 +522,15 @@ export function Settings(props: { closeSettings: () => void }) {
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.modelConfig.presence_penalty =
|
||||
e.currentTarget.valueAsNumber),
|
||||
ModalConfigValidator.presence_penalty(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</SettingItem>
|
||||
</List>
|
||||
</div>
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user