Merge branch 'main' of https://github.com/Yidadaa/ChatGPT-Next-Web
This commit is contained in:
commit
39fb187672
|
@ -216,9 +216,9 @@ If you want to disable parse settings from url, set this to 1.
|
||||||
### `CUSTOM_MODELS` (optional)
|
### `CUSTOM_MODELS` (optional)
|
||||||
|
|
||||||
> Default: Empty
|
> Default: Empty
|
||||||
> Example: `+llama,+claude-2,-gpt-3.5-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list.
|
> Example: `+llama,+claude-2,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` means add `llama, claude-2` to model list, and remove `gpt-3.5-turbo` from list, and display `gpt-4-1106-preview` as `gpt-4-turbo`.
|
||||||
|
|
||||||
To control custom models, use `+` to add a custom model, use `-` to hide a model, separated by comma.
|
To control custom models, use `+` to add a custom model, use `-` to hide a model, use `name:displayName` to customize model name, separated by comma.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|
|
@ -122,9 +122,9 @@ Azure Api 版本,你可以在这里找到:[Azure 文档](https://learn.micro
|
||||||
|
|
||||||
### `CUSTOM_MODELS` (可选)
|
### `CUSTOM_MODELS` (可选)
|
||||||
|
|
||||||
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`。
|
> 示例:`+qwen-7b-chat,+glm-6b,-gpt-3.5-turbo,gpt-4-1106-preview:gpt-4-turbo` 表示增加 `qwen-7b-chat` 和 `glm-6b` 到模型列表,而从列表中删除 `gpt-3.5-turbo`,并将 `gpt-4-1106-preview` 模型名字展示为 `gpt-4-turbo`。
|
||||||
|
|
||||||
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,用英文逗号隔开。
|
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名:展示名` 来自定义模型的展示名,用英文逗号隔开。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ export async function requestOpenai(req: NextRequest) {
|
||||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||||
|
|
||||||
// not undefined and is false
|
// not undefined and is false
|
||||||
if (modelTable[jsonBody?.model ?? ""] === false) {
|
if (modelTable[jsonBody?.model ?? ""].available === false) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
error: true,
|
error: true,
|
||||||
|
|
|
@ -431,11 +431,27 @@ export function ChatActions(props: {
|
||||||
|
|
||||||
// switch model
|
// switch model
|
||||||
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
const currentModel = chatStore.currentSession().mask.modelConfig.model;
|
||||||
const models = useAllModels()
|
const allModels = useAllModels();
|
||||||
.filter((m) => m.available)
|
const models = useMemo(
|
||||||
.map((m) => m.name);
|
() => allModels.filter((m) => m.available),
|
||||||
|
[allModels],
|
||||||
|
);
|
||||||
const [showModelSelector, setShowModelSelector] = useState(false);
|
const [showModelSelector, setShowModelSelector] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if current model is not available
|
||||||
|
// switch to first available model
|
||||||
|
const isUnavaliableModel = !models.some((m) => m.name === currentModel);
|
||||||
|
if (isUnavaliableModel && models.length > 0) {
|
||||||
|
const nextModel = models[0].name as ModelType;
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.mask.modelConfig.model = nextModel),
|
||||||
|
);
|
||||||
|
showToast(nextModel);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [currentModel, models]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles["chat-input-actions"]}>
|
<div className={styles["chat-input-actions"]}>
|
||||||
{couldStop && (
|
{couldStop && (
|
||||||
|
@ -515,8 +531,8 @@ export function ChatActions(props: {
|
||||||
<Selector
|
<Selector
|
||||||
defaultSelectedValue={currentModel}
|
defaultSelectedValue={currentModel}
|
||||||
items={models.map((m) => ({
|
items={models.map((m) => ({
|
||||||
title: m,
|
title: m.displayName,
|
||||||
value: m,
|
value: m.name,
|
||||||
}))}
|
}))}
|
||||||
onClose={() => setShowModelSelector(false)}
|
onClose={() => setShowModelSelector(false)}
|
||||||
onSelection={(s) => {
|
onSelection={(s) => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { Avatar } from "./emoji";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import NextImage from "next/image";
|
import NextImage from "next/image";
|
||||||
|
|
||||||
import { toBlob, toJpeg, toPng } from "html-to-image";
|
import { toBlob, toPng } from "html-to-image";
|
||||||
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
import { DEFAULT_MASK_AVATAR } from "../store/mask";
|
||||||
import { api } from "../client/api";
|
import { api } from "../client/api";
|
||||||
import { prettyObject } from "../utils/format";
|
import { prettyObject } from "../utils/format";
|
||||||
|
@ -41,7 +41,22 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
export function ExportMessageModal(props: { onClose: () => void }) {
|
export function ExportMessageModal(props: { onClose: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div className="modal-mask">
|
<div className="modal-mask">
|
||||||
<Modal title={Locale.Export.Title} onClose={props.onClose}>
|
<Modal
|
||||||
|
title={Locale.Export.Title}
|
||||||
|
onClose={props.onClose}
|
||||||
|
footer={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
textAlign: "center",
|
||||||
|
fontSize: 14,
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
只有清除上下文之后的消息会被展示
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div style={{ minHeight: "40vh" }}>
|
<div style={{ minHeight: "40vh" }}>
|
||||||
<MessageExporter />
|
<MessageExporter />
|
||||||
</div>
|
</div>
|
||||||
|
@ -149,7 +164,7 @@ export function MessageExporter() {
|
||||||
if (exportConfig.includeContext) {
|
if (exportConfig.includeContext) {
|
||||||
ret.push(...session.mask.context);
|
ret.push(...session.mask.context);
|
||||||
}
|
}
|
||||||
ret.push(...session.messages.filter((m, i) => selection.has(m.id)));
|
ret.push(...session.messages.filter((m) => selection.has(m.id)));
|
||||||
return ret;
|
return ret;
|
||||||
}, [
|
}, [
|
||||||
exportConfig.includeContext,
|
exportConfig.includeContext,
|
||||||
|
@ -437,13 +452,13 @@ export function ImagePreviewer(props: {
|
||||||
showToast(Locale.Export.Image.Toast);
|
showToast(Locale.Export.Image.Toast);
|
||||||
const dom = previewRef.current;
|
const dom = previewRef.current;
|
||||||
if (!dom) return;
|
if (!dom) return;
|
||||||
|
|
||||||
const isApp = getClientConfig()?.isApp;
|
const isApp = getClientConfig()?.isApp;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await toPng(dom);
|
const blob = await toPng(dom);
|
||||||
if (!blob) return;
|
if (!blob) return;
|
||||||
|
|
||||||
if (isMobile || (isApp && window.__TAURI__)) {
|
if (isMobile || (isApp && window.__TAURI__)) {
|
||||||
if (isApp && window.__TAURI__) {
|
if (isApp && window.__TAURI__) {
|
||||||
const result = await window.__TAURI__.dialog.save({
|
const result = await window.__TAURI__.dialog.save({
|
||||||
|
@ -459,7 +474,7 @@ export function ImagePreviewer(props: {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result !== null) {
|
if (result !== null) {
|
||||||
const response = await fetch(blob);
|
const response = await fetch(blob);
|
||||||
const buffer = await response.arrayBuffer();
|
const buffer = await response.arrayBuffer();
|
||||||
|
|
|
@ -58,8 +58,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
flex-grow: 1;
|
flex: 1;
|
||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 80px);
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -71,6 +71,12 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
|
@ -73,11 +73,23 @@ export function MessageSelector(props: {
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const session = chatStore.currentSession();
|
const session = chatStore.currentSession();
|
||||||
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
||||||
const messages = session.messages.filter(
|
const allMessages = useMemo(() => {
|
||||||
(m, i) =>
|
let startIndex = Math.max(0, session.clearContextIndex ?? 0);
|
||||||
m.id && // message must have id
|
if (startIndex === session.messages.length - 1) {
|
||||||
isValid(m) &&
|
startIndex = 0;
|
||||||
(i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
|
}
|
||||||
|
return session.messages.slice(startIndex);
|
||||||
|
}, [session.messages, session.clearContextIndex]);
|
||||||
|
|
||||||
|
const messages = useMemo(
|
||||||
|
() =>
|
||||||
|
allMessages.filter(
|
||||||
|
(m, i) =>
|
||||||
|
m.id && // message must have id
|
||||||
|
isValid(m) &&
|
||||||
|
(i >= allMessages.length - 1 || isValid(allMessages[i + 1])),
|
||||||
|
),
|
||||||
|
[allMessages],
|
||||||
);
|
);
|
||||||
const messageCount = messages.length;
|
const messageCount = messages.length;
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
@ -176,6 +188,8 @@ export function MessageSelector(props: {
|
||||||
<div className={styles["messages"]}>
|
<div className={styles["messages"]}>
|
||||||
{messages.map((m, i) => {
|
{messages.map((m, i) => {
|
||||||
if (!isInSearchResult(m.id!)) return null;
|
if (!isInSearchResult(m.id!)) return null;
|
||||||
|
const id = m.id ?? i;
|
||||||
|
const isSelected = props.selection.has(id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -185,7 +199,6 @@ export function MessageSelector(props: {
|
||||||
key={i}
|
key={i}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.updateSelection((selection) => {
|
props.updateSelection((selection) => {
|
||||||
const id = m.id ?? i;
|
|
||||||
selection.has(id) ? selection.delete(id) : selection.add(id);
|
selection.has(id) ? selection.delete(id) : selection.add(id);
|
||||||
});
|
});
|
||||||
onClickIndex(i);
|
onClickIndex(i);
|
||||||
|
@ -206,6 +219,10 @@ export function MessageSelector(props: {
|
||||||
{m.content}
|
{m.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["checkbox"]}>
|
||||||
|
<input type="checkbox" checked={isSelected}></input>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -97,8 +97,9 @@ export function Loading() {
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
title: string;
|
title: string;
|
||||||
children?: any;
|
children?: any;
|
||||||
actions?: JSX.Element[];
|
actions?: React.ReactNode[];
|
||||||
defaultMax?: boolean;
|
defaultMax?: boolean;
|
||||||
|
footer?: React.ReactNode;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
export function Modal(props: ModalProps) {
|
export function Modal(props: ModalProps) {
|
||||||
|
@ -147,6 +148,7 @@ export function Modal(props: ModalProps) {
|
||||||
<div className={styles["modal-content"]}>{props.children}</div>
|
<div className={styles["modal-content"]}>{props.children}</div>
|
||||||
|
|
||||||
<div className={styles["modal-footer"]}>
|
<div className={styles["modal-footer"]}>
|
||||||
|
{props.footer}
|
||||||
<div className={styles["modal-actions"]}>
|
<div className={styles["modal-actions"]}>
|
||||||
{props.actions?.map((action, i) => (
|
{props.actions?.map((action, i) => (
|
||||||
<div key={i} className={styles["modal-action"]}>
|
<div key={i} className={styles["modal-action"]}>
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function useAllModels() {
|
||||||
const models = useMemo(() => {
|
const models = useMemo(() => {
|
||||||
return collectModels(
|
return collectModels(
|
||||||
configStore.models,
|
configStore.models,
|
||||||
[accessStore.customModels, configStore.customModels].join(","),
|
[configStore.customModels, accessStore.customModels].join(","),
|
||||||
);
|
);
|
||||||
}, [accessStore.customModels, configStore.customModels, configStore.models]);
|
}, [accessStore.customModels, configStore.customModels, configStore.models]);
|
||||||
|
|
||||||
|
|
|
@ -4,21 +4,34 @@ export function collectModelTable(
|
||||||
models: readonly LLMModel[],
|
models: readonly LLMModel[],
|
||||||
customModels: string,
|
customModels: string,
|
||||||
) {
|
) {
|
||||||
const modelTable: Record<string, boolean> = {};
|
const modelTable: Record<
|
||||||
|
string,
|
||||||
|
{ available: boolean; name: string; displayName: string }
|
||||||
|
> = {};
|
||||||
|
|
||||||
// default models
|
// default models
|
||||||
models.forEach((m) => (modelTable[m.name] = m.available));
|
models.forEach(
|
||||||
|
(m) =>
|
||||||
|
(modelTable[m.name] = {
|
||||||
|
...m,
|
||||||
|
displayName: m.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// server custom models
|
// server custom models
|
||||||
customModels
|
customModels
|
||||||
.split(",")
|
.split(",")
|
||||||
.filter((v) => !!v && v.length > 0)
|
.filter((v) => !!v && v.length > 0)
|
||||||
.map((m) => {
|
.map((m) => {
|
||||||
if (m.startsWith("+")) {
|
const available = !m.startsWith("-");
|
||||||
modelTable[m.slice(1)] = true;
|
const nameConfig =
|
||||||
} else if (m.startsWith("-")) {
|
m.startsWith("+") || m.startsWith("-") ? m.slice(1) : m;
|
||||||
modelTable[m.slice(1)] = false;
|
const [name, displayName] = nameConfig.split(":");
|
||||||
} else modelTable[m] = true;
|
modelTable[name] = {
|
||||||
|
name,
|
||||||
|
displayName: displayName || name,
|
||||||
|
available,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
return modelTable;
|
return modelTable;
|
||||||
}
|
}
|
||||||
|
@ -31,10 +44,7 @@ export function collectModels(
|
||||||
customModels: string,
|
customModels: string,
|
||||||
) {
|
) {
|
||||||
const modelTable = collectModelTable(models, customModels);
|
const modelTable = collectModelTable(models, customModels);
|
||||||
const allModels = Object.keys(modelTable).map((m) => ({
|
const allModels = Object.values(modelTable);
|
||||||
name: m,
|
|
||||||
available: modelTable[m],
|
|
||||||
}));
|
|
||||||
|
|
||||||
return allModels;
|
return allModels;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue