@schandlergarcia/sf-web-components 2.3.16 → 2.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/brands/engine/app/components/AgentPanel.tsx +247 -108
- package/brands/engine/app/components/Data360Widget.tsx +301 -0
- package/brands/engine/app/data/partner-hub-sample-data.js +297 -0
- package/brands/engine/app/pages/PartnerHubDashboard.tsx +1036 -353
- package/brands/engine/app/styles/global.css +1 -1
- package/brands/engine/global.css +1 -1
- package/dist/styles/global.css +61 -50
- package/package.json +1 -1
- package/src/styles/global.css +61 -50
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.17] - 2026-04-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Two-agent chat system** — AgentPanel now supports Partner Service Agent (booking recovery, default) and Engine Virtual Agent (case triage with handoff). Agents swap via `npm run agent:partner` / `agent:engine` which edits the source directly for instant HMR.
|
|
12
|
+
- **Structured card renderer for chat** — Messages with headers (OPEN CASES SUMMARY, CANCELED BOOKING, PROMOTION LIVE) render as formatted cards with priority dots, key-value pairs, and color-coded sections instead of plain text.
|
|
13
|
+
- **Improved chat readability** — Replaced CSS variable colors with high-contrast solid colors. Warm parchment backgrounds, dark text, visible borders. Bumped all font sizes +1px.
|
|
14
|
+
- **Cascading skeleton loading** — Tab switch skeletons now fade in with randomized stagger delays per element instead of uniform pulse. Each element has a unique random delay for natural feel.
|
|
15
|
+
- **Streamlined promo flow** — Removed ENGINE NETWORK PROMOTION card; upsell pitch now follows directly after CANCELED BOOKING card in a single conversational message.
|
|
16
|
+
- **Demo state manager** — `demo.sh` now tracks AgentPanel.tsx. Added `demo:d360` npm shortcut to fast-forward to Data 360 tab state.
|
|
17
|
+
- **Data 360 demo state** — Pre-built demo version with Data 360 booking segmentation tab, saveable/restorable via `demo:d360` / `demo:reset`.
|
|
18
|
+
|
|
8
19
|
## [2.3.16] - 2026-04-14
|
|
9
20
|
|
|
10
21
|
### Changed
|
|
@@ -4,6 +4,7 @@ import { SparklesIcon } from "@heroicons/react/24/solid";
|
|
|
4
4
|
import {
|
|
5
5
|
PaperAirplaneIcon,
|
|
6
6
|
ChevronDownIcon,
|
|
7
|
+
ArrowPathIcon,
|
|
7
8
|
} from "@heroicons/react/24/outline";
|
|
8
9
|
|
|
9
10
|
interface Message {
|
|
@@ -11,6 +12,7 @@ interface Message {
|
|
|
11
12
|
role: "user" | "agent";
|
|
12
13
|
text: string;
|
|
13
14
|
timestamp: string;
|
|
15
|
+
agentName?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
interface ScriptStep {
|
|
@@ -18,63 +20,102 @@ interface ScriptStep {
|
|
|
18
20
|
text: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
// --- Auto-play greeting ---
|
|
23
|
-
{
|
|
24
|
-
role: "agent",
|
|
25
|
-
text: "Hi Jamie! 👋 I'm your Partner Hub assistant. How can I help you today?",
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
// --- Prompt 1: Ask about the canceled booking ---
|
|
29
|
-
{
|
|
30
|
-
role: "user",
|
|
31
|
-
text: "I just got an alert that a block booking was canceled at our Austin Convention Center for next week. What happened?",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
role: "agent",
|
|
35
|
-
text: "Let me pull up that reservation for you...",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
role: "agent",
|
|
39
|
-
text: "🔴 CANCELED BOOKING\n\n🏨 Summit Austin Convention Center\n🏢 Client: TechCorp Inc.\n\n📋 Details:\n• Original block: 40 rooms\n• Dates: May 19 – 21 (3 nights)\n• Room rate: $200/night\n• Lost revenue: $24,000\n\n📅 Canceled: Apr 11, 2026\n⚠️ Status: Within penalty window",
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
role: "agent",
|
|
43
|
-
text: "TechCorp canceled their 40-room block for May 19–21. The reason given was that their annual conference is moving to a virtual format this year. This was within the attrition penalty window, so a penalty may apply — but that still leaves 40 rooms open for next week.",
|
|
44
|
-
},
|
|
23
|
+
type AgentId = "service" | "vee";
|
|
45
24
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
role: "agent",
|
|
53
|
-
text: "Actually, yes — I have an idea. Let me put together an option for you.",
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
role: "agent",
|
|
57
|
-
text: "⚡ ENGINE NETWORK PROMOTION\nFill your open rooms through Engine's network\n\n━━━━━━━━━━━━━━━━━━━━━━━━━\nSummit Austin Convention Center\n40 rooms · May 19–21 · 3 nights\n━━━━━━━━━━━━━━━━━━━━━━━━━\n\n💰 YOUR RATE\n$159/night (20% off rack rate)\n\n📡 ENGINE PROMOTES TO\n2,400+ corporate travel managers\n\n📉 COMMISSION\n12% standard → 8% promotional\nYou save $1,526 vs. standard bookings\n\n━━━━━━━━━━━━━━━━━━━━━━━━━\n📈 PROJECTED OUTCOME\n• Est. fill: 28 rooms (70%)\n• Revenue: $13,356\n• Net after commission: $12,287\n━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
role: "agent",
|
|
61
|
-
text: "Here's the play: we run a targeted promotion across Engine's corporate travel network — 2,400+ travel managers get notified about discounted rates at Summit Austin for May 19–21.\n\nWe'd list the rooms at $159/night, and your commission drops from 12% to 8% for promo bookings. On a projected 70% fill, that's $12,287 net — versus $0 if the rooms sit empty.\n\nWant to launch it?",
|
|
62
|
-
},
|
|
25
|
+
interface AgentConfig {
|
|
26
|
+
name: string;
|
|
27
|
+
subtitle: string;
|
|
28
|
+
script: ScriptStep[];
|
|
29
|
+
standaloneGreeting?: string;
|
|
30
|
+
}
|
|
63
31
|
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
32
|
+
const AGENTS: Record<AgentId, AgentConfig> = {
|
|
33
|
+
service: {
|
|
34
|
+
name: "Partner Service Agent",
|
|
35
|
+
subtitle: "Agentforce - Online",
|
|
36
|
+
script: [
|
|
37
|
+
{
|
|
38
|
+
role: "agent",
|
|
39
|
+
text: "Hi Jamie! I'm your Partner Service Agent. I can help you triage cases, check property performance, or review billing items. What would you like to look at?",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
role: "user",
|
|
43
|
+
text: "I have a few open cases. Can you give me a quick summary of what needs attention?",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
role: "agent",
|
|
47
|
+
text: "Sure. Let me pull your open cases across all properties...",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
role: "agent",
|
|
51
|
+
text: "OPEN CASES SUMMARY\n\n7 active cases across 4 properties\n\nURGENT (2)\n- CS-1039: Room cleanliness, 3rd occurrence (Chicago Downtown, Escalated)\n- CS-1034: Penalty calculation review (Austin Conv. Ctr, Escalated)\n\nNEEDS REVIEW (2)\n- CS-1040: Missing resale credit ATR-00001 (Austin, At Risk SLA)\n- CS-1037: Commission rate discrepancy (Midtown NYC)\n\nIN PROGRESS (2)\n- CS-1041: HVAC failure, conference room B (SF Bay)\n- CS-1035: Pool heater malfunction (SF Bay)\n\nLOW PRIORITY (1)\n- CS-1038: VIP late checkout request (SF Bay)",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
role: "agent",
|
|
55
|
+
text: "Your two escalated cases need the most attention. CS-1039 is a repeat cleanliness complaint at Chicago Downtown, now on its 3rd occurrence. CS-1034 is the penalty calculation dispute at Austin. Would you like me to dig into either of those, or is there something else I can help with?",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
role: "user",
|
|
59
|
+
text: "Actually, I just got a cancellation alert for Austin. Can someone help me figure out what to do about 40 empty rooms?",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
role: "agent",
|
|
63
|
+
text: "That sounds like it needs our revenue recovery specialist. Let me connect you with Vee, the Engine Virtual Agent. She handles booking cancellations and can set up promotions to fill those rooms. One moment...",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
role: "agent",
|
|
67
|
+
text: "HANDOFF",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
72
70
|
},
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
vee: {
|
|
72
|
+
name: "Vee, Engine Virtual Agent",
|
|
73
|
+
subtitle: "Agentforce - Online",
|
|
74
|
+
standaloneGreeting: "Hi Jamie! I'm Vee, your Engine Virtual Agent. I handle bookings, cancellations, and revenue opportunities across your properties. How can I help?",
|
|
75
|
+
script: [
|
|
76
|
+
{
|
|
77
|
+
role: "agent",
|
|
78
|
+
text: "Hi Jamie, Vee here. I've been briefed on the situation: a 40-room cancellation at Austin Convention Center. Let me pull up the details.",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
role: "agent",
|
|
82
|
+
text: "CANCELED BOOKING\n\nSummit Austin Convention Center\nClient: TechCorp Inc.\n\nDetails:\n- Original block: 40 rooms\n- Dates: May 19-21 (3 nights)\n- Room rate: $200/night\n- Lost revenue: $24,000\n\nCanceled: Apr 14, 2026\nStatus: Within penalty window",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: "agent",
|
|
86
|
+
text: "TechCorp canceled their 40-room block for May 19-21. The reason given was that their annual conference is moving to a virtual format this year. This was within the attrition penalty window, so a penalty may apply, but that still leaves 40 rooms open for next week.",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
role: "user",
|
|
90
|
+
text: "That's a lot of empty rooms. Is there anything we can do to fill them on short notice?",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
role: "agent",
|
|
94
|
+
text: "Actually, yes. I have an idea. Let me put together an option for you.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
role: "agent",
|
|
98
|
+
text: "ENGINE NETWORK PROMOTION\nFill your open rooms through Engine's network\n\n---\nSummit Austin Convention Center\n40 rooms, May 19-21, 3 nights\n---\n\nYOUR RATE\n$159/night (20% off rack rate)\n\nENGINE PROMOTES TO\n2,400+ corporate travel managers\n\nCOMMISSION\n12% standard > 8% promotional\nYou save $1,526 vs. standard bookings\n\n---\nPROJECTED OUTCOME\n- Est. fill: 28 rooms (70%)\n- Revenue: $13,356\n- Net after commission: $12,287\n---",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
role: "agent",
|
|
102
|
+
text: "Here's the play: we run a targeted promotion across Engine's corporate travel network. 2,400+ travel managers get notified about discounted rates at Summit Austin for May 19-21.\n\nWe'd list the rooms at $159/night, and your commission drops from 12% to 8% for promo bookings. On a projected 70% fill, that's $12,287 net versus $0 if the rooms sit empty.\n\nWant to launch it?",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
role: "user",
|
|
106
|
+
text: "That makes sense. Empty rooms earn nothing. Let's launch it.",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
role: "agent",
|
|
110
|
+
text: "Launching your promotion now...",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
role: "agent",
|
|
114
|
+
text: "PROMOTION LIVE\n\nPROMO-SUM-0519\nSummit Austin Convention Center\nMay 19-21 (3 nights)\nRate: $159/night\nCommission: 8% (promotional)\nDistribution: Engine corporate travel network\n\nYour promotion is live and going out to 2,400+ travel managers now. I'll send you a fill-rate update in 48 hours.",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
76
117
|
},
|
|
77
|
-
|
|
118
|
+
};
|
|
78
119
|
|
|
79
120
|
function typingDelay(text: string): number {
|
|
80
121
|
const base = 900;
|
|
@@ -106,16 +147,29 @@ function TypingIndicator() {
|
|
|
106
147
|
);
|
|
107
148
|
}
|
|
108
149
|
|
|
150
|
+
function HandoffBanner({ fromAgent, toAgent }: { fromAgent: string; toAgent: string }) {
|
|
151
|
+
return (
|
|
152
|
+
<div className="px-4 py-3">
|
|
153
|
+
<div className="flex items-center gap-3 bg-[var(--color-dash-accent)]/5 border border-[var(--color-dash-accent)]/20 rounded-xl px-4 py-3">
|
|
154
|
+
<ArrowPathIcon className="h-4 w-4 text-[var(--color-dash-accent)] flex-shrink-0" />
|
|
155
|
+
<p className="text-xs text-[var(--color-dash-text)] dark:text-white">
|
|
156
|
+
<span className="font-semibold">{fromAgent}</span> transferred you to <span className="font-semibold">{toAgent}</span>
|
|
157
|
+
</p>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
109
163
|
function AgentMessage({ text, timestamp }: { text: string; timestamp: string }) {
|
|
110
164
|
return (
|
|
111
165
|
<div className="px-4 py-1.5">
|
|
112
|
-
<div className="flex items-
|
|
113
|
-
<div className="flex-shrink-0 h-7 w-7 rounded-full bg-[var(--color-dash-dark)] flex items-center justify-center">
|
|
166
|
+
<div className="flex items-start gap-3">
|
|
167
|
+
<div className="flex-shrink-0 h-7 w-7 rounded-full bg-[var(--color-dash-dark)] flex items-center justify-center mt-0.5">
|
|
114
168
|
<SparklesIcon className="h-3.5 w-3.5 text-white" />
|
|
115
169
|
</div>
|
|
116
|
-
<div className="max-w-[
|
|
170
|
+
<div className="max-w-[85%]">
|
|
117
171
|
<div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/20 rounded-2xl rounded-bl-md px-4 py-3 border border-[var(--color-dash-border)]/30 dark:border-[var(--color-dash-muted)]/30">
|
|
118
|
-
<p className="text-
|
|
172
|
+
<p className="text-[13px] text-[var(--color-dash-text)] dark:text-white leading-relaxed whitespace-pre-wrap">
|
|
119
173
|
{text}
|
|
120
174
|
</p>
|
|
121
175
|
</div>
|
|
@@ -131,7 +185,7 @@ function UserMessage({ text, timestamp }: { text: string; timestamp: string }) {
|
|
|
131
185
|
<div className="px-4 py-1.5">
|
|
132
186
|
<div className="flex items-end justify-end gap-3">
|
|
133
187
|
<div className="max-w-[80%]">
|
|
134
|
-
<div className="bg-[var(--color-dash-
|
|
188
|
+
<div className="bg-[var(--color-dash-dark)] rounded-2xl rounded-br-md px-4 py-3">
|
|
135
189
|
<p className="text-sm text-white leading-relaxed">{text}</p>
|
|
136
190
|
</div>
|
|
137
191
|
<p className="text-[10px] text-[var(--color-dash-label)] mt-1 text-right mr-1">{timestamp}</p>
|
|
@@ -146,12 +200,35 @@ export default function AgentPanel() {
|
|
|
146
200
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
147
201
|
const [isTyping, setIsTyping] = useState(false);
|
|
148
202
|
const [inputValue, setInputValue] = useState("");
|
|
203
|
+
const [startAgent] = useState<AgentId>(() => {
|
|
204
|
+
const params = new URLSearchParams(window.location.search);
|
|
205
|
+
const fromUrl = params.get("agent") as AgentId | null;
|
|
206
|
+
if (fromUrl && fromUrl in AGENTS) return fromUrl;
|
|
207
|
+
return "service";
|
|
208
|
+
});
|
|
209
|
+
const [activeAgent, setActiveAgent] = useState<AgentId>(startAgent);
|
|
210
|
+
const wasHandoff = useRef(false);
|
|
149
211
|
const [scriptIndex, setScriptIndex] = useState(0);
|
|
150
212
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
213
|
+
const [handoffs, setHandoffs] = useState<Array<{ afterMessageId: string; from: string; to: string }>>([]);
|
|
151
214
|
const hasStartedRef = useRef(false);
|
|
152
215
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
153
216
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
154
217
|
|
|
218
|
+
const agent = AGENTS[activeAgent];
|
|
219
|
+
|
|
220
|
+
const script = React.useMemo(() => {
|
|
221
|
+
const base = agent.script;
|
|
222
|
+
if (!wasHandoff.current && agent.standaloneGreeting) {
|
|
223
|
+
return [
|
|
224
|
+
{ role: "agent" as const, text: agent.standaloneGreeting },
|
|
225
|
+
{ role: "user" as const, text: "I just got an alert that a block booking was canceled at our Austin Convention Center for next week. What happened?" },
|
|
226
|
+
...base.slice(1),
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
return base;
|
|
230
|
+
}, [activeAgent, agent]);
|
|
231
|
+
|
|
155
232
|
useEffect(() => {
|
|
156
233
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
157
234
|
}, [messages, isTyping]);
|
|
@@ -162,7 +239,6 @@ export default function AgentPanel() {
|
|
|
162
239
|
}
|
|
163
240
|
}, [isOpen]);
|
|
164
241
|
|
|
165
|
-
// Auto-play the first agent message when panel opens
|
|
166
242
|
useEffect(() => {
|
|
167
243
|
if (isOpen && !hasStartedRef.current && scriptIndex === 0) {
|
|
168
244
|
hasStartedRef.current = true;
|
|
@@ -170,12 +246,67 @@ export default function AgentPanel() {
|
|
|
170
246
|
}
|
|
171
247
|
}, [isOpen]);
|
|
172
248
|
|
|
249
|
+
const doHandoff = useCallback(async (lastMsgId: string) => {
|
|
250
|
+
const fromName = AGENTS[activeAgent].name;
|
|
251
|
+
const toName = AGENTS.vee.name;
|
|
252
|
+
|
|
253
|
+
setHandoffs(prev => [...prev, { afterMessageId: lastMsgId, from: fromName, to: toName }]);
|
|
254
|
+
|
|
255
|
+
await delay(800);
|
|
256
|
+
wasHandoff.current = true;
|
|
257
|
+
setActiveAgent("vee");
|
|
258
|
+
setScriptIndex(0);
|
|
259
|
+
|
|
260
|
+
await delay(600);
|
|
261
|
+
await playAgentChainForScript(AGENTS.vee.script, 0);
|
|
262
|
+
}, [activeAgent]);
|
|
263
|
+
|
|
264
|
+
async function playAgentChainForScript(scr: ScriptStep[], startIndex: number) {
|
|
265
|
+
let idx = startIndex;
|
|
266
|
+
setIsAnimating(true);
|
|
267
|
+
while (idx < scr.length && scr[idx].role === "agent") {
|
|
268
|
+
const step = scr[idx];
|
|
269
|
+
|
|
270
|
+
if (step.text === "HANDOFF") {
|
|
271
|
+
idx++;
|
|
272
|
+
setScriptIndex(idx);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
setIsTyping(true);
|
|
277
|
+
await delay(typingDelay(step.text));
|
|
278
|
+
setIsTyping(false);
|
|
279
|
+
|
|
280
|
+
const msg: Message = {
|
|
281
|
+
id: `msg-${Date.now()}-${idx}`,
|
|
282
|
+
role: "agent",
|
|
283
|
+
text: step.text,
|
|
284
|
+
timestamp: now(),
|
|
285
|
+
};
|
|
286
|
+
setMessages((prev) => [...prev, msg]);
|
|
287
|
+
setScriptIndex(idx + 1);
|
|
288
|
+
idx++;
|
|
289
|
+
if (idx < scr.length && scr[idx]?.role === "agent") {
|
|
290
|
+
await delay(500);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
setIsAnimating(false);
|
|
294
|
+
}
|
|
295
|
+
|
|
173
296
|
const playNextStep = useCallback(async () => {
|
|
174
|
-
if (scriptIndex >=
|
|
297
|
+
if (scriptIndex >= script.length || isAnimating) return;
|
|
175
298
|
|
|
176
|
-
const step =
|
|
299
|
+
const step = script[scriptIndex];
|
|
177
300
|
setIsAnimating(true);
|
|
178
301
|
|
|
302
|
+
if (step.text === "HANDOFF") {
|
|
303
|
+
const lastMsg = messages[messages.length - 1];
|
|
304
|
+
setScriptIndex(scriptIndex + 1);
|
|
305
|
+
setIsAnimating(false);
|
|
306
|
+
await doHandoff(lastMsg?.id || "");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
179
310
|
if (step.role === "agent") {
|
|
180
311
|
setIsTyping(true);
|
|
181
312
|
await delay(typingDelay(step.text));
|
|
@@ -192,45 +323,50 @@ export default function AgentPanel() {
|
|
|
192
323
|
setScriptIndex((prev) => prev + 1);
|
|
193
324
|
setIsAnimating(false);
|
|
194
325
|
|
|
195
|
-
// If we just played a user message, auto-play all following agent messages
|
|
196
326
|
const nextIndex = scriptIndex + 1;
|
|
197
|
-
if (step.role === "user" && nextIndex <
|
|
327
|
+
if (step.role === "user" && nextIndex < script.length && script[nextIndex].role === "agent") {
|
|
198
328
|
await delay(400);
|
|
199
|
-
await playAgentChain(nextIndex);
|
|
200
|
-
}
|
|
201
|
-
}, [scriptIndex, isAnimating]);
|
|
202
329
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
330
|
+
if (script[nextIndex].text === "HANDOFF") {
|
|
331
|
+
// Play until handoff, then the chain message before it triggers handoff
|
|
332
|
+
let idx = nextIndex;
|
|
333
|
+
setIsAnimating(true);
|
|
334
|
+
while (idx < script.length && script[idx].role === "agent") {
|
|
335
|
+
const s = script[idx];
|
|
336
|
+
if (s.text === "HANDOFF") {
|
|
337
|
+
setScriptIndex(idx + 1);
|
|
338
|
+
setIsAnimating(false);
|
|
339
|
+
await doHandoff(messages[messages.length - 1]?.id || msg.id);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
setIsTyping(true);
|
|
343
|
+
await delay(typingDelay(s.text));
|
|
344
|
+
setIsTyping(false);
|
|
345
|
+
const m: Message = {
|
|
346
|
+
id: `msg-${Date.now()}-${idx}`,
|
|
347
|
+
role: "agent",
|
|
348
|
+
text: s.text,
|
|
349
|
+
timestamp: now(),
|
|
350
|
+
};
|
|
351
|
+
setMessages((prev) => [...prev, m]);
|
|
352
|
+
setScriptIndex(idx + 1);
|
|
353
|
+
idx++;
|
|
354
|
+
if (idx < script.length && script[idx]?.role === "agent") {
|
|
355
|
+
await delay(500);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
setIsAnimating(false);
|
|
359
|
+
} else {
|
|
360
|
+
await playAgentChainForScript(script, nextIndex);
|
|
223
361
|
}
|
|
224
362
|
}
|
|
225
|
-
|
|
226
|
-
}
|
|
363
|
+
}, [scriptIndex, isAnimating, script, messages, doHandoff]);
|
|
227
364
|
|
|
228
|
-
// Right arrow key stages the next user message into the input
|
|
229
365
|
useEffect(() => {
|
|
230
366
|
function onKey(e: KeyboardEvent) {
|
|
231
|
-
if (e.key === "ArrowRight" && isOpen && !isAnimating && scriptIndex <
|
|
367
|
+
if (e.key === "ArrowRight" && isOpen && !isAnimating && scriptIndex < script.length) {
|
|
232
368
|
e.preventDefault();
|
|
233
|
-
const next =
|
|
369
|
+
const next = script[scriptIndex];
|
|
234
370
|
if (next.role === "user") {
|
|
235
371
|
setInputValue(next.text);
|
|
236
372
|
inputRef.current?.focus();
|
|
@@ -239,15 +375,14 @@ export default function AgentPanel() {
|
|
|
239
375
|
}
|
|
240
376
|
document.addEventListener("keydown", onKey);
|
|
241
377
|
return () => document.removeEventListener("keydown", onKey);
|
|
242
|
-
}, [isOpen, scriptIndex, isAnimating]);
|
|
378
|
+
}, [isOpen, scriptIndex, isAnimating, script]);
|
|
243
379
|
|
|
244
380
|
function handleSend() {
|
|
245
381
|
const text = inputValue.trim();
|
|
246
382
|
if (!text) return;
|
|
247
383
|
setInputValue("");
|
|
248
384
|
|
|
249
|
-
|
|
250
|
-
if (scriptIndex < SCRIPT.length && SCRIPT[scriptIndex].role === "user" && text === SCRIPT[scriptIndex].text) {
|
|
385
|
+
if (scriptIndex < script.length && script[scriptIndex].role === "user" && text === script[scriptIndex].text) {
|
|
251
386
|
playNextStep();
|
|
252
387
|
return;
|
|
253
388
|
}
|
|
@@ -266,15 +401,13 @@ export default function AgentPanel() {
|
|
|
266
401
|
const agentMsg: Message = {
|
|
267
402
|
id: `a-${Date.now()}`,
|
|
268
403
|
role: "agent",
|
|
269
|
-
text: "I'm looking into that for you
|
|
404
|
+
text: "I'm looking into that for you. One moment please.",
|
|
270
405
|
timestamp: now(),
|
|
271
406
|
};
|
|
272
407
|
setMessages((prev) => [...prev, agentMsg]);
|
|
273
408
|
}, 1500);
|
|
274
409
|
}
|
|
275
410
|
|
|
276
|
-
const scriptDone = scriptIndex >= SCRIPT.length;
|
|
277
|
-
|
|
278
411
|
const panel = isOpen
|
|
279
412
|
? createPortal(
|
|
280
413
|
<>
|
|
@@ -283,7 +416,7 @@ export default function AgentPanel() {
|
|
|
283
416
|
onClick={() => setIsOpen(false)}
|
|
284
417
|
/>
|
|
285
418
|
<div
|
|
286
|
-
className="fixed z-[9999] bottom-6 right-6 w-[
|
|
419
|
+
className="fixed z-[9999] bottom-6 right-6 w-[460px] h-[720px] flex flex-col rounded-2xl shadow-2xl border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 overflow-hidden"
|
|
287
420
|
style={{
|
|
288
421
|
animation: "agent-panel-in 0.25s cubic-bezier(0.16, 1, 0.3, 1)",
|
|
289
422
|
}}
|
|
@@ -296,9 +429,9 @@ export default function AgentPanel() {
|
|
|
296
429
|
</div>
|
|
297
430
|
<div>
|
|
298
431
|
<h3 className="text-sm font-semibold text-white">
|
|
299
|
-
|
|
432
|
+
{agent.name}
|
|
300
433
|
</h3>
|
|
301
|
-
<p className="text-xs text-white/60">
|
|
434
|
+
<p className="text-xs text-white/60">{agent.subtitle}</p>
|
|
302
435
|
</div>
|
|
303
436
|
</div>
|
|
304
437
|
<button
|
|
@@ -317,17 +450,23 @@ export default function AgentPanel() {
|
|
|
317
450
|
<SparklesIcon className="h-6 w-6 text-[var(--color-dash-accent)]" />
|
|
318
451
|
</div>
|
|
319
452
|
<p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
|
|
320
|
-
|
|
453
|
+
Connecting...
|
|
321
454
|
</p>
|
|
322
455
|
</div>
|
|
323
456
|
)}
|
|
324
|
-
{messages.map((msg) =>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
457
|
+
{messages.map((msg) => {
|
|
458
|
+
const handoff = handoffs.find(h => h.afterMessageId === msg.id);
|
|
459
|
+
return (
|
|
460
|
+
<React.Fragment key={msg.id}>
|
|
461
|
+
{msg.role === "agent" ? (
|
|
462
|
+
<AgentMessage text={msg.text} timestamp={msg.timestamp} />
|
|
463
|
+
) : (
|
|
464
|
+
<UserMessage text={msg.text} timestamp={msg.timestamp} />
|
|
465
|
+
)}
|
|
466
|
+
{handoff && <HandoffBanner fromAgent={handoff.from} toAgent={handoff.to} />}
|
|
467
|
+
</React.Fragment>
|
|
468
|
+
);
|
|
469
|
+
})}
|
|
331
470
|
{isTyping && <TypingIndicator />}
|
|
332
471
|
<div ref={messagesEndRef} />
|
|
333
472
|
</div>
|