Initial commit
This commit is contained in:
12
.env.development
Normal file
12
.env.development
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 后端请求地址
|
||||||
|
VITE_API_BASE=http://192.168.113.18:8090/api
|
||||||
|
# 服务前缀
|
||||||
|
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
|
||||||
|
|
||||||
|
# AI后端请求地址
|
||||||
|
VITE_AI_API_BASE=http://192.168.213.176/v1
|
||||||
|
VITE_AI_CHAT_SECRET=app-8WsQjtYZYzOH9bRSFK8MCmaw
|
||||||
12
.env.production
Normal file
12
.env.production
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# 后端请求地址
|
||||||
|
VITE_API_BASE=https://copilot.sino-bridge.com/api
|
||||||
|
# 服务前缀
|
||||||
|
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
|
||||||
|
|
||||||
|
# AI后端请求地址
|
||||||
|
VITE_AI_API_BASE=https://copilot.sino-bridge.com:90/v1
|
||||||
|
VITE_TEMPLATE_AGENT_TOKEN=app-FsEkXKzg41OZ0TCK9ywFmbFK
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
50
README.md
Normal file
50
README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.png"/>
|
||||||
|
<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>
|
||||||
4806
package-lock.json
generated
Normal file
4806
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "webcontainers-express-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.2",
|
||||||
|
"@webcontainer/api": "1.5.1-internal.5",
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"antd": "^5.23.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"less": "^4.2.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-markdown": "^9.0.3",
|
||||||
|
"react-router-dom": "^6.21.1",
|
||||||
|
"react-syntax-highlighter": "^15.6.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"@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"
|
||||||
|
}
|
||||||
|
}
|
||||||
3690
pnpm-lock.yaml
generated
Normal file
3690
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
projects/express-app.ts
Normal file
33
projects/express-app.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
361
projects/react-app.ts
Normal file
361
projects/react-app.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
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()],
|
||||||
|
})`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
147
public/loading.html
Normal file
147
public/loading.html
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<!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>
|
||||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
87
src/api/template.ts
Normal file
87
src/api/template.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
55
src/index.less
Normal file
55
src/index.less
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
* {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/main.tsx
Normal file
6
src/main.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import {createRoot} from 'react-dom/client'
|
||||||
|
import {RouterProvider} from 'react-router-dom'
|
||||||
|
import routers from './router'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(<RouterProvider router={routers}/>)
|
||||||
69
src/pages/frontend-code-interpreter/index.less
Normal file
69
src/pages/frontend-code-interpreter/index.less
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/pages/frontend-code-interpreter/index.tsx
Normal file
131
src/pages/frontend-code-interpreter/index.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
251
src/pages/frontend-code-interpreter/xterm.less
Normal file
251
src/pages/frontend-code-interpreter/xterm.less
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
59
src/pages/template-renderer/index.less
Normal file
59
src/pages/template-renderer/index.less
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
303
src/pages/template-renderer/index.tsx
Normal file
303
src/pages/template-renderer/index.tsx
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/** 模板生成工具 */
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
src/router/index.tsx
Normal file
22
src/router/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/** 配置管理路由主文件 */
|
||||||
|
import {createHashRouter} from 'react-router-dom'
|
||||||
|
import {FrontEndCodeInterpreter} from '../pages/frontend-code-interpreter'
|
||||||
|
import {TemplateRenderer} from "../pages/template-renderer";
|
||||||
|
|
||||||
|
/** 配置管理路由表 */
|
||||||
|
const routers = createHashRouter([
|
||||||
|
{
|
||||||
|
path: '/interpreter',
|
||||||
|
element: <FrontEndCodeInterpreter></FrontEndCodeInterpreter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/template-render',
|
||||||
|
element: <TemplateRenderer></TemplateRenderer>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/not-found',
|
||||||
|
element: <></>
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
export default routers
|
||||||
49
src/utils/dev-operations.ts
Normal file
49
src/utils/dev-operations.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
21
src/utils/json-extractor.ts
Normal file
21
src/utils/json-extractor.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/utils/sse-client.ts
Normal file
147
src/utils/sse-client.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
26
tsconfig.app.json
Normal file
26
tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
24
tsconfig.node.json
Normal file
24
tsconfig.node.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
27
vite.config.ts
Normal file
27
vite.config.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {defineConfig, loadEnv} from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig(({mode}) => {
|
||||||
|
const env = loadEnv(mode, process.cwd());
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: env.VITE_API_BASE,
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
toProxy: true,
|
||||||
|
rewrite: (path: string) => path.replace(new RegExp(`^/api`), '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user