Spaces:
Sleeping
Sleeping
Download frontend/src/modules/rules/RuleStudioPanel.tsx from build-small-hackathon/tiny-trigger: direct link, hf CLI and curl.
- Browser
- Download file 8.14 kB
-
https://huggingface.co/spaces/build-small-hackathon/tiny-trigger/resolve/498b1c6598afd1f1fbdbf455287c016ad656d8ab/frontend/src/modules/rules/RuleStudioPanel.tsx
- Command line
-
hf download hf://spaces/build-small-hackathon/tiny-trigger@498b1c6598afd1f1fbdbf455287c016ad656d8ab/frontend/src/modules/rules/RuleStudioPanel.tsx
-
curl -L -o RuleStudioPanel.tsx https://huggingface.co/spaces/build-small-hackathon/tiny-trigger/resolve/498b1c6598afd1f1fbdbf455287c016ad656d8ab/frontend/src/modules/rules/RuleStudioPanel.tsx
8.14 kB
| import { useState } from "react" | |
| import { | |
| CheckCircle2, | |
| CircleSlash, | |
| Loader2, | |
| Save, | |
| ShieldCheck, | |
| Sparkles, | |
| Trash2, | |
| Wand2, | |
| } from "lucide-react" | |
| import { useDashboard } from "@/lib/dashboard" | |
| import { Button } from "@/components/ui/button" | |
| import { Textarea } from "@/components/ui/textarea" | |
| import { Badge } from "@/components/ui/badge" | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" | |
| import { Label } from "@/components/ui/label" | |
| import { Switch } from "@/components/ui/switch" | |
| import { cn } from "@/lib/utils" | |
| export function RuleStudioPanel() { | |
| const { | |
| rulesText, | |
| setRulesText, | |
| validate, | |
| validation, | |
| save, | |
| setRuleEnabled, | |
| deleteRule, | |
| compiler, | |
| compile, | |
| compiling, | |
| } = useDashboard() | |
| const [instruction, setInstruction] = useState( | |
| "When a cat appears in the image, notify me.", | |
| ) | |
| const [armedDelete, setArmedDelete] = useState<string | null>(null) | |
| return ( | |
| <Tabs defaultValue="compose"> | |
| <div className="flex items-center justify-between"> | |
| <TabsList> | |
| <TabsTrigger value="compose"> | |
| <Sparkles className="size-3.5" /> | |
| Compose | |
| </TabsTrigger> | |
| <TabsTrigger value="source">Source</TabsTrigger> | |
| <TabsTrigger value="rules"> | |
| Rules | |
| {validation?.ok && ( | |
| <span className="ml-1 font-mono text-[0.625rem] text-muted-foreground"> | |
| {validation.rules.length} | |
| </span> | |
| )} | |
| </TabsTrigger> | |
| </TabsList> | |
| {validation && | |
| (validation.ok ? ( | |
| <Badge variant="live"> | |
| <CheckCircle2 className="size-3" /> | |
| valid | |
| </Badge> | |
| ) : ( | |
| <Badge variant="danger"> | |
| <CircleSlash className="size-3" /> | |
| invalid | |
| </Badge> | |
| ))} | |
| </div> | |
| {/* Compose: NL → rules */} | |
| <TabsContent value="compose"> | |
| <div className="space-y-3"> | |
| <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> | |
| <p className="text-xs text-muted-foreground"> | |
| Describe the automation in plain language. It compiles to validated rules through{" "} | |
| {compiler.provider} — never executable code. | |
| </p> | |
| <Badge variant="default">{compiler.provider}</Badge> | |
| </div> | |
| <Textarea | |
| value={instruction} | |
| onChange={(e) => setInstruction(e.target.value)} | |
| className="min-h-24" | |
| placeholder="When a cat is near the feeder, dispense food once every 30 minutes…" | |
| /> | |
| <Button onClick={() => compile(instruction)} disabled={compiling || !instruction.trim()}> | |
| {compiling ? <Loader2 className="size-4 animate-spin" /> : <Wand2 className="size-4" />} | |
| {compiling | |
| ? "Compiling…" | |
| : `Compile with ${compiler.provider}`} | |
| </Button> | |
| </div> | |
| </TabsContent> | |
| {/* Source: raw editor */} | |
| <TabsContent value="source"> | |
| <div className="space-y-3"> | |
| <div className="relative"> | |
| <div className="absolute left-0 top-0 z-10 rounded-br-md rounded-tl-md border-b border-r border-border bg-black/40 px-2 py-0.5 font-mono text-[0.625rem] uppercase tracking-wider text-muted-foreground"> | |
| yaml · json | |
| </div> | |
| <Textarea | |
| value={rulesText} | |
| onChange={(e) => setRulesText(e.target.value)} | |
| spellCheck={false} | |
| className="min-h-64 bg-black/40 pt-7 font-mono text-xs leading-relaxed" | |
| /> | |
| </div> | |
| {validation && !validation.ok && validation.error && ( | |
| <pre className="max-h-28 overflow-auto rounded-md border border-destructive/30 bg-destructive/5 p-3 font-mono text-[0.6875rem] text-destructive/90 whitespace-pre-wrap"> | |
| {validation.error} | |
| </pre> | |
| )} | |
| <div className="flex gap-2"> | |
| <Button variant="secondary" onClick={validate}> | |
| <ShieldCheck className="size-4" /> | |
| Validate | |
| </Button> | |
| <Button variant="secondary" onClick={save}> | |
| <Save className="size-4" /> | |
| Save | |
| </Button> | |
| </div> | |
| </div> | |
| </TabsContent> | |
| {/* Rules: parsed view */} | |
| <TabsContent value="rules"> | |
| {validation?.ok && validation.rules.length > 0 ? ( | |
| <div className="space-y-2"> | |
| {validation.rules.map((r) => ( | |
| <div | |
| key={r.name} | |
| className="flex flex-col gap-3 rounded-lg border border-border bg-black/20 px-4 py-3 sm:flex-row sm:items-center sm:justify-between" | |
| > | |
| <div className="min-w-0"> | |
| <div className="flex items-center gap-2"> | |
| <span | |
| className={cn( | |
| "size-1.5 rounded-full", | |
| r.enabled ? "bg-primary" : "bg-muted-foreground/40", | |
| )} | |
| /> | |
| <span className="truncate font-mono text-sm text-foreground">{r.name}</span> | |
| {r.name.startsWith("example-") && <Badge variant="default">example</Badge>} | |
| <Badge variant="default">{r.trigger}</Badge> | |
| </div> | |
| <div className="mt-1 pl-3.5 text-xs text-muted-foreground"> | |
| {r.conditions} condition{r.conditions === 1 ? "" : "s"} →{" "} | |
| {r.actions.map((a) => `${a.type}:${a.name}`).join(", ")} | |
| </div> | |
| {r.labels.length > 0 && ( | |
| <div className="mt-2 flex flex-wrap gap-1 pl-3.5"> | |
| {r.labels.map((label) => ( | |
| <span | |
| key={label} | |
| className="rounded border border-border bg-black/30 px-1.5 py-0.5 font-mono text-[0.625rem] text-muted-foreground" | |
| > | |
| {label} | |
| </span> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex shrink-0 items-center gap-2"> | |
| <Label htmlFor={`enabled-${r.name}`} className="text-xs text-muted-foreground"> | |
| {r.enabled ? "enabled" : "disabled"} | |
| </Label> | |
| <Switch | |
| id={`enabled-${r.name}`} | |
| checked={r.enabled} | |
| onCheckedChange={(checked) => setRuleEnabled(r.name, checked)} | |
| /> | |
| <Button | |
| variant={armedDelete === r.name ? "destructive" : "secondary"} | |
| size="sm" | |
| onClick={() => { | |
| if (armedDelete === r.name) { | |
| void deleteRule(r.name) | |
| setArmedDelete(null) | |
| } else { | |
| setArmedDelete(r.name) | |
| } | |
| }} | |
| > | |
| <Trash2 className="size-4" /> | |
| {armedDelete === r.name ? "Confirm" : "Delete"} | |
| </Button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ) : validation?.ok ? ( | |
| <div className="flex flex-col items-center gap-2 py-8 text-center text-muted-foreground"> | |
| <ShieldCheck className="size-7 opacity-40" /> | |
| <p className="text-sm">No rules saved.</p> | |
| </div> | |
| ) : ( | |
| <div className="flex flex-col items-center gap-2 py-8 text-center text-muted-foreground"> | |
| <ShieldCheck className="size-7 opacity-40" /> | |
| <p className="text-sm">Validate the source to see parsed rules.</p> | |
| <Button variant="secondary" size="sm" onClick={validate}> | |
| Validate now | |
| </Button> | |
| </div> | |
| )} | |
| </TabsContent> | |
| </Tabs> | |
| ) | |
| } | |