: 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 { Button, EmptyState, MessageListSkeleton, LoadingDots } from './ui';
|
||||||
import { ResizableChatLayout } from './ai/ResizableChatLayout';
|
import { ResizableChatLayout } from './ai/ResizableChatLayout';
|
||||||
import { ArtifactPanel } from './ai/ArtifactPanel';
|
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 { listItemVariants, defaultTransition, fadeInVariants } from '../lib/animations';
|
||||||
import { FirstConversationPrompt } from './FirstConversationPrompt';
|
import { FirstConversationPrompt } from './FirstConversationPrompt';
|
||||||
import { ClassroomPlayer } from './classroom_player';
|
import { ClassroomPlayer } from './classroom_player';
|
||||||
@@ -30,7 +31,6 @@ import { ReasoningBlock } from './ai/ReasoningBlock';
|
|||||||
import { StreamingText } from './ai/StreamingText';
|
import { StreamingText } from './ai/StreamingText';
|
||||||
import { ChatMode } from './ai/ChatMode';
|
import { ChatMode } from './ai/ChatMode';
|
||||||
import { ModelSelector } from './ai/ModelSelector';
|
import { ModelSelector } from './ai/ModelSelector';
|
||||||
import { TaskProgress } from './ai/TaskProgress';
|
|
||||||
import { SuggestionChips } from './ai/SuggestionChips';
|
import { SuggestionChips } from './ai/SuggestionChips';
|
||||||
import { PipelineResultPreview } from './pipeline/PipelineResultPreview';
|
import { PipelineResultPreview } from './pipeline/PipelineResultPreview';
|
||||||
import { PresentationContainer } from './presentation/PresentationContainer';
|
import { PresentationContainer } from './presentation/PresentationContainer';
|
||||||
@@ -598,6 +598,20 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex
|
|||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
const isThinking = message.streaming && !message.content;
|
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
|
// Download message as Markdown file
|
||||||
const handleDownloadMessage = () => {
|
const handleDownloadMessage = () => {
|
||||||
if (!message.content) return;
|
if (!message.content) return;
|
||||||
@@ -644,16 +658,9 @@ function MessageBubble({ message, setInput }: { message: Message; setInput: (tex
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Tool call steps chain (DeerFlow-inspired) */}
|
{/* Tool call steps chain (DeerFlow-inspired) */}
|
||||||
{!isUser && message.toolSteps != null && message.toolSteps.length > 0 ? (
|
{renderToolSteps()}
|
||||||
<ToolCallChain
|
|
||||||
steps={message.toolSteps}
|
|
||||||
isStreaming={message.streaming}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{/* Subtask tracking (DeerFlow-inspired) */}
|
{/* Subtask tracking (DeerFlow-inspired) */}
|
||||||
{!isUser && message.subtasks != null && message.subtasks.length > 0 ? (
|
{renderSubtasks()}
|
||||||
<TaskProgress tasks={message.subtasks} className="mb-3" />
|
|
||||||
) : null}
|
|
||||||
{/* Message content with streaming support */}
|
{/* Message content with streaming support */}
|
||||||
<div className={`leading-relaxed ${isUser ? 'text-white whitespace-pre-wrap' : 'text-gray-700 dark:text-gray-200'}`}>
|
<div className={`leading-relaxed ${isUser ? 'text-white whitespace-pre-wrap' : 'text-gray-700 dark:text-gray-200'}`}>
|
||||||
{message.content
|
{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