: ChatArea TS2322 workaround + SubscriptionPanel component
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
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.
This commit is contained in:
@@ -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 <ToolCallChain steps={toolCallSteps} isStreaming={!!message.streaming} />;
|
||||
};
|
||||
|
||||
const renderSubtasks = (): React.ReactNode => {
|
||||
if (isUser || !subtaskList || subtaskList.length === 0) return null;
|
||||
return <TaskProgress tasks={subtaskList} className="mb-3" />;
|
||||
};
|
||||
|
||||
// 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 ? (
|
||||
<ToolCallChain
|
||||
steps={message.toolSteps}
|
||||
isStreaming={message.streaming}
|
||||
/>
|
||||
) : null}
|
||||
{renderToolSteps()}
|
||||
{/* Subtask tracking (DeerFlow-inspired) */}
|
||||
{!isUser && message.subtasks != null && message.subtasks.length > 0 ? (
|
||||
<TaskProgress tasks={message.subtasks} className="mb-3" />
|
||||
) : null}
|
||||
{renderSubtasks()}
|
||||
{/* Message content with streaming support */}
|
||||
<div className={`leading-relaxed ${isUser ? 'text-white whitespace-pre-wrap' : 'text-gray-700 dark:text-gray-200'}`}>
|
||||
{message.content
|
||||
|
||||
173
desktop/src/components/SaaS/SubscriptionPanel.tsx
Normal file
173
desktop/src/components/SaaS/SubscriptionPanel.tsx
Normal file
@@ -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<string, string> = {
|
||||
free: '免费版',
|
||||
pro: '专业版',
|
||||
team: '团队版',
|
||||
};
|
||||
|
||||
const PLAN_COLORS: Record<string, string> = {
|
||||
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 (
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span>{label}</span>
|
||||
<span>{current.toLocaleString()} / {displayMax}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${barColor}`}
|
||||
style={{ width: `${Math.max(pct, 1)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-emerald-600" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-5">
|
||||
{/* Plan badge */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-white"
|
||||
style={{ background: color }}
|
||||
>
|
||||
<Crown className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-gray-900">{label}</h2>
|
||||
{isTrialing && (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full">
|
||||
<Clock className="w-3 h-3" />
|
||||
试用中
|
||||
</span>
|
||||
)}
|
||||
{isActive && (
|
||||
<span className="inline-flex items-center gap-1 text-xs text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
已激活
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<CreditCard className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
|
||||
{/* Expiry / trial warning */}
|
||||
{showExpiryWarning && (
|
||||
<div className={`flex items-center gap-2 p-3 rounded-lg text-sm mb-3 ${
|
||||
isTrialing
|
||||
? 'bg-amber-50 text-amber-700 border border-amber-200'
|
||||
: 'bg-red-50 text-red-700 border border-red-200'
|
||||
}`}>
|
||||
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>
|
||||
{isTrialing
|
||||
? `试用还剩 ${daysLeft} 天,请尽快升级以保留 Pro 功能`
|
||||
: `订阅将在 ${daysLeft} 天后到期`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{daysLeft !== null && !showExpiryWarning && (
|
||||
<p className="text-xs text-gray-400">
|
||||
{isTrialing ? `试用剩余 ${daysLeft} 天` : `订阅到期: ${periodEnd?.toLocaleDateString()}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Usage */}
|
||||
{usage && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-4">用量配额</h3>
|
||||
<UsageBar label="中转请求" current={usage.relay_requests} max={usage.max_relay_requests} />
|
||||
<UsageBar label="Hand 执行" current={usage.hand_executions} max={usage.max_hand_executions} />
|
||||
<UsageBar label="Pipeline 运行" current={usage.pipeline_runs} max={usage.max_pipeline_runs} />
|
||||
{sub && (
|
||||
<p className="mt-2 text-xs text-gray-400">
|
||||
周期: {new Date(sub.current_period_start).toLocaleDateString()} — {new Date(sub.current_period_end).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Free plan upgrade prompt */}
|
||||
{planName === 'free' && (
|
||||
<div className="bg-gradient-to-r from-purple-50 to-blue-50 rounded-xl border border-purple-200 p-5">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-2">升级到专业版</h3>
|
||||
<p className="text-xs text-gray-600 mb-3">
|
||||
解锁全部 9 个 Hands、无限 Pipeline 和完整记忆系统
|
||||
</p>
|
||||
<p className="text-xs text-purple-600">
|
||||
请前往「订阅与计费」页面选择计划并完成支付
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user