更新至 v0.2.0_20251226 版本

This commit is contained in:
2025-12-26 16:28:41 +08:00
parent 4773a9239c
commit 617c22b00a
6 changed files with 124 additions and 18 deletions

54
App.tsx
View File

@@ -5,6 +5,7 @@ import { Button } from './components/Button';
import { TextArea } from './components/TextArea';
import { Select } from './components/Select';
import { SettingsModal } from './components/SettingsModal';
import { Toast, ToastType } from './components/Toast';
import { LoadingState, DatabaseType } from './types';
// Default placeholders to help the user understand what to input
@@ -49,8 +50,15 @@ const DB_OPTIONS = [
{ value: 'SQLite', label: 'SQLite' },
];
const MODEL_OPTIONS = [
{ value: 'gemini-3-pro-preview', label: 'Gemini 3.0 Pro (推荐 - 强逻辑)' },
{ value: 'gemini-3-flash-preview', label: 'Gemini 3.0 Flash (极速)' },
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
];
const App: React.FC = () => {
const [databaseType, setDatabaseType] = useState<DatabaseType>('MySQL');
const [model, setModel] = useState<string>('gemini-3-flash-preview');
const [tableStructure, setTableStructure] = useState<string>('');
const [dictionaryData, setDictionaryData] = useState<string>('');
const [requirement, setRequirement] = useState<string>('');
@@ -63,6 +71,9 @@ const App: React.FC = () => {
const [apiKey, setApiKey] = useState<string>('');
const [isSettingsOpen, setIsSettingsOpen] = useState<boolean>(false);
// Toast Notification State
const [toast, setToast] = useState<{message: string, type: ToastType, id: number} | null>(null);
useEffect(() => {
const storedKey = localStorage.getItem('user_api_key');
if (storedKey) {
@@ -70,14 +81,22 @@ const App: React.FC = () => {
}
}, []);
const showToast = (message: string, type: ToastType = 'success') => {
setToast({ message, type, id: Date.now() });
};
const closeToast = () => setToast(null);
const handleSaveApiKey = (key: string) => {
setApiKey(key);
localStorage.setItem('user_api_key', key);
setIsSettingsOpen(false);
showToast("API Key 配置已保存", "success");
};
const handleGenerate = async () => {
if (!tableStructure.trim() || !requirement.trim()) {
showToast("请至少填写表结构和查询需求", "error");
setErrorMsg("请至少填写表结构和查询需求。");
return;
}
@@ -92,24 +111,38 @@ const App: React.FC = () => {
dictionaryData,
requirement,
databaseType,
model,
apiKey // Pass the custom API key
});
setGeneratedSql(sql);
setStatus(LoadingState.SUCCESS);
showToast("SQL 查询生成成功", "success");
} catch (err: any) {
setErrorMsg(err.message || "未知错误");
setStatus(LoadingState.ERROR);
showToast(err.message || "生成失败,请重试", "error");
}
};
const copyToClipboard = () => {
if (generatedSql) {
navigator.clipboard.writeText(generatedSql);
showToast("已复制到剪贴板", "success");
}
};
return (
<div className="flex flex-col h-screen bg-slate-50">
<div className="flex flex-col h-screen bg-slate-50 relative">
{/* Toast Notification Container */}
{toast && (
<Toast
key={toast.id}
message={toast.message}
type={toast.type}
onClose={closeToast}
/>
)}
<SettingsModal
isOpen={isSettingsOpen}
onClose={() => setIsSettingsOpen(false)}
@@ -147,13 +180,18 @@ const App: React.FC = () => {
{/* 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>
<div className="grid grid-cols-2 gap-4">
<Select
label="目标数据库类型"
label="数据库类型"
options={DB_OPTIONS}
value={databaseType}
onChange={(e) => setDatabaseType(e.target.value as DatabaseType)}
helperText="生成结果将适配所选数据库的方言"
/>
<Select
label="使用模型"
options={MODEL_OPTIONS}
value={model}
onChange={(e) => setModel(e.target.value)}
/>
</div>
@@ -207,7 +245,7 @@ const App: React.FC = () => {
<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>
<h2 className="text-lg font-semibold text-slate-700"> <span className="text-xs font-normal text-slate-400 ml-2">({databaseType} by {MODEL_OPTIONS.find(m => m.value === model)?.label?.split('(')[0].trim()})</span></h2>
{status === LoadingState.SUCCESS && (
<Button variant="outline" onClick={copyToClipboard} className="text-xs py-1.5 h-8">
SQL
@@ -221,7 +259,7 @@ const App: React.FC = () => {
<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>
<p></p>
</div>
)}
@@ -231,7 +269,7 @@ const App: React.FC = () => {
<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>
<span className="mt-6 text-sm font-medium text-slate-500">使 {MODEL_OPTIONS.find(m => m.value === model)?.label.split('(')[0]} ...</span>
</div>
</div>
)}
@@ -256,4 +294,4 @@ const App: React.FC = () => {
);
};
export default App;
export default App;

View File

@@ -9,18 +9,18 @@ interface SelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
export const Select: React.FC<SelectProps> = ({ label, options, helperText, className = '', ...props }) => {
return (
<div className="flex flex-col gap-1.5">
<div className="flex flex-col gap-1.5 w-full">
<div className="flex justify-between items-baseline">
<label className="block text-sm font-semibold text-gray-700">
<label className="block text-sm font-semibold text-gray-700 tracking-tight">
{label}
</label>
{helperText && (
<span className="text-xs text-gray-500 italic">{helperText}</span>
)}
</div>
<div className="relative">
<div className="relative group">
<select
className={`block w-full appearance-none rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm p-2.5 pr-8 border bg-white text-gray-800 ${className}`}
className={`appearance-none block w-full rounded-md border border-gray-300 bg-white py-2.5 pl-3 pr-10 text-sm leading-5 text-gray-900 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm transition-colors duration-200 ease-in-out group-hover:border-indigo-300 cursor-pointer ${className}`}
{...props}
>
{options.map((opt) => (
@@ -29,9 +29,9 @@ export const Select: React.FC<SelectProps> = ({ label, options, helperText, clas
</option>
))}
</select>
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-500">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-400 group-hover:text-indigo-500 transition-colors duration-200">
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clipRule="evenodd" />
</svg>
</div>
</div>

64
components/Toast.tsx Normal file
View File

@@ -0,0 +1,64 @@
import React, { useEffect, useState } from 'react';
export type ToastType = 'success' | 'error' | 'info';
export interface ToastProps {
message: string;
type: ToastType;
onClose: () => void;
}
export const Toast: React.FC<ToastProps> = ({ message, type, onClose }) => {
const [visible, setVisible] = useState(false);
useEffect(() => {
// Start entry animation
requestAnimationFrame(() => setVisible(true));
// Auto dismiss
const timer = setTimeout(() => {
setVisible(false);
// Allow exit animation to complete before unmounting
setTimeout(onClose, 300);
}, 3000);
return () => clearTimeout(timer);
}, [onClose]);
const baseClasses = "fixed top-6 left-1/2 transform -translate-x-1/2 z-[100] flex items-center gap-3 px-5 py-3 rounded-lg shadow-xl text-white font-medium text-sm transition-all duration-300 ease-out";
const typeClasses = {
success: "bg-emerald-600 ring-1 ring-emerald-500",
error: "bg-rose-600 ring-1 ring-rose-500",
info: "bg-indigo-600 ring-1 ring-indigo-500"
};
// Animation states
const opacityClass = visible ? "opacity-100 translate-y-0 scale-100" : "opacity-0 -translate-y-4 scale-95";
const icons = {
success: (
<svg className="w-5 h-5 text-emerald-100" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M5 13l4 4L19 7" />
</svg>
),
error: (
<svg className="w-5 h-5 text-rose-100" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M6 18L18 6M6 6l12 12" />
</svg>
),
info: (
<svg className="w-5 h-5 text-indigo-100" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
};
return (
<div className={`${baseClasses} ${typeClasses[type]} ${opacityClass}`}>
{icons[type]}
<span className="drop-shadow-sm">{message}</span>
</div>
);
};

Binary file not shown.

View File

@@ -42,12 +42,15 @@ export const generateSql = async (requestData: SqlGenerationRequest): Promise<st
const ai = new GoogleGenAI({ apiKey: apiKey });
// Using gemini-2.5-flash for speed and good reasoning capabilities on coding tasks
// Use the user-selected model, defaulting to gemini-3-flash-preview if somehow not provided
const modelName = requestData.model || 'gemini-3-flash-preview';
const response = await ai.models.generateContent({
model: 'gemini-2.5-flash',
model: modelName,
contents: generateSqlPrompt(requestData),
config: {
thinkingConfig: { thinkingBudget: 0 }, // Disable thinking for faster direct response
// We allow the model to use its default thinking behavior (especially for Gemini 3 Pro)
// rather than forcing thinkingBudget: 0.
temperature: 0.2, // Lower temperature for more deterministic code generation
}
});

View File

@@ -6,6 +6,7 @@ export interface SqlGenerationRequest {
dictionaryData: string;
requirement: string;
databaseType: DatabaseType;
model: string;
apiKey?: string;
}