Add a democratic idea submission and AI synthesis platform

Implement a full-stack application with a React frontend and a Python Flask backend. The backend integrates with an AI agent to filter political ideas for democratic values and synthesize accepted ideas into a collective voice. Includes API endpoints for idea submission, retrieval, and synthesis, along with database persistence.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 31c5f770-9905-46af-a938-9d40ef3d4404
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/Xzzm5QH
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
pironantoine
2026-04-03 16:25:11 +00:00
parent 4d26b95657
commit f9c4073d21
92 changed files with 8199 additions and 23 deletions
+278
View File
@@ -0,0 +1,278 @@
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { format } from "date-fns";
import { useQueryClient } from "@tanstack/react-query";
import {
useSubmitIdea,
useListIdeas,
useGetIdeaStats,
useGetSynthesis,
getListIdeasQueryKey,
getGetIdeaStatsQueryKey
} from "@workspace/api-client-react";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Loader2, PenTool, CheckCircle2, XCircle, AlertCircle, TrendingUp, Users } from "lucide-react";
const submitIdeaSchema = z.object({
content: z.string()
.min(10, "Your idea must be at least 10 characters long to carry weight.")
.max(1000, "Brevity is the soul of wit. Keep it under 1000 characters."),
author: z.string().max(100).optional(),
});
type SubmitIdeaValues = z.infer<typeof submitIdeaSchema>;
export default function Home() {
const queryClient = useQueryClient();
const [submitResult, setSubmitResult] = React.useState<{ success: boolean; message: string; reason?: string } | null>(null);
const submitIdea = useSubmitIdea();
const { data: ideas, isLoading: isLoadingIdeas } = useListIdeas();
const { data: stats } = useGetIdeaStats();
const { data: synthesis, isLoading: isLoadingSynthesis } = useGetSynthesis({
query: {
refetchInterval: 15000,
}
});
const form = useForm<SubmitIdeaValues>({
resolver: zodResolver(submitIdeaSchema),
defaultValues: {
content: "",
author: "",
},
});
const onSubmit = (data: SubmitIdeaValues) => {
setSubmitResult(null);
submitIdea.mutate({ data }, {
onSuccess: (result) => {
if (result.accepted) {
setSubmitResult({
success: true,
message: "Your voice has been added to the collective manifesto."
});
form.reset();
} else {
setSubmitResult({
success: false,
message: "Your submission was rejected by the democratic filter.",
reason: result.reason
});
}
// Invalidate queries to refresh data
queryClient.invalidateQueries({ queryKey: getListIdeasQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetIdeaStatsQueryKey() });
},
onError: (error) => {
setSubmitResult({
success: false,
message: "An error occurred while submitting your idea. Please try again."
});
}
});
};
return (
<div className="flex-1 grid md:grid-cols-2 lg:grid-cols-[1fr_1.2fr] h-[calc(100vh-4rem)]">
{/* Left Column: Input & Feed */}
<div className="flex flex-col border-r border-border/40 bg-card">
<div className="p-6 md:p-8 flex flex-col gap-6 flex-shrink-0 border-b border-border/40">
<div className="space-y-2">
<h1 className="text-3xl font-serif font-bold text-primary tracking-tight">Speak to the Republic</h1>
<p className="text-muted-foreground font-mono text-sm uppercase tracking-wider">
Your voice matters. Submit your vision for the future.
</p>
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel className="sr-only">Your Idea</FormLabel>
<FormControl>
<Textarea
placeholder="What change do we need? Speak plainly."
className="min-h-[120px] resize-none font-serif text-lg bg-background border-primary/20 focus-visible:ring-primary placeholder:text-muted-foreground/50"
data-testid="input-idea-content"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-start gap-4">
<FormField
control={form.control}
name="author"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="sr-only">Pseudonym (Optional)</FormLabel>
<FormControl>
<Input
placeholder="Pseudonym (optional)"
className="bg-background border-primary/20 focus-visible:ring-primary font-mono text-sm"
data-testid="input-idea-author"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="font-bold tracking-wide flex-shrink-0"
disabled={submitIdea.isPending}
data-testid="button-submit-idea"
>
{submitIdea.isPending ? (
<><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Publishing...</>
) : (
<><PenTool className="mr-2 h-4 w-4" /> Proclaim</>
)}
</Button>
</div>
</form>
</Form>
{submitResult && (
<Alert variant={submitResult.success ? "default" : "destructive"} className="mt-2" data-testid={`alert-submit-${submitResult.success ? 'success' : 'error'}`}>
{submitResult.success ? <CheckCircle2 className="h-4 w-4" /> : <XCircle className="h-4 w-4" />}
<AlertTitle>{submitResult.success ? "Proclamation Accepted" : "Proclamation Rejected"}</AlertTitle>
<AlertDescription>
{submitResult.message}
{submitResult.reason && (
<div className="mt-2 text-sm italic border-l-2 pl-2 py-1 border-current opacity-90">
Reason: {submitResult.reason}
</div>
)}
</AlertDescription>
</Alert>
)}
</div>
{/* Feed of recent accepted ideas */}
<ScrollArea className="flex-1 bg-muted/30">
<div className="p-6 md:p-8">
<h2 className="text-xs font-mono font-bold uppercase tracking-widest text-muted-foreground mb-6 flex items-center gap-2">
<TrendingUp className="h-3 w-3" /> Recent Proclamations
</h2>
<div className="space-y-8">
{isLoadingIdeas ? (
<div className="flex justify-center p-8 text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : ideas && ideas.length > 0 ? (
ideas.slice(0, 20).map((idea) => (
<div key={idea.id} className="group relative pl-4 border-l border-border/60 hover:border-primary/50 transition-colors" data-testid={`card-idea-${idea.id}`}>
<p className="text-foreground/90 font-serif leading-relaxed">
{idea.content}
</p>
<div className="mt-2 flex items-center gap-3 text-xs font-mono text-muted-foreground">
<span className="font-semibold text-primary/70">{idea.author || "Anonymous Citizen"}</span>
<span>&bull;</span>
<span>{format(new Date(idea.createdAt), "MMM d, h:mm a")}</span>
</div>
</div>
))
) : (
<div className="text-center p-8 text-muted-foreground font-mono text-sm border border-dashed border-border/60">
No proclamations recorded yet. Be the first to speak.
</div>
)}
</div>
</div>
</ScrollArea>
</div>
{/* Right Column: Synthesis */}
<div className="flex flex-col bg-[#F9F7F1] relative overflow-hidden">
{/* Decorative noise/texture overlay could go here */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none mix-blend-multiply"
style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noiseFilter%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noiseFilter)%22/%3E%3C/svg%3E")' }}></div>
<div className="p-6 md:p-12 flex-1 flex flex-col relative z-10 overflow-y-auto">
<div className="flex justify-between items-start mb-12">
<div>
<h2 className="text-sm font-mono font-bold uppercase tracking-widest text-primary flex items-center gap-2">
<Users className="h-4 w-4" /> La Voix du Peuple
</h2>
<p className="text-xs font-mono text-muted-foreground mt-1">
The living synthesis of democratic thought
</p>
</div>
{stats && (
<div className="flex gap-4 text-xs font-mono text-right" data-testid="text-stats">
<div className="flex flex-col">
<span className="text-muted-foreground">Voices Joined</span>
<span className="font-bold text-primary">{stats.accepted}</span>
</div>
<div className="flex flex-col">
<span className="text-muted-foreground">Voices Rejected</span>
<span className="font-bold text-destructive">{stats.rejected}</span>
</div>
</div>
)}
</div>
<div className="flex-1 flex flex-col justify-center max-w-2xl mx-auto w-full">
{isLoadingSynthesis ? (
<div className="flex flex-col items-center justify-center gap-4 text-muted-foreground">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="font-mono text-sm uppercase tracking-widest">Listening to the people...</span>
</div>
) : synthesis ? (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-1000 ease-out space-y-8">
<div
className="prose prose-lg prose-slate max-w-none text-2xl md:text-3xl lg:text-4xl leading-tight font-serif text-foreground"
data-testid="text-synthesis-content"
>
{synthesis.text ? (
<div dangerouslySetExpandedAutocorrect={undefined}>{synthesis.text}</div>
) : (
<p className="text-muted-foreground italic text-center text-xl">The pages of our manifesto remain empty. Speak, and it shall be written.</p>
)}
</div>
{synthesis.updatedAt && (
<div className="pt-8 border-t border-border/40 mt-12 flex justify-between items-center text-xs font-mono text-muted-foreground" data-testid="text-synthesis-meta">
<span>Synthesized from {synthesis.ideaCount} democratic ideas</span>
<span className="flex items-center gap-2">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</span>
Live &bull; {format(new Date(synthesis.updatedAt), "HH:mm:ss")}
</span>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4 text-muted-foreground">
<AlertCircle className="h-8 w-8 text-destructive" />
<span className="font-mono text-sm uppercase tracking-widest">Failed to retrieve the manifesto</span>
</div>
)}
</div>
</div>
</div>
</div>
);
}