From a6902c28f5a848970807c05a88e4c4230794f13c Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 4 Apr 2026 13:39:11 +0800 Subject: [PATCH] : ChatArea TS2322 workaround + SubscriptionPanel component The ChatArea.tsx toolSteps/subtasks rendering uses helper functions to avoid TypeScript strict mode && chain producing unknown type in JSX children. Add SubscriptionPanel component for subscription status display in SaaS billing section. --- desktop/src/components/ChatArea.tsx | 29 +-- .../src/components/SaaS/SubscriptionPanel.tsx | 173 ++++++++++++++++++ 2 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 desktop/src/components/SaaS/SubscriptionPanel.tsx diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx index 0d8f55f..152d06a 100644 --- a/desktop/src/components/ChatArea.tsx +++ b/desktop/src/components/ChatArea.tsx @@ -14,7 +14,8 @@ import { Paperclip, SquarePen, ArrowUp, MessageSquare, Download, X, FileText, Im import { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui'; import { ResizableChatLayout } from './ai/ResizableChatLayout'; import { ArtifactPanel } from './ai/ArtifactPanel'; -import { ToolCallChain } from './ai/ToolCallChain'; +import { ToolCallChain, type ToolCallStep } from './ai/ToolCallChain'; +import { TaskProgress, type Subtask } from './ai/TaskProgress'; import { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations'; import { FirstConversationPrompt } from './FirstConversationPrompt'; import { ClassroomPlayer } from './classroom_player'; @@ -30,7 +31,6 @@ import { ReasoningBlock } from './ai/ReasoningBlock'; import { StreamingText } from './ai/StreamingText'; import { ChatMode } from './ai/ChatMode'; import { ModelSelector } from './ai/ModelSelector'; -import { TaskProgress } from './ai/TaskProgress'; import { SuggestionChips } from './ai/SuggestionChips'; import { PipelineResultPreview } from './pipeline/PipelineResultPreview'; import { PresentationContainer } from './presentation/PresentationContainer'; @@ -598,6 +598,20 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex const isUser = message.role === 'user'; const isThinking = message.streaming && !message.content; + // Extract typed arrays for JSX rendering (avoids TS2322 from && chain producing unknown) + const toolCallSteps: ToolCallStep[] | undefined = message.toolSteps; + const subtaskList: Subtask[] | undefined = message.subtasks; + + const renderToolSteps = (): React.ReactNode => { + if (isUser || !toolCallSteps || toolCallSteps.length === 0) return null; + return ; + }; + + const renderSubtasks = (): React.ReactNode => { + if (isUser || !subtaskList || subtaskList.length === 0) return null; + return ; + }; + // Download message as Markdown file const handleDownloadMessage = () => { if (!message.content) return; @@ -644,16 +658,9 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex /> )} {/* Tool call steps chain (DeerFlow-inspired) */} - {!isUser && message.toolSteps != null && message.toolSteps.length > 0 ? ( - - ) : null} + {renderToolSteps()} {/* Subtask tracking (DeerFlow-inspired) */} - {!isUser && message.subtasks != null && message.subtasks.length > 0 ? ( - - ) : null} + {renderSubtasks()} {/* Message content with streaming support */}
{message.content diff --git a/desktop/src/components/SaaS/SubscriptionPanel.tsx b/desktop/src/components/SaaS/SubscriptionPanel.tsx new file mode 100644 index 0000000..2169aa0 --- /dev/null +++ b/desktop/src/components/SaaS/SubscriptionPanel.tsx @@ -0,0 +1,173 @@ +/** + * SubscriptionPanel — 当前订阅详情面板 + * + * 展示当前计划、试用/到期状态、用量配额和操作入口。 + */ + +import { useEffect } from 'react'; +import { useSaaSStore } from '../../store/saasStore'; +import { + CheckCircle, + Clock, + AlertTriangle, + Loader2, + CreditCard, + Crown, +} from 'lucide-react'; + +const PLAN_LABELS: Record = { + free: '免费版', + pro: '专业版', + team: '团队版', +}; + +const PLAN_COLORS: Record = { + free: '#8c8c8c', + pro: '#863bff', + team: '#47bfff', +}; + +function UsageBar({ label, current, max }: { label: string; current: number; max: number | null }) { + const pct = max ? Math.min((current / max) * 100, 100) : 0; + const displayMax = max ? max.toLocaleString() : '∞'; + const barColor = pct >= 90 ? 'bg-red-500' : pct >= 70 ? 'bg-amber-500' : 'bg-emerald-500'; + + return ( +
+
+ {label} + {current.toLocaleString()} / {displayMax} +
+
+
+
+
+ ); +} + +export function SubscriptionPanel() { + const subscription = useSaaSStore((s) => s.subscription); + const billingLoading = useSaaSStore((s) => s.billingLoading); + const fetchBillingOverview = useSaaSStore((s) => s.fetchBillingOverview); + + useEffect(() => { + fetchBillingOverview().catch(() => {}); + }, [fetchBillingOverview]); + + if (billingLoading && !subscription) { + return ( +
+ +
+ ); + } + + const plan = subscription?.plan; + const sub = subscription?.subscription; + const usage = subscription?.usage; + const planName = plan?.name || 'free'; + const color = PLAN_COLORS[planName] || '#666'; + const label = PLAN_LABELS[planName] || planName; + + // Trial / expiry status + const isTrialing = sub?.status === 'trialing'; + const isActive = sub?.status === 'active'; + const trialEnd = sub?.trial_end ? new Date(sub.trial_end) : null; + const periodEnd = sub?.current_period_end ? new Date(sub.current_period_end) : null; + + const now = Date.now(); + const daysLeft = trialEnd + ? Math.max(0, Math.ceil((trialEnd.getTime() - now) / (1000 * 60 * 60 * 24))) + : periodEnd + ? Math.max(0, Math.ceil((periodEnd.getTime() - now) / (1000 * 60 * 60 * 24))) + : null; + + const showExpiryWarning = daysLeft !== null && daysLeft <= 3; + + return ( +
+ {/* Plan badge */} +
+
+
+
+ +
+
+

{label}

+ {isTrialing && ( + + + 试用中 + + )} + {isActive && ( + + + 已激活 + + )} +
+
+ +
+ + {/* Expiry / trial warning */} + {showExpiryWarning && ( +
+ + + {isTrialing + ? `试用还剩 ${daysLeft} 天,请尽快升级以保留 Pro 功能` + : `订阅将在 ${daysLeft} 天后到期`} + +
+ )} + + {daysLeft !== null && !showExpiryWarning && ( +

+ {isTrialing ? `试用剩余 ${daysLeft} 天` : `订阅到期: ${periodEnd?.toLocaleDateString()}`} +

+ )} +
+ + {/* Usage */} + {usage && ( +
+

用量配额

+ + + + {sub && ( +

+ 周期: {new Date(sub.current_period_start).toLocaleDateString()} — {new Date(sub.current_period_end).toLocaleDateString()} +

+ )} +
+ )} + + {/* Free plan upgrade prompt */} + {planName === 'free' && ( +
+

升级到专业版

+

+ 解锁全部 9 个 Hands、无限 Pipeline 和完整记忆系统 +

+

+ 请前往「订阅与计费」页面选择计划并完成支付 +

+
+ )} +
+ ); +}