Initial commit: Copilot Toolbox template project

This commit is contained in:
Evan
2026-01-09 10:49:51 +08:00
commit 0b4b053566
31 changed files with 12085 additions and 0 deletions

44
src/App.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { RouterProvider } from 'react-router-dom'
import { useEffect } from 'react'
import router from '@/router'
import { cacheClear, cacheSet } from '@/utils/cacheUtil'
import { getAgentBaseInfo, getUserInfoById } from '@/api/common'
function App() {
const queryString = window.location.search
const searchParams = new URLSearchParams(queryString)
const tenantId = searchParams.get('tenantId') || ''
const token = searchParams.get('token') || ''
cacheSet('tenantId', tenantId)
cacheSet('token', token)
useEffect(() => {
const agentId = searchParams.get('agentId') || ''
if (agentId) {
getAgentBaseInfo({ id: agentId }).then(res => {
cacheSet('appKey', res.data.appKey)
})
}
}, [])
useEffect(() => {
if (tenantId && token) {
getUserInfoById().then(response => {
if (response.code === 200 && response.data) {
cacheSet('userInfo', JSON.stringify(response.data))
}
})
}
return () => {
cacheClear('userInfo')
}
}, [])
return (
<>
<RouterProvider router={router} />
</>
)
}
export default App

28
src/api/common.ts Normal file
View File

@@ -0,0 +1,28 @@
import { cacheGet } from '@/utils/cacheUtil'
export function getAgentBaseInfo(params: { id: string }) {
const Tenantid = cacheGet('tenantId')
const Token = cacheGet('token')
return fetch(`${import.meta.env.VITE_API_BASE_AI}/agent/baseInfo/info?id=${params.id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Tenantid,
Token
}
}).then(res => res.json())
}
// 获取用户信息
export async function getUserInfoById() {
const Tenantid = cacheGet('tenantId')
const Token = cacheGet('token')
return fetch(`${import.meta.env['VITE_API_BASE_LAMP']}/oauth/anyone/getUserInfoById`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Tenantid,
Token
}
}).then(res => res.json())
}

View File

@@ -0,0 +1,34 @@
export interface TranslationRequest {
source_content: string
}
export interface DifyRequest {
inputs: {
prompt: string
}
query: string
response_mode: string
}
export function translateChineseToEnglish(source_content: string) {
const prompt = `你是一个专业的中英翻译专家。请将下面的中文文本翻译成自然、地道、专业的英文,只返回翻译后的英文内容,不要添加任何额外的解释或格式。待翻译的中文内容是:
${source_content}`
const requestBody: DifyRequest = {
inputs: {
prompt
},
query: '1',
response_mode: 'streaming'
}
return fetch('https://copilot.sino-bridge.com/v1/chat-messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer app-Y6ekYkw3aoUV3jmfZdg24Adh'
},
body: JSON.stringify(requestBody)
})
}

29
src/index.css Normal file
View File

@@ -0,0 +1,29 @@
@import 'tailwindcss';
.route-loading {
position: fixed;
top: 50%;
left: 50%;
width: 120px;
height: 22px;
color: #514b82;
border: 2px solid;
border-radius: 20px;
transform: translate(-50%, -50%);
}
@keyframes l6 {
100% {
inset: 0;
}
}
.route-loading::before {
position: absolute;
inset: 0 100% 0 0;
margin: 2px;
content: '';
background: currentcolor;
border-radius: inherit;
animation: l6 2s infinite;
}

12
src/main.tsx Normal file
View 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
View File

@@ -0,0 +1,57 @@
import { Card, Row, Col, Typography, Space } from 'antd'
import { Link } from 'react-router-dom'
import { RobotOutlined, TranslationOutlined, FileTextOutlined } from '@ant-design/icons'
const { Title, Paragraph } = Typography
const cards = [
{
title: '测试页面 1',
description: 'Dify AI Agent 集成示例',
icon: <RobotOutlined style={{ fontSize: 32, color: '#1890ff' }} />,
link: '/test1'
},
{
title: '测试页面 2',
description: '功能开发中...',
icon: <FileTextOutlined style={{ fontSize: 32, color: '#52c41a' }} />,
link: '/test2'
},
{
title: '中英翻译器',
description: 'AI 驱动的翻译工具',
icon: <TranslationOutlined style={{ fontSize: 32, color: '#722ed1' }} />,
link: '/zh-en-translator'
}
]
const HomePage: React.FC = () => {
return (
<div style={{ padding: 24 }}>
<Title level={2}>使 AI </Title>
<Paragraph style={{ marginBottom: 32 }}>
使访
</Paragraph>
<Row gutter={[16, 16]}>
{cards.map((card, index) => (
<Col xs={24} sm={12} md={8} key={index}>
<Link to={card.link}>
<Card hoverable style={{ height: '100%' }}>
<Space direction='vertical' align='center' style={{ width: '100%' }}>
{card.icon}
<Title level={4} style={{ marginBottom: 0 }}>{card.title}</Title>
<Paragraph type='secondary' style={{ marginBottom: 0 }}>
{card.description}
</Paragraph>
</Space>
</Card>
</Link>
</Col>
))}
</Row>
</div>
)
}
export default HomePage

81
src/pages/test1/index.tsx Normal file
View File

@@ -0,0 +1,81 @@
import { AgUiEventType, DifyAgent } from '@shangzy/ag-ui-dify'
import { RunAgentInput, EventType, TextMessageContentEvent } from '@ag-ui/client'
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
import { useState } from 'react'
const Test1: React.FC = () => {
const [message, setMessage] = useState('')
const userInfo = getUserInfo()
const runAgent = () => {
const difyJson = {
conversation_id: '',
files: [],
query: '1312313',
appKey: cacheGet('appKey'),
inputs: {
Token: cacheGet('token'),
tenantid: cacheGet('tenantId')
},
user: userInfo?.id || 'anonymous'
}
const content: string = JSON.stringify(difyJson)
// 准备输入参数
const input: RunAgentInput = {
threadId: new Date().getTime().toString(),
runId: new Date().getTime().toString(),
messages: [
{
id: new Date().getTime().toString(),
role: 'user',
content: content
}
],
context: [],
tools: []
}
// 订阅Agent事件
new DifyAgent({
baseUrl: '/dify',
showMetadata: true
})
.run(input)
.subscribe({
next: (event: AgUiEventType) => {
console.log('🚀 ~ ChatApp ~ aa ~ event:', event)
try {
switch (event.type) {
case EventType.RUN_STARTED:
// 可以在这里处理运行开始事件
break
case EventType.TEXT_MESSAGE_START:
// 处理消息开始事件
break
case EventType.TEXT_MESSAGE_CONTENT:
const textEvent = event as TextMessageContentEvent
setMessage(prev => prev + textEvent.delta)
break
case EventType.TEXT_MESSAGE_END:
break
case EventType.RUN_FINISHED:
break
}
} catch (err) {
console.error('处理事件时出错:', err)
}
},
error: () => {},
complete: () => {}
})
}
return (
<>
<div onClick={() => runAgent()}></div>
<div>{message}</div>
</>
)
}
export default Test1

158
src/pages/test2/index.tsx Normal file
View File

@@ -0,0 +1,158 @@
import { cacheGet, getUserInfo } from '@/utils/cacheUtil'
import { LoadingOutlined } from '@ant-design/icons'
import { CopilotKit, useCopilotChatInternal as useCopilotChat } from '@copilotkit/react-core'
import { Flex, Mentions, Spin } from 'antd'
import { useCallback, useState } from 'react'
const Chat: React.FC = () => {
const [currentAppKey, setCurrentAppKey] = useState<string>('')
const [newMessage, setNewMessage] = useState('')
const { messages, sendMessage, setMessages, isLoading, reloadMessages, stopGeneration } = useCopilotChat()
const callSendMessage = useCallback(
async (message: string) => {
await sendMessage({
id: new Date().getTime() + '',
role: 'user',
content: message
})
},
[sendMessage]
)
const handleSendMessage = useCallback(() => {
// 提前存好本次的提问内容,重新生成的话直接从缓存中获取之前的提问内容
let question: string = newMessage || ''
const token = cacheGet('token')
const tenantId = cacheGet('tenantId')
let conversation_id = ''
if (messages[1]?.id) {
conversation_id = messages[1]?.id.split('_')[0]
}
const userInfo = getUserInfo()
const difyJson = {
inputs: {
Token: token || '',
tenantid: tenantId || '',
query: question
},
appKey: currentAppKey,
files: [],
user: userInfo?.id || 'anonymous',
query: question,
conversation_id
}
// 设置好目前状态下的聊天列表数据包含之前已经结束的沟通内容以及本次用户的提问本次AI的回答占位
callSendMessage(JSON.stringify(difyJson))
setNewMessage('')
}, [callSendMessage, newMessage, currentAppKey])
const handleParse = (jsonStr: string) => {
let res = ''
try {
const parsed = JSON.parse(jsonStr)
res = parsed?.query
} catch (e) {}
return res
}
// 换行
const handleNewline = () => {
setNewMessage(prevValue => `${prevValue}\n`)
}
const handleKeyDown = async (e: any) => {
e.stopPropagation()
const res = await cacheGet('sendMessage')
if (e.key === 'Enter') {
if (e.ctrlKey) {
if (newMessage) {
if (res === 'ctrlEnter') {
handleSendMessage()
} else {
handleNewline()
}
}
} else {
if (res === 'Enter' || !res) {
if (newMessage) {
handleSendMessage()
} else {
setNewMessage('')
}
}
}
}
}
return (
<>
{messages.map((message, index) => {
const userMessageId = message.id
// 实际的会话消息
return (
<Flex key={index} vertical data-message-id={userMessageId}>
{/* 用户提问 */}
{message.role === 'user' && <Flex key={message.id}>{handleParse(message.content as string) ?? ''}</Flex>}
{message.role === 'assistant' && (
<div>
<div
style={{
width: 'calc(100% - 20px)',
marginLeft: '20px'
}}
>
<Flex>
{isLoading && !message.content && index === messages.length - 1 && (
<Flex>
<Spin indicator={<LoadingOutlined />} />
<span></span>
</Flex>
)}
{message.content ?? ''}
</Flex>
{message?.generativeUI?.()}
</div>
</div>
)}
</Flex>
)
})}
<div className='p-4'>
<Mentions
autoFocus
open={false}
placeholder='请输入内容'
rows={4}
value={newMessage}
maxLength={10000}
onKeyDown={handleKeyDown}
options={[]}
onInput={e => {
const value = (e.target as HTMLInputElement).value
// 检查内容是否只包含空格或回车符
if (/^\s*$/.test(value)) {
setNewMessage('') // 如果只包含空格或回车符,清空输入框
} else {
setNewMessage(value) // 否则更新输入内容
}
}}
/>
</div>
</>
)
}
const Test2: React.FC = () => {
return (
<CopilotKit
runtimeUrl='/agui-api/copilotkit/dify'
showDevConsole={false}
// publicApiKey={'ck_pub_cc922145a5da9b8513bc10df473cd6f7'}
agent='agentic_chat_metadata'
>
<Chat />
</CopilotKit>
)
}
export default Test2

View File

@@ -0,0 +1,216 @@
import { useState } from 'react'
import { Button, Card, Form, Input, message, Space } from 'antd'
import { PageHeader } from '@ant-design/pro-components'
import { translateChineseToEnglish } from '@/api/zh-en-translator'
const ZhEnTranslator = () => {
const [form] = Form.useForm()
const [loading, setLoading] = useState(false)
const [translationResult, setTranslationResult] = useState('')
const [currentInput, setCurrentInput] = useState('')
const maxChars = 5000
const handleTranslate = async () => {
try {
const source_content = form.getFieldValue('source_content') || ''
if (!source_content.trim()) {
message.warning('请输入要翻译的中文内容')
return
}
setLoading(true)
setTranslationResult('')
const response = await translateChineseToEnglish(source_content)
if (!response.ok) {
const errorText = await response.text()
throw new Error(`翻译请求失败: ${response.status} ${errorText}`)
}
if (!response.body) {
throw new Error('响应体为空')
}
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
let fullContent = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
if (trimmedLine === 'data: [DONE]') {
message.success('翻译完成')
setLoading(false)
return
}
continue
}
if (trimmedLine.startsWith('data: ')) {
try {
const data = trimmedLine.slice(6)
const parsed = JSON.parse(data)
if (parsed.event === 'message' && parsed.answer) {
fullContent += parsed.answer
setTranslationResult(fullContent)
} else if (parsed.event === 'error') {
throw new Error(parsed.message || 'Dify API 返回错误')
}
} catch (parseError) {
console.warn('跳过无法解析的行:', trimmedLine)
}
}
}
}
} finally {
reader.releaseLock()
}
if (fullContent) {
message.success('翻译完成')
} else {
throw new Error('未收到翻译结果')
}
} catch (error) {
console.error('翻译错误:', error)
message.error(error instanceof Error ? error.message : '翻译失败,请稍后重试')
} finally {
setLoading(false)
}
}
const handleClear = () => {
form.resetFields()
setTranslationResult('')
setCurrentInput('')
message.info('已清空内容')
}
const handleCopy = async () => {
if (!translationResult) {
message.warning('没有可复制的内容')
return
}
try {
await navigator.clipboard.writeText(translationResult)
message.success('复制成功')
} catch (error) {
console.error('复制失败:', error)
message.error('复制失败,请手动复制')
}
}
return (
<div style={{ maxWidth: '900px', margin: '0 auto', padding: '20px' }}>
<PageHeader
title="中文转英文翻译助手"
subTitle="专业的中文到英文文本翻译工具,支持流式输出,翻译结果地道自然"
style={{ marginBottom: '30px' }}
/>
<Space direction="vertical" style={{ width: '100%' }} size="large">
<Card title="参数输入区">
<Form form={form} layout="vertical">
<Form.Item
name="source_content"
label={
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<span></span>
<span style={{ color: currentInput.length > maxChars ? '#ff4d4f' : '#999' }}>
{currentInput.length} / {maxChars}
</span>
</div>
}
rules={[
{ required: true, message: '请输入要翻译的中文内容' },
{ max: maxChars, message: `输入内容不能超过${maxChars}个字符` }
]}
>
<Input.TextArea
placeholder="请输入需要翻译的中文内容最多5000个字符"
rows={8}
value={currentInput}
onChange={(e) => setCurrentInput(e.target.value)}
showCount
maxLength={maxChars}
style={{ fontSize: '14px' }}
/>
</Form.Item>
</Form>
</Card>
<Space style={{ width: '100%', justifyContent: 'center' }} size="middle">
<Button
type="primary"
size="large"
onClick={handleTranslate}
loading={loading}
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
>
</Button>
<Button
size="large"
onClick={handleClear}
disabled={loading}
style={{ minWidth: '120px', height: '40px', fontSize: '16px' }}
>
</Button>
</Space>
<Card
title="翻译结果"
extra={
translationResult && (
<Button onClick={handleCopy} size="small">
</Button>
)
}
>
<div
style={{
minHeight: '150px',
maxHeight: '400px',
overflowY: 'auto',
padding: '15px',
backgroundColor: '#fafafa',
border: '1px solid #d9d9d9',
borderRadius: '4px',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
fontSize: '14px',
lineHeight: '1.6'
}}
>
{loading ? (
<div style={{ textAlign: 'center', color: '#999', padding: '20px' }}>
...
</div>
) : translationResult || (
<div style={{ textAlign: 'center', color: '#999' }}>
</div>
)}
</div>
</Card>
</Space>
</div>
)
}
export default ZhEnTranslator

59
src/router/index.tsx Normal file
View 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
View File

@@ -0,0 +1,43 @@
export function cacheGet(key: string): string {
return localStorage.getItem(key) || ''
}
export function cacheSet(key: string, value: string) {
localStorage.setItem(key, value)
}
export function cacheClear(key: string) {
localStorage.removeItem(key)
}
export function cacheDeleteAll() {
localStorage.clear()
}
export interface UserInfo {
id?: string
avatar?: string
gender?: string
nickName?: string
username?: string
position?: string
deptName?: string
mobile?: string
bizMail?: string
email?: string
corpName?: string
effectivePoint?: string
totalPoint?: string
}
export function getUserInfo(): UserInfo | null {
const res = localStorage.getItem('userInfo')
if (!res) {
return null
} else {
return JSON.parse(res)
}
}
export function getToken() {
return localStorage.getItem('token')
}

8
src/utils/polyfills.ts Normal file
View File

@@ -0,0 +1,8 @@
if (!Object.hasOwn) {
Object.defineProperty(Object, 'hasOwn', {
value: function(obj: any, prop: string): boolean {
return Object.prototype.hasOwnProperty.call(obj, prop)
},
configurable: true
})
}

2
src/vite-env.d.ts vendored Normal file
View File

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