Update from Vibe Studio
This commit is contained in:
136
src/api/ai-content-generator.ts
Normal file
136
src/api/ai-content-generator.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
// Dify API 配置
|
||||
const API_BASE = 'https://copilot.sino-bridge.com'
|
||||
const API_KEY = 'app-ZA5VuDW1OPQZfY4hrGyFGH6s'
|
||||
|
||||
interface UploadResponse {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
extension: string
|
||||
mime_type: string
|
||||
}
|
||||
|
||||
interface GeneratedFile {
|
||||
file_id: string
|
||||
filename: string
|
||||
preview_url?: string
|
||||
transfer_method: string
|
||||
user: string
|
||||
}
|
||||
|
||||
interface GenerateContentParams {
|
||||
inputs: {
|
||||
user_input: string
|
||||
}
|
||||
query: string
|
||||
response_mode: 'streaming' | 'blocking'
|
||||
files?: GeneratedFile[]
|
||||
}
|
||||
|
||||
interface GenerateContentResponse {
|
||||
event: string
|
||||
task_id?: string
|
||||
id?: string
|
||||
message?: string
|
||||
mode?: string
|
||||
answer?: string
|
||||
metadata?: {
|
||||
usage: {
|
||||
total_price: number
|
||||
currency: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到 Dify
|
||||
*/
|
||||
export async function uploadFile(file: File): Promise<UploadResponse> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await fetch(`${API_BASE}/v1/files/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`上传失败: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Dify 生成内容(流式响应)
|
||||
*/
|
||||
export async function generateContent(
|
||||
params: GenerateContentParams,
|
||||
onChunk: (chunk: string) => void,
|
||||
signal?: AbortSignal
|
||||
): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/v1/chat-messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
signal
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`请求失败: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
throw new Error('无法读取响应流')
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value)
|
||||
const lines = chunk.split('\n').filter(line => line.trim())
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6)
|
||||
if (data === '[DONE]') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed: GenerateContentResponse = JSON.parse(data)
|
||||
|
||||
// 处理不同的消息事件
|
||||
if (parsed.event === 'message') {
|
||||
if (parsed.answer) {
|
||||
onChunk(parsed.answer)
|
||||
}
|
||||
} else if (parsed.event === 'message_file') {
|
||||
// 处理文件相关消息(如果有)
|
||||
console.log('Message file event:', parsed)
|
||||
} else if (parsed.event === 'error') {
|
||||
throw new Error(parsed.message || '生成过程中发生错误')
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('解析响应数据失败:', data, parseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
}
|
||||
233
src/pages/ai-content-generator/index.tsx
Normal file
233
src/pages/ai-content-generator/index.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { useState, useRef } from 'react'
|
||||
import { Card, Form, Input, Button, Upload, message, Space, Typography } from 'antd'
|
||||
import { UploadOutlined, DownloadOutlined, SendOutlined } from '@ant-design/icons'
|
||||
import type { UploadFile, UploadProps } from 'antd/es/upload/interface'
|
||||
import { generateContent, uploadFile } from '@/api/ai-content-generator'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const { TextArea } = Input
|
||||
|
||||
interface GeneratedFile {
|
||||
file_id: string
|
||||
filename: string
|
||||
preview_url?: string
|
||||
transfer_method: string
|
||||
user: string
|
||||
}
|
||||
|
||||
interface FormData {
|
||||
user_input: string
|
||||
}
|
||||
|
||||
const AIContentGenerator: React.FC = () => {
|
||||
const [form] = Form.useForm()
|
||||
const [generatedResult, setGeneratedResult] = useState<string>('')
|
||||
const [isGenerating, setIsGenerating] = useState<boolean>(false)
|
||||
const [uploadedFiles, setUploadedFiles] = useState<GeneratedFile[]>([])
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([])
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
// 文件上传处理
|
||||
const handleUpload = async (file: File) => {
|
||||
try {
|
||||
const result = await uploadFile(file)
|
||||
const newFile: GeneratedFile = {
|
||||
file_id: result.id,
|
||||
filename: result.name,
|
||||
preview_url: '1',
|
||||
transfer_method: 'local_file',
|
||||
user: 'admin'
|
||||
}
|
||||
setUploadedFiles(prev => [...prev, newFile])
|
||||
message.success(`文件 "${file.name}" 上传成功`)
|
||||
return false // 阻止antd默认上传
|
||||
} catch (error) {
|
||||
message.error(`文件上传失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const uploadProps: UploadProps = {
|
||||
beforeUpload: handleUpload,
|
||||
fileList,
|
||||
onChange: ({ fileList: newFileList }) => {
|
||||
setFileList(newFileList)
|
||||
},
|
||||
onRemove: (file) => {
|
||||
const index = fileList.indexOf(file)
|
||||
const newFileList = fileList.slice()
|
||||
newFileList.splice(index, 1)
|
||||
setFileList(newFileList)
|
||||
setUploadedFiles(prev => prev.filter(f => f.filename !== file.name))
|
||||
},
|
||||
accept: '.txt,.pdf,.doc,.docx,.md,.json',
|
||||
maxCount: 5,
|
||||
multiple: true
|
||||
}
|
||||
|
||||
// 生成内容处理
|
||||
const handleGenerate = async (values: FormData) => {
|
||||
if (!values.user_input.trim()) {
|
||||
message.warning('请输入提示词')
|
||||
return
|
||||
}
|
||||
|
||||
setIsGenerating(true)
|
||||
setGeneratedResult('')
|
||||
|
||||
// 取消之前的请求
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort()
|
||||
}
|
||||
abortControllerRef.current = new AbortController()
|
||||
|
||||
try {
|
||||
await generateContent({
|
||||
inputs: {
|
||||
user_input: values.user_input
|
||||
},
|
||||
query: '1',
|
||||
response_mode: 'streaming',
|
||||
files: uploadedFiles
|
||||
}, (chunk) => {
|
||||
setGeneratedResult(prev => prev + chunk)
|
||||
}, abortControllerRef.current.signal)
|
||||
|
||||
message.success('内容生成完成')
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
message.info('生成已取消')
|
||||
} else {
|
||||
message.error(`生成失败: ${error instanceof Error ? error.message : '未知错误'}`)
|
||||
}
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出结果
|
||||
const handleExport = () => {
|
||||
if (!generatedResult.trim()) {
|
||||
message.warning('没有可导出的内容')
|
||||
return
|
||||
}
|
||||
|
||||
const blob = new Blob([generatedResult], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `AI生成内容_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
message.success('导出成功')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* 页面标题区 */}
|
||||
<div className="text-center mb-8">
|
||||
<Title level={2} style={{ color: '#1890ff', marginBottom: 0 }}>
|
||||
AI内容生成工具
|
||||
</Title>
|
||||
</div>
|
||||
|
||||
{/* 参数输入区 */}
|
||||
<Card className="mb-6 shadow-sm">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleGenerate}
|
||||
>
|
||||
<Form.Item
|
||||
name="user_input"
|
||||
label="提示词"
|
||||
rules={[{ required: true, message: '请输入提示词' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="请输入您想要生成的内容描述..."
|
||||
style={{ fontSize: '14px' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="上传文件(可选)">
|
||||
<Upload {...uploadProps}>
|
||||
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||
</Upload>
|
||||
<Text type="secondary" style={{ display: 'block', marginTop: 8 }}>
|
||||
支持 txt, pdf, doc, docx, md, json 格式,最多5个文件
|
||||
</Text>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SendOutlined />}
|
||||
loading={isGenerating}
|
||||
style={{ height: '36px' }}
|
||||
>
|
||||
{isGenerating ? '生成中...' : '生成'}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleExport}
|
||||
disabled={!generatedResult.trim()}
|
||||
style={{ height: '36px' }}
|
||||
>
|
||||
导出
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 内容展示区 */}
|
||||
<Card
|
||||
title="生成结果"
|
||||
className="shadow-sm"
|
||||
style={{ minHeight: '300px' }}
|
||||
>
|
||||
{generatedResult ? (
|
||||
<div
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.6,
|
||||
color: '#262626'
|
||||
}}
|
||||
>
|
||||
{generatedResult}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#8c8c8c',
|
||||
padding: '60px 0',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
生成的内容将在这里显示...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGenerating && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#1890ff',
|
||||
padding: '20px 0',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
正在生成内容,请稍候...
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AIContentGenerator
|
||||
@@ -30,7 +30,7 @@ const router: RouteObject[] = [
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
element: LazyLoad(lazy(() => import('@/pages/home')))
|
||||
element: LazyLoad(lazy(() => import('@/pages/ai-content-generator')))
|
||||
},
|
||||
{
|
||||
path: '/test1',
|
||||
|
||||
Reference in New Issue
Block a user