Summary: - Update Tool C MVP Development Plan (V1.3) * Clarify Python execution as core feature * Add 15 real medical data cleaning scenarios (basic/medium/advanced) * Enhance System Prompt with 10 Few-shot examples * Discover existing Python service (extraction_service) * Update to extend existing service instead of rebuilding - Create Tool C MVP Development TODO List * 3-week plan with 30 tasks (Day 1-15) * 4 core milestones with clear acceptance criteria * Daily checklist and risk management * Detailed task breakdown for each day Key Changes: - Python service: Extend existing extraction_service instead of new setup - Test scenarios: 15 scenarios (5 basic + 5 medium + 5 advanced) - Success criteria: Basic >90%, Medium >80%, Advanced >60%, Total >80% - Development time: Reduced from 3 weeks to 2 weeks (reuse infrastructure) Status: Planning complete, ready to start Day 1 development
479 lines
35 KiB
HTML
479 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>工具C - 科研数据编辑器 V6 (修复版)</title>
|
||
|
||
<!-- 1. 引入 Tailwind CSS -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
|
||
<!-- 2. 引入 React 和 ReactDOM (切换为 jsDelivr 源,更稳定,国内访问更快) -->
|
||
<script crossorigin src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js"></script>
|
||
<script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
|
||
|
||
<!-- 3. 引入 Babel (用于解析 JSX) -->
|
||
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.23.5/babel.min.js"></script>
|
||
|
||
<!-- 4. 引入 Lucide Icons (指定 UMD 版本,修复 Identifier 'Infinity' 报错) -->
|
||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.460.0/dist/umd/lucide.min.js"></script>
|
||
|
||
<style>
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||
@keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
|
||
@keyframes zoomIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
|
||
|
||
.animate-in { animation: fadeIn 0.3s ease-out forwards; }
|
||
.slide-in-right { animation: slideInRight 0.3s ease-out forwards; }
|
||
.zoom-in { animation: zoomIn 0.2s ease-out forwards; }
|
||
|
||
/* 滚动条美化 */
|
||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
|
||
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-slate-50 text-slate-800 font-sans antialiased overflow-hidden">
|
||
|
||
<div id="root"></div>
|
||
|
||
<script type="text/babel">
|
||
const { useState, useEffect, useRef } = React;
|
||
|
||
// --- 图标组件适配 (Safe Shim) ---
|
||
// 使用 ref 容器隔离 React 和 DOM 操作,修复图标渲染问题
|
||
const LucideIcon = ({ name, className, size = 16, ...props }) => {
|
||
const containerRef = useRef(null);
|
||
useEffect(() => {
|
||
// 确保 lucide 全局对象存在且 createIcons 方法可用
|
||
if (containerRef.current && typeof lucide !== 'undefined' && lucide.createIcons) {
|
||
containerRef.current.innerHTML = '';
|
||
const i = document.createElement('i');
|
||
i.setAttribute('data-lucide', name);
|
||
containerRef.current.appendChild(i);
|
||
lucide.createIcons({
|
||
root: containerRef.current,
|
||
nameAttr: 'data-lucide',
|
||
attrs: { class: className, width: size, height: size, ...props }
|
||
});
|
||
}
|
||
}, [name, className, size]);
|
||
return <span ref={containerRef} style={{ display: 'inline-flex', verticalAlign: 'middle' }}></span>;
|
||
};
|
||
|
||
// --- 模拟数据 ---
|
||
const INITIAL_DATA = [
|
||
{ id: 'P001', age: 45, gender: '男', bmi: 24.5, admission_date: '2023-01-12', lab_val: '4.5' },
|
||
{ id: 'P002', age: 62, gender: '女', bmi: 28.1, admission_date: '2023-01-15', lab_val: '5.1' },
|
||
{ id: 'P003', age: 205, gender: '男', bmi: null, admission_date: '2023-02-01', lab_val: '<0.1' },
|
||
{ id: 'P004', age: 58, gender: '女', bmi: 22.4, admission_date: '2023-02-10', lab_val: '4.8' },
|
||
{ id: 'P005', age: 34, gender: '男', bmi: 21.0, admission_date: '2023-03-05', lab_val: '5.2' },
|
||
{ id: 'P006', age: 71, gender: '女', bmi: null, admission_date: '2023-03-12', lab_val: '6.0' },
|
||
{ id: 'P007', age: null, gender: '男', bmi: null, admission_date: '2023-04-01', lab_val: '4.9' },
|
||
{ id: 'P008', age: 49, gender: '男', bmi: 26.8, admission_date: '2023-04-05', lab_val: '5.5' },
|
||
{ id: 'P009', age: 55, gender: '女', bmi: 23.9, admission_date: '2023-04-10', lab_val: '4.2' },
|
||
{ id: 'P010', age: 66, gender: '男', bmi: 29.1, admission_date: '2023-04-12', lab_val: '5.8' },
|
||
];
|
||
|
||
const INITIAL_COLUMNS = [
|
||
{ id: 'id', name: '病人ID', type: 'text', width: 100 },
|
||
{ id: 'age', name: '年龄', type: 'number', width: 80 },
|
||
{ id: 'gender', name: '性别', type: 'category', width: 80 },
|
||
{ id: 'bmi', name: 'BMI指数', type: 'number', width: 100 },
|
||
{ id: 'admission_date', name: '入院日期', type: 'date', width: 120 },
|
||
{ id: 'lab_val', name: '肌酐', type: 'text', width: 100 },
|
||
];
|
||
|
||
const ToolC_EditorV6 = () => {
|
||
// --- 核心状态 ---
|
||
const [data, setData] = useState(INITIAL_DATA);
|
||
const [columns, setColumns] = useState(INITIAL_COLUMNS);
|
||
const [selectedColId, setSelectedColId] = useState(null);
|
||
|
||
// 侧边栏状态 (默认打开 Chat)
|
||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||
const [sidebarTab, setSidebarTab] = useState('chat'); // 'chat' | 'insight'
|
||
|
||
// Chat 状态
|
||
const [messages, setMessages] = useState([
|
||
{ id: 1, role: 'system', content: '您好!我是您的 AI 数据分析师。我可以为您编写代码来清洗数据。试试说:“把年龄大于60的设为老年组”。' }
|
||
]);
|
||
const [inputValue, setInputValue] = useState('');
|
||
const [isTyping, setIsTyping] = useState(false);
|
||
const messagesEndRef = useRef(null);
|
||
|
||
// 滚动到底部
|
||
const scrollToBottom = () => {
|
||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||
};
|
||
useEffect(scrollToBottom, [messages, isTyping]);
|
||
|
||
// 处理发送消息 (模拟 AI 生成代码)
|
||
const handleSend = () => {
|
||
if (!inputValue.trim()) return;
|
||
|
||
const userText = inputValue;
|
||
setMessages(prev => [...prev, { id: Date.now(), role: 'user', content: userText }]);
|
||
setInputValue('');
|
||
setIsTyping(true);
|
||
|
||
// 模拟 AI 思考和生成代码
|
||
setTimeout(() => {
|
||
setIsTyping(false);
|
||
let aiResponse;
|
||
|
||
if (userText.includes('老年') || userText.includes('年龄')) {
|
||
aiResponse = {
|
||
id: Date.now(),
|
||
role: 'assistant',
|
||
content: '没问题。我将使用 Pandas 对 `age` 列进行分箱处理,生成新的 `age_group` 列。请检查以下代码并运行:',
|
||
code: {
|
||
lang: 'python',
|
||
content: `import pandas as pd\n\n# 定义分箱逻辑\ndef categorize_age(age):\n if pd.isna(age):\n return None\n return '老年组' if age > 60 else '非老年组'\n\n# 应用函数生成新列\ndf['age_group'] = df['age'].apply(categorize_age)`,
|
||
status: 'pending' // pending | running | success | error
|
||
}
|
||
};
|
||
} else {
|
||
aiResponse = {
|
||
id: Date.now(),
|
||
role: 'assistant',
|
||
content: '收到。这是一个数据清洗任务,但我目前只演示“年龄分组”的 Python 代码生成能力。您可以试试输入:“把年龄大于60的标记为老年组”。'
|
||
};
|
||
}
|
||
setMessages(prev => [...prev, aiResponse]);
|
||
}, 1200);
|
||
};
|
||
|
||
// 执行代码 (模拟 Pyodide 运行)
|
||
const handleRunCode = (msgId) => {
|
||
// 1. 更新消息状态为 running
|
||
setMessages(prev => prev.map(m =>
|
||
m.id === msgId ? { ...m, code: { ...m.code, status: 'running' } } : m
|
||
));
|
||
|
||
// 2. 模拟执行延迟
|
||
setTimeout(() => {
|
||
// 3. 更新表格数据
|
||
const newData = data.map(row => ({
|
||
...row,
|
||
age_group: row.age && row.age > 60 ? '老年组' : (row.age ? '非老年组' : null)
|
||
}));
|
||
setData(newData);
|
||
|
||
// 检查列是否存在,不存在则添加
|
||
if (!columns.find(c => c.id === 'age_group')) {
|
||
setColumns(prev => [...prev, { id: 'age_group', name: 'age_group', type: 'category', width: 100 }]);
|
||
}
|
||
|
||
// 4. 更新消息状态为 success
|
||
setMessages(prev => prev.map(m =>
|
||
m.id === msgId ? { ...m, code: { ...m.code, status: 'success' } } : m
|
||
));
|
||
|
||
// 5. 追加成功提示
|
||
setMessages(prev => [...prev, {
|
||
id: Date.now(),
|
||
role: 'system',
|
||
content: '✅ 代码执行成功!已新增列 `age_group`。',
|
||
isSuccess: true
|
||
}]);
|
||
|
||
}, 1500);
|
||
};
|
||
|
||
// --- UI 组件 ---
|
||
|
||
const ToolbarButton = ({ iconName, label, colorClass = "text-slate-600 hover:bg-slate-100" }) => (
|
||
<button className={`flex flex-col items-center justify-center w-20 h-14 rounded-lg transition-all hover:shadow-sm ${colorClass}`}>
|
||
<LucideIcon name={iconName} className="w-5 h-5 mb-1" />
|
||
<span className="text-[10px] font-medium">{label}</span>
|
||
</button>
|
||
);
|
||
|
||
return (
|
||
<div className="h-screen w-screen bg-slate-50 font-sans text-slate-800 flex flex-col overflow-hidden">
|
||
|
||
{/* 1. Header */}
|
||
<header className="bg-white border-b border-slate-200 h-14 flex-none flex items-center justify-between px-4 z-20 shadow-sm">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 bg-emerald-600 rounded-lg flex items-center justify-center text-white shadow-emerald-200 shadow-md">
|
||
<LucideIcon name="table-2" size={20} />
|
||
</div>
|
||
<span className="font-bold text-lg text-slate-900 tracking-tight">科研数据编辑器 <span className="text-emerald-600 text-xs px-1.5 py-0.5 bg-emerald-50 rounded-full ml-1">Pro</span></span>
|
||
<div className="h-4 w-[1px] bg-slate-300 mx-2"></div>
|
||
<span className="text-xs text-slate-500 font-mono">lung_cancer_2023.csv</span>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center bg-slate-100 rounded-lg p-1">
|
||
<button className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"><LucideIcon name="undo-2" size={16} /></button>
|
||
<button className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"><LucideIcon name="redo-2" size={16} /></button>
|
||
</div>
|
||
<button className="flex items-center gap-2 px-4 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-all shadow-md">
|
||
<LucideIcon name="download" size={12} /> 导出结果
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{/* 2. Main Workspace (Flex Layout) */}
|
||
<div className="flex-1 flex overflow-hidden">
|
||
|
||
{/* Left: Table Area (Flexible Width) */}
|
||
<div className="flex-1 flex flex-col min-w-0 bg-slate-100/50">
|
||
{/* Flat Toolbar */}
|
||
<div className="bg-white border-b border-slate-200 px-4 py-2 flex items-center gap-1 overflow-x-auto flex-none shadow-sm z-10">
|
||
<ToolbarButton iconName="calculator" label="生成新变量" colorClass="text-emerald-600 bg-emerald-50 hover:bg-emerald-100" />
|
||
<ToolbarButton iconName="calendar-clock" label="时间差" colorClass="text-blue-600 bg-blue-50 hover:bg-blue-100" />
|
||
<ToolbarButton iconName="arrow-left-right" label="横纵转换" colorClass="text-cyan-600 bg-cyan-50 hover:bg-cyan-100" />
|
||
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
|
||
<ToolbarButton iconName="file-search" label="查重" colorClass="text-orange-600 bg-orange-50 hover:bg-orange-100" />
|
||
<ToolbarButton iconName="wand-2" label="多重插补" colorClass="text-rose-600 bg-rose-50 hover:bg-rose-100" />
|
||
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
|
||
<ToolbarButton iconName="filter" label="筛选分析集" colorClass="text-indigo-600 bg-indigo-50 hover:bg-indigo-100" />
|
||
<div className="flex-1"></div>
|
||
<div className="relative">
|
||
<LucideIcon name="search" size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||
<input className="pl-9 pr-4 py-1.5 text-sm bg-slate-100 border-none rounded-full w-48 focus:w-64 transition-all outline-none focus:ring-2 focus:ring-emerald-500/20" placeholder="搜索值..." />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Grid Container */}
|
||
<div className="flex-1 overflow-auto p-6 relative">
|
||
<div className="bg-white border border-slate-200 shadow-sm rounded-xl overflow-hidden min-w-[800px]">
|
||
{/* Grid Header */}
|
||
<div className="flex bg-slate-50 border-b border-slate-200 sticky top-0 z-10">
|
||
{columns.map(col => (
|
||
<div
|
||
key={col.id}
|
||
className={`h-10 px-4 flex items-center justify-between border-r border-slate-200 text-xs font-bold text-slate-600 cursor-pointer hover:bg-slate-100 transition-colors ${selectedColId === col.id ? 'bg-indigo-50 text-indigo-600' : ''}`}
|
||
style={{ width: col.width, flex: 'none' }}
|
||
onClick={() => {
|
||
setSelectedColId(col.id);
|
||
if (!isSidebarOpen) setIsSidebarOpen(true);
|
||
setSidebarTab('insight');
|
||
}}
|
||
>
|
||
<div className="flex items-center gap-1.5 truncate">
|
||
{col.type === 'number' && <LucideIcon name="hash" size={12} className="text-slate-400" />}
|
||
{col.type === 'text' && <LucideIcon name="type" size={12} className="text-slate-400" />}
|
||
{col.type === 'category' && <LucideIcon name="split" size={12} className="text-slate-400" />}
|
||
{col.type === 'date' && <LucideIcon name="calendar" size={12} className="text-slate-400" />}
|
||
{col.name}
|
||
</div>
|
||
{col.id === 'age_group' && <span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* Grid Body */}
|
||
<div className="divide-y divide-slate-100 text-sm text-slate-700 bg-white">
|
||
{data.map((row, idx) => (
|
||
<div key={idx} className={`flex hover:bg-slate-50 transition-colors ${row.age_group ? 'bg-emerald-50/30' : ''}`}>
|
||
{columns.map(col => {
|
||
const val = row[col.id];
|
||
const isNull = val === null || val === '';
|
||
const isOutlier = col.id === 'age' && val > 120;
|
||
|
||
return (
|
||
<div
|
||
key={col.id}
|
||
className={`h-10 px-4 flex items-center border-r border-slate-100 truncate
|
||
${isNull ? 'bg-red-50' : ''}
|
||
${isOutlier ? 'bg-orange-100 text-orange-700 font-bold' : ''}
|
||
${selectedColId === col.id ? 'bg-indigo-50/10' : ''}
|
||
`}
|
||
style={{ width: col.width, flex: 'none' }}
|
||
>
|
||
{isNull ? <span className="text-[10px] text-red-400 italic">NULL</span> : val}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Smart Sidebar (Fixed Width, No Overlay) */}
|
||
{isSidebarOpen && (
|
||
<div className="w-[400px] flex-none border-l border-slate-200 bg-white flex flex-col shadow-xl z-20">
|
||
|
||
{/* Sidebar Tabs */}
|
||
<div className="flex border-b border-slate-200 bg-slate-50">
|
||
<button
|
||
onClick={() => setSidebarTab('chat')}
|
||
className={`flex-1 py-3 text-xs font-bold text-center border-b-2 transition-all flex items-center justify-center gap-2 ${sidebarTab === 'chat' ? 'border-purple-600 text-purple-700 bg-white' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
|
||
>
|
||
<LucideIcon name="sparkles" size={16} /> AI Copilot
|
||
</button>
|
||
<button
|
||
onClick={() => setSidebarTab('insight')}
|
||
className={`flex-1 py-3 text-xs font-bold text-center border-b-2 transition-all flex items-center justify-center gap-2 ${sidebarTab === 'insight' ? 'border-indigo-600 text-indigo-700 bg-white' : 'border-transparent text-slate-500 hover:text-slate-700'}`}
|
||
>
|
||
<LucideIcon name="bar-chart-3" size={16} /> 统计概览
|
||
</button>
|
||
<button onClick={() => setIsSidebarOpen(false)} className="px-3 text-slate-400 hover:text-slate-600 hover:bg-slate-200 border-l border-slate-200 transition-colors">
|
||
<LucideIcon name="arrow-right" size={16} />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Content Area */}
|
||
<div className="flex-1 flex flex-col overflow-hidden relative">
|
||
|
||
{/* === TAB 1: AI Chat (Code Interpreter Mode) === */}
|
||
{sidebarTab === 'chat' && (
|
||
<>
|
||
<div className="flex-1 overflow-y-auto p-4 space-y-5 bg-slate-50/50">
|
||
{messages.map((msg) => (
|
||
<div key={msg.id} className={`flex flex-col animate-in slide-in-right duration-300 ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
|
||
|
||
{/* 文本气泡 */}
|
||
<div className={`max-w-[90%] px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm ${
|
||
msg.role === 'user'
|
||
? 'bg-purple-600 text-white rounded-tr-none'
|
||
: msg.isSuccess
|
||
? 'bg-emerald-50 text-emerald-800 border border-emerald-100'
|
||
: 'bg-white text-slate-700 border border-slate-200 rounded-tl-none'
|
||
}`}>
|
||
{msg.role === 'assistant' && !msg.isSuccess && (
|
||
<div className="flex items-center gap-2 mb-2 text-purple-600 font-bold text-xs uppercase tracking-wider">
|
||
<LucideIcon name="bot" size={12} /> AI Analysis
|
||
</div>
|
||
)}
|
||
{msg.content}
|
||
</div>
|
||
|
||
{/* 代码块卡片 (Code Block) */}
|
||
{msg.code && (
|
||
<div className="mt-3 w-full bg-[#1e1e1e] rounded-xl overflow-hidden shadow-md border border-slate-300 animate-in zoom-in duration-300">
|
||
<div className="bg-[#2d2d2d] px-3 py-2 flex items-center justify-between">
|
||
<div className="flex items-center gap-2 text-xs text-slate-400">
|
||
<LucideIcon name="terminal" size={12} /> Python Environment
|
||
</div>
|
||
<span className="text-[10px] text-slate-500 font-mono">pandas available</span>
|
||
</div>
|
||
<div className="p-3 overflow-x-auto">
|
||
<pre className="text-xs font-mono text-blue-300 leading-5">
|
||
{msg.code.content}
|
||
</pre>
|
||
</div>
|
||
{/* 执行状态栏 */}
|
||
<div className="bg-[#252526] px-3 py-2 border-t border-[#333] flex items-center justify-between">
|
||
{msg.code.status === 'pending' && (
|
||
<button
|
||
onClick={() => handleRunCode(msg.id)}
|
||
className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-500 text-white text-xs px-3 py-1.5 rounded transition-colors"
|
||
>
|
||
<LucideIcon name="play" size={12} className="fill-current" /> 运行代码
|
||
</button>
|
||
)}
|
||
{msg.code.status === 'running' && (
|
||
<span className="flex items-center gap-2 text-yellow-500 text-xs">
|
||
<LucideIcon name="loader-2" size={12} className="animate-spin" /> 执行中 (Pyodide)...
|
||
</span>
|
||
)}
|
||
{msg.code.status === 'success' && (
|
||
<span className="flex items-center gap-2 text-emerald-500 text-xs">
|
||
<LucideIcon name="check-circle-2" size={12} /> 执行成功 (0.4s)
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
{isTyping && (
|
||
<div className="flex items-center gap-2 text-slate-400 text-xs ml-2">
|
||
<span className="w-2 h-2 bg-purple-400 rounded-full animate-bounce"></span>
|
||
<span className="w-2 h-2 bg-purple-400 rounded-full animate-bounce delay-100"></span>
|
||
<span className="w-2 h-2 bg-purple-400 rounded-full animate-bounce delay-200"></span>
|
||
</div>
|
||
)}
|
||
<div ref={messagesEndRef}></div>
|
||
</div>
|
||
|
||
{/* Input */}
|
||
<div className="p-4 bg-white border-t border-slate-200 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||
<div className="relative">
|
||
<textarea
|
||
className="w-full pl-4 pr-12 py-3 text-sm bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none resize-none shadow-inner"
|
||
rows={2}
|
||
placeholder="输入指令,例如:把年龄大于60的标记为老年组..."
|
||
value={inputValue}
|
||
onChange={(e) => setInputValue(e.target.value)}
|
||
onKeyDown={(e) => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }}
|
||
/>
|
||
<button
|
||
className={`absolute right-2 bottom-2 p-2 rounded-lg transition-all ${inputValue.trim() ? 'bg-purple-600 text-white hover:bg-purple-700 shadow-md' : 'bg-slate-200 text-slate-400'}`}
|
||
disabled={!inputValue.trim()}
|
||
onClick={handleSend}
|
||
>
|
||
<LucideIcon name="arrow-right" size={16} />
|
||
</button>
|
||
</div>
|
||
<div className="text-[10px] text-slate-400 mt-2 text-center flex items-center justify-center gap-1">
|
||
<LucideIcon name="settings" size={12} /> 代码在本地浏览器沙箱运行,数据不上传云端
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* === TAB 2: Insight Panel (统计概览) === */}
|
||
{sidebarTab === 'insight' && (
|
||
<div className="flex-1 overflow-y-auto p-5 space-y-6 animate-in">
|
||
{selectedColId ? (
|
||
<>
|
||
<div className="p-4 bg-white rounded-xl border border-slate-200 shadow-sm">
|
||
<div className="text-xs text-slate-500 uppercase tracking-wider mb-1">SELECTED COLUMN</div>
|
||
<h3 className="text-xl font-bold text-slate-900">{columns.find(c => c.id === selectedColId)?.name}</h3>
|
||
<div className="text-xs font-mono text-slate-400 mt-1">{selectedColId}</div>
|
||
</div>
|
||
|
||
{/* Mock Charts */}
|
||
<div className="space-y-3">
|
||
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||
<LucideIcon name="bar-chart-3" size={16} className="text-slate-500" /> 分布概览
|
||
</h4>
|
||
<div className="h-32 bg-slate-50 rounded-lg border border-slate-100 flex items-end justify-between px-2 pb-2 gap-1">
|
||
{[20, 45, 30, 60, 80, 50, 20, 10].map((h, i) => (
|
||
<div key={i} className="flex-1 bg-indigo-200 hover:bg-indigo-400 transition-colors rounded-t" style={{height: `${h}%`}}></div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2 pt-4 border-t border-slate-100">
|
||
<button className="w-full text-left px-4 py-3 rounded-xl bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-indigo-300 transition-all">
|
||
<LucideIcon name="split" size={16} className="text-indigo-600" />
|
||
<div><div className="font-bold">生成分类变量</div><div className="text-[10px] text-slate-500">Binning</div></div>
|
||
</button>
|
||
<button className="w-full text-left px-4 py-3 rounded-xl bg-slate-50 hover:bg-slate-100 text-sm text-slate-700 flex items-center gap-3 border border-slate-200 hover:border-indigo-300 transition-all">
|
||
<LucideIcon name="wand-2" size={16} className="text-rose-600" />
|
||
<div><div className="font-bold">缺失值填补</div><div className="text-[10px] text-slate-500">Imputation</div></div>
|
||
</button>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 text-sm opacity-60">
|
||
<div className="w-16 h-16 bg-slate-100 rounded-full flex items-center justify-center mb-4">
|
||
<LucideIcon name="bar-chart-3" size={32} />
|
||
</div>
|
||
<p>请点击表格列头<br/>查看详细统计</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||
root.render(<ToolC_EditorV6 />);
|
||
</script>
|
||
</body>
|
||
</html> |