Compare commits

..

45 Commits

Author SHA1 Message Date
lloydzhou
23ac2efd89 hotfix and update version 2024-09-07 22:12:42 +08:00
Lloyd Zhou
daeffb2dc6 Merge pull request #5383 from SukkaW/fix-5378
fix(#5378): default plugin ids to empty array
2024-09-07 22:09:35 +08:00
SukkaW
db58ca6c1d fix(#5378): default plugin ids to empty array 2024-09-07 21:32:18 +08:00
Lloyd Zhou
2ff292cbfa Merge pull request #5381 from reggiezhang/patch-1
Add crossOrigin="use-credentials" for site.webmanifest
2024-09-07 16:58:07 +08:00
Reggie Zhang
5a81393863 Add crossOrigin="use-credentials" for site.webmanifest
Add `crossOrigin="use-credentials"` to the `<link>` element for `site.webmanifest` when the site is behind a proxy with authentication.
2024-09-07 16:24:52 +08:00
Lloyd Zhou
116a73d398 Merge pull request #5377 from ConnectAI-E/hotfix/mermaid
hotfix Mermaid can not render. close #5374
2024-09-07 13:01:36 +08:00
lloydzhou
cf0c057164 hotfix Mermaid can not render. close #5374 2024-09-07 13:00:55 +08:00
Lloyd Zhou
fe5a4f4447 Merge pull request #5375 from Kosette/fix-version
fix: update package version
2024-09-07 11:55:23 +08:00
kosette
27828d9ca8 fix: update package version 2024-09-06 23:07:01 +08:00
Dogtiti
2bd799fac6 Merge pull request #5331 from ConnectAI-E/feature/plugin
Feature plugin (GPTs like action based on function call)
2024-09-06 20:06:50 +08:00
lloydzhou
9275f2d753 add awesome plugin repo url 2024-09-06 19:37:24 +08:00
lloydzhou
7455978ee5 default enable artifact 2024-09-06 09:26:06 +08:00
lloydzhou
7c0acc7b77 hotfix tools empty array 2024-09-05 22:02:06 +08:00
Lloyd Zhou
f32dd69acf Merge pull request #7 from ConnectAI-E/feature/plugin-artifact
Feature/plugin artifact
2024-09-05 14:54:01 +08:00
lloydzhou
80b8f956a9 move artifacts into mask settings 2024-09-05 14:49:11 +08:00
lloydzhou
caf50b6e6c move artifacts into mask settings 2024-09-05 14:46:16 +08:00
lloydzhou
b590d0857c disable nextjs proxy, then can using dalle as plugin 2024-09-05 13:51:59 +08:00
Lloyd Zhou
982019307c Merge pull request #6 from ConnectAI-E/feature/plugin-app-cors
using tauri http api run plugin to fixed cors in App
2024-09-04 22:49:35 +08:00
lloydzhou
09aec7b22e using tauri http api run plugin to fixed cors in App 2024-09-04 21:32:22 +08:00
lloydzhou
f9a047aad4 using tauri http api run plugin to fixed cors in App 2024-09-04 21:04:13 +08:00
Lloyd Zhou
85704570f3 Merge pull request #5356 from ConnectAI-E/feature/indexdb
fix: hydrated for indexedDB
2024-09-04 19:14:18 +08:00
lloydzhou
53dcae9e9c update 2024-09-04 13:00:18 +08:00
lloydzhou
04e1ab63bb update readme 2024-09-04 11:47:42 +08:00
Dogtiti
ed9aae531e fix: hydrated 2024-09-03 20:29:01 +08:00
lloydzhou
6ab6b3dbca remove no need code 2024-09-03 20:21:37 +08:00
lloydzhou
7180ed9a60 hotfix 2024-09-03 19:56:22 +08:00
lloydzhou
0a5522d28c update 2024-09-03 19:35:36 +08:00
lloydzhou
c7bc93b32b Merge remote-tracking branch 'connectai/main' into feature/plugin 2024-09-03 19:02:40 +08:00
lloydzhou
d30351e7b0 update readme 2024-09-03 17:18:43 +08:00
Dogtiti
886ffc0af8 fix: hydrated for indexedDB 2024-09-03 17:12:48 +08:00
lloydzhou
4fdd997108 hotfix 2024-09-03 16:23:54 +08:00
lloydzhou
236736deea remove no need code 2024-09-03 15:37:23 +08:00
lloydzhou
2b317f60c8 add config auth location 2024-09-03 12:00:55 +08:00
lloydzhou
3ec67f9f47 add load from url 2024-09-03 00:45:11 +08:00
lloydzhou
6435e7a30e update readme 2024-09-02 23:42:56 +08:00
Dogtiti
97a4a910e0 Merge pull request #5335 from ConnectAI-E/fix/right-click
fix: right click
2024-08-29 20:49:31 +08:00
Dogtiti
19c7a84548 fix: right click 2024-08-29 20:48:04 +08:00
Lloyd Zhou
b6bb1673d4 Merge pull request #5324 from ConnectAI-E/feature/indexdb
feat: add indexDB
2024-08-27 18:07:20 +08:00
Dogtiti
7b6fe66f2a feat: try catch indexedDB error 2024-08-27 10:05:37 +08:00
Dogtiti
c2fc0b4979 feat: try catch indexedDB error 2024-08-27 09:57:07 +08:00
Dogtiti
0b758941a4 feat: clear indexDB 2024-08-26 21:23:21 +08:00
Dogtiti
492b55c893 feat: add indexDB 2024-08-26 21:20:07 +08:00
Dogtiti
4060e367ad feat: add indexDB 2024-08-26 21:13:35 +08:00
Lloyd Zhou
718782f5b1 Merge pull request #5309 from ElricLiu/main
Update README.md
2024-08-24 12:11:46 +08:00
ElricLiu
0c3fb5b2ce Update README.md
add monica sponsored
2024-08-23 17:28:00 +08:00
28 changed files with 358 additions and 109 deletions

View File

@@ -30,6 +30,8 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
</div>
## Enterprise Edition
@@ -89,13 +91,13 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] Desktop App with tauri
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] artifacts
- [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [ ] local knowledge base
## What's New
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 Now supports Artifacts & SD
- 🚀 v2.10.1 support Google Gemini Pro model.
- 🚀 v2.9.11 you can use azure endpoint now.
@@ -126,13 +128,13 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] 使用 tauri 打包桌面应用
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] 插件机制,支持 artifacts联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] artifacts
- [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [x] 插件机制,支持`联网搜索``计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [ ] 本地知识库
## 最新动态
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。

View File

@@ -38,7 +38,6 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
console.log("[Auth] hashed access code:", hashedCode);
console.log("[User IP] ", getIP(req));
console.log("[Time] ", new Date().toLocaleString());
console.log("[ModelProvider] ", modelProvider);
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !apiKey) {
return {

View File

@@ -203,9 +203,8 @@ export class ClaudeApi implements LLMApi {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin as string[],
useChatStore.getState().currentSession().mask?.plugin || [],
);
console.log("getAsTools", tools, funcs);
return stream(
path,
requestBody,
@@ -271,6 +270,8 @@ export class ClaudeApi implements LLMApi {
toolCallMessage: any,
toolCallResult: any[],
) => {
// reset index value
index = -1;
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
@@ -283,7 +284,9 @@ export class ClaudeApi implements LLMApi {
type: "tool_use",
id: tool.id,
name: tool?.function?.name,
input: JSON.parse(tool?.function?.arguments as string),
input: tool?.function?.arguments
? JSON.parse(tool?.function?.arguments)
: {},
}),
),
},

View File

@@ -125,9 +125,8 @@ export class MoonshotApi implements LLMApi {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin as string[],
useChatStore.getState().currentSession().mask?.plugin || [],
);
console.log("getAsTools", tools, funcs);
return stream(
chatPath,
requestPayload,

View File

@@ -244,7 +244,7 @@ export class ChatGPTApi implements LLMApi {
const [tools, funcs] = usePluginStore
.getState()
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin as string[],
useChatStore.getState().currentSession().mask?.plugin || [],
);
// console.log("getAsTools", tools, funcs);
stream(

View File

@@ -98,7 +98,6 @@ import {
REQUEST_TIMEOUT_MS,
UNFINISHED_INPUT,
ServiceProvider,
ArtifactsPlugin,
} from "../constant";
import { Avatar } from "./emoji";
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
@@ -727,38 +726,32 @@ export function ChatActions(props: {
/>
)}
<ChatAction
onClick={() => setShowPluginSelector(true)}
text={Locale.Plugin.Name}
icon={<PluginIcon />}
/>
{showPlugins(currentProviderName, currentModel) && (
<ChatAction
onClick={() => {
if (pluginStore.getAll().length == 0) {
navigate(Path.Plugins);
} else {
setShowPluginSelector(true);
}
}}
text={Locale.Plugin.Name}
icon={<PluginIcon />}
/>
)}
{showPluginSelector && (
<Selector
multiple
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
items={[
{
title: Locale.Plugin.Artifacts,
value: ArtifactsPlugin.Artifacts as string,
},
].concat(
showPlugins(currentProviderName, currentModel)
? pluginStore.getAll().map((item) => ({
// @ts-ignore
title: `${item?.title}@${item?.version}`,
// @ts-ignore
value: item?.id,
}))
: [],
)}
items={pluginStore.getAll().map((item) => ({
title: `${item?.title}@${item?.version}`,
value: item?.id,
}))}
onClose={() => setShowPluginSelector(false)}
onSelection={(s) => {
chatStore.updateCurrentSession((session) => {
session.mask.plugin = s as string[];
});
if (s.includes(ArtifactsPlugin.Artifacts)) {
showToast(ArtifactsPlugin.Artifacts);
}
}}
/>
)}
@@ -1619,7 +1612,7 @@ function _Chat() {
message.content.length === 0 &&
!isUser
}
onContextMenu={(e) => onRightClick(e, message)}
// onContextMenu={(e) => onRightClick(e, message)} // hard to use
onDoubleClickCapture={() => {
if (!isMobileScreen) return;
setUserInput(getMessageTextContent(message));

View File

@@ -19,7 +19,6 @@ import {
HTMLPreview,
HTMLPreviewHander,
} from "./artifacts";
import { ArtifactsPlugin } from "../constant";
import { useChatStore } from "../store";
import { IconButton } from "./button";
@@ -77,7 +76,6 @@ export function PreCode(props: { children: any }) {
const { height } = useWindowSize();
const chatStore = useChatStore();
const session = chatStore.currentSession();
const plugins = session.mask?.plugin;
const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return;
@@ -94,10 +92,7 @@ export function PreCode(props: { children: any }) {
}
}, 600);
const enableArtifacts = useMemo(
() => plugins?.includes(ArtifactsPlugin.Artifacts),
[plugins],
);
const enableArtifacts = session.mask?.enableArtifacts !== false;
//Wrap the paragraph for plain-text
useEffect(() => {
@@ -168,7 +163,7 @@ export function PreCode(props: { children: any }) {
);
}
function CustomCode(props: { children: any }) {
function CustomCode(props: { children: any; className?: string }) {
const ref = useRef<HTMLPreElement>(null);
const [collapsed, setCollapsed] = useState(true);
const [showToggle, setShowToggle] = useState(false);
@@ -187,6 +182,7 @@ function CustomCode(props: { children: any }) {
return (
<>
<code
className={props?.className}
ref={ref}
style={{
maxHeight: collapsed ? "400px" : "none",

View File

@@ -167,6 +167,22 @@ export function MaskConfig(props: {
></input>
</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={props.mask.enableArtifacts !== false}
onChange={(e) => {
props.updateMask((mask) => {
mask.enableArtifacts = e.currentTarget.checked;
});
}}
></input>
</ListItem>
{!props.shouldSyncFromGlobal ? (
<ListItem
title={Locale.Mask.Config.Share.Title}

View File

@@ -10,6 +10,7 @@
max-height: 240px;
overflow-y: auto;
white-space: pre-wrap;
min-width: 300px;
}
}

View File

@@ -1,39 +1,35 @@
import { useDebouncedCallback } from "use-debounce";
import OpenAPIClientAxios from "openapi-client-axios";
import yaml from "js-yaml";
import { PLUGINS_REPO_URL } from "../constant";
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import pluginStyles from "./plugin.module.scss";
import DownloadIcon from "../icons/download.svg";
import EditIcon from "../icons/edit.svg";
import AddIcon from "../icons/add.svg";
import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import EyeIcon from "../icons/eye.svg";
import CopyIcon from "../icons/copy.svg";
import ConfirmIcon from "../icons/confirm.svg";
import ReloadIcon from "../icons/reload.svg";
import GithubIcon from "../icons/github.svg";
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
import {
Input,
PasswordInput,
List,
ListItem,
Modal,
Popover,
Select,
showConfirm,
showToast,
} from "./ui-lib";
import { downloadAs } from "../utils";
import Locale from "../locales";
import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { Path } from "../constant";
import { nanoid } from "nanoid";
import { getClientConfig } from "../config/client";
export function PluginPage() {
const navigate = useNavigate();
@@ -90,6 +86,37 @@ export function PluginPage() {
}
}, 100).bind(null, editingPlugin);
const [loadUrl, setLoadUrl] = useState<string>("");
const loadFromUrl = (loadUrl: string) =>
fetch(loadUrl)
.catch((e) => {
const p = new URL(loadUrl);
return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
headers: {
"X-Base-URL": p.origin,
},
});
})
.then((res) => res.text())
.then((content) => {
try {
return JSON.stringify(JSON.parse(content), null, " ");
} catch (e) {
return content;
}
})
.then((content) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.content = content;
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
});
})
.catch((e) => {
showToast(Locale.Plugin.EditModal.Error);
});
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
@@ -104,6 +131,15 @@ export function PluginPage() {
</div>
<div className="window-actions">
<div className="window-action-button">
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
@@ -137,6 +173,26 @@ export function PluginPage() {
</div>
<div>
{plugins.length == 0 && (
<div
style={{
display: "flex",
margin: "60px auto",
alignItems: "center",
justifyContent: "center",
}}
>
{Locale.Plugin.Page.Find}
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
style={{ marginLeft: 16 }}
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
)}
{plugins.map((m) => (
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
@@ -217,6 +273,30 @@ export function PluginPage() {
<option value="custom">{Locale.Plugin.Auth.Custom}</option>
</select>
</ListItem>
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Location}>
<select
value={editingPlugin?.authLocation}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authLocation = e.target.value;
});
}}
>
<option value="header">
{Locale.Plugin.Auth.LocationHeader}
</option>
<option value="query">
{Locale.Plugin.Auth.LocationQuery}
</option>
<option value="body">
{Locale.Plugin.Auth.LocationBody}
</option>
</select>
</ListItem>
)}
{editingPlugin.authType == "custom" && (
<ListItem title={Locale.Plugin.Auth.CustomHeader}>
<input
@@ -245,25 +325,41 @@ export function PluginPage() {
></PasswordInput>
</ListItem>
)}
<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>
{!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>
<ListItem title={Locale.Plugin.EditModal.Content}>
<div style={{ display: "flex", justifyContent: "flex-end" }}>
<input
type="text"
style={{ minWidth: 200, marginRight: 20 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)}
></input>
<IconButton
icon={<ReloadIcon />}
text={Locale.Plugin.EditModal.Load}
bordered
onClick={() => loadFromUrl(loadUrl)}
/>
</div>
</ListItem>
<ListItem
title={Locale.Plugin.EditModal.Content}
subTitle={
<div
className={`markdown-body ${pluginStyles["plugin-content"]}`}

View File

@@ -50,7 +50,7 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
}
export function ListItem(props: {
title: string;
title?: string;
subTitle?: string | JSX.Element;
children?: JSX.Element | JSX.Element[];
icon?: JSX.Element;

View File

@@ -3,6 +3,7 @@ import path from "path";
export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const RELEASE_URL = `${REPO_URL}/releases`;
@@ -73,10 +74,6 @@ export enum FileName {
Prompts = "prompts.json",
}
export enum ArtifactsPlugin {
Artifacts = "artifacts",
}
export enum StoreKey {
Chat = "chat-next-web-store",
Plugin = "chat-next-web-plugin",

8
app/global.d.ts vendored
View File

@@ -21,10 +21,16 @@ declare interface Window {
writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
writeTextFile(path: string, data: string): Promise<void>;
};
notification:{
notification: {
requestPermission(): Promise<Permission>;
isPermissionGranted(): Promise<boolean>;
sendNotification(options: string | Options): void;
};
http: {
fetch<T>(
url: string,
options?: Record<string, unknown>,
): Promise<Response<T>>;
};
};
}

View File

@@ -41,7 +41,7 @@ export default function RootLayout({
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<link rel="manifest" href="/site.webmanifest"></link>
<link rel="manifest" href="/site.webmanifest" crossOrigin="use-credentials"></link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>
<body>

View File

@@ -532,12 +532,12 @@ const cn = {
},
Plugin: {
Name: "插件",
Artifacts: "Artifacts",
Page: {
Title: "插件",
SubTitle: (count: number) => `${count} 个插件`,
Search: "搜索插件",
Create: "新建",
Find: "您可以在Github上找到优秀的插件",
},
Item: {
Info: (count: number) => `${count} 方法`,
@@ -551,16 +551,21 @@ const cn = {
Basic: "Basic",
Bearer: "Bearer",
Custom: "自定义",
CustomHeader: "自定义",
CustomHeader: "自定义参数名称",
Token: "Token",
Proxy: "使用代理",
ProxyDescription: "使用代理解决 CORS 错误",
Location: "位置",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
Download: "下载",
Auth: "授权方式",
Content: "OpenAPI Schema",
Load: "从网页加载",
Method: "方法",
Error: "格式错误",
},
@@ -599,6 +604,10 @@ const cn = {
Title: "隐藏预设对话",
SubTitle: "隐藏后预设对话不会出现在聊天界面",
},
Artifacts: {
Title: "启用Artifacts",
SubTitle: "启用之后可以直接渲染HTML页面",
},
Share: {
Title: "分享此面具",
SubTitle: "生成此面具的直达链接",

View File

@@ -540,12 +540,12 @@ const en: LocaleType = {
},
Plugin: {
Name: "Plugin",
Artifacts: "Artifacts",
Page: {
Title: "Plugins",
SubTitle: (count: number) => `${count} plugins`,
Search: "Search Plugin",
Create: "Create",
Find: "You can find awesome plugins on github: ",
},
Item: {
Info: (count: number) => `${count} method`,
@@ -559,10 +559,14 @@ const en: LocaleType = {
Basic: "Basic",
Bearer: "Bearer",
Custom: "Custom",
CustomHeader: "Custom Header",
CustomHeader: "Parameter Name",
Token: "Token",
Proxy: "Using Proxy",
ProxyDescription: "Using proxies to solve CORS error",
Location: "Location",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) =>
@@ -570,6 +574,7 @@ const en: LocaleType = {
Download: "Download",
Auth: "Authentication Type",
Content: "OpenAPI Schema",
Load: "Load From URL",
Method: "Method",
Error: "OpenAPI Schema Error",
},
@@ -608,6 +613,10 @@ const en: LocaleType = {
Title: "Hide Context Prompts",
SubTitle: "Do not show in-context prompts in chat",
},
Artifacts: {
Title: "Enable Artifacts",
SubTitle: "Can render HTML page when enable artifacts.",
},
Share: {
Title: "Share This Mask",
SubTitle: "Generate a link to this mask",

View File

@@ -27,6 +27,7 @@ import { createPersistStore } from "../utils/store";
import { collectModelsWithDefaultModel } from "../utils/model";
import { useAccessStore } from "./access";
import { isDalle3 } from "../utils";
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
export type ChatMessageTool = {
id: string;
@@ -409,7 +410,6 @@ export const useChatStore = createPersistStore(
});
},
onAfterTool(tool: ChatMessageTool) {
console.log("onAfterTool", botMessage);
botMessage?.tools?.forEach((t, i, tools) => {
if (tool.id == t.id) {
tools[i] = { ...tool };
@@ -420,7 +420,7 @@ export const useChatStore = createPersistStore(
});
},
onError(error) {
const isAborted = error.message.includes("aborted");
const isAborted = error.message?.includes?.("aborted");
botMessage.content +=
"\n\n" +
prettyObject({
@@ -695,7 +695,8 @@ export const useChatStore = createPersistStore(
set(() => ({ sessions }));
},
clearAllData() {
async clearAllData() {
await indexedDBStorage.clear();
localStorage.clear();
location.reload();
},

View File

@@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, ChatMessage } from "./chat";
import { ModelConfig, useAppConfig } from "./config";
import { StoreKey, ArtifactsPlugin } from "../constant";
import { StoreKey } from "../constant";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
@@ -18,6 +18,7 @@ export type Mask = {
lang: Lang;
builtin: boolean;
plugin?: string[];
enableArtifacts?: boolean;
};
export const DEFAULT_MASK_STATE = {
@@ -38,7 +39,7 @@ export const createEmptyMask = () =>
lang: getLang(),
builtin: false,
createdAt: Date.now(),
plugin: [ArtifactsPlugin.Artifacts as string],
plugin: [],
}) as Mask;
export const useMaskStore = createPersistStore(

View File

@@ -4,6 +4,7 @@ import { StoreKey } from "../constant";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
import yaml from "js-yaml";
import { adapter } from "../utils";
export type Plugin = {
id: string;
@@ -13,6 +14,7 @@ export type Plugin = {
content: string;
builtin: boolean;
authType?: string;
authLocation?: string;
authHeader?: string;
authToken?: string;
usingProxy?: boolean;
@@ -47,19 +49,22 @@ export const FunctionToolService = {
: plugin?.authType == "bearer"
? ` Bearer ${plugin?.authToken}`
: plugin?.authToken;
const authLocation = plugin?.authLocation || "header";
const definition = yaml.load(plugin.content) as any;
const serverURL = definition?.servers?.[0]?.url;
const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
const headers: Record<string, string | undefined> = {
"X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
};
if (authLocation == "header") {
headers[headerName] = tokenValue;
}
const api = new OpenAPIClientAxios({
definition: yaml.load(plugin.content) as any,
axiosConfigDefaults: {
adapter: (window.__TAURI__ ? adapter : ["xhr"]) as any,
baseURL,
headers: {
// 'Cache-Control': 'no-cache',
// 'Content-Type': 'application/json', // TODO
[headerName]: tokenValue,
"X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
},
headers,
},
});
try {
@@ -111,20 +116,26 @@ export const FunctionToolService = {
funcs: operations.reduce((s, o) => {
// @ts-ignore
s[o.operationId] = function (args) {
const argument = [];
const parameters: Record<string, any> = {};
if (o.parameters instanceof Array) {
o.parameters.forEach((p) => {
// @ts-ignore
argument.push(args[p?.name]);
parameters[p?.name] = args[p?.name];
// @ts-ignore
delete args[p?.name];
});
} else {
argument.push(null);
}
argument.push(args);
if (authLocation == "query") {
parameters[headerName] = tokenValue;
} else if (authLocation == "body") {
args[headerName] = tokenValue;
}
// @ts-ignore
return api.client[o.operationId].apply(null, argument);
return api.client[o.operationId](
parameters,
args,
api.axiosConfigDefaults,
);
};
return s;
}, {}),
@@ -188,7 +199,7 @@ export const usePluginStore = createPersistStore(
getAsTools(ids: string[]) {
const plugins = get().plugins;
const selected = ids
const selected = (ids || [])
.map((id) => plugins[id])
.filter((i) => i)
.map((p) => FunctionToolService.add(p));

View File

@@ -2,7 +2,9 @@ import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib";
import Locale from "./locales";
import { RequestMessage } from "./client/api";
import { ServiceProvider } from "./constant";
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
import isObject from "lodash-es/isObject";
import { fetch as tauriFetch, Body, ResponseType } from "@tauri-apps/api/http";
export function trimTopic(topic: string) {
// Fix an issue where double quotes still show in the Indonesian language
@@ -285,3 +287,34 @@ export function showPlugins(provider: ServiceProvider, model: string) {
}
return false;
}
export function fetch(
url: string,
options?: Record<string, unknown>,
): Promise<any> {
if (window.__TAURI__) {
const payload = options?.body || options?.data;
return tauriFetch(url, {
...options,
body:
payload &&
({
type: "Text",
payload,
} as any),
timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
responseType:
options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
} as any);
}
return window.fetch(url, options);
}
export function adapter(config: Record<string, unknown>) {
const { baseURL, url, params, ...rest } = config;
const path = baseURL ? `${baseURL}${url}` : url;
const fetchUrl = params
? `${path}?${new URLSearchParams(params as any).toString()}`
: path;
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
}

View File

@@ -215,7 +215,9 @@ export function stream(
// @ts-ignore
funcs[tool.function.name](
// @ts-ignore
JSON.parse(tool.function.arguments),
tool?.function?.arguments
? JSON.parse(tool?.function?.arguments)
: {},
),
)
.then((res) => {
@@ -275,7 +277,7 @@ export function stream(
method: "POST",
body: JSON.stringify({
...requestPayload,
tools,
tools: tools && tools.length ? tools : undefined,
}),
signal: controller.signal,
headers,

View File

@@ -0,0 +1,44 @@
import { StateStorage } from "zustand/middleware";
import { get, set, del, clear } from "idb-keyval";
class IndexedDBStorage implements StateStorage {
public async getItem(name: string): Promise<string | null> {
try {
const value = (await get(name)) || localStorage.getItem(name);
return value;
} catch (error) {
return localStorage.getItem(name);
}
}
public async setItem(name: string, value: string): Promise<void> {
try {
const _value = JSON.parse(value);
if (!_value?.state?._hasHydrated) {
console.warn("skip setItem", name);
return;
}
await set(name, value);
} catch (error) {
localStorage.setItem(name, value);
}
}
public async removeItem(name: string): Promise<void> {
try {
await del(name);
} catch (error) {
localStorage.removeItem(name);
}
}
public async clear(): Promise<void> {
try {
await clear();
} catch (error) {
localStorage.clear();
}
}
}
export const indexedDBStorage = new IndexedDBStorage();

View File

@@ -1,7 +1,8 @@
import { create } from "zustand";
import { combine, persist } from "zustand/middleware";
import { combine, persist, createJSONStorage } from "zustand/middleware";
import { Updater } from "../typing";
import { deepClone } from "./clone";
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
type SecondParam<T> = T extends (
_f: infer _F,
@@ -13,9 +14,11 @@ type SecondParam<T> = T extends (
type MakeUpdater<T> = {
lastUpdateTime: number;
_hasHydrated: boolean;
markUpdate: () => void;
update: Updater<T>;
setHasHydrated: (state: boolean) => void;
};
type SetStoreState<T> = (
@@ -31,12 +34,20 @@ export function createPersistStore<T extends object, M>(
) => M,
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) {
persistOptions.storage = createJSONStorage(() => indexedDBStorage);
const oldOonRehydrateStorage = persistOptions?.onRehydrateStorage;
persistOptions.onRehydrateStorage = (state) => {
oldOonRehydrateStorage?.(state);
return () => state.setHasHydrated(true);
};
return create(
persist(
combine(
{
...state,
lastUpdateTime: 0,
_hasHydrated: false,
},
(set, get) => {
return {
@@ -55,6 +66,9 @@ export function createPersistStore<T extends object, M>(
lastUpdateTime: Date.now(),
});
},
setHasHydrated: (state: boolean) => {
set({ _hasHydrated: state } as Partial<T & M & MakeUpdater<T>>);
},
} as M & MakeUpdater<T>;
},
),

View File

@@ -65,10 +65,10 @@ if (mode !== "export") {
nextConfig.rewrites = async () => {
const ret = [
// adjust for previous version directly using "/api/proxy/" as proxy base route
{
source: "/api/proxy/v1/:path*",
destination: "https://api.openai.com/v1/:path*",
},
// {
// source: "/api/proxy/v1/:path*",
// destination: "https://api.openai.com/v1/:path*",
// },
{
// https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions
source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*",

View File

@@ -29,6 +29,7 @@
"fuse.js": "^7.0.0",
"heic2any": "^0.0.4",
"html-to-image": "^1.11.11",
"idb-keyval": "^6.2.1",
"lodash-es": "^4.17.21",
"mermaid": "^10.6.1",
"nanoid": "^5.0.3",
@@ -50,14 +51,15 @@
"zustand": "^4.3.8"
},
"devDependencies": {
"@tauri-apps/api": "^1.6.0",
"@tauri-apps/cli": "1.5.11",
"@types/js-yaml": "4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.30",
"@types/react": "^18.2.70",
"@types/react-dom": "^18.2.7",
"@types/react-katex": "^3.0.0",
"@types/spark-md5": "^3.0.4",
"@types/js-yaml": "4.0.9",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^8.49.0",

View File

@@ -17,7 +17,7 @@ tauri-build = { version = "1.5.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.5.4", features = [
tauri = { version = "1.5.4", features = [ "http-all",
"notification-all",
"fs-all",
"clipboard-all",

View File

@@ -9,7 +9,7 @@
},
"package": {
"productName": "NextChat",
"version": "2.14.2"
"version": "2.15.1"
},
"tauri": {
"allowlist": {
@@ -50,6 +50,11 @@
},
"notification": {
"all": true
},
"http": {
"all": true,
"request": true,
"scope": ["https://*", "http://*"]
}
},
"bundle": {

View File

@@ -1553,6 +1553,11 @@
dependencies:
tslib "^2.4.0"
"@tauri-apps/api@^1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186"
integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==
"@tauri-apps/cli-darwin-arm64@1.5.11":
version "1.5.11"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.11.tgz#a831f98f685148e46e8050dbdddbf4bcdda9ddc6"
@@ -3981,6 +3986,11 @@ iconv-lite@0.6:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
idb-keyval@^6.2.1:
version "6.2.1"
resolved "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
ignore@^5.2.0:
version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"