AIBRUH Claude Sonnet 4.6 commited on
Commit
cb92c86
Β·
1 Parent(s): fee10d5

feat: GRI system + Clique promo page + Cleo portrait

Browse files

- lib/gri.ts: Group Response Intelligence β€” keyword trigger detection (free,
no API), 8-category vault (CELEBRATION/GREETING/AGREEMENT/ENCOURAGEMENT/
SIGN_OFF/SURPRISE/THINKING), Web Speech API fallback, anti-repeat + 30s cooldown
- components/clique/GroupListener.tsx: pure logic detector on live transcript
- components/clique/GroupResponseBanner.tsx: animated banner + staggered portraits
+ live speech line display during group response
- CliqueRoom.tsx: GRI wired in (GroupListener + GroupResponseBanner)
- components/CliquePromo.tsx: full marketing promo section replacing SquadScroller
β€” hero, 9-agent roster preview with hover personas, 6-feature grid, dual CTA
- app/page.tsx: SquadScroller replaced with CliquePromo
- public/characters/CLEO_SHIELD.png: Cleo portrait (BOS shield style, Pillow-generated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

app/page.tsx CHANGED
@@ -2,7 +2,7 @@ import Nav from "@/components/Nav";
2
  import HeroVideo from "@/components/HeroVideo";
3
  import StatsBar from "@/components/StatsBar";
4
  import DifferencePanel from "@/components/DifferencePanel";
5
- import SquadScroller from "@/components/SquadScroller";
6
  import KizzyBanner from "@/components/KizzyBanner";
7
  import MatineeBanner from "@/components/MatineeBanner";
8
  import UseCases from "@/components/UseCases";
@@ -18,7 +18,7 @@ export default function Home() {
18
  <HeroVideo />
19
  <StatsBar />
20
  <DifferencePanel />
21
- <div id="clique"><SquadScroller /></div>
22
  <KizzyBanner />
23
  <MatineeBanner />
24
  <UseCases />
 
2
  import HeroVideo from "@/components/HeroVideo";
3
  import StatsBar from "@/components/StatsBar";
4
  import DifferencePanel from "@/components/DifferencePanel";
5
+ import CliquePromo from "@/components/CliquePromo";
6
  import KizzyBanner from "@/components/KizzyBanner";
7
  import MatineeBanner from "@/components/MatineeBanner";
8
  import UseCases from "@/components/UseCases";
 
18
  <HeroVideo />
19
  <StatsBar />
20
  <DifferencePanel />
21
+ <CliquePromo />
22
  <KizzyBanner />
23
  <MatineeBanner />
24
  <UseCases />
components/CliquePromo.tsx ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import Link from "next/link";
3
+ import { useState } from "react";
4
+ import { CLIQUE_ROSTER } from "@/lib/clique-roster";
5
+
6
+ const FEATURES = [
7
+ {
8
+ icon: "🧠",
9
+ title: "QCR Technology",
10
+ sub: "Quantum Consciousness Recollection",
11
+ body: "Every agent builds a genuine relationship with you across sessions. They remember your wins, your preferences, your style. Rapport grows through every interaction. They compete to be your favorite β€” and that competition makes them better.",
12
+ },
13
+ {
14
+ icon: "πŸŽ₯",
15
+ title: "Live Avatar Conversations",
16
+ sub: "Powered by Runway Γ— LiveKit Γ— OpenAI Realtime",
17
+ body: "Call any agent by name and they come alive β€” real-time face, real voice, real personality. The rest of your clique stays in active listening mode, always taking notes. One live session at a time, perfectly orchestrated.",
18
+ },
19
+ {
20
+ icon: "🀝",
21
+ title: "Group Response Intelligence",
22
+ sub: "Pre-rendered collective moments, zero API cost",
23
+ body: "Say \"good job team\" and the whole room celebrates with you β€” genuinely, naturally, together. Pre-built vault of group reactions (celebration, greeting, agreement, encouragement) triggered instantly. Your clique is a real team.",
24
+ },
25
+ {
26
+ icon: "πŸ“",
27
+ title: "Always-On Scribe",
28
+ sub: "Cleo never misses a word",
29
+ body: "Cleo captures every decision, action item, and insight in real-time β€” in both listening and live states. NotebookLM-style notetaking that builds a searchable memory of every meeting you've ever had.",
30
+ },
31
+ {
32
+ icon: "πŸ“ž",
33
+ title: "They Call You",
34
+ sub: "Your AI team reaches out",
35
+ body: "Request a call from any agent and they ring your actual phone β€” with updates, check-ins, and progress reports in their voice. Grant email and LinkedIn access and they can act on your behalf without you even being in the room.",
36
+ },
37
+ {
38
+ icon: "πŸ“±",
39
+ title: "Any Device, Any Camera",
40
+ sub: "Mobile-first from day one",
41
+ body: "Join from your phone, tablet, or laptop. Front-facing camera auto-selected on mobile with device switching and mirror controls. The room comes to you β€” not the other way around.",
42
+ },
43
+ ];
44
+
45
+ const DEFAULT_PREVIEW_IDS = ["amanda","eve","kizzy","jamarr","india","lacara","cleo","naomi","bri"];
46
+
47
+ export default function CliquePromo() {
48
+ const [hoveredAgent, setHoveredAgent] = useState<string | null>(null);
49
+ const previewAgents = DEFAULT_PREVIEW_IDS
50
+ .map(id => CLIQUE_ROSTER.find(a => a.id === id))
51
+ .filter(Boolean);
52
+
53
+ return (
54
+ <section id="clique" style={{ background: "#FDFAF6", overflow: "hidden" }}>
55
+ <style>{`
56
+ @keyframes clique-float {
57
+ 0%,100% { transform: translateY(0px); }
58
+ 50% { transform: translateY(-8px); }
59
+ }
60
+ @keyframes clique-glow {
61
+ 0%,100% { box-shadow: 0 0 20px rgba(200,169,81,.2); }
62
+ 50% { box-shadow: 0 0 40px rgba(200,169,81,.45), 0 0 80px rgba(200,169,81,.1); }
63
+ }
64
+ @keyframes clique-pulse-dot {
65
+ 0%,100% { opacity:1; transform:scale(1); }
66
+ 50% { opacity:.4; transform:scale(.85); }
67
+ }
68
+ .clique-feat-card:hover {
69
+ border-color: rgba(200,169,81,.55) !important;
70
+ background: rgba(200,169,81,.06) !important;
71
+ transform: translateY(-4px);
72
+ }
73
+ .clique-agent-tile:hover {
74
+ transform: translateY(-8px) scale(1.04) !important;
75
+ border-color: #c8a951 !important;
76
+ box-shadow: 0 12px 40px rgba(200,169,81,.3) !important;
77
+ }
78
+ @media (max-width:768px) {
79
+ .clique-hero-title { font-size: 32px !important; }
80
+ .clique-hero-sub { font-size: 15px !important; }
81
+ .clique-feat-grid { grid-template-columns: 1fr !important; }
82
+ .clique-agent-grid { gap: 12px !important; }
83
+ }
84
+ `}</style>
85
+
86
+ {/* ── HERO ── */}
87
+ <div style={{
88
+ background: "linear-gradient(160deg,#0d0905 0%,#1a1004 40%,#0d0905 100%)",
89
+ padding: "80px 28px 72px",
90
+ textAlign: "center",
91
+ position: "relative",
92
+ overflow: "hidden",
93
+ }}>
94
+ {/* Background shimmer grid */}
95
+ <div style={{
96
+ position: "absolute", inset: 0, opacity: 0.04,
97
+ backgroundImage: "linear-gradient(rgba(200,169,81,.8) 1px,transparent 1px),linear-gradient(90deg,rgba(200,169,81,.8) 1px,transparent 1px)",
98
+ backgroundSize: "60px 60px",
99
+ pointerEvents: "none",
100
+ }} />
101
+
102
+ {/* Live indicator */}
103
+ <div style={{ display: "inline-flex", alignItems: "center", gap: 8, marginBottom: 24,
104
+ padding: "6px 16px", border: "1px solid rgba(200,169,81,.3)",
105
+ background: "rgba(200,169,81,.06)", borderRadius: 20 }}>
106
+ <span style={{ width: 6, height: 6, borderRadius: "50%", background: "#4CAF50",
107
+ display: "inline-block", animation: "clique-pulse-dot 1.4s ease-in-out infinite",
108
+ boxShadow: "0 0 6px #4CAF50" }} />
109
+ <span style={{ fontFamily: "'Cinzel',serif", fontSize: 9, letterSpacing: 3,
110
+ textTransform: "uppercase", color: "#c8a951" }}>Now Live on berylize.com</span>
111
+ </div>
112
+
113
+ <h2 className="clique-hero-title" style={{
114
+ fontFamily: "'Cinzel Decorative','Cinzel',serif",
115
+ fontSize: 52, fontWeight: 900, letterSpacing: 2,
116
+ background: "linear-gradient(110deg,#6b4f0a 0%,#c8a951 15%,#fff8c0 35%,#f5e070 50%,#fff8c0 65%,#c8a951 85%,#6b4f0a 100%)",
117
+ WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text",
118
+ marginBottom: 8, lineHeight: 1.15,
119
+ }}>BERYL CLIQUE</h2>
120
+
121
+ <p className="clique-hero-sub" style={{
122
+ fontFamily: "'Cormorant Garamond',serif", fontSize: 22,
123
+ color: "rgba(253,250,246,.65)", fontStyle: "italic",
124
+ marginBottom: 40, letterSpacing: 1,
125
+ }}>Zoom with live conversational AI avatars β€” your team, your way.</p>
126
+
127
+ <p style={{
128
+ fontFamily: "'Cormorant Garamond',serif", fontSize: 17,
129
+ color: "rgba(200,169,81,.7)", lineHeight: 1.75,
130
+ maxWidth: 620, margin: "0 auto 48px",
131
+ }}>
132
+ FaceTime your AI agent clique. Call them by name and they come alive.
133
+ They remember you, compete for your trust, and get real work done β€”
134
+ together, in the room, always on.
135
+ </p>
136
+
137
+ <Link href="/clique" style={{
138
+ display: "inline-block",
139
+ padding: "16px 48px",
140
+ fontFamily: "'Cinzel',serif", fontSize: 13, fontWeight: 700,
141
+ letterSpacing: 3, textTransform: "uppercase", textDecoration: "none",
142
+ color: "#0a0604",
143
+ background: "linear-gradient(110deg,#8B6914 0%,#c8a951 25%,#fff8c0 45%,#f5e070 55%,#c8a951 75%,#8B6914 100%)",
144
+ backgroundSize: "200% auto",
145
+ border: "1px solid rgba(245,224,112,.4)",
146
+ boxShadow: "0 0 30px rgba(200,169,81,.3)",
147
+ animation: "clique-glow 3s ease-in-out infinite",
148
+ }}>Enter the Clique β€Ί</Link>
149
+ </div>
150
+
151
+ {/* ── AGENT ROSTER PREVIEW ── */}
152
+ <div style={{ padding: "60px 28px 52px", background: "#fff" }}>
153
+ <div style={{ textAlign: "center", marginBottom: 40 }}>
154
+ <div style={{ fontFamily: "'Cinzel',serif", fontSize: 11, fontWeight: 700,
155
+ letterSpacing: 3, textTransform: "uppercase", color: "#4CAF50", marginBottom: 10 }}>
156
+ Your Default Team
157
+ </div>
158
+ <h3 style={{ fontFamily: "'Cinzel',serif", fontSize: 22, fontWeight: 600, color: "#0D1117", marginBottom: 6 }}>
159
+ Meet The <span style={{ color: "#1a5f7a" }}>Beryl Clique</span>
160
+ </h3>
161
+ <p style={{ fontFamily: "'Cormorant Garamond',serif", fontSize: 15,
162
+ color: "#888", fontStyle: "italic", marginTop: 4 }}>
163
+ 16 agents. Each one wants to earn your trust.
164
+ </p>
165
+ <div style={{ width: 40, height: 2, background: "#4CAF50", margin: "14px auto 0" }} />
166
+ </div>
167
+
168
+ <div className="clique-agent-grid" style={{
169
+ display: "flex", flexWrap: "wrap", gap: 18,
170
+ justifyContent: "center", maxWidth: 900, margin: "0 auto",
171
+ }}>
172
+ {previewAgents.map(agent => agent && (
173
+ <div
174
+ key={agent.id}
175
+ className="clique-agent-tile"
176
+ onMouseEnter={() => setHoveredAgent(agent.id)}
177
+ onMouseLeave={() => setHoveredAgent(null)}
178
+ style={{
179
+ display: "flex", flexDirection: "column", alignItems: "center", gap: 8,
180
+ cursor: "pointer",
181
+ transition: "transform .25s, border-color .25s, box-shadow .25s",
182
+ }}
183
+ >
184
+ <div style={{
185
+ width: 88, height: 88,
186
+ borderRadius: agent.isCSA ? "50%" : 6,
187
+ overflow: "hidden",
188
+ border: hoveredAgent === agent.id
189
+ ? "2px solid #c8a951"
190
+ : agent.isCSA
191
+ ? "2px solid rgba(200,169,81,.6)"
192
+ : "2px solid rgba(200,169,81,.22)",
193
+ background: "#ddd4c0",
194
+ boxShadow: "0 4px 14px rgba(0,0,0,.08)",
195
+ transition: "border-color .25s, box-shadow .25s",
196
+ }}>
197
+ <img
198
+ src={agent.portrait}
199
+ alt={agent.name}
200
+ style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "top center" }}
201
+ />
202
+ </div>
203
+ <div style={{ textAlign: "center" }}>
204
+ <div style={{ fontFamily: "'Cinzel',serif", fontSize: 10, fontWeight: 600,
205
+ color: "#0D1117", letterSpacing: 1 }}>{agent.name}</div>
206
+ <div style={{ fontFamily: "'Cormorant Garamond',serif", fontSize: 10,
207
+ color: "#aaa", fontStyle: "italic", marginTop: 1 }}>{agent.role}</div>
208
+ {agent.isCSA && (
209
+ <div style={{ fontFamily: "'Cinzel',serif", fontSize: 7, letterSpacing: 1.5,
210
+ color: "#c8a951", textTransform: "uppercase", marginTop: 2 }}>CSA</div>
211
+ )}
212
+ </div>
213
+ </div>
214
+ ))}
215
+ </div>
216
+
217
+ {hoveredAgent && (() => {
218
+ const a = CLIQUE_ROSTER.find(x => x.id === hoveredAgent);
219
+ return a ? (
220
+ <div style={{
221
+ textAlign: "center", marginTop: 24,
222
+ fontFamily: "'Cormorant Garamond',serif", fontSize: 14,
223
+ color: "#555", fontStyle: "italic", minHeight: 24,
224
+ transition: "opacity .2s",
225
+ }}>
226
+ <strong style={{ color: "#1a5f7a", fontStyle: "normal" }}>{a.name}</strong>
227
+ {" β€” "}{a.persona}
228
+ </div>
229
+ ) : null;
230
+ })()}
231
+ </div>
232
+
233
+ {/* ── 6-FEATURE GRID ── */}
234
+ <div style={{ padding: "64px 28px 72px", background: "#FDFAF6" }}>
235
+ <div style={{ textAlign: "center", marginBottom: 48 }}>
236
+ <div style={{ fontFamily: "'Cinzel',serif", fontSize: 11, fontWeight: 700,
237
+ letterSpacing: 3, textTransform: "uppercase", color: "#c8a951", marginBottom: 10 }}>
238
+ Built Different
239
+ </div>
240
+ <h3 style={{ fontFamily: "'Cinzel',serif", fontSize: 22, fontWeight: 600,
241
+ color: "#0D1117" }}>Everything That Makes It One of a Kind</h3>
242
+ <div style={{ width: 40, height: 2, background: "linear-gradient(90deg,#8B6914,#f5e070,#8B6914)",
243
+ margin: "14px auto 0" }} />
244
+ </div>
245
+
246
+ <div className="clique-feat-grid" style={{
247
+ display: "grid",
248
+ gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
249
+ gap: 20, maxWidth: 1000, margin: "0 auto",
250
+ }}>
251
+ {FEATURES.map(f => (
252
+ <div
253
+ key={f.title}
254
+ className="clique-feat-card"
255
+ style={{
256
+ padding: "28px 24px",
257
+ border: "1px solid rgba(200,169,81,.2)",
258
+ background: "#fff",
259
+ borderRadius: 8,
260
+ transition: "border-color .25s, background .25s, transform .25s",
261
+ cursor: "default",
262
+ }}
263
+ >
264
+ <div style={{ fontSize: 28, marginBottom: 14 }}>{f.icon}</div>
265
+ <div style={{ fontFamily: "'Cinzel',serif", fontSize: 12, fontWeight: 700,
266
+ letterSpacing: 2, textTransform: "uppercase", color: "#0D1117",
267
+ marginBottom: 4 }}>{f.title}</div>
268
+ <div style={{ fontFamily: "'Cormorant Garamond',serif", fontSize: 12,
269
+ color: "#c8a951", fontStyle: "italic", marginBottom: 12 }}>{f.sub}</div>
270
+ <p style={{ fontFamily: "'Cormorant Garamond',serif", fontSize: 15,
271
+ color: "#555", lineHeight: 1.7, margin: 0 }}>{f.body}</p>
272
+ </div>
273
+ ))}
274
+ </div>
275
+ </div>
276
+
277
+ {/* ── BOTTOM CTA ── */}
278
+ <div style={{
279
+ textAlign: "center",
280
+ padding: "60px 28px 72px",
281
+ background: "linear-gradient(160deg,#0d0905 0%,#1a1004 50%,#0d0905 100%)",
282
+ }}>
283
+ <p style={{ fontFamily: "'Cinzel',serif", fontSize: 11, letterSpacing: 3,
284
+ textTransform: "uppercase", color: "rgba(200,169,81,.5)", marginBottom: 20 }}>
285
+ Version 1 Β· Beryl Operating System
286
+ </p>
287
+ <h3 style={{
288
+ fontFamily: "'Cinzel Decorative','Cinzel',serif", fontSize: 28, fontWeight: 700,
289
+ background: "linear-gradient(135deg,#8B6914,#c8a951,#f5e070,#c8a951,#8B6914)",
290
+ WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text",
291
+ marginBottom: 16,
292
+ }}>Your Clique Is Waiting</h3>
293
+ <p style={{ fontFamily: "'Cormorant Garamond',serif", fontSize: 17,
294
+ color: "rgba(253,250,246,.55)", maxWidth: 480, margin: "0 auto 36px",
295
+ fontStyle: "italic", lineHeight: 1.7 }}>
296
+ Call your team by name. They already know you.
297
+ </p>
298
+ <Link href="/clique" style={{
299
+ display: "inline-block", padding: "15px 44px",
300
+ fontFamily: "'Cinzel',serif", fontSize: 12, fontWeight: 700,
301
+ letterSpacing: 3, textTransform: "uppercase", textDecoration: "none",
302
+ color: "#0a0604",
303
+ background: "linear-gradient(110deg,#8B6914,#c8a951,#f5e070,#c8a951,#8B6914)",
304
+ border: "1px solid rgba(245,224,112,.35)",
305
+ }}>Open the Room β€Ί</Link>
306
+ </div>
307
+ </section>
308
+ );
309
+ }
components/clique/CliqueRoom.tsx CHANGED
@@ -6,6 +6,9 @@ import AgentTile from "./AgentTile";
6
  import CallControls from "./CallControls";
7
  import CameraPanel from "./CameraPanel";
8
  import AccessGrantPanel, { AccessGrants } from "./AccessGrantPanel";
 
 
 
9
 
10
  type AgentState = "listening" | "live" | "offline";
11
  type AgentStateMap = Record<string, AgentState>;
@@ -41,8 +44,16 @@ export default function CliqueRoom() {
41
  const [grants, setGrants] = useState<AccessGrants>({
42
  email: false, linkedin: false, phone: null, phoneCallEnabled: false,
43
  });
 
 
 
44
  const userVideoRef = useRef<HTMLVideoElement>(null);
45
 
 
 
 
 
 
46
  useEffect(() => {
47
  setQCR(loadQCRProfiles(getDefaultTeam()));
48
  }, []);
@@ -219,6 +230,14 @@ export default function CliqueRoom() {
219
  />
220
  )}
221
 
 
 
 
 
 
 
 
 
222
  {/* ── CALL ME MODAL ── */}
223
  {callMeOpen && (
224
  <CallMeModal
 
6
  import CallControls from "./CallControls";
7
  import CameraPanel from "./CameraPanel";
8
  import AccessGrantPanel, { AccessGrants } from "./AccessGrantPanel";
9
+ import GroupListener from "./GroupListener";
10
+ import GroupResponseBanner from "./GroupResponseBanner";
11
+ import { GRIClip, GRICategory } from "@/lib/gri";
12
 
13
  type AgentState = "listening" | "live" | "offline";
14
  type AgentStateMap = Record<string, AgentState>;
 
44
  const [grants, setGrants] = useState<AccessGrants>({
45
  email: false, linkedin: false, phone: null, phoneCallEnabled: false,
46
  });
47
+ const [griClip, setGriClip] = useState<GRIClip | null>(null);
48
+ const [griCat, setGriCat] = useState<GRICategory | null>(null);
49
+ const [transcript, setTranscript] = useState("");
50
  const userVideoRef = useRef<HTMLVideoElement>(null);
51
 
52
+ const handleGRI = useCallback((clip: GRIClip, category: GRICategory) => {
53
+ setGriClip(clip);
54
+ setGriCat(category);
55
+ }, []);
56
+
57
  useEffect(() => {
58
  setQCR(loadQCRProfiles(getDefaultTeam()));
59
  }, []);
 
230
  />
231
  )}
232
 
233
+ {/* ── GROUP RESPONSE INTELLIGENCE ── */}
234
+ <GroupListener transcript={transcript} onTrigger={handleGRI} />
235
+ <GroupResponseBanner
236
+ clip={griClip}
237
+ category={griCat}
238
+ onDone={() => { setGriClip(null); setGriCat(null); }}
239
+ />
240
+
241
  {/* ── CALL ME MODAL ── */}
242
  {callMeOpen && (
243
  <CallMeModal
components/clique/GroupListener.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useRef, useCallback } from "react";
3
+ import { detectGRITrigger, selectClip, playClipSpeech, GRIClip, GRICategory } from "@/lib/gri";
4
+
5
+ interface Props {
6
+ /** Live transcript text β€” feed each new utterance here */
7
+ transcript: string;
8
+ onTrigger: (clip: GRIClip, category: GRICategory) => void;
9
+ }
10
+
11
+ const COOLDOWN_MS = 30_000;
12
+
13
+ export default function GroupListener({ transcript, onTrigger }: Props) {
14
+ const lastFiredMs = useRef<number>(0);
15
+ const lastClipId = useRef<string | undefined>(undefined);
16
+ const prevText = useRef<string>("");
17
+
18
+ useEffect(() => {
19
+ // Only check new text added since last render
20
+ const newText = transcript.slice(prevText.current.length).trim();
21
+ prevText.current = transcript;
22
+ if (!newText) return;
23
+
24
+ const now = Date.now();
25
+ if (now - lastFiredMs.current < COOLDOWN_MS) return;
26
+
27
+ const category = detectGRITrigger(newText);
28
+ if (!category) return;
29
+
30
+ const clip = selectClip(category, lastClipId.current);
31
+ lastClipId.current = clip.id;
32
+ lastFiredMs.current = now;
33
+ onTrigger(clip, category);
34
+ }, [transcript, onTrigger]);
35
+
36
+ return null; // pure logic component
37
+ }
components/clique/GroupResponseBanner.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { GRIClip, GRICategory, playClipSpeech } from "@/lib/gri";
4
+ import { CLIQUE_ROSTER } from "@/lib/clique-roster";
5
+
6
+ interface Props {
7
+ clip: GRIClip | null;
8
+ category: GRICategory | null;
9
+ onDone: () => void;
10
+ }
11
+
12
+ const CATEGORY_LABEL: Record<GRICategory, string> = {
13
+ CELEBRATION: "πŸŽ‰ The clique is celebrating with you",
14
+ GREETING: "πŸ‘‹ Your clique says good morning",
15
+ AGREEMENT: "βœ“ The room agrees",
16
+ ENCOURAGEMENT: "πŸ’ͺ Your clique has your back",
17
+ SIGN_OFF: "✨ Until next time",
18
+ SURPRISE: "😲 The clique reacts",
19
+ THINKING: "πŸ€” The room is thinking",
20
+ };
21
+
22
+ export default function GroupResponseBanner({ clip, category, onDone }: Props) {
23
+ const [visible, setVisible] = useState(false);
24
+ const [line, setLine] = useState("");
25
+ const stopRef = useRef<(() => void) | null>(null);
26
+
27
+ useEffect(() => {
28
+ if (!clip || !category) { setVisible(false); return; }
29
+
30
+ setVisible(true);
31
+ setLine("");
32
+
33
+ // If pre-rendered audio exists, play it; otherwise use Web Speech API
34
+ if (clip.audioUrl) {
35
+ const audio = new Audio(clip.audioUrl);
36
+ audio.play().catch(() => {});
37
+ const timer = setTimeout(() => { setVisible(false); onDone(); }, clip.durationMs + 500);
38
+ return () => { audio.pause(); clearTimeout(timer); };
39
+ } else {
40
+ let lineIdx = 0;
41
+ const showLines = () => {
42
+ if (lineIdx < clip.speechLines.length) {
43
+ setLine(clip.speechLines[lineIdx].text);
44
+ lineIdx++;
45
+ }
46
+ };
47
+ showLines();
48
+ const interval = setInterval(showLines, clip.durationMs / clip.speechLines.length);
49
+ const stop = playClipSpeech(clip, () => {
50
+ setVisible(false);
51
+ onDone();
52
+ });
53
+ stopRef.current = stop;
54
+ const timer = setTimeout(() => { setVisible(false); onDone(); }, clip.durationMs + 2000);
55
+ return () => {
56
+ clearInterval(interval);
57
+ clearTimeout(timer);
58
+ stopRef.current?.();
59
+ };
60
+ }
61
+ }, [clip, category, onDone]);
62
+
63
+ if (!visible || !clip || !category) return null;
64
+
65
+ const featured = clip.agentIds
66
+ .map(id => CLIQUE_ROSTER.find(a => a.id === id))
67
+ .filter(Boolean);
68
+
69
+ return (
70
+ <div style={{
71
+ position: "fixed",
72
+ bottom: 90, left: "50%",
73
+ transform: "translateX(-50%)",
74
+ zIndex: 250,
75
+ background: "rgba(13,9,5,.97)",
76
+ border: "1px solid rgba(200,169,81,.45)",
77
+ borderRadius: 12,
78
+ padding: "16px 24px",
79
+ minWidth: 320,
80
+ maxWidth: 560,
81
+ boxShadow: "0 8px 60px rgba(0,0,0,.5), 0 0 0 1px rgba(200,169,81,.1)",
82
+ display: "flex",
83
+ flexDirection: "column",
84
+ alignItems: "center",
85
+ gap: 12,
86
+ animation: "gri-in .3s cubic-bezier(.34,1.56,.64,1) both",
87
+ }}>
88
+ <style>{`
89
+ @keyframes gri-in {
90
+ from { opacity:0; transform:translateX(-50%) translateY(16px) scale(.95); }
91
+ to { opacity:1; transform:translateX(-50%) translateY(0) scale(1); }
92
+ }
93
+ @keyframes gri-portrait-pop {
94
+ 0% { transform:scale(1); }
95
+ 50% { transform:scale(1.1); }
96
+ 100% { transform:scale(1); }
97
+ }
98
+ `}</style>
99
+
100
+ {/* Category label */}
101
+ <div style={{
102
+ fontFamily: "'Cinzel',serif",
103
+ fontSize: 10,
104
+ letterSpacing: 2.5,
105
+ textTransform: "uppercase",
106
+ color: "#c8a951",
107
+ }}>{CATEGORY_LABEL[category]}</div>
108
+
109
+ {/* Agent portraits */}
110
+ <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
111
+ {featured.map((agent, i) => agent && (
112
+ <div key={agent.id} style={{
113
+ width: 44, height: 44,
114
+ borderRadius: "50%",
115
+ overflow: "hidden",
116
+ border: "2px solid rgba(200,169,81,.5)",
117
+ flexShrink: 0,
118
+ animation: `gri-portrait-pop .4s ease ${i * 80}ms both`,
119
+ }}>
120
+ <img
121
+ src={agent.portrait}
122
+ alt={agent.name}
123
+ style={{ width: "100%", height: "100%", objectFit: "cover", objectPosition: "top" }}
124
+ />
125
+ </div>
126
+ ))}
127
+ </div>
128
+
129
+ {/* Live speech line */}
130
+ {line && (
131
+ <p style={{
132
+ fontFamily: "'Cormorant Garamond',serif",
133
+ fontSize: 15,
134
+ color: "rgba(255,255,255,.88)",
135
+ fontStyle: "italic",
136
+ textAlign: "center",
137
+ lineHeight: 1.5,
138
+ margin: 0,
139
+ }}>"{line}"</p>
140
+ )}
141
+ </div>
142
+ );
143
+ }
lib/gri.ts ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * GRI β€” GROUP RESPONSE INTELLIGENCE
3
+ *
4
+ * A cost-free trigger system for collective agent moments.
5
+ * Pre-rendered audio/video clips play when the room should respond as one.
6
+ * No API call at trigger time β€” everything is a pre-built vault.
7
+ *
8
+ * ARCHITECTURE
9
+ * ────────────
10
+ * 1. DETECTOR Pure keyword + sentiment pattern matching on the live transcript.
11
+ * No LLM needed. Runs client-side, zero cost.
12
+ * 2. VAULT Library of pre-rendered group audio clips (ElevenLabs/MiniMax bulk
13
+ * renders, done once, served free forever). Fallback = Web Speech API.
14
+ * 3. SELECTOR Picks a random clip from the matched category.
15
+ * Anti-repeat memory (never plays same clip twice in a row).
16
+ * 4. COOLDOWN 30s minimum between group responses. The room never over-reacts.
17
+ *
18
+ * VAULT CATEGORIES
19
+ * ────────────────
20
+ * CELEBRATION "You crushed it!" collective cheer
21
+ * GREETING Morning / session-start welcome
22
+ * AGREEMENT Collective nod β€” "Exactly, that's it"
23
+ * ENCOURAGEMENT "You've got this, keep going"
24
+ * SIGN_OFF End-of-session collective goodbye
25
+ * SURPRISE Wow / that's unexpected
26
+ * THINKING Collective "hmm, let us think on that"
27
+ */
28
+
29
+ export type GRICategory =
30
+ | "CELEBRATION"
31
+ | "GREETING"
32
+ | "AGREEMENT"
33
+ | "ENCOURAGEMENT"
34
+ | "SIGN_OFF"
35
+ | "SURPRISE"
36
+ | "THINKING";
37
+
38
+ export interface GRIClip {
39
+ id: string;
40
+ category: GRICategory;
41
+ /** Path under /gri-vault/ or a full URL. Falls back to speechLines if null. */
42
+ audioUrl: string | null;
43
+ /** Web Speech API fallback lines β€” one per agent voice */
44
+ speechLines: Array<{ text: string; voice?: string; pitch?: number; rate?: number }>;
45
+ /** Which agent portraits to highlight during playback */
46
+ agentIds: string[];
47
+ durationMs: number;
48
+ }
49
+
50
+ /** Trigger patterns per category β€” pure string matching, no LLM */
51
+ const TRIGGERS: Record<GRICategory, string[]> = {
52
+ CELEBRATION: [
53
+ "good job", "great job", "great work", "well done", "amazing work",
54
+ "you crushed it", "nailed it", "killed it", "you did it", "we did it",
55
+ "proud of you", "fantastic", "excellent work", "that was incredible",
56
+ "we shipped", "it's live", "we launched",
57
+ ],
58
+ GREETING: [
59
+ "good morning", "good afternoon", "good evening",
60
+ "hello everyone", "hey team", "hi everyone", "hey guys",
61
+ "what's up team", "morning everyone", "let's get started",
62
+ ],
63
+ AGREEMENT: [
64
+ "exactly", "that's right", "i agree", "absolutely", "totally agree",
65
+ "you're right", "yes exactly", "that's it", "spot on", "100%",
66
+ ],
67
+ ENCOURAGEMENT: [
68
+ "keep going", "you got this", "don't give up", "almost there",
69
+ "stay with it", "you can do it", "we believe in you", "push through",
70
+ ],
71
+ SIGN_OFF: [
72
+ "good night", "bye everyone", "see you tomorrow", "signing off",
73
+ "great meeting", "talk soon", "have a good one", "until next time",
74
+ "that's a wrap", "we're done", "meeting adjourned",
75
+ ],
76
+ SURPRISE: [
77
+ "no way", "oh wow", "seriously", "are you serious", "that's wild",
78
+ "i can't believe", "unbelievable", "wait what", "whoa",
79
+ ],
80
+ THINKING: [
81
+ "let me think", "hmm", "good question", "that's a tough one",
82
+ "interesting question", "not sure", "let's think about this",
83
+ ],
84
+ };
85
+
86
+ /** Pre-built vault β€” swap audioUrl from null β†’ real path once clips are rendered */
87
+ export const GRI_VAULT: GRIClip[] = [
88
+ // ── CELEBRATION ──
89
+ {
90
+ id: "cel-01", category: "CELEBRATION", audioUrl: null,
91
+ agentIds: ["amanda","eve","kizzy","jamarr","india"],
92
+ durationMs: 3500,
93
+ speechLines: [
94
+ { text: "Yes! You crushed it!", voice: "shimmer", pitch: 1.1, rate: 1.1 },
95
+ { text: "That's what I'm talking about!", voice: "ash", pitch: 1.0, rate: 1.05 },
96
+ { text: "Incredible work. Seriously.", voice: "sage", pitch: 0.95, rate: 0.95 },
97
+ { text: "We are SO proud of you right now.", voice: "coral", pitch: 1.05, rate: 1.0 },
98
+ { text: "Let's go!", voice: "verse", pitch: 1.2, rate: 1.2 },
99
+ ],
100
+ },
101
+ {
102
+ id: "cel-02", category: "CELEBRATION", audioUrl: null,
103
+ agentIds: ["naomi","bri","terrell","jeff","lacara"],
104
+ durationMs: 4000,
105
+ speechLines: [
106
+ { text: "Outstanding. Genuinely outstanding.", voice: "ballad", pitch: 0.95, rate: 0.9 },
107
+ { text: "The data doesn't lie β€” that was elite.", voice: "echo", pitch: 1.0, rate: 1.0 },
108
+ { text: "Shipped clean, shipped fast. Perfect.", voice: "cedar", pitch: 1.0, rate: 0.95 },
109
+ { text: "The design held up beautifully.", voice: "ballad", pitch: 1.05, rate: 1.0 },
110
+ { text: "That's how we do it.", voice: "verse", pitch: 1.1, rate: 1.05 },
111
+ ],
112
+ },
113
+ {
114
+ id: "cel-03", category: "CELEBRATION", audioUrl: null,
115
+ agentIds: ["amanda","kizzy","shelly","maria","nu"],
116
+ durationMs: 3000,
117
+ speechLines: [
118
+ { text: "The whole room is celebrating right now.", voice: "marin", pitch: 1.0, rate: 1.0 },
119
+ { text: "We've been rooting for you. This is everything.", voice: "coral", pitch: 1.05, rate: 0.95 },
120
+ { text: "The community is going to love this.", voice: "shimmer", pitch: 1.0, rate: 1.0 },
121
+ { text: "This is a moment.", voice: "sage", pitch: 0.9, rate: 0.9 },
122
+ ],
123
+ },
124
+
125
+ // ── GREETING ──
126
+ {
127
+ id: "greet-01", category: "GREETING", audioUrl: null,
128
+ agentIds: ["amanda","eve","kizzy"],
129
+ durationMs: 3000,
130
+ speechLines: [
131
+ { text: "Good morning! Ready when you are.", voice: "marin", pitch: 1.0, rate: 1.0 },
132
+ { text: "Morning. Let's make this a great one.", voice: "shimmer", pitch: 1.0, rate: 0.95 },
133
+ { text: "Hey! So good to see you today.", voice: "coral", pitch: 1.1, rate: 1.05 },
134
+ ],
135
+ },
136
+ {
137
+ id: "greet-02", category: "GREETING", audioUrl: null,
138
+ agentIds: ["jamarr","india","terrell","jeff"],
139
+ durationMs: 2800,
140
+ speechLines: [
141
+ { text: "Let's get it! Big things today.", voice: "ash", pitch: 1.1, rate: 1.1 },
142
+ { text: "Ready to build. What are we doing first?", voice: "coral", pitch: 1.0, rate: 1.05 },
143
+ { text: "Numbers are prepped. Ready.", voice: "echo", pitch: 1.0, rate: 0.95 },
144
+ { text: "Operations standing by.", voice: "cedar", pitch: 0.95, rate: 0.9 },
145
+ ],
146
+ },
147
+
148
+ // ── AGREEMENT ──
149
+ {
150
+ id: "agree-01", category: "AGREEMENT", audioUrl: null,
151
+ agentIds: ["eve","jessica","amanda","naomi"],
152
+ durationMs: 2500,
153
+ speechLines: [
154
+ { text: "Exactly. That's precisely it.", voice: "shimmer", pitch: 1.0, rate: 0.95 },
155
+ { text: "Agreed. That's the right call.", voice: "sage", pitch: 0.95, rate: 0.9 },
156
+ { text: "Mmm-hmm. Yes, that's it.", voice: "marin", pitch: 1.0, rate: 0.95 },
157
+ { text: "100%. Building on that right now.", voice: "ballad", pitch: 1.0, rate: 1.0 },
158
+ ],
159
+ },
160
+
161
+ // ── ENCOURAGEMENT ──
162
+ {
163
+ id: "enc-01", category: "ENCOURAGEMENT", audioUrl: null,
164
+ agentIds: ["amanda","kizzy","jamarr","india"],
165
+ durationMs: 3200,
166
+ speechLines: [
167
+ { text: "You've got this. We're right behind you.", voice: "marin", pitch: 1.0, rate: 0.95 },
168
+ { text: "I believe in you. Keep pushing.", voice: "coral", pitch: 1.05, rate: 1.0 },
169
+ { text: "Almost there. Do not stop now.", voice: "ash", pitch: 1.0, rate: 1.0 },
170
+ { text: "The finish line is right there. Go.", voice: "coral", pitch: 1.05, rate: 1.05 },
171
+ ],
172
+ },
173
+
174
+ // ── SIGN OFF ──
175
+ {
176
+ id: "bye-01", category: "SIGN_OFF", audioUrl: null,
177
+ agentIds: ["amanda","eve","kizzy","cleo"],
178
+ durationMs: 3500,
179
+ speechLines: [
180
+ { text: "Great session today. Rest well.", voice: "marin", pitch: 1.0, rate: 0.9 },
181
+ { text: "Good work everyone. Same time tomorrow.", voice: "shimmer", pitch: 0.95, rate: 0.9 },
182
+ { text: "Take care of yourself. You earned it.", voice: "coral", pitch: 1.0, rate: 0.9 },
183
+ { text: "Minutes are captured. Talk soon.", voice: "sage", pitch: 0.95, rate: 0.9 },
184
+ ],
185
+ },
186
+
187
+ // ── SURPRISE ──
188
+ {
189
+ id: "wow-01", category: "SURPRISE", audioUrl: null,
190
+ agentIds: ["nu","jamarr","india","bri"],
191
+ durationMs: 2500,
192
+ speechLines: [
193
+ { text: "Wait β€” seriously?!", voice: "verse", pitch: 1.2, rate: 1.15 },
194
+ { text: "No way. That's wild.", voice: "ash", pitch: 1.1, rate: 1.1 },
195
+ { text: "I did NOT see that coming.", voice: "coral", pitch: 1.15, rate: 1.1 },
196
+ { text: "Okay, updating my research β€” that changes things.", voice: "verse", pitch: 1.0, rate: 1.05 },
197
+ ],
198
+ },
199
+
200
+ // ── THINKING ──
201
+ {
202
+ id: "think-01", category: "THINKING", audioUrl: null,
203
+ agentIds: ["eve","jessica","terrell","bri"],
204
+ durationMs: 2800,
205
+ speechLines: [
206
+ { text: "Mmm. Let us think on this.", voice: "shimmer", pitch: 0.9, rate: 0.85 },
207
+ { text: "That's a good one. Give us a second.", voice: "sage", pitch: 0.95, rate: 0.9 },
208
+ { text: "Pulling the data on that now.", voice: "echo", pitch: 1.0, rate: 0.95 },
209
+ { text: "Cross-referencing. Interesting angle.", voice: "verse", pitch: 0.95, rate: 0.9 },
210
+ ],
211
+ },
212
+ ];
213
+
214
+ /** Detect which GRI category (if any) a transcript phrase triggers */
215
+ export function detectGRITrigger(text: string): GRICategory | null {
216
+ const lower = text.toLowerCase().trim();
217
+ for (const [cat, patterns] of Object.entries(TRIGGERS) as [GRICategory, string[]][]) {
218
+ if (patterns.some(p => lower.includes(p))) return cat;
219
+ }
220
+ return null;
221
+ }
222
+
223
+ /** Pick a random clip from category, avoiding last used */
224
+ export function selectClip(category: GRICategory, lastClipId?: string): GRIClip {
225
+ const pool = GRI_VAULT.filter(c => c.category === category && c.id !== lastClipId);
226
+ const candidates = pool.length > 0 ? pool : GRI_VAULT.filter(c => c.category === category);
227
+ return candidates[Math.floor(Math.random() * candidates.length)];
228
+ }
229
+
230
+ /** Play a GRI clip via Web Speech API (free fallback) */
231
+ export function playClipSpeech(clip: GRIClip, onEnd?: () => void): () => void {
232
+ if (typeof window === "undefined" || !window.speechSynthesis) {
233
+ onEnd?.();
234
+ return () => {};
235
+ }
236
+ const synth = window.speechSynthesis;
237
+ synth.cancel();
238
+
239
+ let i = 0;
240
+ const playNext = () => {
241
+ if (i >= clip.speechLines.length) { onEnd?.(); return; }
242
+ const line = clip.speechLines[i++];
243
+ const utt = new SpeechSynthesisUtterance(line.text);
244
+ utt.pitch = line.pitch ?? 1;
245
+ utt.rate = line.rate ?? 1;
246
+ utt.volume = 0.85;
247
+ utt.onend = () => setTimeout(playNext, 220);
248
+ synth.speak(utt);
249
+ };
250
+ playNext();
251
+ return () => synth.cancel();
252
+ }
public/characters/CLEO_SHIELD.png ADDED

Git LFS Details

  • SHA256: aeed1bc5e16c55f16856d7793656c6646102bacabfcf35894dc0b95038527912
  • Pointer size: 131 Bytes
  • Size of remote file: 111 kB