Add and update frontend files
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Responsive } from 'react-grid-layout';
|
||||
import WidthProvider from 'react-grid-layout';
|
||||
import { useDashboardStore } from '../store/dashboardStore';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X } from "lucide-react";
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line } from 'recharts';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
export function Dashboard() {
|
||||
const { charts, removeChart } = useDashboardStore();
|
||||
const ResponsiveGridLayout = useMemo(() => WidthProvider(Responsive as any) as any, []);
|
||||
|
||||
const layouts = useMemo(() => ({
|
||||
lg: charts.map((c) => c.layout)
|
||||
}), [charts]);
|
||||
|
||||
const onLayoutChange = (_currentLayout: any, _allLayouts: any) => {
|
||||
// updateLayout(currentLayout); // This might cause infinite loops if not handled carefully
|
||||
// For simplicity, we just log it or update it if needed.
|
||||
// In a real app, we would debounce this and save to backend.
|
||||
};
|
||||
|
||||
if (charts.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-muted-foreground">
|
||||
<p>No charts in dashboard.</p>
|
||||
<p className="text-sm">Go to Chat and add some visualizations!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 h-full overflow-y-auto">
|
||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
<ResponsiveGridLayout
|
||||
className="layout"
|
||||
layouts={layouts}
|
||||
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
|
||||
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
|
||||
rowHeight={100}
|
||||
onLayoutChange={onLayoutChange}
|
||||
isDraggable
|
||||
isResizable
|
||||
>
|
||||
{charts.map((chart) => (
|
||||
<div key={chart.id} className="relative group">
|
||||
<Card className="h-full flex flex-col shadow-sm border-muted">
|
||||
<CardHeader className="pb-2 shrink-0 flex flex-row items-center justify-between space-y-0">
|
||||
<div>
|
||||
<CardTitle className="text-base">{chart.title}</CardTitle>
|
||||
<CardDescription className="text-xs">{chart.type.toUpperCase()} Chart</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => removeChart(chart.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 min-h-0 p-2">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{chart.type === 'bar' ? (
|
||||
<BarChart data={chart.data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
||||
<XAxis dataKey="name" tickLine={false} axisLine={false} tick={{ fontSize: 10, fill: '#6b7280' }} />
|
||||
<YAxis tickLine={false} axisLine={false} tick={{ fontSize: 10, fill: '#6b7280' }} />
|
||||
<Tooltip
|
||||
cursor={{ fill: 'rgba(0,0,0,0.05)' }}
|
||||
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
||||
/>
|
||||
<Bar dataKey="sales" fill="#3b82f6" radius={[4, 4, 0, 0]} name="Sales" />
|
||||
<Bar dataKey="profit" fill="#10b981" radius={[4, 4, 0, 0]} name="Profit" />
|
||||
</BarChart>
|
||||
) : (
|
||||
<LineChart data={chart.data}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e5e7eb" />
|
||||
<XAxis dataKey="name" tickLine={false} axisLine={false} tick={{ fontSize: 10, fill: '#6b7280' }} />
|
||||
<YAxis tickLine={false} axisLine={false} tick={{ fontSize: 10, fill: '#6b7280' }} />
|
||||
<Tooltip
|
||||
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
||||
/>
|
||||
<Line type="monotone" dataKey="sales" stroke="#3b82f6" strokeWidth={2} dot={{ r: 4 }} />
|
||||
<Line type="monotone" dataKey="profit" stroke="#10b981" strokeWidth={2} dot={{ r: 4 }} />
|
||||
</LineChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</ResponsiveGridLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Save, Loader2 } from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
interface LLMConfig {
|
||||
id: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
api_key?: string;
|
||||
api_base?: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
const [configId, setConfigId] = useState<string | null>(null);
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [provider, setProvider] = useState('openai');
|
||||
const [model, setModel] = useState('gpt-4-turbo');
|
||||
const [baseUrl, setBaseUrl] = useState('https://api.openai.com/v1');
|
||||
const [enableVoice, setEnableVoice] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const configs = await api.get<LLMConfig[]>('/api/v1/llm');
|
||||
const activeConfig = configs.find(c => c.is_active) || configs[0];
|
||||
if (activeConfig) {
|
||||
setConfigId(activeConfig.id);
|
||||
setProvider(activeConfig.provider);
|
||||
setModel(activeConfig.model);
|
||||
setApiKey(activeConfig.api_key || '');
|
||||
setBaseUrl(activeConfig.api_base || '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch LLM config", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const configData = {
|
||||
provider,
|
||||
model,
|
||||
api_key: apiKey,
|
||||
api_base: baseUrl,
|
||||
is_active: true
|
||||
};
|
||||
|
||||
if (configId) {
|
||||
await api.put(`/api/v1/llm/${configId}`, configData);
|
||||
} else {
|
||||
const newId = Date.now().toString();
|
||||
await api.post('/api/v1/llm', { ...configData, id: newId });
|
||||
setConfigId(newId);
|
||||
}
|
||||
alert("Settings saved successfully!");
|
||||
} catch (error) {
|
||||
console.error("Failed to save settings", error);
|
||||
alert("Failed to save settings");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 h-full overflow-y-auto">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold">Settings</h1>
|
||||
<p className="text-muted-foreground">Configure AI model and application preferences</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 max-w-2xl">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>LLM Configuration</CardTitle>
|
||||
<CardDescription>Manage your Large Language Model settings</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="provider">Provider</Label>
|
||||
<Select value={provider} onValueChange={(val) => val && setProvider(val)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
<SelectItem value="anthropic">Anthropic</SelectItem>
|
||||
<SelectItem value="azure">Azure OpenAI</SelectItem>
|
||||
<SelectItem value="local">Local (Ollama/LM Studio)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="model">Model</Label>
|
||||
<Select value={model} onValueChange={(val) => val && setModel(val)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="gpt-4-turbo">GPT-4 Turbo</SelectItem>
|
||||
<SelectItem value="gpt-4o">GPT-4o</SelectItem>
|
||||
<SelectItem value="gpt-3.5-turbo">GPT-3.5 Turbo</SelectItem>
|
||||
<SelectItem value="claude-3-opus">Claude 3 Opus</SelectItem>
|
||||
<SelectItem value="claude-3-sonnet">Claude 3 Sonnet</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api-key">API Key</Label>
|
||||
<Input
|
||||
id="api-key"
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="base-url">Base URL (Optional)</Label>
|
||||
<Input
|
||||
id="base-url"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Interface Settings</CardTitle>
|
||||
<CardDescription>Customize your experience</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="voice-mode">Voice Mode</Label>
|
||||
<p className="text-sm text-muted-foreground">Enable voice input and output</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="voice-mode"
|
||||
checked={enableVoice}
|
||||
onCheckedChange={setEnableVoice}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="dark-mode">Dark Mode</Label>
|
||||
<p className="text-sm text-muted-foreground">Toggle dark/light theme</p>
|
||||
</div>
|
||||
<Switch id="dark-mode" defaultChecked />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button onClick={handleSave} className="ml-auto" disabled={isSaving}>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Trash2, Edit2, Plus, Terminal, Loader2 } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
interface Skill {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
content: string;
|
||||
type: 'python' | 'sql' | 'api';
|
||||
}
|
||||
|
||||
export function Skills() {
|
||||
const [skills, setSkills] = useState<Skill[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [newSkill, setNewSkill] = useState<Partial<Skill>>({ type: 'python', content: '' });
|
||||
|
||||
useEffect(() => {
|
||||
fetchSkills();
|
||||
}, []);
|
||||
|
||||
const fetchSkills = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await api.get<Skill[]>('/api/v1/skills');
|
||||
setSkills(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch skills", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSkill = async () => {
|
||||
if (newSkill.name && newSkill.description && newSkill.content) {
|
||||
try {
|
||||
const skillToCreate = {
|
||||
...newSkill,
|
||||
id: Date.now().toString(),
|
||||
};
|
||||
const createdSkill = await api.post<Skill>('/api/v1/skills', skillToCreate);
|
||||
setSkills([...skills, createdSkill]);
|
||||
setNewSkill({ type: 'python', content: '' });
|
||||
setIsDialogOpen(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to create skill", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSkill = async (id: string) => {
|
||||
try {
|
||||
await api.delete(`/api/v1/skills/${id}`);
|
||||
setSkills(skills.filter(s => s.id !== id));
|
||||
} catch (error) {
|
||||
console.error("Failed to delete skill", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 h-full flex flex-col overflow-hidden">
|
||||
<div className="flex justify-between items-center mb-6 shrink-0">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Skills</h1>
|
||||
<p className="text-muted-foreground">Manage AI capabilities and tools</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger render={
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Skill
|
||||
</Button>
|
||||
} />
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Skill</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newSkill.name || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, name: e.target.value})}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="type" className="text-right">Type</Label>
|
||||
<Select
|
||||
value={newSkill.type}
|
||||
onValueChange={(val: any) => setNewSkill({...newSkill, type: val})}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="python">Python</SelectItem>
|
||||
<SelectItem value="sql">SQL</SelectItem>
|
||||
<SelectItem value="api">API</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
value={newSkill.description || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, description: e.target.value})}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="content" className="text-right">Content</Label>
|
||||
<Textarea
|
||||
id="content"
|
||||
value={newSkill.content || ''}
|
||||
onChange={(e) => setNewSkill({...newSkill, content: e.target.value})}
|
||||
className="col-span-3 font-mono text-xs"
|
||||
placeholder="Python code, SQL query template, or API spec..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddSkill}>Save Skill</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 pb-4">
|
||||
{skills.map((skill) => (
|
||||
<Card key={skill.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-base font-medium flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4 text-muted-foreground" />
|
||||
{skill.name}
|
||||
</CardTitle>
|
||||
<CardDescription>{skill.type.toUpperCase()}</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground">
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => handleDeleteSkill(skill.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{skill.description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user