"use client" import type React from "react" import { useState, useRef, useEffect } from "react" import { ArrowUpDown, Filter, Search, ThumbsDown, ThumbsUp, SortAsc, SortDesc, CheckCircle, X, Clock, Check, Flag, AlertTriangle, FileText, Loader2, } from "lucide-react" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Progress } from "@/components/ui/progress" import { Switch } from "@/components/ui/switch" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { useWalletStore } from "@/lib/wallet-store" import { cn } from "@/lib/utils" import { toast } from "@/components/ui/use-toast" import { ToastAction } from "@/components/ui/toast" import { ethers } from "ethers" import { PERSPECTIVE_CONTRACT_ABI, PERSPECTIVE_CONTRACT_ADDRESS } from "@/lib/constants" // Define the insight type type Insight = { id: number; title: string; description: string; category: string; votes: { yes: number; no: number }; status: "voting" | "consensus" | "rejected"; dateAdded: string; isVerified: boolean; moderationStatus: "flagged" | "approved" | "rejected" | null; flags: string[]; aiFlags: { reason: string; confidence: number } | null; }; // Mock data for insights const mockInsights: Insight[] = [ { id: 1, title: "Increase Park Funding", description: "Allocate 15% more funding to local parks for maintenance and new facilities", category: "Environmental Policy", votes: { yes: 245, no: 32 }, status: "voting", // voting, consensus, rejected dateAdded: "2024-02-15", isVerified: true, moderationStatus: null, // null, 'flagged', 'approved', 'rejected' flags: [], // list of flag reasons by users aiFlags: null, // null or { reason: string, confidence: number } }, { id: 2, title: "Public Transportation Expansion", description: "Extend bus routes to underserved neighborhoods and increase service frequency", category: "Infrastructure", votes: { yes: 189, no: 45 }, status: "voting", dateAdded: "2024-02-10", isVerified: true, moderationStatus: null, flags: [], aiFlags: null, }, { id: 3, title: "After-School Programs", description: "Create free after-school programs for K-8 students in public schools", category: "Education", votes: { yes: 312, no: 28 }, status: "consensus", dateAdded: "2024-01-28", isVerified: true, moderationStatus: null, flags: [], aiFlags: null, }, { id: 4, title: "Community Health Clinics", description: "Establish walk-in clinics in underserved areas with sliding scale fees", category: "Healthcare", votes: { yes: 278, no: 42 }, status: "consensus", dateAdded: "2024-01-22", isVerified: true, moderationStatus: null, flags: [], aiFlags: null, }, { id: 5, title: "Affordable Housing Initiative", description: "Require 20% affordable units in new residential developments over 50 units", category: "Housing", votes: { yes: 156, no: 98 }, status: "voting", dateAdded: "2024-01-15", isVerified: false, moderationStatus: null, flags: [], aiFlags: null, }, { id: 6, title: "Bike Lane Network", description: "Create a connected network of protected bike lanes throughout the city", category: "Infrastructure", votes: { yes: 203, no: 87 }, status: "voting", dateAdded: "2024-01-05", isVerified: true, moderationStatus: null, flags: [], aiFlags: null, }, { id: 7, title: "Universal Basic Income Pilot", description: "Launch a 12-month UBI pilot program for 500 residents", category: "Economic Policy", votes: { yes: 102, no: 178 }, status: "voting", dateAdded: "2024-02-01", isVerified: false, moderationStatus: "flagged", flags: ["Inappropriate", "Other"], aiFlags: null, }, { id: 8, title: "City Wi-Fi Network Expansion", description: "Free public Wi-Fi in all public spaces and municipal buildings", category: "Technology", votes: { yes: 187, no: 23 }, status: "voting", dateAdded: "2024-01-12", isVerified: false, moderationStatus: null, flags: [], aiFlags: { reason: "Repetitive text patterns", confidence: 0.87 }, }, { id: 9, title: "Green Roof Initiative", description: "Require all new commercial buildings to include green roof elements", category: "Environmental Policy", votes: { yes: 221, no: 67 }, status: "voting", dateAdded: "2024-02-03", isVerified: false, moderationStatus: "flagged", flags: ["Spam"], aiFlags: { reason: "Generic content", confidence: 0.76 }, } ] export default function InsightsDashboard() { const [insights, setInsights] = useState(mockInsights) const [activeTab, setActiveTab] = useState("all") const [searchQuery, setSearchQuery] = useState("") const [categoryFilter, setCategoryFilter] = useState("all") const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc") const [sortBy, setSortBy] = useState<"votes" | "date">("votes") const { walletConnected, isVerified } = useWalletStore() const [userVotes, setUserVotes] = useState>({}) const [pendingToast, setPendingToast] = useState<{ type: "single" | "batch"; voteType?: "yes" | "no"; insightId?: number; batchSize?: number } | null>(null) const lastToastRef = useRef<{ type: string, id?: number, time: number }>({ type: "", time: 0 }) // Batch voting state const [batchMode, setBatchMode] = useState(false) const [selectedInsights, setSelectedInsights] = useState>({}) const [isSubmittingBatch, setIsSubmittingBatch] = useState(false) const [countdownSeconds, setCountdownSeconds] = useState(5) const countdownRef = useRef(null) // Moderation and visibility state const [showUnverified, setShowUnverified] = useState(false) const [showModQueue, setShowModQueue] = useState(false) const [flaggingInsight, setFlaggingInsight] = useState(null) const [flagReason, setFlagReason] = useState<"Spam" | "Inappropriate" | "Other" | null>(null) const [flagDetails, setFlagDetails] = useState("") const [isFlagging, setIsFlagging] = useState(false) const [submittingModerationAction, setSubmittingModerationAction] = useState(false) const filteredInsights = insights.filter((insight) => { // Filter by tab if (activeTab === "consensus" && insight.status !== "consensus") return false if (activeTab === "voting" && insight.status !== "voting") return false if (activeTab === "moderation" && insight.moderationStatus !== "flagged") return false // Filter by unverified visibility if (!showUnverified && !insight.isVerified) return false // Filter by search query if ( searchQuery && !insight.title.toLowerCase().includes(searchQuery.toLowerCase()) && !insight.description.toLowerCase().includes(searchQuery.toLowerCase()) ) return false // Filter by category if (categoryFilter !== "all" && insight.category !== categoryFilter) return false return true }) const sortInsights = (insights: typeof filteredInsights) => { return [...insights].sort((a, b) => { if (sortBy === "votes") { const totalVotesA = a.votes.yes + a.votes.no const totalVotesB = b.votes.yes + b.votes.no return sortOrder === "desc" ? totalVotesB - totalVotesA : totalVotesA - totalVotesB } else { // Assuming each insight has a dateAdded property const dateA = new Date(a.dateAdded || "2023-01-01").getTime() const dateB = new Date(b.dateAdded || "2023-01-01").getTime() return sortOrder === "desc" ? dateB - dateA : dateA - dateB } }) } const sortedInsights = sortInsights(filteredInsights) // Count selected insights const selectedCount = Object.keys(selectedInsights).length // Handle individual vote const handleVote = (id: number, voteType: "yes" | "no") => { // Record the user's vote setUserVotes((prev) => ({ ...prev, [id]: voteType, })) // Update the insight's vote count setInsights( insights.map((insight) => { if (insight.id === id) { const updatedVotes = { ...insight.votes, [voteType]: insight.votes[voteType] + 1, } // Check if consensus is reached (75% yes votes) const totalVotes = updatedVotes.yes + updatedVotes.no const status = updatedVotes.yes / totalVotes > 0.75 ? "consensus" : "voting" return { ...insight, votes: updatedVotes, status } } return insight }), ) // Queue toast for individual votes (not batch mode) if (!batchMode) { setPendingToast({ type: "single", voteType, insightId: id }) } } // Toggle batch mode const toggleBatchMode = () => { if (batchMode) { // Clear selections when exiting batch mode setSelectedInsights({}) } setBatchMode(!batchMode) } // Toggle selection of an insight in batch mode const toggleSelection = (id: number, voteType: "yes" | "no") => { // Find the insight const insight = insights.find((i) => i.id === id) // Don't allow selection if insight has reached consensus if (insight?.status === "consensus") return setSelectedInsights((prev) => { const newSelections = { ...prev } // If already selected with this vote type, remove it if (newSelections[id] === voteType) { delete newSelections[id] } else { // Otherwise add/update it newSelections[id] = voteType } return newSelections }) } // Start batch submission with countdown const startBatchSubmission = () => { if (selectedCount === 0) return setIsSubmittingBatch(true) setCountdownSeconds(5) // Start countdown countdownRef.current = setInterval(() => { setCountdownSeconds((prev) => { if (prev <= 1) { // Clear interval when countdown reaches 0 if (countdownRef.current) clearInterval(countdownRef.current) // Process the batch processBatchVotes() return 0 } return prev - 1 }) }, 1000) } // Cancel batch submission const cancelBatchSubmission = () => { if (countdownRef.current) { clearInterval(countdownRef.current) } setIsSubmittingBatch(false) } // Process all votes in the batch const processBatchVotes = () => { const batchSize = Object.keys(selectedInsights).length if (batchSize === 0) return // Apply all votes Object.entries(selectedInsights).forEach(([idStr, voteType]) => { const id = Number.parseInt(idStr) handleVote(id, voteType) }) // Queue toast for batch submission setPendingToast({ type: "batch", batchSize }) // Reset batch state setSelectedInsights({}) setIsSubmittingBatch(false) } // Handle toast notifications useEffect(() => { if (pendingToast) { // Check for duplicate toast prevention (only show if different from last toast or more than 2 seconds passed) const now = Date.now() const isDuplicate = pendingToast.type === lastToastRef.current.type && (pendingToast.type === "single" ? pendingToast.insightId === lastToastRef.current.id : true) && now - lastToastRef.current.time < 2000; if (!isDuplicate) { if (pendingToast.type === "single") { const insight = insights.find(i => i.id === pendingToast.insightId) toast({ title: "Vote submitted", description: `You voted ${pendingToast.voteType} on "${insight?.title}"`, action: OK, }) // Update last toast reference lastToastRef.current = { type: "single", id: pendingToast.insightId, time: now } } else if (pendingToast.type === "batch") { toast({ title: "Batch votes submitted", description: `Successfully submitted ${pendingToast.batchSize} votes`, action: OK, }) // Update last toast reference lastToastRef.current = { type: "batch", time: now } } } setPendingToast(null) } }, [pendingToast, insights]) // Enhanced HoldButton component with improved animation and feedback function HoldButton({ children, onComplete, disabled = false, variant = "default", className = "", holdTime = 3000, // 3 seconds insightId, voteType, }: { children: React.ReactNode onComplete: () => void disabled?: boolean variant?: "default" | "outline" className?: string holdTime?: number insightId: number voteType: "yes" | "no" }) { const [isHolding, setIsHolding] = useState(false) const [progress, setProgress] = useState(0) const [completed, setCompleted] = useState(false) const [localVoted, setLocalVoted] = useState(false) // Track local vote state const timerRef = useRef(null) const startTimeRef = useRef(0) const animationRef = useRef(null) // Check if this insight has already been voted on const hasVoted = userVotes[insightId] !== undefined || localVoted const isThisVote = userVotes[insightId] === voteType // Reset completed state when the insight changes useEffect(() => { setCompleted(isThisVote) }, [isThisVote]) const startHold = () => { if (disabled || hasVoted) return setIsHolding(true) setCompleted(false) startTimeRef.current = Date.now() // Use requestAnimationFrame for smoother animation const animate = () => { const elapsed = Date.now() - startTimeRef.current const newProgress = Math.min((elapsed / holdTime) * 100, 100) setProgress(newProgress) if (newProgress >= 100) { setCompleted(true) // Add a small delay before triggering the action for visual feedback timerRef.current = setTimeout(() => { setLocalVoted(true) // Set local vote state immediately onComplete() setIsHolding(false) setProgress(0) }, 300) } else { animationRef.current = requestAnimationFrame(animate) } } animationRef.current = requestAnimationFrame(animate) } const endHold = () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current) animationRef.current = null } if (timerRef.current) { clearTimeout(timerRef.current) timerRef.current = null } if (!completed) { setIsHolding(false) setProgress(0) } } useEffect(() => { return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current) } if (timerRef.current) { clearTimeout(timerRef.current) } } }, []) // Get the base button color based on variant const baseColor = variant === "default" ? "bg-primary text-primary-foreground" : "bg-background text-foreground border border-input" // Get the success color const successColor = "bg-green-500 text-white" return ( ) } // Batch selection button component function BatchSelectionButton({ children, insightId, voteType, className = "", variant = "default", disabled = false, }: { children: React.ReactNode insightId: number voteType: "yes" | "no" className?: string variant?: "default" | "outline" disabled?: boolean }) { // Check if this insight is selected with this vote type const isSelected = selectedInsights[insightId] === voteType // Check if this insight has already been voted on const hasVoted = userVotes[insightId] !== undefined // Base styles const baseColor = variant === "default" ? "bg-primary text-primary-foreground" : "bg-background text-foreground border border-input" const selectedColor = voteType === "yes" ? "bg-green-500 text-white" : "bg-red-500 text-white" return ( ) } // Handle flagging an insight const handleFlagInsight = async () => { if (!flaggingInsight || !flagReason) return; setIsFlagging(true); try { const insight = insights.find(i => i.id === flaggingInsight); if (!insight) throw new Error("Insight not found"); // Call smart contract const provider = new ethers.providers.Web3Provider( window.ethereum as ethers.providers.ExternalProvider ); const signer = provider.getSigner(); const contract = new ethers.Contract( PERSPECTIVE_CONTRACT_ADDRESS, PERSPECTIVE_CONTRACT_ABI, signer ); // Submit flag to blockchain const tx = await contract.flagInsight( flaggingInsight, flagReason, flagDetails || "", { gasLimit: 500000 } ); // Wait for transaction await tx.wait(); // Update local state setInsights(insights.map(insight => { if (insight.id === flaggingInsight) { return { ...insight, moderationStatus: "flagged", flags: [...insight.flags, flagReason] }; } return insight; })); // Show toast toast({ title: "Flag submitted", description: `You flagged "${insight.title}" as ${flagReason}`, action: OK, }); // Reset state setFlaggingInsight(null); setFlagReason(null); setFlagDetails(""); } catch (error) { console.error("Error flagging insight:", error); toast({ title: "Error", description: "Failed to submit flag. Please try again.", variant: "destructive", }); } finally { setIsFlagging(false); } }; // Handle moderation decision const handleModerateInsight = async (insightId: number, approved: boolean) => { setSubmittingModerationAction(true); try { // Call smart contract const provider = new ethers.providers.Web3Provider( window.ethereum as ethers.providers.ExternalProvider ); const signer = provider.getSigner(); const contract = new ethers.Contract( PERSPECTIVE_CONTRACT_ADDRESS, PERSPECTIVE_CONTRACT_ABI, signer ); // Submit moderation decision to blockchain const tx = await contract.moderateInsight( insightId, approved, { gasLimit: 300000 } ); // Wait for transaction await tx.wait(); // Update local state setInsights(insights.map(insight => { if (insight.id === insightId) { return { ...insight, moderationStatus: approved ? "approved" : "rejected" }; } return insight; })); // Show toast const insight = insights.find(i => i.id === insightId); toast({ title: approved ? "Insight approved" : "Insight rejected", description: `You ${approved ? "approved" : "rejected"} "${insight?.title}"`, action: OK, }); } catch (error) { console.error("Error moderating insight:", error); toast({ title: "Error", description: "Failed to submit moderation decision. Please try again.", variant: "destructive", }); } finally { setSubmittingModerationAction(false); } }; return (

Insights Dashboard

Review and vote on AI-generated insights derived from community perspectives.

{/* Batch Mode Toggle */} {walletConnected && (
{batchMode ? "Select multiple insights to vote on at once" : "Vote on insights individually"}
{batchMode && selectedCount > 0 && (
{selectedCount} selected {isSubmittingBatch ? (
Submitting in {countdownSeconds}s
) : ( )}
)}
)} {/* Filters and Search */}
setSearchQuery(e.target.value)} />
Sort by setSortBy("votes")}>Votes {sortBy === "votes" && "✓"} setSortBy("date")}>Date {sortBy === "date" && "✓"} setSortOrder(sortOrder === "desc" ? "asc" : "desc")}> {sortOrder === "desc" ? "Ascending" : "Descending"}
{/* Tabs */}
All Insights Voting Open Consensus {isVerified && ( Moderation {insights.filter(i => i.moderationStatus === "flagged").length > 0 && ( {insights.filter(i => i.moderationStatus === "flagged").length} )} )}
{!walletConnected && (

Connect your wallet to vote on insights

Your vote helps shape community consensus and policy decisions

)} {/* Batch submission overlay */} {isSubmittingBatch && (

Submitting Votes

Your votes will be submitted in {countdownSeconds} seconds. You can cancel if you need to make changes.

{Object.entries(selectedInsights).map(([idStr, voteType]) => { const id = Number.parseInt(idStr) const insight = insights.find((i) => i.id === id) if (!insight) return null return (
{insight.title} {voteType === "yes" ? "Yes" : "No"}
) })}
)} {batchMode && (

Note: Insights that have already reached consensus cannot be selected for batch voting.

)} {/* Insights Grid */}
{activeTab === "moderation" ? ( // Moderation queue view sortedInsights.map((insight) => (
{insight.title} {!insight.isVerified && ( Unverified )} {insight.category}

{insight.description}

{/* Flags from users */} {insight.flags.length > 0 && (

User Flags:

{insight.flags.map((flag, index) => ( {flag} ))}
)} {/* AI Flags */} {insight.aiFlags && (

AI Flagged:

{insight.aiFlags.confidence >= 0.8 ? "High" : "Medium"} Confidence

Reason: {insight.aiFlags.reason}

)}
)) ) : ( // Regular insights view sortedInsights.map((insight) => ( {/* Verification and moderation status badges */}
{!insight.isVerified && ( Unverified )} {insight.aiFlags && ( AI Flagged )}
{insight.title} {insight.category}
{insight.status === "consensus" ? "Consensus" : "Voting"}

{insight.description}

{/* AI Flag details */} {insight.aiFlags && (

AI Flag: {insight.aiFlags.reason}

)}
Yes: {insight.votes.yes} No: {insight.votes.no}
{/* Flag button for verified users */} {isVerified && !insight.isVerified && (
)}
{batchMode ? ( // Batch mode voting buttons <> No Yes ) : ( // Individual voting buttons <> handleVote(insight.id, "no")} disabled={insight.status === "consensus"} insightId={insight.id} voteType="no" > No handleVote(insight.id, "yes")} disabled={insight.status === "consensus"} insightId={insight.id} voteType="yes" > Yes )}
)) )}
{filteredInsights.length === 0 && (

No insights match your filters

Try adjusting your search or filters

)} {/* Floating batch submission button for mobile */} {batchMode && selectedCount > 0 && !isSubmittingBatch && (
)} {/* Flagging Dialog */} !open && setFlaggingInsight(null)}> Flag Insight Report this insight for review by community moderators.
{flagReason === "Other" && (