Files
zclaw_openfang/desktop/src/components/ButlerPanel/ProposalsSection.tsx
iven 2e5f63be32
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
docs: reorganize docs — archive outdated, create brainstorming folder
- 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
2026-04-07 09:54:30 +08:00

143 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}