| 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 |