返回 AiToEarn
page.tsx
根目录 / project / aitoearn-web / src / app / [lng] / chat / page.tsx
1 /**
2 * 分享对话查看页面 - Shared Chat View
3 * 功能:通过分享 token 查看对话记录(只读模式)
4 * 路由:/chat?token=xxx
5 */
6 'use client'
7
8 import type { TaskDetail } from '@/api/ai/ai.types'
9 import { ArrowLeft, Eye, Link2Off } from 'lucide-react'
10 import { useRouter, useSearchParams } from 'next/navigation'
11 import { useCallback, useEffect, useRef, useState } from 'react'
12 import { agentApi } from '@/api/ai/ai.api'
13 import { useTransClient } from '@/app/i18n/client'
14 import { ChatMessage } from '@/components/Chat/ChatMessage'
15 import { Button } from '@/components/ui/button'
16 import { Skeleton } from '@/components/ui/skeleton'
17 import { useDocumentTitle } from '@/hooks'
18 import { convertMessages } from './[taskId]/utils'
19
20 /** 分享页面加载骨架屏 */
21 function SharedChatSkeleton() {
22 return (
23 <div className="flex flex-col h-full">
24 {/* Header skeleton */}
25 <div className="flex items-center gap-3 px-4 py-3">
26 <Skeleton className="w-8 h-8 rounded-md" />
27 <Skeleton className="h-5 w-40" />
28 </div>
29 {/* Messages skeleton */}
30 <div className="flex-1 px-4 py-4">
31 <div className="max-w-6xl mx-auto space-y-4">
32 {Array.from({ length: 4 }).map((_, i) => (
33 <div key={i} className="flex gap-3">
34 <Skeleton className="w-10 h-10 rounded-full shrink-0" />
35 <div className="flex-1 space-y-2">
36 <Skeleton className="h-4 w-32" />
37 <Skeleton className="h-16 w-full rounded-lg" />
38 </div>
39 </div>
40 ))}
41 </div>
42 </div>
43 </div>
44 )
45 }
46
47 /** 分享链接无效/过期提示 */
48 function SharedChatError({ type, onBack }: { type: 'notFound' | 'expired', onBack: () => void }) {
49 const { t } = useTransClient('share')
50
51 return (
52 <div className="flex flex-col h-full">
53 {/* Header */}
54 <header className="flex items-center gap-3 px-4 py-3 shrink-0">
55 <Button variant="ghost" size="icon" onClick={onBack} className="w-8 h-8 cursor-pointer">
56 <ArrowLeft className="w-5 h-5" />
57 </Button>
58 <h1 className="text-base font-medium text-foreground">{t('sharedConversation')}</h1>
59 </header>
60
61 {/* Error content */}
62 <div className="flex-1 flex items-center justify-center p-8">
63 <div className="flex flex-col items-center gap-4 text-center max-w-sm">
64 <div className="w-16 h-16 rounded-full bg-muted-foreground/10 flex items-center justify-center">
65 <Link2Off className="w-8 h-8 text-muted-foreground" />
66 </div>
67 <h2 className="text-lg font-medium text-foreground">
68 {type === 'expired' ? t('linkExpired') : t('linkNotFound')}
69 </h2>
70 <p className="text-sm text-muted-foreground">
71 {type === 'expired'
72 ? t('linkExpiredDesc')
73 : t('linkNotFoundDesc')}
74 </p>
75 <Button onClick={onBack} className="mt-2 cursor-pointer">
76 {t('backToHome')}
77 </Button>
78 </div>
79 </div>
80 </div>
81 )
82 }
83
84 export default function SharedChatPage() {
85 const { t } = useTransClient('share')
86 const router = useRouter()
87 const searchParams = useSearchParams()
88 const token = searchParams.get('token')
89
90 // 状态
91 const [loading, setLoading] = useState(true)
92 const [error, setError] = useState<'notFound' | 'expired' | null>(null)
93 const [taskDetail, setTaskDetail] = useState<TaskDetail | null>(null)
94
95 // 滚动控制
96 const containerRef = useRef<HTMLDivElement>(null)
97 const bottomRef = useRef<HTMLDivElement>(null)
98
99 // 动态更新页面标题
100 useDocumentTitle(taskDetail?.title, t('sharedConversation'))
101
102 // 加载分享数据
103 useEffect(() => {
104 if (!token) {
105 setError('notFound')
106 setLoading(false)
107 return
108 }
109
110 const loadSharedTask = async () => {
111 try {
112 setLoading(true)
113 setError(null)
114
115 let data: TaskDetail | null = null
116
117 // Debug 模式:从本地文件加载数据
118 if (token === 'debug') {
119 const res = await fetch('/en/agent测试res查询发布详情数据.txt')
120 const json = await res.json()
121 data = json.data
122 }
123 else {
124 // 正常模式:调用 API
125 const res = await agentApi.getTaskByShareToken(token)
126 data = res?.data ?? null
127 }
128
129 if (data) {
130 setTaskDetail(data)
131 }
132 else {
133 setError('notFound')
134 }
135 }
136 catch (err: any) {
137 console.error('Failed to load shared task:', err)
138 // 根据错误类型判断是过期还是不存在
139 if (err?.response?.status === 410 || (typeof err?.message === 'string' && err.message.includes('expired'))) {
140 setError('expired')
141 }
142 else {
143 setError('notFound')
144 }
145 }
146 finally {
147 setLoading(false)
148 }
149 }
150
151 loadSharedTask()
152 }, [token])
153
154 // 返回首页
155 const handleBack = useCallback(() => {
156 router.push('/')
157 }, [router])
158
159 // 转换消息格式
160 const displayMessages = taskDetail?.messages ? convertMessages(taskDetail.messages) : []
161
162 // 过滤出用户和 AI 消息
163 const filteredMessages = displayMessages.filter(
164 message => message.role === 'user' || message.role === 'assistant',
165 )
166
167 // 加载中
168 if (loading) {
169 return <SharedChatSkeleton />
170 }
171
172 // 错误状态
173 if (error) {
174 return <SharedChatError type={error} onBack={handleBack} />
175 }
176
177 return (
178 <div className="flex flex-col h-full">
179 {/* 顶部导航 - 只读模式 */}
180 <header className="flex items-center gap-3 px-4 py-3 shrink-0">
181 {/* 返回按钮 */}
182 <Button variant="ghost" size="icon" onClick={handleBack} className="w-8 h-8 cursor-pointer">
183 <ArrowLeft className="w-5 h-5" />
184 </Button>
185
186 {/* 标题 */}
187 <h1 className="text-base font-medium text-foreground line-clamp-1 flex-1">
188 {taskDetail?.title || t('sharedConversation')}
189 </h1>
190
191 {/* 只读标识 */}
192 <div className="flex items-center gap-1.5 px-2 py-1 rounded bg-muted/50 text-muted-foreground">
193 <Eye className="w-3.5 h-3.5" />
194 <span className="text-xs">{t('viewOnly')}</span>
195 </div>
196 </header>
197
198 {/* 消息列表 - 只读 */}
199 <div className="flex-1 relative overflow-hidden">
200 <div ref={containerRef} className="h-full overflow-y-auto">
201 <div className="max-w-6xl mx-auto px-4 pt-4 pb-4 flex gap-4 flex-col">
202 {filteredMessages.map(message => (
203 <ChatMessage
204 key={message.id}
205 role={message.role as 'user' | 'assistant'}
206 content={message.content}
207 medias={message.medias}
208 status={message.status}
209 errorMessage={message.errorMessage}
210 createdAt={message.createdAt}
211 steps={message.steps}
212 // 只读模式:不显示 actions 和 publishFlows
213 actions={[]}
214 publishFlows={[]}
215 isGenerating={false}
216 />
217 ))}
218
219 {/* 底部占位元素 */}
220 <div ref={bottomRef} />
221 </div>
222 </div>
223 </div>
224
225 {/* 底部提示 - 只读模式无输入框 */}
226 <div className="p-4 shrink-0">
227 <div className="max-w-6xl mx-auto flex items-center justify-center gap-2 text-sm text-muted-foreground">
228 <Eye className="w-4 h-4" />
229 <span>
230 {t('viewOnly')}
231 {' '}
232 -
233 {t('sharedConversation')}
234 </span>
235 </div>
236 </div>
237 </div>
238 )
239 }
240
240 lines Plain Text