refactor: cleanup template and add new pages
- Remove unused template files (frontend-code-interpreter, template-renderer) - Add home, test1, test2, zh-en-translator pages - Update router configuration - Add Docker and tailwind config - Update environment files and configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
44
src/App.tsx
Normal file
44
src/App.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { RouterProvider } from 'react-router-dom'
|
||||
import { useEffect } from 'react'
|
||||
import router from '@/router'
|
||||
import { cacheClear, cacheSet } from '@/utils/cacheUtil'
|
||||
import { getAgentBaseInfo, getUserInfoById } from '@/api/common'
|
||||
|
||||
function App() {
|
||||
const queryString = window.location.search
|
||||
const searchParams = new URLSearchParams(queryString)
|
||||
const tenantId = searchParams.get('tenantId') || ''
|
||||
const token = searchParams.get('token') || ''
|
||||
|
||||
cacheSet('tenantId', tenantId)
|
||||
cacheSet('token', token)
|
||||
useEffect(() => {
|
||||
const agentId = searchParams.get('agentId') || ''
|
||||
|
||||
if (agentId) {
|
||||
getAgentBaseInfo({ id: agentId }).then(res => {
|
||||
cacheSet('appKey', res.data.appKey)
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantId && token) {
|
||||
getUserInfoById().then(response => {
|
||||
if (response.code === 200 && response.data) {
|
||||
cacheSet('userInfo', JSON.stringify(response.data))
|
||||
}
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
cacheClear('userInfo')
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<>
|
||||
<RouterProvider router={router} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
28
src/api/common.ts
Normal file
28
src/api/common.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { cacheGet } from '@/utils/cacheUtil'
|
||||
|
||||
export function getAgentBaseInfo(params: { id: string }) {
|
||||
const Tenantid = cacheGet('tenantId')
|
||||
const Token = cacheGet('token')
|
||||
return fetch(`${import.meta.env.VITE_API_BASE_AI}/agent/baseInfo/info?id=${params.id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Tenantid,
|
||||
Token
|
||||
}
|
||||
}).then(res => res.json())
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export async function getUserInfoById() {
|
||||
const Tenantid = cacheGet('tenantId')
|
||||
const Token = cacheGet('token')
|
||||
return fetch(`${import.meta.env['VITE_API_BASE_LAMP']}/oauth/anyone/getUserInfoById`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Tenantid,
|
||||
Token
|
||||
}
|
||||
}).then(res => res.json())
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import {SSEClient, SSERequest} from "../utils/sse-client.ts";
|
||||
|
||||
const VITE_API_BASE_DOC = import.meta.env['VITE_API_BASE_DOC'] || "";
|
||||
const VITE_AI_API_BASE = import.meta.env['VITE_AI_API_BASE'] || "";
|
||||
const VITE_TEMPLATE_AGENT_TOKEN = import.meta.env['VITE_TEMPLATE_AGENT_TOKEN'] || "";
|
||||
|
||||
export async function listAllTemplates() {
|
||||
const res = await fetch(`${VITE_API_BASE_DOC}/doc/resumeTemp/list`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: ""
|
||||
})
|
||||
});
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function convertFileToPDF(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
return fetch(`${VITE_API_BASE_DOC}/doc/resume/formatConvert`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadFile(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
formData.append("user", "resume-generation");
|
||||
return fetch(`${VITE_AI_API_BASE}/files/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Authorization": `Bearer ${VITE_TEMPLATE_AGENT_TOKEN}`
|
||||
},
|
||||
body: formData
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export async function templatePadding(templateId: string, data: object) {
|
||||
return fetch(`${VITE_API_BASE_DOC}/doc/resume/padding?templateId=${templateId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
paddingParameter: data
|
||||
})
|
||||
}).then(res => res.json());
|
||||
}
|
||||
|
||||
export async function structGeneration(id: string, {
|
||||
onMessage,
|
||||
onError,
|
||||
onFinish
|
||||
}: Pick<SSERequest, "onMessage" | "onError" | "onFinish">) {
|
||||
const client = new SSEClient(VITE_TEMPLATE_AGENT_TOKEN);
|
||||
return client.sendMessage({
|
||||
type: "chat",
|
||||
query: "生成",
|
||||
user: "resume-generation",
|
||||
files: [
|
||||
{
|
||||
type: "document",
|
||||
transfer_method: "local_file",
|
||||
upload_file_id: id
|
||||
}
|
||||
],
|
||||
onMessage,
|
||||
onError,
|
||||
onFinish
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function downloadPaddingResult(shortUrl: string) {
|
||||
const link = document.createElement('a')
|
||||
link.href = `https://copilot.sino-bridge.com/api/langwell-doc-server/doc/resume/download/${shortUrl}`
|
||||
link.download = ""
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
}
|
||||
|
||||
34
src/api/zh-en-translator.ts
Normal file
34
src/api/zh-en-translator.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export interface TranslationRequest {
|
||||
source_content: string
|
||||
}
|
||||
|
||||
export interface DifyRequest {
|
||||
inputs: {
|
||||
prompt: string
|
||||
}
|
||||
query: string
|
||||
response_mode: string
|
||||
}
|
||||
|
||||
export function translateChineseToEnglish(source_content: string) {
|
||||
const prompt = `你是一个专业的中英翻译专家。请将下面的中文文本翻译成自然、地道、专业的英文,只返回翻译后的英文内容,不要添加任何额外的解释或格式。待翻译的中文内容是:
|
||||
|
||||
${source_content}`
|
||||
|
||||
const requestBody: DifyRequest = {
|
||||
inputs: {
|
||||
prompt
|
||||
},
|
||||
query: '1',
|
||||
response_mode: 'streaming'
|
||||
}
|
||||
|
||||
return fetch('https://copilot.sino-bridge.com/v1/chat-messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer app-Y6ekYkw3aoUV3jmfZdg24Adh'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
})
|
||||
}
|
||||
29
src/index.css
Normal file
29
src/index.css
Normal file
@@ -0,0 +1,29 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
.route-loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 120px;
|
||||
height: 22px;
|
||||
color: #514b82;
|
||||
border: 2px solid;
|
||||
border-radius: 20px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes l6 {
|
||||
100% {
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.route-loading::before {
|
||||
position: absolute;
|
||||
inset: 0 100% 0 0;
|
||||
margin: 2px;
|
||||
content: '';
|
||||
background: currentcolor;
|
||||
border-radius: inherit;
|
||||
animation: l6 2s infinite;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background: linear-gradient(180deg, rgba(189, 225, 255, 0.4) 0%, rgba(224, 242, 255, 0) 100%);
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
padding: 12px 24px;
|
||||
height: 72px;
|
||||
|
||||
.title-text {
|
||||
color: transparent;
|
||||
background: linear-gradient(116deg, #1888ff 16%, #2f54eb 88%);
|
||||
-webkit-background-clip: text;
|
||||
user-select: none;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
16
src/main.tsx
16
src/main.tsx
@@ -1,6 +1,12 @@
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import {RouterProvider} from 'react-router-dom'
|
||||
import routers from './router'
|
||||
import './index.less'
|
||||
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(<RouterProvider router={routers}/>)
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
.ant-splitter {
|
||||
padding: 12px 24px;
|
||||
gap: 12px;
|
||||
flex-grow: 1;
|
||||
|
||||
.ant-splitter-panel {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** 控制代码块的高度撑满父容器 */
|
||||
.ant-spin-nested-loading, .ant-spin-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-spin-container {
|
||||
.editor {
|
||||
position: relative;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
max-height: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f7f7f7;
|
||||
|
||||
.terminal {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s ease;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.iframe-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import { WebContainer } from '@webcontainer/api'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
// import {EXPRESS_APP} from "../../../projects/express-app.ts";
|
||||
import { REACT_APP } from '../../../projects/react-app.ts'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { installDependencies, startDevServer } from '../../utils/dev-operations.ts'
|
||||
import { Button, Flex, Space, Spin, Splitter, Tooltip, Typography } from 'antd'
|
||||
import { CheckCircleFilled, CopyOutlined, DownloadOutlined } from '@ant-design/icons'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
|
||||
import './xterm.less'
|
||||
import './index.less'
|
||||
|
||||
let containerInstance: WebContainer
|
||||
|
||||
export const FrontEndCodeInterpreter = () => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const terminalRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [codeLoading, setCodeLoading] = useState<boolean>(true)
|
||||
const [devServerStarted, setDevServerStarted] = useState<boolean>(false)
|
||||
|
||||
const [codeString, setCodeString] = useState<string>('')
|
||||
const [copyText, setCopyText] = useState<string>('复制')
|
||||
const [copyIcon, setCopyIcon] = useState(<CopyOutlined />)
|
||||
const [downloadText, setDownloadText] = useState<string>('下载')
|
||||
|
||||
const init = async () => {
|
||||
if (containerInstance != null) return
|
||||
containerInstance = await WebContainer.boot()
|
||||
setLoading(false)
|
||||
containerInstance
|
||||
.mount(REACT_APP)
|
||||
.then(() => {
|
||||
setCodeString(REACT_APP['src']['directory']['App.tsx'].file.contents.trim())
|
||||
setCodeLoading(false)
|
||||
})
|
||||
.then(() => {
|
||||
const terminal = new Terminal({
|
||||
convertEol: true
|
||||
})
|
||||
terminalRef.current && terminal.open(terminalRef.current)
|
||||
return terminal
|
||||
})
|
||||
.then(terminal => {
|
||||
installDependencies(containerInstance, terminal).then(res => {
|
||||
res &&
|
||||
startDevServer(containerInstance, terminal, url => {
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.src = url
|
||||
setDevServerStarted(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
init()
|
||||
}, [])
|
||||
|
||||
const handleCopyClick = () => {
|
||||
navigator.clipboard
|
||||
.writeText(codeString)
|
||||
.then(() => {
|
||||
setCopyText('已复制')
|
||||
setCopyIcon(<CheckCircleFilled />)
|
||||
})
|
||||
.finally(() =>
|
||||
setTimeout(() => {
|
||||
setCopyText('复制')
|
||||
setCopyIcon(<CopyOutlined />)
|
||||
}, 2000)
|
||||
)
|
||||
}
|
||||
|
||||
const handleDownloadClick = () => {
|
||||
setDownloadText('已下载')
|
||||
setTimeout(() => {
|
||||
setDownloadText('下载')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spin tip='加载中' spinning={loading} fullscreen size='large' />
|
||||
<Flex className='container' vertical>
|
||||
<Flex className='toolbar' justify='space-between'>
|
||||
<Typography.Text className='title-text'>代码解释器</Typography.Text>
|
||||
<Space>
|
||||
<Tooltip placement='bottom' title={copyText}>
|
||||
<Button color='primary' variant='link' icon={copyIcon} onClick={handleCopyClick}></Button>
|
||||
</Tooltip>
|
||||
<Tooltip placement='bottom' title={downloadText}>
|
||||
<Button color='primary' variant='link' icon={<DownloadOutlined />} onClick={handleDownloadClick}></Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</Flex>
|
||||
<Splitter>
|
||||
<Splitter.Panel defaultSize='40%' min='20%' max='70%'>
|
||||
<Spin tip='代码加载中' spinning={codeLoading} size='large'>
|
||||
<SyntaxHighlighter
|
||||
className='editor custom-scrollbar'
|
||||
language='jsx'
|
||||
showLineNumbers={true}
|
||||
wrapLines={true}
|
||||
style={dark}
|
||||
customStyle={{
|
||||
border: 'none',
|
||||
margin: '0'
|
||||
}}
|
||||
>
|
||||
{codeString}
|
||||
</SyntaxHighlighter>
|
||||
</Spin>
|
||||
</Splitter.Panel>
|
||||
<Splitter.Panel>
|
||||
<Flex className='preview' vertical>
|
||||
<div ref={terminalRef} className={`terminal ${devServerStarted ? 'hidden' : ''}`} />
|
||||
<div className={`iframe-wrapper ${devServerStarted ? 'visible' : ''}`}>
|
||||
<iframe ref={iframeRef} src='/loading.html' frameBorder='0' />
|
||||
</div>
|
||||
</Flex>
|
||||
</Splitter.Panel>
|
||||
</Splitter>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden auto;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
width: 100% !important;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen .xterm-rows div {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer,
|
||||
.xterm .xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm.column-select.focus {
|
||||
/* Column selection mode */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility:not(.debug),
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree {
|
||||
user-select: text;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.xterm-underline-2 {
|
||||
text-decoration: double underline;
|
||||
}
|
||||
|
||||
.xterm-underline-3 {
|
||||
text-decoration: wavy underline;
|
||||
}
|
||||
|
||||
.xterm-underline-4 {
|
||||
text-decoration: dotted underline;
|
||||
}
|
||||
|
||||
.xterm-underline-5 {
|
||||
text-decoration: dashed underline;
|
||||
}
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 {
|
||||
text-decoration: overline underline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-2 {
|
||||
text-decoration: overline double underline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-3 {
|
||||
text-decoration: overline wavy underline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-4 {
|
||||
text-decoration: overline dotted underline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-5 {
|
||||
text-decoration: overline dashed underline;
|
||||
}
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
57
src/pages/home/index.tsx
Normal file
57
src/pages/home/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Card, Row, Col, Typography, Space } from 'antd'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { RobotOutlined, TranslationOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
|
||||
const { Title, Paragraph } = Typography
|
||||
|
||||
const cards = [
|
||||
{
|
||||
title: '测试页面 1',
|
||||
description: 'Dify AI Agent 集成示例',
|
||||
icon: <RobotOutlined style={{ fontSize: 32, color: '#1890ff' }} />,
|
||||
link: '/test1'
|
||||
},
|
||||
{
|
||||
title: '测试页面 2',
|
||||
description: '功能开发中...',
|
||||
icon: <FileTextOutlined style={{ fontSize: 32, color: '#52c41a' }} />,
|
||||
link: '/test2'
|
||||
},
|
||||
{
|
||||
title: '中英翻译器',
|
||||
description: 'AI 驱动的翻译工具',
|
||||
icon: <TranslationOutlined style={{ fontSize: 32, color: '#722ed1' }} />,
|
||||
link: '/zh-en-translator'
|
||||
}
|
||||
]
|
||||
|
||||
const HomePage: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Title level={2}>欢迎使用 AI 办公助手</Title>
|
||||
<Paragraph style={{ marginBottom: 32 }}>
|
||||
选择下方功能开始使用,或通过顶部导航访问其他页面。
|
||||
</Paragraph>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{cards.map((card, index) => (
|
||||
<Col xs={24} sm={12} md={8} key={index}>
|
||||
<Link to={card.link}>
|
||||
<Card hoverable style={{ height: '100%' }}>
|
||||
<Space direction='vertical' align='center' style={{ width: '100%' }}>
|
||||
{card.icon}
|
||||
<Title level={4} style={{ marginBottom: 0 }}>{card.title}</Title>
|
||||
<Paragraph type='secondary' style={{ marginBottom: 0 }}>
|
||||
{card.description}
|
||||
</Paragraph>
|
||||
</Space>
|
||||
</Card>
|
||||
</Link>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomePage
|
||||
@@ -1,59 +0,0 @@
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
height: calc(100% - 100px);
|
||||
|
||||
.template-form {
|
||||
margin: 0 auto;
|
||||
width: 40%;
|
||||
|
||||
|
||||
.ant-upload-wrapper {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
opacity: 0;
|
||||
animation: slideIn 0.3s ease-in-out forwards;
|
||||
height: 100%;
|
||||
min-height: 1px;
|
||||
width: 60%;
|
||||
margin-right: 24px;
|
||||
|
||||
.preview-header {
|
||||
flex: 0 0 auto;
|
||||
|
||||
.template-select {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
background-color: #f1f1f1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
/** 模板生成工具 */
|
||||
import {Button, Card, Flex, message, Select, Space, Spin, Typography, Upload} from "antd";
|
||||
import {useCallback, useEffect, useRef, useState} from "react";
|
||||
import {
|
||||
convertFileToPDF,
|
||||
downloadPaddingResult,
|
||||
listAllTemplates,
|
||||
structGeneration,
|
||||
templatePadding,
|
||||
uploadFile
|
||||
} from "../../api/template.ts";
|
||||
import {CheckCircleFilled, DownloadOutlined, InboxOutlined, LoadingOutlined} from "@ant-design/icons";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter'
|
||||
import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
|
||||
import "./index.less";
|
||||
import {extractJSONFromString} from "../../utils/json-extractor.ts";
|
||||
|
||||
export const TemplateRenderer = () => {
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
// FIXME 这个装填应该提交到全局状态库
|
||||
const [globalLoading, setGlobalLoading] = useState<boolean>(true)
|
||||
|
||||
// 点击【开始生成】后,状态变更
|
||||
const [startSending, setStartSending] = useState<boolean>(false);
|
||||
|
||||
// 生成状态
|
||||
const [generating, setGenerating] = useState<boolean>(false);
|
||||
// 生成消息
|
||||
const [messages, setMessages] = useState<string>("");
|
||||
// 生成异常
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [generationMetadata, setGenerationMetadata] = useState<object>();
|
||||
|
||||
const [templates, setTemplates] = useState<object[]>([]);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>();
|
||||
const [uploadedFile, setUploadedFile] = useState<object>();
|
||||
|
||||
// 用于存储完整的响应文本
|
||||
const fullContentRef = useRef<string>('');
|
||||
// 用于控制动画帧
|
||||
const animationFrameRef = useRef<number>();
|
||||
// 用于跟踪当前显示的字符位置
|
||||
const currentIndexRef = useRef<number>(0);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "模板填充工具";
|
||||
listAllTemplates().then(response => {
|
||||
if (response.code === 200 && response.data) {
|
||||
const templates = response.data.map(item => {
|
||||
return {value: item.id, label: <span>{item.title}</span>}
|
||||
})
|
||||
setTemplates(templates);
|
||||
}
|
||||
}).finally(() => {
|
||||
setGlobalLoading(false);
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
// 清理函数
|
||||
const cleanup = () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
fullContentRef.current = '';
|
||||
currentIndexRef.current = 0;
|
||||
};
|
||||
|
||||
const smoothRender = useCallback(() => {
|
||||
const renderNextChunk = () => {
|
||||
if (currentIndexRef.current < fullContentRef.current.length) {
|
||||
// 每次渲染多个字符以提高性能,同时保持流畅性
|
||||
const chunkSize = 2;
|
||||
const nextIndex = Math.min(
|
||||
currentIndexRef.current + chunkSize,
|
||||
fullContentRef.current.length
|
||||
);
|
||||
|
||||
setMessages(fullContentRef.current.slice(0, nextIndex));
|
||||
currentIndexRef.current = nextIndex;
|
||||
|
||||
// 继续下一帧渲染
|
||||
animationFrameRef.current = requestAnimationFrame(renderNextChunk);
|
||||
}
|
||||
};
|
||||
|
||||
renderNextChunk();
|
||||
}, []);
|
||||
|
||||
// 处理新收到的文本
|
||||
const handleNewText = useCallback((text: string) => {
|
||||
fullContentRef.current += text;
|
||||
// 如果当前没有动画在进行,启动新的渲染
|
||||
if (!animationFrameRef.current) {
|
||||
smoothRender();
|
||||
}
|
||||
}, [smoothRender]);
|
||||
|
||||
const handleGeneration = useCallback(async (fileId: string) => {
|
||||
setGenerating(true);
|
||||
setError(null);
|
||||
setMessages("");
|
||||
cleanup();
|
||||
|
||||
try {
|
||||
await structGeneration(fileId, {
|
||||
onMessage: (text, finished) => {
|
||||
if (text) {
|
||||
handleNewText(text);
|
||||
setMessages(prev => prev + text);
|
||||
}
|
||||
if (finished) {
|
||||
setGenerating(false);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error.message);
|
||||
setGenerating(false);
|
||||
cleanup();
|
||||
},
|
||||
onFinish: (metadata) => {
|
||||
setGenerationMetadata(metadata);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '生成过程中发生错误');
|
||||
setGenerating(false);
|
||||
cleanup();
|
||||
}
|
||||
}, [handleNewText]);
|
||||
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
const originalFilename = file.name.substring(0, file.name.lastIndexOf("."));
|
||||
const originalFileExt = file.name.substring(file.name.lastIndexOf(".") + 1);
|
||||
if (["pdf", "docx"].includes(originalFileExt)) {
|
||||
messageApi.open({
|
||||
key: "uploading",
|
||||
type: 'loading',
|
||||
content: '文件上传中',
|
||||
});
|
||||
convertFileToPDF(file).then(async (response) => {
|
||||
if (response["status"] && response["status"] === 500) {
|
||||
messageApi.open({
|
||||
key: "uploading",
|
||||
type: 'error',
|
||||
content: '文件处理异常,请稍后重试',
|
||||
duration: 1
|
||||
});
|
||||
} else if ("blob" in response) {
|
||||
const blob = await response.blob();
|
||||
const pdfFile = new File([blob], `${originalFilename}.pdf`, {type: 'application/pdf'});
|
||||
uploadFile(pdfFile).then(async (response) => {
|
||||
if (response.id) {
|
||||
setUploadedFile(response);
|
||||
messageApi.open({
|
||||
key: "uploading",
|
||||
type: 'success',
|
||||
content: '文件上传成功',
|
||||
duration: 1
|
||||
});
|
||||
} else {
|
||||
messageApi.open({
|
||||
key: "uploading",
|
||||
type: 'error',
|
||||
content: '文件上传失败',
|
||||
duration: 1
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
messageApi.error("目前仅支持.docx,.pdf类型的文件,请您将文件转成这些格式后再次进行上传");
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerationStart = () => {
|
||||
setStartSending(true);
|
||||
handleGeneration(uploadedFile!.id);
|
||||
}
|
||||
|
||||
|
||||
const handleTemplatePadding = () => {
|
||||
const templatePaddingData: string | null = extractJSONFromString(messages);
|
||||
if (selectedTemplateId && templatePaddingData) {
|
||||
templatePadding(selectedTemplateId, templatePaddingData).then(response => {
|
||||
console.log(response);
|
||||
if (response.code === 200) {
|
||||
const url: string = response.data.shortUrl;
|
||||
if (url) {
|
||||
downloadPaddingResult(url);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const markdownComponents = {
|
||||
code({inline, className, children, ...props}) {
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (<SyntaxHighlighter
|
||||
{...props}
|
||||
className='editor custom-scrollbar'
|
||||
language={match?.[1]}
|
||||
showLineNumbers={true}
|
||||
wrapLines={true}
|
||||
style={dark}
|
||||
customStyle={{
|
||||
border: 'none',
|
||||
margin: '0'
|
||||
}}
|
||||
children={String(children).replace(/\n$/, "")}
|
||||
>
|
||||
</SyntaxHighlighter>) : (
|
||||
<code {...props} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<Spin tip='加载中' spinning={globalLoading} fullscreen size='large'/>
|
||||
<Flex className='toolbar' justify='space-between'>
|
||||
<Typography.Text className='title-text'>模板填充工具</Typography.Text>
|
||||
<Space>
|
||||
</Space>
|
||||
</Flex>
|
||||
<Flex className="content-wrapper" gap="large">
|
||||
<Card className="template-form">
|
||||
<Flex vertical gap="middle">
|
||||
<Upload.Dragger
|
||||
showUploadList={false}
|
||||
multiple={false}
|
||||
beforeUpload={beforeUpload}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
{uploadedFile ? <CheckCircleFilled/> : <InboxOutlined/>}
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
{uploadedFile ? uploadedFile.name : "点击或者将文件拖拽到这里进行上传"}
|
||||
</p>
|
||||
<p className="ant-upload-hint">
|
||||
{uploadedFile ? "点击或者将文件拖拽到这里重新上传" :
|
||||
<>
|
||||
<p>在这里上传您的文件,让AI帮您进行解析</p>
|
||||
<p>目前仅支持上传一个文件,支持.docx,.pdf类型</p>
|
||||
</>
|
||||
}
|
||||
</p>
|
||||
</Upload.Dragger>
|
||||
<Button size="large" type="primary" disabled={!uploadedFile || generating}
|
||||
onClick={handleGenerationStart}
|
||||
|
||||
>开 始 AI 提 取</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
|
||||
{startSending && (
|
||||
<Flex className="preview-panel" vertical gap="middle">
|
||||
<Flex className="preview-header" justify="space-between" gap="middle">
|
||||
<Select
|
||||
className="template-select"
|
||||
size="large"
|
||||
placeholder={generationMetadata ? "文件提取完毕,请选择模板" : "文件提取中,请稍后"}
|
||||
options={templates}
|
||||
disabled={!generationMetadata}
|
||||
onChange={setSelectedTemplateId}
|
||||
/>
|
||||
<Button icon={<DownloadOutlined/>} type="primary" size="large" disabled={!selectedTemplateId}
|
||||
onClick={handleTemplatePadding}
|
||||
>下 载</Button>
|
||||
</Flex>
|
||||
<Card className="preview-content custom-scrollbar">
|
||||
{messages ?
|
||||
<ReactMarkdown
|
||||
className="markdown-body custom-scrollbar"
|
||||
components={markdownComponents}
|
||||
>
|
||||
{messages}
|
||||
</ReactMarkdown>
|
||||
: (
|
||||
<>
|
||||
<LoadingOutlined style={{fontSize: 48, marginBottom: 24}}/>
|
||||
<Typography.Text>正在提取文件信息,请不要关闭或刷新页面</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
81
src/pages/test1/index.tsx
Normal file
81
src/pages/test1/index.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { AgUiEventType, DifyAgent } from '@shangzy/ag-ui-dify'
|
||||
import { RunAgentInput, EventType, TextMessageContentEvent } from '@ag-ui/client'
|
||||
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
|
||||
import { useState } from 'react'
|
||||
|
||||
const Test1: React.FC = () => {
|
||||
const [message, setMessage] = useState('')
|
||||
const userInfo = getUserInfo()
|
||||
const runAgent = () => {
|
||||
const difyJson = {
|
||||
conversation_id: '',
|
||||
files: [],
|
||||
query: '1312313',
|
||||
appKey: cacheGet('appKey'),
|
||||
inputs: {
|
||||
Token: cacheGet('token'),
|
||||
tenantid: cacheGet('tenantId')
|
||||
},
|
||||
user: userInfo?.id || 'anonymous'
|
||||
}
|
||||
const content: string = JSON.stringify(difyJson)
|
||||
// 准备输入参数
|
||||
const input: RunAgentInput = {
|
||||
threadId: new Date().getTime().toString(),
|
||||
runId: new Date().getTime().toString(),
|
||||
messages: [
|
||||
{
|
||||
id: new Date().getTime().toString(),
|
||||
role: 'user',
|
||||
content: content
|
||||
}
|
||||
],
|
||||
context: [],
|
||||
tools: []
|
||||
}
|
||||
// 订阅Agent事件
|
||||
new DifyAgent({
|
||||
baseUrl: '/dify',
|
||||
showMetadata: true
|
||||
})
|
||||
.run(input)
|
||||
.subscribe({
|
||||
next: (event: AgUiEventType) => {
|
||||
console.log('🚀 ~ ChatApp ~ aa ~ event:', event)
|
||||
try {
|
||||
switch (event.type) {
|
||||
case EventType.RUN_STARTED:
|
||||
// 可以在这里处理运行开始事件
|
||||
break
|
||||
|
||||
case EventType.TEXT_MESSAGE_START:
|
||||
// 处理消息开始事件
|
||||
break
|
||||
|
||||
case EventType.TEXT_MESSAGE_CONTENT:
|
||||
const textEvent = event as TextMessageContentEvent
|
||||
setMessage(prev => prev + textEvent.delta)
|
||||
break
|
||||
|
||||
case EventType.TEXT_MESSAGE_END:
|
||||
break
|
||||
|
||||
case EventType.RUN_FINISHED:
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('处理事件时出错:', err)
|
||||
}
|
||||
},
|
||||
error: () => {},
|
||||
complete: () => {}
|
||||
})
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div onClick={() => runAgent()}>发起请求</div>
|
||||
<div>{message}</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default Test1
|
||||
158
src/pages/test2/index.tsx
Normal file
158
src/pages/test2/index.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { CopilotKit, useCopilotChatInternal as useCopilotChat } from '@copilotkit/react-core'
|
||||
import { Flex, Mentions, Spin } from 'antd'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
const Chat: React.FC = () => {
|
||||
const [currentAppKey, setCurrentAppKey] = useState<string>('')
|
||||
const [newMessage, setNewMessage] = useState('')
|
||||
const { messages, sendMessage, setMessages, isLoading, reloadMessages, stopGeneration } = useCopilotChat()
|
||||
const callSendMessage = useCallback(
|
||||
async (message: string) => {
|
||||
await sendMessage({
|
||||
id: new Date().getTime() + '',
|
||||
role: 'user',
|
||||
content: message
|
||||
})
|
||||
},
|
||||
[sendMessage]
|
||||
)
|
||||
|
||||
const handleSendMessage = useCallback(() => {
|
||||
// 提前存好本次的提问内容,重新生成的话直接从缓存中获取之前的提问内容
|
||||
let question: string = newMessage || ''
|
||||
const token = cacheGet('token')
|
||||
const tenantId = cacheGet('tenantId')
|
||||
let conversation_id = ''
|
||||
if (messages[1]?.id) {
|
||||
conversation_id = messages[1]?.id.split('_')[0]
|
||||
}
|
||||
const userInfo = getUserInfo()
|
||||
const difyJson = {
|
||||
inputs: {
|
||||
Token: token || '',
|
||||
tenantid: tenantId || '',
|
||||
query: question
|
||||
},
|
||||
appKey: currentAppKey,
|
||||
files: [],
|
||||
user: userInfo?.id || 'anonymous',
|
||||
query: question,
|
||||
conversation_id
|
||||
}
|
||||
// 设置好目前状态下的聊天列表数据,包含之前已经结束的沟通内容,以及本次用户的提问,本次AI的回答占位
|
||||
callSendMessage(JSON.stringify(difyJson))
|
||||
setNewMessage('')
|
||||
}, [callSendMessage, newMessage, currentAppKey])
|
||||
|
||||
const handleParse = (jsonStr: string) => {
|
||||
let res = ''
|
||||
try {
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
res = parsed?.query
|
||||
} catch (e) {}
|
||||
return res
|
||||
}
|
||||
|
||||
// 换行
|
||||
const handleNewline = () => {
|
||||
setNewMessage(prevValue => `${prevValue}\n`)
|
||||
}
|
||||
const handleKeyDown = async (e: any) => {
|
||||
e.stopPropagation()
|
||||
const res = await cacheGet('sendMessage')
|
||||
if (e.key === 'Enter') {
|
||||
if (e.ctrlKey) {
|
||||
if (newMessage) {
|
||||
if (res === 'ctrlEnter') {
|
||||
handleSendMessage()
|
||||
} else {
|
||||
handleNewline()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (res === 'Enter' || !res) {
|
||||
if (newMessage) {
|
||||
handleSendMessage()
|
||||
} else {
|
||||
setNewMessage('')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{messages.map((message, index) => {
|
||||
const userMessageId = message.id
|
||||
// 实际的会话消息
|
||||
return (
|
||||
<Flex key={index} vertical data-message-id={userMessageId}>
|
||||
{/* 用户提问 */}
|
||||
{message.role === 'user' && <Flex key={message.id}>{handleParse(message.content as string) ?? ''}</Flex>}
|
||||
|
||||
{message.role === 'assistant' && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
width: 'calc(100% - 20px)',
|
||||
marginLeft: '20px'
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
{isLoading && !message.content && index === messages.length - 1 && (
|
||||
<Flex>
|
||||
<Spin indicator={<LoadingOutlined />} />
|
||||
<span>检索中</span>
|
||||
</Flex>
|
||||
)}
|
||||
{message.content ?? ''}
|
||||
</Flex>
|
||||
{message?.generativeUI?.()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
<div className='p-4'>
|
||||
<Mentions
|
||||
autoFocus
|
||||
open={false}
|
||||
placeholder='请输入内容'
|
||||
rows={4}
|
||||
value={newMessage}
|
||||
maxLength={10000}
|
||||
onKeyDown={handleKeyDown}
|
||||
options={[]}
|
||||
onInput={e => {
|
||||
const value = (e.target as HTMLInputElement).value
|
||||
// 检查内容是否只包含空格或回车符
|
||||
if (/^\s*$/.test(value)) {
|
||||
setNewMessage('') // 如果只包含空格或回车符,清空输入框
|
||||
} else {
|
||||
setNewMessage(value) // 否则更新输入内容
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Test2: React.FC = () => {
|
||||
return (
|
||||
<CopilotKit
|
||||
runtimeUrl='/agui-api/copilotkit/dify'
|
||||
showDevConsole={false}
|
||||
// publicApiKey={'ck_pub_cc922145a5da9b8513bc10df473cd6f7'}
|
||||
agent='agentic_chat_metadata'
|
||||
>
|
||||
<Chat />
|
||||
</CopilotKit>
|
||||
)
|
||||
}
|
||||
|
||||
export default Test2
|
||||
216
src/pages/zh-en-translator/index.tsx
Normal file
216
src/pages/zh-en-translator/index.tsx
Normal file
@@ -0,0 +1,216 @@
|
||||
import { useState } from 'react'
|
||||
import { Button, Card, Form, Input, message, Space } from 'antd'
|
||||
import { PageHeader } from '@ant-design/pro-components'
|
||||
import { translateChineseToEnglish } from '@/api/zh-en-translator'
|
||||
|
||||
const ZhEnTranslator = () => {
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [translationResult, setTranslationResult] = useState('')
|
||||
const [currentInput, setCurrentInput] = useState('')
|
||||
const maxChars = 5000
|
||||
|
||||
const handleTranslate = async () => {
|
||||
try {
|
||||
const source_content = form.getFieldValue('source_content') || ''
|
||||
if (!source_content.trim()) {
|
||||
message.warning('请输入要翻译的中文内容')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setTranslationResult('')
|
||||
|
||||
const response = await translateChineseToEnglish(source_content)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`翻译请求失败: ${response.status} ${errorText}`)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('响应体为空')
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ''
|
||||
let fullContent = ''
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
|
||||
if (trimmedLine === 'data: [DONE]') {
|
||||
message.success('翻译完成')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith('data: ')) {
|
||||
try {
|
||||
const data = trimmedLine.slice(6)
|
||||
const parsed = JSON.parse(data)
|
||||
|
||||
if (parsed.event === 'message' && parsed.answer) {
|
||||
fullContent += parsed.answer
|
||||
setTranslationResult(fullContent)
|
||||
} else if (parsed.event === 'error') {
|
||||
throw new Error(parsed.message || 'Dify API 返回错误')
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('跳过无法解析的行:', trimmedLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
if (fullContent) {
|
||||
message.success('翻译完成')
|
||||
} else {
|
||||
throw new Error('未收到翻译结果')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('翻译错误:', error)
|
||||
message.error(error instanceof Error ? error.message : '翻译失败,请稍后重试')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
form.resetFields()
|
||||
setTranslationResult('')
|
||||
setCurrentInput('')
|
||||
message.info('已清空内容')
|
||||
}
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!translationResult) {
|
||||
message.warning('没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(translationResult)
|
||||
message.success('复制成功')
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error)
|
||||
message.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px' }}>
|
||||
<PageHeader
|
||||
title="中文转英文翻译助手"
|
||||
subTitle="专业的中文到英文文本翻译工具,支持流式输出,翻译结果地道自然"
|
||||
style={{ marginBottom: '30px' }}
|
||||
/>
|
||||
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="large">
|
||||
<Card title="参数输入区">
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="source_content"
|
||||
label={
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||
<span>中文原文</span>
|
||||
<span style={{ color: currentInput.length > maxChars ? '#ff4d4f' : '#999' }}>
|
||||
{currentInput.length} / {maxChars}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
rules={[
|
||||
{ required: true, message: '请输入要翻译的中文内容' },
|
||||
{ max: maxChars, message: `输入内容不能超过${maxChars}个字符` }
|
||||
]}
|
||||
>
|
||||
<Input.TextArea
|
||||
placeholder="请输入需要翻译的中文内容(最多5000个字符)"
|
||||
rows={8}
|
||||
value={currentInput}
|
||||
onChange={(e) => setCurrentInput(e.target.value)}
|
||||
showCount
|
||||
maxLength={maxChars}
|
||||
style={{ fontSize: '14px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
<Space style={{ width: '100%', justifyContent: 'center' }} size="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={handleTranslate}
|
||||
loading={loading}
|
||||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||||
>
|
||||
翻译
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={handleClear}
|
||||
disabled={loading}
|
||||
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Card
|
||||
title="翻译结果"
|
||||
extra={
|
||||
translationResult && (
|
||||
<Button onClick={handleCopy} size="small">
|
||||
复制
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: '150px',
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
padding: '15px',
|
||||
backgroundColor: '#fafafa',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordWrap: 'break-word',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.6'
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
|
||||
正在翻译中,请稍候...
|
||||
</div>
|
||||
) : translationResult || (
|
||||
<div style={{ textAlign: 'center', color: '#999' }}>
|
||||
翻译结果将在这里显示
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Space>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ZhEnTranslator
|
||||
@@ -1,22 +1,59 @@
|
||||
/** 配置管理路由主文件 */
|
||||
import {createHashRouter} from 'react-router-dom'
|
||||
import {FrontEndCodeInterpreter} from '../pages/frontend-code-interpreter'
|
||||
import {TemplateRenderer} from "../pages/template-renderer";
|
||||
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 routers = createHashRouter([
|
||||
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: '/interpreter',
|
||||
element: <FrontEndCodeInterpreter></FrontEndCodeInterpreter>
|
||||
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: '/template-render',
|
||||
element: <TemplateRenderer></TemplateRenderer>
|
||||
},
|
||||
{
|
||||
path: '/not-found',
|
||||
element: <></>
|
||||
path: '*',
|
||||
element: <Navigate to='/404' />
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
export default routers
|
||||
export default createHashRouter(router)
|
||||
|
||||
43
src/utils/cacheUtil.ts
Normal file
43
src/utils/cacheUtil.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export function cacheGet(key: string): string {
|
||||
return localStorage.getItem(key) || ''
|
||||
}
|
||||
|
||||
export function cacheSet(key: string, value: string) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
export function cacheClear(key: string) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
export function cacheDeleteAll() {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
id?: string
|
||||
avatar?: string
|
||||
gender?: string
|
||||
nickName?: string
|
||||
username?: string
|
||||
position?: string
|
||||
deptName?: string
|
||||
mobile?: string
|
||||
bizMail?: string
|
||||
email?: string
|
||||
corpName?: string
|
||||
effectivePoint?: string
|
||||
totalPoint?: string
|
||||
}
|
||||
|
||||
export function getUserInfo(): UserInfo | null {
|
||||
const res = localStorage.getItem('userInfo')
|
||||
if (!res) {
|
||||
return null
|
||||
} else {
|
||||
return JSON.parse(res)
|
||||
}
|
||||
}
|
||||
export function getToken() {
|
||||
return localStorage.getItem('token')
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {WebContainer} from "@webcontainer/api";
|
||||
import {Terminal} from "@xterm/xterm";
|
||||
|
||||
export async function installDependencies(instance: WebContainer, terminal: Terminal) {
|
||||
const installProcess = await instance.spawn('npm', ['install']);
|
||||
terminal.write("依赖安装中...");
|
||||
installProcess.output.pipeTo(new WritableStream({
|
||||
write(data) {
|
||||
terminal.write(data);
|
||||
}
|
||||
}));
|
||||
const exitCode = await installProcess.exit;
|
||||
if (exitCode == 0) {
|
||||
terminal.write("依赖安装完毕,准备运行\n");
|
||||
return true;
|
||||
} else {
|
||||
terminal.write(`依赖安装失败,报错信息如上`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function startDevServer(instance: WebContainer, terminal: Terminal, callback: (url: string) => void) {
|
||||
instance.on('server-ready', (_port, url) => {
|
||||
terminal.write("服务启动完毕,即将进行加载");
|
||||
setTimeout(() => {
|
||||
callback(url);
|
||||
}, 500)
|
||||
});
|
||||
|
||||
terminal.write("启动服务中...");
|
||||
const serverProcess = await instance.spawn('npm', ['run', 'start']);
|
||||
serverProcess.output.pipeTo(
|
||||
new WritableStream({
|
||||
write(data) {
|
||||
terminal.write(data);
|
||||
},
|
||||
})
|
||||
)
|
||||
const exitCode = await serverProcess.exit;
|
||||
if (exitCode != 0) {
|
||||
terminal.write(`服务启动失败,报错信息如上`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeIndexJS(instance: WebContainer, content: string) {
|
||||
await instance.fs.writeFile('/index.js', content);
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export function extractJSONFromString(text: string) {
|
||||
try {
|
||||
// 首先尝试匹配 ```json``` 格式
|
||||
const jsonBlockPattern = /```(?:json|JSON)\s*\n?(.*?)\s*```/s;
|
||||
let match = text.trim().match(jsonBlockPattern);
|
||||
|
||||
if (match) {
|
||||
return match[1].trim();
|
||||
} else {
|
||||
// 如果没有找到```json```标记,尝试直接匹配JSON对象
|
||||
const jsonPattern = /{.*}/s;
|
||||
match = text.match(jsonPattern);
|
||||
if (match) {
|
||||
return match[0].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
throw new Error(`发生错误:${e.message}`);
|
||||
}
|
||||
}
|
||||
8
src/utils/polyfills.ts
Normal file
8
src/utils/polyfills.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
if (!Object.hasOwn) {
|
||||
Object.defineProperty(Object, 'hasOwn', {
|
||||
value: function(obj: any, prop: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(obj, prop)
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
const VITE_AI_API_BASE = import.meta.env['VITE_AI_API_BASE'] || "";
|
||||
|
||||
|
||||
export type SSERequest = {
|
||||
type: "chat" | "completion";
|
||||
query: string;
|
||||
inputs?: Record<string, string>;
|
||||
conversationId?: string;
|
||||
user: string;
|
||||
files?: object[];
|
||||
responseMode?: "streaming" | "blocking";
|
||||
onMessage: (message: string | null, finished: boolean) => void;
|
||||
onError: (error: Error) => void;
|
||||
onFinish: (metadata: object) => void;
|
||||
}
|
||||
|
||||
export class SSEClient {
|
||||
private readonly token: string;
|
||||
private controller: AbortController | null;
|
||||
|
||||
constructor(token: string) {
|
||||
this.controller = null;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
// 发送聊天消息并处理 SSE 响应
|
||||
async sendMessage({
|
||||
type = "chat",
|
||||
query,
|
||||
inputs = {},
|
||||
conversationId = '',
|
||||
user,
|
||||
files = [],
|
||||
responseMode = 'streaming',
|
||||
onMessage,
|
||||
onError,
|
||||
onFinish
|
||||
}: SSERequest) {
|
||||
try {
|
||||
// 创建 AbortController 用于取消请求
|
||||
this.controller = new AbortController();
|
||||
|
||||
// 准备请求数据
|
||||
const requestData = {
|
||||
query,
|
||||
inputs,
|
||||
response_mode: responseMode,
|
||||
conversation_id: conversationId,
|
||||
user,
|
||||
files
|
||||
};
|
||||
|
||||
// 发送请求
|
||||
const response = await fetch(`${VITE_AI_API_BASE}/${type === "chat" ? "chat-messages" : "completions"}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
body: JSON.stringify(requestData),
|
||||
signal: this.controller.signal
|
||||
});
|
||||
|
||||
if (response === null || response.body === null) throw new Error(`HTTP error!`);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
|
||||
// 获取响应的 ReadableStream
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 解码二进制数据
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
|
||||
// 处理完整的 SSE 消息
|
||||
const messages = buffer.split('\n\n');
|
||||
buffer = messages.pop() || ''; // 保留最后一个不完整的消息
|
||||
|
||||
for (const message of messages) {
|
||||
if (!message.trim() || !message.startsWith('data: ')) continue;
|
||||
|
||||
try {
|
||||
// 解析 SSE 消息
|
||||
const data = JSON.parse(message.slice(6));
|
||||
|
||||
// 根据不同的事件类型处理消息
|
||||
switch (data.event) {
|
||||
case 'message':
|
||||
onMessage?.(data.answer, false);
|
||||
break;
|
||||
|
||||
case 'message_end':
|
||||
onMessage?.(null, true);
|
||||
onFinish?.(data.metadata);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
onError?.(new Error(data.message));
|
||||
break;
|
||||
|
||||
case 'workflow_started':
|
||||
case 'node_started':
|
||||
case 'node_finished':
|
||||
case 'workflow_finished':
|
||||
// 处理工作流相关事件
|
||||
console.log(`Workflow event: ${data.event}`, data);
|
||||
break;
|
||||
|
||||
case 'tts_message':
|
||||
// 处理语音合成消息
|
||||
if (data.audio) {
|
||||
// 处理 base64 编码的音频数据
|
||||
console.log('Received TTS audio chunk');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tts_message_end':
|
||||
console.log('TTS streaming completed');
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing SSE message:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
onError?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 取消请求
|
||||
cancel() {
|
||||
if (this.controller) {
|
||||
this.controller.abort();
|
||||
this.controller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -1 +1,2 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '@tailwindcss/vite'
|
||||
Reference in New Issue
Block a user