更新至 v0.2.0_20251226 版本
This commit is contained in:
54
App.tsx
54
App.tsx
@@ -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;
|
||||
|
||||
@@ -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
64
components/Toast.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
BIN
releases/WORK-SH-XPC-SQL-0.2.0_20251226.zip
Normal file
BIN
releases/WORK-SH-XPC-SQL-0.2.0_20251226.zip
Normal file
Binary file not shown.
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user