Initial commit

This commit is contained in:
2026-01-07 14:39:50 +08:00
commit 459c49ec41
30 changed files with 10501 additions and 0 deletions

12
.env.development Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View 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

File diff suppressed because it is too large Load Diff

33
projects/express-app.ts Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

87
src/api/template.ts Normal file
View 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
View 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
View 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}/>)

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

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

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

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

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

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

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

@@ -0,0 +1 @@
/// <reference types="vite/client" />

26
tsconfig.app.json Normal file
View 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
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
tsconfig.node.json Normal file
View 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
View 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`), '')
}
}
}
}
})