Merge branch 'ChatGPTNextWeb:main' into main

This commit is contained in:
endless-learner 2024-09-24 23:03:03 -07:00 committed by GitHub
commit 47fb40d572
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 173 additions and 67 deletions

View File

@ -66,4 +66,4 @@ ANTHROPIC_API_VERSION=
ANTHROPIC_URL= ANTHROPIC_URL=
### (optional) ### (optional)
WHITE_WEBDEV_ENDPOINTS= WHITE_WEBDAV_ENDPOINTS=

View File

@ -340,7 +340,7 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
Change default model Change default model
### `WHITE_WEBDEV_ENDPOINTS` (optional) ### `WHITE_WEBDAV_ENDPOINTS` (optional)
You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format You can use this option if you want to increase the number of webdav service addresses you are allowed to access, as required by the format
- Each address must be a complete endpoint - Each address must be a complete endpoint

View File

@ -202,7 +202,7 @@ ByteDance Api Url.
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。 如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
### `WHITE_WEBDEV_ENDPOINTS` (可选) ### `WHITE_WEBDAV_ENDPOINTS` (可选)
如果你想增加允许访问的webdav服务地址可以使用该选项格式要求 如果你想增加允许访问的webdav服务地址可以使用该选项格式要求
- 每一个地址必须是一个完整的 endpoint - 每一个地址必须是一个完整的 endpoint

View File

@ -193,7 +193,7 @@ ByteDance API の URL。
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。 リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
### `WHITE_WEBDEV_ENDPOINTS` (オプション) ### `WHITE_WEBDAV_ENDPOINTS` (オプション)
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件: アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
- 各アドレスは完全なエンドポイントでなければなりません。 - 各アドレスは完全なエンドポイントでなければなりません。

View File

@ -6,7 +6,7 @@ const config = getServerSideConfig();
const mergedAllowedWebDavEndpoints = [ const mergedAllowedWebDavEndpoints = [
...internalAllowedWebDavEndpoints, ...internalAllowedWebDavEndpoints,
...config.allowedWebDevEndpoints, ...config.allowedWebDavEndpoints,
].filter((domain) => Boolean(domain.trim())); ].filter((domain) => Boolean(domain.trim()));
const normalizeUrl = (url: string) => { const normalizeUrl = (url: string) => {

View File

@ -277,6 +277,7 @@ export class ChatGPTApi implements LLMApi {
); );
} }
if (shouldStream) { if (shouldStream) {
let index = -1;
const [tools, funcs] = usePluginStore const [tools, funcs] = usePluginStore
.getState() .getState()
.getAsTools( .getAsTools(
@ -302,10 +303,10 @@ export class ChatGPTApi implements LLMApi {
}>; }>;
const tool_calls = choices[0]?.delta?.tool_calls; const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) { if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id; const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments; const args = tool_calls[0]?.function?.arguments;
if (id) { if (id) {
index += 1;
runTools.push({ runTools.push({
id, id,
type: tool_calls[0]?.type, type: tool_calls[0]?.type,
@ -327,6 +328,8 @@ export class ChatGPTApi implements LLMApi {
toolCallMessage: any, toolCallMessage: any,
toolCallResult: any[], toolCallResult: any[],
) => { ) => {
// reset index value
index = -1;
// @ts-ignore // @ts-ignore
requestPayload?.messages?.splice( requestPayload?.messages?.splice(
// @ts-ignore // @ts-ignore

View File

@ -21,6 +21,7 @@ import {
} from "./artifacts"; } from "./artifacts";
import { useChatStore } from "../store"; import { useChatStore } from "../store";
import { IconButton } from "./button"; import { IconButton } from "./button";
import { useAppConfig } from "../store/config";
export function Mermaid(props: { code: string }) { export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -92,7 +93,9 @@ export function PreCode(props: { children: any }) {
} }
}, 600); }, 600);
const enableArtifacts = session.mask?.enableArtifacts !== false; const config = useAppConfig();
const enableArtifacts =
session.mask?.enableArtifacts !== false && config.enableArtifacts;
//Wrap the paragraph for plain-text //Wrap the paragraph for plain-text
useEffect(() => { useEffect(() => {
@ -128,8 +131,9 @@ export function PreCode(props: { children: any }) {
className="copy-code-button" className="copy-code-button"
onClick={() => { onClick={() => {
if (ref.current) { if (ref.current) {
const code = ref.current.innerText; copyToClipboard(
copyToClipboard(code); ref.current.querySelector("code")?.innerText ?? "",
);
} }
}} }}
></span> ></span>
@ -278,6 +282,20 @@ function _MarkDownContent(props: { content: string }) {
p: (pProps) => <p {...pProps} dir="auto" />, p: (pProps) => <p {...pProps} dir="auto" />,
a: (aProps) => { a: (aProps) => {
const href = aProps.href || ""; const href = aProps.href || "";
if (/\.(aac|mp3|opus|wav)$/.test(href)) {
return (
<figure>
<audio controls src={href}></audio>
</figure>
);
}
if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
return (
<video controls width="99.9%">
<source src={href} />
</video>
);
}
const isInternal = /^\/#/i.test(href); const isInternal = /^\/#/i.test(href);
const target = isInternal ? "_self" : aProps.target ?? "_blank"; const target = isInternal ? "_self" : aProps.target ?? "_blank";
return <a {...aProps} target={target} />; return <a {...aProps} target={target} />;

View File

@ -166,6 +166,7 @@ export function MaskConfig(props: {
></input> ></input>
</ListItem> </ListItem>
{globalConfig.enableArtifacts && (
<ListItem <ListItem
title={Locale.Mask.Config.Artifacts.Title} title={Locale.Mask.Config.Artifacts.Title}
subTitle={Locale.Mask.Config.Artifacts.SubTitle} subTitle={Locale.Mask.Config.Artifacts.SubTitle}
@ -181,6 +182,7 @@ export function MaskConfig(props: {
}} }}
></input> ></input>
</ListItem> </ListItem>
)}
{!props.shouldSyncFromGlobal ? ( {!props.shouldSyncFromGlobal ? (
<ListItem <ListItem

View File

@ -10,7 +10,29 @@
max-height: 240px; max-height: 240px;
overflow-y: auto; overflow-y: auto;
white-space: pre-wrap; white-space: pre-wrap;
min-width: 300px; min-width: 280px;
} }
} }
.plugin-schema {
display: flex;
justify-content: flex-end;
flex-direction: row;
input {
margin-right: 20px;
@media screen and (max-width: 600px) {
margin-right: 0px;
}
}
@media screen and (max-width: 600px) {
flex-direction: column;
gap: 5px;
button {
padding: 10px;
}
}
}

View File

@ -12,7 +12,6 @@ import EditIcon from "../icons/edit.svg";
import AddIcon from "../icons/add.svg"; import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg"; import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg"; import DeleteIcon from "../icons/delete.svg";
import EyeIcon from "../icons/eye.svg";
import ConfirmIcon from "../icons/confirm.svg"; import ConfirmIcon from "../icons/confirm.svg";
import ReloadIcon from "../icons/reload.svg"; import ReloadIcon from "../icons/reload.svg";
import GithubIcon from "../icons/github.svg"; import GithubIcon from "../icons/github.svg";
@ -29,7 +28,6 @@ import {
import Locale from "../locales"; import Locale from "../locales";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useState } from "react"; import { useState } from "react";
import { getClientConfig } from "../config/client";
export function PluginPage() { export function PluginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -209,19 +207,11 @@ export function PluginPage() {
</div> </div>
</div> </div>
<div className={styles["mask-actions"]}> <div className={styles["mask-actions"]}>
{m.builtin ? (
<IconButton
icon={<EyeIcon />}
text={Locale.Plugin.Item.View}
onClick={() => setEditingPluginId(m.id)}
/>
) : (
<IconButton <IconButton
icon={<EditIcon />} icon={<EditIcon />}
text={Locale.Plugin.Item.Edit} text={Locale.Plugin.Item.Edit}
onClick={() => setEditingPluginId(m.id)} onClick={() => setEditingPluginId(m.id)}
/> />
)}
{!m.builtin && ( {!m.builtin && (
<IconButton <IconButton
icon={<DeleteIcon />} icon={<DeleteIcon />}
@ -325,30 +315,13 @@ export function PluginPage() {
></PasswordInput> ></PasswordInput>
</ListItem> </ListItem>
)} )}
{!getClientConfig()?.isApp && (
<ListItem
title={Locale.Plugin.Auth.Proxy}
subTitle={Locale.Plugin.Auth.ProxyDescription}
>
<input
type="checkbox"
checked={editingPlugin?.usingProxy}
style={{ minWidth: 16 }}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.usingProxy = e.currentTarget.checked;
});
}}
></input>
</ListItem>
)}
</List> </List>
<List> <List>
<ListItem title={Locale.Plugin.EditModal.Content}> <ListItem title={Locale.Plugin.EditModal.Content}>
<div style={{ display: "flex", justifyContent: "flex-end" }}> <div className={pluginStyles["plugin-schema"]}>
<input <input
type="text" type="text"
style={{ minWidth: 200, marginRight: 20 }} style={{ minWidth: 200 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)} onInput={(e) => setLoadUrl(e.currentTarget.value)}
></input> ></input>
<IconButton <IconButton

View File

@ -1465,6 +1465,23 @@ export function Settings() {
} }
></input> ></input>
</ListItem> </ListItem>
<ListItem
title={Locale.Mask.Config.Artifacts.Title}
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Artifacts.Title}
type="checkbox"
checked={config.enableArtifacts}
onChange={(e) =>
updateConfig(
(config) =>
(config.enableArtifacts = e.currentTarget.checked),
)
}
></input>
</ListItem>
</List> </List>
<SyncItems /> <SyncItems />

View File

@ -154,8 +154,8 @@ export const getServerSideConfig = () => {
// `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`, // `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
// ); // );
const allowedWebDevEndpoints = ( const allowedWebDavEndpoints = (
process.env.WHITE_WEBDEV_ENDPOINTS ?? "" process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
).split(","); ).split(",");
return { return {
@ -229,6 +229,6 @@ export const getServerSideConfig = () => {
disableFastLink: !!process.env.DISABLE_FAST_LINK, disableFastLink: !!process.env.DISABLE_FAST_LINK,
customModels, customModels,
defaultModel, defaultModel,
allowedWebDevEndpoints, allowedWebDavEndpoints,
}; };
}; };

View File

@ -615,6 +615,7 @@ export const useChatStore = createPersistStore(
providerName, providerName,
}, },
onFinish(message) { onFinish(message) {
if (!isValidMessage(message)) return;
get().updateCurrentSession( get().updateCurrentSession(
(session) => (session) =>
(session.topic = (session.topic =
@ -690,6 +691,10 @@ export const useChatStore = createPersistStore(
}, },
}); });
} }
function isValidMessage(message: any): boolean {
return typeof message === "string" && !message.startsWith("```json");
}
}, },
updateStat(message: ChatMessage) { updateStat(message: ChatMessage) {

View File

@ -50,6 +50,8 @@ export const DEFAULT_CONFIG = {
enableAutoGenerateTitle: true, enableAutoGenerateTitle: true,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH, sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
enableArtifacts: true, // show artifacts config
disablePromptHint: false, disablePromptHint: false,
dontShowMaskSplashScreen: false, // dont show splash screen when create chat dontShowMaskSplashScreen: false, // dont show splash screen when create chat

View File

@ -2,8 +2,12 @@ import OpenAPIClientAxios from "openapi-client-axios";
import { StoreKey } from "../constant"; import { StoreKey } from "../constant";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { getClientConfig } from "../config/client";
import yaml from "js-yaml"; import yaml from "js-yaml";
import { adapter } from "../utils"; import { adapter } from "../utils";
import { useAccessStore } from "./access";
const isApp = getClientConfig()?.isApp;
export type Plugin = { export type Plugin = {
id: string; id: string;
@ -16,7 +20,6 @@ export type Plugin = {
authLocation?: string; authLocation?: string;
authHeader?: string; authHeader?: string;
authToken?: string; authToken?: string;
usingProxy?: boolean;
}; };
export type FunctionToolItem = { export type FunctionToolItem = {
@ -51,13 +54,20 @@ export const FunctionToolService = {
const authLocation = plugin?.authLocation || "header"; const authLocation = plugin?.authLocation || "header";
const definition = yaml.load(plugin.content) as any; const definition = yaml.load(plugin.content) as any;
const serverURL = definition?.servers?.[0]?.url; const serverURL = definition?.servers?.[0]?.url;
const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL; const baseURL = !isApp ? "/api/proxy" : serverURL;
const headers: Record<string, string | undefined> = { const headers: Record<string, string | undefined> = {
"X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined, "X-Base-URL": !isApp ? serverURL : undefined,
}; };
if (authLocation == "header") { if (authLocation == "header") {
headers[headerName] = tokenValue; headers[headerName] = tokenValue;
} }
// try using openaiApiKey for Dalle3 Plugin.
if (!tokenValue && plugin.id === "dalle3") {
const openaiApiKey = useAccessStore.getState().openaiApiKey;
if (openaiApiKey) {
headers[headerName] = `Bearer ${openaiApiKey}`;
}
}
const api = new OpenAPIClientAxios({ const api = new OpenAPIClientAxios({
definition: yaml.load(plugin.content) as any, definition: yaml.load(plugin.content) as any,
axiosConfigDefaults: { axiosConfigDefaults: {
@ -165,7 +175,7 @@ export const usePluginStore = createPersistStore(
(set, get) => ({ (set, get) => ({
create(plugin?: Partial<Plugin>) { create(plugin?: Partial<Plugin>) {
const plugins = get().plugins; const plugins = get().plugins;
const id = nanoid(); const id = plugin?.id || nanoid();
plugins[id] = { plugins[id] = {
...createEmptyPlugin(), ...createEmptyPlugin(),
...plugin, ...plugin,
@ -220,5 +230,42 @@ export const usePluginStore = createPersistStore(
{ {
name: StoreKey.Plugin, name: StoreKey.Plugin,
version: 1, version: 1,
onRehydrateStorage(state) {
// Skip store rehydration on server side
if (typeof window === "undefined") {
return;
}
fetch("./plugins.json")
.then((res) => res.json())
.then((res) => {
Promise.all(
res.map((item: any) =>
// skip get schema
state.get(item.id)
? item
: fetch(item.schema)
.then((res) => res.text())
.then((content) => ({
...item,
content,
}))
.catch((e) => item),
),
).then((builtinPlugins: any) => {
builtinPlugins
.filter((item: any) => item?.content)
.forEach((item: any) => {
const plugin = state.create(item);
state.updatePlugin(plugin.id, (plugin) => {
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
plugin.builtin = true;
});
});
});
});
},
}, },
); );

17
public/plugins.json Normal file
View File

@ -0,0 +1,17 @@
[
{
"id": "dalle3",
"name": "Dalle3",
"schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/dalle/openapi.json"
},
{
"id": "arxivsearch",
"name": "ArxivSearch",
"schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/arxivsearch/openapi.json"
},
{
"id": "duckduckgolite",
"name": "DuckDuckGoLiteSearch",
"schema": "https://ghp.ci/https://raw.githubusercontent.com/ChatGPTNextWeb/NextChat-Awesome-Plugins/main/plugins/duckduckgolite/openapi.json"
}
]