@schandlergarcia/sf-web-components 2.3.17 → 2.5.0

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.
Files changed (94) hide show
  1. package/.a4drules/skills/command-center-builder/SKILL.md +3 -2
  2. package/.a4drules/skills/component-library/SKILL.md +50 -4
  3. package/.a4drules/skills/component-library/card-components.md +88 -0
  4. package/.a4drules/skills/component-library/when-to-use.md +1 -0
  5. package/CHANGELOG.md +40 -0
  6. package/CLAUDE.md +12 -13
  7. package/README.md +0 -15
  8. package/dist/components/library/cards/KanbanBoard.js +313 -0
  9. package/dist/components/library/cards/KanbanBoard.js.map +1 -0
  10. package/dist/components/library/index.js +60 -57
  11. package/dist/components/library/index.js.map +1 -1
  12. package/dist/components/workspace/ComponentRegistry.js +5 -2
  13. package/dist/components/workspace/ComponentRegistry.js.map +1 -1
  14. package/dist/index.js +84 -82
  15. package/dist/index.js.map +1 -1
  16. package/dist/styles/global.css +44 -57
  17. package/package.json +7 -2
  18. package/scripts/apply-brand.mjs +47 -30
  19. package/scripts/postinstall.mjs +1 -11
  20. package/src/components/library/cards/KanbanBoard.jsx +507 -0
  21. package/src/components/library/index.jsx +1 -0
  22. package/src/styles/global.css +44 -57
  23. package/brands/engine/PARTNER_HUB_PRD.md +0 -584
  24. package/brands/engine/agentApiConfig.ts +0 -36
  25. package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
  26. package/brands/engine/app/api/graphqlClient.ts +0 -25
  27. package/brands/engine/app/api/partnerQueries.ts +0 -212
  28. package/brands/engine/app/appLayout.tsx +0 -5
  29. package/brands/engine/app/components/AgentPanel.tsx +0 -541
  30. package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
  31. package/brands/engine/app/components/Data360Widget.tsx +0 -301
  32. package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
  33. package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
  34. package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
  35. package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
  36. package/brands/engine/app/config/agentApi.ts +0 -36
  37. package/brands/engine/app/data/partner-hub-sample-data.js +0 -297
  38. package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  39. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  40. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  41. package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  42. package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  43. package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
  44. package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
  45. package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
  46. package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
  47. package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
  48. package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
  49. package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
  50. package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
  51. package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
  52. package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
  53. package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  54. package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
  55. package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  56. package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
  57. package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
  58. package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
  59. package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
  60. package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
  61. package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
  62. package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
  63. package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
  64. package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  65. package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
  66. package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
  67. package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
  68. package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
  69. package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
  70. package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
  71. package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
  72. package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
  73. package/brands/engine/app/navigationMenu.tsx +0 -80
  74. package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
  75. package/brands/engine/app/pages/AccountSearch.tsx +0 -305
  76. package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
  77. package/brands/engine/app/pages/DataTest.tsx +0 -78
  78. package/brands/engine/app/pages/Home.tsx +0 -5
  79. package/brands/engine/app/pages/NotFound.tsx +0 -19
  80. package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2760
  81. package/brands/engine/app/pages/Search.tsx +0 -13
  82. package/brands/engine/app/router-utils.tsx +0 -35
  83. package/brands/engine/app/routes.tsx +0 -39
  84. package/brands/engine/app/styles/global.css +0 -269
  85. package/brands/engine/brand.css +0 -40
  86. package/brands/engine/engine-command-center-prd.md +0 -575
  87. package/brands/engine/engine-live-data.js +0 -135
  88. package/brands/engine/engine-sample-data.js +0 -378
  89. package/brands/engine/engine_logo.png +0 -0
  90. package/brands/engine/global.css +0 -269
  91. package/brands/engine/partner-hub-sample-data.js +0 -281
  92. package/brands/engine/schema.graphql +0 -292
  93. package/brands/engine/useEngineLiveData.ts +0 -49
  94. package/brands/engine/useEvaAgent.ts +0 -288
@@ -1,541 +0,0 @@
1
- import React, { useState, useRef, useEffect, useCallback } from "react";
2
- import { createPortal } from "react-dom";
3
- import { SparklesIcon } from "@heroicons/react/24/solid";
4
- import {
5
- PaperAirplaneIcon,
6
- ChevronDownIcon,
7
- ArrowPathIcon,
8
- } from "@heroicons/react/24/outline";
9
-
10
- interface Message {
11
- id: string;
12
- role: "user" | "agent";
13
- text: string;
14
- timestamp: string;
15
- agentName?: string;
16
- }
17
-
18
- interface ScriptStep {
19
- role: "user" | "agent";
20
- text: string;
21
- }
22
-
23
- type AgentId = "service" | "vee";
24
-
25
- interface AgentConfig {
26
- name: string;
27
- subtitle: string;
28
- script: ScriptStep[];
29
- standaloneGreeting?: string;
30
- }
31
-
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
- ],
70
- },
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
- ],
117
- },
118
- };
119
-
120
- function typingDelay(text: string): number {
121
- const base = 900;
122
- const perChar = 10;
123
- const jitter = Math.random() * 500;
124
- return Math.min(base + text.length * perChar + jitter, 5500);
125
- }
126
-
127
- function now() {
128
- return new Date().toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
129
- }
130
-
131
- function TypingIndicator() {
132
- return (
133
- <div className="flex items-center gap-1 px-4 py-1.5">
134
- <div className="flex items-end gap-3">
135
- <div className="flex-shrink-0 h-7 w-7 rounded-full bg-[var(--color-dash-dark)] flex items-center justify-center">
136
- <SparklesIcon className="h-3.5 w-3.5 text-white" />
137
- </div>
138
- <div className="bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/20 rounded-2xl rounded-bl-md px-4 py-3">
139
- <div className="flex items-center gap-1.5">
140
- <span className="h-2 w-2 rounded-full bg-[var(--color-dash-label)] animate-bounce" style={{ animationDelay: "0ms" }} />
141
- <span className="h-2 w-2 rounded-full bg-[var(--color-dash-label)] animate-bounce" style={{ animationDelay: "150ms" }} />
142
- <span className="h-2 w-2 rounded-full bg-[var(--color-dash-label)] animate-bounce" style={{ animationDelay: "300ms" }} />
143
- </div>
144
- </div>
145
- </div>
146
- </div>
147
- );
148
- }
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
-
163
- function AgentMessage({ text, timestamp }: { text: string; timestamp: string }) {
164
- return (
165
- <div className="px-4 py-1.5">
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">
168
- <SparklesIcon className="h-3.5 w-3.5 text-white" />
169
- </div>
170
- <div className="max-w-[85%]">
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">
172
- <p className="text-[13px] text-[var(--color-dash-text)] dark:text-white leading-relaxed whitespace-pre-wrap">
173
- {text}
174
- </p>
175
- </div>
176
- <p className="text-[10px] text-[var(--color-dash-label)] mt-1 ml-1">{timestamp}</p>
177
- </div>
178
- </div>
179
- </div>
180
- );
181
- }
182
-
183
- function UserMessage({ text, timestamp }: { text: string; timestamp: string }) {
184
- return (
185
- <div className="px-4 py-1.5">
186
- <div className="flex items-end justify-end gap-3">
187
- <div className="max-w-[80%]">
188
- <div className="bg-[var(--color-dash-dark)] rounded-2xl rounded-br-md px-4 py-3">
189
- <p className="text-sm text-white leading-relaxed">{text}</p>
190
- </div>
191
- <p className="text-[10px] text-[var(--color-dash-label)] mt-1 text-right mr-1">{timestamp}</p>
192
- </div>
193
- </div>
194
- </div>
195
- );
196
- }
197
-
198
- export default function AgentPanel() {
199
- const [isOpen, setIsOpen] = useState(false);
200
- const [messages, setMessages] = useState<Message[]>([]);
201
- const [isTyping, setIsTyping] = useState(false);
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);
211
- const [scriptIndex, setScriptIndex] = useState(0);
212
- const [isAnimating, setIsAnimating] = useState(false);
213
- const [handoffs, setHandoffs] = useState<Array<{ afterMessageId: string; from: string; to: string }>>([]);
214
- const hasStartedRef = useRef(false);
215
- const messagesEndRef = useRef<HTMLDivElement>(null);
216
- const inputRef = useRef<HTMLInputElement>(null);
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
-
232
- useEffect(() => {
233
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
234
- }, [messages, isTyping]);
235
-
236
- useEffect(() => {
237
- if (isOpen) {
238
- setTimeout(() => inputRef.current?.focus(), 300);
239
- }
240
- }, [isOpen]);
241
-
242
- useEffect(() => {
243
- if (isOpen && !hasStartedRef.current && scriptIndex === 0) {
244
- hasStartedRef.current = true;
245
- playNextStep();
246
- }
247
- }, [isOpen]);
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
-
296
- const playNextStep = useCallback(async () => {
297
- if (scriptIndex >= script.length || isAnimating) return;
298
-
299
- const step = script[scriptIndex];
300
- setIsAnimating(true);
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
-
310
- if (step.role === "agent") {
311
- setIsTyping(true);
312
- await delay(typingDelay(step.text));
313
- setIsTyping(false);
314
- }
315
-
316
- const msg: Message = {
317
- id: `msg-${Date.now()}`,
318
- role: step.role,
319
- text: step.text,
320
- timestamp: now(),
321
- };
322
- setMessages((prev) => [...prev, msg]);
323
- setScriptIndex((prev) => prev + 1);
324
- setIsAnimating(false);
325
-
326
- const nextIndex = scriptIndex + 1;
327
- if (step.role === "user" && nextIndex < script.length && script[nextIndex].role === "agent") {
328
- await delay(400);
329
-
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);
361
- }
362
- }
363
- }, [scriptIndex, isAnimating, script, messages, doHandoff]);
364
-
365
- useEffect(() => {
366
- function onKey(e: KeyboardEvent) {
367
- if (e.key === "ArrowRight" && isOpen && !isAnimating && scriptIndex < script.length) {
368
- e.preventDefault();
369
- const next = script[scriptIndex];
370
- if (next.role === "user") {
371
- setInputValue(next.text);
372
- inputRef.current?.focus();
373
- }
374
- }
375
- }
376
- document.addEventListener("keydown", onKey);
377
- return () => document.removeEventListener("keydown", onKey);
378
- }, [isOpen, scriptIndex, isAnimating, script]);
379
-
380
- function handleSend() {
381
- const text = inputValue.trim();
382
- if (!text) return;
383
- setInputValue("");
384
-
385
- if (scriptIndex < script.length && script[scriptIndex].role === "user" && text === script[scriptIndex].text) {
386
- playNextStep();
387
- return;
388
- }
389
-
390
- const userMsg: Message = {
391
- id: `u-${Date.now()}`,
392
- role: "user",
393
- text,
394
- timestamp: now(),
395
- };
396
- setMessages((prev) => [...prev, userMsg]);
397
-
398
- setIsTyping(true);
399
- setTimeout(() => {
400
- setIsTyping(false);
401
- const agentMsg: Message = {
402
- id: `a-${Date.now()}`,
403
- role: "agent",
404
- text: "I'm looking into that for you. One moment please.",
405
- timestamp: now(),
406
- };
407
- setMessages((prev) => [...prev, agentMsg]);
408
- }, 1500);
409
- }
410
-
411
- const panel = isOpen
412
- ? createPortal(
413
- <>
414
- <div
415
- className="fixed inset-0 z-[9998] bg-black/20 backdrop-blur-sm lg:hidden"
416
- onClick={() => setIsOpen(false)}
417
- />
418
- <div
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"
420
- style={{
421
- animation: "agent-panel-in 0.25s cubic-bezier(0.16, 1, 0.3, 1)",
422
- }}
423
- >
424
- {/* Header */}
425
- <div className="bg-[var(--color-dash-dark)] px-5 py-4 flex items-center justify-between flex-shrink-0">
426
- <div className="flex items-center gap-3">
427
- <div className="h-8 w-8 rounded-full bg-white/10 flex items-center justify-center">
428
- <SparklesIcon className="h-4 w-4 text-white" />
429
- </div>
430
- <div>
431
- <h3 className="text-sm font-semibold text-white">
432
- {agent.name}
433
- </h3>
434
- <p className="text-xs text-white/60">{agent.subtitle}</p>
435
- </div>
436
- </div>
437
- <button
438
- onClick={() => setIsOpen(false)}
439
- className="rounded-lg p-1.5 text-white/60 hover:text-white hover:bg-white/10 transition-colors"
440
- >
441
- <ChevronDownIcon className="h-5 w-5" />
442
- </button>
443
- </div>
444
-
445
- {/* Messages */}
446
- <div className="flex-1 overflow-y-auto bg-white dark:bg-[var(--color-dash-text)] py-4">
447
- {messages.length === 0 && !isTyping && (
448
- <div className="flex flex-col items-center justify-center h-full gap-3 px-6 text-center">
449
- <div className="h-12 w-12 rounded-full bg-[var(--color-dash-accent)]/10 flex items-center justify-center">
450
- <SparklesIcon className="h-6 w-6 text-[var(--color-dash-accent)]" />
451
- </div>
452
- <p className="text-sm text-[var(--color-dash-muted)] dark:text-[var(--color-dash-label)]">
453
- Connecting...
454
- </p>
455
- </div>
456
- )}
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
- })}
470
- {isTyping && <TypingIndicator />}
471
- <div ref={messagesEndRef} />
472
- </div>
473
-
474
- {/* Input */}
475
- <div className="flex-shrink-0 bg-white dark:bg-[var(--color-dash-text)] border-t border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 px-4 py-3">
476
- <form
477
- onSubmit={(e) => {
478
- e.preventDefault();
479
- handleSend();
480
- }}
481
- className="flex items-center gap-2"
482
- >
483
- <input
484
- ref={inputRef}
485
- type="text"
486
- value={inputValue}
487
- onChange={(e) => setInputValue(e.target.value)}
488
- placeholder="Type your message..."
489
- className="flex-1 bg-[var(--color-dash-surface)] dark:bg-[var(--color-dash-muted)]/10 border border-[var(--color-dash-label)]/20 dark:border-[var(--color-dash-muted)]/30 rounded-xl px-4 py-2.5 text-sm text-[var(--color-dash-text)] dark:text-white placeholder:text-[var(--color-dash-label)] focus:outline-none focus:ring-2 focus:ring-[var(--color-dash-accent)]/30 focus:border-[var(--color-dash-accent)] transition-all"
490
- />
491
- <button
492
- type="submit"
493
- disabled={!inputValue.trim()}
494
- className="flex-shrink-0 h-10 w-10 rounded-xl bg-[var(--color-dash-accent)] hover:brightness-110 disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center transition-all"
495
- >
496
- <PaperAirplaneIcon className="h-4 w-4 text-white" />
497
- </button>
498
- </form>
499
- <p className="text-[10px] text-[var(--color-dash-label)] text-center mt-2">
500
- Powered by Agentforce
501
- </p>
502
- </div>
503
- </div>
504
- </>,
505
- document.body
506
- )
507
- : null;
508
-
509
- return (
510
- <>
511
- {panel}
512
-
513
- {!isOpen && (
514
- <button
515
- onClick={() => setIsOpen(true)}
516
- className="fixed z-[9999] bottom-6 right-6 h-14 w-14 rounded-full bg-[var(--color-dash-dark)] hover:scale-110 shadow-xl hover:shadow-2xl flex items-center justify-center transition-all duration-200"
517
- style={{
518
- animation: "agent-bubble-in 0.3s cubic-bezier(0.16, 1, 0.3, 1)",
519
- }}
520
- >
521
- <SparklesIcon className="h-6 w-6 text-white" />
522
- </button>
523
- )}
524
-
525
- <style>{`
526
- @keyframes agent-panel-in {
527
- from { opacity: 0; transform: translateY(20px) scale(0.95); }
528
- to { opacity: 1; transform: translateY(0) scale(1); }
529
- }
530
- @keyframes agent-bubble-in {
531
- from { opacity: 0; transform: scale(0.5); }
532
- to { opacity: 1; transform: scale(1); }
533
- }
534
- `}</style>
535
- </>
536
- );
537
- }
538
-
539
- function delay(ms: number) {
540
- return new Promise((resolve) => setTimeout(resolve, ms));
541
- }