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:
2026-01-07 15:08:02 +08:00
parent 459c49ec41
commit 026e33a08a
42 changed files with 8570 additions and 6980 deletions

View File

@@ -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

View File

@@ -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
View File

@@ -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
View 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
View 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"]

View File

@@ -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,
},
})
```

View File

@@ -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 },
],
},
},
)

View File

@@ -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>

5358
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
} }
} }

5992
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}`,
},
},
};

View File

@@ -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()],
})`
}
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View 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())
}

View File

@@ -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()
}

View 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
View 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;
}

View File

@@ -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;
}
}

View File

@@ -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>
)

View File

@@ -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;
}
}
}

View File

@@ -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>
</>
)
}

View File

@@ -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
View 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

View File

@@ -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;
}
}

View File

@@ -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
View 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
View 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

View 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

View File

@@ -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: '/test1',
element: LazyLoad(lazy(() => import('@/pages/test1')))
},
{
path: '/test2',
element: LazyLoad(lazy(() => import('@/pages/test2')))
},
{
path: '/zh-en-translator',
element: LazyLoad(lazy(() => import('@/pages/zh-en-translator')))
},
{
path: '/404',
element: <>404</>
}
]
}, },
{ {
path: '/template-render', path: '*',
element: <TemplateRenderer></TemplateRenderer> element: <Navigate to='/404' />
},
{
path: '/not-found',
element: <></>
} }
]) ]
export default routers export default createHashRouter(router)

43
src/utils/cacheUtil.ts Normal file
View 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')
}

View File

@@ -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);
}

View File

@@ -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
View 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
})
}

View File

@@ -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
View File

@@ -1 +1,2 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '@tailwindcss/vite'

5
tailwind.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
darkMode: 'class',
plugins: []
}

View File

@@ -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"]
} }

View File

@@ -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"]
} }

View File

@@ -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: {
"/api": {
target: env.VITE_API_BASE,
changeOrigin: true,
ws: true,
toProxy: true,
rewrite: (path: string) => path.replace(new RegExp(`^/api`), '')
}
} }
},
optimizeDeps: {
include: ['dayjs/locale/zh-cn']
},
server: {
host: '0.0.0.0',
allowedHosts: true as true,
cors: true,
open: true,
port: 5173
},
// 生产预览服务器配置 (vite preview) - 仅用于本地测试
preview: {
host: '0.0.0.0',
port: 3000,
cors: true
} }
} }
}) })