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: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
element: LazyLoad(lazy(() => import('@/pages/home')))
|
element: LazyLoad(lazy(() => import('@/pages/ai-content-generator')))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/test1',
|
path: '/test1',
|
||||||
|
|||||||
Reference in New Issue
Block a user