| 1 | /** |
| 2 | * AI 社媒内容组件 - AI social media Page Content |
| 3 | * 客户端组件,包含所有交互逻辑 |
| 4 | */ |
| 5 | 'use client' |
| 6 | |
| 7 | import type { IHomeChatRef } from './components/HomeChat' |
| 8 | import { ArrowUp, Upload } from 'lucide-react' |
| 9 | import { useParams, useRouter, useSearchParams } from 'next/navigation' |
| 10 | import { useCallback, useEffect, useRef, useState } from 'react' |
| 11 | import { Button } from '@/components/ui/button' |
| 12 | import { useAgentStore } from '@/store/agent' |
| 13 | import { cn } from '@/utils/className' |
| 14 | import { toast } from '@/utils/ui/toast' |
| 15 | import { useTransClient } from '../../i18n/client' |
| 16 | import AgentFeatures from './components/AgentFeatures' |
| 17 | import EcosystemDiagram from './components/EcosystemDiagram' |
| 18 | import { HomeChat } from './components/HomeChat' |
| 19 | import PromptGallery from './components/PromptGallery' |
| 20 | import TaskPreview from './components/TaskPreview' |
| 21 | |
| 22 | export function AiSocialPageContent() { |
| 23 | const { t } = useTransClient('home') |
| 24 | |
| 25 | // Store 方法 |
| 26 | const { setDebugFiles } = useAgentStore() |
| 27 | |
| 28 | // 拖拽上传状态 |
| 29 | const [isDragging, setIsDragging] = useState(false) |
| 30 | const dragCounterRef = useRef(0) |
| 31 | const homeChatRef = useRef<IHomeChatRef>(null) |
| 32 | |
| 33 | // 拖拽事件处理 |
| 34 | const handleDragEnter = useCallback((e: React.DragEvent) => { |
| 35 | e.preventDefault() |
| 36 | e.stopPropagation() |
| 37 | dragCounterRef.current++ |
| 38 | if (e.dataTransfer.types.includes('Files')) { |
| 39 | setIsDragging(true) |
| 40 | } |
| 41 | }, []) |
| 42 | |
| 43 | const handleDragLeave = useCallback((e: React.DragEvent) => { |
| 44 | e.preventDefault() |
| 45 | e.stopPropagation() |
| 46 | dragCounterRef.current-- |
| 47 | if (dragCounterRef.current === 0) { |
| 48 | setIsDragging(false) |
| 49 | } |
| 50 | }, []) |
| 51 | |
| 52 | const handleDragOver = useCallback((e: React.DragEvent) => { |
| 53 | e.preventDefault() |
| 54 | e.stopPropagation() |
| 55 | }, []) |
| 56 | |
| 57 | const handleDrop = useCallback((e: React.DragEvent) => { |
| 58 | e.preventDefault() |
| 59 | e.stopPropagation() |
| 60 | setIsDragging(false) |
| 61 | dragCounterRef.current = 0 |
| 62 | |
| 63 | const files = e.dataTransfer.files |
| 64 | if (files.length > 0) { |
| 65 | homeChatRef.current?.handleFileDrop(files) |
| 66 | } |
| 67 | }, []) |
| 68 | |
| 69 | /** |
| 70 | * 回到顶部按钮组件 |
| 71 | * @param position 按钮位置,'left' 或 'right' |
| 72 | */ |
| 73 | function BackToTop({ position = 'left' }: { position?: 'left' | 'right' }) { |
| 74 | const [isVisible, setIsVisible] = useState(false) |
| 75 | |
| 76 | // 监听滚动显示/隐藏按钮 |
| 77 | useEffect(() => { |
| 78 | const handleScroll = () => { |
| 79 | // 滚动超过 400px 显示按钮 |
| 80 | setIsVisible(window.scrollY > 400) |
| 81 | } |
| 82 | |
| 83 | window.addEventListener('scroll', handleScroll, { passive: true }) |
| 84 | return () => window.removeEventListener('scroll', handleScroll) |
| 85 | }, []) |
| 86 | |
| 87 | // 点击回到顶部 |
| 88 | const scrollToTop = useCallback(() => { |
| 89 | window.scrollTo({ top: 0, behavior: 'smooth' }) |
| 90 | }, []) |
| 91 | |
| 92 | return ( |
| 93 | <Button |
| 94 | size="icon" |
| 95 | onClick={scrollToTop} |
| 96 | className={cn( |
| 97 | 'fixed bottom-8 z-50 w-12 h-12 rounded-full', |
| 98 | 'shadow-lg transition-all duration-300 transform', |
| 99 | position === 'left' ? 'left-8' : 'right-8', |
| 100 | isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none', |
| 101 | )} |
| 102 | aria-label="Back to top" |
| 103 | > |
| 104 | <ArrowUp className="w-5 h-5" /> |
| 105 | </Button> |
| 106 | ) |
| 107 | } |
| 108 | |
| 109 | const [appliedPrompt, setAppliedPrompt] = useState<string>('') |
| 110 | const [appliedMaterials, setAppliedMaterials] = useState<string[]>([]) |
| 111 | |
| 112 | // 处理提示词应用 |
| 113 | const handleApplyPrompt = useCallback( |
| 114 | (data: { prompt: string, materials?: string[], mode: 'edit' | 'generate' }) => { |
| 115 | setAppliedPrompt(data.prompt) |
| 116 | setAppliedMaterials(data.materials || []) |
| 117 | // 滚动到顶部 |
| 118 | window.scrollTo({ top: 0, behavior: 'smooth' }) |
| 119 | toast.success('Prompt applied!') |
| 120 | }, |
| 121 | [], |
| 122 | ) |
| 123 | |
| 124 | // 从 URL query 读取 agentExternalPrompt 和 agentTaskId(由任务页通过 query 传参) |
| 125 | const searchParams = useSearchParams() |
| 126 | const router = useRouter() |
| 127 | const [agentTaskId, setAgentTaskId] = useState<string>('') |
| 128 | const params = useParams() |
| 129 | |
| 130 | // 解析 debug URL 参数,设置 debug 模式 |
| 131 | useEffect(() => { |
| 132 | try { |
| 133 | const debugParam = searchParams.get('debug') |
| 134 | if (debugParam) { |
| 135 | // 解析 debug=[file1.txt,file2.txt] 或 debug=file1.txt,file2.txt 格式 |
| 136 | const cleanedParam = debugParam.replace(/^\[|\]$/g, '') |
| 137 | const files = cleanedParam |
| 138 | .split(',') |
| 139 | .map(f => f.trim()) |
| 140 | .filter(Boolean) |
| 141 | |
| 142 | if (files.length > 0) { |
| 143 | setDebugFiles(files) |
| 144 | |
| 145 | // 清理 URL 上的 debug 参数 |
| 146 | const url = new URL(window.location.href) |
| 147 | url.searchParams.delete('debug') |
| 148 | router.replace(url.pathname + url.search) |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | catch (e) { |
| 153 | console.warn('[AiSocialPageContent] Failed to parse debug param:', e) |
| 154 | } |
| 155 | }, [searchParams, router, setDebugFiles]) |
| 156 | |
| 157 | useEffect(() => { |
| 158 | try { |
| 159 | const prompt = searchParams.get('agentExternalPrompt') |
| 160 | const id = searchParams.get('agentTaskId') |
| 161 | if (prompt) { |
| 162 | setAppliedPrompt(prompt) |
| 163 | } |
| 164 | if (id) { |
| 165 | setAgentTaskId(id) |
| 166 | } |
| 167 | // 清理 URL 上的 query,避免重复 |
| 168 | if (prompt || id) { |
| 169 | router.replace(`/${params.lng}/ai-social`) |
| 170 | } |
| 171 | } |
| 172 | catch (e) { |
| 173 | // ignore |
| 174 | } |
| 175 | }, [searchParams, router, params.lng]) |
| 176 | |
| 177 | // 清除外部提示词 |
| 178 | const handleClearExternalPrompt = useCallback(() => { |
| 179 | setAppliedPrompt('') |
| 180 | setAppliedMaterials([]) |
| 181 | }, []) |
| 182 | |
| 183 | return ( |
| 184 | <div |
| 185 | className="bg-background" |
| 186 | onDragEnter={handleDragEnter} |
| 187 | onDragLeave={handleDragLeave} |
| 188 | onDragOver={handleDragOver} |
| 189 | onDrop={handleDrop} |
| 190 | > |
| 191 | {/* 拖拽遮罩层 */} |
| 192 | {isDragging && ( |
| 193 | <div className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm flex items-center justify-center pointer-events-none"> |
| 194 | <div className="flex flex-col items-center gap-4 p-8 rounded-2xl border-2 border-dashed border-primary bg-card"> |
| 195 | <Upload className="w-12 h-12 text-primary" /> |
| 196 | <p className="text-lg font-medium text-foreground">{t('dropToUpload')}</p> |
| 197 | </div> |
| 198 | </div> |
| 199 | )} |
| 200 | |
| 201 | {/* 首屏 Chat 区域 */} |
| 202 | <section className="min-h-[60vh] flex items-center justify-center px-4 pt-16 pb-8 md:pt-24 md:pb-12"> |
| 203 | <HomeChat |
| 204 | ref={homeChatRef} |
| 205 | externalPrompt={appliedPrompt} |
| 206 | externalMaterials={appliedMaterials} |
| 207 | onClearExternalPrompt={handleClearExternalPrompt} |
| 208 | agentTaskId={agentTaskId} |
| 209 | /> |
| 210 | </section> |
| 211 | |
| 212 | {/* 任务预览区域 - 无数据时自动隐藏 */} |
| 213 | <TaskPreview limit={4} className="px-4 py-8" /> |
| 214 | |
| 215 | {/* /!* 提示词画廊区域 *!/ */} |
| 216 | <PromptGallery onApplyPrompt={handleApplyPrompt} /> |
| 217 | |
| 218 | {/* AI Agent 功能亮点(独立展示) */} |
| 219 | <AgentFeatures /> |
| 220 | |
| 221 | {/* 一图解读 AiToEarn 生态 */} |
| 222 | <EcosystemDiagram /> |
| 223 | |
| 224 | {/* 回到顶部按钮 - 右侧 */} |
| 225 | <BackToTop position="right" /> |
| 226 | </div> |
| 227 | ) |
| 228 | } |
| 229 |