mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-02-09 07:54:27 +08:00
Compare commits
125 Commits
v2.15.0
...
aef119bb2b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef119bb2b | ||
|
|
b2de4c0461 | ||
|
|
540f93ad25 | ||
|
|
cc33b7b6bf | ||
|
|
7f857284bb | ||
|
|
9bd1635064 | ||
|
|
4c9fe56f33 | ||
|
|
29296d42d7 | ||
|
|
d306491e88 | ||
|
|
a7d8abbc4d | ||
|
|
4c6a593ae3 | ||
|
|
9ba1b39cf5 | ||
|
|
454c247de6 | ||
|
|
cb6576ec04 | ||
|
|
2758ea5ddd | ||
|
|
a2e9e78457 | ||
|
|
b720ba4c30 | ||
|
|
7151c0134a | ||
|
|
97099849b7 | ||
|
|
1fa58c60d9 | ||
|
|
5aaa190a7d | ||
|
|
b9e6dce193 | ||
|
|
7b1667f204 | ||
|
|
87c5b72ea2 | ||
|
|
649c5be64e | ||
|
|
fc0042a799 | ||
|
|
269d064e0a | ||
|
|
6c8143b7de | ||
|
|
f9f99639db | ||
|
|
46fc2a5012 | ||
|
|
90e7b5aecf | ||
|
|
ed20fd2962 | ||
|
|
4c3fd55a75 | ||
|
|
d95d509046 | ||
|
|
4a60512ae7 | ||
|
|
0e210cf8de | ||
|
|
35aa2c7270 | ||
|
|
23f2b6213c | ||
|
|
3a969054e3 | ||
|
|
4d1f9e49d4 | ||
|
|
2474d5b6d2 | ||
|
|
c75d9e3de4 | ||
|
|
df222ded12 | ||
|
|
212d15fdd0 | ||
|
|
b5ba05dd83 | ||
|
|
accb526cd6 | ||
|
|
a8c70d84a9 | ||
|
|
10d7a64f88 | ||
|
|
d51bbb4a81 | ||
|
|
848f794149 | ||
|
|
7f1b44befe | ||
|
|
9ddd5a0566 | ||
|
|
a3b664763e | ||
|
|
fd47bc1dc3 | ||
|
|
dfaafe3adb | ||
|
|
b4dc4d34eb | ||
|
|
3ae8ec1af6 | ||
|
|
5c34666334 | ||
|
|
212605a7e3 | ||
|
|
4ddfa9af8d | ||
|
|
36a0c7b8a3 | ||
|
|
9e1e0a7252 | ||
|
|
027e5adf67 | ||
|
|
e986088bec | ||
|
|
63ffd473d5 | ||
|
|
9e5d92dc58 | ||
|
|
8ac9141a29 | ||
|
|
313c942350 | ||
|
|
26c3edd023 | ||
|
|
9a5a3d4ce4 | ||
|
|
6e79b9a7a2 | ||
|
|
b32d82e6c1 | ||
|
|
a3585685df | ||
|
|
f379865e2c | ||
|
|
4eb4c31438 | ||
|
|
84a7afcd94 | ||
|
|
fa48ace39b | ||
|
|
1b869d9305 | ||
|
|
93bc2f5870 | ||
|
|
37c0cfe1e9 | ||
|
|
fc27441561 | ||
|
|
79cfbac11f | ||
|
|
df62736ff6 | ||
|
|
6a464b3e5f | ||
|
|
57fcda80df | ||
|
|
db39fbc419 | ||
|
|
3dabe47c78 | ||
|
|
affc194cde | ||
|
|
03fa580a55 | ||
|
|
d0dce654bf | ||
|
|
169323e238 | ||
|
|
71df415b14 | ||
|
|
6bb01bc564 | ||
|
|
07c6fe5975 | ||
|
|
5964181cb2 | ||
|
|
4b8288a2b2 | ||
|
|
88b1c1c6a5 | ||
|
|
1234deabfa | ||
|
|
ebaeb5a0d5 | ||
|
|
45306bbb6c | ||
|
|
18e2403b01 | ||
|
|
e578c5f3ad | ||
|
|
61245e3d7e | ||
|
|
7804182d0d | ||
|
|
f2195154f6 | ||
|
|
35f77f45a2 | ||
|
|
992c3a5d3a | ||
|
|
d51d7b6797 | ||
|
|
23ac2efd89 | ||
|
|
daeffb2dc6 | ||
|
|
db58ca6c1d | ||
|
|
2ff292cbfa | ||
|
|
5a81393863 | ||
|
|
116a73d398 | ||
|
|
cf0c057164 | ||
|
|
c1b74201e4 | ||
|
|
c5168c2132 | ||
|
|
318e0989a2 | ||
|
|
d8b1781d7b | ||
|
|
ed5aea0521 | ||
|
|
e9f90a4d82 | ||
|
|
f86b220c92 | ||
|
|
93f1762e6c | ||
|
|
2f410fc09f | ||
|
|
56eb9d1430 |
@@ -66,4 +66,4 @@ ANTHROPIC_API_VERSION=
|
||||
ANTHROPIC_URL=
|
||||
|
||||
### (optional)
|
||||
WHITE_WEBDEV_ENDPOINTS=
|
||||
WHITE_WEBDAV_ENDPOINTS=
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals",
|
||||
"plugins": ["prettier"]
|
||||
"plugins": ["prettier", "unused-imports"],
|
||||
"rules": {
|
||||
"unused-imports/no-unused-imports": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
4
.github/workflows/app.yml
vendored
4
.github/workflows/app.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
node-version: 18
|
||||
- name: get version
|
||||
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').package.version")" >> $GITHUB_ENV
|
||||
run: echo "PACKAGE_VERSION=$(node -p "require('./src-tauri/tauri.conf.json').version")" >> $GITHUB_ENV
|
||||
- name: create release
|
||||
id: create-release
|
||||
uses: actions/github-script@v6
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
if: matrix.config.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
sudo apt-get install -y libgtk-3-dev libsoup-3.0-dev libjavascriptcoregtk-4.1 libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: install frontend dependencies
|
||||
run: yarn install # change this to npm or pnpm depending on which one you use
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
|
||||
2
.github/workflows/deploy_preview.yml
vendored
2
.github/workflows/deploy_preview.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
run: npm install --global vercel@latest
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
id: cache-npm
|
||||
with:
|
||||
path: ~/.npm
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -46,3 +46,4 @@ dev
|
||||
*.key.pub
|
||||
|
||||
masks.json
|
||||
package-lock.json
|
||||
@@ -340,7 +340,7 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
|
||||
|
||||
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:
|
||||
- Each address must be a complete endpoint
|
||||
|
||||
@@ -202,7 +202,7 @@ ByteDance Api Url.
|
||||
|
||||
如果你想禁用从链接解析预制设置,将此环境变量设置为 1 即可。
|
||||
|
||||
### `WHITE_WEBDEV_ENDPOINTS` (可选)
|
||||
### `WHITE_WEBDAV_ENDPOINTS` (可选)
|
||||
|
||||
如果你想增加允许访问的webdav服务地址,可以使用该选项,格式要求:
|
||||
- 每一个地址必须是一个完整的 endpoint
|
||||
|
||||
@@ -193,7 +193,7 @@ ByteDance API の URL。
|
||||
|
||||
リンクからのプリセット設定解析を無効にしたい場合は、この環境変数を 1 に設定します。
|
||||
|
||||
### `WHITE_WEBDEV_ENDPOINTS` (オプション)
|
||||
### `WHITE_WEBDAV_ENDPOINTS` (オプション)
|
||||
|
||||
アクセス許可を与える WebDAV サービスのアドレスを追加したい場合、このオプションを使用します。フォーマット要件:
|
||||
- 各アドレスは完全なエンドポイントでなければなりません。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiPath } from "@/app/constant";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { NextRequest } from "next/server";
|
||||
import { handle as openaiHandler } from "../../openai";
|
||||
import { handle as azureHandler } from "../../azure";
|
||||
import { handle as googleHandler } from "../../google";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
Alibaba,
|
||||
ALIBABA_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
@@ -10,7 +9,6 @@ import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
ANTHROPIC_BASE_URL,
|
||||
Anthropic,
|
||||
ApiPath,
|
||||
DEFAULT_MODELS,
|
||||
ServiceProvider,
|
||||
ModelProvider,
|
||||
} from "@/app/constant";
|
||||
@@ -98,6 +97,7 @@ async function request(req: NextRequest) {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
[authHeaderName]: authValue,
|
||||
"anthropic-version":
|
||||
req.headers.get("anthropic-version") ||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import { ModelProvider } from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
BAIDU_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
BAIDU_OATUH_URL,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import {
|
||||
DEFAULT_MODELS,
|
||||
OPENAI_BASE_URL,
|
||||
GEMINI_BASE_URL,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
|
||||
import { isModelAvailableInServer } from "../utils/model";
|
||||
import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
|
||||
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "./auth";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
ApiPath,
|
||||
GEMINI_BASE_URL,
|
||||
Google,
|
||||
ModelProvider,
|
||||
} from "@/app/constant";
|
||||
import { ApiPath, GEMINI_BASE_URL, ModelProvider } from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
Iflytek,
|
||||
IFLYTEK_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
@@ -10,7 +9,6 @@ import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||
// iflytek
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
Moonshot,
|
||||
MOONSHOT_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
@@ -10,7 +9,6 @@ import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import type { RequestPayload } from "@/app/client/platforms/openai";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||
|
||||
if (config.disableGPT4) {
|
||||
remoteModelRes.data = remoteModelRes.data.filter(
|
||||
(m) => !m.id.startsWith("gpt-4") || m.id.startsWith("gpt-4o-mini"),
|
||||
(m) =>
|
||||
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o")) ||
|
||||
m.id.startsWith("gpt-4o-mini"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
TENCENT_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
Tencent,
|
||||
} from "@/app/constant";
|
||||
import { TENCENT_BASE_URL, ModelProvider } from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { getHeader } from "@/app/utils/tencent";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
@@ -6,7 +6,7 @@ const config = getServerSideConfig();
|
||||
|
||||
const mergedAllowedWebDavEndpoints = [
|
||||
...internalAllowedWebDavEndpoints,
|
||||
...config.allowedWebDevEndpoints,
|
||||
...config.allowedWebDavEndpoints,
|
||||
].filter((domain) => Boolean(domain.trim()));
|
||||
|
||||
const normalizeUrl = (url: string) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import {
|
||||
ACCESS_CODE_PREFIX,
|
||||
Azure,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
@@ -26,6 +25,7 @@ export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
|
||||
export const Models = ["gpt-3.5-turbo", "gpt-4"] as const;
|
||||
export const TTSModels = ["tts-1", "tts-1-hd"] as const;
|
||||
export type ChatModel = ModelType;
|
||||
|
||||
export interface MultimodalContent {
|
||||
@@ -54,6 +54,15 @@ export interface LLMConfig {
|
||||
style?: DalleRequestPayload["style"];
|
||||
}
|
||||
|
||||
export interface SpeechOptions {
|
||||
model: string;
|
||||
input: string;
|
||||
voice: string;
|
||||
response_format?: string;
|
||||
speed?: number;
|
||||
onController?: (controller: AbortController) => void;
|
||||
}
|
||||
|
||||
export interface ChatOptions {
|
||||
messages: RequestMessage[];
|
||||
config: LLMConfig;
|
||||
@@ -88,6 +97,7 @@ export interface LLMModelProvider {
|
||||
|
||||
export abstract class LLMApi {
|
||||
abstract chat(options: ChatOptions): Promise<void>;
|
||||
abstract speech(options: SpeechOptions): Promise<ArrayBuffer>;
|
||||
abstract usage(): Promise<LLMUsage>;
|
||||
abstract models(): Promise<LLMModel[]>;
|
||||
}
|
||||
@@ -206,13 +216,16 @@ export function validString(x: string): boolean {
|
||||
return x?.length > 0;
|
||||
}
|
||||
|
||||
export function getHeaders() {
|
||||
export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
const accessStore = useAccessStore.getState();
|
||||
const chatStore = useChatStore.getState();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
let headers: Record<string, string> = {};
|
||||
if (!ignoreHeaders) {
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
}
|
||||
|
||||
const clientConfig = getClientConfig();
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
SpeechOptions,
|
||||
MultimodalContent,
|
||||
} from "../api";
|
||||
import Locale from "../../locales";
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { getMessageTextContent, fetch } from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -83,6 +84,10 @@ export class QwenApi implements LLMApi {
|
||||
return res?.output?.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
@@ -173,6 +178,7 @@ export class QwenApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
|
||||
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
|
||||
import { Anthropic, ApiPath } from "@/app/constant";
|
||||
import { ChatOptions, getHeaders, LLMApi, SpeechOptions } from "../api";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
@@ -7,15 +7,6 @@ import {
|
||||
usePluginStore,
|
||||
ChatMessageTool,
|
||||
} from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
|
||||
import Locale from "../../locales";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
@@ -80,6 +71,10 @@ const ClaudeMapper = {
|
||||
const keys = ["claude-2, claude-instant-1"];
|
||||
|
||||
export class ClaudeApi implements LLMApi {
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] claude response: ", res);
|
||||
|
||||
@@ -203,7 +198,7 @@ export class ClaudeApi implements LLMApi {
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin as string[],
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
path,
|
||||
@@ -389,11 +384,7 @@ export class ClaudeApi implements LLMApi {
|
||||
|
||||
// if endpoint is empty, use default endpoint
|
||||
if (baseUrl.trim().length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
baseUrl = ApiPath.Anthropic;
|
||||
}
|
||||
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
MultimodalContent,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { getMessageTextContent, fetch } from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -75,6 +76,10 @@ export class ErnieApi implements LLMApi {
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
|
||||
@@ -192,6 +197,7 @@ export class ErnieApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
MultimodalContent,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { getMessageTextContent, fetch } from "@/app/utils";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -77,6 +78,10 @@ export class DoubaoApi implements LLMApi {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages = options.messages.map((v) => ({
|
||||
role: v.role,
|
||||
@@ -160,6 +165,7 @@ export class DoubaoApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { ApiPath, Google, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
LLMUsage,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
@@ -13,6 +18,7 @@ import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
fetch,
|
||||
} from "@/app/utils";
|
||||
import { preProcessImageContent } from "@/app/utils/chat";
|
||||
|
||||
@@ -25,9 +31,8 @@ export class GeminiProApi implements LLMApi {
|
||||
baseUrl = accessStore.googleUrl;
|
||||
}
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
if (baseUrl.length === 0) {
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
|
||||
baseUrl = ApiPath.Google;
|
||||
}
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
@@ -56,6 +61,10 @@ export class GeminiProApi implements LLMApi {
|
||||
""
|
||||
);
|
||||
}
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions): Promise<void> {
|
||||
const apiClient = this;
|
||||
let multimodal = false;
|
||||
@@ -206,6 +215,7 @@ export class GeminiProApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
Iflytek,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
import { ApiPath, Iflytek, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import { ChatOptions, getHeaders, LLMApi, LLMModel } from "../api";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { getMessageTextContent, fetch } from "@/app/utils";
|
||||
|
||||
import { OpenAIListModelResponse, RequestPayload } from "./openai";
|
||||
import { RequestPayload } from "./openai";
|
||||
|
||||
export class SparkApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
@@ -32,9 +32,7 @@ export class SparkApi implements LLMApi {
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Iflytek;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = ApiPath.Iflytek;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -53,6 +51,10 @@ export class SparkApi implements LLMApi {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
@@ -139,6 +141,7 @@ export class SparkApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
"use client";
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
DEFAULT_MODELS,
|
||||
Moonshot,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { ApiPath, Moonshot, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
@@ -15,28 +8,16 @@ import {
|
||||
ChatMessageTool,
|
||||
usePluginStore,
|
||||
} from "@/app/store";
|
||||
import { collectModelsWithDefaultModel } from "@/app/utils/model";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
|
||||
import { stream } from "@/app/utils/chat";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
LLMUsage,
|
||||
MultimodalContent,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
|
||||
import { OpenAIListModelResponse, RequestPayload } from "./openai";
|
||||
import { RequestPayload } from "./openai";
|
||||
|
||||
export class MoonshotApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
@@ -51,9 +32,8 @@ export class MoonshotApi implements LLMApi {
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Moonshot;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -72,6 +52,10 @@ export class MoonshotApi implements LLMApi {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
@@ -125,7 +109,7 @@ export class MoonshotApi implements LLMApi {
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin as string[],
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
chatPath,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
DEFAULT_MODELS,
|
||||
OpenaiPath,
|
||||
Azure,
|
||||
@@ -33,17 +32,11 @@ import {
|
||||
LLMModel,
|
||||
LLMUsage,
|
||||
MultimodalContent,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
isDalle3 as _isDalle3,
|
||||
} from "@/app/utils";
|
||||
@@ -101,9 +94,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -147,6 +138,44 @@ export class ChatGPTApi implements LLMApi {
|
||||
return res.choices?.at(0)?.message?.content ?? res;
|
||||
}
|
||||
|
||||
async speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
const requestPayload = {
|
||||
model: options.model,
|
||||
input: options.input,
|
||||
voice: options.voice,
|
||||
response_format: options.response_format,
|
||||
speed: options.speed,
|
||||
};
|
||||
|
||||
console.log("[Request] openai speech payload: ", requestPayload);
|
||||
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const speechPath = this.path(OpenaiPath.SpeechPath);
|
||||
const speechPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
const res = await fetch(speechPath, speechPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
return await res.arrayBuffer();
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a speech request", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
@@ -160,6 +189,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
let requestPayload: RequestPayload | DalleRequestPayload;
|
||||
|
||||
const isDalle3 = _isDalle3(options.config.model);
|
||||
const isO1 = options.config.model.startsWith("o1");
|
||||
if (isDalle3) {
|
||||
const prompt = getMessageTextContent(
|
||||
options.messages.slice(-1)?.pop() as any,
|
||||
@@ -181,30 +211,32 @@ export class ChatGPTApi implements LLMApi {
|
||||
const content = visionModel
|
||||
? await preProcessImageContent(v.content)
|
||||
: getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
if (!(isO1 && v.role === "system"))
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
|
||||
requestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
stream: !isO1 ? options.config.stream : false,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
temperature: !isO1 ? modelConfig.temperature : 1,
|
||||
presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
|
||||
frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
|
||||
top_p: !isO1 ? modelConfig.top_p : 1,
|
||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
// add max_tokens to vision model
|
||||
if (visionModel && modelConfig.model.includes("preview")) {
|
||||
if (visionModel) {
|
||||
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !isDalle3 && !!options.config.stream;
|
||||
const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
@@ -241,10 +273,11 @@ export class ChatGPTApi implements LLMApi {
|
||||
);
|
||||
}
|
||||
if (shouldStream) {
|
||||
let index = -1;
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin as string[],
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
// console.log("getAsTools", tools, funcs);
|
||||
stream(
|
||||
@@ -266,10 +299,10 @@ export class ChatGPTApi implements LLMApi {
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
index += 1;
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
@@ -291,6 +324,8 @@ export class ChatGPTApi implements LLMApi {
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// reset index value
|
||||
index = -1;
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
@@ -313,7 +348,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
);
|
||||
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
@@ -407,7 +442,9 @@ export class ChatGPTApi implements LLMApi {
|
||||
});
|
||||
|
||||
const resJson = (await res.json()) as OpenAIListModelResponse;
|
||||
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-"));
|
||||
const chatModels = resJson.data?.filter(
|
||||
(m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
|
||||
);
|
||||
console.log("[Models]", chatModels);
|
||||
|
||||
if (!chatModels) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { ApiPath, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import {
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
MultimodalContent,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
@@ -15,8 +16,7 @@ import {
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { getMessageTextContent, isVisionModel, fetch } from "@/app/utils";
|
||||
import mapKeys from "lodash-es/mapKeys";
|
||||
import mapValues from "lodash-es/mapValues";
|
||||
import isArray from "lodash-es/isArray";
|
||||
@@ -68,10 +68,7 @@ export class HunyuanApi implements LLMApi {
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
baseUrl = ApiPath.Tencent;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -89,6 +86,10 @@ export class HunyuanApi implements LLMApi {
|
||||
return res.Choices?.at(0)?.Message?.Content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
const messages = options.messages.map((v, index) => ({
|
||||
@@ -174,6 +175,7 @@ export class HunyuanApi implements LLMApi {
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
@@ -38,6 +38,7 @@ interface ChatCommands {
|
||||
next?: Command;
|
||||
prev?: Command;
|
||||
clear?: Command;
|
||||
fork?: Command;
|
||||
del?: Command;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useWindowSize } from "@/app/utils";
|
||||
import { IconButton } from "./button";
|
||||
import { nanoid } from "nanoid";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
@@ -80,7 +79,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
|
||||
}, [props.autoHeight, props.height, iframeHeight]);
|
||||
|
||||
const srcDoc = useMemo(() => {
|
||||
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
|
||||
const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
|
||||
if (props.code.includes("<!DOCTYPE html>")) {
|
||||
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
import {
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
import { useChatStore } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import { Mask } from "../store/mask";
|
||||
|
||||
@@ -646,3 +646,51 @@
|
||||
bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-key-container {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.shortcut-key-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.shortcut-key-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
padding: 10px;
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.shortcut-key-title {
|
||||
font-size: 14px;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.shortcut-key-keys {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--border-in-light);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
background-color: var(--gray);
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.shortcut-key span {
|
||||
font-size: 12px;
|
||||
color: var(--black);
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import RenameIcon from "../icons/rename.svg";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
import ReturnIcon from "../icons/return.svg";
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
import SpeakIcon from "../icons/speak.svg";
|
||||
import SpeakStopIcon from "../icons/speak-stop.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import LoadingButtonIcon from "../icons/loading.svg";
|
||||
import PromptIcon from "../icons/prompt.svg";
|
||||
@@ -42,6 +44,8 @@ import SizeIcon from "../icons/size.svg";
|
||||
import QualityIcon from "../icons/hd.svg";
|
||||
import StyleIcon from "../icons/palette.svg";
|
||||
import PluginIcon from "../icons/plugin.svg";
|
||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||
import ReloadIcon from "../icons/reload.svg";
|
||||
|
||||
import {
|
||||
ChatMessage,
|
||||
@@ -67,6 +71,7 @@ import {
|
||||
isVisionModel,
|
||||
isDalle3,
|
||||
showPlugins,
|
||||
safeLocalStorage,
|
||||
} from "../utils";
|
||||
|
||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||
@@ -93,7 +98,8 @@ import {
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CHAT_PAGE_SIZE,
|
||||
LAST_INPUT_KEY,
|
||||
DEFAULT_TTS_ENGINE,
|
||||
ModelProvider,
|
||||
Path,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
UNFINISHED_INPUT,
|
||||
@@ -109,6 +115,13 @@ import { getClientConfig } from "../config/client";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { MultimodalContent } from "../client/api";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
import { ClientApi } from "../client/api";
|
||||
import { createTTSPlayer } from "../utils/audio";
|
||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||
|
||||
const ttsPlayer = createTTSPlayer();
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
@@ -188,7 +201,7 @@ function PromptToast(props: {
|
||||
|
||||
return (
|
||||
<div className={styles["prompt-toast"]} key="prompt-toast">
|
||||
{props.showToast && (
|
||||
{props.showToast && context.length > 0 && (
|
||||
<div
|
||||
className={styles["prompt-toast-inner"] + " clickable"}
|
||||
role="button"
|
||||
@@ -437,6 +450,8 @@ export function ChatActions(props: {
|
||||
showPromptHints: () => void;
|
||||
hitBottom: boolean;
|
||||
uploading: boolean;
|
||||
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUserInput: (input: string) => void;
|
||||
}) {
|
||||
const config = useAppConfig();
|
||||
const navigate = useNavigate();
|
||||
@@ -502,6 +517,8 @@ export function ChatActions(props: {
|
||||
const currentStyle =
|
||||
chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
useEffect(() => {
|
||||
const show = isVisionModel(currentModel);
|
||||
setShowUploadImage(show);
|
||||
@@ -512,8 +529,8 @@ export function ChatActions(props: {
|
||||
|
||||
// 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 isUnavailableModel = !models.some((m) => m.name === currentModel);
|
||||
if (isUnavailableModel && models.length > 0) {
|
||||
// show next model to default model if exist
|
||||
let nextModel = models.find((model) => model.isDefault) || models[0];
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
@@ -617,7 +634,7 @@ export function ChatActions(props: {
|
||||
items={models.map((m) => ({
|
||||
title: `${m.displayName}${
|
||||
m?.provider?.providerName
|
||||
? "(" + m?.provider?.providerName + ")"
|
||||
? " (" + m?.provider?.providerName + ")"
|
||||
: ""
|
||||
}`,
|
||||
value: `${m.name}@${m?.provider?.providerName}`,
|
||||
@@ -755,6 +772,14 @@ export function ChatActions(props: {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isMobileScreen && (
|
||||
<ChatAction
|
||||
onClick={() => props.setShowShortcutKeyModal(true)}
|
||||
text={Locale.Chat.ShortcutKey.Title}
|
||||
icon={<ShortcutkeyIcon />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -829,6 +854,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ShortcutKeyModal(props: { onClose: () => void }) {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
const shortcuts = [
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.newChat,
|
||||
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
|
||||
},
|
||||
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastCode,
|
||||
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.copyLastMessage,
|
||||
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
|
||||
},
|
||||
{
|
||||
title: Locale.Chat.ShortcutKey.showShortcutKey,
|
||||
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Chat.ShortcutKey.Title}
|
||||
onClose={props.onClose}
|
||||
actions={[
|
||||
<IconButton
|
||||
type="primary"
|
||||
text={Locale.UI.Confirm}
|
||||
icon={<ConfirmIcon />}
|
||||
key="ok"
|
||||
onClick={() => {
|
||||
props.onClose();
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div className={styles["shortcut-key-container"]}>
|
||||
<div className={styles["shortcut-key-grid"]}>
|
||||
{shortcuts.map((shortcut, index) => (
|
||||
<div key={index} className={styles["shortcut-key-item"]}>
|
||||
<div className={styles["shortcut-key-title"]}>
|
||||
{shortcut.title}
|
||||
</div>
|
||||
<div className={styles["shortcut-key-keys"]}>
|
||||
{shortcut.keys.map((key, i) => (
|
||||
<div key={i} className={styles["shortcut-key"]}>
|
||||
<span>{key}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function _Chat() {
|
||||
type RenderMessage = ChatMessage & { preview?: boolean };
|
||||
|
||||
@@ -904,6 +990,7 @@ function _Chat() {
|
||||
chatStore.updateCurrentSession(
|
||||
(session) => (session.clearContextIndex = session.messages.length),
|
||||
),
|
||||
fork: () => chatStore.forkSession(),
|
||||
del: () => chatStore.deleteSession(chatStore.currentSessionIndex),
|
||||
});
|
||||
|
||||
@@ -941,7 +1028,7 @@ function _Chat() {
|
||||
.onUserInput(userInput, attachImages)
|
||||
.then(() => setIsLoading(false));
|
||||
setAttachImages([]);
|
||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
||||
chatStore.setLastInput(userInput);
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
if (!isMobileScreen) inputRef.current?.focus();
|
||||
@@ -1007,7 +1094,7 @@ function _Chat() {
|
||||
userInput.length <= 0 &&
|
||||
!(e.metaKey || e.altKey || e.ctrlKey)
|
||||
) {
|
||||
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
|
||||
setUserInput(chatStore.lastInput ?? "");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
@@ -1107,10 +1194,55 @@ function _Chat() {
|
||||
});
|
||||
};
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const [speechStatus, setSpeechStatus] = useState(false);
|
||||
const [speechLoading, setSpeechLoading] = useState(false);
|
||||
async function openaiSpeech(text: string) {
|
||||
if (speechStatus) {
|
||||
ttsPlayer.stop();
|
||||
setSpeechStatus(false);
|
||||
} else {
|
||||
var api: ClientApi;
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
const config = useAppConfig.getState();
|
||||
setSpeechLoading(true);
|
||||
ttsPlayer.init();
|
||||
let audioBuffer: ArrayBuffer;
|
||||
const { markdownToTxt } = require("markdown-to-txt");
|
||||
const textContent = markdownToTxt(text);
|
||||
if (config.ttsConfig.engine !== DEFAULT_TTS_ENGINE) {
|
||||
const edgeVoiceName = accessStore.edgeVoiceName();
|
||||
const tts = new MsEdgeTTS();
|
||||
await tts.setMetadata(
|
||||
edgeVoiceName,
|
||||
OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3,
|
||||
);
|
||||
audioBuffer = await tts.toArrayBuffer(textContent);
|
||||
} else {
|
||||
audioBuffer = await api.llm.speech({
|
||||
model: config.ttsConfig.model,
|
||||
input: textContent,
|
||||
voice: config.ttsConfig.voice,
|
||||
speed: config.ttsConfig.speed,
|
||||
});
|
||||
}
|
||||
setSpeechStatus(true);
|
||||
ttsPlayer
|
||||
.play(audioBuffer, () => {
|
||||
setSpeechStatus(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("[OpenAI Speech]", e);
|
||||
showToast(prettyObject(e));
|
||||
setSpeechStatus(false);
|
||||
})
|
||||
.finally(() => setSpeechLoading(false));
|
||||
}
|
||||
}
|
||||
|
||||
const context: RenderMessage[] = useMemo(() => {
|
||||
return session.mask.hideContext ? [] : session.mask.context.slice();
|
||||
}, [session.mask.context, session.mask.hideContext]);
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
if (
|
||||
context.length === 0 &&
|
||||
@@ -1373,6 +1505,70 @@ function _Chat() {
|
||||
setAttachImages(images);
|
||||
}
|
||||
|
||||
// 快捷键 shortcut keys
|
||||
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: any) => {
|
||||
// 打开新聊天 command + shift + o
|
||||
if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "o"
|
||||
) {
|
||||
event.preventDefault();
|
||||
setTimeout(() => {
|
||||
chatStore.newSession();
|
||||
navigate(Path.Chat);
|
||||
}, 10);
|
||||
}
|
||||
// 聚焦聊天输入 shift + esc
|
||||
else if (event.shiftKey && event.key.toLowerCase() === "escape") {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
// 复制最后一个代码块 command + shift + ;
|
||||
else if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.code === "Semicolon"
|
||||
) {
|
||||
event.preventDefault();
|
||||
const copyCodeButton =
|
||||
document.querySelectorAll<HTMLElement>(".copy-code-button");
|
||||
if (copyCodeButton.length > 0) {
|
||||
copyCodeButton[copyCodeButton.length - 1].click();
|
||||
}
|
||||
}
|
||||
// 复制最后一个回复 command + shift + c
|
||||
else if (
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey &&
|
||||
event.key.toLowerCase() === "c"
|
||||
) {
|
||||
event.preventDefault();
|
||||
const lastNonUserMessage = messages
|
||||
.filter((message) => message.role !== "user")
|
||||
.pop();
|
||||
if (lastNonUserMessage) {
|
||||
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
|
||||
copyToClipboard(lastMessageContent);
|
||||
}
|
||||
}
|
||||
// 展示快捷键 command + /
|
||||
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
|
||||
event.preventDefault();
|
||||
setShowShortcutKeyModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [messages, chatStore, navigate]);
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
@@ -1401,6 +1597,17 @@ function _Chat() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<ReloadIcon />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.RefreshTitle}
|
||||
onClick={() => {
|
||||
showToast(Locale.Chat.Actions.RefreshToast);
|
||||
chatStore.summarizeSession(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!isMobileScreen && (
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
@@ -1572,6 +1779,25 @@ function _Chat() {
|
||||
)
|
||||
}
|
||||
/>
|
||||
{config.ttsConfig.enable && (
|
||||
<ChatAction
|
||||
text={
|
||||
speechStatus
|
||||
? Locale.Chat.Actions.StopSpeech
|
||||
: Locale.Chat.Actions.Speech
|
||||
}
|
||||
icon={
|
||||
speechStatus ? (
|
||||
<SpeakStopIcon />
|
||||
) : (
|
||||
<SpeakIcon />
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
openaiSpeech(getMessageTextContent(message))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1689,6 +1915,8 @@ function _Chat() {
|
||||
setUserInput("/");
|
||||
onSearch("");
|
||||
}}
|
||||
setShowShortcutKeyModal={setShowShortcutKeyModal}
|
||||
setUserInput={setUserInput}
|
||||
/>
|
||||
<label
|
||||
className={`${styles["chat-input-panel-inner"]} ${
|
||||
@@ -1760,6 +1988,10 @@ function _Chat() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showShortcutKeyModal && (
|
||||
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
|
||||
if (props.model) {
|
||||
return (
|
||||
<div className="no-dark">
|
||||
{props.model?.startsWith("gpt-4") ? (
|
||||
{props.model?.startsWith("gpt-4") ||
|
||||
props.model?.startsWith("chatgpt-4o") ? (
|
||||
<BlackBotIcon className="user-avatar" />
|
||||
) : (
|
||||
<BotIcon className="user-avatar" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ISSUE_URL } from "../constant";
|
||||
import Locale from "../locales";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useSyncStore } from "../store/sync";
|
||||
import { useChatStore } from "../store/chat";
|
||||
|
||||
interface IErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
@@ -30,8 +31,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||
try {
|
||||
useSyncStore.getState().export();
|
||||
} finally {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
useChatStore.getState().clearAllData();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ChatMessage, ModelType, useAppConfig, useChatStore } from "../store";
|
||||
import { ChatMessage, useAppConfig, useChatStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
import styles from "./exporter.module.scss";
|
||||
import {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from "./artifacts";
|
||||
import { useChatStore } from "../store";
|
||||
import { IconButton } from "./button";
|
||||
import { useAppConfig } from "../store/config";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -92,7 +93,9 @@ export function PreCode(props: { children: any }) {
|
||||
}
|
||||
}, 600);
|
||||
|
||||
const enableArtifacts = session.mask?.enableArtifacts !== false;
|
||||
const config = useAppConfig();
|
||||
const enableArtifacts =
|
||||
session.mask?.enableArtifacts !== false && config.enableArtifacts;
|
||||
|
||||
//Wrap the paragraph for plain-text
|
||||
useEffect(() => {
|
||||
@@ -128,8 +131,9 @@ export function PreCode(props: { children: any }) {
|
||||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const code = ref.current.innerText;
|
||||
copyToClipboard(code);
|
||||
copyToClipboard(
|
||||
ref.current.querySelector("code")?.innerText ?? "",
|
||||
);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
@@ -163,7 +167,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);
|
||||
@@ -182,6 +186,7 @@ function CustomCode(props: { children: any }) {
|
||||
return (
|
||||
<>
|
||||
<code
|
||||
className={props?.className}
|
||||
ref={ref}
|
||||
style={{
|
||||
maxHeight: collapsed ? "400px" : "none",
|
||||
@@ -236,9 +241,26 @@ function escapeBrackets(text: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function tryWrapHtmlCode(text: string) {
|
||||
// try add wrap html code (fixed: html codeblock include 2 newline)
|
||||
return text
|
||||
.replace(
|
||||
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
|
||||
(match, quoteStart, lang, newLine, doctype) => {
|
||||
return !quoteStart ? "\n```html\n" + doctype : match;
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
|
||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(() => {
|
||||
return escapeBrackets(escapeDollarNumber(props.content));
|
||||
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
|
||||
}, [props.content]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -37,7 +37,7 @@ import Locale, { AllLangs, ALL_LANG_OPTIONS, Lang } from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import chatStyle from "./chat.module.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
copyToClipboard,
|
||||
downloadAs,
|
||||
@@ -48,7 +48,6 @@ import { Updater } from "../typing";
|
||||
import { ModelConfigList } from "./model-config";
|
||||
import { FileName, Path } from "../constant";
|
||||
import { BUILTIN_MASK_STORE } from "../masks";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
@@ -167,21 +166,23 @@ 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>
|
||||
{globalConfig.enableArtifacts && (
|
||||
<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
|
||||
@@ -426,16 +427,7 @@ export function MaskPage() {
|
||||
const maskStore = useMaskStore();
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const [filterLang, setFilterLang] = useState<Lang | undefined>(
|
||||
() => localStorage.getItem("Mask-language") as Lang | undefined,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (filterLang) {
|
||||
localStorage.setItem("Mask-language", filterLang);
|
||||
} else {
|
||||
localStorage.removeItem("Mask-language");
|
||||
}
|
||||
}, [filterLang]);
|
||||
const filterLang = maskStore.language;
|
||||
|
||||
const allMasks = maskStore
|
||||
.getAll()
|
||||
@@ -542,9 +534,9 @@ export function MaskPage() {
|
||||
onChange={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
if (value === Locale.Settings.Lang.All) {
|
||||
setFilterLang(undefined);
|
||||
maskStore.setLanguage(undefined);
|
||||
} else {
|
||||
setFilterLang(value as Lang);
|
||||
maskStore.setLanguage(value as Lang);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,13 +5,19 @@ import Locale from "../locales";
|
||||
import { InputRange } from "./input-range";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { groupBy } from "lodash-es";
|
||||
|
||||
export function ModelConfigList(props: {
|
||||
modelConfig: ModelConfig;
|
||||
updateConfig: (updater: (config: ModelConfig) => void) => void;
|
||||
}) {
|
||||
const allModels = useAllModels();
|
||||
const groupModels = groupBy(
|
||||
allModels.filter((v) => v.available),
|
||||
"provider.providerName",
|
||||
);
|
||||
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
|
||||
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -19,6 +25,7 @@ export function ModelConfigList(props: {
|
||||
<Select
|
||||
aria-label={Locale.Settings.Model}
|
||||
value={value}
|
||||
align="left"
|
||||
onChange={(e) => {
|
||||
const [model, providerName] = e.currentTarget.value.split("@");
|
||||
props.updateConfig((config) => {
|
||||
@@ -27,13 +34,15 @@ export function ModelConfigList(props: {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{allModels
|
||||
.filter((v) => v.available)
|
||||
.map((v, i) => (
|
||||
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
|
||||
{v.displayName}({v.provider?.providerName})
|
||||
</option>
|
||||
))}
|
||||
{Object.keys(groupModels).map((providerName, index) => (
|
||||
<optgroup label={providerName} key={index}>
|
||||
{groupModels[providerName].map((v, i) => (
|
||||
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
|
||||
{v.displayName}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
@@ -228,6 +237,30 @@ export function ModelConfigList(props: {
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.CompressModel.Title}
|
||||
subTitle={Locale.Settings.CompressModel.SubTitle}
|
||||
>
|
||||
<Select
|
||||
aria-label={Locale.Settings.CompressModel.Title}
|
||||
value={compressModelValue}
|
||||
onChange={(e) => {
|
||||
const [model, providerName] = e.currentTarget.value.split("@");
|
||||
props.updateConfig((config) => {
|
||||
config.compressModel = ModalConfigValidator.model(model);
|
||||
config.compressProviderName = providerName as ServiceProvider;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{allModels
|
||||
.filter((v) => v.available)
|
||||
.map((v, i) => (
|
||||
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
|
||||
{v.displayName}({v.provider?.providerName})
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,29 @@
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ 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 ConfirmIcon from "../icons/confirm.svg";
|
||||
import ReloadIcon from "../icons/reload.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
@@ -28,8 +27,7 @@ import {
|
||||
} from "./ui-lib";
|
||||
import Locale from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { useState } from "react";
|
||||
|
||||
export function PluginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -209,19 +207,11 @@ export function PluginPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["mask-actions"]}>
|
||||
{m.builtin ? (
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
text={Locale.Plugin.Item.View}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Plugin.Item.Edit}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Plugin.Item.Edit}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
{!m.builtin && (
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
@@ -325,30 +315,13 @@ export function PluginPage() {
|
||||
></PasswordInput>
|
||||
</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" }}>
|
||||
<div className={pluginStyles["plugin-schema"]}>
|
||||
<input
|
||||
type="text"
|
||||
style={{ minWidth: 200, marginRight: 20 }}
|
||||
style={{ minWidth: 200 }}
|
||||
onInput={(e) => setLoadUrl(e.currentTarget.value)}
|
||||
></input>
|
||||
<IconButton
|
||||
|
||||
@@ -80,6 +80,7 @@ import { useSyncStore } from "../store/sync";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { ProviderType } from "../utils/cloud";
|
||||
import { TTSConfigList } from "./tts-config";
|
||||
|
||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
const promptStore = usePromptStore();
|
||||
@@ -1464,6 +1465,23 @@ export function Settings() {
|
||||
}
|
||||
></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={config.enableArtifacts}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.enableArtifacts = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
@@ -1646,6 +1664,17 @@ export function Settings() {
|
||||
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||
)}
|
||||
|
||||
<List>
|
||||
<TTSConfigList
|
||||
ttsConfig={config.ttsConfig}
|
||||
updateConfig={(updater) => {
|
||||
const ttsConfig = { ...config.ttsConfig };
|
||||
updater(ttsConfig);
|
||||
config.update((config) => (config.ttsConfig = ttsConfig));
|
||||
}}
|
||||
/>
|
||||
</List>
|
||||
|
||||
<DangerItems />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -7,7 +7,6 @@ import SettingsIcon from "../icons/settings.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import MaskIcon from "../icons/mask.svg";
|
||||
import DragIcon from "../icons/drag.svg";
|
||||
@@ -254,11 +253,6 @@ export function SideBar(props: { className?: string }) {
|
||||
{showPluginSelector && (
|
||||
<Selector
|
||||
items={[
|
||||
{
|
||||
title: "👇 Please select the plugin you need to use",
|
||||
value: "-",
|
||||
disable: true,
|
||||
},
|
||||
...PLUGINS.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
|
||||
133
app/components/tts-config.tsx
Normal file
133
app/components/tts-config.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { TTSConfig, TTSConfigValidator } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
import {
|
||||
DEFAULT_TTS_ENGINE,
|
||||
DEFAULT_TTS_ENGINES,
|
||||
DEFAULT_TTS_MODELS,
|
||||
DEFAULT_TTS_VOICES,
|
||||
} from "../constant";
|
||||
import { InputRange } from "./input-range";
|
||||
|
||||
export function TTSConfigList(props: {
|
||||
ttsConfig: TTSConfig;
|
||||
updateConfig: (updater: (config: TTSConfig) => void) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.TTS.Enable.Title}
|
||||
subTitle={Locale.Settings.TTS.Enable.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.ttsConfig.enable}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.enable = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
{/* <ListItem
|
||||
title={Locale.Settings.TTS.Autoplay.Title}
|
||||
subTitle={Locale.Settings.TTS.Autoplay.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.ttsConfig.autoplay}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.autoplay = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem> */}
|
||||
<ListItem title={Locale.Settings.TTS.Engine}>
|
||||
<Select
|
||||
value={props.ttsConfig.engine}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.engine = TTSConfigValidator.engine(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{DEFAULT_TTS_ENGINES.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
{props.ttsConfig.engine === DEFAULT_TTS_ENGINE && (
|
||||
<>
|
||||
<ListItem title={Locale.Settings.TTS.Model}>
|
||||
<Select
|
||||
value={props.ttsConfig.model}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.model = TTSConfigValidator.model(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{DEFAULT_TTS_MODELS.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.TTS.Voice.Title}
|
||||
subTitle={Locale.Settings.TTS.Voice.SubTitle}
|
||||
>
|
||||
<Select
|
||||
value={props.ttsConfig.voice}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.voice = TTSConfigValidator.voice(
|
||||
e.currentTarget.value,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{DEFAULT_TTS_VOICES.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.TTS.Speed.Title}
|
||||
subTitle={Locale.Settings.TTS.Speed.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.TTS.Speed.Title}
|
||||
value={props.ttsConfig.speed?.toFixed(1)}
|
||||
min="0.3"
|
||||
max="4.0"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.speed = TTSConfigValidator.speed(
|
||||
e.currentTarget.valueAsNumber,
|
||||
)),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
119
app/components/tts.module.scss
Normal file
119
app/components/tts.module.scss
Normal file
@@ -0,0 +1,119 @@
|
||||
@import "../styles/animation.scss";
|
||||
.plugin-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.plugin-page-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.plugin-filter {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 20px;
|
||||
animation: slide-in ease 0.3s;
|
||||
height: 40px;
|
||||
|
||||
display: flex;
|
||||
|
||||
.search-bar {
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-bar:focus {
|
||||
border: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.plugin-filter-lang {
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.plugin-create {
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
box-sizing: border-box;
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border: var(--border-in-light);
|
||||
animation: slide-in ease 0.3s;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
.plugin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.plugin-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.plugin-title {
|
||||
.plugin-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.plugin-info {
|
||||
font-size: 12px;
|
||||
}
|
||||
.plugin-runtime-warning {
|
||||
font-size: 12px;
|
||||
color: #f86c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-actions {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
transition: all ease 0.3s;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 10px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: var(--card-shadow);
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: var(--border-in-light);
|
||||
}
|
||||
|
||||
.plugin-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,6 +252,12 @@
|
||||
position: relative;
|
||||
max-width: fit-content;
|
||||
|
||||
&.left-align-option {
|
||||
option {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.select-with-icon-select {
|
||||
height: 100%;
|
||||
border: var(--border-in-light);
|
||||
|
||||
@@ -292,13 +292,19 @@ export function PasswordInput(
|
||||
|
||||
export function Select(
|
||||
props: React.DetailedHTMLProps<
|
||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||
React.SelectHTMLAttributes<HTMLSelectElement> & {
|
||||
align?: "left" | "center";
|
||||
},
|
||||
HTMLSelectElement
|
||||
>,
|
||||
) {
|
||||
const { className, children, ...otherProps } = props;
|
||||
const { className, children, align, ...otherProps } = props;
|
||||
return (
|
||||
<div className={`${styles["select-with-icon"]} ${className}`}>
|
||||
<div
|
||||
className={`${styles["select-with-icon"]} ${
|
||||
align === "left" ? styles["left-align-option"] : ""
|
||||
} ${className}`}
|
||||
>
|
||||
<select className={styles["select-with-icon-select"]} {...otherProps}>
|
||||
{children}
|
||||
</select>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const getBuildConfig = () => {
|
||||
|
||||
const buildMode = process.env.BUILD_MODE ?? "standalone";
|
||||
const isApp = !!process.env.BUILD_APP;
|
||||
const version = "v" + tauriConfig.package.version;
|
||||
const version = "v" + tauriConfig.version;
|
||||
|
||||
const commitInfo = (() => {
|
||||
try {
|
||||
|
||||
@@ -120,12 +120,15 @@ export const getServerSideConfig = () => {
|
||||
if (disableGPT4) {
|
||||
if (customModels) customModels += ",";
|
||||
customModels += DEFAULT_MODELS.filter(
|
||||
(m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"),
|
||||
(m) =>
|
||||
(m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o")) &&
|
||||
!m.name.startsWith("gpt-4o-mini"),
|
||||
)
|
||||
.map((m) => "-" + m.name)
|
||||
.join(",");
|
||||
if (
|
||||
defaultModel.startsWith("gpt-4") &&
|
||||
(defaultModel.startsWith("gpt-4") ||
|
||||
defaultModel.startsWith("chatgpt-4o")) &&
|
||||
!defaultModel.startsWith("gpt-4o-mini")
|
||||
)
|
||||
defaultModel = "";
|
||||
@@ -151,8 +154,8 @@ export const getServerSideConfig = () => {
|
||||
// `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
||||
// );
|
||||
|
||||
const allowedWebDevEndpoints = (
|
||||
process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
|
||||
const allowedWebDavEndpoints = (
|
||||
process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
|
||||
).split(",");
|
||||
|
||||
return {
|
||||
@@ -226,6 +229,6 @@ export const getServerSideConfig = () => {
|
||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||
customModels,
|
||||
defaultModel,
|
||||
allowedWebDevEndpoints,
|
||||
allowedWebDavEndpoints,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import path from "path";
|
||||
|
||||
export const OWNER = "ChatGPTNextWeb";
|
||||
export const REPO = "ChatGPT-Next-Web";
|
||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||
@@ -13,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||
|
||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||
|
||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
@@ -152,6 +149,7 @@ export const Anthropic = {
|
||||
|
||||
export const OpenaiPath = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
SpeechPath: "v1/audio/speech",
|
||||
ImagePath: "v1/images/generations",
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
SubsPath: "dashboard/billing/subscription",
|
||||
@@ -246,15 +244,32 @@ export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
"gpt-4o": "2023-10",
|
||||
"gpt-4o-2024-05-13": "2023-10",
|
||||
"gpt-4o-2024-08-06": "2023-10",
|
||||
"chatgpt-4o-latest": "2023-10",
|
||||
"gpt-4o-mini": "2023-10",
|
||||
"gpt-4o-mini-2024-07-18": "2023-10",
|
||||
"gpt-4-vision-preview": "2023-04",
|
||||
"o1-mini": "2023-10",
|
||||
"o1-preview": "2023-10",
|
||||
// After improvements,
|
||||
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
||||
"gemini-pro": "2023-12",
|
||||
"gemini-pro-vision": "2023-12",
|
||||
};
|
||||
|
||||
export const DEFAULT_TTS_ENGINE = "OpenAI-TTS";
|
||||
export const DEFAULT_TTS_ENGINES = ["OpenAI-TTS", "Edge-TTS"];
|
||||
export const DEFAULT_TTS_MODEL = "tts-1";
|
||||
export const DEFAULT_TTS_VOICE = "alloy";
|
||||
export const DEFAULT_TTS_MODELS = ["tts-1", "tts-1-hd"];
|
||||
export const DEFAULT_TTS_VOICES = [
|
||||
"alloy",
|
||||
"echo",
|
||||
"fable",
|
||||
"onyx",
|
||||
"nova",
|
||||
"shimmer",
|
||||
];
|
||||
|
||||
const openaiModels = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-1106",
|
||||
@@ -268,12 +283,15 @@ const openaiModels = [
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-2024-08-06",
|
||||
"chatgpt-4o-latest",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"gpt-4-vision-preview",
|
||||
"gpt-4-turbo-2024-04-09",
|
||||
"gpt-4-1106-preview",
|
||||
"dall-e-3",
|
||||
"o1-mini",
|
||||
"o1-preview",
|
||||
];
|
||||
|
||||
const googleModels = [
|
||||
|
||||
1
app/icons/shortcutkey.svg
Normal file
1
app/icons/shortcutkey.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M42 7H6C4.89543 7 4 7.89543 4 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 734 B |
1
app/icons/speak-stop.svg
Normal file
1
app/icons/speak-stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 495 B |
1
app/icons/speak.svg
Normal file
1
app/icons/speak.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="16" height="16" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"><path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"></path></svg>
|
||||
|
After Width: | Height: | Size: 485 B |
16
app/icons/voice-white.svg
Normal file
16
app/icons/voice-white.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 20 20">
|
||||
<defs>
|
||||
<rect id="path_0" width="20" height="20" x="0" y="0" />
|
||||
</defs>
|
||||
<g opacity="1" transform="translate(0 0) rotate(0 8 8)">
|
||||
<mask id="bg-mask-0" fill="#fff">
|
||||
<use xlink:href="#path_0" />
|
||||
</mask>
|
||||
<g mask="url(#bg-mask-0)">
|
||||
<path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" fill="#333333">
|
||||
</path>
|
||||
<path d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z" fill="#333333">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 708 B |
@@ -41,7 +41,11 @@ 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>
|
||||
|
||||
@@ -43,6 +43,8 @@ const ar: PartialLocaleType = {
|
||||
PinToastAction: "عرض",
|
||||
Delete: "حذف",
|
||||
Edit: "تحرير",
|
||||
RefreshTitle: "تحديث العنوان",
|
||||
RefreshToast: "تم إرسال طلب تحديث العنوان",
|
||||
},
|
||||
Commands: {
|
||||
new: "دردشة جديدة",
|
||||
@@ -404,6 +406,10 @@ const ar: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "النموذج",
|
||||
CompressModel: {
|
||||
Title: "نموذج الضغط",
|
||||
SubTitle: "النموذج المستخدم لضغط السجل التاريخي",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "العشوائية (temperature)",
|
||||
SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود",
|
||||
|
||||
@@ -43,6 +43,8 @@ const bn: PartialLocaleType = {
|
||||
PinToastAction: "দেখুন",
|
||||
Delete: "মুছে ফেলুন",
|
||||
Edit: "সম্পাদনা করুন",
|
||||
RefreshTitle: "শিরোনাম রিফ্রেশ করুন",
|
||||
RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে",
|
||||
},
|
||||
Commands: {
|
||||
new: "নতুন চ্যাট",
|
||||
@@ -411,6 +413,10 @@ const bn: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "মডেল (model)",
|
||||
CompressModel: {
|
||||
Title: "সংকোচন মডেল",
|
||||
SubTitle: "ইতিহাস সংকুচিত করার জন্য ব্যবহৃত মডেল",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "যাদুকরিতা (temperature)",
|
||||
SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",
|
||||
|
||||
@@ -43,6 +43,10 @@ const cn = {
|
||||
Delete: "删除",
|
||||
Edit: "编辑",
|
||||
FullScreen: "全屏",
|
||||
RefreshTitle: "刷新标题",
|
||||
RefreshToast: "已发送刷新标题请求",
|
||||
Speech: "朗读",
|
||||
StopSpeech: "停止",
|
||||
},
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
@@ -50,6 +54,7 @@ const cn = {
|
||||
next: "下一个聊天",
|
||||
prev: "上一个聊天",
|
||||
clear: "清除上下文",
|
||||
fork: "复制聊天",
|
||||
del: "删除聊天",
|
||||
},
|
||||
InputActions: {
|
||||
@@ -76,11 +81,21 @@ const cn = {
|
||||
return inputHints + ",/ 触发补全,: 触发命令";
|
||||
},
|
||||
Send: "发送",
|
||||
StartSpeak: "说话",
|
||||
StopSpeak: "停止",
|
||||
Config: {
|
||||
Reset: "清除记忆",
|
||||
SaveAs: "存为面具",
|
||||
},
|
||||
IsContext: "预设提示词",
|
||||
ShortcutKey: {
|
||||
Title: "键盘快捷方式",
|
||||
newChat: "打开新聊天",
|
||||
focusInput: "聚焦输入框",
|
||||
copyLastMessage: "复制最后一个回复",
|
||||
copyLastCode: "复制最后一个代码块",
|
||||
showShortcutKey: "显示快捷方式",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "分享聊天记录",
|
||||
@@ -461,6 +476,10 @@ const cn = {
|
||||
},
|
||||
|
||||
Model: "模型 (model)",
|
||||
CompressModel: {
|
||||
Title: "压缩模型",
|
||||
SubTitle: "用于压缩历史记录的模型",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
SubTitle: "值越大,回复越随机",
|
||||
@@ -481,6 +500,26 @@ const cn = {
|
||||
Title: "频率惩罚度 (frequency_penalty)",
|
||||
SubTitle: "值越大,越有可能降低重复字词",
|
||||
},
|
||||
TTS: {
|
||||
Enable: {
|
||||
Title: "启用文本转语音",
|
||||
SubTitle: "启用文本生成语音服务",
|
||||
},
|
||||
Autoplay: {
|
||||
Title: "启用自动朗读",
|
||||
SubTitle: "自动生成语音并播放,需先开启文本转语音开关",
|
||||
},
|
||||
Model: "模型",
|
||||
Engine: "转换引擎",
|
||||
Voice: {
|
||||
Title: "声音",
|
||||
SubTitle: "生成语音时使用的声音",
|
||||
},
|
||||
Speed: {
|
||||
Title: "速度",
|
||||
SubTitle: "生成语音的速度",
|
||||
},
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "新的聊天",
|
||||
@@ -495,8 +534,8 @@ const cn = {
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "已写入剪切板",
|
||||
Failed: "复制失败,请赋予剪切板权限",
|
||||
Success: "已写入剪贴板",
|
||||
Failed: "复制失败,请赋予剪贴板权限",
|
||||
},
|
||||
Download: {
|
||||
Success: "内容已下载到您的目录。",
|
||||
|
||||
@@ -43,6 +43,8 @@ const cs: PartialLocaleType = {
|
||||
PinToastAction: "Zobrazit",
|
||||
Delete: "Smazat",
|
||||
Edit: "Upravit",
|
||||
RefreshTitle: "Obnovit název",
|
||||
RefreshToast: "Požadavek na obnovení názvu byl odeslán",
|
||||
},
|
||||
Commands: {
|
||||
new: "Nová konverzace",
|
||||
@@ -410,6 +412,10 @@ const cs: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Model (model)",
|
||||
CompressModel: {
|
||||
Title: "Kompresní model",
|
||||
SubTitle: "Model používaný pro kompresi historie",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Náhodnost (temperature)",
|
||||
SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi",
|
||||
|
||||
@@ -43,6 +43,8 @@ const de: PartialLocaleType = {
|
||||
PinToastAction: "Ansehen",
|
||||
Delete: "Löschen",
|
||||
Edit: "Bearbeiten",
|
||||
RefreshTitle: "Titel aktualisieren",
|
||||
RefreshToast: "Anfrage zur Titelaktualisierung gesendet",
|
||||
},
|
||||
Commands: {
|
||||
new: "Neues Gespräch",
|
||||
@@ -421,6 +423,10 @@ const de: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Modell",
|
||||
CompressModel: {
|
||||
Title: "Kompressionsmodell",
|
||||
SubTitle: "Modell zur Komprimierung des Verlaufs",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Zufälligkeit (temperature)",
|
||||
SubTitle: "Je höher der Wert, desto zufälliger die Antwort",
|
||||
|
||||
@@ -45,6 +45,10 @@ const en: LocaleType = {
|
||||
Delete: "Delete",
|
||||
Edit: "Edit",
|
||||
FullScreen: "FullScreen",
|
||||
RefreshTitle: "Refresh Title",
|
||||
RefreshToast: "Title refresh request sent",
|
||||
Speech: "Play",
|
||||
StopSpeech: "Stop",
|
||||
},
|
||||
Commands: {
|
||||
new: "Start a new chat",
|
||||
@@ -52,6 +56,7 @@ const en: LocaleType = {
|
||||
next: "Next Chat",
|
||||
prev: "Previous Chat",
|
||||
clear: "Clear Context",
|
||||
fork: "Copy Chat",
|
||||
del: "Delete Chat",
|
||||
},
|
||||
InputActions: {
|
||||
@@ -78,11 +83,21 @@ const en: LocaleType = {
|
||||
return inputHints + ", / to search prompts, : to use commands";
|
||||
},
|
||||
Send: "Send",
|
||||
StartSpeak: "Start Speak",
|
||||
StopSpeak: "Stop Speak",
|
||||
Config: {
|
||||
Reset: "Reset to Default",
|
||||
SaveAs: "Save as Mask",
|
||||
},
|
||||
IsContext: "Contextual Prompt",
|
||||
ShortcutKey: {
|
||||
Title: "Keyboard Shortcuts",
|
||||
newChat: "Open New Chat",
|
||||
focusInput: "Focus Input Field",
|
||||
copyLastMessage: "Copy Last Reply",
|
||||
copyLastCode: "Copy Last Code Block",
|
||||
showShortcutKey: "Show Shortcuts",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "Export Messages",
|
||||
@@ -466,6 +481,10 @@ const en: LocaleType = {
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
CompressModel: {
|
||||
Title: "Compression Model",
|
||||
SubTitle: "Model used to compress history",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
SubTitle: "A larger value makes the more random output",
|
||||
@@ -488,6 +507,27 @@ const en: LocaleType = {
|
||||
SubTitle:
|
||||
"A larger value decreasing the likelihood to repeat the same line",
|
||||
},
|
||||
TTS: {
|
||||
Enable: {
|
||||
Title: "Enable TTS",
|
||||
SubTitle: "Enable text-to-speech service",
|
||||
},
|
||||
Autoplay: {
|
||||
Title: "Enable Autoplay",
|
||||
SubTitle:
|
||||
"Automatically generate speech and play, you need to enable the text-to-speech switch first",
|
||||
},
|
||||
Model: "Model",
|
||||
Voice: {
|
||||
Title: "Voice",
|
||||
SubTitle: "The voice to use when generating the audio",
|
||||
},
|
||||
Speed: {
|
||||
Title: "Speed",
|
||||
SubTitle: "The speed of the generated audio",
|
||||
},
|
||||
Engine: "TTS Engine",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "New Conversation",
|
||||
|
||||
@@ -44,6 +44,8 @@ const es: PartialLocaleType = {
|
||||
PinToastAction: "Ver",
|
||||
Delete: "Eliminar",
|
||||
Edit: "Editar",
|
||||
RefreshTitle: "Actualizar título",
|
||||
RefreshToast: "Se ha enviado la solicitud de actualización del título",
|
||||
},
|
||||
Commands: {
|
||||
new: "Nueva conversación",
|
||||
@@ -423,6 +425,10 @@ const es: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Modelo (model)",
|
||||
CompressModel: {
|
||||
Title: "Modelo de compresión",
|
||||
SubTitle: "Modelo utilizado para comprimir el historial",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Aleatoriedad (temperature)",
|
||||
SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado",
|
||||
|
||||
@@ -43,6 +43,8 @@ const fr: PartialLocaleType = {
|
||||
PinToastAction: "Voir",
|
||||
Delete: "Supprimer",
|
||||
Edit: "Modifier",
|
||||
RefreshTitle: "Actualiser le titre",
|
||||
RefreshToast: "Demande d'actualisation du titre envoyée",
|
||||
},
|
||||
Commands: {
|
||||
new: "Nouvelle discussion",
|
||||
@@ -422,6 +424,10 @@ const fr: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Modèle",
|
||||
CompressModel: {
|
||||
Title: "Modèle de compression",
|
||||
SubTitle: "Modèle utilisé pour compresser l'historique",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Aléatoire (temperature)",
|
||||
SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires",
|
||||
|
||||
@@ -43,6 +43,8 @@ const id: PartialLocaleType = {
|
||||
PinToastAction: "Lihat",
|
||||
Delete: "Hapus",
|
||||
Edit: "Edit",
|
||||
RefreshTitle: "Segarkan Judul",
|
||||
RefreshToast: "Permintaan penyegaran judul telah dikirim",
|
||||
},
|
||||
Commands: {
|
||||
new: "Obrolan Baru",
|
||||
@@ -411,6 +413,10 @@ const id: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
CompressModel: {
|
||||
Title: "Model Kompresi",
|
||||
SubTitle: "Model yang digunakan untuk mengompres riwayat",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Randomness (temperature)",
|
||||
SubTitle: "Semakin tinggi nilainya, semakin acak responsnya",
|
||||
|
||||
@@ -18,10 +18,13 @@ import ar from "./ar";
|
||||
import bn from "./bn";
|
||||
import sk from "./sk";
|
||||
import { merge } from "../utils/merge";
|
||||
import { safeLocalStorage } from "@/app/utils";
|
||||
|
||||
import type { LocaleType } from "./cn";
|
||||
export type { LocaleType, PartialLocaleType } from "./cn";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
const ALL_LANGS = {
|
||||
cn,
|
||||
en,
|
||||
@@ -82,17 +85,11 @@ merge(fallbackLang, targetLang);
|
||||
export default fallbackLang as LocaleType;
|
||||
|
||||
function getItem(key: string) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return localStorage.getItem(key);
|
||||
}
|
||||
|
||||
function setItem(key: string, value: string) {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch {}
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
function getLanguage() {
|
||||
@@ -137,3 +134,34 @@ export function getISOLang() {
|
||||
const lang = getLang();
|
||||
return isoLangString[lang] ?? lang;
|
||||
}
|
||||
|
||||
const DEFAULT_STT_LANG = "zh-CN";
|
||||
export const STT_LANG_MAP: Record<Lang, string> = {
|
||||
cn: "zh-CN",
|
||||
en: "en-US",
|
||||
pt: "pt-BR",
|
||||
tw: "zh-TW",
|
||||
jp: "ja-JP",
|
||||
ko: "ko-KR",
|
||||
id: "id-ID",
|
||||
fr: "fr-FR",
|
||||
es: "es-ES",
|
||||
it: "it-IT",
|
||||
tr: "tr-TR",
|
||||
de: "de-DE",
|
||||
vi: "vi-VN",
|
||||
ru: "ru-RU",
|
||||
cs: "cs-CZ",
|
||||
no: "no-NO",
|
||||
ar: "ar-SA",
|
||||
bn: "bn-BD",
|
||||
sk: "sk-SK",
|
||||
};
|
||||
|
||||
export function getSTTLang(): string {
|
||||
try {
|
||||
return STT_LANG_MAP[getLang()];
|
||||
} catch {
|
||||
return DEFAULT_STT_LANG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ const it: PartialLocaleType = {
|
||||
PinToastAction: "Visualizza",
|
||||
Delete: "Elimina",
|
||||
Edit: "Modifica",
|
||||
RefreshTitle: "Aggiorna titolo",
|
||||
RefreshToast: "Richiesta di aggiornamento del titolo inviata",
|
||||
},
|
||||
Commands: {
|
||||
new: "Nuova chat",
|
||||
@@ -423,6 +425,10 @@ const it: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Modello (model)",
|
||||
CompressModel: {
|
||||
Title: "Modello di compressione",
|
||||
SubTitle: "Modello utilizzato per comprimere la cronologia",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Casualità (temperature)",
|
||||
SubTitle: "Valore più alto, risposte più casuali",
|
||||
|
||||
@@ -43,6 +43,8 @@ const jp: PartialLocaleType = {
|
||||
PinToastAction: "見る",
|
||||
Delete: "削除",
|
||||
Edit: "編集",
|
||||
RefreshTitle: "タイトルを更新",
|
||||
RefreshToast: "タイトル更新リクエストが送信されました",
|
||||
},
|
||||
Commands: {
|
||||
new: "新しいチャット",
|
||||
@@ -407,6 +409,10 @@ const jp: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "モデル (model)",
|
||||
CompressModel: {
|
||||
Title: "圧縮モデル",
|
||||
SubTitle: "履歴を圧縮するために使用されるモデル",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "ランダム性 (temperature)",
|
||||
SubTitle: "値が大きいほど応答がランダムになります",
|
||||
|
||||
@@ -43,6 +43,8 @@ const ko: PartialLocaleType = {
|
||||
PinToastAction: "보기",
|
||||
Delete: "삭제",
|
||||
Edit: "편집",
|
||||
RefreshTitle: "제목 새로고침",
|
||||
RefreshToast: "제목 새로고침 요청이 전송되었습니다",
|
||||
},
|
||||
Commands: {
|
||||
new: "새 채팅",
|
||||
@@ -404,6 +406,10 @@ const ko: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "모델 (model)",
|
||||
CompressModel: {
|
||||
Title: "압축 모델",
|
||||
SubTitle: "기록을 압축하는 데 사용되는 모델",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "무작위성 (temperature)",
|
||||
SubTitle: "값이 클수록 응답이 더 무작위적",
|
||||
|
||||
@@ -44,6 +44,8 @@ const no: PartialLocaleType = {
|
||||
PinToastAction: "Se",
|
||||
Delete: "Slett",
|
||||
Edit: "Rediger",
|
||||
RefreshTitle: "Oppdater tittel",
|
||||
RefreshToast: "Forespørsel om titteloppdatering sendt",
|
||||
},
|
||||
Commands: {
|
||||
new: "Ny samtale",
|
||||
@@ -415,6 +417,10 @@ const no: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Modell",
|
||||
CompressModel: {
|
||||
Title: "Komprimeringsmodell",
|
||||
SubTitle: "Modell brukt for å komprimere historikken",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Tilfeldighet (temperature)",
|
||||
SubTitle: "Høyere verdi gir mer tilfeldige svar",
|
||||
|
||||
@@ -43,6 +43,8 @@ const pt: PartialLocaleType = {
|
||||
PinToastAction: "Visualizar",
|
||||
Delete: "Deletar",
|
||||
Edit: "Editar",
|
||||
RefreshTitle: "Atualizar Título",
|
||||
RefreshToast: "Solicitação de atualização de título enviada",
|
||||
},
|
||||
Commands: {
|
||||
new: "Iniciar um novo chat",
|
||||
@@ -346,6 +348,10 @@ const pt: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Modelo",
|
||||
CompressModel: {
|
||||
Title: "Modelo de Compressão",
|
||||
SubTitle: "Modelo usado para comprimir o histórico",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Temperatura",
|
||||
SubTitle: "Um valor maior torna a saída mais aleatória",
|
||||
|
||||
@@ -43,6 +43,8 @@ const ru: PartialLocaleType = {
|
||||
PinToastAction: "Просмотреть",
|
||||
Delete: "Удалить",
|
||||
Edit: "Редактировать",
|
||||
RefreshTitle: "Обновить заголовок",
|
||||
RefreshToast: "Запрос на обновление заголовка отправлен",
|
||||
},
|
||||
Commands: {
|
||||
new: "Новый чат",
|
||||
@@ -414,6 +416,10 @@ const ru: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Модель",
|
||||
CompressModel: {
|
||||
Title: "Модель сжатия",
|
||||
SubTitle: "Модель, используемая для сжатия истории",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Случайность (temperature)",
|
||||
SubTitle: "Чем больше значение, тем более случайные ответы",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { LocaleType } from "./index";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
|
||||
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
|
||||
@@ -45,6 +44,8 @@ const sk: PartialLocaleType = {
|
||||
PinToastAction: "Zobraziť",
|
||||
Delete: "Vymazať",
|
||||
Edit: "Upraviť",
|
||||
RefreshTitle: "Obnoviť názov",
|
||||
RefreshToast: "Požiadavka na obnovenie názvu bola odoslaná",
|
||||
},
|
||||
Commands: {
|
||||
new: "Začať nový chat",
|
||||
@@ -365,6 +366,10 @@ const sk: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Model",
|
||||
CompressModel: {
|
||||
Title: "Kompresný model",
|
||||
SubTitle: "Model používaný na kompresiu histórie",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Teplota",
|
||||
SubTitle: "Vyššia hodnota robí výstup náhodnejším",
|
||||
|
||||
@@ -43,6 +43,8 @@ const tr: PartialLocaleType = {
|
||||
PinToastAction: "Görünüm",
|
||||
Delete: "Sil",
|
||||
Edit: "Düzenle",
|
||||
RefreshTitle: "Başlığı Yenile",
|
||||
RefreshToast: "Başlık yenileme isteği gönderildi",
|
||||
},
|
||||
Commands: {
|
||||
new: "Yeni sohbet",
|
||||
@@ -414,6 +416,10 @@ const tr: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Model (model)",
|
||||
CompressModel: {
|
||||
Title: "Sıkıştırma Modeli",
|
||||
SubTitle: "Geçmişi sıkıştırmak için kullanılan model",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Rastgelelik (temperature)",
|
||||
SubTitle: "Değer arttıkça yanıt daha rastgele olur",
|
||||
|
||||
@@ -43,6 +43,8 @@ const tw = {
|
||||
PinToastAction: "檢視",
|
||||
Delete: "刪除",
|
||||
Edit: "編輯",
|
||||
RefreshTitle: "刷新標題",
|
||||
RefreshToast: "已發送刷新標題請求",
|
||||
},
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
@@ -81,6 +83,14 @@ const tw = {
|
||||
SaveAs: "另存新檔",
|
||||
},
|
||||
IsContext: "預設提示詞",
|
||||
ShortcutKey: {
|
||||
Title: "鍵盤快捷方式",
|
||||
newChat: "打開新聊天",
|
||||
focusInput: "聚焦輸入框",
|
||||
copyLastMessage: "複製最後一個回覆",
|
||||
copyLastCode: "複製最後一個代碼塊",
|
||||
showShortcutKey: "顯示快捷方式",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "將聊天記錄匯出為 Markdown",
|
||||
@@ -360,6 +370,10 @@ const tw = {
|
||||
},
|
||||
|
||||
Model: "模型 (model)",
|
||||
CompressModel: {
|
||||
Title: "壓縮模型",
|
||||
SubTitle: "用於壓縮歷史記錄的模型",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "隨機性 (temperature)",
|
||||
SubTitle: "值越大,回應越隨機",
|
||||
|
||||
@@ -43,6 +43,8 @@ const vi: PartialLocaleType = {
|
||||
PinToastAction: "Xem",
|
||||
Delete: "Xóa",
|
||||
Edit: "Chỉnh sửa",
|
||||
RefreshTitle: "Làm mới tiêu đề",
|
||||
RefreshToast: "Đã gửi yêu cầu làm mới tiêu đề",
|
||||
},
|
||||
Commands: {
|
||||
new: "Tạo cuộc trò chuyện mới",
|
||||
@@ -410,6 +412,10 @@ const vi: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Model: "Mô hình (model)",
|
||||
CompressModel: {
|
||||
Title: "Mô hình nén",
|
||||
SubTitle: "Mô hình được sử dụng để nén lịch sử",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Độ ngẫu nhiên (temperature)",
|
||||
SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên",
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Mask } from "../store/mask";
|
||||
import { CN_MASKS } from "./cn";
|
||||
import { TW_MASKS } from "./tw";
|
||||
import { EN_MASKS } from "./en";
|
||||
|
||||
import { type BuiltinMask } from "./typing";
|
||||
export { type BuiltinMask } from "./typing";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
GoogleSafetySettingsThreshold,
|
||||
ServiceProvider,
|
||||
StoreKey,
|
||||
@@ -15,46 +14,6 @@ let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
|
||||
const isApp = getClientConfig()?.buildMode === "export";
|
||||
|
||||
const DEFAULT_OPENAI_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/openai"
|
||||
: ApiPath.OpenAI;
|
||||
|
||||
const DEFAULT_GOOGLE_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/google"
|
||||
: ApiPath.Google;
|
||||
|
||||
const DEFAULT_ANTHROPIC_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
|
||||
const DEFAULT_BAIDU_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/baidu"
|
||||
: ApiPath.Baidu;
|
||||
|
||||
const DEFAULT_BYTEDANCE_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/bytedance"
|
||||
: ApiPath.ByteDance;
|
||||
|
||||
const DEFAULT_ALIBABA_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
||||
: ApiPath.Alibaba;
|
||||
|
||||
const DEFAULT_TENCENT_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
|
||||
const DEFAULT_MOONSHOT_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/moonshot"
|
||||
: ApiPath.Moonshot;
|
||||
|
||||
const DEFAULT_STABILITY_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/stability"
|
||||
: ApiPath.Stability;
|
||||
|
||||
const DEFAULT_IFLYTEK_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/iflytek"
|
||||
: ApiPath.Iflytek;
|
||||
|
||||
const DEFAULT_ACCESS_STATE = {
|
||||
accessCode: "",
|
||||
useCustomConfig: false,
|
||||
@@ -62,7 +21,7 @@ const DEFAULT_ACCESS_STATE = {
|
||||
provider: ServiceProvider.OpenAI,
|
||||
|
||||
// openai
|
||||
openaiUrl: DEFAULT_OPENAI_URL,
|
||||
openaiUrl: ApiPath.OpenAI as string,
|
||||
openaiApiKey: "",
|
||||
|
||||
// azure
|
||||
@@ -71,44 +30,44 @@ const DEFAULT_ACCESS_STATE = {
|
||||
azureApiVersion: "2023-08-01-preview",
|
||||
|
||||
// google ai studio
|
||||
googleUrl: DEFAULT_GOOGLE_URL,
|
||||
googleUrl: ApiPath.Google as string,
|
||||
googleApiKey: "",
|
||||
googleApiVersion: "v1",
|
||||
googleSafetySettings: GoogleSafetySettingsThreshold.BLOCK_ONLY_HIGH,
|
||||
|
||||
// anthropic
|
||||
anthropicUrl: DEFAULT_ANTHROPIC_URL,
|
||||
anthropicUrl: ApiPath.Anthropic as string,
|
||||
anthropicApiKey: "",
|
||||
anthropicApiVersion: "2023-06-01",
|
||||
|
||||
// baidu
|
||||
baiduUrl: DEFAULT_BAIDU_URL,
|
||||
baiduUrl: ApiPath.Baidu as string,
|
||||
baiduApiKey: "",
|
||||
baiduSecretKey: "",
|
||||
|
||||
// bytedance
|
||||
bytedanceUrl: DEFAULT_BYTEDANCE_URL,
|
||||
bytedanceUrl: ApiPath.ByteDance as string,
|
||||
bytedanceApiKey: "",
|
||||
|
||||
// alibaba
|
||||
alibabaUrl: DEFAULT_ALIBABA_URL,
|
||||
alibabaUrl: ApiPath.Alibaba as string,
|
||||
alibabaApiKey: "",
|
||||
|
||||
// moonshot
|
||||
moonshotUrl: DEFAULT_MOONSHOT_URL,
|
||||
moonshotUrl: ApiPath.Moonshot as string,
|
||||
moonshotApiKey: "",
|
||||
|
||||
//stability
|
||||
stabilityUrl: DEFAULT_STABILITY_URL,
|
||||
stabilityUrl: ApiPath.Stability as string,
|
||||
stabilityApiKey: "",
|
||||
|
||||
// tencent
|
||||
tencentUrl: DEFAULT_TENCENT_URL,
|
||||
tencentUrl: ApiPath.Tencent as string,
|
||||
tencentSecretKey: "",
|
||||
tencentSecretId: "",
|
||||
|
||||
// iflytek
|
||||
iflytekUrl: DEFAULT_IFLYTEK_URL,
|
||||
iflytekUrl: ApiPath.Iflytek as string,
|
||||
iflytekApiKey: "",
|
||||
iflytekApiSecret: "",
|
||||
|
||||
@@ -120,6 +79,9 @@ const DEFAULT_ACCESS_STATE = {
|
||||
disableFastLink: false,
|
||||
customModels: "",
|
||||
defaultModel: "",
|
||||
|
||||
// tts config
|
||||
edgeTTSVoiceName: "zh-CN-YunxiNeural",
|
||||
};
|
||||
|
||||
export const useAccessStore = createPersistStore(
|
||||
@@ -132,6 +94,12 @@ export const useAccessStore = createPersistStore(
|
||||
return get().needCode;
|
||||
},
|
||||
|
||||
edgeVoiceName() {
|
||||
this.fetch();
|
||||
|
||||
return get().edgeTTSVoiceName;
|
||||
},
|
||||
|
||||
isValidOpenAI() {
|
||||
return ensure(get(), ["openaiApiKey"]);
|
||||
},
|
||||
@@ -204,8 +172,8 @@ export const useAccessStore = createPersistStore(
|
||||
.then((res) => {
|
||||
// Set default model from env request
|
||||
let defaultModel = res.defaultModel ?? "";
|
||||
DEFAULT_CONFIG.modelConfig.model =
|
||||
defaultModel !== "" ? defaultModel : "gpt-3.5-turbo";
|
||||
if (defaultModel !== "")
|
||||
DEFAULT_CONFIG.modelConfig.model = defaultModel;
|
||||
return res;
|
||||
})
|
||||
.then((res: DangerConfig) => {
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import { trimTopic, getMessageTextContent } from "../utils";
|
||||
import { getMessageTextContent, trimTopic } from "../utils";
|
||||
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||
import { nanoid } from "nanoid";
|
||||
import type {
|
||||
ClientApi,
|
||||
MultimodalContent,
|
||||
RequestMessage,
|
||||
} from "../client/api";
|
||||
import { getClientApi } from "../client/api";
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_SYSTEM_TEMPLATE,
|
||||
KnowledgeCutOffDate,
|
||||
StoreKey,
|
||||
SUMMARIZE_MODEL,
|
||||
GEMINI_SUMMARIZE_MODEL,
|
||||
} from "../constant";
|
||||
import { getClientApi } from "../client/api";
|
||||
import type {
|
||||
ClientApi,
|
||||
RequestMessage,
|
||||
MultimodalContent,
|
||||
} from "../client/api";
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { isDalle3, safeLocalStorage } from "../utils";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
import { nanoid } from "nanoid";
|
||||
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";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
export type ChatMessageTool = {
|
||||
id: string;
|
||||
@@ -104,27 +102,6 @@ function createEmptySession(): ChatSession {
|
||||
};
|
||||
}
|
||||
|
||||
function getSummarizeModel(currentModel: string) {
|
||||
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||
if (currentModel.startsWith("gpt")) {
|
||||
const configStore = useAppConfig.getState();
|
||||
const accessStore = useAccessStore.getState();
|
||||
const allModel = collectModelsWithDefaultModel(
|
||||
configStore.models,
|
||||
[configStore.customModels, accessStore.customModels].join(","),
|
||||
accessStore.defaultModel,
|
||||
);
|
||||
const summarizeModel = allModel.find(
|
||||
(m) => m.name === SUMMARIZE_MODEL && m.available,
|
||||
);
|
||||
return summarizeModel?.name ?? currentModel;
|
||||
}
|
||||
if (currentModel.startsWith("gemini")) {
|
||||
return GEMINI_SUMMARIZE_MODEL;
|
||||
}
|
||||
return currentModel;
|
||||
}
|
||||
|
||||
function countMessages(msgs: ChatMessage[]) {
|
||||
return msgs.reduce(
|
||||
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
||||
@@ -179,6 +156,7 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
const DEFAULT_CHAT_STATE = {
|
||||
sessions: [createEmptySession()],
|
||||
currentSessionIndex: 0,
|
||||
lastInput: "",
|
||||
};
|
||||
|
||||
export const useChatStore = createPersistStore(
|
||||
@@ -192,6 +170,28 @@ export const useChatStore = createPersistStore(
|
||||
}
|
||||
|
||||
const methods = {
|
||||
forkSession() {
|
||||
// 获取当前会话
|
||||
const currentSession = get().currentSession();
|
||||
if (!currentSession) return;
|
||||
|
||||
const newSession = createEmptySession();
|
||||
|
||||
newSession.topic = currentSession.topic;
|
||||
newSession.messages = [...currentSession.messages];
|
||||
newSession.mask = {
|
||||
...currentSession.mask,
|
||||
modelConfig: {
|
||||
...currentSession.mask.modelConfig,
|
||||
},
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
currentSessionIndex: 0,
|
||||
sessions: [newSession, ...state.sessions],
|
||||
}));
|
||||
},
|
||||
|
||||
clearSessions() {
|
||||
set(() => ({
|
||||
sessions: [createEmptySession()],
|
||||
@@ -476,7 +476,8 @@ export const useChatStore = createPersistStore(
|
||||
// system prompts, to get close to OpenAI Web ChatGPT
|
||||
const shouldInjectSystemPrompts =
|
||||
modelConfig.enableInjectSystemPrompts &&
|
||||
session.mask.modelConfig.model.startsWith("gpt-");
|
||||
(session.mask.modelConfig.model.startsWith("gpt-") ||
|
||||
session.mask.modelConfig.model.startsWith("chatgpt-"));
|
||||
|
||||
var systemPrompts: ChatMessage[] = [];
|
||||
systemPrompts = shouldInjectSystemPrompts
|
||||
@@ -568,7 +569,7 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
},
|
||||
|
||||
summarizeSession() {
|
||||
summarizeSession(refreshTitle: boolean = false) {
|
||||
const config = useAppConfig.getState();
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
@@ -577,7 +578,7 @@ export const useChatStore = createPersistStore(
|
||||
return;
|
||||
}
|
||||
|
||||
const providerName = modelConfig.providerName;
|
||||
const providerName = modelConfig.compressProviderName;
|
||||
const api: ClientApi = getClientApi(providerName);
|
||||
|
||||
// remove error messages if any
|
||||
@@ -586,20 +587,30 @@ export const useChatStore = createPersistStore(
|
||||
// should summarize topic after chating more than 50 words
|
||||
const SUMMARIZE_MIN_LEN = 50;
|
||||
if (
|
||||
config.enableAutoGenerateTitle &&
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(messages) >= SUMMARIZE_MIN_LEN
|
||||
(config.enableAutoGenerateTitle &&
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(messages) >= SUMMARIZE_MIN_LEN) ||
|
||||
refreshTitle
|
||||
) {
|
||||
const topicMessages = messages.concat(
|
||||
createMessage({
|
||||
role: "user",
|
||||
content: Locale.Store.Prompt.Topic,
|
||||
}),
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
messages.length - modelConfig.historyMessageCount,
|
||||
);
|
||||
const topicMessages = messages
|
||||
.slice(
|
||||
startIndex < messages.length ? startIndex : messages.length - 1,
|
||||
messages.length,
|
||||
)
|
||||
.concat(
|
||||
createMessage({
|
||||
role: "user",
|
||||
content: Locale.Store.Prompt.Topic,
|
||||
}),
|
||||
);
|
||||
api.llm.chat({
|
||||
messages: topicMessages,
|
||||
config: {
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
model: modelConfig.compressModel,
|
||||
stream: false,
|
||||
providerName,
|
||||
},
|
||||
@@ -662,7 +673,7 @@ export const useChatStore = createPersistStore(
|
||||
config: {
|
||||
...modelcfg,
|
||||
stream: true,
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
model: modelConfig.compressModel,
|
||||
},
|
||||
onUpdate(message) {
|
||||
session.memoryPrompt = message;
|
||||
@@ -700,13 +711,18 @@ export const useChatStore = createPersistStore(
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
},
|
||||
setLastInput(lastInput: string) {
|
||||
set({
|
||||
lastInput,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return methods;
|
||||
},
|
||||
{
|
||||
name: StoreKey.Chat,
|
||||
version: 3.1,
|
||||
version: 3.2,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as any;
|
||||
const newState = JSON.parse(
|
||||
@@ -753,6 +769,16 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
}
|
||||
|
||||
// add default summarize model for every session
|
||||
if (version < 3.2) {
|
||||
newState.sessions.forEach((s) => {
|
||||
const config = useAppConfig.getState();
|
||||
s.mask.modelConfig.compressModel = config.modelConfig.compressModel;
|
||||
s.mask.modelConfig.compressProviderName =
|
||||
config.modelConfig.compressProviderName;
|
||||
});
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,12 +5,21 @@ import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
DEFAULT_TTS_ENGINE,
|
||||
DEFAULT_TTS_ENGINES,
|
||||
DEFAULT_TTS_MODEL,
|
||||
DEFAULT_TTS_MODELS,
|
||||
DEFAULT_TTS_VOICE,
|
||||
DEFAULT_TTS_VOICES,
|
||||
StoreKey,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||
export type TTSModelType = (typeof DEFAULT_TTS_MODELS)[number];
|
||||
export type TTSVoiceType = (typeof DEFAULT_TTS_VOICES)[number];
|
||||
export type TTSEngineType = (typeof DEFAULT_TTS_ENGINES)[number];
|
||||
|
||||
export enum SubmitKey {
|
||||
Enter = "Enter",
|
||||
@@ -41,6 +50,8 @@ export const DEFAULT_CONFIG = {
|
||||
enableAutoGenerateTitle: true,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
|
||||
enableArtifacts: true, // show artifacts config
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||
@@ -50,7 +61,7 @@ export const DEFAULT_CONFIG = {
|
||||
models: DEFAULT_MODELS as any as LLMModel[],
|
||||
|
||||
modelConfig: {
|
||||
model: "gpt-3.5-turbo" as ModelType,
|
||||
model: "gpt-4o-mini" as ModelType,
|
||||
providerName: "OpenAI" as ServiceProvider,
|
||||
temperature: 0.5,
|
||||
top_p: 1,
|
||||
@@ -60,17 +71,29 @@ export const DEFAULT_CONFIG = {
|
||||
sendMemory: true,
|
||||
historyMessageCount: 4,
|
||||
compressMessageLengthThreshold: 1000,
|
||||
compressModel: "gpt-4o-mini" as ModelType,
|
||||
compressProviderName: "OpenAI" as ServiceProvider,
|
||||
enableInjectSystemPrompts: true,
|
||||
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||
size: "1024x1024" as DalleSize,
|
||||
quality: "standard" as DalleQuality,
|
||||
style: "vivid" as DalleStyle,
|
||||
},
|
||||
|
||||
ttsConfig: {
|
||||
enable: false,
|
||||
autoplay: false,
|
||||
engine: DEFAULT_TTS_ENGINE,
|
||||
model: DEFAULT_TTS_MODEL,
|
||||
voice: DEFAULT_TTS_VOICE,
|
||||
speed: 1.0,
|
||||
},
|
||||
};
|
||||
|
||||
export type ChatConfig = typeof DEFAULT_CONFIG;
|
||||
|
||||
export type ModelConfig = ChatConfig["modelConfig"];
|
||||
export type TTSConfig = ChatConfig["ttsConfig"];
|
||||
|
||||
export function limitNumber(
|
||||
x: number,
|
||||
@@ -85,6 +108,21 @@ export function limitNumber(
|
||||
return Math.min(max, Math.max(min, x));
|
||||
}
|
||||
|
||||
export const TTSConfigValidator = {
|
||||
engine(x: string) {
|
||||
return x as TTSEngineType;
|
||||
},
|
||||
model(x: string) {
|
||||
return x as TTSModelType;
|
||||
},
|
||||
voice(x: string) {
|
||||
return x as TTSVoiceType;
|
||||
},
|
||||
speed(x: number) {
|
||||
return limitNumber(x, 0.25, 4.0, 1.0);
|
||||
},
|
||||
};
|
||||
|
||||
export const ModalConfigValidator = {
|
||||
model(x: string) {
|
||||
return x as ModelType;
|
||||
@@ -140,7 +178,22 @@ export const useAppConfig = createPersistStore(
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Config,
|
||||
version: 3.9,
|
||||
version: 4,
|
||||
|
||||
merge(persistedState, currentState) {
|
||||
const state = persistedState as ChatConfig | undefined;
|
||||
if (!state) return { ...currentState };
|
||||
const models = currentState.models.slice();
|
||||
state.models.forEach((pModel) => {
|
||||
const idx = models.findIndex(
|
||||
(v) => v.name === pModel.name && v.provider === pModel.provider,
|
||||
);
|
||||
if (idx !== -1) models[idx] = pModel;
|
||||
else models.push(pModel);
|
||||
});
|
||||
return { ...currentState, ...state, models: models };
|
||||
},
|
||||
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as ChatConfig;
|
||||
|
||||
@@ -178,6 +231,13 @@ export const useAppConfig = createPersistStore(
|
||||
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||
}
|
||||
|
||||
if (version < 4) {
|
||||
state.modelConfig.compressModel =
|
||||
DEFAULT_CONFIG.modelConfig.compressModel;
|
||||
state.modelConfig.compressProviderName =
|
||||
DEFAULT_CONFIG.modelConfig.compressProviderName;
|
||||
}
|
||||
|
||||
return state as any;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -23,9 +23,12 @@ export type Mask = {
|
||||
|
||||
export const DEFAULT_MASK_STATE = {
|
||||
masks: {} as Record<string, Mask>,
|
||||
language: undefined as Lang | undefined,
|
||||
};
|
||||
|
||||
export type MaskState = typeof DEFAULT_MASK_STATE;
|
||||
export type MaskState = typeof DEFAULT_MASK_STATE & {
|
||||
language?: Lang | undefined;
|
||||
};
|
||||
|
||||
export const DEFAULT_MASK_AVATAR = "gpt-bot";
|
||||
export const createEmptyMask = () =>
|
||||
@@ -102,6 +105,11 @@ export const useMaskStore = createPersistStore(
|
||||
search(text: string) {
|
||||
return Object.values(get().masks);
|
||||
},
|
||||
setLanguage(language: Lang | undefined) {
|
||||
set({
|
||||
language,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Mask,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import OpenAPIClientAxios from "openapi-client-axios";
|
||||
import { getLang, Lang } from "../locales";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import yaml from "js-yaml";
|
||||
import { adapter } from "../utils";
|
||||
import { useAccessStore } from "./access";
|
||||
|
||||
const isApp = getClientConfig()?.isApp;
|
||||
|
||||
export type Plugin = {
|
||||
id: string;
|
||||
@@ -17,7 +20,6 @@ export type Plugin = {
|
||||
authLocation?: string;
|
||||
authHeader?: string;
|
||||
authToken?: string;
|
||||
usingProxy?: boolean;
|
||||
};
|
||||
|
||||
export type FunctionToolItem = {
|
||||
@@ -47,18 +49,25 @@ export const FunctionToolService = {
|
||||
plugin?.authType == "basic"
|
||||
? `Basic ${plugin?.authToken}`
|
||||
: plugin?.authType == "bearer"
|
||||
? ` Bearer ${plugin?.authToken}`
|
||||
? `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 baseURL = !isApp ? "/api/proxy" : serverURL;
|
||||
const headers: Record<string, string | undefined> = {
|
||||
"X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
|
||||
"X-Base-URL": !isApp ? serverURL : undefined,
|
||||
};
|
||||
if (authLocation == "header") {
|
||||
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({
|
||||
definition: yaml.load(plugin.content) as any,
|
||||
axiosConfigDefaults: {
|
||||
@@ -166,7 +175,7 @@ export const usePluginStore = createPersistStore(
|
||||
(set, get) => ({
|
||||
create(plugin?: Partial<Plugin>) {
|
||||
const plugins = get().plugins;
|
||||
const id = nanoid();
|
||||
const id = plugin?.id || nanoid();
|
||||
plugins[id] = {
|
||||
...createEmptyPlugin(),
|
||||
...plugin,
|
||||
@@ -199,7 +208,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));
|
||||
@@ -221,5 +230,42 @@ export const usePluginStore = createPersistStore(
|
||||
{
|
||||
name: StoreKey.Plugin,
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { getLang } from "../locales";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { StoreKey } from "../constant";
|
||||
import { getLang } from "../locales";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export interface Prompt {
|
||||
@@ -147,6 +147,11 @@ export const usePromptStore = createPersistStore(
|
||||
},
|
||||
|
||||
onRehydrateStorage(state) {
|
||||
// Skip store rehydration on server side
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const PROMPT_URL = "./prompts.json";
|
||||
|
||||
type PromptList = Array<[string, string]>;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { Updater } from "../typing";
|
||||
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import {
|
||||
@@ -13,7 +12,6 @@ import { downloadAs, readFromFile } from "../utils";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import Locale from "../locales";
|
||||
import { createSyncClient, ProviderType } from "../utils/cloud";
|
||||
import { corsPath } from "../utils/cors";
|
||||
|
||||
export interface WebDavConfig {
|
||||
server: string;
|
||||
@@ -27,7 +25,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
useProxy: true,
|
||||
proxyUrl: corsPath(ApiPath.Cors),
|
||||
proxyUrl: ApiPath.Cors as string,
|
||||
|
||||
webdav: {
|
||||
endpoint: "",
|
||||
@@ -100,15 +98,17 @@ export const useSyncStore = createPersistStore(
|
||||
const remoteState = await client.get(config.username);
|
||||
if (!remoteState || remoteState === "") {
|
||||
await client.set(config.username, JSON.stringify(localState));
|
||||
console.log("[Sync] Remote state is empty, using local state instead.");
|
||||
return
|
||||
console.log(
|
||||
"[Sync] Remote state is empty, using local state instead.",
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const parsedRemoteState = JSON.parse(
|
||||
await client.get(config.username),
|
||||
) as AppState;
|
||||
mergeAppState(localState, parsedRemoteState);
|
||||
setLocalAppState(localState);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Sync] failed to get remote state", e);
|
||||
throw e;
|
||||
|
||||
@@ -8,8 +8,6 @@ import { getClientConfig } from "../config/client";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import ChatGptIcon from "../icons/chatgpt.png";
|
||||
import Locale from "../locales";
|
||||
import { use } from "react";
|
||||
import { useAppConfig } from ".";
|
||||
import { ClientApi } from "../client/api";
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
86
app/utils.ts
86
app/utils.ts
@@ -2,9 +2,8 @@ import { useEffect, useState } from "react";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
import { RequestMessage } from "./client/api";
|
||||
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
|
||||
import isObject from "lodash-es/isObject";
|
||||
import { fetch as tauriFetch, Body, ResponseType } from "@tauri-apps/api/http";
|
||||
import { ServiceProvider } from "./constant";
|
||||
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
||||
|
||||
export function trimTopic(topic: string) {
|
||||
// Fix an issue where double quotes still show in the Indonesian language
|
||||
@@ -293,19 +292,7 @@ export function fetch(
|
||||
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 tauriFetch(url, options);
|
||||
}
|
||||
return window.fetch(url, options);
|
||||
}
|
||||
@@ -316,5 +303,70 @@ export function adapter(config: Record<string, unknown>) {
|
||||
const fetchUrl = params
|
||||
? `${path}?${new URLSearchParams(params as any).toString()}`
|
||||
: path;
|
||||
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
|
||||
if (window.__TAURI__) {
|
||||
return tauriFetch(fetchUrl as string, rest)
|
||||
.then((res) => res.text())
|
||||
.then((data: any) => ({ data }));
|
||||
}
|
||||
return window.fetch(fetchUrl as string, rest);
|
||||
}
|
||||
|
||||
export function safeLocalStorage(): {
|
||||
getItem: (key: string) => string | null;
|
||||
setItem: (key: string, value: string) => void;
|
||||
removeItem: (key: string) => void;
|
||||
clear: () => void;
|
||||
} {
|
||||
let storage: Storage | null;
|
||||
|
||||
try {
|
||||
if (typeof window !== "undefined" && window.localStorage) {
|
||||
storage = window.localStorage;
|
||||
} else {
|
||||
storage = null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("localStorage is not available:", e);
|
||||
storage = null;
|
||||
}
|
||||
|
||||
return {
|
||||
getItem(key: string): string | null {
|
||||
if (storage) {
|
||||
return storage.getItem(key);
|
||||
} else {
|
||||
console.warn(
|
||||
`Attempted to get item "${key}" from localStorage, but localStorage is not available.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
setItem(key: string, value: string): void {
|
||||
if (storage) {
|
||||
storage.setItem(key, value);
|
||||
} else {
|
||||
console.warn(
|
||||
`Attempted to set item "${key}" in localStorage, but localStorage is not available.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
removeItem(key: string): void {
|
||||
if (storage) {
|
||||
storage.removeItem(key);
|
||||
} else {
|
||||
console.warn(
|
||||
`Attempted to remove item "${key}" from localStorage, but localStorage is not available.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
clear(): void {
|
||||
if (storage) {
|
||||
storage.clear();
|
||||
} else {
|
||||
console.warn(
|
||||
"Attempted to clear localStorage, but localStorage is not available.",
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
45
app/utils/audio.ts
Normal file
45
app/utils/audio.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
type TTSPlayer = {
|
||||
init: () => void;
|
||||
play: (audioBuffer: ArrayBuffer, onended: () => void | null) => Promise<void>;
|
||||
stop: () => void;
|
||||
};
|
||||
|
||||
export function createTTSPlayer(): TTSPlayer {
|
||||
let audioContext: AudioContext | null = null;
|
||||
let audioBufferSourceNode: AudioBufferSourceNode | null = null;
|
||||
|
||||
const init = () => {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
audioContext.suspend();
|
||||
};
|
||||
|
||||
const play = async (audioBuffer: ArrayBuffer, onended: () => void | null) => {
|
||||
if (audioBufferSourceNode) {
|
||||
audioBufferSourceNode.stop();
|
||||
audioBufferSourceNode.disconnect();
|
||||
}
|
||||
|
||||
const buffer = await audioContext!.decodeAudioData(audioBuffer);
|
||||
audioBufferSourceNode = audioContext!.createBufferSource();
|
||||
audioBufferSourceNode.buffer = buffer;
|
||||
audioBufferSourceNode.connect(audioContext!.destination);
|
||||
audioContext!.resume().then(() => {
|
||||
audioBufferSourceNode!.start();
|
||||
});
|
||||
audioBufferSourceNode.onended = onended;
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (audioBufferSourceNode) {
|
||||
audioBufferSourceNode.stop();
|
||||
audioBufferSourceNode.disconnect();
|
||||
audioBufferSourceNode = null;
|
||||
}
|
||||
if (audioContext) {
|
||||
audioContext.close();
|
||||
audioContext = null;
|
||||
}
|
||||
};
|
||||
|
||||
return { init, play, stop };
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "./format";
|
||||
import { fetch } from "@/app/utils";
|
||||
|
||||
export function compressImage(file: Blob, maxSize: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -287,6 +288,7 @@ export function stream(
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any, // using tauriFetch or window.fetch
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { ApiPath, DEFAULT_API_HOST } from "../constant";
|
||||
|
||||
export function corsPath(path: string) {
|
||||
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : "";
|
||||
|
||||
if (baseUrl === "" && path === "") {
|
||||
return "";
|
||||
}
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
if (!path.endsWith("/")) {
|
||||
path += "/";
|
||||
}
|
||||
|
||||
return `${baseUrl}${path}`;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { StateStorage } from "zustand/middleware";
|
||||
import { get, set, del, clear } from "idb-keyval";
|
||||
import { safeLocalStorage } from "@/app/utils";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
class IndexedDBStorage implements StateStorage {
|
||||
public async getItem(name: string): Promise<string | null> {
|
||||
|
||||
391
app/utils/ms_edge_tts.ts
Normal file
391
app/utils/ms_edge_tts.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
// import axios from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import { randomBytes } from "crypto";
|
||||
import { Readable } from "stream";
|
||||
|
||||
// Modified according to https://github.com/Migushthe2nd/MsEdgeTTS
|
||||
|
||||
/**
|
||||
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,volume,-Indicates%20the%20volume
|
||||
*/
|
||||
export enum VOLUME {
|
||||
SILENT = "silent",
|
||||
X_SOFT = "x-soft",
|
||||
SOFT = "soft",
|
||||
MEDIUM = "medium",
|
||||
LOUD = "loud",
|
||||
X_LOUD = "x-LOUD",
|
||||
DEFAULT = "default",
|
||||
}
|
||||
|
||||
/**
|
||||
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,rate,-Indicates%20the%20speaking
|
||||
*/
|
||||
export enum RATE {
|
||||
X_SLOW = "x-slow",
|
||||
SLOW = "slow",
|
||||
MEDIUM = "medium",
|
||||
FAST = "fast",
|
||||
X_FAST = "x-fast",
|
||||
DEFAULT = "default",
|
||||
}
|
||||
|
||||
/**
|
||||
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,pitch,-Indicates%20the%20baseline
|
||||
*/
|
||||
export enum PITCH {
|
||||
X_LOW = "x-low",
|
||||
LOW = "low",
|
||||
MEDIUM = "medium",
|
||||
HIGH = "high",
|
||||
X_HIGH = "x-high",
|
||||
DEFAULT = "default",
|
||||
}
|
||||
|
||||
/**
|
||||
* Only a few of the [possible formats](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/rest-text-to-speech#audio-outputs) are accepted.
|
||||
*/
|
||||
export enum OUTPUT_FORMAT {
|
||||
// Streaming =============================
|
||||
// AMR_WB_16000HZ = "amr-wb-16000hz",
|
||||
// AUDIO_16KHZ_16BIT_32KBPS_MONO_OPUS = "audio-16khz-16bit-32kbps-mono-opus",
|
||||
// AUDIO_16KHZ_32KBITRATE_MONO_MP3 = "audio-16khz-32kbitrate-mono-mp3",
|
||||
// AUDIO_16KHZ_64KBITRATE_MONO_MP3 = "audio-16khz-64kbitrate-mono-mp3",
|
||||
// AUDIO_16KHZ_128KBITRATE_MONO_MP3 = "audio-16khz-128kbitrate-mono-mp3",
|
||||
// AUDIO_24KHZ_16BIT_24KBPS_MONO_OPUS = "audio-24khz-16bit-24kbps-mono-opus",
|
||||
// AUDIO_24KHZ_16BIT_48KBPS_MONO_OPUS = "audio-24khz-16bit-48kbps-mono-opus",
|
||||
AUDIO_24KHZ_48KBITRATE_MONO_MP3 = "audio-24khz-48kbitrate-mono-mp3",
|
||||
AUDIO_24KHZ_96KBITRATE_MONO_MP3 = "audio-24khz-96kbitrate-mono-mp3",
|
||||
// AUDIO_24KHZ_160KBITRATE_MONO_MP3 = "audio-24khz-160kbitrate-mono-mp3",
|
||||
// AUDIO_48KHZ_96KBITRATE_MONO_MP3 = "audio-48khz-96kbitrate-mono-mp3",
|
||||
// AUDIO_48KHZ_192KBITRATE_MONO_MP3 = "audio-48khz-192kbitrate-mono-mp3",
|
||||
// OGG_16KHZ_16BIT_MONO_OPUS = "ogg-16khz-16bit-mono-opus",
|
||||
// OGG_24KHZ_16BIT_MONO_OPUS = "ogg-24khz-16bit-mono-opus",
|
||||
// OGG_48KHZ_16BIT_MONO_OPUS = "ogg-48khz-16bit-mono-opus",
|
||||
// RAW_8KHZ_8BIT_MONO_ALAW = "raw-8khz-8bit-mono-alaw",
|
||||
// RAW_8KHZ_8BIT_MONO_MULAW = "raw-8khz-8bit-mono-mulaw",
|
||||
// RAW_8KHZ_16BIT_MONO_PCM = "raw-8khz-16bit-mono-pcm",
|
||||
// RAW_16KHZ_16BIT_MONO_PCM = "raw-16khz-16bit-mono-pcm",
|
||||
// RAW_16KHZ_16BIT_MONO_TRUESILK = "raw-16khz-16bit-mono-truesilk",
|
||||
// RAW_22050HZ_16BIT_MONO_PCM = "raw-22050hz-16bit-mono-pcm",
|
||||
// RAW_24KHZ_16BIT_MONO_PCM = "raw-24khz-16bit-mono-pcm",
|
||||
// RAW_24KHZ_16BIT_MONO_TRUESILK = "raw-24khz-16bit-mono-truesilk",
|
||||
// RAW_44100HZ_16BIT_MONO_PCM = "raw-44100hz-16bit-mono-pcm",
|
||||
// RAW_48KHZ_16BIT_MONO_PCM = "raw-48khz-16bit-mono-pcm",
|
||||
// WEBM_16KHZ_16BIT_MONO_OPUS = "webm-16khz-16bit-mono-opus",
|
||||
// WEBM_24KHZ_16BIT_24KBPS_MONO_OPUS = "webm-24khz-16bit-24kbps-mono-opus",
|
||||
WEBM_24KHZ_16BIT_MONO_OPUS = "webm-24khz-16bit-mono-opus",
|
||||
// Non-streaming =============================
|
||||
// RIFF_8KHZ_8BIT_MONO_ALAW = "riff-8khz-8bit-mono-alaw",
|
||||
// RIFF_8KHZ_8BIT_MONO_MULAW = "riff-8khz-8bit-mono-mulaw",
|
||||
// RIFF_8KHZ_16BIT_MONO_PCM = "riff-8khz-16bit-mono-pcm",
|
||||
// RIFF_22050HZ_16BIT_MONO_PCM = "riff-22050hz-16bit-mono-pcm",
|
||||
// RIFF_24KHZ_16BIT_MONO_PCM = "riff-24khz-16bit-mono-pcm",
|
||||
// RIFF_44100HZ_16BIT_MONO_PCM = "riff-44100hz-16bit-mono-pcm",
|
||||
// RIFF_48KHZ_16BIT_MONO_PCM = "riff-48khz-16bit-mono-pcm",
|
||||
}
|
||||
|
||||
export type Voice = {
|
||||
Name: string;
|
||||
ShortName: string;
|
||||
Gender: string;
|
||||
Locale: string;
|
||||
SuggestedCodec: string;
|
||||
FriendlyName: string;
|
||||
Status: string;
|
||||
};
|
||||
|
||||
export class ProsodyOptions {
|
||||
/**
|
||||
* The pitch to use.
|
||||
* Can be any {@link PITCH}, or a relative frequency in Hz (+50Hz), a relative semitone (+2st), or a relative percentage (+50%).
|
||||
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,pitch,-Indicates%20the%20baseline)
|
||||
*/
|
||||
pitch?: PITCH | string = "+0Hz";
|
||||
/**
|
||||
* The rate to use.
|
||||
* Can be any {@link RATE}, or a relative number (0.5), or string with a relative percentage (+50%).
|
||||
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,rate,-Indicates%20the%20speaking)
|
||||
*/
|
||||
rate?: RATE | string | number = 1.0;
|
||||
/**
|
||||
* The volume to use.
|
||||
* Can be any {@link VOLUME}, or an absolute number (0, 100), a string with a relative number (+50), or a relative percentage (+50%).
|
||||
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,volume,-Indicates%20the%20volume)
|
||||
*/
|
||||
volume?: VOLUME | string | number = 100.0;
|
||||
}
|
||||
|
||||
export class MsEdgeTTS {
|
||||
static OUTPUT_FORMAT = OUTPUT_FORMAT;
|
||||
private static TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
|
||||
private static VOICES_URL = `https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=${MsEdgeTTS.TRUSTED_CLIENT_TOKEN}`;
|
||||
private static SYNTH_URL = `wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1?TrustedClientToken=${MsEdgeTTS.TRUSTED_CLIENT_TOKEN}`;
|
||||
private static BINARY_DELIM = "Path:audio\r\n";
|
||||
private static VOICE_LANG_REGEX = /\w{2}-\w{2}/;
|
||||
private readonly _enableLogger;
|
||||
private _ws: WebSocket | undefined;
|
||||
private _voice: any;
|
||||
private _voiceLocale: any;
|
||||
private _outputFormat: any;
|
||||
private _streams: { [key: string]: Readable } = {};
|
||||
private _startTime = 0;
|
||||
|
||||
private _log(...o: any[]) {
|
||||
if (this._enableLogger) {
|
||||
console.log(...o);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new `MsEdgeTTS` instance.
|
||||
*
|
||||
* @param agent (optional, **NOT SUPPORTED IN BROWSER**) Use a custom http.Agent implementation like [https-proxy-agent](https://github.com/TooTallNate/proxy-agents) or [socks-proxy-agent](https://github.com/TooTallNate/proxy-agents/tree/main/packages/socks-proxy-agent).
|
||||
* @param enableLogger=false whether to enable the built-in logger. This logs connections inits, disconnects, and incoming data to the console
|
||||
*/
|
||||
public constructor(enableLogger: boolean = false) {
|
||||
this._enableLogger = enableLogger;
|
||||
}
|
||||
|
||||
private async _send(message: any) {
|
||||
for (let i = 1; i <= 3 && this._ws!.readyState !== this._ws!.OPEN; i++) {
|
||||
if (i == 1) {
|
||||
this._startTime = Date.now();
|
||||
}
|
||||
this._log("connecting: ", i);
|
||||
await this._initClient();
|
||||
}
|
||||
this._ws!.send(message);
|
||||
}
|
||||
|
||||
private _initClient() {
|
||||
this._ws = new WebSocket(MsEdgeTTS.SYNTH_URL);
|
||||
|
||||
this._ws.binaryType = "arraybuffer";
|
||||
return new Promise((resolve, reject) => {
|
||||
this._ws!.onopen = () => {
|
||||
this._log(
|
||||
"Connected in",
|
||||
(Date.now() - this._startTime) / 1000,
|
||||
"seconds",
|
||||
);
|
||||
this._send(
|
||||
`Content-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n
|
||||
{
|
||||
"context": {
|
||||
"synthesis": {
|
||||
"audio": {
|
||||
"metadataoptions": {
|
||||
"sentenceBoundaryEnabled": "false",
|
||||
"wordBoundaryEnabled": "false"
|
||||
},
|
||||
"outputFormat": "${this._outputFormat}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
).then(resolve);
|
||||
};
|
||||
this._ws!.onmessage = (m: any) => {
|
||||
const buffer = Buffer.from(m.data as ArrayBuffer);
|
||||
const message = buffer.toString();
|
||||
const requestId = /X-RequestId:(.*?)\r\n/gm.exec(message)![1];
|
||||
if (message.includes("Path:turn.start")) {
|
||||
// start of turn, ignore
|
||||
} else if (message.includes("Path:turn.end")) {
|
||||
// end of turn, close stream
|
||||
this._streams[requestId].push(null);
|
||||
} else if (message.includes("Path:response")) {
|
||||
// context response, ignore
|
||||
} else if (
|
||||
message.includes("Path:audio") &&
|
||||
m.data instanceof ArrayBuffer
|
||||
) {
|
||||
this._pushAudioData(buffer, requestId);
|
||||
} else {
|
||||
this._log("UNKNOWN MESSAGE", message);
|
||||
}
|
||||
};
|
||||
this._ws!.onclose = () => {
|
||||
this._log(
|
||||
"disconnected after:",
|
||||
(Date.now() - this._startTime) / 1000,
|
||||
"seconds",
|
||||
);
|
||||
for (const requestId in this._streams) {
|
||||
this._streams[requestId].push(null);
|
||||
}
|
||||
};
|
||||
this._ws!.onerror = function (error: any) {
|
||||
reject("Connect Error: " + error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _pushAudioData(audioBuffer: Buffer, requestId: string) {
|
||||
const audioStartIndex =
|
||||
audioBuffer.indexOf(MsEdgeTTS.BINARY_DELIM) +
|
||||
MsEdgeTTS.BINARY_DELIM.length;
|
||||
const audioData = audioBuffer.subarray(audioStartIndex);
|
||||
this._streams[requestId].push(audioData);
|
||||
this._log("received audio chunk, size: ", audioData?.length);
|
||||
}
|
||||
|
||||
private _SSMLTemplate(input: string, options: ProsodyOptions = {}): string {
|
||||
// in case future updates to the edge API block these elements, we'll be concatenating strings.
|
||||
options = { ...new ProsodyOptions(), ...options };
|
||||
return `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${this._voiceLocale}">
|
||||
<voice name="${this._voice}">
|
||||
<prosody pitch="${options.pitch}" rate="${options.rate}" volume="${options.volume}">
|
||||
${input}
|
||||
</prosody>
|
||||
</voice>
|
||||
</speak>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of voices available in Microsoft Edge.
|
||||
* These, however, are not all. The complete list of voices supported by this module [can be found here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support) (neural, standard, and preview).
|
||||
*/
|
||||
// getVoices(): Promise<Voice[]> {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// axios
|
||||
// .get(MsEdgeTTS.VOICES_URL)
|
||||
// .then((res) => resolve(res.data))
|
||||
// .catch(reject);
|
||||
// });
|
||||
// }
|
||||
getVoices(): Promise<Voice[]> {
|
||||
return fetch(MsEdgeTTS.VOICES_URL)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => data as Voice[])
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the required information for the speech to be synthesised and inits a new WebSocket connection.
|
||||
* Must be called at least once before text can be synthesised.
|
||||
* Saved in this instance. Can be called at any time times to update the metadata.
|
||||
*
|
||||
* @param voiceName a string with any `ShortName`. A list of all available neural voices can be found [here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#neural-voices). However, it is not limited to neural voices: standard voices can also be used. A list of standard voices can be found [here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#standard-voices)
|
||||
* @param outputFormat any {@link OUTPUT_FORMAT}
|
||||
* @param voiceLocale (optional) any voice locale that is supported by the voice. See the list of all voices for compatibility. If not provided, the locale will be inferred from the `voiceName`
|
||||
*/
|
||||
async setMetadata(
|
||||
voiceName: string,
|
||||
outputFormat: OUTPUT_FORMAT,
|
||||
voiceLocale?: string,
|
||||
) {
|
||||
const oldVoice = this._voice;
|
||||
const oldVoiceLocale = this._voiceLocale;
|
||||
const oldOutputFormat = this._outputFormat;
|
||||
|
||||
this._voice = voiceName;
|
||||
this._voiceLocale = voiceLocale;
|
||||
if (!this._voiceLocale) {
|
||||
const voiceLangMatch = MsEdgeTTS.VOICE_LANG_REGEX.exec(this._voice);
|
||||
if (!voiceLangMatch)
|
||||
throw new Error("Could not infer voiceLocale from voiceName!");
|
||||
this._voiceLocale = voiceLangMatch[0];
|
||||
}
|
||||
this._outputFormat = outputFormat;
|
||||
|
||||
const changed =
|
||||
oldVoice !== this._voice ||
|
||||
oldVoiceLocale !== this._voiceLocale ||
|
||||
oldOutputFormat !== this._outputFormat;
|
||||
|
||||
// create new client
|
||||
if (changed || this._ws!.readyState !== this._ws!.OPEN) {
|
||||
this._startTime = Date.now();
|
||||
await this._initClient();
|
||||
}
|
||||
}
|
||||
|
||||
private _metadataCheck() {
|
||||
if (!this._ws)
|
||||
throw new Error(
|
||||
"Speech synthesis not configured yet. Run setMetadata before calling toStream or toFile.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the WebSocket connection.
|
||||
*/
|
||||
close() {
|
||||
this._ws!.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes raw audio synthesised from text in real-time to a {@link Readable}. Uses a basic {@link _SSMLTemplate SML template}.
|
||||
*
|
||||
* @param input the text to synthesise. Can include SSML elements.
|
||||
* @param options (optional) {@link ProsodyOptions}
|
||||
* @returns {Readable} - a `stream.Readable` with the audio data
|
||||
*/
|
||||
toStream(input: string, options?: ProsodyOptions): Readable {
|
||||
const { stream } = this._rawSSMLRequest(this._SSMLTemplate(input, options));
|
||||
return stream;
|
||||
}
|
||||
|
||||
toArrayBuffer(input: string, options?: ProsodyOptions): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data: Uint8Array[] = [];
|
||||
const readable = this.toStream(input, options);
|
||||
readable.on("data", (chunk) => {
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
readable.on("end", () => {
|
||||
resolve(Buffer.concat(data).buffer);
|
||||
});
|
||||
|
||||
readable.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes raw audio synthesised from a request in real-time to a {@link Readable}. Has no SSML template. Basic SSML should be provided in the request.
|
||||
*
|
||||
* @param requestSSML the SSML to send. SSML elements required in order to work.
|
||||
* @returns {Readable} - a `stream.Readable` with the audio data
|
||||
*/
|
||||
rawToStream(requestSSML: string): Readable {
|
||||
const { stream } = this._rawSSMLRequest(requestSSML);
|
||||
return stream;
|
||||
}
|
||||
|
||||
private _rawSSMLRequest(requestSSML: string): {
|
||||
stream: Readable;
|
||||
requestId: string;
|
||||
} {
|
||||
this._metadataCheck();
|
||||
|
||||
const requestId = randomBytes(16).toString("hex");
|
||||
const request =
|
||||
`X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nPath:ssml\r\n\r\n
|
||||
` + requestSSML.trim();
|
||||
// https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/speech-synthesis-markup
|
||||
const self = this;
|
||||
const stream = new Readable({
|
||||
read() {},
|
||||
destroy(error: Error | null, callback: (error: Error | null) => void) {
|
||||
delete self._streams[requestId];
|
||||
callback(error);
|
||||
},
|
||||
});
|
||||
this._streams[requestId] = stream;
|
||||
this._send(request).then();
|
||||
return { stream, requestId };
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@
|
||||
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||
"app:build": "yarn mask && yarn tauri build",
|
||||
"ios:init": "yarn tauri ios init",
|
||||
"ios:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri ios dev\"",
|
||||
"ios:build": "yarn mask && yarn tauri ios build",
|
||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||
"prepare": "husky install",
|
||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
||||
@@ -22,6 +25,7 @@
|
||||
"@hello-pangea/dnd": "^16.5.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@tauri-apps/plugin-http": "^2.0.0-rc.2",
|
||||
"@vercel/analytics": "^0.1.11",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"axios": "^1.7.5",
|
||||
@@ -31,6 +35,7 @@
|
||||
"html-to-image": "^1.11.11",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"markdown-to-txt": "^2.0.1",
|
||||
"mermaid": "^10.6.1",
|
||||
"nanoid": "^5.0.3",
|
||||
"next": "^14.1.1",
|
||||
@@ -51,8 +56,7 @@
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@tauri-apps/cli": "^2.0.0-rc.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.30",
|
||||
@@ -66,6 +70,7 @@
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^3.0.2",
|
||||
|
||||
17
public/plugins.json
Normal file
17
public/plugins.json
Normal 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"
|
||||
}
|
||||
]
|
||||
1
src-tauri/.gitignore
vendored
1
src-tauri/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/
|
||||
4064
src-tauri/Cargo.lock
generated
4064
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -2,44 +2,55 @@
|
||||
name = "nextchat"
|
||||
version = "0.1.0"
|
||||
description = "A cross platform app for LLM ChatBot."
|
||||
authors = ["Yidadaa"]
|
||||
authors = ["GPTsMotion Tech LLC"]
|
||||
license = "mit"
|
||||
repository = ""
|
||||
repository = "https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web"
|
||||
default-run = "nextchat"
|
||||
edition = "2021"
|
||||
rust-version = "1.60"
|
||||
rust-version = "1.71"
|
||||
|
||||
[lib]
|
||||
name = "nextchat"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.5.1", features = [] }
|
||||
tauri-build = { version = "2.0.0-rc", features = [] }
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.5.4", features = [ "http-all",
|
||||
"notification-all",
|
||||
"fs-all",
|
||||
"clipboard-all",
|
||||
"dialog-all",
|
||||
"shell-open",
|
||||
"updater",
|
||||
"window-close",
|
||||
"window-hide",
|
||||
"window-maximize",
|
||||
"window-minimize",
|
||||
"window-set-icon",
|
||||
"window-set-ignore-cursor-events",
|
||||
"window-set-resizable",
|
||||
"window-show",
|
||||
"window-start-dragging",
|
||||
"window-unmaximize",
|
||||
"window-unminimize",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.0.0-rc.15", features = [] }
|
||||
tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-clipboard-manager = "2.0.0-rc.4"
|
||||
tauri-plugin-dialog = "2.0.0-rc.7"
|
||||
tauri-plugin-fs = "2.0.0-rc.5"
|
||||
tauri-plugin-http = "2.0.0-rc.5"
|
||||
tauri-plugin-updater = "2.0.0-rc.3"
|
||||
tauri-plugin-notification = "2.0.0-rc.5"
|
||||
tauri-plugin-shell = "2.0.0-rc.3"
|
||||
tauri-plugin-window-state = "2.0.0-rc.5"
|
||||
|
||||
|
||||
[replace]
|
||||
# using this version from github fixed data_directory for webkitgtk
|
||||
"wry:0.43.1" = { git = "https://github.com/lloydzhou/wry", branch="webkitgtk-data_manager-directory" }
|
||||
|
||||
# Optimize for smaller binary size
|
||||
[profile.release]
|
||||
panic = "abort" # Strip expensive panic clean-up logic
|
||||
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
|
||||
lto = true # Enables link to optimizations
|
||||
opt-level = "s" # Optimize for binary size
|
||||
strip = true # Remove debug symbols
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
|
||||
|
||||
|
||||
29
src-tauri/capabilities/main.json
Normal file
29
src-tauri/capabilities/main.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"identifier": "main",
|
||||
"description": "permissions for desktop app",
|
||||
"local": true,
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-maximize",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"fs:default",
|
||||
"fs:allow-exists",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*"
|
||||
},
|
||||
{
|
||||
"url": "http://*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"notification:default",
|
||||
"shell:allow-open",
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state"
|
||||
]
|
||||
}
|
||||
24
src-tauri/src/lib.rs
Normal file
24
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.setup(|app| {
|
||||
if cfg!(debug_assertions) {
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
.build(),
|
||||
)?;
|
||||
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
nextchat::run()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user