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
- Create docs/brainstorming/ with 5 discussion records (Mar 16 - Apr 7) - Archive ~30 outdated audit reports (V5-V11) to docs/archive/old-audits/ - Archive superseded analysis docs to docs/archive/old-analysis/ - Archive completed session plans to docs/archive/old-plans/ - Archive old test reports/validations to respective archive folders - Remove empty directories left after moves - Keep current docs: TRUTH.md, feature docs, deployment, knowledge-base, superpowers
143 lines
6.0 KiB
TypeScript
143 lines
6.0 KiB
TypeScript
import { CheckCircle, XCircle, Clock, Lightbulb, ChevronDown, ChevronUp } from 'lucide-react';
|
||
import { useState } from 'react';
|
||
import type { ButlerProposal } from '../../lib/viking-client';
|
||
import { updateButlerProposalStatus } from '../../lib/viking-client';
|
||
|
||
interface ProposalsSectionProps {
|
||
proposals: ButlerProposal[];
|
||
onStatusChange?: () => void;
|
||
}
|
||
|
||
function ProposalStatusBadge({ status }: { status: ButlerProposal['status'] }) {
|
||
const config = {
|
||
pending: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-300', icon: Clock, label: '待处理' },
|
||
accepted: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300', icon: CheckCircle, label: '已接受' },
|
||
rejected: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-500 dark:text-gray-400', icon: XCircle, label: '已拒绝' },
|
||
completed: { bg: 'bg-emerald-100 dark:bg-emerald-900/30', text: 'text-emerald-700 dark:text-emerald-300', icon: CheckCircle, label: '已完成' },
|
||
};
|
||
const c = config[status];
|
||
const Icon = c.icon;
|
||
return (
|
||
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium ${c.bg} ${c.text}`}>
|
||
<Icon className="w-3 h-3" />
|
||
{c.label}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
export function ProposalsSection({ proposals, onStatusChange }: ProposalsSectionProps) {
|
||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||
const [updating, setUpdating] = useState<string | null>(null);
|
||
|
||
const handleStatusUpdate = async (proposalId: string, status: string) => {
|
||
setUpdating(proposalId);
|
||
try {
|
||
await updateButlerProposalStatus(proposalId, status);
|
||
onStatusChange?.();
|
||
} catch {
|
||
// Status update failed — clear local optimistic state on next refresh
|
||
onStatusChange?.();
|
||
} finally {
|
||
setUpdating(null);
|
||
}
|
||
};
|
||
|
||
if (proposals.length === 0) {
|
||
return (
|
||
<div className="text-center py-8">
|
||
<Lightbulb className="w-8 h-8 mx-auto mb-2 text-gray-300 dark:text-gray-600" />
|
||
<p className="text-sm text-gray-500 dark:text-gray-400">暂无方案</p>
|
||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||
当管家发现高置信度痛点时,会自动生成解决方案
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
{proposals.map((proposal) => {
|
||
const isExpanded = expandedId === proposal.id;
|
||
const isUpdating = updating === proposal.id;
|
||
|
||
return (
|
||
<div
|
||
key={proposal.id}
|
||
className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 overflow-hidden"
|
||
>
|
||
<button
|
||
className="w-full px-3 py-2.5 flex items-start gap-2 text-left hover:bg-gray-50 dark:hover:bg-gray-750"
|
||
onClick={() => setExpandedId(isExpanded ? null : proposal.id)}
|
||
>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1 truncate">
|
||
{proposal.title}
|
||
</div>
|
||
<ProposalStatusBadge status={proposal.status} />
|
||
</div>
|
||
{isExpanded ? (
|
||
<ChevronUp className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||
) : (
|
||
<ChevronDown className="w-4 h-4 text-gray-400 flex-shrink-0" />
|
||
)}
|
||
</button>
|
||
|
||
{isExpanded && (
|
||
<div className="px-3 pb-3 border-t border-gray-100 dark:border-gray-700 pt-2 space-y-3">
|
||
<p className="text-xs text-gray-600 dark:text-gray-400 whitespace-pre-line">
|
||
{proposal.description}
|
||
</p>
|
||
|
||
{proposal.steps.length > 0 && (
|
||
<div className="space-y-1.5">
|
||
<div className="text-xs font-medium text-gray-600 dark:text-gray-300">步骤:</div>
|
||
{proposal.steps.map((step) => (
|
||
<div
|
||
key={step.index}
|
||
className="flex items-start gap-2 text-xs"
|
||
>
|
||
<span className="flex-shrink-0 w-5 h-5 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 flex items-center justify-center font-medium">
|
||
{step.index}
|
||
</span>
|
||
<div className="flex-1">
|
||
<div className="font-medium text-gray-700 dark:text-gray-300">
|
||
{step.action}
|
||
</div>
|
||
<div className="text-gray-500 dark:text-gray-400">
|
||
{step.detail}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{proposal.status === 'pending' && (
|
||
<div className="flex gap-2 pt-1">
|
||
<button
|
||
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-xs font-medium bg-emerald-500 text-white hover:bg-emerald-600 disabled:opacity-50"
|
||
onClick={() => handleStatusUpdate(proposal.id, 'accepted')}
|
||
disabled={isUpdating}
|
||
>
|
||
<CheckCircle className="w-3.5 h-3.5" />
|
||
接受
|
||
</button>
|
||
<button
|
||
className="flex items-center gap-1 px-3 py-1.5 rounded-md text-xs font-medium bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600 disabled:opacity-50"
|
||
onClick={() => handleStatusUpdate(proposal.id, 'rejected')}
|
||
disabled={isUpdating}
|
||
>
|
||
<XCircle className="w-3.5 h-3.5" />
|
||
拒绝
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|