Skip to content

Commit d3c2f5a

Browse files
jwaldripclaude
andcommitted
feat(website): add interactive workflow visualizer
Add animated workflow visualizer showing hat transitions for all four AI-DLC workflows (Default, TDD, Adversarial, Hypothesis). Features: - Workflow selector tabs with smooth transitions - Animated hat nodes with active/completed states - Play/pause/step controls for animation playback - Hat detail cards showing responsibilities - Iteration loop visualization for each workflow - Operating mode badges (HITL/OHOTL/AHOTL) - Responsive design for mobile and desktop Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1b8b288 commit d3c2f5a

File tree

15 files changed

+1466
-0
lines changed

15 files changed

+1466
-0
lines changed

bun.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

website/app/components/Header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ThemeToggle } from "./ThemeToggle"
88
const navItems = [
99
{ href: "/", label: "Home" },
1010
{ href: "/big-picture/", label: "Big Picture" },
11+
{ href: "/workflows/", label: "Workflows" },
1112
{ href: "/docs/", label: "Docs" },
1213
{ href: "/tools/mode-selector/", label: "Mode Selector" },
1314
{ href: "/blog/", label: "Blog" },
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"use client"
2+
3+
import { motion, AnimatePresence } from "framer-motion"
4+
import type { Hat, WorkflowStep } from "./types"
5+
6+
interface HatDetailCardProps {
7+
hat: Hat | null
8+
step: WorkflowStep | null
9+
}
10+
11+
export function HatDetailCard({ hat, step }: HatDetailCardProps) {
12+
return (
13+
<AnimatePresence mode="wait">
14+
{hat && step ? (
15+
<motion.div
16+
key={hat.id}
17+
initial={{ opacity: 0, y: 20 }}
18+
animate={{ opacity: 1, y: 0 }}
19+
exit={{ opacity: 0, y: -20 }}
20+
transition={{ duration: 0.3 }}
21+
className={`
22+
p-6 rounded-2xl border-2
23+
${hat.color.bg} dark:${hat.color.bgDark}
24+
${hat.color.border} dark:${hat.color.borderDark}
25+
`}
26+
>
27+
<div className="flex items-start gap-4">
28+
{/* Emoji */}
29+
<div className="w-14 h-14 rounded-full bg-white dark:bg-gray-800 shadow-md flex items-center justify-center text-3xl flex-shrink-0">
30+
{hat.emoji}
31+
</div>
32+
33+
<div className="flex-1 min-w-0">
34+
{/* Header */}
35+
<div className="flex items-center gap-3 mb-2">
36+
<h3
37+
className={`text-xl font-bold ${hat.color.text} dark:${hat.color.textDark}`}
38+
>
39+
{hat.name}
40+
</h3>
41+
<span
42+
className={`
43+
text-xs px-2 py-0.5 rounded-full font-medium
44+
${hat.mode === "HITL" ? "bg-blue-200 text-blue-800 dark:bg-blue-900 dark:text-blue-200" : ""}
45+
${hat.mode === "OHOTL" ? "bg-green-200 text-green-800 dark:bg-green-900 dark:text-green-200" : ""}
46+
${hat.mode === "AHOTL" ? "bg-orange-200 text-orange-800 dark:bg-orange-900 dark:text-orange-200" : ""}
47+
`}
48+
>
49+
{hat.mode}
50+
</span>
51+
</div>
52+
53+
{/* Step description */}
54+
<p className="text-gray-700 dark:text-gray-300 mb-4">
55+
{step.description}
56+
</p>
57+
58+
{/* Responsibilities */}
59+
<div>
60+
<h4 className="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">
61+
Responsibilities
62+
</h4>
63+
<ul className="space-y-1">
64+
{hat.responsibilities.map((responsibility, index) => (
65+
<motion.li
66+
key={responsibility}
67+
initial={{ opacity: 0, x: -10 }}
68+
animate={{ opacity: 1, x: 0 }}
69+
transition={{ delay: index * 0.1 }}
70+
className="flex items-start gap-2 text-sm text-gray-600 dark:text-gray-400"
71+
>
72+
<svg
73+
className={`w-4 h-4 mt-0.5 flex-shrink-0 ${hat.color.text} dark:${hat.color.textDark}`}
74+
fill="none"
75+
viewBox="0 0 24 24"
76+
stroke="currentColor"
77+
>
78+
<path
79+
strokeLinecap="round"
80+
strokeLinejoin="round"
81+
strokeWidth={2}
82+
d="M9 12l2 2 4-4"
83+
/>
84+
</svg>
85+
{responsibility}
86+
</motion.li>
87+
))}
88+
</ul>
89+
</div>
90+
</div>
91+
</div>
92+
</motion.div>
93+
) : (
94+
<motion.div
95+
key="placeholder"
96+
initial={{ opacity: 0 }}
97+
animate={{ opacity: 1 }}
98+
exit={{ opacity: 0 }}
99+
className="p-6 rounded-2xl border-2 border-dashed border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/30"
100+
>
101+
<p className="text-center text-gray-500 dark:text-gray-400">
102+
Click on a hat to see its details, or press play to see the workflow
103+
in action.
104+
</p>
105+
</motion.div>
106+
)}
107+
</AnimatePresence>
108+
)
109+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"use client"
2+
3+
import { motion } from "framer-motion"
4+
import type { Hat } from "./types"
5+
6+
interface HatNodeProps {
7+
hat: Hat
8+
isActive: boolean
9+
isCompleted: boolean
10+
stepNumber: number
11+
onClick: () => void
12+
}
13+
14+
export function HatNode({
15+
hat,
16+
isActive,
17+
isCompleted,
18+
stepNumber,
19+
onClick,
20+
}: HatNodeProps) {
21+
return (
22+
<motion.button
23+
type="button"
24+
onClick={onClick}
25+
className={`
26+
relative flex flex-col items-center gap-2 p-4 rounded-2xl
27+
transition-colors duration-300 cursor-pointer
28+
focus:outline-none focus:ring-2 focus:ring-offset-2
29+
${hat.color.bg} dark:${hat.color.bgDark}
30+
${isActive ? `${hat.color.border} dark:${hat.color.borderDark} border-2` : "border border-transparent"}
31+
${isCompleted ? "opacity-60" : "opacity-100"}
32+
hover:scale-105
33+
`}
34+
animate={{
35+
scale: isActive ? 1.1 : 1,
36+
boxShadow: isActive
37+
? `0 0 24px 4px ${hat.color.glow}`
38+
: "0 0 0 0 transparent",
39+
}}
40+
transition={{
41+
scale: { type: "spring", stiffness: 300, damping: 20 },
42+
boxShadow: { duration: 0.3 },
43+
}}
44+
aria-label={`${hat.name} - ${hat.description}`}
45+
aria-pressed={isActive}
46+
>
47+
{/* Step number badge */}
48+
<div
49+
className={`
50+
absolute -top-2 -left-2 w-6 h-6 rounded-full
51+
flex items-center justify-center text-xs font-bold
52+
${isActive ? "bg-gray-900 text-white dark:bg-white dark:text-gray-900" : "bg-gray-300 text-gray-700 dark:bg-gray-700 dark:text-gray-300"}
53+
`}
54+
>
55+
{stepNumber + 1}
56+
</div>
57+
58+
{/* Emoji circle */}
59+
<motion.div
60+
className={`
61+
w-16 h-16 rounded-full flex items-center justify-center text-3xl
62+
bg-white dark:bg-gray-800 shadow-md
63+
${isActive ? "ring-2 ring-offset-2 " + hat.color.border : ""}
64+
`}
65+
animate={{
66+
rotate: isActive ? [0, -5, 5, -5, 0] : 0,
67+
}}
68+
transition={{
69+
duration: 0.5,
70+
repeat: isActive ? Number.POSITIVE_INFINITY : 0,
71+
repeatDelay: 2,
72+
}}
73+
>
74+
{hat.emoji}
75+
</motion.div>
76+
77+
{/* Name */}
78+
<span
79+
className={`font-semibold text-sm ${hat.color.text} dark:${hat.color.textDark}`}
80+
>
81+
{hat.name}
82+
</span>
83+
84+
{/* Mode badge */}
85+
<span
86+
className={`
87+
text-xs px-2 py-0.5 rounded-full font-medium
88+
${hat.mode === "HITL" ? "bg-blue-200 text-blue-800 dark:bg-blue-900 dark:text-blue-200" : ""}
89+
${hat.mode === "OHOTL" ? "bg-green-200 text-green-800 dark:bg-green-900 dark:text-green-200" : ""}
90+
${hat.mode === "AHOTL" ? "bg-orange-200 text-orange-800 dark:bg-orange-900 dark:text-orange-200" : ""}
91+
`}
92+
>
93+
{hat.mode}
94+
</span>
95+
96+
{/* Completed checkmark */}
97+
{isCompleted && (
98+
<motion.div
99+
initial={{ scale: 0 }}
100+
animate={{ scale: 1 }}
101+
className="absolute -top-1 -right-1 w-6 h-6 rounded-full bg-green-500 text-white flex items-center justify-center"
102+
>
103+
<svg
104+
className="w-4 h-4"
105+
fill="none"
106+
viewBox="0 0 24 24"
107+
stroke="currentColor"
108+
>
109+
<path
110+
strokeLinecap="round"
111+
strokeLinejoin="round"
112+
strokeWidth={3}
113+
d="M5 13l4 4L19 7"
114+
/>
115+
</svg>
116+
</motion.div>
117+
)}
118+
</motion.button>
119+
)
120+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"use client"
2+
3+
import { motion } from "framer-motion"
4+
import type { IterationLoop as IterationLoopType } from "./types"
5+
6+
interface IterationLoopProps {
7+
loop: IterationLoopType
8+
isActive: boolean
9+
stepCount: number
10+
}
11+
12+
export function IterationLoop({
13+
loop,
14+
isActive,
15+
stepCount,
16+
}: IterationLoopProps) {
17+
// Calculate the span of the loop (how many steps it covers)
18+
const loopSpan = loop.fromStep - loop.toStep
19+
20+
return (
21+
<div className="relative w-full mt-6">
22+
{/* The curved arrow going backwards */}
23+
<svg
24+
className="w-full h-16 overflow-visible"
25+
viewBox="0 0 400 60"
26+
preserveAspectRatio="xMidYMid meet"
27+
>
28+
<defs>
29+
<marker
30+
id="arrowhead"
31+
markerWidth="10"
32+
markerHeight="7"
33+
refX="9"
34+
refY="3.5"
35+
orient="auto"
36+
>
37+
<polygon
38+
points="0 0, 10 3.5, 0 7"
39+
fill="currentColor"
40+
className="text-gray-400 dark:text-gray-500"
41+
/>
42+
</marker>
43+
<marker
44+
id="arrowhead-active"
45+
markerWidth="10"
46+
markerHeight="7"
47+
refX="9"
48+
refY="3.5"
49+
orient="auto"
50+
>
51+
<polygon
52+
points="0 0, 10 3.5, 0 7"
53+
fill="currentColor"
54+
className="text-amber-500"
55+
/>
56+
</marker>
57+
</defs>
58+
59+
{/* Background path */}
60+
<motion.path
61+
d={`M ${80 + loop.fromStep * 80} 0
62+
C ${80 + loop.fromStep * 80} 50,
63+
${80 + loop.toStep * 80} 50,
64+
${80 + loop.toStep * 80} 0`}
65+
fill="none"
66+
stroke="currentColor"
67+
strokeWidth="2"
68+
strokeDasharray="6 4"
69+
className="text-gray-300 dark:text-gray-600"
70+
markerEnd="url(#arrowhead)"
71+
/>
72+
73+
{/* Animated path */}
74+
<motion.path
75+
d={`M ${80 + loop.fromStep * 80} 0
76+
C ${80 + loop.fromStep * 80} 50,
77+
${80 + loop.toStep * 80} 50,
78+
${80 + loop.toStep * 80} 0`}
79+
fill="none"
80+
stroke="currentColor"
81+
strokeWidth="2"
82+
strokeDasharray="6 4"
83+
className="text-amber-500"
84+
initial={{ pathLength: 0, opacity: 0 }}
85+
animate={{
86+
pathLength: isActive ? 1 : 0,
87+
opacity: isActive ? 1 : 0,
88+
}}
89+
transition={{ duration: 0.8, ease: "easeInOut" }}
90+
markerEnd="url(#arrowhead-active)"
91+
/>
92+
</svg>
93+
94+
{/* Label */}
95+
<motion.div
96+
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
97+
animate={{
98+
opacity: isActive ? 1 : 0.5,
99+
scale: isActive ? 1.1 : 1,
100+
}}
101+
transition={{ duration: 0.3 }}
102+
>
103+
<span className="px-3 py-1 text-xs font-medium rounded-full bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-200 whitespace-nowrap">
104+
{loop.label}
105+
</span>
106+
</motion.div>
107+
</div>
108+
)
109+
}

0 commit comments

Comments
 (0)