Enhance app with project management, budget tools, and voice commands
Adds ProjectManager, ConversationHistory, EnhancedVoiceCommands components, translation keys, and updates Settings page. Replit-Commit-Author: Agent Replit-Commit-Session-Id: ff0be73b-afdd-4747-978b-bb8301fb0a82 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/9777c70b-fc38-4831-8d6b-78dfffe041b0/82569113-480a-45a6-8982-ffee4f083c5a.jpg
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
MessageSquare,
|
||||
Search,
|
||||
Filter,
|
||||
BookOpen,
|
||||
Trash2,
|
||||
Download,
|
||||
Star,
|
||||
Clock,
|
||||
Brain,
|
||||
User,
|
||||
Bot,
|
||||
Volume2
|
||||
} from "lucide-react";
|
||||
|
||||
interface Conversation {
|
||||
id: number;
|
||||
type: string;
|
||||
prompt: string;
|
||||
response: string;
|
||||
modelUsed: string;
|
||||
processingTime: number;
|
||||
wasSpoken: boolean;
|
||||
rating?: number;
|
||||
tags?: string[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ConversationStats {
|
||||
totalConversations: number;
|
||||
averageRating: number;
|
||||
totalProcessingTime: number;
|
||||
favoriteTopics: string[];
|
||||
mostUsedModel: string;
|
||||
}
|
||||
|
||||
export function ConversationHistory() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [typeFilter, setTypeFilter] = useState("all");
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation | null>(null);
|
||||
|
||||
const { data: conversations = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/ai/conversations", searchQuery, typeFilter],
|
||||
queryFn: () => apiRequest(`/api/ai/conversations?search=${searchQuery}&type=${typeFilter}`),
|
||||
});
|
||||
|
||||
const { data: stats } = useQuery({
|
||||
queryKey: ["/api/ai/conversations/stats"],
|
||||
queryFn: () => apiRequest("/api/ai/conversations/stats"),
|
||||
});
|
||||
|
||||
const deleteConversationMutation = useMutation({
|
||||
mutationFn: (id: number) => apiRequest(`/api/ai/conversations/${id}`, { method: "DELETE" }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations/stats"] });
|
||||
toast({
|
||||
title: t("success.deleted"),
|
||||
description: t("ai.conversationDeleted"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const rateConversationMutation = useMutation({
|
||||
mutationFn: ({ id, rating }: { id: number; rating: number }) =>
|
||||
apiRequest(`/api/ai/conversations/${id}/rate`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ rating }),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/ai/conversations/stats"] });
|
||||
toast({
|
||||
title: t("success.updated"),
|
||||
description: t("ai.conversationRated"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const exportConversationsMutation = useMutation({
|
||||
mutationFn: () => apiRequest("/api/ai/conversations/export"),
|
||||
onSuccess: (data) => {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `conversations-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: t("success.exported"),
|
||||
description: t("ai.conversationsExported"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const filteredConversations = conversations.filter((conv: Conversation) => {
|
||||
const matchesSearch = !searchQuery ||
|
||||
conv.prompt.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
conv.response.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesType = typeFilter === "all" || conv.type === typeFilter;
|
||||
return matchesSearch && matchesType;
|
||||
});
|
||||
|
||||
const conversationTypes = [
|
||||
{ value: "all", label: t("ai.allTypes") },
|
||||
{ value: "joke", label: t("ai.jokes") },
|
||||
{ value: "insight", label: t("ai.insights") },
|
||||
{ value: "response", label: t("ai.responses") },
|
||||
{ value: "task_help", label: t("ai.taskHelp") },
|
||||
{ value: "financial_advice", label: t("ai.financialAdvice") },
|
||||
];
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "joke": return "😄";
|
||||
case "insight": return "💡";
|
||||
case "task_help": return "✅";
|
||||
case "financial_advice": return "💰";
|
||||
default: return "💬";
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Card key={i} className="animate-pulse">
|
||||
<CardHeader className="space-y-2">
|
||||
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||||
<div className="h-3 bg-muted rounded w-1/2"></div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-20 bg-muted rounded"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">{t("ai.conversationHistory")}</h2>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => exportConversationsMutation.mutate()}
|
||||
disabled={exportConversationsMutation.isPending}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
{t("ai.exportConversations")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
{stats && (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("ai.totalConversations")}</CardTitle>
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.totalConversations}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("ai.averageRating")}</CardTitle>
|
||||
<Star className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{stats.averageRating?.toFixed(1) || "N/A"}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("ai.averageResponseTime")}</CardTitle>
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{(stats.totalProcessingTime / stats.totalConversations)?.toFixed(0) || "N/A"}ms</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("ai.mostUsedModel")}</CardTitle>
|
||||
<Brain className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-sm font-bold">{stats.mostUsedModel || "N/A"}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex space-x-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("ai.searchConversations")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="px-3 py-2 border rounded-md bg-background"
|
||||
>
|
||||
{conversationTypes.map((type) => (
|
||||
<option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Conversations List */}
|
||||
<div className="space-y-4">
|
||||
{filteredConversations.length > 0 ? (
|
||||
filteredConversations.map((conversation: Conversation) => (
|
||||
<Card key={conversation.id} className="hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-lg">{getTypeIcon(conversation.type)}</span>
|
||||
<Badge variant="secondary">{t(`ai.${conversation.type}`)}</Badge>
|
||||
{conversation.wasSpoken && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Volume2 className="h-3 w-3 mr-1" />
|
||||
{t("ai.spoken")}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{conversation.processingTime}ms
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{format(new Date(conversation.createdAt), "MMM dd, yyyy HH:mm")}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => deleteConversationMutation.mutate(conversation.id)}
|
||||
className="h-8 w-8 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<User className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-1">{t("ai.userPrompt")}</p>
|
||||
<p className="text-sm">{conversation.prompt}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="w-8 h-8 rounded-full bg-secondary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Bot className="h-4 w-4 text-secondary-foreground" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-1">{t("ai.aiResponse")}</p>
|
||||
<p className="text-sm">{conversation.response}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("ai.model")}: {conversation.modelUsed}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
onClick={() => rateConversationMutation.mutate({ id: conversation.id, rating: star })}
|
||||
className={`h-4 w-4 ${
|
||||
(conversation.rating || 0) >= star
|
||||
? "text-yellow-400 fill-current"
|
||||
: "text-gray-300"
|
||||
} hover:text-yellow-400 transition-colors`}
|
||||
>
|
||||
<Star className="h-4 w-4" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">{t("ai.noConversations")}</h3>
|
||||
<p className="text-muted-foreground text-center">
|
||||
{searchQuery || typeFilter !== "all"
|
||||
? t("ai.noMatchingConversations")
|
||||
: t("ai.noConversationsDescription")
|
||||
}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detailed View Dialog */}
|
||||
<Dialog open={!!selectedConversation} onOpenChange={() => setSelectedConversation(null)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("ai.conversationDetails")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedConversation && format(new Date(selectedConversation.createdAt), "MMMM dd, yyyy 'at' HH:mm")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{selectedConversation && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="font-medium">{t("ai.type")}:</span>
|
||||
<Badge variant="secondary" className="ml-2">
|
||||
{t(`ai.${selectedConversation.type}`)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">{t("ai.processingTime")}:</span>
|
||||
<span className="ml-2">{selectedConversation.processingTime}ms</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">{t("ai.model")}:</span>
|
||||
<span className="ml-2">{selectedConversation.modelUsed}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">{t("ai.wasSpoken")}:</span>
|
||||
<span className="ml-2">{selectedConversation.wasSpoken ? t("common.yes") : t("common.no")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="h-64">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">{t("ai.userPrompt")}</h4>
|
||||
<p className="text-sm bg-muted p-3 rounded">{selectedConversation.prompt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">{t("ai.aiResponse")}</h4>
|
||||
<p className="text-sm bg-muted p-3 rounded">{selectedConversation.response}</p>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { Plus, FolderOpen, Calendar, Users, MoreHorizontal, Edit, Trash2, CheckCircle2, Circle, Clock } from "lucide-react";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
|
||||
const projectSchema = z.object({
|
||||
name: z.string().min(1, "Project name is required"),
|
||||
description: z.string().optional(),
|
||||
status: z.enum(["active", "on_hold", "completed", "cancelled"]),
|
||||
color: z.string().regex(/^#[0-9A-F]{6}$/i, "Invalid color format"),
|
||||
deadline: z.string().optional(),
|
||||
});
|
||||
|
||||
type ProjectFormData = z.infer<typeof projectSchema>;
|
||||
|
||||
interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
status: string;
|
||||
color: string;
|
||||
deadline?: string;
|
||||
taskCount: number;
|
||||
completedTasks: number;
|
||||
progress: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
active: "bg-green-500",
|
||||
on_hold: "bg-yellow-500",
|
||||
completed: "bg-blue-500",
|
||||
cancelled: "bg-red-500",
|
||||
};
|
||||
|
||||
const predefinedColors = [
|
||||
"#3B82F6", "#EF4444", "#10B981", "#F59E0B",
|
||||
"#8B5CF6", "#EC4899", "#06B6D4", "#84CC16"
|
||||
];
|
||||
|
||||
export function ProjectManager() {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState<Project | null>(null);
|
||||
|
||||
const form = useForm<ProjectFormData>({
|
||||
resolver: zodResolver(projectSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
status: "active",
|
||||
color: predefinedColors[0],
|
||||
},
|
||||
});
|
||||
|
||||
const { data: projects = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/projects"],
|
||||
});
|
||||
|
||||
const createProjectMutation = useMutation({
|
||||
mutationFn: (data: ProjectFormData) => apiRequest("/api/projects", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/projects"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
toast({
|
||||
title: t("success.created"),
|
||||
description: t("tasks.projectCreated"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateProjectMutation = useMutation({
|
||||
mutationFn: ({ id, ...data }: ProjectFormData & { id: number }) =>
|
||||
apiRequest(`/api/projects/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/projects"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
setEditingProject(null);
|
||||
form.reset();
|
||||
toast({
|
||||
title: t("success.updated"),
|
||||
description: t("tasks.projectUpdated"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const deleteProjectMutation = useMutation({
|
||||
mutationFn: (id: number) => apiRequest(`/api/projects/${id}`, {
|
||||
method: "DELETE",
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/projects"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/tasks"] });
|
||||
toast({
|
||||
title: t("success.deleted"),
|
||||
description: t("tasks.projectDeleted"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (data: ProjectFormData) => {
|
||||
if (editingProject) {
|
||||
updateProjectMutation.mutate({ ...data, id: editingProject.id });
|
||||
} else {
|
||||
createProjectMutation.mutate(data);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (project: Project) => {
|
||||
setEditingProject(project);
|
||||
form.reset({
|
||||
name: project.name,
|
||||
description: project.description || "",
|
||||
status: project.status as "active" | "on_hold" | "completed" | "cancelled",
|
||||
color: project.color,
|
||||
deadline: project.deadline?.split('T')[0],
|
||||
});
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case "completed":
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
||||
case "on_hold":
|
||||
return <Clock className="h-4 w-4 text-yellow-500" />;
|
||||
case "cancelled":
|
||||
return <Circle className="h-4 w-4 text-red-500" />;
|
||||
default:
|
||||
return <Circle className="h-4 w-4 text-blue-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Card key={i} className="animate-pulse">
|
||||
<CardHeader className="space-y-2">
|
||||
<div className="h-4 bg-muted rounded w-3/4"></div>
|
||||
<div className="h-3 bg-muted rounded w-1/2"></div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
<div className="h-2 bg-muted rounded"></div>
|
||||
<div className="flex justify-between">
|
||||
<div className="h-3 bg-muted rounded w-1/4"></div>
|
||||
<div className="h-3 bg-muted rounded w-1/4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">{t("tasks.projects")}</h2>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button onClick={() => { setEditingProject(null); form.reset(); }}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("tasks.createProject")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingProject ? t("tasks.editProject") : t("tasks.createProject")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("tasks.projectDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("tasks.projectName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("tasks.enterProjectName")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("tasks.description")} ({t("common.optional")})</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t("tasks.enterProjectDescription")}
|
||||
className="resize-none"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="status"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("tasks.status")}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("tasks.selectStatus")} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="active">{t("tasks.active")}</SelectItem>
|
||||
<SelectItem value="on_hold">{t("tasks.onHold")}</SelectItem>
|
||||
<SelectItem value="completed">{t("tasks.completed")}</SelectItem>
|
||||
<SelectItem value="cancelled">{t("tasks.cancelled")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="deadline"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("tasks.deadline")} ({t("common.optional")})</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="date" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="color"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("tasks.projectColor")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex space-x-2">
|
||||
{predefinedColors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
className={`w-8 h-8 rounded-full border-2 ${
|
||||
field.value === color ? 'border-foreground' : 'border-muted'
|
||||
}`}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => field.onChange(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={createProjectMutation.isPending || updateProjectMutation.isPending}>
|
||||
{editingProject ? t("common.update") : t("common.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{projects.map((project: Project) => (
|
||||
<Card key={project.id} className="relative hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: project.color }}
|
||||
/>
|
||||
<CardTitle className="text-lg truncate">{project.name}</CardTitle>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleEdit(project)}>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
{t("common.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => deleteProjectMutation.mutate(project.id)}
|
||||
className="text-destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
{t("common.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getStatusIcon(project.status)}
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t(`tasks.${project.status}`)}
|
||||
</Badge>
|
||||
{project.deadline && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Calendar className="h-3 w-3 mr-1" />
|
||||
{new Date(project.deadline).toLocaleDateString()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{project.description && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{project.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>{t("tasks.progress")}</span>
|
||||
<span>{project.completedTasks}/{project.taskCount} {t("tasks.tasks")}</span>
|
||||
</div>
|
||||
<Progress value={project.progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-1">
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span>{project.taskCount} {t("tasks.tasks")}</span>
|
||||
</div>
|
||||
<span>{new Date(project.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{projects.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<FolderOpen className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">{t("tasks.noProjects")}</h3>
|
||||
<p className="text-muted-foreground text-center mb-4">
|
||||
{t("tasks.noProjectsDescription")}
|
||||
</p>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("tasks.createFirstProject")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { useVoiceIntegration } from "@/hooks/useVoiceIntegration";
|
||||
import {
|
||||
Mic,
|
||||
MicOff,
|
||||
Settings,
|
||||
Plus,
|
||||
Play,
|
||||
Square,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Languages,
|
||||
Zap,
|
||||
Brain,
|
||||
MessageSquare,
|
||||
Clock,
|
||||
Star
|
||||
} from "lucide-react";
|
||||
|
||||
const customCommandSchema = z.object({
|
||||
name: z.string().min(1, "Command name is required"),
|
||||
trigger: z.string().min(1, "Trigger phrase is required"),
|
||||
action: z.enum(["navigate", "create_task", "add_expense", "custom_script"]),
|
||||
parameters: z.string().optional(),
|
||||
isActive: z.boolean().default(true),
|
||||
language: z.enum(["en", "ar", "both"]),
|
||||
});
|
||||
|
||||
type CustomCommandFormData = z.infer<typeof customCommandSchema>;
|
||||
|
||||
interface VoiceCommand {
|
||||
id: number;
|
||||
name: string;
|
||||
trigger: string;
|
||||
action: string;
|
||||
parameters?: string;
|
||||
isActive: boolean;
|
||||
language: string;
|
||||
usageCount: number;
|
||||
accuracy: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface VoiceSettings {
|
||||
language: string;
|
||||
continuous: boolean;
|
||||
interimResults: boolean;
|
||||
confidenceThreshold: number;
|
||||
speakingRate: number;
|
||||
voicePitch: number;
|
||||
voiceVolume: number;
|
||||
autoSpeak: boolean;
|
||||
wakeWord: string;
|
||||
}
|
||||
|
||||
const bilingualCommands = {
|
||||
en: [
|
||||
{ trigger: "create task", action: "create_task", description: "Create a new task" },
|
||||
{ trigger: "add expense", action: "add_expense", description: "Add an expense" },
|
||||
{ trigger: "add income", action: "add_income", description: "Add income" },
|
||||
{ trigger: "go to dashboard", action: "navigate", parameters: "/dashboard", description: "Navigate to dashboard" },
|
||||
{ trigger: "go to tasks", action: "navigate", parameters: "/tasks", description: "Navigate to tasks" },
|
||||
{ trigger: "go to finances", action: "navigate", parameters: "/finances", description: "Navigate to finances" },
|
||||
{ trigger: "show summary", action: "show_summary", description: "Show financial summary" },
|
||||
{ trigger: "tell me a joke", action: "ai_joke", description: "Get a daily joke" },
|
||||
{ trigger: "start timer", action: "start_timer", description: "Start time tracking" },
|
||||
{ trigger: "stop timer", action: "stop_timer", description: "Stop time tracking" },
|
||||
],
|
||||
ar: [
|
||||
{ trigger: "إنشاء مهمة", action: "create_task", description: "إنشاء مهمة جديدة" },
|
||||
{ trigger: "أضف مصروف", action: "add_expense", description: "إضافة مصروف" },
|
||||
{ trigger: "أضف دخل", action: "add_income", description: "إضافة دخل" },
|
||||
{ trigger: "اذهب إلى لوحة التحكم", action: "navigate", parameters: "/dashboard", description: "الانتقال إلى لوحة التحكم" },
|
||||
{ trigger: "اذهب إلى المهام", action: "navigate", parameters: "/tasks", description: "الانتقال إلى المهام" },
|
||||
{ trigger: "اذهب إلى الماليات", action: "navigate", parameters: "/finances", description: "الانتقال إلى الماليات" },
|
||||
{ trigger: "أظهر الملخص", action: "show_summary", description: "عرض الملخص المالي" },
|
||||
{ trigger: "احك لي نكتة", action: "ai_joke", description: "الحصول على نكتة اليوم" },
|
||||
{ trigger: "ابدأ المؤقت", action: "start_timer", description: "بدء تتبع الوقت" },
|
||||
{ trigger: "أوقف المؤقت", action: "stop_timer", description: "إيقاف تتبع الوقت" },
|
||||
]
|
||||
};
|
||||
|
||||
export function EnhancedVoiceCommands() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const { speak, isListening, toggleListening, isSupported } = useVoiceIntegration();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isTraining, setIsTraining] = useState(false);
|
||||
const [trainingPhrase, setTrainingPhrase] = useState("");
|
||||
const [voiceSettings, setVoiceSettings] = useState<VoiceSettings>({
|
||||
language: i18n.language,
|
||||
continuous: true,
|
||||
interimResults: true,
|
||||
confidenceThreshold: 0.7,
|
||||
speakingRate: 1.0,
|
||||
voicePitch: 1.0,
|
||||
voiceVolume: 1.0,
|
||||
autoSpeak: true,
|
||||
wakeWord: "hey assistant",
|
||||
});
|
||||
|
||||
const form = useForm<CustomCommandFormData>({
|
||||
resolver: zodResolver(customCommandSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
trigger: "",
|
||||
action: "navigate",
|
||||
parameters: "",
|
||||
isActive: true,
|
||||
language: "both",
|
||||
},
|
||||
});
|
||||
|
||||
const { data: customCommands = [], isLoading } = useQuery({
|
||||
queryKey: ["/api/voice/custom-commands"],
|
||||
});
|
||||
|
||||
const { data: voiceHistory = [] } = useQuery({
|
||||
queryKey: ["/api/voice/history"],
|
||||
});
|
||||
|
||||
const createCommandMutation = useMutation({
|
||||
mutationFn: (data: CustomCommandFormData) => apiRequest("/api/voice/custom-commands", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/voice/custom-commands"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
toast({
|
||||
title: t("success.created"),
|
||||
description: t("voice.customCommandCreated"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateSettingsMutation = useMutation({
|
||||
mutationFn: (settings: VoiceSettings) => apiRequest("/api/voice/settings", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(settings),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: t("success.updated"),
|
||||
description: t("voice.settingsUpdated"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (data: CustomCommandFormData) => {
|
||||
createCommandMutation.mutate(data);
|
||||
};
|
||||
|
||||
const handleTraining = async () => {
|
||||
if (!trainingPhrase) return;
|
||||
|
||||
setIsTraining(true);
|
||||
speak(t("voice.repeatPhrase") + ": " + trainingPhrase);
|
||||
|
||||
// Start listening for the training phrase
|
||||
setTimeout(() => {
|
||||
setIsTraining(false);
|
||||
toast({
|
||||
title: t("voice.trainingComplete"),
|
||||
description: t("voice.phraseTrainingComplete"),
|
||||
});
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const currentLanguageCommands = bilingualCommands[i18n.language as keyof typeof bilingualCommands] || bilingualCommands.en;
|
||||
|
||||
const handleSettingsUpdate = (key: keyof VoiceSettings, value: any) => {
|
||||
const newSettings = { ...voiceSettings, [key]: value };
|
||||
setVoiceSettings(newSettings);
|
||||
updateSettingsMutation.mutate(newSettings);
|
||||
};
|
||||
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<MicOff className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">{t("voice.notSupported")}</h3>
|
||||
<p className="text-muted-foreground text-center">
|
||||
{t("voice.notSupportedDescription")}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-2xl font-bold">{t("voice.title")}</h2>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant={isListening ? "destructive" : "default"}
|
||||
onClick={toggleListening}
|
||||
className="gap-2"
|
||||
>
|
||||
{isListening ? <Square className="h-4 w-4" /> : <Mic className="h-4 w-4" />}
|
||||
{isListening ? t("voice.stopListening") : t("voice.startListening")}
|
||||
</Button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("voice.addCustomCommand")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("voice.createCustomCommand")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("voice.customCommandDescription")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("voice.commandName")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("voice.enterCommandName")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="trigger"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("voice.triggerPhrase")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("voice.enterTriggerPhrase")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={createCommandMutation.isPending}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="commands" className="space-y-4">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="commands">{t("voice.commands")}</TabsTrigger>
|
||||
<TabsTrigger value="training">{t("voice.training")}</TabsTrigger>
|
||||
<TabsTrigger value="settings">{t("voice.settings")}</TabsTrigger>
|
||||
<TabsTrigger value="history">{t("voice.history")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="commands" className="space-y-4">
|
||||
<div className="grid gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Languages className="h-5 w-5" />
|
||||
{t("voice.builtInCommands")} ({i18n.language.toUpperCase()})
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("voice.builtInCommandsDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-3">
|
||||
{currentLanguageCommands.map((command, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">"{command.trigger}"</p>
|
||||
<p className="text-sm text-muted-foreground">{command.description}</p>
|
||||
</div>
|
||||
<Badge variant="secondary">{command.action}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="h-5 w-5" />
|
||||
{t("voice.customCommands")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("voice.customCommandsDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{customCommands.length > 0 ? (
|
||||
<div className="grid gap-3">
|
||||
{customCommands.map((command: VoiceCommand) => (
|
||||
<div key={command.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">"{command.trigger}"</p>
|
||||
<p className="text-sm text-muted-foreground">{command.name}</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{command.language}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{command.usageCount} uses
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{(command.accuracy * 100).toFixed(0)}% accuracy
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch checked={command.isActive} />
|
||||
<Badge variant="secondary">{command.action}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Zap className="h-8 w-8 mx-auto mb-2" />
|
||||
<p>{t("voice.noCustomCommands")}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="training" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Brain className="h-5 w-5" />
|
||||
{t("voice.voiceTraining")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("voice.voiceTrainingDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("voice.trainingPhrase")}</Label>
|
||||
<Input
|
||||
value={trainingPhrase}
|
||||
onChange={(e) => setTrainingPhrase(e.target.value)}
|
||||
placeholder={t("voice.enterTrainingPhrase")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleTraining}
|
||||
disabled={!trainingPhrase || isTraining}
|
||||
className="w-full"
|
||||
>
|
||||
{isTraining ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 mr-2 animate-spin" />
|
||||
{t("voice.training")}...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
{t("voice.startTraining")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
<h4 className="font-medium mb-2">{t("voice.trainingTips")}</h4>
|
||||
<ul className="text-sm text-muted-foreground space-y-1">
|
||||
<li>• {t("voice.trainingTip1")}</li>
|
||||
<li>• {t("voice.trainingTip2")}</li>
|
||||
<li>• {t("voice.trainingTip3")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5" />
|
||||
{t("voice.voiceSettings")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t("voice.confidenceThreshold")}: {(voiceSettings.confidenceThreshold * 100).toFixed(0)}%</Label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.1"
|
||||
max="1.0"
|
||||
step="0.1"
|
||||
value={voiceSettings.confidenceThreshold}
|
||||
onChange={(e) => handleSettingsUpdate('confidenceThreshold', parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("voice.speakingRate")}: {voiceSettings.speakingRate.toFixed(1)}x</Label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="2.0"
|
||||
step="0.1"
|
||||
value={voiceSettings.speakingRate}
|
||||
onChange={(e) => handleSettingsUpdate('speakingRate', parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("voice.voicePitch")}: {voiceSettings.voicePitch.toFixed(1)}</Label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="2.0"
|
||||
step="0.1"
|
||||
value={voiceSettings.voicePitch}
|
||||
onChange={(e) => handleSettingsUpdate('voicePitch', parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("voice.voiceVolume")}: {(voiceSettings.voiceVolume * 100).toFixed(0)}%</Label>
|
||||
<input
|
||||
type="range"
|
||||
min="0.0"
|
||||
max="1.0"
|
||||
step="0.1"
|
||||
value={voiceSettings.voiceVolume}
|
||||
onChange={(e) => handleSettingsUpdate('voiceVolume', parseFloat(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>{t("voice.autoSpeak")}</Label>
|
||||
<p className="text-sm text-muted-foreground">{t("voice.autoSpeakDescription")}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={voiceSettings.autoSpeak}
|
||||
onCheckedChange={(checked) => handleSettingsUpdate('autoSpeak', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label>{t("voice.continuous")}</Label>
|
||||
<p className="text-sm text-muted-foreground">{t("voice.continuousDescription")}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={voiceSettings.continuous}
|
||||
onCheckedChange={(checked) => handleSettingsUpdate('continuous', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>{t("voice.wakeWord")}</Label>
|
||||
<Input
|
||||
value={voiceSettings.wakeWord}
|
||||
onChange={(e) => handleSettingsUpdate('wakeWord', e.target.value)}
|
||||
placeholder={t("voice.enterWakeWord")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => speak(t("voice.testMessage"))}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<Volume2 className="h-4 w-4 mr-2" />
|
||||
{t("voice.testVoice")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="history" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
{t("voice.commandHistory")}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t("voice.commandHistoryDescription")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{voiceHistory.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{voiceHistory.slice(0, 10).map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div>
|
||||
<p className="font-medium">"{entry.transcription}"</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{entry.intent} • {new Date(entry.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={entry.confidence > 0.8 ? "default" : "secondary"}>
|
||||
{(entry.confidence * 100).toFixed(0)}%
|
||||
</Badge>
|
||||
{entry.confidence > 0.9 && <Star className="h-4 w-4 text-yellow-500" />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<MessageSquare className="h-8 w-8 mx-auto mb-2" />
|
||||
<p>{t("voice.noHistory")}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,7 +62,28 @@
|
||||
"noTasks": "لا توجد مهام",
|
||||
"taskCreated": "تم إنشاء المهمة بنجاح",
|
||||
"taskUpdated": "تم تحديث المهمة بنجاح",
|
||||
"taskDeleted": "تم حذف المهمة بنجاح"
|
||||
"taskDeleted": "تم حذف المهمة بنجاح",
|
||||
"projects": "المشاريع",
|
||||
"createProject": "إنشاء مشروع",
|
||||
"editProject": "تعديل المشروع",
|
||||
"projectDescription": "تنظيم المهام في مشاريع لإدارة أفضل",
|
||||
"projectName": "اسم المشروع",
|
||||
"enterProjectName": "أدخل اسم المشروع",
|
||||
"enterProjectDescription": "أدخل وصف المشروع",
|
||||
"selectStatus": "اختر الحالة",
|
||||
"active": "نشط",
|
||||
"onHold": "معلق",
|
||||
"cancelled": "ملغي",
|
||||
"deadline": "الموعد النهائي",
|
||||
"projectColor": "لون المشروع",
|
||||
"progress": "التقدم",
|
||||
"tasks": "مهام",
|
||||
"projectCreated": "تم إنشاء المشروع بنجاح",
|
||||
"projectUpdated": "تم تحديث المشروع بنجاح",
|
||||
"projectDeleted": "تم حذف المشروع بنجاح",
|
||||
"noProjects": "لا توجد مشاريع",
|
||||
"noProjectsDescription": "قم بإنشاء أول مشروع لتنظيم مهامك",
|
||||
"createFirstProject": "إنشاء أول مشروع"
|
||||
},
|
||||
"finances": {
|
||||
"title": "الماليات",
|
||||
|
||||
@@ -94,7 +94,30 @@
|
||||
"exportData": "Export Data",
|
||||
"billReminders": "Bill Reminders",
|
||||
"upcomingBills": "Upcoming Bills",
|
||||
"overdueBills": "Overdue Bills"
|
||||
"overdueBills": "Overdue Bills",
|
||||
"createBudget": "Create Budget",
|
||||
"editBudget": "Edit Budget",
|
||||
"budgetDescription": "Set spending limits and track your financial goals",
|
||||
"budgetName": "Budget Name",
|
||||
"enterBudgetName": "Enter budget name",
|
||||
"period": "Period",
|
||||
"selectPeriod": "Select period",
|
||||
"weekly": "Weekly",
|
||||
"monthly": "Monthly",
|
||||
"yearly": "Yearly",
|
||||
"selectCategory": "Select category",
|
||||
"alertThreshold": "Alert Threshold",
|
||||
"startDate": "Start Date",
|
||||
"endDate": "End Date",
|
||||
"activeBudget": "Active Budget",
|
||||
"activeBudgetDescription": "Enable budget tracking and alerts",
|
||||
"budgetCreated": "Budget created successfully",
|
||||
"budgetUpdated": "Budget updated successfully",
|
||||
"budgetDeleted": "Budget deleted successfully",
|
||||
"noBudgets": "No budgets found",
|
||||
"noBudgetsDescription": "Create your first budget to start tracking your spending",
|
||||
"createFirstBudget": "Create First Budget",
|
||||
"inactive": "Inactive"
|
||||
},
|
||||
"ai": {
|
||||
"title": "AI Assistant",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@/context/ThemeContext";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -7,9 +9,11 @@ import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { LanguageSwitcher } from "@/components/LanguageSwitcher";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import Sidebar from "@/components/layout/Sidebar";
|
||||
import { User, Bell, Shield, Palette } from "lucide-react";
|
||||
import { User, Bell, Shield, Palette, Moon, Sun, Globe, Download, Trash2 } from "lucide-react";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, updateProfile } = useAuth();
|
||||
|
||||
Reference in New Issue
Block a user