├── . dockerignore
├── .eslintrc.json
├── app
├── icon.png
├── fonts
│ ├── GeistVF.woff
│ └── GeistMonoVF.woff
├── adapter
│ ├── yiyan
│ │ ├── logo.png
│ │ ├── login.ts
│ │ ├── models.ts
│ │ ├── utils.ts
│ │ ├── api.ts
│ │ ├── translater.tsx
│ │ ├── settings.tsx
│ │ └── yiyan.svg
│ ├── moonshot
│ │ ├── logo.png
│ │ ├── models.ts
│ │ ├── utils.ts
│ │ ├── translater.tsx
│ │ ├── settings.tsx
│ │ └── api.ts
│ ├── claude
│ │ ├── models.tsx
│ │ ├── claude_logo.svg
│ │ ├── utils.ts
│ │ ├── claude_text.svg
│ │ ├── api.ts
│ │ ├── translater.tsx
│ │ ├── anthropic.svg
│ │ └── settings.tsx
│ ├── openai
│ │ ├── models.ts
│ │ ├── utils.ts
│ │ ├── logo.svg
│ │ ├── api.ts
│ │ ├── translater.tsx
│ │ └── settings.tsx
│ └── interface.ts
├── images
│ ├── providers
│ │ ├── claude.png
│ │ ├── google.png
│ │ ├── openai.png
│ │ ├── moonshot.png
│ │ └── qianfan.jpg
│ └── logo.svg
├── unils.ts
├── globals.css
├── settings
│ └── providers
│ │ └── page.tsx
├── layout.tsx
└── page.tsx
├── postcss.config.mjs
├── next.config.mjs
├── tailwind.config.ts
├── Dockerfile
├── .gitignore
├── tsconfig.json
├── README.md
├── locales
├── zh.json
├── ja.json
└── en.json
├── package.json
├── i18n
└── request.ts
└── components
└── header.tsx
/. dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | .env
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/app/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/icon.png
--------------------------------------------------------------------------------
/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/app/adapter/yiyan/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/adapter/yiyan/logo.png
--------------------------------------------------------------------------------
/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/app/adapter/moonshot/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/adapter/moonshot/logo.png
--------------------------------------------------------------------------------
/app/images/providers/claude.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/claude.png
--------------------------------------------------------------------------------
/app/images/providers/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/google.png
--------------------------------------------------------------------------------
/app/images/providers/openai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/openai.png
--------------------------------------------------------------------------------
/app/images/providers/moonshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/moonshot.png
--------------------------------------------------------------------------------
/app/images/providers/qianfan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wuhaoworld/hive-translate/HEAD/app/images/providers/qianfan.jpg
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import createNextIntlPlugin from 'next-intl/plugin';
2 |
3 | const withNextIntl = createNextIntlPlugin();
4 | /** @type {import('next').NextConfig} */
5 | const nextConfig = {};
6 |
7 | export default withNextIntl(nextConfig);
--------------------------------------------------------------------------------
/app/unils.ts:
--------------------------------------------------------------------------------
1 | export function addIfNotExists(arr: string[], element: string) {
2 | if (!arr.includes(element)) {
3 | arr.push(element);
4 | }
5 | return arr;
6 | }
7 | export function removeIfExists(arr: string[], element: string) {
8 | const index = arr.indexOf(element);
9 | if (index !== -1) {
10 | arr.splice(index, 1);
11 | }
12 | return arr;
13 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | background: "var(--background)",
13 | foreground: "var(--foreground)",
14 | },
15 | },
16 | },
17 | plugins: [],
18 | };
19 | export default config;
20 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 使用 Node.js 官方镜像作为基础镜像
2 | FROM node:20-slim AS base
3 |
4 | # 设置工作目录
5 | WORKDIR /app
6 |
7 | # 复制 package.json 和 package-lock.json
8 | COPY package*.json ./
9 |
10 | # 安装依赖
11 | RUN npm config set registry https://registry.npmmirror.com
12 | RUN npm install
13 |
14 | # 复制所有项目文件
15 | COPY . .
16 |
17 | # 构建 Next.js 应用
18 | RUN npm run build
19 |
20 | # 设置环境变量
21 | ENV NODE_ENV production
22 |
23 | # 暴露应用运行的端口
24 | EXPOSE 3000
25 |
26 | # 启动 Next.js 应用
27 | CMD ["npm", "start"]
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/app/adapter/moonshot/models.ts:
--------------------------------------------------------------------------------
1 | import { LLMModel } from "@/app/adapter/interface"
2 | export const provider = {
3 | id: 'moonshot',
4 | providerName: 'Moonshot AI',
5 | }
6 |
7 | export const modelList: LLMModel[] = [
8 | {
9 | 'name': 'moonshot-v1-8k',
10 | 'displayName': 'Moonshot v1 8K',
11 | provider
12 | },
13 | {
14 | 'name': 'moonshot-v1-32k',
15 | 'displayName': 'Moonshot v1 32K',
16 | provider
17 | },
18 | {
19 | 'name': 'moonshot-v1-128k',
20 | 'displayName': 'Moonshot v1 128K',
21 | provider
22 | },
23 | ]
--------------------------------------------------------------------------------
/app/adapter/yiyan/login.ts:
--------------------------------------------------------------------------------
1 | 'use server'
2 | export async function login(apikey: string, secretkey: string) {
3 | const Loginurl = `https://aip.baidubce.com/oauth/2.0/token?client_id=${apikey}&client_secret=${secretkey}&grant_type=client_credentials`;
4 | const resp = await fetch(Loginurl).catch(() => {
5 | return new Response('{"error": true, "message": "input error"}', { status: 504, statusText: "input error" })
6 | }
7 | );
8 | const result = await resp.json();
9 | if (result.error) {
10 | return { error: true, message: `校验失败,API Key 或 Secret Key 错误` };
11 | } else {
12 | return result;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --background: #ffffff;
7 | --foreground: #171717;
8 | }
9 |
10 | @media (prefers-color-scheme: dark) {
11 | :root {
12 | --background: #0a0a0a;
13 | --foreground: #ededed;
14 | }
15 | }
16 |
17 | body {
18 | color: var(--foreground);
19 | background: var(--background);
20 | font-family: Arial, Helvetica, sans-serif;
21 | }
22 |
23 | @layer utilities {
24 | .text-balance {
25 | text-wrap: balance;
26 | }
27 | }
28 |
29 | .translate-result p{
30 | margin-bottom: 1em;
31 | }
32 |
33 | .translate-result :last-child{
34 | margin-bottom: 0;
35 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/app/adapter/claude/models.tsx:
--------------------------------------------------------------------------------
1 | import { LLMModel } from "@/app/adapter/interface"
2 | export const provider = {
3 | id: 'claude',
4 | providerName: 'Claude AI',
5 | }
6 |
7 | export const modelList: LLMModel[] = [
8 | {
9 | 'name': 'claude-3-5-sonnet-20240620',
10 | 'displayName': 'Claude 3.5 Sonnet',
11 | provider
12 | },
13 | {
14 | 'name': 'claude-3-sonnet-20240229',
15 | 'displayName': 'Claude 3 Sonnet',
16 | provider
17 | },
18 | {
19 | 'name': 'claude-3-opus-20240229',
20 | 'displayName': 'Claude 3 Opus',
21 | provider
22 | },
23 |
24 | {
25 | 'name': 'claude-3-haiku-20240307',
26 | 'displayName': 'Claude 3 Haiku',
27 | provider
28 | }
29 | ]
--------------------------------------------------------------------------------
/app/adapter/openai/models.ts:
--------------------------------------------------------------------------------
1 | import { LLMModel } from "@/app/adapter/interface"
2 | export const provider = {
3 | id: 'openai',
4 | providerName: 'Open AI',
5 | }
6 |
7 | export const modelList: LLMModel[] = [
8 | {
9 | 'name': 'gpt-4o',
10 | 'displayName': 'GPT 4o',
11 | provider
12 | },
13 | {
14 | 'name': 'gpt-4o-mini',
15 | 'displayName': 'GPT 4o mini',
16 | provider
17 | },
18 | {
19 | 'name': 'gpt-3.5-turbo',
20 | 'displayName': 'GPT 3.5 Turbo',
21 | provider
22 | },
23 | {
24 | 'name': 'gpt-4-turbo-preview',
25 | 'displayName': 'GPT 4 Turbo',
26 | provider
27 | },
28 | {
29 | 'name': 'gpt-4-32k',
30 | 'displayName': 'GPT 4 32k',
31 | provider
32 | },
33 |
34 | ]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 这是一个基于 Next.js 和 AI 大模型的翻译小工具,特点:
2 |
3 | * 同时调用多个大模型返回的翻译结果,方便对比翻译文本的质量
4 | * 完全客户端调用,API Key 信息存储在本地,没有泄露风险
5 | * 当前支持 Open AI、 Claude、 Moonshot、文心一言
6 |
7 | 线上预览链接:https://hive-translate.vercel.app/
8 |
9 | 
10 |
11 |
12 |
13 | ## 本地运行
14 |
15 | ### 开发预览
16 | ```bash
17 | npm run dev
18 | # or
19 | yarn dev
20 | # or
21 | pnpm dev
22 | ```
23 | 打开 [http://localhost:3000](http://localhost:3000) 即可预览。
24 |
25 | ### 本地运行
26 |
27 | ```
28 | npm run build
29 | npm run start
30 | ```
31 |
32 | ## 在 Vercel 上部署
33 | 点击下面的按钮,即可部署。
34 |
35 | [](https://vercel.com/new/clone?repository-url=https://github.com/wuhaoworld/hive-translate&project-name=hive-translate)
36 |
--------------------------------------------------------------------------------
/app/settings/providers/page.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import MoonshotSettings from "@/app/adapter/moonshot/settings";
3 | import OpenAiSettings from "@/app/adapter/openai/settings";
4 | import YiyanSettings from "@/app/adapter/yiyan/settings";
5 | import ClaudeSettings from "@/app/adapter/claude/settings";
6 | import { useTranslations } from 'next-intl';
7 |
8 | export default function Component() {
9 | const t = useTranslations('Settings');
10 | return (
11 | <>
12 |
13 |
14 |
15 | {t('modelSettings')}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/locales/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "Common": {
3 | "settings": "设置"
4 | },
5 | "HomePage": {
6 | "meta-description": "最好用的 AI 翻译工具",
7 | "aiTranslate": "AI 翻译",
8 | "paste": "粘贴",
9 | "clear": "清空",
10 | "translate": "翻译",
11 | "addProvider": "添加翻译服务",
12 | "resultPlaceholder": "翻译结果将展示在这里",
13 | "copy": "复制",
14 | "copied": "已复制",
15 | "please": "请",
16 | "config": "设置",
17 | "noProviderNotice": "尚未设置任何翻译服务,",
18 | "clickHere": "点此设置"
19 | },
20 | "Settings": {
21 | "modelSettings": "模型设置",
22 | "status": "启用",
23 | "enabled": "已启用",
24 | "disabled": "未启用",
25 | "endpoint": "中转地址",
26 | "optional": "选填",
27 | "defaultModel": "翻译时默认使用的模型",
28 | "configGuide": "查看设置引导",
29 | "save": "保存",
30 | "cancel": "取消"
31 | },
32 | "Language": {
33 | "auto": "自动识别",
34 | "english": "English",
35 | "simplifiedChinese": "中文(简体)",
36 | "traditionalChinese": "中文(繁体)",
37 | "japanese": "日文",
38 | "korean": "韩语",
39 | "french": "法语",
40 | "german": "德语",
41 | "spanish": "西班牙语"
42 | }
43 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hive-translate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@ant-design/icons": "^5.5.1",
13 | "@ant-design/nextjs-registry": "^1.0.1",
14 | "@hello-pangea/dnd": "^17.0.0",
15 | "@vercel/analytics": "^1.3.1",
16 | "antd": "^5.22.5",
17 | "next": "^14.2.20",
18 | "next-intl": "^3.26.2",
19 | "react": "^18",
20 | "react-beautiful-dnd": "^13.1.1",
21 | "react-copy-to-clipboard": "^5.1.0",
22 | "react-dom": "^18",
23 | "react-markdown": "^9.0.1",
24 | "remark-gfm": "^4.0.0"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20.16.9",
28 | "@types/react": "^18",
29 | "@types/react-beautiful-dnd": "^13.1.8",
30 | "@types/react-copy-to-clipboard": "^5.0.7",
31 | "@types/react-dom": "^18",
32 | "eslint": "^8",
33 | "eslint-config-next": "14.2.13",
34 | "postcss": "^8",
35 | "tailwindcss": "^3.4.1",
36 | "typescript": "^5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/locales/ja.json:
--------------------------------------------------------------------------------
1 | {
2 | "Common": {
3 | "settings": "設定"
4 | },
5 | "HomePage": {
6 | "meta-description": "最も使いやすいAI翻訳ツール",
7 | "aiTranslate": "AI翻訳",
8 | "paste": "貼り付け",
9 | "clear": "クリア",
10 | "translate": "翻訳",
11 | "addProvider": "翻訳サービスを追加",
12 | "resultPlaceholder": "翻訳結果はここに表示されます",
13 | "copy": "コピー",
14 | "copied": "コピーしました",
15 | "please": "お願いします",
16 | "config": "設定",
17 | "noProviderNotice": "まだ翻訳サービスが設定されていません、",
18 | "clickHere": "ここをクリックして設定"
19 | },
20 | "Settings": {
21 | "modelSettings": "モデル設定",
22 | "status": "有効にする",
23 | "enabled": "有効になりました",
24 | "disabled": "無効になりました",
25 | "endpoint": "中継アドレス",
26 | "optional": "選択入力",
27 | "defaultModel": "翻訳時にデフォルトで使用されるモデル",
28 | "configGuide": "設定ガイドを表示",
29 | "save": "保存",
30 | "cancel": "キャンセル"
31 | },
32 | "Language": {
33 | "auto": "自動認識",
34 | "english": "English",
35 | "simplifiedChinese": "中国語(簡体字)",
36 | "traditionalChinese": "中国語(繁体字)",
37 | "japanese": "日本語",
38 | "korean": "韓国語",
39 | "french": "フランス語",
40 | "german": "ドイツ語",
41 | "spanish": "スペイン語"
42 | }
43 | }
--------------------------------------------------------------------------------
/app/adapter/yiyan/models.ts:
--------------------------------------------------------------------------------
1 | import { LLMModel } from "@/app/adapter/interface"
2 |
3 | export const provider = {
4 | id: 'yiyan',
5 | providerName: '文心一言',
6 | }
7 |
8 | export const modelList: LLMModel[] = [
9 | {
10 | 'name': 'ERNIE-Speed',
11 | 'displayName': 'ERNIE-Speed',
12 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_speed',
13 | provider
14 | },
15 | {
16 | 'name': 'ERNIE-Lite',
17 | 'displayName': 'ERNIE-Lite',
18 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant',
19 | provider
20 | },
21 | {
22 | 'name': 'ERNIE-Bot 4.0',
23 | 'displayName': 'ERNIE-Bot 4.0',
24 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro',
25 | provider
26 | },
27 | {
28 | 'name': 'ERNIE-Bot-8K',
29 | 'displayName': 'ERNIE-Bot-8K',
30 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot_8k',
31 | provider
32 | },
33 | {
34 | 'name': 'ERNIE-Bot-turbo',
35 | 'displayName': 'ERNIE-Bot-turbo',
36 | 'apiUrl': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant',
37 | provider
38 | }
39 | ];
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { AntdRegistry } from '@ant-design/nextjs-registry';
2 | import type { Metadata } from "next";
3 | import { Analytics } from '@vercel/analytics/react';
4 | import { Header } from '@/components/header';
5 | import { NextIntlClientProvider } from 'next-intl';
6 | import { getLocale, getMessages } from 'next-intl/server';
7 | import "./globals.css";
8 |
9 | export const metadata: Metadata = {
10 | title: "AI Translate",
11 | description: "The best AI translation tool",
12 | icons: {
13 | icon: '/icon.png'
14 | },
15 | };
16 |
17 | export default async function RootLayout({
18 | children,
19 | }: Readonly<{
20 | children: React.ReactNode;
21 | }>) {
22 | const locale = await getLocale();
23 | const messages = await getMessages();
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {children}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "Common": {
3 | "settings": "Settings"
4 | },
5 | "HomePage": {
6 | "meta-description": "The best AI translation tool",
7 | "aiTranslate": "AI Translate",
8 | "paste": "Paste",
9 | "clear": "Clear",
10 | "translate": "Translate",
11 | "addProvider": "Add Translation Provider",
12 | "resultPlaceholder": "The translation results will be displayed here.",
13 | "copy": "Copy",
14 | "copied": "Copied",
15 | "please": "Please",
16 | "config": "Config",
17 | "noProviderNotice": "No translation provider have been set up yet, ",
18 | "clickHere": "Click here to set"
19 | },
20 | "Settings": {
21 | "modelSettings": "Model Settings",
22 | "status": "Status",
23 | "enabled": "Enabled",
24 | "disabled": "Disabled",
25 | "endpoint": "Endpoint",
26 | "optional": "Optional",
27 | "defaultModel": "Default Model",
28 | "configGuide": "Configuration Guide",
29 | "save": "Save",
30 | "cancel": "Cancel"
31 | },
32 | "Language": {
33 | "auto": "Detect Language",
34 | "english": "English",
35 | "simplifiedChinese": "Simplified Chinese",
36 | "traditionalChinese": "Traditional Chinese",
37 | "japanese": "Japanese",
38 | "korean": "Korean",
39 | "french": "French",
40 | "german": "German",
41 | "spanish": "Spanish"
42 | }
43 | }
--------------------------------------------------------------------------------
/i18n/request.ts:
--------------------------------------------------------------------------------
1 | import { getRequestConfig } from 'next-intl/server';
2 | import { headers } from 'next/headers';
3 |
4 | export default getRequestConfig(async () => {
5 |
6 | const locales = ['en', 'zh', 'ja'];
7 | const defaultLocale = 'en';
8 |
9 | const headersList = headers();
10 | // 获取 cookie 中的语言设置
11 | const cookieLanguage = headersList.get('cookie')?.split(';')
12 | .map(cookie => cookie.trim())
13 | .find(cookie => cookie.startsWith('language='))
14 | ?.split('=')[1];
15 | // 如果 cookie 中有有效的语言设置,直接使用
16 | if (cookieLanguage && locales.includes(cookieLanguage)) {
17 | return {
18 | locale: cookieLanguage,
19 | messages: (await import(`../locales/${cookieLanguage}.json`)).default
20 | };
21 | }
22 | const acceptLanguage = headersList.get('accept-language') || '';
23 | // 解析用户偏好的语言列表
24 | const userLanguages = acceptLanguage.split(',')
25 | .map(lang => {
26 | const [language, weight = 'q=1.0'] = lang.split(';');
27 | return {
28 | language: language.split('-')[0], // 只取主要语言代码
29 | weight: parseFloat(weight.split('=')[1])
30 | };
31 | })
32 | .sort((a, b) => b.weight - a.weight);
33 |
34 | // 查找第一个匹配的支持语言
35 | const matchedLocale = userLanguages.find(
36 | ({ language }) => locales.includes(language)
37 | );
38 | const locale = matchedLocale ? matchedLocale.language : defaultLocale;
39 | return {
40 | locale,
41 | messages: (await import(`../locales/${locale}.json`)).default
42 | };
43 | });
--------------------------------------------------------------------------------
/app/adapter/interface.ts:
--------------------------------------------------------------------------------
1 | export abstract class LLMApi {
2 | abstract chat(options: ChatOptions): Promise;
3 | abstract usage(): Promise;
4 | abstract models(): Promise;
5 | }
6 |
7 | export interface ChatOptions {
8 | messages: RequestMessage[];
9 | config: LLMConfig;
10 | apiUrl?: string;
11 | apiKey?: string;
12 | onUpdate: (message: string) => void;
13 | onFinish: (message: string) => void;
14 | onError?: (err: Error) => void;
15 | onController?: (controller: AbortController) => void;
16 | }
17 |
18 | // 暂时只支持文本
19 | export interface RequestMessage {
20 | role: 'user' | 'assistant' | 'system';
21 | content: string;
22 | }
23 |
24 | export interface LLMConfig {
25 | model: string;
26 | temperature?: number;
27 | top_p?: number;
28 | stream?: boolean;
29 | presence_penalty?: number;
30 | frequency_penalty?: number;
31 | }
32 |
33 | export interface LLMUsage {
34 | used: number;
35 | total: number;
36 | }
37 |
38 | export interface LLMModel {
39 | name: string;
40 | displayName: string;
41 | apiUrl?: string;
42 | // available: boolean;
43 | provider: LLMModelProvider;
44 | }
45 |
46 | // export interface LLMModel {
47 | // name: string;
48 | // available: boolean;
49 | // provider: LLMModelProvider;
50 | // }
51 |
52 | export interface LLMModelProvider {
53 | id: string;
54 | providerName: string;
55 | status?: boolean
56 | // providerType: string;
57 | }
58 |
59 | export default interface TranslaterComponent {
60 | startTranslate: (question: string, language: string, completeCallback: (result: string) => void) => void;
61 | stopTranslate: () => void;
62 | clear: () => void;
63 | }
--------------------------------------------------------------------------------
/app/adapter/claude/claude_logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/adapter/claude/utils.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ChatOptions } from '@/app/adapter/interface'
3 | import { RequestMessage } from '@/app/adapter/interface'
4 | import { CladueApi } from './api';
5 | export function translate(
6 | fromLanguage: string,
7 | toLanguage: string,
8 | text: string,
9 | model: string,
10 | onUpdateMessage: (text: string) => void,
11 | onFinish?: (text: string) => void,
12 | onError?: (text?: string) => void,
13 | ) {
14 | const apikey = localStorage.getItem('claude_api_key') || '';
15 | const apiUrl = localStorage.getItem('claude_proxy_url') || '';
16 | const bot = new CladueApi();
17 | let messages: RequestMessage[];
18 | if (fromLanguage.toLowerCase() === 'auto') {
19 | messages = [
20 | {
21 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text.
22 | Source Text: ${text}
23 | Translated Text:` }]
24 | } else {
25 | messages = [
26 | {
27 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text.
28 | Source Text: ${text}.
29 | Translated Text:` }]
30 | }
31 | const options: ChatOptions = {
32 | messages: messages,
33 | config: { model: model },
34 | apiKey: apikey,
35 | apiUrl: apiUrl,
36 | onUpdate: (message: string) => {
37 | onUpdateMessage(message)
38 | },
39 | onFinish: async (message: string) => {
40 | if (onFinish) {
41 | onFinish(message)
42 | }
43 | },
44 | onError: (err: Error) => {
45 | if (onError) { // 检查 onError 是否已定义
46 | onError(err.message)
47 | }
48 | },
49 | onController: (controller: AbortController) => {
50 | // console.log("controller", controller)
51 | }
52 | }
53 | bot.chat(options);
54 | }
55 |
--------------------------------------------------------------------------------
/app/adapter/moonshot/utils.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ChatOptions } from '@/app/adapter/interface'
3 | import { RequestMessage } from '@/app/adapter/interface'
4 | import { MoonshotApi } from './api';
5 | export function translate(
6 | fromLanguage: string,
7 | toLanguage: string,
8 | text: string,
9 | model:string,
10 | onUpdateMessage: (text:string) => void,
11 | onFinish?: (text: string) => void,
12 | onError?: (text?:string) => void,
13 | ) {
14 | const apikey = localStorage.getItem('moonshot_api_key') || '';
15 | const bot = new MoonshotApi();
16 | let messages: RequestMessage[];
17 | if (fromLanguage.toLowerCase() === 'auto') {
18 | messages = [
19 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` },
20 | {
21 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text.
22 | Source Text: ${text}
23 | Translated Text:` }]
24 | } else {
25 | messages = [
26 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` },
27 | {
28 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text.
29 | Source Text: ${text}.
30 | Translated Text:` }]
31 | }
32 |
33 | const options: ChatOptions = {
34 | messages: messages,
35 | config: { model: model },
36 | apiKey: apikey,
37 | onUpdate: (message: string) => {
38 | onUpdateMessage(message)
39 | },
40 | onFinish: async (message: string) => {
41 | if (onFinish) {
42 | onFinish(message)
43 | }
44 | },
45 | onError: (err: Error) => {
46 | if (onError) { // 检查 onError 是否已定义
47 | onError(err.message)
48 | }
49 | },
50 | onController: (controller: AbortController) => {
51 | // console.log("controller", controller)
52 | }
53 | }
54 | bot.chat(options);
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/app/adapter/openai/utils.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ChatOptions } from '@/app/adapter/interface'
3 | import { RequestMessage } from '@/app/adapter/interface'
4 | import { ChatGPTApi } from './api';
5 | export function translate(
6 | fromLanguage: string,
7 | toLanguage: string,
8 | text: string,
9 | model : string,
10 | onUpdateMessage: (text: string) => void,
11 | onFinish?: (text: string) => void,
12 | onError?: (text?: string) => void,
13 | ) {
14 | const apikey = localStorage.getItem('openai_api_key') || '';
15 | const apiUrl = localStorage.getItem('openai_proxy_url') || '';
16 | const bot = new ChatGPTApi();
17 | let messages: RequestMessage[];
18 | if (fromLanguage.toLowerCase() === 'auto') {
19 | messages = [
20 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` },
21 | {
22 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text.
23 | Source Text: ${text}
24 | Translated Text:` }]
25 | } else {
26 | messages = [
27 | { 'role': 'system', 'content': `You are a professional, authentic machine translation engine.` },
28 | {
29 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text.
30 | Source Text: ${text}.
31 | Translated Text:` }]
32 | }
33 |
34 | const options: ChatOptions = {
35 | messages: messages,
36 | config: { model: model },
37 | apiKey: apikey,
38 | apiUrl: apiUrl,
39 | onUpdate: (message: string) => {
40 | onUpdateMessage(message)
41 | },
42 | onFinish: async (message: string) => {
43 | if (onFinish) {
44 | onFinish(message)
45 | }
46 | },
47 | onError: (err: Error) => {
48 | if (onError) { // 检查 onError 是否已定义
49 | onError(err.message)
50 | }
51 | },
52 | onController: (controller: AbortController) => {
53 | // console.log("controller", controller)
54 | }
55 | }
56 | bot.chat(options);
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/app/adapter/yiyan/utils.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ChatOptions } from '@/app/adapter/interface'
3 | import { RequestMessage } from '@/app/adapter/interface'
4 | import { YiyanApi } from './api';
5 | import { modelList } from './models';
6 | export function translate(
7 | fromLanguage: string,
8 | toLanguage: string,
9 | text: string,
10 | model: string,
11 | onUpdateMessage: (text: string) => void,
12 | onFinish?: (text: string) => void,
13 | onError?: (text?: string) => void,
14 | ) {
15 | const apikey = localStorage.getItem('yiyan_api_key') || '';
16 | const apisecret = localStorage.getItem('yiyan_api_secret') || '';
17 | const bot = new YiyanApi();
18 | const modelObj = modelList.filter(item => item.name === model);
19 | let apiUrl;
20 | if (modelObj.length > 1) {
21 | apiUrl = modelObj[0]['apiUrl'];
22 | } else {
23 | apiUrl = modelList[0]['apiUrl'];
24 | }
25 |
26 | let messages: RequestMessage[];
27 | if (fromLanguage.toLowerCase() === 'auto') {
28 | messages = [
29 | {
30 | 'role': 'user', 'content': `Translate the following source text to ${toLanguage}, Output translation directly without any additional text.
31 | Source Text: ${text}
32 | Translated Text:` }]
33 | } else {
34 | messages = [
35 | {
36 | 'role': 'user', 'content': `Translate the following source text from ${fromLanguage} to ${toLanguage}, Output translation directly without any additional text.
37 | Source Text: ${text}.
38 | Translated Text:` }]
39 | }
40 |
41 | const options: ChatOptions = {
42 | messages: messages,
43 | config: { model: model },
44 | apiKey: `${apikey}-${apisecret}`,
45 | apiUrl: apiUrl,
46 | onUpdate: (message: string) => {
47 | onUpdateMessage(message)
48 | },
49 | onFinish: async (message: string) => {
50 | if (onFinish) {
51 | onFinish(message)
52 | }
53 | },
54 | onError: (err: Error) => {
55 | if (onError) { // 检查 onError 是否已定义
56 | onError(err.message)
57 | }
58 | },
59 | onController: (controller: AbortController) => {
60 | // console.log("controller", controller)
61 | }
62 | }
63 | bot.chat(options);
64 | }
65 |
66 |
--------------------------------------------------------------------------------
/app/adapter/claude/claude_text.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useEffect, useState } from 'react';
3 | import Image from 'next/image';
4 | import Link from 'next/link'
5 | import { Button, Select } from 'antd';
6 | import { useTranslations } from 'next-intl';
7 | import { SettingOutlined, TranslationOutlined } from '@ant-design/icons';
8 | import logo from '@/app/images/logo.svg';
9 | export function Header() {
10 | const t = useTranslations('Common');
11 | const [currentLang, setCurrentLang] = useState('zh');
12 |
13 | useEffect(() => {
14 | // 从 cookie 中获取语言设置
15 | const getCookie = (name: string) => {
16 | const value = `; ${document.cookie}`;
17 | const parts = value.split(`; ${name}=`);
18 | if (parts.length === 2) return parts.pop()?.split(';').shift();
19 | return undefined;
20 | };
21 |
22 | // 获取浏览器语言
23 | const getBrowserLanguage = () => {
24 | const lang = navigator.language.toLowerCase();
25 | if (lang.startsWith('zh')) return 'zh';
26 | if (lang.startsWith('ja')) return 'ja';
27 | return 'en'; // 默认返回英文
28 | };
29 |
30 | // 设置当前语言
31 | const savedLang = getCookie('language');
32 | if (savedLang && ['zh', 'en', 'ja'].includes(savedLang)) {
33 | setCurrentLang(savedLang);
34 | } else {
35 | const browserLang = getBrowserLanguage();
36 | setCurrentLang(browserLang);
37 | document.cookie = `language=${browserLang}; path=/`;
38 | }
39 | }, []);
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 | Hive Translate
48 |
49 |
50 |
51 |
52 | }
54 | value={currentLang}
55 | onChange={(value) => {
56 | document.cookie = `language=${value}; path=/`;
57 | window.location.reload();
58 | }}
59 | options={[
60 | { value: 'zh', label: '中文' },
61 | { value: 'en', label: 'English' },
62 | { value: 'ja', label: '日本語' },
63 | ]}
64 | />
65 |
66 | }>{t('settings')}
67 |
68 |
69 |
70 | )
71 | }
--------------------------------------------------------------------------------
/app/adapter/claude/api.ts:
--------------------------------------------------------------------------------
1 | import { ChatOptions, LLMApi, LLMModel, LLMUsage } from '@/app/adapter/interface'
2 | // const defaultApiUrl = "https://api.gptsapi.net/v1/messages";
3 | const defaultApiUrl = "https://api.anthropic.com/v1/messages";
4 |
5 | export class CladueApi implements LLMApi {
6 | async chat(options: ChatOptions) {
7 | let apiUrl: string = ''
8 | if (options.apiUrl !== '') {
9 | if (options.apiUrl?.endsWith('/v1/messages')) {
10 | // do nothing
11 | }
12 | if (options.apiUrl?.endsWith('/')) {
13 | apiUrl = options.apiUrl + 'v1/messages';
14 | } else {
15 | apiUrl = options.apiUrl + '/v1/messages';
16 | }
17 | } else {
18 | apiUrl = defaultApiUrl;
19 | }
20 |
21 | let answer = "";
22 | try {
23 | const resp = await fetch(apiUrl, {
24 | method: 'POST',
25 | headers: {
26 | 'Content-Type': 'application/json',
27 | 'x-api-key': `${options.apiKey}`
28 | },
29 | body: JSON.stringify({
30 | "stream": true,
31 | "anthropic-version": "2023-06-01",
32 | 'max_tokens': 2048,
33 | "model": `${options.config.model}`,
34 | "messages": options.messages,
35 | })
36 | });
37 |
38 | if (!resp.ok) {
39 | try {
40 | const result = await resp.json();
41 | if (result.error) {
42 | if (result.msg) {
43 | answer = `Error: ${result.msg}`;
44 | } else if (result.error.message) {
45 | answer = `Error: ${result.error.message}`;
46 | }
47 | } else {
48 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
49 | }
50 | } catch {
51 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
52 | }
53 |
54 | // options.onError?.(result);
55 | options.onUpdate(answer);
56 | options.onFinish(answer);
57 | return;
58 | }
59 |
60 | const reader = resp.body?.getReader();
61 | const decoder = new TextDecoder();
62 | while (true) {
63 | if (reader == null) {
64 | break;
65 | }
66 | const { done, value } = await reader.read();
67 | if (done) {
68 | break;
69 | }
70 | const chunk = decoder.decode(value);
71 |
72 | const lines = chunk.split("\n");
73 | const parsedLines = lines
74 | .map((line) => line.replace(/^data:/, "").trim())
75 | .filter((line) => line !== "")
76 | .map((line) => {
77 | try {
78 | return JSON.parse(line);
79 | } catch (error) {
80 | console.error("JSON parse error:", error, "in line:", line);
81 | return null;
82 | }
83 | })
84 | .filter((line) => line !== null)
85 | // 处理错误提示
86 | if (parsedLines[0]?.error) {
87 | answer = `Error: ${parsedLines[0].msg}`;
88 | options.onUpdate(answer);
89 | options.onFinish(answer);
90 | return;
91 | }
92 | for (const parsedLine of parsedLines) {
93 | // 修改为支持 Claude API 的处理方式
94 | if (parsedLine.type === "content_block_delta") {
95 | const { delta } = parsedLine;
96 | if (delta.type === 'text_delta') {
97 | const content = delta.text;
98 | // 更新 UI
99 | if (content) {
100 | answer += content;
101 | options.onUpdate(answer);
102 | }
103 | }
104 | } else if (parsedLine.type === 'message_stop') {
105 | options.onFinish(answer);
106 | } else {
107 | // 其他处理逻辑
108 | }
109 | }
110 | }
111 | options.onFinish(answer);
112 | } catch(error) {
113 | answer = "接口请求失败,请检查网络连接或接口地址、 Token 是否正确";
114 | options.onUpdate(answer);
115 | options.onFinish(answer);
116 | }
117 | }
118 |
119 | usage(): Promise {
120 | throw new Error('Method not implemented.');
121 | }
122 |
123 | models(): Promise {
124 | throw new Error('Method not implemented.');
125 | }
126 |
127 | }
--------------------------------------------------------------------------------
/app/adapter/yiyan/api.ts:
--------------------------------------------------------------------------------
1 | import { ChatOptions, LLMApi, LLMModel, LLMUsage } from '@/app/adapter/interface'
2 | import { login } from './login'
3 |
4 | interface AuthInfo {
5 | expires_in: number;
6 | expires_at?: number;
7 | refresh_token: string;
8 | session_key: string;
9 | access_token: string;
10 | scope: string;
11 | session_secret: string;
12 | }
13 |
14 | export class YiyanApi implements LLMApi {
15 | async chat(options: ChatOptions) {
16 | let answer = "";
17 | const access_token = await this.getToken();
18 | const apiUrl = `${options.apiUrl}?access_token=${access_token}`;
19 |
20 | try {
21 | const resp = await fetch(apiUrl, {
22 | method: 'POST',
23 | headers: {
24 | 'Content-Type': 'application/json'
25 | },
26 | body: JSON.stringify({
27 | "stream": true,
28 | "model": `${options.config.model}`,
29 | "messages": options.messages,
30 | })
31 | });
32 |
33 | if (resp.status < 200 || resp.status > 299) {
34 | const result = await resp.json();
35 | if (result.error) {
36 | if (result.msg) {
37 | answer = `Error: ${result.msg}`;
38 | } else if (result.error.message) {
39 | answer = `Error: ${result.error.message}`;
40 | }
41 | } else {
42 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
43 | }
44 | options.onUpdate(answer);
45 | options.onFinish(answer);
46 | return;
47 | }
48 |
49 | const reader = resp.body?.getReader();
50 | const decoder = new TextDecoder();
51 | while (true) {
52 | if (reader == null) {
53 | break;
54 | }
55 | const { done, value } = await reader.read();
56 | if (done) {
57 | break;
58 | }
59 | const chunk = decoder.decode(value);
60 | const lines = chunk.split("\n");
61 | const parsedLines = lines
62 | .map((line) => line.replace(/^data: /, "").trim()) // Remove the "data: " prefix
63 | .filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]"
64 | .map((line) => JSON.parse(line)); // Parse the JSON string
65 |
66 | if (parsedLines[0]?.error_code) {
67 | answer = `Error: ${parsedLines[0].error_msg}`;
68 | options.onUpdate(answer);
69 | options.onFinish(answer);
70 | return;
71 | }
72 |
73 | for (const parsedLine of parsedLines) {
74 | const { is_end, result } = parsedLine;
75 | if (result) {
76 | answer += result;
77 | options.onUpdate(answer);
78 | }
79 | if (is_end) {
80 | break;
81 | }
82 | }
83 | }
84 | options.onFinish(answer);
85 | }
86 | catch (error) {
87 | answer = "接口请求失败,请检查网络连接或接口地址、 Token 是否正确";
88 | options.onUpdate(answer);
89 | options.onFinish(answer);
90 | }
91 | }
92 |
93 | async getToken() {
94 | const yiyan_authinfo_plain = localStorage.getItem('yiyan_authinfo');
95 | const currentStamp = Math.floor(new Date().getTime() / 1000);
96 | if (yiyan_authinfo_plain) {
97 | const yiyan_authinfo: AuthInfo = JSON.parse(localStorage.getItem('yiyan_authinfo') + '');
98 | if (yiyan_authinfo.expires_at && currentStamp < yiyan_authinfo.expires_at) {
99 | return yiyan_authinfo.access_token;
100 | }
101 | }
102 | // 处理重新验证
103 | const savedApikey = localStorage.getItem('yiyan_api_key');
104 | const savedSecret = localStorage.getItem('yiyan_api_secret');
105 | if (savedApikey && savedSecret) {
106 | const result = await login(savedApikey, savedSecret);
107 | result.expires_at = currentStamp + result.expires_in;
108 | localStorage.setItem('yiyan_authinfo', JSON.stringify(result));
109 | const { access_token } = result;
110 | return access_token;
111 | } else {
112 | throw new Error('error: 请先设置 API Key 和 Secret Key');
113 | }
114 |
115 | }
116 |
117 | usage(): Promise {
118 | throw new Error('Method not implemented.');
119 | }
120 |
121 | models(): Promise {
122 | throw new Error('Method not implemented.');
123 | }
124 |
125 | }
--------------------------------------------------------------------------------
/app/adapter/openai/logo.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/adapter/openai/api.ts:
--------------------------------------------------------------------------------
1 | import { ChatOptions, LLMApi, LLMModel, LLMUsage } from '@/app/adapter/interface'
2 | // const defaultApiUrl = "https://api.nextapi.fun/openai/v1/chat/completions";
3 | const defaultApiUrl = "https://api.openai.com/v1/chat/completions";
4 |
5 | export class ChatGPTApi implements LLMApi {
6 | async chat(options: ChatOptions) {
7 | let apiUrl: string = ''
8 | if (options.apiUrl !== '') {
9 | if (options.apiUrl?.endsWith('/v1/chat/completions')) {
10 | // do nothing
11 | }
12 | if (options.apiUrl?.endsWith('/')) {
13 | apiUrl = options.apiUrl + 'v1/chat/completions';
14 | } else {
15 | apiUrl = options.apiUrl + '/v1/chat/completions';
16 | }
17 | } else {
18 | apiUrl = defaultApiUrl;
19 | }
20 | let answer = "";
21 | try {
22 | const resp = await fetch(apiUrl, {
23 | method: 'POST',
24 | headers: {
25 | 'Content-Type': 'application/json',
26 | 'Authorization': `Bearer ${options.apiKey}`
27 | },
28 | body: JSON.stringify({
29 | "stream": true,
30 | "model": `${options.config.model}`,
31 | "messages": options.messages,
32 | })
33 | });
34 |
35 | if (!resp.ok) {
36 | try {
37 | const result = await resp.json();
38 | if (result.error) {
39 | if (result.msg) {
40 | answer = `Error: ${result.msg}`;
41 | } else if (result.error.message) {
42 | answer = `Error: ${result.error.message}`;
43 | }
44 | } else {
45 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
46 | }
47 | } catch {
48 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
49 | }
50 |
51 | // options.onError?.(result);
52 | options.onUpdate(answer);
53 | options.onFinish(answer);
54 | return;
55 | }
56 |
57 | const reader = resp.body?.getReader();
58 | const decoder = new TextDecoder();
59 | while (true) {
60 | if (reader == null) {
61 | break;
62 | }
63 | const { done, value } = await reader.read();
64 | if (done) {
65 | break;
66 | }
67 | const chunk = decoder.decode(value);
68 | const lines = chunk.split("\n");
69 | const parsedLines = lines
70 | .map((line) => line.replace(/^data: /, "").trim())
71 | .filter((line) => line !== "" && line !== "[DONE]")
72 | .map((line) => {
73 | try {
74 | return JSON.parse(line);
75 | } catch (error) {
76 | console.error("JSON parse error:", error, "in line:", line);
77 | return null;
78 | }
79 | })
80 | .filter((line) => line !== null);
81 |
82 | // 处理错误提示
83 | if (parsedLines[0]?.error) {
84 | answer = `Error: ${parsedLines[0].msg}`;
85 | options.onUpdate(answer);
86 | options.onFinish(answer);
87 | return;
88 | }
89 |
90 | for (const parsedLine of parsedLines) {
91 | if (parsedLine.msg) {
92 | answer = parsedLine.msg;
93 | options.onError?.(parsedLine.msg);
94 | options.onUpdate(answer);
95 | options.onFinish(answer);
96 | return;
97 | }
98 |
99 | // 检查 choices 是否存在且非空
100 | if (parsedLine.choices && parsedLine.choices.length > 0) {
101 | const { delta } = parsedLine.choices[0];
102 | const { content } = delta;
103 | // 更新 UI
104 | if (content) {
105 | answer += content;
106 | options.onUpdate(answer);
107 | }
108 | } else {
109 | console.warn("没有可用的 choices");
110 | }
111 | }
112 | }
113 | options.onFinish(answer);
114 | } catch (error) {
115 | answer = "接口请求失败,请检查网络连接或接口地址、 Token 是否正确";
116 | options.onUpdate(answer);
117 | options.onFinish(answer);
118 | }
119 | }
120 |
121 | // async autoGenerateTitle(options: ChatOptions) {
122 |
123 | // }
124 |
125 | usage(): Promise {
126 | throw new Error('Method not implemented.');
127 | }
128 |
129 | models(): Promise {
130 | throw new Error('Method not implemented.');
131 | }
132 |
133 | }
134 |
135 | interface Options2 {
136 | apikey: string;
137 | apiUrl?: string;
138 | messages: Array<{ role: string, content: string }>;
139 | model: string;
140 | };
141 |
142 | interface CompleteCallback {
143 | (title: string): void;
144 | }
--------------------------------------------------------------------------------
/app/images/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/adapter/yiyan/translater.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useEffect, useState, useImperativeHandle } from 'react';
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import Markdown from 'react-markdown';
5 | import remarkGfm from 'remark-gfm';
6 | import { translate } from './utils';
7 | import { Button, Skeleton, Collapse, theme, Tooltip } from 'antd';
8 | import { CopyToClipboard } from 'react-copy-to-clipboard';
9 | import { CopyOutlined } from '@ant-design/icons';
10 | import yiyanLogo from '@/app/images/providers/yiyan.svg';
11 | import { modelList } from './models';
12 | import {useTranslations} from 'next-intl';
13 |
14 | const YiyanTranslater = forwardRef((props, ref) => {
15 | const t = useTranslations('HomePage');
16 | const [copyNotice, setCopyNotice] = useState(t('copy'));
17 | const [activeKey, setActiveKey] = useState('');
18 | const [translatedText, setTranslatedText] = useState('');
19 | const [resultStatus, setResultStatus] = useState('init');
20 | const [savedModel, setSavedModel] = useState(modelList[0]);
21 | const { token } = theme.useToken();
22 |
23 | useImperativeHandle(ref, () => ({
24 | startTranslate: (from: string, to: string, text: string, finishCallback: () => {}) => {
25 | const apikey = localStorage.getItem('yiyan_api_key') || '';
26 | const model = localStorage.getItem('yiyan_model') || 'ERNIE-Speed';
27 | setActiveKey('yiyan');
28 | if (apikey === '') {
29 | setResultStatus('need_api_key');
30 | finishCallback();
31 | return;
32 | }
33 | setResultStatus('loading');
34 | setTranslatedText('');
35 | translate(
36 | from,
37 | to,
38 | text,
39 | model,
40 | (message: string) => {
41 | setTranslatedText(message);
42 | setResultStatus('done');
43 | },
44 | finishCallback
45 | )
46 | }
47 | }));
48 |
49 | useEffect(() => {
50 | const saved_yiyan_model = localStorage.getItem('yiyan_model') || modelList[0]['name'];
51 | const curretnModel = modelList.filter(item => item.name === saved_yiyan_model)
52 | if(curretnModel.length>0){
53 | setSavedModel(curretnModel[0])
54 | }
55 | }, []);
56 |
57 | const panelStyle: React.CSSProperties = {
58 | background: "#fff",
59 | borderRadius: token.borderRadiusLG,
60 | border: `1px solid ${token.colorBorder}`,
61 | };
62 |
63 | const contentFooter =
64 | {
65 | setCopyNotice(t('copied'));
66 | setTimeout(() => {
67 | setCopyNotice(t('copy'));
68 | }, 2000);
69 | }}>
70 |
71 |
72 |
73 |
74 |
;
75 | function ChildrenDisplay(status: string) {
76 | switch (status) {
77 | case 'init':
78 | return <>{t('resultPlaceholder')}
{contentFooter}>;
79 | case 'need_api_key':
80 | return <>{t('please')} API Key
{contentFooter}>;
81 | case 'loading':
82 | return <>{contentFooter}>;
83 | case 'done':
84 | return <>{translatedText}
{contentFooter}>;
85 | default:
86 | return <>{t('resultPlaceholder')}
{contentFooter}>;
87 | }
88 | }
89 |
90 | const label =
91 |
92 | 百度千帆/文心一言
93 | {savedModel.displayName==='' ? '' : ({savedModel.displayName})}
94 |
;
95 |
96 | const yiyanItems = [
97 | {
98 | key: 'yiyan',
99 | label: label,
100 | children: ChildrenDisplay(resultStatus),
101 | style: panelStyle,
102 | }
103 | ];
104 |
105 | return (
106 | {
116 | if (activeKey === 'yiyan') {
117 | setActiveKey('')
118 | } else {
119 | setActiveKey('yiyan')
120 | }
121 | }}
122 | />
123 | )
124 | });
125 |
126 | YiyanTranslater.displayName = 'YiyanTranslater';
127 |
128 | export default YiyanTranslater
--------------------------------------------------------------------------------
/app/adapter/openai/translater.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useState, useEffect, useImperativeHandle } from 'react';
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import Markdown from 'react-markdown';
5 | import remarkGfm from 'remark-gfm';
6 | import openaiLogo from '@/app/images/providers/openai.png';
7 | import { translate } from './utils';
8 | import { Button, Skeleton, Collapse, theme, Tooltip } from 'antd';
9 | import { CopyToClipboard } from 'react-copy-to-clipboard';
10 | import { CopyOutlined } from '@ant-design/icons';
11 | import { modelList } from './models';
12 | import {useTranslations} from 'next-intl';
13 |
14 | const OpenaiTranslater = forwardRef((props, ref) => {
15 | const t = useTranslations('HomePage');
16 | const [copyNotice, setCopyNotice] = useState(t('copy'));
17 | const [activeKey, setActiveKey] = useState('');
18 | const [translatedText, setTranslatedText] = useState('');
19 | const [resultStatus, setResultStatus] = useState('init');
20 | const [savedModel, setSavedModel] = useState(modelList[0]);
21 | const { token } = theme.useToken();
22 |
23 | useImperativeHandle(ref, () => ({
24 | startTranslate: (from: string, to: string, text: string, finishCallback: () => {}) => {
25 | const apikey = localStorage.getItem('openai_api_key') || '';
26 | const model = localStorage.getItem('openai_model') || 'gpt-4o-mini';
27 | if (apikey === '') {
28 | setResultStatus('need_api_key');
29 | setActiveKey('openai');
30 | finishCallback();
31 | return;
32 | }
33 | setResultStatus('loading');
34 | setActiveKey('openai');
35 | setTranslatedText('');
36 | translate(
37 | from,
38 | to,
39 | text,
40 | model,
41 | (message: string) => {
42 | setTranslatedText(message);
43 | setResultStatus('done');
44 | },
45 | finishCallback
46 | )
47 | }
48 | }));
49 |
50 | useEffect(() => {
51 | const saved_openai_model = localStorage.getItem('openai_model') || modelList[0]['name'];
52 | const curretnModel = modelList.filter(item => item.name === saved_openai_model)
53 | if(curretnModel.length>0){
54 | setSavedModel(curretnModel[0])
55 | }
56 | }, []);
57 |
58 | const panelStyle: React.CSSProperties = {
59 | background: "#fff",
60 | borderRadius: token.borderRadiusLG,
61 | border: `1px solid ${token.colorBorder}`,
62 | };
63 | function ChildrenDisplay(status: string) {
64 | switch (status) {
65 | case 'init':
66 | return <>{t('resultPlaceholder')}
{contentFooter}>;
67 | case 'need_api_key':
68 | return <>{t('please')} API Key
{contentFooter}>;
69 | case 'loading':
70 | return <>{contentFooter}>;
71 | case 'done':
72 | return <>{translatedText}
{contentFooter}>;
73 | default:
74 | return <>{t('resultPlaceholder')}
{contentFooter}>;
75 | }
76 | }
77 |
78 | const contentFooter =
79 | {
80 | setCopyNotice(t('copied'));
81 | setTimeout(() => {
82 | setCopyNotice(t('copy'));
83 | }, 2000);
84 | }}>
85 |
86 |
87 |
88 |
89 |
;
90 | const label =
91 |
92 | Open AI
93 | {savedModel.displayName==='' ? '' : ({savedModel.displayName})}
94 |
;
95 | const openAiItems = [
96 | {
97 | key: 'openai',
98 | label: label,
99 | children: ChildrenDisplay(resultStatus),
100 | style: panelStyle,
101 | }
102 | ];
103 |
104 |
105 | return (
106 | {
116 | if (activeKey === 'openai') {
117 | setActiveKey('')
118 | } else {
119 | setActiveKey('openai')
120 | }
121 | }}
122 | />
123 | )
124 | });
125 |
126 | OpenaiTranslater.displayName = 'OpenaiTranslater';
127 |
128 | export default OpenaiTranslater
--------------------------------------------------------------------------------
/app/adapter/claude/translater.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useState, useEffect, useImperativeHandle } from 'react';
2 | import { LLMModel } from "@/app/adapter/interface"
3 | import Image from 'next/image';
4 | import Link from 'next/link';
5 | import Markdown from 'react-markdown';
6 | import remarkGfm from 'remark-gfm';
7 | import claudeLogo from './claude_logo.svg';
8 | import { translate } from './utils';
9 | import { Button, Skeleton, Collapse, theme, Tooltip } from 'antd';
10 | import { CopyToClipboard } from 'react-copy-to-clipboard';
11 | import { CopyOutlined } from '@ant-design/icons';
12 | import { modelList } from './models';
13 | import {useTranslations} from 'next-intl';
14 |
15 | const ClaudeTranslater = forwardRef((props, ref) => {
16 | const t = useTranslations('HomePage');
17 | const [copyNotice, setCopyNotice] = useState(t('copy'));
18 | const [activeKey, setActiveKey] = useState('');
19 | const [translatedText, setTranslatedText] = useState('');
20 | const [resultStatus, setResultStatus] = useState('init');
21 | const [savedModel, setSavedModel] = useState(modelList[0]);
22 | const { token } = theme.useToken();
23 |
24 | useImperativeHandle(ref, () => ({
25 | startTranslate: (from: string, to: string, text: string, finishCallback: () => {}) => {
26 | const apikey = localStorage.getItem('claude_api_key') || '';
27 | const model = localStorage.getItem('claude_model') || 'claude-3-5-sonnet-20240620';
28 | if (apikey === '') {
29 | setResultStatus('need_api_key');
30 | setActiveKey('claude');
31 | finishCallback();
32 | return;
33 | }
34 | setResultStatus('loading');
35 | setActiveKey('claude');
36 | setTranslatedText('');
37 | translate(
38 | from,
39 | to,
40 | text,
41 | model,
42 | (message: string) => {
43 | setTranslatedText(message);
44 | setResultStatus('done');
45 | },
46 | finishCallback
47 | )
48 | }
49 | }));
50 |
51 | useEffect(() => {
52 | const saved_clude_model = localStorage.getItem('claude_model') || modelList[0]['name'];
53 | const curretnModel = modelList.filter(item => item.name === saved_clude_model);
54 | if(curretnModel.length>0){
55 | setSavedModel(curretnModel[0])
56 | }
57 | }, []);
58 |
59 | const panelStyle: React.CSSProperties = {
60 | background: "#fff",
61 | borderRadius: token.borderRadiusLG,
62 | border: `1px solid ${token.colorBorder}`,
63 | };
64 | function ChildrenDisplay(status: string) {
65 | switch (status) {
66 | case 'init':
67 | return <>{t('resultPlaceholder')}
{contentFooter}>;
68 | case 'need_api_key':
69 | return <>{t('please')} API Key
{contentFooter}>;
70 | case 'loading':
71 | return <>{contentFooter}>;
72 | case 'done':
73 | return <>{translatedText}
{contentFooter}>;
74 | default:
75 | return <>{t('resultPlaceholder')}
{contentFooter}>;
76 | }
77 | }
78 |
79 | const contentFooter =
80 | {
81 | setCopyNotice(t('copied'));
82 | setTimeout(() => {
83 | setCopyNotice(t('copy'));
84 | }, 2000);
85 | }}>
86 |
87 |
88 |
89 |
90 |
;
91 | const label =
92 |
93 | Claude
94 | {({savedModel.displayName})}
95 |
;
96 | const claudeItems = [
97 | {
98 | key: 'claude',
99 | label: label,
100 | children: ChildrenDisplay(resultStatus),
101 | style: panelStyle,
102 | }
103 | ];
104 |
105 | return (
106 | {
116 | if (activeKey === 'claude') {
117 | setActiveKey('')
118 | } else {
119 | setActiveKey('claude')
120 | }
121 | }}
122 | />
123 | )
124 | });
125 |
126 | ClaudeTranslater.displayName = 'ClaudeTranslater';
127 |
128 | export default ClaudeTranslater
--------------------------------------------------------------------------------
/app/adapter/moonshot/translater.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef, useState, useEffect, useImperativeHandle } from 'react';
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import Markdown from 'react-markdown';
5 | import remarkGfm from 'remark-gfm';
6 | import moonshotLogo from '@/app/images/providers/moonshot.png';
7 | import { translate } from './utils';
8 | import { Button, Skeleton, Collapse, theme, Tooltip } from 'antd';
9 | import { CopyToClipboard } from 'react-copy-to-clipboard';
10 | import { CopyOutlined } from '@ant-design/icons';
11 | import { modelList } from './models';
12 | import {useTranslations} from 'next-intl';
13 |
14 | const MoonshotTranslater = forwardRef((props, ref) => {
15 | const t = useTranslations('HomePage');
16 | const [copyNotice, setCopyNotice] = useState(t('copy'));
17 | const [activeKey, setActiveKey] = useState('');
18 | const [translatedText, setTranslatedText] = useState('');
19 | const [resultStatus, setResultStatus] = useState('init');
20 | const [savedModel, setSavedModel] = useState(modelList[0]);
21 | const { token } = theme.useToken();
22 |
23 | useImperativeHandle(ref, () => ({
24 | startTranslate: (from: string, to: string, text: string, finishCallback: () => {}) => {
25 | const apikey = localStorage.getItem('moonshot_api_key') || '';
26 | const model = localStorage.getItem('moonshot_model') || 'moonshot-v1-8k';
27 | if (apikey === '') {
28 | setResultStatus('need_api_key');
29 | setActiveKey('moonshot');
30 | finishCallback();
31 | return;
32 | }
33 | setResultStatus('loading');
34 | setActiveKey('moonshot');
35 | setTranslatedText('');
36 | translate(
37 | from,
38 | to,
39 | text,
40 | model,
41 | (message: string) => {
42 | setTranslatedText(message);
43 | setResultStatus('done');
44 | },
45 | finishCallback
46 | )
47 | }
48 | }));
49 |
50 | useEffect(() => {
51 | const saved_moonshot_model = localStorage.getItem('moonshot_model') || modelList[0]['name'];
52 | const curretnModel = modelList.filter(item => item.name === saved_moonshot_model)
53 | if (curretnModel.length > 0) {
54 | setSavedModel(curretnModel[0])
55 | }
56 | }, []);
57 |
58 | const panelStyle: React.CSSProperties = {
59 | background: "#fff",
60 | borderRadius: token.borderRadiusLG,
61 | border: `1px solid ${token.colorBorder}`,
62 | };
63 |
64 |
65 | const contentFooter =
66 | {
67 | setCopyNotice(t('copied'));
68 | setTimeout(() => {
69 | setCopyNotice(t('copy'));
70 | }, 2000);
71 | }}>
72 |
73 |
74 |
75 |
76 |
;
77 |
78 | function ChildrenDisplay(status: string) {
79 | switch (status) {
80 | case 'init':
81 | return <>{t('resultPlaceholder')}
{contentFooter}>;
82 | case 'need_api_key':
83 | return <>{t('please')} API Key
{contentFooter}>;
84 | case 'loading':
85 | return <>{contentFooter}>;
86 | case 'done':
87 | return <>{translatedText}
{contentFooter}>;
88 | default:
89 | return <>{t('resultPlaceholder')}
{contentFooter}>;
90 | }
91 | }
92 |
93 | const label =
94 |
95 | Moonshot
96 | {/* {savedModel ? '' : ({(savedModel as { displayName: string }).displayName})} */}
97 | {savedModel.displayName === '' ? '' : ({savedModel.displayName})}
98 |
99 | const moonshotItems = [
100 | {
101 | key: 'moonshot',
102 | label: label,
103 | children: ChildrenDisplay(resultStatus),
104 | style: panelStyle,
105 | }
106 | ];
107 |
108 | return (
109 | {
119 | if (activeKey === 'moonshot') {
120 | setActiveKey('')
121 | } else {
122 | setActiveKey('moonshot')
123 | }
124 | }}
125 | />
126 | )
127 | });
128 |
129 | MoonshotTranslater.displayName = 'MoonshotTranslater';
130 |
131 | export default MoonshotTranslater
--------------------------------------------------------------------------------
/app/adapter/claude/anthropic.svg:
--------------------------------------------------------------------------------
1 |
79 |
--------------------------------------------------------------------------------
/app/adapter/moonshot/settings.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import React, { useState, useEffect } from 'react';
5 | import { Modal } from 'antd';
6 | import { Button, Form, Input, Switch, Select, ConfigProvider, message } from 'antd';
7 | import logo from "./logo.png";
8 | import { modelList } from './models';
9 | import { addIfNotExists, removeIfExists } from '@/app/unils';
10 | import { useTranslations } from 'next-intl';
11 |
12 | type FormValues = {
13 | status: boolean;
14 | apikey: string;
15 | model: string;
16 | }
17 |
18 | const MoonshotSettings = () => {
19 | const c = useTranslations('Common');
20 | const t = useTranslations('Settings');
21 | const [messageApi, contextHolder] = message.useMessage();
22 | const [moonshotStatus, setMoonshotStatus] = useState(false)
23 | const [isModalOpen, setIsModalOpen] = useState(false);
24 | const [form] = Form.useForm();
25 |
26 | useEffect(() => {
27 | const saved_moonshot_status: boolean = localStorage.getItem('moonshot_status') === "true" || false;
28 | const saved_moonshot_api_key = localStorage.getItem('moonshot_api_key') || '';
29 | const saved_moonshot_model = localStorage.getItem('moonshot_model') || modelList[0]['name'];
30 | setMoonshotStatus(saved_moonshot_status);
31 | form.setFieldValue("status", saved_moonshot_status);
32 | form.setFieldValue("apikey", saved_moonshot_api_key);
33 | form.setFieldValue("model", saved_moonshot_model);
34 | }, [form]);
35 | const showModal = () => {
36 | setIsModalOpen(true);
37 | };
38 |
39 | const handleOk = () => {
40 | form.submit();
41 | };
42 |
43 | const clearLocalSetting = () => {
44 | setMoonshotStatus(false);
45 | localStorage.removeItem('moonshot_status');
46 | localStorage.removeItem('moonshot_api_key');
47 | localStorage.removeItem('moonshot_model');
48 | form.setFieldValue("status", false);
49 | form.setFieldValue("apikey", '');
50 | form.setFieldValue("model", '');
51 | messageApi.success('清除本地设置成功');
52 | setIsModalOpen(false);
53 | };
54 |
55 | const onFinish = (values: FormValues) => {
56 | setMoonshotStatus(values.status);
57 | localStorage.setItem('moonshot_status', String(values.status));
58 | localStorage.setItem('moonshot_api_key', values.apikey);
59 | localStorage.setItem('moonshot_model', values.model);
60 |
61 | const localSavedProvidersString = localStorage.getItem('localSavedProviders');
62 | let localSavedProviders = localSavedProvidersString ? JSON.parse(localSavedProvidersString) : []
63 | if (values.status) {
64 | localSavedProviders = addIfNotExists(localSavedProviders, 'moonshot')
65 | } else {
66 | localSavedProviders = removeIfExists(localSavedProviders, 'moonshot')
67 | }
68 | localStorage.setItem('localSavedProviders', JSON.stringify(localSavedProviders));
69 |
70 | setIsModalOpen(false);
71 | };
72 |
73 | const handleCancel = () => {
74 | setIsModalOpen(false);
75 | };
76 | return (
77 |
78 | {contextHolder}
79 |
80 |
81 | {moonshotStatus ?
82 | <>
83 |
84 |
{t('enabled')}>
85 | :
86 | <>
87 |
{t('disabled')}
88 | >}
89 |
90 |
91 |
99 |
100 |
104 | {t('configGuide')}
105 |
106 |
107 |
108 |
111 |
114 |
115 |
116 | }
117 | >
118 |
119 |
128 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
151 |
152 |
153 |
154 |
155 | );
156 | };
157 |
158 | export default MoonshotSettings;
159 |
--------------------------------------------------------------------------------
/app/adapter/openai/settings.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import React, { useState, useEffect } from 'react';
5 | import { Modal } from 'antd';
6 | import { Button, Form, Input, Switch, ConfigProvider, message, Select } from 'antd';
7 | import logo from "./logo.svg";
8 | import { modelList } from './models';
9 | import { addIfNotExists, removeIfExists } from '@/app/unils';
10 | import { useTranslations } from 'next-intl';
11 |
12 | type FormValues = {
13 | status: boolean;
14 | apikey: string;
15 | proxy_url: string;
16 | model: string;
17 | }
18 |
19 | const OpenAiSettings = () => {
20 | const c = useTranslations('Common');
21 | const t = useTranslations('Settings');
22 | const [messageApi, contextHolder] = message.useMessage();
23 | const [openAiStatus, setOpenAiStatus] = useState(false)
24 | const [isModalOpen, setIsModalOpen] = useState(false);
25 | const [form] = Form.useForm();
26 |
27 | useEffect(() => {
28 | const saved_openai_status: boolean = localStorage.getItem('openai_status') === "true" || false;
29 | const saved_openai_api_key = localStorage.getItem('openai_api_key') || '';
30 | const saved_openai_proxy_url = localStorage.getItem('openai_proxy_url') || '';
31 | const saved_openai_model = localStorage.getItem('openai_model') || modelList[0]['name'];
32 | setOpenAiStatus(saved_openai_status);
33 | form.setFieldValue("status", saved_openai_status);
34 | form.setFieldValue("apikey", saved_openai_api_key);
35 | form.setFieldValue("proxy_url", saved_openai_proxy_url);
36 | form.setFieldValue("model", saved_openai_model);
37 | }, [form]);
38 | const showModal = () => {
39 | setIsModalOpen(true);
40 | };
41 |
42 | const handleOk = () => {
43 | form.submit();
44 | };
45 |
46 | const clearLocalSetting = () => {
47 | setOpenAiStatus(false);
48 | localStorage.removeItem('openai_status');
49 | localStorage.removeItem('openai_api_key');
50 | localStorage.removeItem('openai_proxy_url');
51 | localStorage.removeItem('openai_model');
52 | form.setFieldValue("status", false);
53 | form.setFieldValue("apikey", '');
54 | form.setFieldValue("proxy_url", '');
55 | messageApi.success('清除本地设置成功');
56 | setIsModalOpen(false);
57 | };
58 | const onFinish = (values: FormValues) => {
59 | setOpenAiStatus(values.status);
60 | localStorage.setItem('openai_status', String(values.status));
61 | localStorage.setItem('openai_api_key', values.apikey);
62 | localStorage.setItem('openai_proxy_url', values.proxy_url);
63 | localStorage.setItem('openai_model', values.model);
64 |
65 | const localSavedProvidersString = localStorage.getItem('localSavedProviders');
66 | let localSavedProviders = localSavedProvidersString ? JSON.parse(localSavedProvidersString) : []
67 | if (values.status) {
68 | localSavedProviders = addIfNotExists(localSavedProviders, 'openai')
69 | } else {
70 | localSavedProviders = removeIfExists(localSavedProviders, 'openai')
71 | }
72 | localStorage.setItem('localSavedProviders', JSON.stringify(localSavedProviders));
73 |
74 | setIsModalOpen(false);
75 | };
76 |
77 | const handleCancel = () => {
78 | setIsModalOpen(false);
79 | };
80 | return (
81 |
82 | {contextHolder}
83 |
84 |
85 | {openAiStatus ?
86 | <>
87 |
88 |
{t('enabled')}>
89 | :
90 | <>
91 |
{t('disabled')}
92 | >}
93 |
94 |
95 |
103 |
104 |
108 | {t('configGuide')}
109 |
110 |
111 |
112 |
115 |
118 |
119 |
120 | }
121 | >
122 |
123 |
132 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
158 |
159 |
160 |
161 |
162 | );
163 | };
164 |
165 | export default OpenAiSettings;
166 |
--------------------------------------------------------------------------------
/app/adapter/yiyan/settings.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Image from 'next/image';
3 | import React, { useState, useEffect } from 'react';
4 | import { Modal } from 'antd';
5 | import Link from 'next/link';
6 | import { Button, Form, Input, Select, Switch, ConfigProvider, message } from 'antd';
7 | import logo from "./logo.png"
8 | import { modelList } from './models';
9 | import { addIfNotExists, removeIfExists } from '@/app/unils';
10 | import { useTranslations } from 'next-intl';
11 |
12 | type FormValues = {
13 | status: boolean;
14 | apikey: string;
15 | apisecret: string;
16 | model: string;
17 | }
18 |
19 | const YiyanSettings = () => {
20 | const c = useTranslations('Common');
21 | const t = useTranslations('Settings');
22 | const [messageApi, contextHolder] = message.useMessage();
23 | const [yiyanStatus, setYiyanStatus] = useState(false)
24 | const [isModalOpen, setIsModalOpen] = useState(false);
25 | const [form] = Form.useForm();
26 |
27 | useEffect(() => {
28 | const saved_yiyan_status: boolean = localStorage.getItem('yiyan_status') === "true" || false;
29 | const saved_yiyan_api_key = localStorage.getItem('yiyan_api_key') || '';
30 | const saved_yiyan_api_secret = localStorage.getItem('yiyan_api_secret') || '';
31 | const saved_yiyan_model = localStorage.getItem('yiyan_model') || modelList[0]['name'];
32 | setYiyanStatus(saved_yiyan_status);
33 | form.setFieldValue("status", saved_yiyan_status);
34 | form.setFieldValue("apikey", saved_yiyan_api_key);
35 | form.setFieldValue("apisecret", saved_yiyan_api_secret);
36 | form.setFieldValue("model", saved_yiyan_model);
37 | }, [form]);
38 | const showModal = () => {
39 | setIsModalOpen(true);
40 | };
41 |
42 | const handleOk = () => {
43 | form.submit();
44 | };
45 |
46 | const clearLocalSetting = () => {
47 | setYiyanStatus(false);
48 | localStorage.removeItem('yiyan_status');
49 | localStorage.removeItem('yiyan_api_key');
50 | localStorage.removeItem('yiyan_api_secret');
51 | localStorage.removeItem('yiyan_model');
52 | form.setFieldValue("status", false);
53 | form.setFieldValue("apikey", '');
54 | form.setFieldValue("apisecret", '');
55 | messageApi.success('清除本地设置成功');
56 | setIsModalOpen(false);
57 | };
58 | const onFinish = (values: FormValues) => {
59 | setYiyanStatus(values.status);
60 | localStorage.setItem('yiyan_status', String(values.status));
61 | localStorage.setItem('yiyan_api_key', values.apikey);
62 | localStorage.setItem('yiyan_api_secret', values.apisecret);
63 | localStorage.setItem('yiyan_model', values.model);
64 |
65 | const localSavedProvidersString = localStorage.getItem('localSavedProviders');
66 | let localSavedProviders = localSavedProvidersString ? JSON.parse(localSavedProvidersString) : []
67 | if (values.status) {
68 | localSavedProviders = addIfNotExists(localSavedProviders, 'yiyan')
69 | } else {
70 | localSavedProviders = removeIfExists(localSavedProviders, 'yiyan')
71 | }
72 | localStorage.setItem('localSavedProviders', JSON.stringify(localSavedProviders));
73 |
74 | setIsModalOpen(false);
75 | };
76 |
77 | const handleCancel = () => {
78 | setIsModalOpen(false);
79 | };
80 | return (
81 |
82 | {contextHolder}
83 |
84 |
85 | {yiyanStatus ?
86 | <>
87 |
88 |
{t('enabled')}>
89 | :
90 | <>
91 |
{t('disabled')}
92 | >}
93 |
94 |
95 |
103 |
104 |
108 | {t('configGuide')}
109 |
110 | {/* */}
116 |
117 |
118 |
121 |
124 |
125 |
126 | }
127 | >
128 |
129 |
138 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
164 |
165 |
166 |
167 |
168 | );
169 | };
170 |
171 | export default YiyanSettings;
172 |
--------------------------------------------------------------------------------
/app/adapter/claude/settings.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import Image from 'next/image';
3 | import Link from 'next/link';
4 | import React, { useState, useEffect } from 'react';
5 | import { Modal, Select } from 'antd';
6 | import { Divider } from 'antd';
7 | import { Button, Form, Input, Switch, ConfigProvider, message } from 'antd';
8 | import anthropic from "./anthropic.svg";
9 | import claudeLogo from "./claude_logo.svg";
10 | import claudeText from "./claude_text.svg";
11 | import { modelList } from './models';
12 | import { addIfNotExists, removeIfExists } from '@/app/unils';
13 | import { useTranslations } from 'next-intl';
14 |
15 | type FormValues = {
16 | status: boolean;
17 | apikey: string;
18 | proxy_url: string;
19 | model: string;
20 | }
21 |
22 | const ClaudeSettings = () => {
23 | const c = useTranslations('Common');
24 | const t = useTranslations('Settings');
25 | const [messageApi, contextHolder] = message.useMessage();
26 | const [claudeStatus, setClaudeStatus] = useState(false)
27 | const [isModalOpen, setIsModalOpen] = useState(false);
28 | const [form] = Form.useForm();
29 |
30 | useEffect(() => {
31 | const saved_claude_status: boolean = localStorage.getItem('claude_status') === "true" || false;
32 | const saved_claude_api_key = localStorage.getItem('claude_api_key') || '';
33 | const saved_claude_proxy_url = localStorage.getItem('claude_proxy_url') || '';
34 | const saved_claude_model = localStorage.getItem('claude_model') || modelList[0]['name'];
35 | setClaudeStatus(saved_claude_status);
36 | form.setFieldValue("status", saved_claude_status);
37 | form.setFieldValue("apikey", saved_claude_api_key);
38 | form.setFieldValue("proxy_url", saved_claude_proxy_url);
39 | form.setFieldValue("model", saved_claude_model);
40 | }, [form]);
41 | const showModal = () => {
42 | setIsModalOpen(true);
43 | };
44 |
45 | const handleOk = () => {
46 | form.submit();
47 | };
48 |
49 | const clearLocalSetting = () => {
50 | setClaudeStatus(false);
51 | localStorage.removeItem('claude_status');
52 | localStorage.removeItem('claude_api_key');
53 | localStorage.removeItem('claude_proxy_url');
54 | localStorage.removeItem('claude_model');
55 | form.setFieldValue("status", false);
56 | form.setFieldValue("apikey", '');
57 | form.setFieldValue("proxy_url", '');
58 | messageApi.success('清除本地设置成功');
59 | setIsModalOpen(false);
60 | };
61 | const onFinish = (values: FormValues) => {
62 | setClaudeStatus(values.status);
63 | localStorage.setItem('claude_status', String(values.status));
64 | localStorage.setItem('claude_api_key', values.apikey);
65 | localStorage.setItem('claude_proxy_url', values.proxy_url);
66 | localStorage.setItem('claude_model', values.model);
67 |
68 | const localSavedProvidersString = localStorage.getItem('localSavedProviders');
69 | let localSavedProviders = localSavedProvidersString ? JSON.parse(localSavedProvidersString) : []
70 | if (values.status) {
71 | localSavedProviders = addIfNotExists(localSavedProviders, 'claude')
72 | } else {
73 | localSavedProviders = removeIfExists(localSavedProviders, 'claude')
74 | }
75 | localStorage.setItem('localSavedProviders', JSON.stringify(localSavedProviders));
76 |
77 | setIsModalOpen(false);
78 | };
79 |
80 | const handleCancel = () => {
81 | setIsModalOpen(false);
82 | };
83 | return (
84 |
85 | {contextHolder}
86 |
92 |
93 | {claudeStatus ?
94 | <>
95 |
96 |
{t('enabled')}>
97 | :
98 | <>
99 |
{t('disabled')}
100 | >}
101 |
102 |
103 |
111 |
112 |
116 | {t('configGuide')}
117 |
118 |
119 |
120 |
123 |
126 |
127 |
128 | }
129 | >
130 |
131 |
140 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
166 |
167 |
168 |
169 |
170 | );
171 | };
172 |
173 | export default ClaudeSettings;
174 |
--------------------------------------------------------------------------------
/app/adapter/moonshot/api.ts:
--------------------------------------------------------------------------------
1 | import { ChatOptions, LLMApi, LLMModel, LLMUsage } from '@/app/adapter/interface'
2 | const url = "https://api.moonshot.cn/v1/chat/completions";
3 |
4 | export class MoonshotApi implements LLMApi {
5 | async chat(options: ChatOptions) {
6 | const apiUrl = options.apiUrl || url;
7 | let answer = "";
8 | try {
9 | const resp = await fetch(apiUrl, {
10 | method: 'POST',
11 | headers: {
12 | 'Content-Type': 'application/json',
13 | 'Authorization': `Bearer ${options.apiKey}`
14 | },
15 | body: JSON.stringify({
16 | "stream": true,
17 | "model": `${options.config.model}`,
18 | "messages": options.messages,
19 | })
20 | });
21 |
22 | if (!resp.ok) {
23 | try {
24 | const result = await resp.json();
25 | if (result.error) {
26 | if (result.msg) {
27 | answer = `Error: ${result.msg}`;
28 | } else if (result.error.message) {
29 | answer = `Error: ${result.error.message}`;
30 | }
31 | } else {
32 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
33 | }
34 | } catch {
35 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
36 | }
37 |
38 | // options.onError?.(result);
39 | options.onUpdate(answer);
40 | options.onFinish(answer);
41 | return;
42 | }
43 |
44 | const reader = resp.body?.getReader();
45 | const decoder = new TextDecoder();
46 | while (true) {
47 | if (reader == null) {
48 | break;
49 | }
50 | const { done, value } = await reader.read();
51 | if (done) {
52 | break;
53 | }
54 | const chunk = decoder.decode(value);
55 | const lines = chunk.split("\n");
56 | const parsedLines = lines
57 | .map((line) => line.replace(/^data: /, "").trim())
58 | .filter((line) => line !== "" && line !== "[DONE]")
59 | .map((line) => {
60 | try {
61 | return JSON.parse(line);
62 | } catch (error) {
63 | console.error("JSON parse error:", error, "in line:", line);
64 | return null;
65 | }
66 | })
67 | .filter((line) => line !== null);
68 |
69 | // 处理错误提示
70 | if (parsedLines[0]?.error) {
71 | answer = `Error: ${parsedLines[0].msg}`;
72 | options.onUpdate(answer);
73 | options.onFinish(answer);
74 | return;
75 | }
76 |
77 | for (const parsedLine of parsedLines) {
78 | if (parsedLine.msg) {
79 | answer = parsedLine.msg;
80 | options.onError?.(parsedLine.msg);
81 | options.onUpdate(answer);
82 | options.onFinish(answer);
83 | return;
84 | }
85 | const { choices } = parsedLine;
86 | const { delta } = choices[0];
87 | const { content } = delta;
88 | // Update the UI with the new content
89 | if (content) {
90 | answer += content;
91 | options.onUpdate(answer);
92 | }
93 | }
94 | }
95 | options.onFinish(answer);
96 | } catch (error) {
97 | answer = "接口请求失败,请检查网络连接或接口地址、 Token 是否正确";
98 | options.onUpdate(answer);
99 | options.onFinish(answer);
100 | }
101 | }
102 |
103 | // async autoGenerateTitle(options: ChatOptions) {
104 |
105 | // }
106 |
107 | usage(): Promise {
108 | throw new Error('Method not implemented.');
109 | }
110 |
111 | models(): Promise {
112 | throw new Error('Method not implemented.');
113 | }
114 |
115 | }
116 |
117 | interface Options2 {
118 | apikey: string;
119 | apiUrl?: string;
120 | messages: Array<{ role: string, content: string }>;
121 | model: string;
122 | };
123 |
124 | interface CompleteCallback {
125 | (title: string): void;
126 | }
127 | export async function getGptResponse2(options: Options2, callback: Function, completeCallback: CompleteCallback) {
128 | // console.log(options)
129 | let apiUrl = "";
130 | if (options.apiUrl) {
131 | apiUrl = options.apiUrl;
132 | } else {
133 | apiUrl = url;
134 | }
135 | const resp = await Promise.race([fetch(apiUrl, {
136 | method: 'POST',
137 | headers: {
138 | 'Content-Type': 'application/json',
139 | 'Authorization': `Bearer ${options.apikey}`
140 | },
141 | body: JSON.stringify({
142 | "stream": true,
143 | "model": `${options.model}`,
144 | "messages": options.messages
145 | })
146 | }),
147 |
148 | new Promise((resolve, reject) => {
149 | setTimeout(() => {
150 | resolve(new Response('{"error": true, "msg": "request timeout"}', { status: 504, statusText: "timeout" }));
151 | }, 10000);
152 | })
153 |
154 | ]) as Response;
155 |
156 | let answer = "";
157 |
158 | // 判断响应状态码是否失败
159 | if (resp.status < 200 || resp.status > 299) {
160 | const result = await resp.json();
161 | if (result.error) {
162 | if (result.msg) {
163 | answer = `Error: ${result.msg}`;
164 | } else if (result.error.message) {
165 | answer = `Error: ${result.error.message}`;
166 | }
167 | } else {
168 | answer = "接口响应错误,请检查接口地址和 Token 是否正确";
169 | }
170 | callback(answer);
171 | completeCallback(answer);
172 | return;
173 | }
174 |
175 | const reader = resp.body?.getReader();
176 | const decoder = new TextDecoder();
177 | while (true) {
178 | if (reader == null) {
179 | break;
180 | }
181 | const { done, value } = await reader.read();
182 | if (done) {
183 | break;
184 | }
185 | const chunk = decoder.decode(value);
186 | const lines = chunk.split("\n");
187 | // const parsedLines = lines
188 | // .map((line) => line.replace(/^data: /, "").trim()) // Remove the "data: " prefix
189 | // .filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]"
190 | // .map((line) => JSON.parse(line)); // Parse the JSON string
191 |
192 | const parsedLines = lines
193 | .map((line) => line.replace(/^data: /, "").trim())
194 | .filter((line) => line !== "" && line !== "[DONE]")
195 | .map((line) => {
196 | try {
197 | return JSON.parse(line);
198 | } catch (error) {
199 | console.error("JSON parse error:", error, "in line:", line);
200 | return null;
201 | }
202 | })
203 | .filter((line) => line !== null);
204 |
205 | // 处理错误提示
206 | if (parsedLines[0]?.error) {
207 | answer = `Error: ${parsedLines[0].msg}`;
208 | callback(answer);
209 | completeCallback(answer);
210 | return;
211 | }
212 |
213 | for (const parsedLine of parsedLines) {
214 | const { choices } = parsedLine;
215 | const { delta } = choices[0];
216 | const { content } = delta;
217 | // Update the UI with the new content
218 | if (content) {
219 | answer += content;
220 | // console.log(answer);
221 | callback(answer);
222 | }
223 | }
224 | }
225 | completeCallback(answer);
226 | };
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { Select, Input, message, Alert, Popover } from 'antd';
3 | const { TextArea } = Input;
4 | import Link from 'next/link';
5 | import Image from 'next/image';
6 | import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
7 | import { SwapOutlined, HolderOutlined, CloseOutlined } from '@ant-design/icons';
8 | import { Button } from 'antd';
9 | import { useTranslations } from 'next-intl';
10 | import React, { useState, useRef, useEffect } from 'react';
11 | import MoonshotTranslater from '@/app/adapter/moonshot/translater';
12 | import OpenaiTranslater from '@/app/adapter/openai/translater';
13 | import ClaudeTranslater from '@/app/adapter/claude/translater';
14 | import YiyanTranslater from '@/app/adapter/yiyan/translater';
15 |
16 | import openaiLogo from '@/app/images/providers/openai.png';
17 | import yiyanLogo from '@/app/images/providers/yiyan.svg';
18 | import claudeLogo from '@/app/images/providers/claude.png';
19 | import moonshotLogo from '@/app/images/providers/moonshot.png';
20 |
21 | export default function Home() {
22 | const t = useTranslations('HomePage');
23 | const l = useTranslations('Language');
24 | const [messageApi, contextHolder] = message.useMessage();
25 | const [showNotice, setShowNotice] = useState(false);
26 | const [inputText, setInputText] = useState('');
27 | const [selectedFromLanguage, setSelectedFromLanguage] = useState('Auto');
28 | const [selectedToLanguage, setSelectedToLanguage] = useState('Simplified Chinese');
29 |
30 | const [isLoading, setIsLoading] = useState(false);
31 | const openaiRef = useRef();
32 | const claudeRef = useRef();
33 | const moonshotRef = useRef();
34 | const yiyanRef = useRef();
35 | const handleInputTextChange = (value: string) => {
36 | setInputText(value)
37 | };
38 |
39 | const clearInput = () => {
40 | setInputText('');
41 | }
42 |
43 | const handlePaste = async () => {
44 | try {
45 | const text = await navigator.clipboard.readText(); // 读取剪贴板内容
46 | setInputText(text); // 将内容设置到输入框
47 | } catch (err) {
48 | messageApi.error('读取剪贴板失败,请手动粘贴');
49 | }
50 | };
51 | const startTranslate = () => {
52 | if (!inputText) {
53 | return;
54 | }
55 | setIsLoading(true);
56 | if (openaiRef.current) {
57 | (openaiRef.current as any).startTranslate(selectedFromLanguage, selectedToLanguage, inputText, () => { setIsLoading(false); });
58 | }
59 | if (claudeRef.current) {
60 | (claudeRef.current as any).startTranslate(selectedFromLanguage, selectedToLanguage, inputText, () => { setIsLoading(false); });
61 | }
62 | if (moonshotRef.current) {
63 | (moonshotRef.current as any).startTranslate(selectedFromLanguage, selectedToLanguage, inputText, () => { setIsLoading(false); });
64 | }
65 | if (yiyanRef.current) {
66 | (yiyanRef.current as any).startTranslate(selectedFromLanguage, selectedToLanguage, inputText, () => { setIsLoading(false); });
67 | }
68 | }
69 |
70 |
71 | const handleFromLanguage = (value: React.SetStateAction) => {
72 | setSelectedFromLanguage(value)
73 | }
74 |
75 | const handleToLanguage = (value: React.SetStateAction) => {
76 | setSelectedToLanguage(value)
77 | }
78 |
79 | const [providers, setProviders] = useState([
80 | { id: 'openai', DisplayName: 'Open AI', logo: openaiLogo, provider: OpenaiTranslater, ref: openaiRef },
81 | { id: 'claude', DisplayName: 'Claude', logo: claudeLogo, provider: ClaudeTranslater, ref: claudeRef },
82 | { id: 'moonshot', DisplayName: 'Moonshot', logo: moonshotLogo, provider: MoonshotTranslater, ref: moonshotRef },
83 | { id: 'yiyan', DisplayName: '百度千帆/文心一言', logo: yiyanLogo, provider: YiyanTranslater, ref: yiyanRef },
84 | ]);
85 |
86 | const [localProviders, setLocalProviders] = useState([]);
87 | const [toAddProviders, setToAddProviders] = useState(providers);
88 |
89 | useEffect(() => {
90 | const localSavedProvidersString = localStorage.getItem('localSavedProviders');
91 | let localSavedProviders: string[];
92 | try {
93 | localSavedProviders = JSON.parse(localSavedProvidersString || '[]');
94 | } catch (e) {
95 | localSavedProviders = [];
96 | }
97 | if (localSavedProviders.length > 0) {
98 | const filteredArr = localSavedProviders
99 | .map((item: any) => providers.find(provider => provider.id === item))
100 | .filter(item => item !== undefined); // 过滤掉 undefined
101 | setLocalProviders(filteredArr);
102 | }
103 | }, [providers]);
104 |
105 | useEffect(() => {
106 | const toAddProvidersArr = providers.filter(item => !localProviders.some(local => local.id === item.id));
107 | setToAddProviders(toAddProvidersArr);
108 | }, [localProviders, providers]);
109 |
110 | useEffect(() => {
111 | if (
112 | localStorage.getItem('claude_status') === 'true' ||
113 | localStorage.getItem('moonshot_status') === 'true' ||
114 | localStorage.getItem('openai_status') === 'true' ||
115 | localStorage.getItem('yiyan_status') === 'true') {
116 | setShowNotice(false);
117 | } else {
118 | setLocalProviders(providers);
119 | setShowNotice(true);
120 | }
121 | }, [providers]);
122 |
123 | const onDragEnd = (result: any) => {
124 | if (!result.destination) {
125 | return;
126 | }
127 | const newItems = Array.from(localProviders);
128 | const [reorderedItem] = newItems.splice(result.source.index, 1);
129 | newItems.splice(result.destination.index, 0, reorderedItem);
130 | const localSavedProviders = newItems.map((i) => {
131 | return i.id;
132 | });
133 | localStorage.setItem('localSavedProviders', JSON.stringify(localSavedProviders));
134 | setLocalProviders(newItems);
135 | };
136 |
137 | const swapLanguage = () => {
138 | if (selectedFromLanguage === 'Auto' && selectedToLanguage === 'Simplified Chinese') {
139 | setSelectedFromLanguage('Simplified Chinese');
140 | setSelectedToLanguage('English');
141 | return;
142 | } else if (selectedFromLanguage === 'Auto') {
143 | setSelectedFromLanguage(selectedToLanguage);
144 | setSelectedToLanguage('Simplified Chinese');
145 | } else {
146 | const temp = selectedFromLanguage;
147 | setSelectedFromLanguage(selectedToLanguage);
148 | setSelectedToLanguage(temp);
149 | }
150 | }
151 |
152 | const HideProvider = (providerId: string) => {
153 | const filteredArr = localProviders.filter(item => item.id !== providerId);
154 | setLocalProviders(filteredArr);
155 | const localSavedProvidersString = filteredArr.map((i) => {
156 | return i.id;
157 | });
158 | localStorage.setItem('localSavedProviders', JSON.stringify(localSavedProvidersString));
159 | }
160 | const handleAddProvider = (providerId: string) => {
161 | const exits = localProviders.some((currentValue) => {
162 | return providerId === currentValue.id;
163 | })
164 | if (exits) return;
165 | const toAddProvider = providers.find((item) => {
166 | return item.id === providerId;
167 | })
168 | if (toAddProvider) {
169 | const localSavedProvidersString = localProviders.map((i) => {
170 | return i.id;
171 | });
172 | localStorage.setItem('localSavedProviders', JSON.stringify([...localSavedProvidersString, providerId]));
173 | setLocalProviders((m) => ([...m, toAddProvider]));
174 | }
175 | }
176 |
177 | return (
178 |
179 | {showNotice ?
180 |
{t('noProviderNotice')}{t('clickHere')}}
183 | type="warning"
184 | closable
185 | />
186 | : ''}
187 |
188 | {contextHolder}
189 |
190 |
{t('aiTranslate')}
191 |
192 |
193 |
194 |
213 | } size='small' />
218 |
236 |
237 |
238 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
261 | {
262 | toAddProviders.length === 0 && 已全部添加
263 | }
264 | {toAddProviders.length > 0 &&
265 | toAddProviders.map((item, index) => (
266 | { handleAddProvider(item.id) }}
268 | className='flex flex-row hover:bg-gray-100 rounded-lg p-2 items-center cursor-pointer'>
269 |
270 | {item.DisplayName}
271 |
272 | )
273 | )
274 | }
275 | } arrow={false}>
276 |
277 |
278 |
279 |
280 |
281 | {(provided) => (
282 |
283 | {localProviders.map((item, index) => (
284 |
285 | {(provided) => (
286 |
295 |
296 |
299 |
300 |
301 | {}
302 |
303 |
304 |
312 |
313 | )}
314 |
315 | ))}
316 | {provided.placeholder}
317 |
318 | )}
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 | );
329 | }
330 |
--------------------------------------------------------------------------------
/app/adapter/yiyan/yiyan.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------