返回 AiToEarn
index.tsx
1 /**
2 * ActionCard - Action 卡片组件
3 * 用于在聊天消息中显示可交互的 action 卡片
4 * 支持:连接频道、更新授权、跳转发布等
5 */
6
7 'use client'
8
9 import type { IActionCard } from '@/store/agent/agent.types'
10 import { AlertCircle, ArrowRight, CreditCard, Link2, RefreshCw, Send } from 'lucide-react'
11 import { useParams, usePathname, useRouter } from 'next/navigation'
12 import { useCallback } from 'react'
13 import { useShallow } from 'zustand/react/shallow'
14 import { useTransClient } from '@/app/i18n/client'
15 import { useChannelManagerStore } from '@/components/ChannelManager'
16 import { Button } from '@/components/ui/button'
17 import { useAccountStore } from '@/store'
18 import { useAgentStore } from '@/store/agent'
19 import { ActionRegistry } from '@/store/agent/handlers/action.handlers'
20 import { PluginStatus, usePluginStore } from '@/store/plugin'
21 import { useUserStore } from '@/store/user'
22 import { navigateToLogin } from '@/utils/auth'
23 import { cn } from '@/utils/className'
24 import { toast } from '@/utils/ui/toast'
25
26 export interface IActionCardProps {
27 /** Action 数据 */
28 action: IActionCard
29 /** 自定义类名 */
30 className?: string
31 }
32
33 /** 平台名称映射 */
34 const PLATFORM_NAMES: Record<string, string> = {
35 douyin: '抖音',
36 xhs: '小红书',
37 wxSph: '微信视频号',
38 KWAI: '快手',
39 youtube: 'YouTube',
40 wxGzh: '微信公众号',
41 bilibili: 'Bilibili',
42 twitter: 'Twitter',
43 tiktok: 'TikTok',
44 facebook: 'Facebook',
45 instagram: 'Instagram',
46 threads: 'Threads',
47 pinterest: 'Pinterest',
48 linkedin: 'LinkedIn',
49 }
50
51 /** 获取平台显示名称 */
52 function getPlatformDisplayName(platform?: string): string {
53 if (!platform)
54 return 'Platform'
55 return PLATFORM_NAMES[platform] || platform.charAt(0).toUpperCase() + platform.slice(1)
56 }
57
58 /** Action 卡片配置 */
59 interface IActionConfig {
60 icon: React.ReactNode
61 title: string
62 description: string
63 buttonText: string
64 bgClass: string
65 borderClass: string
66 iconClass: string
67 }
68
69 /**
70 * ActionCard - 渲染单个 Action 卡片
71 */
72 export function ActionCard({ action, className }: IActionCardProps) {
73 const router = useRouter()
74 const pathname = usePathname()
75 const { lng } = useParams()
76 const { t } = useTransClient('chat')
77 const token = useUserStore(state => state.token)
78
79 // Agent store 方法
80 const { continueTask } = useAgentStore()
81
82 // 频道管理器
83 const { openConnectList, setOnAuthSuccess, closeModal } = useChannelManagerStore(
84 useShallow(state => ({
85 openConnectList: state.openConnectList,
86 setOnAuthSuccess: state.setOnAuthSuccess,
87 closeModal: state.closeModal,
88 })),
89 )
90
91 const platformName = getPlatformDisplayName(action.platform)
92
93 // 根据 action 类型获取配置
94 const getActionConfig = (): IActionConfig | null => {
95 switch (action.type) {
96 case 'insufficientCredits':
97 return {
98 icon: <CreditCard className="w-5 h-5" />,
99 title: t('action.insufficientCredits') || '积分不足',
100 description: t('action.insufficientCreditsDesc') || '任务已暂停,请充值积分后继续。',
101 buttonText: t('action.rechargeCredits') || '充值积分',
102 bgClass:
103 'bg-gradient-to-br from-amber-50 to-yellow-50 dark:from-amber-950/30 dark:to-yellow-950/30',
104 borderClass: 'border-amber-200 dark:border-amber-800',
105 iconClass: 'text-amber-600 dark:text-amber-400 bg-amber-100 dark:bg-amber-900/50',
106 }
107 case 'errorOnly':
108 return {
109 icon: <AlertCircle className="w-5 h-5" />,
110 title: action.title || t('action.error') || '生成失败',
111 description: action.description || t('action.errorDesc') || '生成失败,请稍后重试。',
112 buttonText: '',
113 bgClass:
114 'bg-gradient-to-br from-rose-50 to-pink-50 dark:from-rose-950/30 dark:to-pink-950/30',
115 borderClass: 'border-rose-200 dark:border-rose-800',
116 iconClass: 'text-rose-600 dark:text-rose-400 bg-rose-100 dark:bg-rose-900/50',
117 }
118 case 'createChannel':
119 return {
120 icon: <Link2 className="w-5 h-5" />,
121 title: t('action.addChannel') || 'Add Channel',
122 description:
123 t('action.addChannelDesc', { platform: platformName })
124 || `You haven't connected a ${platformName} account yet. Please add a channel to publish content.`,
125 buttonText: t('action.addChannelNow') || 'Add Channel',
126 bgClass:
127 'bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30',
128 borderClass: 'border-blue-200 dark:border-blue-800',
129 iconClass: 'text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/50',
130 }
131 case 'updateChannel':
132 return {
133 icon: <RefreshCw className="w-5 h-5" />,
134 title: t('action.updateAuth') || '更新授权',
135 description:
136 t('action.updateAuthDesc', { platform: platformName })
137 || `${platformName} 账号授权已过期,请重新授权`,
138 buttonText: t('action.reauthorize') || '重新授权',
139 bgClass:
140 'bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-950/30 dark:to-orange-950/30',
141 borderClass: 'border-amber-200 dark:border-amber-800',
142 iconClass: 'text-amber-600 dark:text-amber-400 bg-amber-100 dark:bg-amber-900/50',
143 }
144 case 'loginChannel':
145 return {
146 icon: <AlertCircle className="w-5 h-5" />,
147 title: t('action.loginChannel') || '登录频道',
148 description:
149 t('action.loginChannelDesc', { platform: platformName })
150 || `请先登录 ${platformName} 账号`,
151 buttonText: t('action.goLogin') || '去登录',
152 bgClass:
153 'bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-950/30 dark:to-pink-950/30',
154 borderClass: 'border-purple-200 dark:border-purple-800',
155 iconClass: 'text-purple-600 dark:text-purple-400 bg-purple-100 dark:bg-purple-900/50',
156 }
157 case 'navigateToPublish':
158 return {
159 icon: <Send className="w-5 h-5" />,
160 title: t('action.readyToPublish') || '准备发布',
161 description:
162 t('action.readyToPublishDesc', { platform: platformName })
163 || `内容已准备好,可以发布到 ${platformName}`,
164 buttonText: t('action.goPublish') || '去发布',
165 bgClass:
166 'bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-950/30 dark:to-emerald-950/30',
167 borderClass: 'border-green-200 dark:border-green-800',
168 iconClass: 'text-green-600 dark:text-green-400 bg-green-100 dark:bg-green-900/50',
169 }
170 default:
171 return null
172 }
173 }
174
175 const config = getActionConfig()
176
177 /**
178 * 处理账号添加成功 - 用于 ChannelManager 回调
179 */
180 const handleAccountAddSuccess = useCallback(async () => {
181 // 清除回调
182 setOnAuthSuccess(null)
183
184 closeModal()
185
186 // 检查是否在 chat 页面
187 if (pathname.includes('/chat/')) {
188 // 提取 taskId
189 const pathParts = pathname.split('/')
190 const taskIdIndex = pathParts.findIndex(part => part === 'chat')
191 if (taskIdIndex !== -1 && pathParts[taskIdIndex + 1]) {
192 const taskId = pathParts[taskIdIndex + 1]
193
194 // 自动发送消息
195 const platformDisplayName = getPlatformDisplayName(action.platform)
196 const autoMessage = `I have added the "${platformDisplayName}" channel account, please continue.`
197
198 try {
199 await continueTask({
200 prompt: autoMessage,
201 medias: [],
202 t: t as (key: string) => string,
203 taskId,
204 })
205 }
206 catch (error) {
207 console.error('Auto send message failed:', error)
208 }
209 }
210 }
211 }, [pathname, action.platform, continueTask, t, setOnAuthSuccess])
212
213 /**
214 * 打开频道管理器并设置成功回调
215 */
216 const openChannelManager = useCallback(() => {
217 // 设置授权成功回调
218 setOnAuthSuccess(handleAccountAddSuccess)
219 // 打开频道管理器
220 openConnectList()
221 }, [setOnAuthSuccess, handleAccountAddSuccess, openConnectList])
222
223 // 处理按钮点击
224 const handleClick = () => {
225 const platform = action.platform || ''
226
227 switch (action.type) {
228 case 'insufficientCredits':
229 useAccountStore.getState().setLowBalanceAlertOpen(true)
230 return
231 case 'createChannel':
232 // 未登录时跳转登录页
233 if (!token) {
234 toast.warning(t('home.loginRequired') || 'Please login first')
235 navigateToLogin()
236 return
237 }
238 // 打开频道管理器
239 openChannelManager()
240 break
241 case 'updateChannel':
242 router.push(`/${lng}/accounts?updateChannel=${platform}`)
243 break
244 case 'loginChannel':
245 router.push(`/${lng}/accounts?loginChannel=${platform}`)
246 break
247 case 'navigateToPublish': { // 构建发布参数
248 // 检查是否是插件平台(小红书、抖音)
249 if (platform === 'xhs') {
250 const pluginStatus = usePluginStore.getState().status
251 if (pluginStatus === PluginStatus.READY) {
252 // 插件已就绪,使用 ActionRegistry 执行发布
253 ActionRegistry.execute(
254 {
255 type: 'fullContent',
256 action: 'navigateToPublish',
257 platform: action.platform,
258 accountId: action.accountId,
259 title: action.title,
260 description: action.description,
261 medias: action.medias,
262 tags: action.tags,
263 },
264 { router, lng: lng as string, t },
265 )
266 return
267 }
268 else {
269 // 插件未就绪,提示用户
270 toast.warning(t('plugin.platformNeedsPlugin') || '该平台需要安装插件才能发布')
271 return
272 }
273 }
274 // 非插件平台,保持原有逻辑
275 const params = new URLSearchParams()
276 params.set('action', 'publish')
277 params.set('aiGenerated', 'true')
278 if (platform)
279 params.set('platform', platform)
280 if (action.accountId)
281 params.set('accountId', action.accountId)
282 if (action.title)
283 params.set('title', action.title)
284 if (action.description)
285 params.set('description', action.description)
286 if (action.tags?.length)
287 params.set('tags', JSON.stringify(action.tags))
288 if (action.medias?.length)
289 params.set('medias', JSON.stringify(action.medias))
290 router.push(`/${lng}/accounts?${params.toString()}`)
291 break
292 }
293 default:
294 router.push(`/${lng}/accounts`)
295 }
296 }
297
298 return (
299 <>
300 {
301 config ? (
302 <div
303 className={cn(
304 'rounded-xl border p-4 transition-all hover:shadow-md',
305 config.bgClass,
306 config.borderClass,
307 className,
308 )}
309 >
310 {/* 头部:图标 + 标题 */}
311 <div className="flex items-center gap-3 mb-3">
312 <div className={cn('p-2 rounded-lg', config.iconClass)}>{config.icon}</div>
313 <div className="flex-1">
314 <h4 className="font-semibold text-foreground">{config.title}</h4>
315 {action.platform && <span className="text-xs text-muted-foreground">{platformName}</span>}
316 </div>
317 </div>
318
319 {/* 描述 */}
320 <p className="text-sm text-muted-foreground mb-4 leading-relaxed">{config.description}</p>
321
322 {/* 操作按钮:errorOnly 不显示按钮 */}
323 {action.type !== 'errorOnly' && (
324 <Button onClick={handleClick} className="w-full group" variant="default">
325 {config.buttonText}
326 <ArrowRight className="w-4 h-4 ml-2 transition-transform group-hover:translate-x-1" />
327 </Button>
328 )}
329 </div>
330 ) : <></>
331 }
332 </>
333
334 )
335 }
336
337 export default ActionCard
338
338 lines Plain Text