Initial commit: Copilot Toolbox template project
This commit is contained in:
18
.env.development
Normal file
18
.env.development
Normal file
@@ -0,0 +1,18 @@
|
||||
# 网站标题
|
||||
VITE_DOCUMENT_TITLE='AI办公助手'
|
||||
|
||||
# 服务前缀
|
||||
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
|
||||
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
|
||||
VITE_API_BASE_NOTE=/langwell-api/langwell-notes-server
|
||||
VITE_API_BASE_PUB=/langwell-api/langwell-pub-server
|
||||
VITE_API_BASE_INS=/langwell-api/langwell-ins-server
|
||||
VITE_API_BASE_TOOL=/copilot-tool
|
||||
VITE_API_BASE_LAMP=/lamp-api
|
||||
VITE_DIRECTION_API = /direction-api
|
||||
# 语音识别
|
||||
VITE_API_BASE_VOICE=/voice-api
|
||||
# AI后端请求地址
|
||||
VITE_AI_API_BASE=/v1
|
||||
# Xiren-Lite SDK 配置 - 开发环境使用代理
|
||||
VITE_XIREN_LITE_BASE_URL=/xiren-api
|
||||
18
.env.production
Normal file
18
.env.production
Normal file
@@ -0,0 +1,18 @@
|
||||
# 网站标题
|
||||
VITE_DOCUMENT_TITLE='AI办公助手'
|
||||
|
||||
# 服务前缀
|
||||
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
|
||||
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
|
||||
VITE_API_BASE_NOTE=/langwell-api/langwell-notes-server
|
||||
VITE_API_BASE_PUB=/langwell-api/langwell-pub-server
|
||||
VITE_API_BASE_INS=/langwell-api/langwell-ins-server
|
||||
VITE_API_BASE_TOOL=/copilot-tool
|
||||
VITE_API_BASE_LAMP=/lamp-api
|
||||
VITE_DIRECTION_API = /direction-api
|
||||
# 语音识别
|
||||
VITE_API_BASE_VOICE=/voice-api
|
||||
# AI后端请求地址
|
||||
VITE_AI_API_BASE=/v1
|
||||
# Xiren-Lite SDK 配置 - 开发环境使用代理
|
||||
VITE_XIREN_LITE_BASE_URL=/xiren-api
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.history
|
||||
# Editor directories and files
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
dist.zip
|
||||
copilot-toolbox.zip
|
||||
copilot-toolbox/
|
||||
.trae
|
||||
87
CLAUDE.md
Normal file
87
CLAUDE.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Copilot Toolbox is a React + TypeScript + Vite frontend template for building modern AI-powered applications. Integrates Dify AI, CopilotKit with Ant Design and Tailwind CSS.
|
||||
|
||||
**This is a template project - all development must extend this project, not create new repositories.**
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- React 18 + TypeScript + Vite 6
|
||||
- Ant Design 5.x (UI components)
|
||||
- Tailwind CSS 4.x + Less (styling)
|
||||
- React Router DOM 6.x (Hash mode routing)
|
||||
- @copilotkit/react-core, @ag-ui/client, @shangzy/ag-ui-dify (AI integration)
|
||||
|
||||
## Directory Conventions
|
||||
|
||||
| Type | Path Pattern | Example |
|
||||
|------|--------------|---------|
|
||||
| Pages | `src/pages/[name]/index.tsx` | `src/pages/home/index.tsx` |
|
||||
| API | `src/api/[name].ts` | `src/api/user.ts` |
|
||||
| Components | `src/components/[name]/index.tsx` | `src/components/Header/index.tsx` |
|
||||
| Utils | `src/utils/[name].ts` | `src/utils/cacheUtil.ts` |
|
||||
|
||||
- Use `@/` alias for imports (configured in vite.config.ts)
|
||||
- TypeScript strict mode enabled
|
||||
- Tailwind CSS for styling, Ant Design for components
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
pnpm install # Install dependencies
|
||||
pnpm dev # Dev server (port 5173)
|
||||
pnpm build # Production build
|
||||
pnpm preview # Preview production build (port 3000)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All API base paths use proxy prefixes. Set in `.env.development` or `.env.production`:
|
||||
|
||||
```
|
||||
VITE_API_BASE_AI=/langwell-api/langwell-ai-server
|
||||
VITE_API_BASE_DOC=/langwell-api/langwell-doc-server
|
||||
VITE_API_BASE_LAMP=/lamp-api
|
||||
VITE_AI_API_BASE=/v1
|
||||
```
|
||||
|
||||
## API Pattern
|
||||
|
||||
```typescript
|
||||
// src/api/[name].ts
|
||||
export function getData(params: { id: string }) {
|
||||
return fetch(`${import.meta.env.VITE_API_BASE_AI}/endpoint`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(res => res.json())
|
||||
}
|
||||
```
|
||||
|
||||
## Routing
|
||||
|
||||
Routes defined in `src/router/index.tsx` using lazy loading:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: '/page-name',
|
||||
element: LazyLoad(lazy(() => import('@/pages/page-name')))
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Features
|
||||
|
||||
1. Create page: `src/pages/[name]/index.tsx`
|
||||
2. Create API: `src/api/[name].ts`
|
||||
3. Add route: `src/router/index.tsx`
|
||||
4. Use `@/` alias for imports
|
||||
|
||||
## Restrictions
|
||||
|
||||
- Do NOT create new repositories - extend this project
|
||||
- Do NOT remove CLAUDE.md
|
||||
- Use `@/` alias, not relative paths
|
||||
- TypeScript strict mode cannot be disabled
|
||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# 配置阿里云 npm 镜像
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装所有依赖(包括 devDependencies)
|
||||
RUN npm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 运行开发服务器
|
||||
CMD ["npm", "run", "dev", "--", "--port", "3000"]
|
||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
container_name: copilot-toolbox-dev
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "3000"
|
||||
25
index.html
Normal file
25
index.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<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" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<meta http-equiv="Expires" content="0">
|
||||
<title>AI办公助手</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script>
|
||||
window.iFrameResizer = {
|
||||
heightCalculationMethod: 'lowestElement'
|
||||
}
|
||||
</script>
|
||||
<script src="/iframeResizer.contentWindow.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
5696
package-lock.json
generated
Normal file
5696
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "copilot-toolbox",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
"build": "vite build --mode production",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ag-ui/client": "^0.0.42",
|
||||
"@ant-design/icons": "^5.5.2",
|
||||
"@ant-design/pro-components": "^2.8.10",
|
||||
"@copilotkit/react-core": "1.10.6",
|
||||
"@shangzy/ag-ui-dify": "^0.1.0",
|
||||
"antd": "^5.23.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"less": "^4.2.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.0.12",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"path": "^0.12.7",
|
||||
"prettier-plugin-tailwindcss": "^0.1.7",
|
||||
"tailwindcss": "4.0.12",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.0.7"
|
||||
}
|
||||
}
|
||||
3968
pnpm-lock.yaml
generated
Normal file
3968
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1311
public/iframeResizer.contentWindow.js
Normal file
1311
public/iframeResizer.contentWindow.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 183 KiB |
44
src/App.tsx
Normal file
44
src/App.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import router from '@/router'
|
||||
import { cacheClear, cacheSet } from '@/utils/cacheUtil'
|
||||
import { getAgentBaseInfo, getUserInfoById } from '@/api/common'
|
||||
|
||||
function App() {
|
||||
const queryString = window.location.search
|
||||
const searchParams = new URLSearchParams(queryString)
|
||||
const tenantId = searchParams.get('tenantId') || ''
|
||||
const token = searchParams.get('token') || ''
|
||||
|
||||
cacheSet('tenantId', tenantId)
|
||||
cacheSet('token', token)
|
||||
useEffect(() => {
|
||||
const agentId = searchParams.get('agentId') || ''
|
||||
|
||||
if (agentId) {
|
||||
getAgentBaseInfo({ id: agentId }).then(res => {
|
||||
cacheSet('appKey', res.data.appKey)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId && token) {
|
||||
getUserInfoById().then(response => {
|
||||
if (response.code === 200 && response.data) {
|
||||
cacheSet('userInfo', JSON.stringify(response.data))
|
||||
}
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
cacheClear('userInfo')
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<RouterProvider router={router} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
28
src/api/common.ts
Normal file
28
src/api/common.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { cacheGet } from '@/utils/cacheUtil'
|
||||
|
||||
export function getAgentBaseInfo(params: { id: string }) {
|
||||
const Tenantid = cacheGet('tenantId')
|
||||
const Token = cacheGet('token')
|
||||
return fetch(`${import.meta.env.VITE_API_BASE_AI}/agent/baseInfo/info?id=${params.id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Tenantid,
|
||||
Token
|
||||
}
|
||||
}).then(res => res.json())
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export async function getUserInfoById() {
|
||||
const Tenantid = cacheGet('tenantId')
|
||||
const Token = cacheGet('token')
|
||||
return fetch(`${import.meta.env['VITE_API_BASE_LAMP']}/oauth/anyone/getUserInfoById`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Tenantid,
|
||||
Token
|
||||
}
|
||||
}).then(res => res.json())
|
||||
}
|
||||
34
src/api/zh-en-translator.ts
Normal file
34
src/api/zh-en-translator.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface TranslationRequest {
|
||||
source_content: string
|
||||
}
|
||||
|
||||
export interface DifyRequest {
|
||||
inputs: {
|
||||
prompt: string
|
||||
}
|
||||
query: string
|
||||
response_mode: string
|
||||
}
|
||||
|
||||
export function translateChineseToEnglish(source_content: string) {
|
||||
const prompt = `你是一个专业的中英翻译专家。请将下面的中文文本翻译成自然、地道、专业的英文,只返回翻译后的英文内容,不要添加任何额外的解释或格式。待翻译的中文内容是:
|
||||
|
||||
${source_content}`
|
||||
|
||||
const requestBody: DifyRequest = {
|
||||
inputs: {
|
||||
prompt
|
||||
},
|
||||
query: '1',
|
||||
response_mode: 'streaming'
|
||||
}
|
||||
|
||||
return fetch('https://copilot.sino-bridge.com/v1/chat-messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer app-Y6ekYkw3aoUV3jmfZdg24Adh'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
}
|
||||
29
src/index.css
Normal file
29
src/index.css
Normal file
@@ -0,0 +1,29 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
.route-loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 120px;
|
||||
height: 22px;
|
||||
color: #514b82;
|
||||
border: 2px solid;
|
||||
border-radius: 20px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes l6 {
|
||||
100% {
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.route-loading::before {
|
||||
position: absolute;
|
||||
inset: 0 100% 0 0;
|
||||
margin: 2px;
|
||||
content: '';
|
||||
background: currentcolor;
|
||||
border-radius: inherit;
|
||||
animation: l6 2s infinite;
|
||||
}
|
||||
12
src/main.tsx
Normal file
12
src/main.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import './utils/polyfills'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/es/locale/zh_CN'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
)
|
||||
57
src/pages/home/index.tsx
Normal file
57
src/pages/home/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Card, Row, Col, Typography, Space } from 'antd'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { RobotOutlined, TranslationOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
|
||||
const { Title, Paragraph } = Typography
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: '测试页面 1',
|
||||
description: 'Dify AI Agent 集成示例',
|
||||
icon: <RobotOutlined style={{ fontSize: 32, color: '#1890ff' }} />,
|
||||
link: '/test1'
|
||||
},
|
||||
{
|
||||
title: '测试页面 2',
|
||||
description: '功能开发中...',
|
||||
icon: <FileTextOutlined style={{ fontSize: 32, color: '#52c41a' }} />,
|
||||
link: '/test2'
|
||||
},
|
||||
{
|
||||
title: '中英翻译器',
|
||||
description: 'AI 驱动的翻译工具',
|
||||
icon: <TranslationOutlined style={{ fontSize: 32, color: '#722ed1' }} />,
|
||||
link: '/zh-en-translator'
|
||||
}
|
||||
]
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Title level={2}>欢迎使用 AI 办公助手</Title>
|
||||
<Paragraph style={{ marginBottom: 32 }}>
|
||||
选择下方功能开始使用,或通过顶部导航访问其他页面。
|
||||
</Paragraph>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{cards.map((card, index) => (
|
||||
<Col xs={24} sm={12} md={8} key={index}>
|
||||
<Link to={card.link}>
|
||||
<Card hoverable style={{ height: '100%' }}>
|
||||
<Space direction='vertical' align='center' style={{ width: '100%' }}>
|
||||
{card.icon}
|
||||
<Title level={4} style={{ marginBottom: 0 }}>{card.title}</Title>
|
||||
<Paragraph type='secondary' style={{ marginBottom: 0 }}>
|
||||
{card.description}
|
||||
</Paragraph>
|
||||
</Space>
|
||||
</Card>
|
||||
</Link>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
81
src/pages/test1/index.tsx
Normal file
81
src/pages/test1/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { AgUiEventType, DifyAgent } from '@shangzy/ag-ui-dify'
|
||||
import { RunAgentInput, EventType, TextMessageContentEvent } from '@ag-ui/client'
|
||||
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
|
||||
import { useState } from 'react'
|
||||
|
||||
const Test1: React.FC = () => {
|
||||
const [message, setMessage] = useState('')
|
||||
const userInfo = getUserInfo()
|
||||
const runAgent = () => {
|
||||
const difyJson = {
|
||||
conversation_id: '',
|
||||
files: [],
|
||||
query: '1312313',
|
||||
appKey: cacheGet('appKey'),
|
||||
inputs: {
|
||||
Token: cacheGet('token'),
|
||||
tenantid: cacheGet('tenantId')
|
||||
},
|
||||
user: userInfo?.id || 'anonymous'
|
||||
}
|
||||
const content: string = JSON.stringify(difyJson)
|
||||
// 准备输入参数
|
||||
const input: RunAgentInput = {
|
||||
threadId: new Date().getTime().toString(),
|
||||
runId: new Date().getTime().toString(),
|
||||
messages: [
|
||||
{
|
||||
id: new Date().getTime().toString(),
|
||||
role: 'user',
|
||||
content: content
|
||||
}
|
||||
],
|
||||
context: [],
|
||||
tools: []
|
||||
}
|
||||
// 订阅Agent事件
|
||||
new DifyAgent({
|
||||
baseUrl: '/dify',
|
||||
showMetadata: true
|
||||
})
|
||||
.run(input)
|
||||
.subscribe({
|
||||
next: (event: AgUiEventType) => {
|
||||
console.log('🚀 ~ ChatApp ~ aa ~ event:', event)
|
||||
try {
|
||||
switch (event.type) {
|
||||
case EventType.RUN_STARTED:
|
||||
// 可以在这里处理运行开始事件
|
||||
break
|
||||
|
||||
case EventType.TEXT_MESSAGE_START:
|
||||
// 处理消息开始事件
|
||||
break
|
||||
|
||||
case EventType.TEXT_MESSAGE_CONTENT:
|
||||
const textEvent = event as TextMessageContentEvent
|
||||
setMessage(prev => prev + textEvent.delta)
|
||||
break
|
||||
|
||||
case EventType.TEXT_MESSAGE_END:
|
||||
break
|
||||
|
||||
case EventType.RUN_FINISHED:
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('处理事件时出错:', err)
|
||||
}
|
||||
},
|
||||
error: () => {},
|
||||
complete: () => {}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => runAgent()}>发起请求</div>
|
||||
<div>{message}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Test1
|
||||
158
src/pages/test2/index.tsx
Normal file
158
src/pages/test2/index.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { CopilotKit, useCopilotChatInternal as useCopilotChat } from '@copilotkit/react-core'
|
||||
import { Flex, Mentions, Spin } from 'antd'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
const Chat: React.FC = () => {
|
||||
const [currentAppKey, setCurrentAppKey] = useState<string>('')
|
||||
const [newMessage, setNewMessage] = useState('')
|
||||
const { messages, sendMessage, setMessages, isLoading, reloadMessages, stopGeneration } = useCopilotChat()
|
||||
const callSendMessage = useCallback(
|
||||
async (message: string) => {
|
||||
await sendMessage({
|
||||
id: new Date().getTime() + '',
|
||||
role: 'user',
|
||||
content: message
|
||||
})
|
||||
},
|
||||
[sendMessage]
|
||||
)
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
// 提前存好本次的提问内容,重新生成的话直接从缓存中获取之前的提问内容
|
||||
let question: string = newMessage || ''
|
||||
const token = cacheGet('token')
|
||||
const tenantId = cacheGet('tenantId')
|
||||
let conversation_id = ''
|
||||
if (messages[1]?.id) {
|
||||
conversation_id = messages[1]?.id.split('_')[0]
|
||||
}
|
||||
const userInfo = getUserInfo()
|
||||
const difyJson = {
|
||||
inputs: {
|
||||
Token: token || '',
|
||||
tenantid: tenantId || '',
|
||||
query: question
|
||||
},
|
||||
appKey: currentAppKey,
|
||||
files: [],
|
||||
user: userInfo?.id || 'anonymous',
|
||||
query: question,
|
||||
conversation_id
|
||||
}
|
||||
// 设置好目前状态下的聊天列表数据,包含之前已经结束的沟通内容,以及本次用户的提问,本次AI的回答占位
|
||||
callSendMessage(JSON.stringify(difyJson))
|
||||
setNewMessage('')
|
||||
}, [callSendMessage, newMessage, currentAppKey])
|
||||
|
||||
const handleParse = (jsonStr: string) => {
|
||||
let res = ''
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
res = parsed?.query
|
||||
} catch (e) {}
|
||||
return res
|
||||
}
|
||||
|
||||
// 换行
|
||||
const handleNewline = () => {
|
||||
setNewMessage(prevValue => `${prevValue}\n`)
|
||||
}
|
||||
const handleKeyDown = async (e: any) => {
|
||||
e.stopPropagation()
|
||||
const res = await cacheGet('sendMessage')
|
||||
if (e.key === 'Enter') {
|
||||
if (e.ctrlKey) {
|
||||
if (newMessage) {
|
||||
if (res === 'ctrlEnter') {
|
||||
handleSendMessage()
|
||||
} else {
|
||||
handleNewline()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (res === 'Enter' || !res) {
|
||||
if (newMessage) {
|
||||
handleSendMessage()
|
||||
} else {
|
||||
setNewMessage('')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{messages.map((message, index) => {
|
||||
const userMessageId = message.id
|
||||
// 实际的会话消息
|
||||
return (
|
||||
<Flex key={index} vertical data-message-id={userMessageId}>
|
||||
{/* 用户提问 */}
|
||||
{message.role === 'user' && <Flex key={message.id}>{handleParse(message.content as string) ?? ''}</Flex>}
|
||||
|
||||
{message.role === 'assistant' && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
width: 'calc(100% - 20px)',
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
{isLoading && !message.content && index === messages.length - 1 && (
|
||||
<Flex>
|
||||
<Spin indicator={<LoadingOutlined />} />
|
||||
<span>检索中</span>
|
||||
</Flex>
|
||||
)}
|
||||
{message.content ?? ''}
|
||||
</Flex>
|
||||
{message?.generativeUI?.()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
<div className='p-4'>
|
||||
<Mentions
|
||||
autoFocus
|
||||
open={false}
|
||||
placeholder='请输入内容'
|
||||
rows={4}
|
||||
value={newMessage}
|
||||
maxLength={10000}
|
||||
onKeyDown={handleKeyDown}
|
||||
options={[]}
|
||||
onInput={e => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
// 检查内容是否只包含空格或回车符
|
||||
if (/^\s*$/.test(value)) {
|
||||
setNewMessage('') // 如果只包含空格或回车符,清空输入框
|
||||
} else {
|
||||
setNewMessage(value) // 否则更新输入内容
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Test2: React.FC = () => {
|
||||
return (
|
||||
<CopilotKit
|
||||
runtimeUrl='/agui-api/copilotkit/dify'
|
||||
showDevConsole={false}
|
||||
// publicApiKey={'ck_pub_cc922145a5da9b8513bc10df473cd6f7'}
|
||||
agent='agentic_chat_metadata'
|
||||
>
|
||||
<Chat />
|
||||
</CopilotKit>
|
||||
)
|
||||
}
|
||||
|
||||
export default Test2
|
||||
216
src/pages/zh-en-translator/index.tsx
Normal file
216
src/pages/zh-en-translator/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Card, Form, Input, message, Space } from 'antd'
|
||||
import { PageHeader } from '@ant-design/pro-components'
|
||||
import { translateChineseToEnglish } from '@/api/zh-en-translator'
|
||||
|
||||
const ZhEnTranslator = () => {
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [translationResult, setTranslationResult] = useState('')
|
||||
const [currentInput, setCurrentInput] = useState('')
|
||||
const maxChars = 5000
|
||||
|
||||
const handleTranslate = async () => {
|
||||
try {
|
||||
const source_content = form.getFieldValue('source_content') || ''
|
||||
if (!source_content.trim()) {
|
||||
message.warning('请输入要翻译的中文内容')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setTranslationResult('')
|
||||
|
||||
const response = await translateChineseToEnglish(source_content)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`翻译请求失败: ${response.status} ${errorText}`)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('响应体为空')
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ''
|
||||
let fullContent = ''
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
|
||||
if (trimmedLine === 'data: [DONE]') {
|
||||
message.success('翻译完成')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith('data: ')) {
|
||||
try {
|
||||
const data = trimmedLine.slice(6)
|
||||
const parsed = JSON.parse(data)
|
||||
|
||||
if (parsed.event === 'message' && parsed.answer) {
|
||||
fullContent += parsed.answer
|
||||
setTranslationResult(fullContent)
|
||||
} else if (parsed.event === 'error') {
|
||||
throw new Error(parsed.message || 'Dify API 返回错误')
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('跳过无法解析的行:', trimmedLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
if (fullContent) {
|
||||
message.success('翻译完成')
|
||||
} else {
|
||||
throw new Error('未收到翻译结果')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('翻译错误:', error)
|
||||
message.error(error instanceof Error ? error.message : '翻译失败,请稍后重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
form.resetFields()
|
||||
setTranslationResult('')
|
||||
setCurrentInput('')
|
||||
message.info('已清空内容')
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!translationResult) {
|
||||
message.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(translationResult)
|
||||
message.success('复制成功')
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px' }}>
|
||||
<PageHeader
|
||||
title="中文转英文翻译助手"
|
||||
subTitle="专业的中文到英文文本翻译工具,支持流式输出,翻译结果地道自然"
|
||||
style={{ marginBottom: '30px' }}
|
||||
/>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<Card title="参数输入区">
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="source_content"
|
||||
label={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||
<span>中文原文</span>
|
||||
<span style={{ color: currentInput.length > maxChars ? '#ff4d4f' : '#999' }}>
|
||||
{currentInput.length} / {maxChars}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
rules={[
|
||||
{ required: true, message: '请输入要翻译的中文内容' },
|
||||
{ max: maxChars, message: `输入内容不能超过${maxChars}个字符` }
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入需要翻译的中文内容(最多5000个字符)"
|
||||
rows={8}
|
||||
value={currentInput}
|
||||
onChange={(e) => setCurrentInput(e.target.value)}
|
||||
showCount
|
||||
maxLength={maxChars}
|
||||
style={{ fontSize: '14px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Space style={{ width: '100%', justifyContent: 'center' }} size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleTranslate}
|
||||
loading={loading}
|
||||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||||
>
|
||||
翻译
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={handleClear}
|
||||
disabled={loading}
|
||||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Card
|
||||
title="翻译结果"
|
||||
extra={
|
||||
translationResult && (
|
||||
<Button onClick={handleCopy} size="small">
|
||||
复制
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: '150px',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
padding: '15px',
|
||||
backgroundColor: '#fafafa',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6'
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
|
||||
正在翻译中,请稍候...
|
||||
</div>
|
||||
) : translationResult || (
|
||||
<div style={{ textAlign: 'center', color: '#999' }}>
|
||||
翻译结果将在这里显示
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ZhEnTranslator
|
||||
59
src/router/index.tsx
Normal file
59
src/router/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Spin } from 'antd'
|
||||
import { FC, ReactNode, Suspense, lazy } from 'react'
|
||||
import { Navigate, RouteObject } from 'react-router-dom'
|
||||
import { createHashRouter } from 'react-router-dom'
|
||||
|
||||
const Loading = () => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh'
|
||||
}}
|
||||
>
|
||||
<Spin size='large' />
|
||||
</div>
|
||||
)
|
||||
|
||||
export const LazyLoad = (Component: FC): ReactNode => {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
const router: RouteObject[] = [
|
||||
{
|
||||
path: '/',
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: LazyLoad(lazy(() => import('@/pages/home')))
|
||||
},
|
||||
{
|
||||
path: '/test1',
|
||||
element: LazyLoad(lazy(() => import('@/pages/test1')))
|
||||
},
|
||||
{
|
||||
path: '/test2',
|
||||
element: LazyLoad(lazy(() => import('@/pages/test2')))
|
||||
},
|
||||
{
|
||||
path: '/zh-en-translator',
|
||||
element: LazyLoad(lazy(() => import('@/pages/zh-en-translator')))
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
element: <>404</>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to='/404' />
|
||||
}
|
||||
]
|
||||
|
||||
export default createHashRouter(router)
|
||||
43
src/utils/cacheUtil.ts
Normal file
43
src/utils/cacheUtil.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export function cacheGet(key: string): string {
|
||||
return localStorage.getItem(key) || ''
|
||||
}
|
||||
|
||||
export function cacheSet(key: string, value: string) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
export function cacheClear(key: string) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
export function cacheDeleteAll() {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id?: string
|
||||
avatar?: string
|
||||
gender?: string
|
||||
nickName?: string
|
||||
username?: string
|
||||
position?: string
|
||||
deptName?: string
|
||||
mobile?: string
|
||||
bizMail?: string
|
||||
email?: string
|
||||
corpName?: string
|
||||
effectivePoint?: string
|
||||
totalPoint?: string
|
||||
}
|
||||
|
||||
export function getUserInfo(): UserInfo | null {
|
||||
const res = localStorage.getItem('userInfo')
|
||||
if (!res) {
|
||||
return null
|
||||
} else {
|
||||
return JSON.parse(res)
|
||||
}
|
||||
}
|
||||
export function getToken() {
|
||||
return localStorage.getItem('token')
|
||||
}
|
||||
8
src/utils/polyfills.ts
Normal file
8
src/utils/polyfills.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
if (!Object.hasOwn) {
|
||||
Object.defineProperty(Object, 'hasOwn', {
|
||||
value: function(obj: any, prop: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(obj, prop)
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
}
|
||||
2
src/vite-env.d.ts
vendored
Normal file
2
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '@tailwindcss/vite'
|
||||
5
tailwind.config.js
Normal file
5
tailwind.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
darkMode: 'class',
|
||||
plugins: []
|
||||
}
|
||||
29
tsconfig.app.json
Normal file
29
tsconfig.app.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "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,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "src/api/.ts"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
28
tsconfig.node.json
Normal file
28
tsconfig.node.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"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,
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
33
vite.config.ts
Normal file
33
vite.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
base: './',
|
||||
plugins: [tailwindcss(), react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['dayjs/locale/zh-cn']
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
allowedHosts: true as true,
|
||||
cors: true,
|
||||
open: true,
|
||||
port: 5173
|
||||
},
|
||||
// 生产预览服务器配置 (vite preview) - 仅用于本地测试
|
||||
preview: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
cors: true
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user