refactor: cleanup template and add new pages
- Remove unused template files (frontend-code-interpreter, template-renderer) - Add home, test1, test2, zh-en-translator pages - Update router configuration - Add Docker and tailwind config - Update environment files and configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
# 后端请求地址
|
# 网站标题
|
||||||
VITE_API_BASE=http://192.168.113.18:8090/api
|
VITE_DOCUMENT_TITLE='AI办公助手'
|
||||||
# 服务前缀
|
|
||||||
VITE_API_BASE_PUB=/langwell-pub-server
|
|
||||||
VITE_API_BASE_SYS=/langwell-sys-server
|
|
||||||
VITE_API_BASE_NOTE=/langwell-notes-server
|
|
||||||
VITE_API_BASE_AI=/langwell-ai-server
|
|
||||||
VITE_API_BASE_DOC=/langwell-doc-server
|
|
||||||
|
|
||||||
|
# 服务前缀
|
||||||
|
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
|
||||||
|
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
|
||||||
|
VITE_API_BASE_NOTE=/langwell-api/langwell-notes-server
|
||||||
|
VITE_API_BASE_PUB=/langwell-api/langwell-pub-server
|
||||||
|
VITE_API_BASE_INS=/langwell-api/langwell-ins-server
|
||||||
|
VITE_API_BASE_TOOL=/copilot-tool
|
||||||
|
VITE_API_BASE_LAMP=/lamp-api
|
||||||
|
VITE_DIRECTION_API = /direction-api
|
||||||
|
# 语音识别
|
||||||
|
VITE_API_BASE_VOICE=/voice-api
|
||||||
# AI后端请求地址
|
# AI后端请求地址
|
||||||
VITE_AI_API_BASE=http://192.168.213.176/v1
|
VITE_AI_API_BASE=/v1
|
||||||
VITE_AI_CHAT_SECRET=app-8WsQjtYZYzOH9bRSFK8MCmaw
|
# Xiren-Lite SDK 配置 - 开发环境使用代理
|
||||||
|
VITE_XIREN_LITE_BASE_URL=/xiren-api
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
# 后端请求地址
|
# 网站标题
|
||||||
VITE_API_BASE=https://copilot.sino-bridge.com/api
|
VITE_DOCUMENT_TITLE='AI办公助手'
|
||||||
# 服务前缀
|
|
||||||
VITE_API_BASE_PUB=/langwell-pub-server
|
|
||||||
VITE_API_BASE_SYS=/langwell-sys-server
|
|
||||||
VITE_API_BASE_NOTE=/langwell-notes-server
|
|
||||||
VITE_API_BASE_AI=/langwell-ai-server
|
|
||||||
VITE_API_BASE_DOC=/langwell-doc-server
|
|
||||||
|
|
||||||
|
# 服务前缀
|
||||||
|
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
|
||||||
|
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
|
||||||
|
VITE_API_BASE_NOTE=/langwell-api/langwell-notes-server
|
||||||
|
VITE_API_BASE_PUB=/langwell-api/langwell-pub-server
|
||||||
|
VITE_API_BASE_INS=/langwell-api/langwell-ins-server
|
||||||
|
VITE_API_BASE_TOOL=/copilot-tool
|
||||||
|
VITE_API_BASE_LAMP=/lamp-api
|
||||||
|
VITE_DIRECTION_API = /direction-api
|
||||||
|
# 语音识别
|
||||||
|
VITE_API_BASE_VOICE=/voice-api
|
||||||
# AI后端请求地址
|
# AI后端请求地址
|
||||||
VITE_AI_API_BASE=https://copilot.sino-bridge.com:90/v1
|
VITE_AI_API_BASE=/v1
|
||||||
VITE_TEMPLATE_AGENT_TOKEN=app-FsEkXKzg41OZ0TCK9ywFmbFK
|
# Xiren-Lite SDK 配置 - 开发环境使用代理
|
||||||
|
VITE_XIREN_LITE_BASE_URL=/xiren-api
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -11,9 +11,8 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.history
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -22,3 +21,7 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
dist.zip
|
||||||
|
copilot-toolbox.zip
|
||||||
|
copilot-toolbox/
|
||||||
|
.trae
|
||||||
87
CLAUDE.md
Normal file
87
CLAUDE.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Copilot Toolbox is a React + TypeScript + Vite frontend template for building modern AI-powered applications. Integrates Dify AI, CopilotKit with Ant Design and Tailwind CSS.
|
||||||
|
|
||||||
|
**This is a template project - all development must extend this project, not create new repositories.**
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- React 18 + TypeScript + Vite 6
|
||||||
|
- Ant Design 5.x (UI components)
|
||||||
|
- Tailwind CSS 4.x + Less (styling)
|
||||||
|
- React Router DOM 6.x (Hash mode routing)
|
||||||
|
- @copilotkit/react-core, @ag-ui/client, @shangzy/ag-ui-dify (AI integration)
|
||||||
|
|
||||||
|
## Directory Conventions
|
||||||
|
|
||||||
|
| Type | Path Pattern | Example |
|
||||||
|
|------|--------------|---------|
|
||||||
|
| Pages | `src/pages/[name]/index.tsx` | `src/pages/home/index.tsx` |
|
||||||
|
| API | `src/api/[name].ts` | `src/api/user.ts` |
|
||||||
|
| Components | `src/components/[name]/index.tsx` | `src/components/Header/index.tsx` |
|
||||||
|
| Utils | `src/utils/[name].ts` | `src/utils/cacheUtil.ts` |
|
||||||
|
|
||||||
|
- Use `@/` alias for imports (configured in vite.config.ts)
|
||||||
|
- TypeScript strict mode enabled
|
||||||
|
- Tailwind CSS for styling, Ant Design for components
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # Install dependencies
|
||||||
|
pnpm dev # Dev server (port 5173)
|
||||||
|
pnpm build # Production build
|
||||||
|
pnpm preview # Preview production build (port 3000)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
All API base paths use proxy prefixes. Set in `.env.development` or `.env.production`:
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
|
||||||
|
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
|
||||||
|
VITE_API_BASE_LAMP=/lamp-api
|
||||||
|
VITE_AI_API_BASE=/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Pattern
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/api/[name].ts
|
||||||
|
export function getData(params: { id: string }) {
|
||||||
|
return fetch(`${import.meta.env.VITE_API_BASE_AI}/endpoint`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
}).then(res => res.json())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
Routes defined in `src/router/index.tsx` using lazy loading:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
path: '/page-name',
|
||||||
|
element: LazyLoad(lazy(() => import('@/pages/page-name')))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Features
|
||||||
|
|
||||||
|
1. Create page: `src/pages/[name]/index.tsx`
|
||||||
|
2. Create API: `src/api/[name].ts`
|
||||||
|
3. Add route: `src/router/index.tsx`
|
||||||
|
4. Use `@/` alias for imports
|
||||||
|
|
||||||
|
## Restrictions
|
||||||
|
|
||||||
|
- Do NOT create new repositories - extend this project
|
||||||
|
- Do NOT remove CLAUDE.md
|
||||||
|
- Use `@/` alias, not relative paths
|
||||||
|
- TypeScript strict mode cannot be disabled
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# ==================== 构建阶段 ====================
|
||||||
|
FROM node:24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 配置 npm 镜像
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 复制依赖文件并安装
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# 复制源代码并构建
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ==================== 运行阶段 ====================
|
||||||
|
FROM node:24-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 只安装服务器依赖(独立的 package.json)
|
||||||
|
COPY server/package.json ./
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com && npm install --omit=dev
|
||||||
|
|
||||||
|
# 复制构建产物
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
COPY --from=builder /app/server-dist ./server-dist
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["node", "server-dist/server/index.js"]
|
||||||
50
README.md
50
README.md
@@ -1,50 +0,0 @@
|
|||||||
# React + TypeScript + Vite
|
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
|
||||||
|
|
||||||
- Configure the top-level `parserOptions` property like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default tseslint.config({
|
|
||||||
languageOptions: {
|
|
||||||
// other options...
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
|
||||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// eslint.config.js
|
|
||||||
import react from 'eslint-plugin-react'
|
|
||||||
|
|
||||||
export default tseslint.config({
|
|
||||||
// Set the react version
|
|
||||||
settings: { react: { version: '18.3' } },
|
|
||||||
plugins: {
|
|
||||||
// Add the react plugin
|
|
||||||
react,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// other rules...
|
|
||||||
// Enable its recommended rules
|
|
||||||
...react.configs.recommended.rules,
|
|
||||||
...react.configs['jsx-runtime'].rules,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
|
|
||||||
export default tseslint.config(
|
|
||||||
{ ignores: ['dist'] },
|
|
||||||
{
|
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
'react-refresh': reactRefresh,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
...reactHooks.configs.recommended.rules,
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
28
index.html
28
index.html
@@ -1,13 +1,25 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.png"/>
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>代码解释器</title>
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>AI办公助手</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
<script>
|
||||||
|
window.iFrameResizer = {
|
||||||
|
heightCalculationMethod: 'lowestElement'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="/iframeResizer.contentWindow.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
5354
package-lock.json
generated
5354
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -1,40 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "webcontainers-express-app",
|
"name": "copilot-toolbox",
|
||||||
"private": true,
|
"version": "1.0.0",
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"build": "tsc -b && vite build",
|
"build": "vite build --mode production",
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ag-ui/client": "^0.0.42",
|
||||||
"@ant-design/icons": "^5.5.2",
|
"@ant-design/icons": "^5.5.2",
|
||||||
"@webcontainer/api": "1.5.1-internal.5",
|
"@ant-design/pro-components": "^2.8.10",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@copilotkit/react-core": "1.10.6",
|
||||||
|
"@shangzy/ag-ui-dify": "^0.1.0",
|
||||||
"antd": "^5.23.0",
|
"antd": "^5.23.0",
|
||||||
"axios": "^1.7.9",
|
"dayjs": "^1.11.13",
|
||||||
"less": "^4.2.1",
|
"less": "^4.2.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-markdown": "^9.0.3",
|
"react-router-dom": "^6.28.1"
|
||||||
"react-router-dom": "^6.21.1",
|
|
||||||
"react-syntax-highlighter": "^15.6.1",
|
|
||||||
"react-transition-group": "^4.4.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@tailwindcss/vite": "4.0.12",
|
||||||
"@types/node": "^22.10.5",
|
"@types/node": "^22.10.5",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.17.0",
|
"path": "^0.12.7",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"prettier-plugin-tailwindcss": "^0.1.7",
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
"tailwindcss": "4.0.12",
|
||||||
"globals": "^15.14.0",
|
"typescript": "~5.6.3",
|
||||||
"typescript": "~5.6.2",
|
"vite": "^6.0.7"
|
||||||
"typescript-eslint": "^8.18.2",
|
|
||||||
"vite": "^6.0.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5944
pnpm-lock.yaml
generated
5944
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
|||||||
export const EXPRESS_APP = {
|
|
||||||
'index.js': {
|
|
||||||
file: {
|
|
||||||
contents: `import express from 'express';
|
|
||||||
const app = express();
|
|
||||||
const port = 3111;
|
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
|
||||||
res.send('Welcome to a WebContainers app! 🥳');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(\`App is live at http://localhost:\${port}\`);
|
|
||||||
});`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'package.json': {
|
|
||||||
file: {
|
|
||||||
contents: `
|
|
||||||
{
|
|
||||||
"name": "example-app",
|
|
||||||
"type": "module",
|
|
||||||
"dependencies": {
|
|
||||||
"express": "latest",
|
|
||||||
"nodemon": "latest"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "nodemon --watch './' index.js"
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
export const REACT_APP = {
|
|
||||||
'src': {
|
|
||||||
'directory': {
|
|
||||||
'vite-env.d.ts': {
|
|
||||||
file: {
|
|
||||||
contents: `/// <reference types="vite/client" />`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'main.tsx': {
|
|
||||||
file: {
|
|
||||||
contents: `import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import App from './App.tsx'
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>,
|
|
||||||
)
|
|
||||||
`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'App.tsx': {
|
|
||||||
file: {
|
|
||||||
contents: `import React, { useState } from 'react';
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
const [activeTab, setActiveTab] = useState('all');
|
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const projects = [
|
|
||||||
{ id: 1, title: "项目一", category: "web", description: "响应式网站设计" },
|
|
||||||
{ id: 2, title: "项目二", category: "mobile", description: "移动应用开发" },
|
|
||||||
{ id: 3, title: "项目三", category: "web", description: "电商平台开发" },
|
|
||||||
{ id: 4, title: "项目四", category: "mobile", description: "移动游戏设计" }
|
|
||||||
];
|
|
||||||
|
|
||||||
const filteredProjects = activeTab === 'all'
|
|
||||||
? projects
|
|
||||||
: projects.filter(project => project.category === activeTab);
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
container: {
|
|
||||||
maxWidth: '1200px',
|
|
||||||
margin: '0 auto',
|
|
||||||
padding: '20px',
|
|
||||||
fontFamily: 'Arial, sans-serif',
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
padding: '20px',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
||||||
},
|
|
||||||
nav: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
logo: {
|
|
||||||
fontSize: '24px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
menuButton: {
|
|
||||||
display: 'none',
|
|
||||||
'@media (max-width: 768px)': {
|
|
||||||
display: 'block',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
navLinks: {
|
|
||||||
display: 'flex',
|
|
||||||
gap: '20px',
|
|
||||||
},
|
|
||||||
navLink: {
|
|
||||||
color: '#333',
|
|
||||||
textDecoration: 'none',
|
|
||||||
padding: '8px 16px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
transition: 'background-color 0.3s',
|
|
||||||
},
|
|
||||||
hero: {
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '60px 20px',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
},
|
|
||||||
heroTitle: {
|
|
||||||
fontSize: '48px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: '20px',
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
heroSubtitle: {
|
|
||||||
fontSize: '20px',
|
|
||||||
color: '#666',
|
|
||||||
marginBottom: '30px',
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
backgroundColor: '#007bff',
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
padding: '12px 24px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '16px',
|
|
||||||
transition: 'background-color 0.3s',
|
|
||||||
},
|
|
||||||
buttonOutline: {
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
border: '2px solid #007bff',
|
|
||||||
color: '#007bff',
|
|
||||||
},
|
|
||||||
projectsSection: {
|
|
||||||
padding: '40px 0',
|
|
||||||
},
|
|
||||||
tabsContainer: {
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: '10px',
|
|
||||||
marginBottom: '30px',
|
|
||||||
},
|
|
||||||
tab: {
|
|
||||||
padding: '8px 16px',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
backgroundColor: '#eee',
|
|
||||||
},
|
|
||||||
activeTab: {
|
|
||||||
backgroundColor: '#007bff',
|
|
||||||
color: 'white',
|
|
||||||
},
|
|
||||||
projectsGrid: {
|
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
|
||||||
gap: '20px',
|
|
||||||
padding: '20px',
|
|
||||||
},
|
|
||||||
projectCard: {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '20px',
|
|
||||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
||||||
},
|
|
||||||
projectTitle: {
|
|
||||||
fontSize: '20px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: '10px',
|
|
||||||
},
|
|
||||||
projectDescription: {
|
|
||||||
color: '#666',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<header style={styles.header}>
|
|
||||||
<nav style={styles.nav}>
|
|
||||||
<div style={styles.logo}>Demo</div>
|
|
||||||
<div style={styles.navLinks}>
|
|
||||||
<a href="#" style={styles.navLink}>首页</a>
|
|
||||||
<a href="#" style={styles.navLink}>项目</a>
|
|
||||||
<a href="#" style={styles.navLink}>关于</a>
|
|
||||||
<a href="#" style={styles.navLink}>联系</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section style={styles.hero}>
|
|
||||||
<h1 style={styles.heroTitle}>欢迎来到我们的演示页面</h1>
|
|
||||||
<p style={styles.heroSubtitle}>探索创新项目,体验卓越设计</p>
|
|
||||||
<div style={{ display: 'flex', gap: '20px', justifyContent: 'center' }}>
|
|
||||||
<button style={styles.button}>开始使用</button>
|
|
||||||
<button style={{...styles.button, ...styles.buttonOutline}}>了解更多</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section style={styles.projectsSection}>
|
|
||||||
<div style={styles.container}>
|
|
||||||
<div style={styles.tabsContainer}>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...styles.tab,
|
|
||||||
...(activeTab === 'all' ? styles.activeTab : {})
|
|
||||||
}}
|
|
||||||
onClick={() => setActiveTab('all')}
|
|
||||||
>
|
|
||||||
全部
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...styles.tab,
|
|
||||||
...(activeTab === 'web' ? styles.activeTab : {})
|
|
||||||
}}
|
|
||||||
onClick={() => setActiveTab('web')}
|
|
||||||
>
|
|
||||||
Web
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...styles.tab,
|
|
||||||
...(activeTab === 'mobile' ? styles.activeTab : {})
|
|
||||||
}}
|
|
||||||
onClick={() => setActiveTab('mobile')}
|
|
||||||
>
|
|
||||||
Mobile
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={styles.projectsGrid}>
|
|
||||||
{filteredProjects.map(project => (
|
|
||||||
<div key={project.id} style={styles.projectCard}>
|
|
||||||
<h3 style={styles.projectTitle}>{project.title}</h3>
|
|
||||||
<p style={styles.projectDescription}>{project.description}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App
|
|
||||||
`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"index.html": {
|
|
||||||
file: {
|
|
||||||
contents: `<!doctype html>
|
|
||||||
<html lang="zn">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>解释结果</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'package.json': {
|
|
||||||
file: {
|
|
||||||
contents: `{
|
|
||||||
"name": "react-app",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"start": "vite",
|
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/js": "^9.17.0",
|
|
||||||
"@types/react": "^18.3.18",
|
|
||||||
"@types/react-dom": "^18.3.5",
|
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
|
||||||
"eslint": "^9.17.0",
|
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.16",
|
|
||||||
"globals": "^15.14.0",
|
|
||||||
"typescript": "~5.6.2",
|
|
||||||
"typescript-eslint": "^8.18.2",
|
|
||||||
"vite": "^6.0.5"
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"tsconfig.app.json": {
|
|
||||||
file: {
|
|
||||||
contents: `{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tsconfig.json": {
|
|
||||||
file: {
|
|
||||||
contents: `{
|
|
||||||
"files": [],
|
|
||||||
"references": [
|
|
||||||
{ "path": "./tsconfig.app.json" },
|
|
||||||
{ "path": "./tsconfig.node.json" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tsconfig.node.json": {
|
|
||||||
file: {
|
|
||||||
contents: `{
|
|
||||||
"compilerOptions": {
|
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
||||||
"target": "ES2022",
|
|
||||||
"lib": ["ES2023"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedSideEffectImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vite.config.ts": {
|
|
||||||
file: {
|
|
||||||
contents: `import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
})`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
1311
public/iframeResizer.contentWindow.js
Normal file
1311
public/iframeResizer.contentWindow.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,147 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>代码执行结果</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
color: #fff;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
padding: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.03);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(24, 136, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 主加载动画 */
|
|
||||||
.loading-ring {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-ring:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-top-color: #1888ff;
|
|
||||||
border-right-color: #1888ff;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: rgba(255, 255, 255, 0.9);
|
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-top: 8px;
|
|
||||||
background: linear-gradient(90deg, #1888ff, #3498ff);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 背景装饰 */
|
|
||||||
.background-line {
|
|
||||||
position: absolute;
|
|
||||||
height: 1px;
|
|
||||||
width: 100px;
|
|
||||||
background: linear-gradient(90deg,
|
|
||||||
transparent,
|
|
||||||
rgba(24, 136, 255, 0.2),
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-top {
|
|
||||||
top: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-bottom {
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 动画定义 */
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 发光效果 */
|
|
||||||
.glow {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle,
|
|
||||||
rgba(24, 136, 255, 0.1) 0%,
|
|
||||||
transparent 70%
|
|
||||||
);
|
|
||||||
z-index: -1;
|
|
||||||
animation: glow 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glow {
|
|
||||||
0%, 100% {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="loading-container">
|
|
||||||
<div class="background-line line-top"></div>
|
|
||||||
<div class="background-line line-bottom"></div>
|
|
||||||
<div class="glow"></div>
|
|
||||||
<div class="loading-ring"></div>
|
|
||||||
<div class="loading-text">正在执行</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
44
src/App.tsx
Normal file
44
src/App.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { RouterProvider } from 'react-router-dom'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import router from '@/router'
|
||||||
|
import { cacheClear, cacheSet } from '@/utils/cacheUtil'
|
||||||
|
import { getAgentBaseInfo, getUserInfoById } from '@/api/common'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const queryString = window.location.search
|
||||||
|
const searchParams = new URLSearchParams(queryString)
|
||||||
|
const tenantId = searchParams.get('tenantId') || ''
|
||||||
|
const token = searchParams.get('token') || ''
|
||||||
|
|
||||||
|
cacheSet('tenantId', tenantId)
|
||||||
|
cacheSet('token', token)
|
||||||
|
useEffect(() => {
|
||||||
|
const agentId = searchParams.get('agentId') || ''
|
||||||
|
|
||||||
|
if (agentId) {
|
||||||
|
getAgentBaseInfo({ id: agentId }).then(res => {
|
||||||
|
cacheSet('appKey', res.data.appKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tenantId && token) {
|
||||||
|
getUserInfoById().then(response => {
|
||||||
|
if (response.code === 200 && response.data) {
|
||||||
|
cacheSet('userInfo', JSON.stringify(response.data))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
cacheClear('userInfo')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
28
src/api/common.ts
Normal file
28
src/api/common.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { cacheGet } from '@/utils/cacheUtil'
|
||||||
|
|
||||||
|
export function getAgentBaseInfo(params: { id: string }) {
|
||||||
|
const Tenantid = cacheGet('tenantId')
|
||||||
|
const Token = cacheGet('token')
|
||||||
|
return fetch(`${import.meta.env.VITE_API_BASE_AI}/agent/baseInfo/info?id=${params.id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Tenantid,
|
||||||
|
Token
|
||||||
|
}
|
||||||
|
}).then(res => res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
export async function getUserInfoById() {
|
||||||
|
const Tenantid = cacheGet('tenantId')
|
||||||
|
const Token = cacheGet('token')
|
||||||
|
return fetch(`${import.meta.env['VITE_API_BASE_LAMP']}/oauth/anyone/getUserInfoById`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Tenantid,
|
||||||
|
Token
|
||||||
|
}
|
||||||
|
}).then(res => res.json())
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import {SSEClient, SSERequest} from "../utils/sse-client.ts";
|
|
||||||
|
|
||||||
const VITE_API_BASE_DOC = import.meta.env['VITE_API_BASE_DOC'] || "";
|
|
||||||
const VITE_AI_API_BASE = import.meta.env['VITE_AI_API_BASE'] || "";
|
|
||||||
const VITE_TEMPLATE_AGENT_TOKEN = import.meta.env['VITE_TEMPLATE_AGENT_TOKEN'] || "";
|
|
||||||
|
|
||||||
export async function listAllTemplates() {
|
|
||||||
const res = await fetch(`${VITE_API_BASE_DOC}/doc/resumeTemp/list`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
title: ""
|
|
||||||
})
|
|
||||||
});
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function convertFileToPDF(file: File) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
|
|
||||||
return fetch(`${VITE_API_BASE_DOC}/doc/resume/formatConvert`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function uploadFile(file: File) {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("file", file);
|
|
||||||
formData.append("user", "resume-generation");
|
|
||||||
return fetch(`${VITE_AI_API_BASE}/files/upload`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${VITE_TEMPLATE_AGENT_TOKEN}`
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
}).then(res => res.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function templatePadding(templateId: string, data: object) {
|
|
||||||
return fetch(`${VITE_API_BASE_DOC}/doc/resume/padding?templateId=${templateId}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
paddingParameter: data
|
|
||||||
})
|
|
||||||
}).then(res => res.json());
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function structGeneration(id: string, {
|
|
||||||
onMessage,
|
|
||||||
onError,
|
|
||||||
onFinish
|
|
||||||
}: Pick<SSERequest, "onMessage" | "onError" | "onFinish">) {
|
|
||||||
const client = new SSEClient(VITE_TEMPLATE_AGENT_TOKEN);
|
|
||||||
return client.sendMessage({
|
|
||||||
type: "chat",
|
|
||||||
query: "生成",
|
|
||||||
user: "resume-generation",
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
type: "document",
|
|
||||||
transfer_method: "local_file",
|
|
||||||
upload_file_id: id
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onMessage,
|
|
||||||
onError,
|
|
||||||
onFinish
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function downloadPaddingResult(shortUrl: string) {
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = `https://copilot.sino-bridge.com/api/langwell-doc-server/doc/resume/download/${shortUrl}`
|
|
||||||
link.download = ""
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
link.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
34
src/api/zh-en-translator.ts
Normal file
34
src/api/zh-en-translator.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
export interface TranslationRequest {
|
||||||
|
source_content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DifyRequest {
|
||||||
|
inputs: {
|
||||||
|
prompt: string
|
||||||
|
}
|
||||||
|
query: string
|
||||||
|
response_mode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function translateChineseToEnglish(source_content: string) {
|
||||||
|
const prompt = `你是一个专业的中英翻译专家。请将下面的中文文本翻译成自然、地道、专业的英文,只返回翻译后的英文内容,不要添加任何额外的解释或格式。待翻译的中文内容是:
|
||||||
|
|
||||||
|
${source_content}`
|
||||||
|
|
||||||
|
const requestBody: DifyRequest = {
|
||||||
|
inputs: {
|
||||||
|
prompt
|
||||||
|
},
|
||||||
|
query: '1',
|
||||||
|
response_mode: 'streaming'
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch('https://copilot.sino-bridge.com/v1/chat-messages', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer app-Y6ekYkw3aoUV3jmfZdg24Adh'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(requestBody)
|
||||||
|
})
|
||||||
|
}
|
||||||
29
src/index.css
Normal file
29
src/index.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
.route-loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 120px;
|
||||||
|
height: 22px;
|
||||||
|
color: #514b82;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 20px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes l6 {
|
||||||
|
100% {
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.route-loading::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 100% 0 0;
|
||||||
|
margin: 2px;
|
||||||
|
content: '';
|
||||||
|
background: currentcolor;
|
||||||
|
border-radius: inherit;
|
||||||
|
animation: l6 2s infinite;
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
background: linear-gradient(180deg, rgba(189, 225, 255, 0.4) 0%, rgba(224, 242, 255, 0) 100%);
|
|
||||||
border-radius: 0.5rem 0.5rem 0 0;
|
|
||||||
padding: 12px 24px;
|
|
||||||
height: 72px;
|
|
||||||
|
|
||||||
.title-text {
|
|
||||||
color: transparent;
|
|
||||||
background: linear-gradient(116deg, #1888ff 16%, #2f54eb 88%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
user-select: none;
|
|
||||||
font-size: 30px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar {
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-corner {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
16
src/main.tsx
16
src/main.tsx
@@ -1,6 +1,12 @@
|
|||||||
import {createRoot} from 'react-dom/client'
|
import './utils/polyfills'
|
||||||
import {RouterProvider} from 'react-router-dom'
|
import { createRoot } from 'react-dom/client'
|
||||||
import routers from './router'
|
import { ConfigProvider } from 'antd'
|
||||||
import './index.less'
|
import zhCN from 'antd/es/locale/zh_CN'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<RouterProvider router={routers}/>)
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<ConfigProvider locale={zhCN}>
|
||||||
|
<App />
|
||||||
|
</ConfigProvider>
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
.ant-splitter {
|
|
||||||
padding: 12px 24px;
|
|
||||||
gap: 12px;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
.ant-splitter-panel {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 控制代码块的高度撑满父容器 */
|
|
||||||
.ant-spin-nested-loading, .ant-spin-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-spin-container {
|
|
||||||
.editor {
|
|
||||||
position: relative;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
max-height: 1000px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
|
|
||||||
.terminal {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.iframe-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
|
|
||||||
&.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import { WebContainer } from '@webcontainer/api'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
// import {EXPRESS_APP} from "../../../projects/express-app.ts";
|
|
||||||
import { REACT_APP } from '../../../projects/react-app.ts'
|
|
||||||
import { Terminal } from '@xterm/xterm'
|
|
||||||
import { installDependencies, startDevServer } from '../../utils/dev-operations.ts'
|
|
||||||
import { Button, Flex, Space, Spin, Splitter, Tooltip, Typography } from 'antd'
|
|
||||||
import { CheckCircleFilled, CopyOutlined, DownloadOutlined } from '@ant-design/icons'
|
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|
||||||
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
|
||||||
|
|
||||||
import './xterm.less'
|
|
||||||
import './index.less'
|
|
||||||
|
|
||||||
let containerInstance: WebContainer
|
|
||||||
|
|
||||||
export const FrontEndCodeInterpreter = () => {
|
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
||||||
const terminalRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
|
||||||
const [codeLoading, setCodeLoading] = useState<boolean>(true)
|
|
||||||
const [devServerStarted, setDevServerStarted] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const [codeString, setCodeString] = useState<string>('')
|
|
||||||
const [copyText, setCopyText] = useState<string>('复制')
|
|
||||||
const [copyIcon, setCopyIcon] = useState(<CopyOutlined />)
|
|
||||||
const [downloadText, setDownloadText] = useState<string>('下载')
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
if (containerInstance != null) return
|
|
||||||
containerInstance = await WebContainer.boot()
|
|
||||||
setLoading(false)
|
|
||||||
containerInstance
|
|
||||||
.mount(REACT_APP)
|
|
||||||
.then(() => {
|
|
||||||
setCodeString(REACT_APP['src']['directory']['App.tsx'].file.contents.trim())
|
|
||||||
setCodeLoading(false)
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const terminal = new Terminal({
|
|
||||||
convertEol: true
|
|
||||||
})
|
|
||||||
terminalRef.current && terminal.open(terminalRef.current)
|
|
||||||
return terminal
|
|
||||||
})
|
|
||||||
.then(terminal => {
|
|
||||||
installDependencies(containerInstance, terminal).then(res => {
|
|
||||||
res &&
|
|
||||||
startDevServer(containerInstance, terminal, url => {
|
|
||||||
if (iframeRef.current) {
|
|
||||||
iframeRef.current.src = url
|
|
||||||
setDevServerStarted(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
init()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleCopyClick = () => {
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(codeString)
|
|
||||||
.then(() => {
|
|
||||||
setCopyText('已复制')
|
|
||||||
setCopyIcon(<CheckCircleFilled />)
|
|
||||||
})
|
|
||||||
.finally(() =>
|
|
||||||
setTimeout(() => {
|
|
||||||
setCopyText('复制')
|
|
||||||
setCopyIcon(<CopyOutlined />)
|
|
||||||
}, 2000)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDownloadClick = () => {
|
|
||||||
setDownloadText('已下载')
|
|
||||||
setTimeout(() => {
|
|
||||||
setDownloadText('下载')
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Spin tip='加载中' spinning={loading} fullscreen size='large' />
|
|
||||||
<Flex className='container' vertical>
|
|
||||||
<Flex className='toolbar' justify='space-between'>
|
|
||||||
<Typography.Text className='title-text'>代码解释器</Typography.Text>
|
|
||||||
<Space>
|
|
||||||
<Tooltip placement='bottom' title={copyText}>
|
|
||||||
<Button color='primary' variant='link' icon={copyIcon} onClick={handleCopyClick}></Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip placement='bottom' title={downloadText}>
|
|
||||||
<Button color='primary' variant='link' icon={<DownloadOutlined />} onClick={handleDownloadClick}></Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
<Splitter>
|
|
||||||
<Splitter.Panel defaultSize='40%' min='20%' max='70%'>
|
|
||||||
<Spin tip='代码加载中' spinning={codeLoading} size='large'>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
className='editor custom-scrollbar'
|
|
||||||
language='jsx'
|
|
||||||
showLineNumbers={true}
|
|
||||||
wrapLines={true}
|
|
||||||
style={dark}
|
|
||||||
customStyle={{
|
|
||||||
border: 'none',
|
|
||||||
margin: '0'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{codeString}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</Spin>
|
|
||||||
</Splitter.Panel>
|
|
||||||
<Splitter.Panel>
|
|
||||||
<Flex className='preview' vertical>
|
|
||||||
<div ref={terminalRef} className={`terminal ${devServerStarted ? 'hidden' : ''}`} />
|
|
||||||
<div className={`iframe-wrapper ${devServerStarted ? 'visible' : ''}`}>
|
|
||||||
<iframe ref={iframeRef} src='/loading.html' frameBorder='0' />
|
|
||||||
</div>
|
|
||||||
</Flex>
|
|
||||||
</Splitter.Panel>
|
|
||||||
</Splitter>
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
||||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
||||||
* https://github.com/chjj/term.js
|
|
||||||
* @license MIT
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* Originally forked from (with the author's permission):
|
|
||||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
||||||
* http://bellard.org/jslinux/
|
|
||||||
* Copyright (c) 2011 Fabrice Bellard
|
|
||||||
* The original design remains. The terminal itself
|
|
||||||
* has been extended to include xterm CSI codes, among
|
|
||||||
* other features.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default styles for xterm.js
|
|
||||||
*/
|
|
||||||
|
|
||||||
.xterm {
|
|
||||||
cursor: text;
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm.focus,
|
|
||||||
.xterm:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-helpers {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
/**
|
|
||||||
* The z-index of the helpers must be higher than the canvases in order for
|
|
||||||
* IMEs to appear on top.
|
|
||||||
*/
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-helper-textarea {
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
|
||||||
position: absolute;
|
|
||||||
opacity: 0;
|
|
||||||
left: -9999em;
|
|
||||||
top: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
z-index: -5;
|
|
||||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .composition-view {
|
|
||||||
/* TODO: Composition position got messed up somewhere */
|
|
||||||
background: #000;
|
|
||||||
color: #FFF;
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
white-space: nowrap;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .composition-view.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-viewport {
|
|
||||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
|
||||||
background-color: #000;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow: hidden auto;
|
|
||||||
cursor: default;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-screen {
|
|
||||||
position: relative;
|
|
||||||
width: 100% !important;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-screen .xterm-rows div {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-screen canvas {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-scroll-area {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-char-measure-element {
|
|
||||||
display: inline-block;
|
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -9999em;
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm.enable-mouse-events {
|
|
||||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm.xterm-cursor-pointer,
|
|
||||||
.xterm .xterm-cursor-pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm.column-select.focus {
|
|
||||||
/* Column selection mode */
|
|
||||||
cursor: crosshair;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-accessibility:not(.debug),
|
|
||||||
.xterm .xterm-message {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 10;
|
|
||||||
color: transparent;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .xterm-accessibility-tree {
|
|
||||||
user-select: text;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm .live-region {
|
|
||||||
position: absolute;
|
|
||||||
left: -9999px;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-dim {
|
|
||||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
|
||||||
* explicitly in the generated class and reset to 1 here */
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-underline-1 {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-underline-2 {
|
|
||||||
text-decoration: double underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-underline-3 {
|
|
||||||
text-decoration: wavy underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-underline-4 {
|
|
||||||
text-decoration: dotted underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-underline-5 {
|
|
||||||
text-decoration: dashed underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-overline {
|
|
||||||
text-decoration: overline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-overline.xterm-underline-1 {
|
|
||||||
text-decoration: overline underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-overline.xterm-underline-2 {
|
|
||||||
text-decoration: overline double underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-overline.xterm-underline-3 {
|
|
||||||
text-decoration: overline wavy underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-overline.xterm-underline-4 {
|
|
||||||
text-decoration: overline dotted underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-overline.xterm-underline-5 {
|
|
||||||
text-decoration: overline dashed underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-strikethrough {
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
|
||||||
z-index: 6;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
|
||||||
z-index: 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-decoration-overview-ruler {
|
|
||||||
z-index: 8;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xterm-decoration-top {
|
|
||||||
z-index: 2;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
57
src/pages/home/index.tsx
Normal file
57
src/pages/home/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Card, Row, Col, Typography, Space } from 'antd'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { RobotOutlined, TranslationOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
const { Title, Paragraph } = Typography
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{
|
||||||
|
title: '测试页面 1',
|
||||||
|
description: 'Dify AI Agent 集成示例',
|
||||||
|
icon: <RobotOutlined style={{ fontSize: 32, color: '#1890ff' }} />,
|
||||||
|
link: '/test1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '测试页面 2',
|
||||||
|
description: '功能开发中...',
|
||||||
|
icon: <FileTextOutlined style={{ fontSize: 32, color: '#52c41a' }} />,
|
||||||
|
link: '/test2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '中英翻译器',
|
||||||
|
description: 'AI 驱动的翻译工具',
|
||||||
|
icon: <TranslationOutlined style={{ fontSize: 32, color: '#722ed1' }} />,
|
||||||
|
link: '/zh-en-translator'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const HomePage: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24 }}>
|
||||||
|
<Title level={2}>欢迎使用 AI 办公助手</Title>
|
||||||
|
<Paragraph style={{ marginBottom: 32 }}>
|
||||||
|
选择下方功能开始使用,或通过顶部导航访问其他页面。
|
||||||
|
</Paragraph>
|
||||||
|
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{cards.map((card, index) => (
|
||||||
|
<Col xs={24} sm={12} md={8} key={index}>
|
||||||
|
<Link to={card.link}>
|
||||||
|
<Card hoverable style={{ height: '100%' }}>
|
||||||
|
<Space direction='vertical' align='center' style={{ width: '100%' }}>
|
||||||
|
{card.icon}
|
||||||
|
<Title level={4} style={{ marginBottom: 0 }}>{card.title}</Title>
|
||||||
|
<Paragraph type='secondary' style={{ marginBottom: 0 }}>
|
||||||
|
{card.description}
|
||||||
|
</Paragraph>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
.content-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - 100px);
|
|
||||||
|
|
||||||
.template-form {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 40%;
|
|
||||||
|
|
||||||
|
|
||||||
.ant-upload-wrapper {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-panel {
|
|
||||||
opacity: 0;
|
|
||||||
animation: slideIn 0.3s ease-in-out forwards;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 1px;
|
|
||||||
width: 60%;
|
|
||||||
margin-right: 24px;
|
|
||||||
|
|
||||||
.preview-header {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
.template-select {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-content {
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
overflow: auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5em;
|
|
||||||
overflow: auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
/** 模板生成工具 */
|
|
||||||
import {Button, Card, Flex, message, Select, Space, Spin, Typography, Upload} from "antd";
|
|
||||||
import {useCallback, useEffect, useRef, useState} from "react";
|
|
||||||
import {
|
|
||||||
convertFileToPDF,
|
|
||||||
downloadPaddingResult,
|
|
||||||
listAllTemplates,
|
|
||||||
structGeneration,
|
|
||||||
templatePadding,
|
|
||||||
uploadFile
|
|
||||||
} from "../../api/template.ts";
|
|
||||||
import {CheckCircleFilled, DownloadOutlined, InboxOutlined, LoadingOutlined} from "@ant-design/icons";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'
|
|
||||||
import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism'
|
|
||||||
|
|
||||||
import "./index.less";
|
|
||||||
import {extractJSONFromString} from "../../utils/json-extractor.ts";
|
|
||||||
|
|
||||||
export const TemplateRenderer = () => {
|
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
|
||||||
// FIXME 这个装填应该提交到全局状态库
|
|
||||||
const [globalLoading, setGlobalLoading] = useState<boolean>(true)
|
|
||||||
|
|
||||||
// 点击【开始生成】后,状态变更
|
|
||||||
const [startSending, setStartSending] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// 生成状态
|
|
||||||
const [generating, setGenerating] = useState<boolean>(false);
|
|
||||||
// 生成消息
|
|
||||||
const [messages, setMessages] = useState<string>("");
|
|
||||||
// 生成异常
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [generationMetadata, setGenerationMetadata] = useState<object>();
|
|
||||||
|
|
||||||
const [templates, setTemplates] = useState<object[]>([]);
|
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>();
|
|
||||||
const [uploadedFile, setUploadedFile] = useState<object>();
|
|
||||||
|
|
||||||
// 用于存储完整的响应文本
|
|
||||||
const fullContentRef = useRef<string>('');
|
|
||||||
// 用于控制动画帧
|
|
||||||
const animationFrameRef = useRef<number>();
|
|
||||||
// 用于跟踪当前显示的字符位置
|
|
||||||
const currentIndexRef = useRef<number>(0);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.title = "模板填充工具";
|
|
||||||
listAllTemplates().then(response => {
|
|
||||||
if (response.code === 200 && response.data) {
|
|
||||||
const templates = response.data.map(item => {
|
|
||||||
return {value: item.id, label: <span>{item.title}</span>}
|
|
||||||
})
|
|
||||||
setTemplates(templates);
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
setGlobalLoading(false);
|
|
||||||
})
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return cleanup;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 清理函数
|
|
||||||
const cleanup = () => {
|
|
||||||
if (animationFrameRef.current) {
|
|
||||||
cancelAnimationFrame(animationFrameRef.current);
|
|
||||||
}
|
|
||||||
fullContentRef.current = '';
|
|
||||||
currentIndexRef.current = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const smoothRender = useCallback(() => {
|
|
||||||
const renderNextChunk = () => {
|
|
||||||
if (currentIndexRef.current < fullContentRef.current.length) {
|
|
||||||
// 每次渲染多个字符以提高性能,同时保持流畅性
|
|
||||||
const chunkSize = 2;
|
|
||||||
const nextIndex = Math.min(
|
|
||||||
currentIndexRef.current + chunkSize,
|
|
||||||
fullContentRef.current.length
|
|
||||||
);
|
|
||||||
|
|
||||||
setMessages(fullContentRef.current.slice(0, nextIndex));
|
|
||||||
currentIndexRef.current = nextIndex;
|
|
||||||
|
|
||||||
// 继续下一帧渲染
|
|
||||||
animationFrameRef.current = requestAnimationFrame(renderNextChunk);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderNextChunk();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 处理新收到的文本
|
|
||||||
const handleNewText = useCallback((text: string) => {
|
|
||||||
fullContentRef.current += text;
|
|
||||||
// 如果当前没有动画在进行,启动新的渲染
|
|
||||||
if (!animationFrameRef.current) {
|
|
||||||
smoothRender();
|
|
||||||
}
|
|
||||||
}, [smoothRender]);
|
|
||||||
|
|
||||||
const handleGeneration = useCallback(async (fileId: string) => {
|
|
||||||
setGenerating(true);
|
|
||||||
setError(null);
|
|
||||||
setMessages("");
|
|
||||||
cleanup();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await structGeneration(fileId, {
|
|
||||||
onMessage: (text, finished) => {
|
|
||||||
if (text) {
|
|
||||||
handleNewText(text);
|
|
||||||
setMessages(prev => prev + text);
|
|
||||||
}
|
|
||||||
if (finished) {
|
|
||||||
setGenerating(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setError(error.message);
|
|
||||||
setGenerating(false);
|
|
||||||
cleanup();
|
|
||||||
},
|
|
||||||
onFinish: (metadata) => {
|
|
||||||
setGenerationMetadata(metadata);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : '生成过程中发生错误');
|
|
||||||
setGenerating(false);
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
}, [handleNewText]);
|
|
||||||
|
|
||||||
|
|
||||||
const beforeUpload = (file: File) => {
|
|
||||||
const originalFilename = file.name.substring(0, file.name.lastIndexOf("."));
|
|
||||||
const originalFileExt = file.name.substring(file.name.lastIndexOf(".") + 1);
|
|
||||||
if (["pdf", "docx"].includes(originalFileExt)) {
|
|
||||||
messageApi.open({
|
|
||||||
key: "uploading",
|
|
||||||
type: 'loading',
|
|
||||||
content: '文件上传中',
|
|
||||||
});
|
|
||||||
convertFileToPDF(file).then(async (response) => {
|
|
||||||
if (response["status"] && response["status"] === 500) {
|
|
||||||
messageApi.open({
|
|
||||||
key: "uploading",
|
|
||||||
type: 'error',
|
|
||||||
content: '文件处理异常,请稍后重试',
|
|
||||||
duration: 1
|
|
||||||
});
|
|
||||||
} else if ("blob" in response) {
|
|
||||||
const blob = await response.blob();
|
|
||||||
const pdfFile = new File([blob], `${originalFilename}.pdf`, {type: 'application/pdf'});
|
|
||||||
uploadFile(pdfFile).then(async (response) => {
|
|
||||||
if (response.id) {
|
|
||||||
setUploadedFile(response);
|
|
||||||
messageApi.open({
|
|
||||||
key: "uploading",
|
|
||||||
type: 'success',
|
|
||||||
content: '文件上传成功',
|
|
||||||
duration: 1
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
messageApi.open({
|
|
||||||
key: "uploading",
|
|
||||||
type: 'error',
|
|
||||||
content: '文件上传失败',
|
|
||||||
duration: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
messageApi.error("目前仅支持.docx,.pdf类型的文件,请您将文件转成这些格式后再次进行上传");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGenerationStart = () => {
|
|
||||||
setStartSending(true);
|
|
||||||
handleGeneration(uploadedFile!.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const handleTemplatePadding = () => {
|
|
||||||
const templatePaddingData: string | null = extractJSONFromString(messages);
|
|
||||||
if (selectedTemplateId && templatePaddingData) {
|
|
||||||
templatePadding(selectedTemplateId, templatePaddingData).then(response => {
|
|
||||||
console.log(response);
|
|
||||||
if (response.code === 200) {
|
|
||||||
const url: string = response.data.shortUrl;
|
|
||||||
if (url) {
|
|
||||||
downloadPaddingResult(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const markdownComponents = {
|
|
||||||
code({inline, className, children, ...props}) {
|
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
|
||||||
return !inline && match ? (<SyntaxHighlighter
|
|
||||||
{...props}
|
|
||||||
className='editor custom-scrollbar'
|
|
||||||
language={match?.[1]}
|
|
||||||
showLineNumbers={true}
|
|
||||||
wrapLines={true}
|
|
||||||
style={dark}
|
|
||||||
customStyle={{
|
|
||||||
border: 'none',
|
|
||||||
margin: '0'
|
|
||||||
}}
|
|
||||||
children={String(children).replace(/\n$/, "")}
|
|
||||||
>
|
|
||||||
</SyntaxHighlighter>) : (
|
|
||||||
<code {...props} className={className}>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{contextHolder}
|
|
||||||
<Spin tip='加载中' spinning={globalLoading} fullscreen size='large'/>
|
|
||||||
<Flex className='toolbar' justify='space-between'>
|
|
||||||
<Typography.Text className='title-text'>模板填充工具</Typography.Text>
|
|
||||||
<Space>
|
|
||||||
</Space>
|
|
||||||
</Flex>
|
|
||||||
<Flex className="content-wrapper" gap="large">
|
|
||||||
<Card className="template-form">
|
|
||||||
<Flex vertical gap="middle">
|
|
||||||
<Upload.Dragger
|
|
||||||
showUploadList={false}
|
|
||||||
multiple={false}
|
|
||||||
beforeUpload={beforeUpload}
|
|
||||||
>
|
|
||||||
<p className="ant-upload-drag-icon">
|
|
||||||
{uploadedFile ? <CheckCircleFilled/> : <InboxOutlined/>}
|
|
||||||
</p>
|
|
||||||
<p className="ant-upload-text">
|
|
||||||
{uploadedFile ? uploadedFile.name : "点击或者将文件拖拽到这里进行上传"}
|
|
||||||
</p>
|
|
||||||
<p className="ant-upload-hint">
|
|
||||||
{uploadedFile ? "点击或者将文件拖拽到这里重新上传" :
|
|
||||||
<>
|
|
||||||
<p>在这里上传您的文件,让AI帮您进行解析</p>
|
|
||||||
<p>目前仅支持上传一个文件,支持.docx,.pdf类型</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</Upload.Dragger>
|
|
||||||
<Button size="large" type="primary" disabled={!uploadedFile || generating}
|
|
||||||
onClick={handleGenerationStart}
|
|
||||||
|
|
||||||
>开 始 AI 提 取</Button>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{startSending && (
|
|
||||||
<Flex className="preview-panel" vertical gap="middle">
|
|
||||||
<Flex className="preview-header" justify="space-between" gap="middle">
|
|
||||||
<Select
|
|
||||||
className="template-select"
|
|
||||||
size="large"
|
|
||||||
placeholder={generationMetadata ? "文件提取完毕,请选择模板" : "文件提取中,请稍后"}
|
|
||||||
options={templates}
|
|
||||||
disabled={!generationMetadata}
|
|
||||||
onChange={setSelectedTemplateId}
|
|
||||||
/>
|
|
||||||
<Button icon={<DownloadOutlined/>} type="primary" size="large" disabled={!selectedTemplateId}
|
|
||||||
onClick={handleTemplatePadding}
|
|
||||||
>下 载</Button>
|
|
||||||
</Flex>
|
|
||||||
<Card className="preview-content custom-scrollbar">
|
|
||||||
{messages ?
|
|
||||||
<ReactMarkdown
|
|
||||||
className="markdown-body custom-scrollbar"
|
|
||||||
components={markdownComponents}
|
|
||||||
>
|
|
||||||
{messages}
|
|
||||||
</ReactMarkdown>
|
|
||||||
: (
|
|
||||||
<>
|
|
||||||
<LoadingOutlined style={{fontSize: 48, marginBottom: 24}}/>
|
|
||||||
<Typography.Text>正在提取文件信息,请不要关闭或刷新页面</Typography.Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
81
src/pages/test1/index.tsx
Normal file
81
src/pages/test1/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { AgUiEventType, DifyAgent } from '@shangzy/ag-ui-dify'
|
||||||
|
import { RunAgentInput, EventType, TextMessageContentEvent } from '@ag-ui/client'
|
||||||
|
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
const Test1: React.FC = () => {
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
const runAgent = () => {
|
||||||
|
const difyJson = {
|
||||||
|
conversation_id: '',
|
||||||
|
files: [],
|
||||||
|
query: '1312313',
|
||||||
|
appKey: cacheGet('appKey'),
|
||||||
|
inputs: {
|
||||||
|
Token: cacheGet('token'),
|
||||||
|
tenantid: cacheGet('tenantId')
|
||||||
|
},
|
||||||
|
user: userInfo?.id || 'anonymous'
|
||||||
|
}
|
||||||
|
const content: string = JSON.stringify(difyJson)
|
||||||
|
// 准备输入参数
|
||||||
|
const input: RunAgentInput = {
|
||||||
|
threadId: new Date().getTime().toString(),
|
||||||
|
runId: new Date().getTime().toString(),
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: new Date().getTime().toString(),
|
||||||
|
role: 'user',
|
||||||
|
content: content
|
||||||
|
}
|
||||||
|
],
|
||||||
|
context: [],
|
||||||
|
tools: []
|
||||||
|
}
|
||||||
|
// 订阅Agent事件
|
||||||
|
new DifyAgent({
|
||||||
|
baseUrl: '/dify',
|
||||||
|
showMetadata: true
|
||||||
|
})
|
||||||
|
.run(input)
|
||||||
|
.subscribe({
|
||||||
|
next: (event: AgUiEventType) => {
|
||||||
|
console.log('🚀 ~ ChatApp ~ aa ~ event:', event)
|
||||||
|
try {
|
||||||
|
switch (event.type) {
|
||||||
|
case EventType.RUN_STARTED:
|
||||||
|
// 可以在这里处理运行开始事件
|
||||||
|
break
|
||||||
|
|
||||||
|
case EventType.TEXT_MESSAGE_START:
|
||||||
|
// 处理消息开始事件
|
||||||
|
break
|
||||||
|
|
||||||
|
case EventType.TEXT_MESSAGE_CONTENT:
|
||||||
|
const textEvent = event as TextMessageContentEvent
|
||||||
|
setMessage(prev => prev + textEvent.delta)
|
||||||
|
break
|
||||||
|
|
||||||
|
case EventType.TEXT_MESSAGE_END:
|
||||||
|
break
|
||||||
|
|
||||||
|
case EventType.RUN_FINISHED:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('处理事件时出错:', err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {},
|
||||||
|
complete: () => {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClick={() => runAgent()}>发起请求</div>
|
||||||
|
<div>{message}</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default Test1
|
||||||
158
src/pages/test2/index.tsx
Normal file
158
src/pages/test2/index.tsx
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
|
||||||
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
|
import { CopilotKit, useCopilotChatInternal as useCopilotChat } from '@copilotkit/react-core'
|
||||||
|
import { Flex, Mentions, Spin } from 'antd'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
|
const Chat: React.FC = () => {
|
||||||
|
const [currentAppKey, setCurrentAppKey] = useState<string>('')
|
||||||
|
const [newMessage, setNewMessage] = useState('')
|
||||||
|
const { messages, sendMessage, setMessages, isLoading, reloadMessages, stopGeneration } = useCopilotChat()
|
||||||
|
const callSendMessage = useCallback(
|
||||||
|
async (message: string) => {
|
||||||
|
await sendMessage({
|
||||||
|
id: new Date().getTime() + '',
|
||||||
|
role: 'user',
|
||||||
|
content: message
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[sendMessage]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSendMessage = useCallback(() => {
|
||||||
|
// 提前存好本次的提问内容,重新生成的话直接从缓存中获取之前的提问内容
|
||||||
|
let question: string = newMessage || ''
|
||||||
|
const token = cacheGet('token')
|
||||||
|
const tenantId = cacheGet('tenantId')
|
||||||
|
let conversation_id = ''
|
||||||
|
if (messages[1]?.id) {
|
||||||
|
conversation_id = messages[1]?.id.split('_')[0]
|
||||||
|
}
|
||||||
|
const userInfo = getUserInfo()
|
||||||
|
const difyJson = {
|
||||||
|
inputs: {
|
||||||
|
Token: token || '',
|
||||||
|
tenantid: tenantId || '',
|
||||||
|
query: question
|
||||||
|
},
|
||||||
|
appKey: currentAppKey,
|
||||||
|
files: [],
|
||||||
|
user: userInfo?.id || 'anonymous',
|
||||||
|
query: question,
|
||||||
|
conversation_id
|
||||||
|
}
|
||||||
|
// 设置好目前状态下的聊天列表数据,包含之前已经结束的沟通内容,以及本次用户的提问,本次AI的回答占位
|
||||||
|
callSendMessage(JSON.stringify(difyJson))
|
||||||
|
setNewMessage('')
|
||||||
|
}, [callSendMessage, newMessage, currentAppKey])
|
||||||
|
|
||||||
|
const handleParse = (jsonStr: string) => {
|
||||||
|
let res = ''
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(jsonStr)
|
||||||
|
res = parsed?.query
|
||||||
|
} catch (e) {}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// 换行
|
||||||
|
const handleNewline = () => {
|
||||||
|
setNewMessage(prevValue => `${prevValue}\n`)
|
||||||
|
}
|
||||||
|
const handleKeyDown = async (e: any) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const res = await cacheGet('sendMessage')
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
if (newMessage) {
|
||||||
|
if (res === 'ctrlEnter') {
|
||||||
|
handleSendMessage()
|
||||||
|
} else {
|
||||||
|
handleNewline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (res === 'Enter' || !res) {
|
||||||
|
if (newMessage) {
|
||||||
|
handleSendMessage()
|
||||||
|
} else {
|
||||||
|
setNewMessage('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{messages.map((message, index) => {
|
||||||
|
const userMessageId = message.id
|
||||||
|
// 实际的会话消息
|
||||||
|
return (
|
||||||
|
<Flex key={index} vertical data-message-id={userMessageId}>
|
||||||
|
{/* 用户提问 */}
|
||||||
|
{message.role === 'user' && <Flex key={message.id}>{handleParse(message.content as string) ?? ''}</Flex>}
|
||||||
|
|
||||||
|
{message.role === 'assistant' && (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 'calc(100% - 20px)',
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex>
|
||||||
|
{isLoading && !message.content && index === messages.length - 1 && (
|
||||||
|
<Flex>
|
||||||
|
<Spin indicator={<LoadingOutlined />} />
|
||||||
|
<span>检索中</span>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{message.content ?? ''}
|
||||||
|
</Flex>
|
||||||
|
{message?.generativeUI?.()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<div className='p-4'>
|
||||||
|
<Mentions
|
||||||
|
autoFocus
|
||||||
|
open={false}
|
||||||
|
placeholder='请输入内容'
|
||||||
|
rows={4}
|
||||||
|
value={newMessage}
|
||||||
|
maxLength={10000}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
options={[]}
|
||||||
|
onInput={e => {
|
||||||
|
const value = (e.target as HTMLInputElement).value
|
||||||
|
// 检查内容是否只包含空格或回车符
|
||||||
|
if (/^\s*$/.test(value)) {
|
||||||
|
setNewMessage('') // 如果只包含空格或回车符,清空输入框
|
||||||
|
} else {
|
||||||
|
setNewMessage(value) // 否则更新输入内容
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Test2: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<CopilotKit
|
||||||
|
runtimeUrl='/agui-api/copilotkit/dify'
|
||||||
|
showDevConsole={false}
|
||||||
|
// publicApiKey={'ck_pub_cc922145a5da9b8513bc10df473cd6f7'}
|
||||||
|
agent='agentic_chat_metadata'
|
||||||
|
>
|
||||||
|
<Chat />
|
||||||
|
</CopilotKit>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Test2
|
||||||
216
src/pages/zh-en-translator/index.tsx
Normal file
216
src/pages/zh-en-translator/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Button, Card, Form, Input, message, Space } from 'antd'
|
||||||
|
import { PageHeader } from '@ant-design/pro-components'
|
||||||
|
import { translateChineseToEnglish } from '@/api/zh-en-translator'
|
||||||
|
|
||||||
|
const ZhEnTranslator = () => {
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [translationResult, setTranslationResult] = useState('')
|
||||||
|
const [currentInput, setCurrentInput] = useState('')
|
||||||
|
const maxChars = 5000
|
||||||
|
|
||||||
|
const handleTranslate = async () => {
|
||||||
|
try {
|
||||||
|
const source_content = form.getFieldValue('source_content') || ''
|
||||||
|
if (!source_content.trim()) {
|
||||||
|
message.warning('请输入要翻译的中文内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setTranslationResult('')
|
||||||
|
|
||||||
|
const response = await translateChineseToEnglish(source_content)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
throw new Error(`翻译请求失败: ${response.status} ${errorText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error('响应体为空')
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = response.body.getReader()
|
||||||
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
let buffer = ''
|
||||||
|
let fullContent = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
const lines = buffer.split('\n')
|
||||||
|
buffer = lines.pop() || ''
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim()
|
||||||
|
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
|
||||||
|
if (trimmedLine === 'data: [DONE]') {
|
||||||
|
message.success('翻译完成')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedLine.startsWith('data: ')) {
|
||||||
|
try {
|
||||||
|
const data = trimmedLine.slice(6)
|
||||||
|
const parsed = JSON.parse(data)
|
||||||
|
|
||||||
|
if (parsed.event === 'message' && parsed.answer) {
|
||||||
|
fullContent += parsed.answer
|
||||||
|
setTranslationResult(fullContent)
|
||||||
|
} else if (parsed.event === 'error') {
|
||||||
|
throw new Error(parsed.message || 'Dify API 返回错误')
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn('跳过无法解析的行:', trimmedLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reader.releaseLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullContent) {
|
||||||
|
message.success('翻译完成')
|
||||||
|
} else {
|
||||||
|
throw new Error('未收到翻译结果')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('翻译错误:', error)
|
||||||
|
message.error(error instanceof Error ? error.message : '翻译失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
form.resetFields()
|
||||||
|
setTranslationResult('')
|
||||||
|
setCurrentInput('')
|
||||||
|
message.info('已清空内容')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
if (!translationResult) {
|
||||||
|
message.warning('没有可复制的内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(translationResult)
|
||||||
|
message.success('复制成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制失败:', error)
|
||||||
|
message.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px' }}>
|
||||||
|
<PageHeader
|
||||||
|
title="中文转英文翻译助手"
|
||||||
|
subTitle="专业的中文到英文文本翻译工具,支持流式输出,翻译结果地道自然"
|
||||||
|
style={{ marginBottom: '30px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||||
|
<Card title="参数输入区">
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="source_content"
|
||||||
|
label={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||||
|
<span>中文原文</span>
|
||||||
|
<span style={{ color: currentInput.length > maxChars ? '#ff4d4f' : '#999' }}>
|
||||||
|
{currentInput.length} / {maxChars}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入要翻译的中文内容' },
|
||||||
|
{ max: maxChars, message: `输入内容不能超过${maxChars}个字符` }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="请输入需要翻译的中文内容(最多5000个字符)"
|
||||||
|
rows={8}
|
||||||
|
value={currentInput}
|
||||||
|
onChange={(e) => setCurrentInput(e.target.value)}
|
||||||
|
showCount
|
||||||
|
maxLength={maxChars}
|
||||||
|
style={{ fontSize: '14px' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Space style={{ width: '100%', justifyContent: 'center' }} size="middle">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={handleTranslate}
|
||||||
|
loading={loading}
|
||||||
|
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||||||
|
>
|
||||||
|
翻译
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
onClick={handleClear}
|
||||||
|
disabled={loading}
|
||||||
|
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="翻译结果"
|
||||||
|
extra={
|
||||||
|
translationResult && (
|
||||||
|
<Button onClick={handleCopy} size="small">
|
||||||
|
复制
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: '150px',
|
||||||
|
maxHeight: '400px',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '15px',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '1.6'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
|
||||||
|
正在翻译中,请稍候...
|
||||||
|
</div>
|
||||||
|
) : translationResult || (
|
||||||
|
<div style={{ textAlign: 'center', color: '#999' }}>
|
||||||
|
翻译结果将在这里显示
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ZhEnTranslator
|
||||||
@@ -1,22 +1,59 @@
|
|||||||
/** 配置管理路由主文件 */
|
import { Spin } from 'antd'
|
||||||
import {createHashRouter} from 'react-router-dom'
|
import { FC, ReactNode, Suspense, lazy } from 'react'
|
||||||
import {FrontEndCodeInterpreter} from '../pages/frontend-code-interpreter'
|
import { Navigate, RouteObject } from 'react-router-dom'
|
||||||
import {TemplateRenderer} from "../pages/template-renderer";
|
import { createHashRouter } from 'react-router-dom'
|
||||||
|
|
||||||
/** 配置管理路由表 */
|
const Loading = () => (
|
||||||
const routers = createHashRouter([
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size='large' />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const LazyLoad = (Component: FC): ReactNode => {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<Component />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const router: RouteObject[] = [
|
||||||
{
|
{
|
||||||
path: '/interpreter',
|
path: '/',
|
||||||
element: <FrontEndCodeInterpreter></FrontEndCodeInterpreter>
|
children: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: LazyLoad(lazy(() => import('@/pages/home')))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/template-render',
|
path: '/test1',
|
||||||
element: <TemplateRenderer></TemplateRenderer>
|
element: LazyLoad(lazy(() => import('@/pages/test1')))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/not-found',
|
path: '/test2',
|
||||||
element: <></>
|
element: LazyLoad(lazy(() => import('@/pages/test2')))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/zh-en-translator',
|
||||||
|
element: LazyLoad(lazy(() => import('@/pages/zh-en-translator')))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
element: <>404</>
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <Navigate to='/404' />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
export default routers
|
export default createHashRouter(router)
|
||||||
|
|||||||
43
src/utils/cacheUtil.ts
Normal file
43
src/utils/cacheUtil.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
export function cacheGet(key: string): string {
|
||||||
|
return localStorage.getItem(key) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cacheSet(key: string, value: string) {
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cacheClear(key: string) {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cacheDeleteAll() {
|
||||||
|
localStorage.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
id?: string
|
||||||
|
avatar?: string
|
||||||
|
gender?: string
|
||||||
|
nickName?: string
|
||||||
|
username?: string
|
||||||
|
position?: string
|
||||||
|
deptName?: string
|
||||||
|
mobile?: string
|
||||||
|
bizMail?: string
|
||||||
|
email?: string
|
||||||
|
corpName?: string
|
||||||
|
effectivePoint?: string
|
||||||
|
totalPoint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfo(): UserInfo | null {
|
||||||
|
const res = localStorage.getItem('userInfo')
|
||||||
|
if (!res) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return JSON.parse(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function getToken() {
|
||||||
|
return localStorage.getItem('token')
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import {WebContainer} from "@webcontainer/api";
|
|
||||||
import {Terminal} from "@xterm/xterm";
|
|
||||||
|
|
||||||
export async function installDependencies(instance: WebContainer, terminal: Terminal) {
|
|
||||||
const installProcess = await instance.spawn('npm', ['install']);
|
|
||||||
terminal.write("依赖安装中...");
|
|
||||||
installProcess.output.pipeTo(new WritableStream({
|
|
||||||
write(data) {
|
|
||||||
terminal.write(data);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
const exitCode = await installProcess.exit;
|
|
||||||
if (exitCode == 0) {
|
|
||||||
terminal.write("依赖安装完毕,准备运行\n");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
terminal.write(`依赖安装失败,报错信息如上`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function startDevServer(instance: WebContainer, terminal: Terminal, callback: (url: string) => void) {
|
|
||||||
instance.on('server-ready', (_port, url) => {
|
|
||||||
terminal.write("服务启动完毕,即将进行加载");
|
|
||||||
setTimeout(() => {
|
|
||||||
callback(url);
|
|
||||||
}, 500)
|
|
||||||
});
|
|
||||||
|
|
||||||
terminal.write("启动服务中...");
|
|
||||||
const serverProcess = await instance.spawn('npm', ['run', 'start']);
|
|
||||||
serverProcess.output.pipeTo(
|
|
||||||
new WritableStream({
|
|
||||||
write(data) {
|
|
||||||
terminal.write(data);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const exitCode = await serverProcess.exit;
|
|
||||||
if (exitCode != 0) {
|
|
||||||
terminal.write(`服务启动失败,报错信息如上`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function writeIndexJS(instance: WebContainer, content: string) {
|
|
||||||
await instance.fs.writeFile('/index.js', content);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
export function extractJSONFromString(text: string) {
|
|
||||||
try {
|
|
||||||
// 首先尝试匹配 ```json``` 格式
|
|
||||||
const jsonBlockPattern = /```(?:json|JSON)\s*\n?(.*?)\s*```/s;
|
|
||||||
let match = text.trim().match(jsonBlockPattern);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
return match[1].trim();
|
|
||||||
} else {
|
|
||||||
// 如果没有找到```json```标记,尝试直接匹配JSON对象
|
|
||||||
const jsonPattern = /{.*}/s;
|
|
||||||
match = text.match(jsonPattern);
|
|
||||||
if (match) {
|
|
||||||
return match[0].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`发生错误:${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
src/utils/polyfills.ts
Normal file
8
src/utils/polyfills.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
if (!Object.hasOwn) {
|
||||||
|
Object.defineProperty(Object, 'hasOwn', {
|
||||||
|
value: function(obj: any, prop: string): boolean {
|
||||||
|
return Object.prototype.hasOwnProperty.call(obj, prop)
|
||||||
|
},
|
||||||
|
configurable: true
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
const VITE_AI_API_BASE = import.meta.env['VITE_AI_API_BASE'] || "";
|
|
||||||
|
|
||||||
|
|
||||||
export type SSERequest = {
|
|
||||||
type: "chat" | "completion";
|
|
||||||
query: string;
|
|
||||||
inputs?: Record<string, string>;
|
|
||||||
conversationId?: string;
|
|
||||||
user: string;
|
|
||||||
files?: object[];
|
|
||||||
responseMode?: "streaming" | "blocking";
|
|
||||||
onMessage: (message: string | null, finished: boolean) => void;
|
|
||||||
onError: (error: Error) => void;
|
|
||||||
onFinish: (metadata: object) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SSEClient {
|
|
||||||
private readonly token: string;
|
|
||||||
private controller: AbortController | null;
|
|
||||||
|
|
||||||
constructor(token: string) {
|
|
||||||
this.controller = null;
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送聊天消息并处理 SSE 响应
|
|
||||||
async sendMessage({
|
|
||||||
type = "chat",
|
|
||||||
query,
|
|
||||||
inputs = {},
|
|
||||||
conversationId = '',
|
|
||||||
user,
|
|
||||||
files = [],
|
|
||||||
responseMode = 'streaming',
|
|
||||||
onMessage,
|
|
||||||
onError,
|
|
||||||
onFinish
|
|
||||||
}: SSERequest) {
|
|
||||||
try {
|
|
||||||
// 创建 AbortController 用于取消请求
|
|
||||||
this.controller = new AbortController();
|
|
||||||
|
|
||||||
// 准备请求数据
|
|
||||||
const requestData = {
|
|
||||||
query,
|
|
||||||
inputs,
|
|
||||||
response_mode: responseMode,
|
|
||||||
conversation_id: conversationId,
|
|
||||||
user,
|
|
||||||
files
|
|
||||||
};
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
const response = await fetch(`${VITE_AI_API_BASE}/${type === "chat" ? "chat-messages" : "completions"}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${this.token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestData),
|
|
||||||
signal: this.controller.signal
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response === null || response.body === null) throw new Error(`HTTP error!`);
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
|
|
||||||
|
|
||||||
// 获取响应的 ReadableStream
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const {done, value} = await reader.read();
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码二进制数据
|
|
||||||
buffer += decoder.decode(value, {stream: true});
|
|
||||||
|
|
||||||
// 处理完整的 SSE 消息
|
|
||||||
const messages = buffer.split('\n\n');
|
|
||||||
buffer = messages.pop() || ''; // 保留最后一个不完整的消息
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
if (!message.trim() || !message.startsWith('data: ')) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 解析 SSE 消息
|
|
||||||
const data = JSON.parse(message.slice(6));
|
|
||||||
|
|
||||||
// 根据不同的事件类型处理消息
|
|
||||||
switch (data.event) {
|
|
||||||
case 'message':
|
|
||||||
onMessage?.(data.answer, false);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'message_end':
|
|
||||||
onMessage?.(null, true);
|
|
||||||
onFinish?.(data.metadata);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'error':
|
|
||||||
onError?.(new Error(data.message));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'workflow_started':
|
|
||||||
case 'node_started':
|
|
||||||
case 'node_finished':
|
|
||||||
case 'workflow_finished':
|
|
||||||
// 处理工作流相关事件
|
|
||||||
console.log(`Workflow event: ${data.event}`, data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tts_message':
|
|
||||||
// 处理语音合成消息
|
|
||||||
if (data.audio) {
|
|
||||||
// 处理 base64 编码的音频数据
|
|
||||||
console.log('Received TTS audio chunk');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tts_message_end':
|
|
||||||
console.log('TTS streaming completed');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing SSE message:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
onError?.(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消请求
|
|
||||||
cancel() {
|
|
||||||
if (this.controller) {
|
|
||||||
this.controller.abort();
|
|
||||||
this.controller = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1 +1,2 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
declare module '@tailwindcss/vite'
|
||||||
5
tailwind.config.js
Normal file
5
tailwind.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
darkMode: 'class',
|
||||||
|
plugins: []
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2022",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
@@ -20,7 +20,10 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src", "src/api/.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,11 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
import {defineConfig, loadEnv} from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig(({mode}) => {
|
export default defineConfig(() => {
|
||||||
const env = loadEnv(mode, process.cwd());
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [react()],
|
base: './',
|
||||||
server: {
|
plugins: [tailwindcss(), react()],
|
||||||
headers: {
|
resolve: {
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
alias: {
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
proxy: {
|
optimizeDeps: {
|
||||||
"/api": {
|
include: ['dayjs/locale/zh-cn']
|
||||||
target: env.VITE_API_BASE,
|
},
|
||||||
changeOrigin: true,
|
server: {
|
||||||
ws: true,
|
host: '0.0.0.0',
|
||||||
toProxy: true,
|
allowedHosts: true as true,
|
||||||
rewrite: (path: string) => path.replace(new RegExp(`^/api`), '')
|
cors: true,
|
||||||
|
open: true,
|
||||||
|
port: 5173
|
||||||
|
},
|
||||||
|
// 生产预览服务器配置 (vite preview) - 仅用于本地测试
|
||||||
|
preview: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 3000,
|
||||||
|
cors: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user