reasoning-steps.tsx
styles.css
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
"use client";
2
3
import React, { useState, useEffect } from "react";
4
import { motion, AnimatePresence } from "framer-motion";
5
import { ChevronDown, Users, Disc, Loader2 } from "lucide-react";
6
import { cn } from "@/lib/utils";
7
8
export interface ReasoningStepsProps {
9
className?: string;
10
loop?: boolean;
11
}
12
13
const NineDots = ({ className }: { className?: string }) => (
14
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" className={className}>
15
<circle cx="2.5" cy="2.5" r="1.5" />
16
<circle cx="7" cy="2.5" r="1.5" />
17
<circle cx="11.5" cy="2.5" r="1.5" />
18
<circle cx="2.5" cy="7" r="1.5" />
19
<circle cx="7" cy="7" r="1.5" />
20
<circle cx="11.5" cy="7" r="1.5" />
21
<circle cx="2.5" cy="11.5" r="1.5" />
22
<circle cx="7" cy="11.5" r="1.5" />
23
<circle cx="11.5" cy="11.5" r="1.5" />
24
</svg>
25
);
26
27
type RenderItem =
28
| { type: 'title', id: string, prefix: string, suffix: string, delay: number }
29
| { type: 'pills', id: string, pills: { icon: React.ElementType, label: string, color: string, bg: string }[], delay: number }
30
| { type: 'thought', id: string, text: string, delay: number };
31
32
const DEMO_ITEMS: RenderItem[] = [
33
{ type: 'title', id: 't1', prefix: 'Reviewing', suffix: 'the incident call', delay: 300 },
34
{
35
type: 'pills', id: 'p1', delay: 400, pills: [
36
{ icon: Users, label: "Mar 4 incident review", color: "text-blue-500 dark:text-blue-400", bg: "bg-zinc-100 dark:bg-gradient-to-b dark:from-[#2C2C2E] dark:to-[#1C1C1E] border border-zinc-200/80 dark:border-white/10 dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]" },
37
{ icon: Disc, label: "ENG-2841 rollout policy", color: "text-indigo-500 dark:text-indigo-400", bg: "bg-zinc-100 dark:bg-gradient-to-b dark:from-[#2C2C2E] dark:to-[#1C1C1E] border border-zinc-200/80 dark:border-white/10 dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]" }
38
]
39
},
40
{ type: 'thought', id: 'th1', text: 'anyone thought to revert.', delay: 400 },
41
{ type: 'title', id: 't2', prefix: 'Drafting', suffix: 'the rollout policy', delay: 400 },
42
{ type: 'thought', id: 'th2', text: 'Reading the Mar 4 incident review end to end and marking the passages where the rollback comes up.', delay: 600 },
43
{ type: 'thought', id: 'th3', text: 'Checking the existing deploy policy for a threshold to inherit rather than inventing one here.', delay: 600 },
44
{ type: 'thought', id: 'th4', text: 'It holds anything above 25% of traffic for a second reviewer, so the fast path needs a ceiling too.', delay: 800 },
45
];
46
47
export function ReasoningSteps({
48
className,
49
loop = true
50
}: ReasoningStepsProps) {
51
const [visibleIndex, setVisibleIndex] = useState(-1);
52
const [isCollapsed, setIsCollapsed] = useState(false);
53
54
useEffect(() => {
55
let isMounted = true;
56
let currentIdx = -1;
57
let timeoutId: NodeJS.Timeout;
58
59
const nextStep = () => {
60
if (!isMounted) return;
61
currentIdx++;
62
if (currentIdx < DEMO_ITEMS.length) {
63
setVisibleIndex(currentIdx);
64
timeoutId = setTimeout(nextStep, DEMO_ITEMS[currentIdx].delay);
65
} else {
66
timeoutId = setTimeout(() => {
67
if (!isMounted) return;
68
setIsCollapsed(true);
69
70
if (loop) {
71
timeoutId = setTimeout(() => {
72
if (!isMounted) return;
73
setIsCollapsed(false);
74
setVisibleIndex(-1);
75
currentIdx = -1;
76
timeoutId = setTimeout(nextStep, 300);
77
}, 2000);
78
}
79
}, 800);
80
}
81
};
82
83
timeoutId = setTimeout(nextStep, 500);
84
85
return () => {
86
isMounted = false;
87
clearTimeout(timeoutId);
88
};
89
}, [loop]);
90
91
return (
92
<div className={cn("flex flex-col items-center justify-center min-h-[300px] w-full font-matter", className)}>
93
<div className="w-full max-w-[540px] flex flex-col items-start px-4">
94
<AnimatePresence mode="wait">
95
{isCollapsed ? (
96
<motion.div
97
key="collapsed"
98
initial={{ opacity: 0, scale: 0.95, y: 10, filter: "blur(4px)" }}
99
animate={{ opacity: 1, scale: 1, y: 0, filter: "blur(0px)" }}
100
exit={{ opacity: 0, scale: 0.95, y: -10, filter: "blur(4px)" }}
101
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
102
className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[13px] font-matter font-medium bg-black/5 dark:bg-gradient-to-b dark:from-[#2C2C2E] dark:to-[#1C1C1E] border border-black/10 dark:border-white/10 text-zinc-700 dark:text-[#F5F5F7] shadow-sm hover:border-black/20 dark:hover:border-white/20 hover:scale-[1.02] active:scale-[0.98] transition-all cursor-pointer"
103
>
104
Thought for 7s <ChevronDown className="w-3.5 h-3.5 ml-0.5 opacity-70" />
105
</motion.div>
106
) : (
107
<motion.div
108
key="expanded"
109
initial={{ opacity: 0 }}
110
animate={{ opacity: 1 }}
111
exit={{ opacity: 0, filter: "blur(4px)", scale: 0.95 }}
112
transition={{ duration: 0.5 }}
113
className="relative w-full h-[150px]"
114
>
115
{/* Mask container to fade out items as they scroll up */}
116
<div
117
className="absolute inset-0 overflow-hidden"
118
style={{
119
maskImage: 'linear-gradient(to bottom, transparent, black 15%, black 100%)',
120
WebkitMaskImage: 'linear-gradient(to bottom, transparent, black 15%, black 100%)'
121
}}
122
>
123
{/* Stack anchored at the bottom so adding items pushes older ones up */}
124
<div className="absolute bottom-0 left-0 w-full flex flex-col gap-4 pb-4 px-4">
125
<AnimatePresence initial={false}>
126
{DEMO_ITEMS.slice(0, visibleIndex + 1).map((item, i, arr) => (
127
<motion.div
128
key={item.id}
129
layout
130
initial={{ opacity: 0, y: 20, filter: "blur(4px)" }}
131
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
132
transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
133
className="w-full"
134
>
135
{item.type === 'title' && (
136
<div className="flex items-start text-[15px] font-medium tracking-tight">
137
{/* If it's the most recently added item (and is a title), show spinner, else NineDots */}
138
{i === arr.length - 1 && visibleIndex < DEMO_ITEMS.length - 1 ? (
139
<Loader2 className="w-4 h-4 animate-spin text-zinc-400 dark:text-zinc-500 mr-3 mt-[2px] flex-shrink-0" />
140
) : (
141
<NineDots className="text-zinc-400 dark:text-zinc-500 mr-3 mt-[3px] flex-shrink-0" />
142
)}
143
<div>
144
<span className="text-zinc-900 dark:text-zinc-100">{item.prefix}</span>
145
<span className="text-zinc-500 dark:text-zinc-400 ml-1.5">{item.suffix}</span>
146
</div>
147
</div>
148
)}
149
150
{item.type === 'pills' && (
151
<div className="flex items-center gap-2 flex-wrap pl-7">
152
{item.pills.map((p, idx) => (
153
<div key={idx} className={cn("flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[12.5px] font-medium border", p.bg)}>
154
<p.icon className={cn("w-3.5 h-3.5", p.color)} />
155
<span className="text-zinc-700 dark:text-zinc-300">{p.label}</span>
156
</div>
157
))}
158
</div>
159
)}
160
161
{item.type === 'thought' && (
162
<div className="pl-7 pr-4 text-[14px] text-zinc-600 dark:text-zinc-400/90 leading-[1.6]">
163
{item.text}
164
</div>
165
)}
166
</motion.div>
167
))}
168
</AnimatePresence>
169
</div>
170
</div>
171
</motion.div>
172
)}
173
</AnimatePresence>
174
</div>
175
</div>
176
);
177
}
178

