diff --git a/client/src/components/ai/ConversationHistory.tsx b/client/src/components/ai/ConversationHistory.tsx new file mode 100644 index 0000000..58fe006 --- /dev/null +++ b/client/src/components/ai/ConversationHistory.tsx @@ -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(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 ( +
+ {[...Array(3)].map((_, i) => ( + + +
+
+
+ +
+
+
+ ))} +
+ ); + } + + return ( +
+
+

{t("ai.conversationHistory")}

+ +
+ + {/* Stats Cards */} + {stats && ( +
+ + + {t("ai.totalConversations")} + + + +
{stats.totalConversations}
+
+
+ + + + {t("ai.averageRating")} + + + +
{stats.averageRating?.toFixed(1) || "N/A"}
+
+
+ + + + {t("ai.averageResponseTime")} + + + +
{(stats.totalProcessingTime / stats.totalConversations)?.toFixed(0) || "N/A"}ms
+
+
+ + + + {t("ai.mostUsedModel")} + + + +
{stats.mostUsedModel || "N/A"}
+
+
+
+ )} + + {/* Filters */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ +
+ + {/* Conversations List */} +
+ {filteredConversations.length > 0 ? ( + filteredConversations.map((conversation: Conversation) => ( + + +
+
+ {getTypeIcon(conversation.type)} + {t(`ai.${conversation.type}`)} + {conversation.wasSpoken && ( + + + {t("ai.spoken")} + + )} + + {conversation.processingTime}ms + +
+
+ + {format(new Date(conversation.createdAt), "MMM dd, yyyy HH:mm")} + + +
+
+
+ +
+
+
+ +
+
+

{t("ai.userPrompt")}

+

{conversation.prompt}

+
+
+ +
+
+ +
+
+

{t("ai.aiResponse")}

+

{conversation.response}

+
+
+
+ +
+
+ + {t("ai.model")}: {conversation.modelUsed} + +
+ +
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+
+
+ )) + ) : ( + + + +

{t("ai.noConversations")}

+

+ {searchQuery || typeFilter !== "all" + ? t("ai.noMatchingConversations") + : t("ai.noConversationsDescription") + } +

+
+
+ )} +
+ + {/* Detailed View Dialog */} + setSelectedConversation(null)}> + + + {t("ai.conversationDetails")} + + {selectedConversation && format(new Date(selectedConversation.createdAt), "MMMM dd, yyyy 'at' HH:mm")} + + + {selectedConversation && ( +
+
+
+ {t("ai.type")}: + + {t(`ai.${selectedConversation.type}`)} + +
+
+ {t("ai.processingTime")}: + {selectedConversation.processingTime}ms +
+
+ {t("ai.model")}: + {selectedConversation.modelUsed} +
+
+ {t("ai.wasSpoken")}: + {selectedConversation.wasSpoken ? t("common.yes") : t("common.no")} +
+
+ + +
+
+

{t("ai.userPrompt")}

+

{selectedConversation.prompt}

+
+
+

{t("ai.aiResponse")}

+

{selectedConversation.response}

+
+
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/components/tasks/ProjectManager.tsx b/client/src/components/tasks/ProjectManager.tsx new file mode 100644 index 0000000..fc0ec78 --- /dev/null +++ b/client/src/components/tasks/ProjectManager.tsx @@ -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; + +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(null); + + const form = useForm({ + 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 ; + case "on_hold": + return ; + case "cancelled": + return ; + default: + return ; + } + }; + + if (isLoading) { + return ( +
+ {[...Array(3)].map((_, i) => ( + + +
+
+
+ +
+
+
+
+
+
+
+
+
+ ))} +
+ ); + } + + return ( +
+
+

{t("tasks.projects")}

+ + + + + + + + {editingProject ? t("tasks.editProject") : t("tasks.createProject")} + + + {t("tasks.projectDescription")} + + +
+ + ( + + {t("tasks.projectName")} + + + + + + )} + /> + + ( + + {t("tasks.description")} ({t("common.optional")}) + +