Update from Vibe Studio

This commit is contained in:
Vibe Studio
2026-01-22 08:32:35 +00:00
parent 976704f9ec
commit 502fa340d2
3 changed files with 370 additions and 1 deletions

View 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()
}
}

View 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

View File

@@ -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',