Outfit + JetBrains Mono typography, soft dark palette with blue undertones, electric teal primary, purple-branded AI features, noise texture, glow effects, glassmorphism, and refined grid/tree.
104 lines
3.0 KiB
TypeScript
104 lines
3.0 KiB
TypeScript
import { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { AiSettingsPopover } from "./AiSettingsPopover";
|
|
import { useGenerateSql } from "@/hooks/use-ai";
|
|
import { Sparkles, Loader2, X, Eraser } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
interface Props {
|
|
connectionId: string;
|
|
onSqlGenerated: (sql: string) => void;
|
|
onClose: () => void;
|
|
onExecute?: () => void;
|
|
}
|
|
|
|
export function AiBar({ connectionId, onSqlGenerated, onClose, onExecute }: Props) {
|
|
const [prompt, setPrompt] = useState("");
|
|
const generateMutation = useGenerateSql();
|
|
|
|
const handleGenerate = () => {
|
|
if (!prompt.trim() || generateMutation.isPending) return;
|
|
generateMutation.mutate(
|
|
{ connectionId, prompt },
|
|
{
|
|
onSuccess: (sql) => {
|
|
onSqlGenerated(sql);
|
|
},
|
|
onError: (err) => {
|
|
toast.error("AI generation failed", { description: String(err) });
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
onExecute?.();
|
|
return;
|
|
}
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleGenerate();
|
|
return;
|
|
}
|
|
if (e.key === "Escape") {
|
|
e.stopPropagation();
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="tusk-ai-bar flex items-center gap-2 px-2 py-1.5 tusk-fade-in">
|
|
<Sparkles className="h-3.5 w-3.5 shrink-0 tusk-ai-icon" />
|
|
<Input
|
|
value={prompt}
|
|
onChange={(e) => setPrompt(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder="Describe the query you want..."
|
|
className="h-7 min-w-0 flex-1 border-tusk-purple/20 bg-tusk-purple/5 text-xs placeholder:text-muted-foreground/40 focus:border-tusk-purple/40 focus:ring-tusk-purple/20"
|
|
autoFocus
|
|
disabled={generateMutation.isPending}
|
|
/>
|
|
<Button
|
|
size="xs"
|
|
variant="ghost"
|
|
className="gap-1 text-[11px] text-tusk-purple hover:bg-tusk-purple/10 hover:text-tusk-purple"
|
|
onClick={handleGenerate}
|
|
disabled={generateMutation.isPending || !prompt.trim()}
|
|
>
|
|
{generateMutation.isPending ? (
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
) : (
|
|
"Generate"
|
|
)}
|
|
</Button>
|
|
{prompt.trim() && (
|
|
<Button
|
|
size="icon-xs"
|
|
variant="ghost"
|
|
onClick={() => setPrompt("")}
|
|
title="Clear prompt"
|
|
disabled={generateMutation.isPending}
|
|
className="text-muted-foreground"
|
|
>
|
|
<Eraser className="h-3 w-3" />
|
|
</Button>
|
|
)}
|
|
<AiSettingsPopover />
|
|
<Button
|
|
size="icon-xs"
|
|
variant="ghost"
|
|
onClick={onClose}
|
|
title="Close AI bar"
|
|
className="text-muted-foreground"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|