Spaces:
Running
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 +2 -2
- components/CliquePromo.tsx +309 -0
- components/clique/CliqueRoom.tsx +19 -0
- components/clique/GroupListener.tsx +37 -0
- components/clique/GroupResponseBanner.tsx +143 -0
- lib/gri.ts +252 -0
- public/characters/CLEO_SHIELD.png +3 -0
|
@@ -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
|
| 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 |
-
<
|
| 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 />
|
|
@@ -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 |
+
}
|
|
@@ -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
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
Git LFS Details
|