Files
ai-app-sql/App.tsx
2025-12-09 22:38:08 +08:00

259 lines
10 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 React, { useState, useEffect } from 'react';
import { generateSql } from './services/gemini';
import { Button } from './components/Button';
import { TextArea } from './components/TextArea';
import { Select } from './components/Select';
import { SettingsModal } from './components/SettingsModal';
import { LoadingState, DatabaseType } from './types';
// Default placeholders to help the user understand what to input
const PLACEHOLDER_STRUCTURE = `例如:
CREATE TABLE student (
id INT PRIMARY KEY,
name VARCHAR(50) COMMENT '姓名',
school_code VARCHAR(20) COMMENT '学校代码',
nation_code VARCHAR(10) COMMENT '国籍代码',
politics_code VARCHAR(10) COMMENT '政治面貌代码',
year VARCHAR(4) COMMENT '年度',
address VARCHAR(200) COMMENT '家庭住址'
);
CREATE TABLE school (
code VARCHAR(20),
name VARCHAR(100)
);`;
const PLACEHOLDER_DICT = `例如:
字典表 dict_common (
type_code VARCHAR(50), -- 字典类型,如 'NATION', 'POLITICS'
item_code VARCHAR(50), -- 实际值,如 '01', 'CN'
item_name VARCHAR(100) -- 显示名,如 '汉族', '中国'
)
关联关系:
- student.nation_code -> dict_common (type_code='NATION')
- student.politics_code -> dict_common (type_code='POLITICS')`;
const PLACEHOLDER_REQ = `例如:
我需要查询学校基本信息。
输出:学校名称,学校代码,学生姓名,手机号,家庭住址,年度,政治面貌(需要字典翻译),国籍(需要字典翻译)。`;
const DB_OPTIONS = [
{ value: 'MySQL', label: 'MySQL / MariaDB' },
{ value: 'PostgreSQL', label: 'PostgreSQL' },
{ value: 'Oracle', label: 'Oracle Database' },
{ value: 'SQL Server', label: 'SQL Server (MSSQL)' },
{ value: 'Hive', label: 'Hive / SparkSQL' },
{ value: 'Dm', label: '达梦数据库 (Dameng)' },
{ value: 'SQLite', label: 'SQLite' },
];
const App: React.FC = () => {
const [databaseType, setDatabaseType] = useState<DatabaseType>('MySQL');
const [tableStructure, setTableStructure] = useState<string>('');
const [dictionaryData, setDictionaryData] = useState<string>('');
const [requirement, setRequirement] = useState<string>('');
const [generatedSql, setGeneratedSql] = useState<string>('');
const [status, setStatus] = useState<LoadingState>(LoadingState.IDLE);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
// API Key Management
const [apiKey, setApiKey] = useState<string>('');
const [isSettingsOpen, setIsSettingsOpen] = useState<boolean>(false);
useEffect(() => {
const storedKey = localStorage.getItem('user_api_key');
if (storedKey) {
setApiKey(storedKey);
}
}, []);
const handleSaveApiKey = (key: string) => {
setApiKey(key);
localStorage.setItem('user_api_key', key);
setIsSettingsOpen(false);
};
const handleGenerate = async () => {
if (!tableStructure.trim() || !requirement.trim()) {
setErrorMsg("请至少填写表结构和查询需求。");
return;
}
setStatus(LoadingState.LOADING);
setErrorMsg(null);
setGeneratedSql('');
try {
const sql = await generateSql({
tableStructure,
dictionaryData,
requirement,
databaseType,
apiKey // Pass the custom API key
});
setGeneratedSql(sql);
setStatus(LoadingState.SUCCESS);
} catch (err: any) {
setErrorMsg(err.message || "未知错误");
setStatus(LoadingState.ERROR);
}
};
const copyToClipboard = () => {
if (generatedSql) {
navigator.clipboard.writeText(generatedSql);
}
};
return (
<div className="flex flex-col h-screen bg-slate-50">
<SettingsModal
isOpen={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
onSave={handleSaveApiKey}
currentApiKey={apiKey}
/>
{/* Header */}
<header className="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shrink-0">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</div>
<div>
<h1 className="text-xl font-bold text-slate-800">SQL Translate Pro</h1>
<p className="text-xs text-slate-500"> SQL & </p>
</div>
</div>
<div>
<Button variant="outline" onClick={() => setIsSettingsOpen(true)} className="flex items-center gap-2 text-xs md:text-sm">
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
API Key
</Button>
</div>
</header>
{/* Main Content - Two Columns Layout */}
<main className="flex-1 overflow-hidden flex flex-col md:flex-row">
{/* Left Panel: Inputs */}
<div className="w-full md:w-1/2 lg:w-5/12 p-6 flex flex-col gap-6 overflow-y-auto border-r border-gray-200 bg-white">
<div>
<Select
label="目标数据库类型"
options={DB_OPTIONS}
value={databaseType}
onChange={(e) => setDatabaseType(e.target.value as DatabaseType)}
helperText="生成结果将适配所选数据库的方言"
/>
</div>
<div className="flex-1 min-h-[200px]">
<TextArea
label="1. 表结构与字段说明"
helperText="粘贴 DDL 或字段描述"
placeholder={PLACEHOLDER_STRUCTURE}
value={tableStructure}
onChange={(e) => setTableStructure(e.target.value)}
/>
</div>
<div className="flex-1 min-h-[150px]">
<TextArea
label="2. 字典表信息"
helperText="说明字典表结构及关联方式"
placeholder={PLACEHOLDER_DICT}
value={dictionaryData}
onChange={(e) => setDictionaryData(e.target.value)}
/>
</div>
<div className="flex-1 min-h-[120px]">
<TextArea
label="3. 最终需求"
helperText="你想要查询哪些字段?"
placeholder={PLACEHOLDER_REQ}
value={requirement}
onChange={(e) => setRequirement(e.target.value)}
/>
</div>
<div className="pt-2 sticky bottom-0 bg-white pb-2">
<Button
onClick={handleGenerate}
isLoading={status === LoadingState.LOADING}
className="w-full shadow-lg"
>
SQL
</Button>
{errorMsg && (
<div className="mt-2 text-red-500 text-sm bg-red-50 p-2 rounded border border-red-100">
{errorMsg}
</div>
)}
</div>
</div>
{/* Right Panel: Output */}
<div className="w-full md:w-1/2 lg:w-7/12 bg-slate-50 flex flex-col overflow-hidden relative">
<div className="px-6 py-4 border-b border-gray-200 bg-white flex justify-between items-center shrink-0">
<h2 className="text-lg font-semibold text-slate-700"> <span className="text-xs font-normal text-slate-400 ml-2">({databaseType})</span></h2>
{status === LoadingState.SUCCESS && (
<Button variant="outline" onClick={copyToClipboard} className="text-xs py-1.5 h-8">
SQL
</Button>
)}
</div>
<div className="flex-1 p-6 overflow-y-auto">
{status === LoadingState.IDLE && (
<div className="h-full flex flex-col items-center justify-center text-slate-400">
<svg className="w-16 h-16 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
<p></p>
</div>
)}
{status === LoadingState.LOADING && (
<div className="h-full flex flex-col items-center justify-center text-indigo-500">
<div className="animate-pulse flex flex-col items-center">
<div className="h-2.5 bg-indigo-200 rounded-full w-48 mb-4"></div>
<div className="h-2 bg-indigo-100 rounded-full w-32 mb-2.5"></div>
<div className="h-2 bg-indigo-100 rounded-full w-40"></div>
<span className="mt-6 text-sm font-medium text-slate-500"> {databaseType} ...</span>
</div>
</div>
)}
{status === LoadingState.SUCCESS && (
<div className="relative group">
<pre className="block p-4 rounded-lg bg-slate-900 text-slate-50 font-mono text-sm leading-relaxed whitespace-pre-wrap shadow-inner border border-slate-700">
<code>{generatedSql}</code>
</pre>
</div>
)}
{status === LoadingState.ERROR && (
<div className="h-full flex items-center justify-center text-red-400">
<p></p>
</div>
)}
</div>
</div>
</main>
</div>
);
};
export default App;