diff --git a/src/api/ai-content-generator.ts b/src/api/ai-content-generator.ts new file mode 100644 index 0000000..1feb936 --- /dev/null +++ b/src/api/ai-content-generator.ts @@ -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 { + 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 { + 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() + } +} diff --git a/src/pages/ai-content-generator/index.tsx b/src/pages/ai-content-generator/index.tsx new file mode 100644 index 0000000..259079e --- /dev/null +++ b/src/pages/ai-content-generator/index.tsx @@ -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('') + const [isGenerating, setIsGenerating] = useState(false) + const [uploadedFiles, setUploadedFiles] = useState([]) + const [fileList, setFileList] = useState([]) + const abortControllerRef = useRef(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 ( +
+
+ {/* 页面标题区 */} +
+ + AI内容生成工具 + +
+ + {/* 参数输入区 */} + +
+ +