代码修改
This commit is contained in:
1 parent
8f5af46ec2
commit
8f75e12871
168 files changed
+76575
No files matched your search
@@ -0,0 +1,233 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Check, Megaphone, Target, Users, X, Zap } from 'lucide-react';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import type { ExpertCard, SelectableMcpKey } from './types';
|
||||
|
||||
interface McpOption {
|
||||
key: SelectableMcpKey;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
accent: 'purpose' | 'actor' | 'behavior' | 'narrative';
|
||||
}
|
||||
|
||||
const MCP_OPTIONS: McpOption[] = [
|
||||
{ key: 'purpose', name: '目的', description: '识别本次推演的核心目的,构成后续推理的锚点。', icon: Target, accent: 'purpose' },
|
||||
{ key: 'actor', name: '行为体', description: '推演支撑目的所需的关键行为体角色。', icon: Users, accent: 'actor' },
|
||||
{ key: 'behavior_factor', name: '预期行为和驱动因素', description: '为每个行为体推演预期行为及背后的驱动因素。', icon: Zap, accent: 'behavior' },
|
||||
{ key: 'narrative', name: '叙事', description: '基于驱动因素构建可被复用的叙事框架。', icon: Megaphone, accent: 'narrative' },
|
||||
];
|
||||
|
||||
const ACCENT_LIGHT: Record<McpOption['accent'], { selected: string; idle: string; chip: string }> = {
|
||||
purpose: {
|
||||
selected: 'border-orange-300 bg-orange-50/70 ring-1 ring-orange-200',
|
||||
idle: 'border-gray-200 bg-white hover:border-orange-200',
|
||||
chip: 'bg-orange-50 text-orange-600',
|
||||
},
|
||||
actor: {
|
||||
selected: 'border-red-300 bg-red-50/70 ring-1 ring-red-200',
|
||||
idle: 'border-gray-200 bg-white hover:border-red-200',
|
||||
chip: 'bg-red-50 text-red-600',
|
||||
},
|
||||
behavior: {
|
||||
selected: 'border-emerald-300 bg-emerald-50/70 ring-1 ring-emerald-200',
|
||||
idle: 'border-gray-200 bg-white hover:border-emerald-200',
|
||||
chip: 'bg-emerald-50 text-emerald-600',
|
||||
},
|
||||
narrative: {
|
||||
selected: 'border-amber-300 bg-amber-50/70 ring-1 ring-amber-200',
|
||||
idle: 'border-gray-200 bg-white hover:border-amber-200',
|
||||
chip: 'bg-amber-50 text-amber-600',
|
||||
},
|
||||
};
|
||||
|
||||
const ACCENT_DARK: Record<McpOption['accent'], { selected: string; idle: string; chip: string }> = {
|
||||
purpose: {
|
||||
selected: 'border-violet-300/50 bg-violet-500/15 ring-1 ring-violet-300/30',
|
||||
idle: 'border-slate-700 bg-slate-800/60 hover:border-violet-300/40',
|
||||
chip: 'bg-violet-500/20 text-violet-200',
|
||||
},
|
||||
actor: {
|
||||
selected: 'border-cyan-300/50 bg-cyan-500/15 ring-1 ring-cyan-300/30',
|
||||
idle: 'border-slate-700 bg-slate-800/60 hover:border-cyan-300/40',
|
||||
chip: 'bg-cyan-500/20 text-cyan-200',
|
||||
},
|
||||
behavior: {
|
||||
selected: 'border-emerald-300/50 bg-emerald-500/15 ring-1 ring-emerald-300/30',
|
||||
idle: 'border-slate-700 bg-slate-800/60 hover:border-emerald-300/40',
|
||||
chip: 'bg-emerald-500/20 text-emerald-200',
|
||||
},
|
||||
narrative: {
|
||||
selected: 'border-amber-300/50 bg-amber-500/15 ring-1 ring-amber-300/30',
|
||||
idle: 'border-slate-700 bg-slate-800/60 hover:border-amber-300/40',
|
||||
chip: 'bg-amber-500/20 text-amber-200',
|
||||
},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
expert: ExpertCard | null;
|
||||
initialSelected?: SelectableMcpKey[];
|
||||
onCancel: () => void;
|
||||
onConfirm: (selected: SelectableMcpKey[]) => void;
|
||||
}
|
||||
|
||||
const McpSelectModal: React.FC<Props> = ({ open, expert, initialSelected, onCancel, onConfirm }) => {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
const defaultAll = useMemo<SelectableMcpKey[]>(() => MCP_OPTIONS.map((m) => m.key), []);
|
||||
const [selected, setSelected] = useState<SelectableMcpKey[]>(
|
||||
initialSelected && initialSelected.length > 0 ? initialSelected : defaultAll,
|
||||
);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setSelected(initialSelected && initialSelected.length > 0 ? initialSelected : defaultAll);
|
||||
setError(null);
|
||||
}
|
||||
}, [defaultAll, initialSelected, open]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const toggle = (key: SelectableMcpKey) => {
|
||||
setSelected((prev) => {
|
||||
const has = prev.includes(key);
|
||||
if (has) {
|
||||
if (prev.length <= 1) {
|
||||
setError('至少选择一个生成步骤');
|
||||
return prev;
|
||||
}
|
||||
setError(null);
|
||||
return prev.filter((k) => k !== key);
|
||||
}
|
||||
setError(null);
|
||||
return [...prev, key];
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (selected.length === 0) {
|
||||
setError('至少选择一个生成步骤');
|
||||
return;
|
||||
}
|
||||
const ordered = MCP_OPTIONS.map((m) => m.key).filter((k) => selected.includes(k));
|
||||
onConfirm(ordered);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center px-4">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||
onClick={onCancel}
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className={`relative z-10 w-full max-w-xl rounded-2xl border shadow-2xl ${
|
||||
isDark ? 'border-slate-700 bg-slate-900 text-slate-100' : 'border-gray-200 bg-white text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<div className={`flex items-center justify-between border-b px-5 py-4 ${
|
||||
isDark ? 'border-slate-700' : 'border-gray-100'
|
||||
}`}>
|
||||
<div className="min-w-0">
|
||||
<div className="text-base font-semibold">选择本次执行的 MCP</div>
|
||||
<div className={`text-xs mt-0.5 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
{expert ? `专家:${expert.name}` : '默认全部勾选,至少保留 1 个步骤'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className={`flex h-8 w-8 items-center justify-center rounded-lg transition ${
|
||||
isDark ? 'text-slate-400 hover:bg-slate-800' : 'text-gray-400 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[60vh] overflow-y-auto px-5 py-4 space-y-2">
|
||||
{MCP_OPTIONS.map((opt) => {
|
||||
const isSelected = selected.includes(opt.key);
|
||||
const palette = isDark ? ACCENT_DARK[opt.accent] : ACCENT_LIGHT[opt.accent];
|
||||
const Icon = opt.icon;
|
||||
return (
|
||||
<button
|
||||
key={opt.key}
|
||||
type="button"
|
||||
onClick={() => toggle(opt.key)}
|
||||
className={`w-full text-left rounded-xl border px-3 py-3 transition ${
|
||||
isSelected ? palette.selected : palette.idle
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className={`flex h-9 w-9 items-center justify-center rounded-xl shrink-0 ${palette.chip}`}>
|
||||
<Icon className="w-4 h-4" />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-semibold">{opt.name}</div>
|
||||
<div className={`text-xs mt-0.5 leading-relaxed ${isDark ? 'text-slate-400' : 'text-gray-600'}`}>
|
||||
{opt.description}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`shrink-0 mt-0.5 flex h-5 w-5 items-center justify-center rounded-md border transition ${
|
||||
isSelected
|
||||
? isDark
|
||||
? 'border-cyan-300 bg-cyan-400/40 text-cyan-50'
|
||||
: 'border-red-400 bg-red-500 text-white'
|
||||
: isDark
|
||||
? 'border-slate-600 text-transparent'
|
||||
: 'border-gray-300 text-transparent'
|
||||
}`}
|
||||
>
|
||||
<Check className="w-3 h-3" />
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{error && (
|
||||
<div className={`text-xs ${isDark ? 'text-rose-300' : 'text-rose-600'}`}>{error}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center justify-between gap-3 border-t px-5 py-3 ${
|
||||
isDark ? 'border-slate-700' : 'border-gray-100'
|
||||
}`}>
|
||||
<div className={`text-[11px] ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
||||
后端固定执行顺序:目的 → 行为体 → 预期行为和驱动因素 → 叙事
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className={`rounded-lg border px-3 py-1.5 text-sm font-medium transition ${
|
||||
isDark
|
||||
? 'border-slate-700 bg-slate-800 text-slate-200 hover:bg-slate-700'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleConfirm}
|
||||
className={`rounded-lg px-3 py-1.5 text-sm font-semibold text-white transition ${
|
||||
isDark
|
||||
? 'bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-400 hover:to-blue-400'
|
||||
: 'bg-gradient-to-r from-red-500 to-rose-500 hover:from-red-400 hover:to-rose-400'
|
||||
}`}
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default McpSelectModal;
|
||||
Reference in new issue
Block a user