@schandlergarcia/sf-web-components 1.9.82 → 1.9.84
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/.a4drules/skills/command-center-builder/getting-started.md +17 -5
- package/CHANGELOG.md +18 -0
- package/data/agentApiConfig.ts +36 -0
- package/data/engine-command-center-prd.md +34 -13
- package/data/useEvaAgent.ts +288 -0
- package/package.json +1 -1
- package/scripts/postinstall.mjs +35 -0
- package/scripts/reset-command-center.sh +22 -22
- package/src/templates/config/vite.config.ts.template +18 -0
|
@@ -114,14 +114,17 @@ Only create `Flight__c.Weather_Impact__c` (PRD section 14a) AFTER the user confi
|
|
|
114
114
|
|
|
115
115
|
**How to work:** Make surgical edits to the existing `EngineDashboard.tsx`. Do NOT rewrite the file.
|
|
116
116
|
|
|
117
|
+
> **Important:** We do NOT use `AgentforceConversationClient` (the iframe widget). All agent communication goes through the REST-based Agent API via the `useEvaAgent` hook. The Vite dev server proxies the requests (`/sf-oauth` and `/sf-agent`) to avoid CORS issues.
|
|
118
|
+
|
|
117
119
|
**Files to read first (only these):**
|
|
118
120
|
1. `src/pages/EngineDashboard.tsx` — the current dashboard
|
|
119
121
|
|
|
120
122
|
**Edits to make (in order):**
|
|
121
123
|
|
|
122
|
-
1. **Add
|
|
124
|
+
1. **Add imports:**
|
|
123
125
|
```tsx
|
|
124
126
|
import { ChatBar } from "@/components/library";
|
|
127
|
+
import useEvaAgent from "@/hooks/useEvaAgent";
|
|
125
128
|
```
|
|
126
129
|
|
|
127
130
|
2. **Add chat suggestions constant** at module scope:
|
|
@@ -134,9 +137,15 @@ Only create `Flight__c.Weather_Impact__c` (PRD section 14a) AFTER the user confi
|
|
|
134
137
|
];
|
|
135
138
|
```
|
|
136
139
|
|
|
137
|
-
3. **Add
|
|
140
|
+
3. **Add agent hook + handler** inside the component:
|
|
138
141
|
```tsx
|
|
139
|
-
const
|
|
142
|
+
const { messages: evaMessages, isReady, isSending, connect, sendMessage } = useEvaAgent();
|
|
143
|
+
useEffect(() => { connect(); }, [connect]);
|
|
144
|
+
|
|
145
|
+
const handleChat = (text: string) => {
|
|
146
|
+
sendMessage(text);
|
|
147
|
+
return { text: `Looking into: "${text}"…` };
|
|
148
|
+
};
|
|
140
149
|
```
|
|
141
150
|
|
|
142
151
|
4. **Insert ChatBar JSX** between the map section and the data panels:
|
|
@@ -151,7 +160,7 @@ Only create `Flight__c.Weather_Impact__c` (PRD section 14a) AFTER the user confi
|
|
|
151
160
|
</div>
|
|
152
161
|
```
|
|
153
162
|
|
|
154
|
-
5.
|
|
163
|
+
5. Do **NOT** add `AgentforceConversationClient` — the `useEvaAgent` hook replaces it entirely.
|
|
155
164
|
|
|
156
165
|
**After adding Eva — STOP and suggest (do not create yet):**
|
|
157
166
|
|
|
@@ -166,6 +175,7 @@ Say: *"Now I'll write an Apex service class to publish disruption records to tha
|
|
|
166
175
|
Only create `TravelDisruptionEventService` (PRD section 14c) AFTER the user confirms.
|
|
167
176
|
|
|
168
177
|
**What NOT to build without asking:**
|
|
178
|
+
- Do NOT use `AgentforceConversationClient` — use `useEvaAgent` hook instead
|
|
169
179
|
- Do NOT create platform events or Apex classes without user confirmation
|
|
170
180
|
- Do NOT rewrite EngineDashboard.tsx from scratch
|
|
171
181
|
- Do NOT read library component source files
|
|
@@ -182,8 +192,10 @@ Only create `TravelDisruptionEventService` (PRD section 14c) AFTER the user conf
|
|
|
182
192
|
| Flip `dataStrategy.ts` to `false` | Skip | Edit | — |
|
|
183
193
|
| Custom field metadata | Skip | Suggest → wait | — |
|
|
184
194
|
| ChatBar (Eva) | Skip | Skip | Surgical edit |
|
|
185
|
-
|
|
|
195
|
+
| `useEvaAgent` hook (Agent API) | Skip | Skip | Surgical edit |
|
|
186
196
|
| Platform event | Skip | Skip | Suggest → wait |
|
|
187
197
|
| Apex service | Skip | Skip | Suggest → wait |
|
|
188
198
|
|
|
189
199
|
**Data mode control:** `ENABLE_SAMPLE_DATA_CACHE` in `src/lib/dataStrategy.ts` is the backend switch. `CommandCenter.tsx` reads this flag and passes `initialMode` to `DataModeProvider`. Phase 1 leaves it `true` (sample data). Phase 2 flips it to `false` (live data — different travelers, cities, and metrics). There is no UI toggle.
|
|
200
|
+
|
|
201
|
+
**Agent communication:** `useEvaAgent` hook in `src/hooks/useEvaAgent.ts` handles OAuth → session → messages → cleanup via the Agent REST API. Config is in `src/config/agentApi.ts`. Vite proxy rules (`/sf-oauth`, `/sf-agent`) in `vite.config.ts` forward requests to the Salesforce org and Agent API host.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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
|
+
## [1.9.84] - 2026-04-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Reset script no longer deletes custom fields** — `.field-meta.xml` files in `force-app/main/default/objects/` are now preserved across resets. Only Apex classes and platform events are cleaned.
|
|
12
|
+
- **Reset script preserves `TCC_TravelMetricsController`** — Apex class cleanup now skips permanent classes listed in `KEEP_CLASSES` array.
|
|
13
|
+
|
|
14
|
+
## [1.9.83] - 2026-04-07
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Agentforce Agent REST API integration** — Replaced iframe-based `AgentforceConversationClient` with direct API calls via the new `useEvaAgent` hook (`data/useEvaAgent.ts`). The hook handles the full lifecycle: OAuth client-credentials → create session → send messages → end session on unmount.
|
|
18
|
+
- **Agent API config** (`data/agentApiConfig.ts`) — Credentials and endpoint configuration for the Agentforce Agent API, installed to `src/config/agentApi.ts`.
|
|
19
|
+
- **Vite proxy rules** — Added `/sf-oauth` and `/sf-agent` proxy entries in `vite.config.ts` template so Agent API calls from the browser avoid CORS.
|
|
20
|
+
- **Postinstall** — Now copies `useEvaAgent.ts` to `src/hooks/` and `agentApiConfig.ts` to `src/config/agentApi.ts`.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **PRD section 9** — Replaced `AgentforceConversationClient` instructions with `useEvaAgent` hook usage and Agent API flow.
|
|
24
|
+
- **Build guide Phase 3** — Updated surgical edit instructions to use `useEvaAgent` instead of the iframe widget.
|
|
25
|
+
|
|
8
26
|
## [1.9.82] - 2026-04-06
|
|
9
27
|
|
|
10
28
|
### Fixed
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentforce Agent API Configuration
|
|
3
|
+
*
|
|
4
|
+
* These values connect the ChatBar to the real Agentforce agent (Eva)
|
|
5
|
+
* via the REST-based Agent API instead of the iframe-based
|
|
6
|
+
* AgentforceConversationClient.
|
|
7
|
+
*
|
|
8
|
+
* Flow: OAuth token → create session → send messages → end session
|
|
9
|
+
*
|
|
10
|
+
* All requests are proxied through the Vite dev server to avoid CORS.
|
|
11
|
+
* See vite.config.ts proxy rules:
|
|
12
|
+
* /sf-oauth/* → myDomainUrl (for OAuth token)
|
|
13
|
+
* /sf-agent/* → agentApiBaseUrl (for Agent API calls)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const AGENT_API_CONFIG = {
|
|
17
|
+
myDomainUrl: "https://tdx26-keynote-org-1com.my.salesforce.com",
|
|
18
|
+
|
|
19
|
+
clientId:
|
|
20
|
+
"3MVG9Gm6vbdjgMWSOIAuIN3VSB5Rju6PgYQ5rl1yH3bVTTg9E2as4.C61Q0cyT.zqv2vUWNaxrm.A7SW5o3t7",
|
|
21
|
+
clientSecret:
|
|
22
|
+
"9ADF795A183A6B074A2E4B4CB1748B8DF7090C74191AF1C190213B512A733E03",
|
|
23
|
+
|
|
24
|
+
agentId: "0Xxa5000000rQlxCAE",
|
|
25
|
+
|
|
26
|
+
agentApiBaseUrl: "https://api.salesforce.com",
|
|
27
|
+
|
|
28
|
+
bypassUser: true,
|
|
29
|
+
|
|
30
|
+
demoTraveler: {
|
|
31
|
+
contactId: "003a500000mj4TlAAI",
|
|
32
|
+
email: "sarah.chen@arcline.ai",
|
|
33
|
+
firstName: "Sarah",
|
|
34
|
+
lastName: "Chen",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -254,9 +254,13 @@ const spendChartData = useDataSource({
|
|
|
254
254
|
|
|
255
255
|
## 9. Eva — Agentforce Integration
|
|
256
256
|
|
|
257
|
-
Two
|
|
257
|
+
Two pieces work together: a **ChatBar** for user input and the **`useEvaAgent` hook** which talks to the Agentforce Agent REST API.
|
|
258
258
|
|
|
259
|
-
**
|
|
259
|
+
> **Important:** We do NOT use `AgentforceConversationClient` (the iframe widget). All agent communication goes through the REST-based Agent API via the `useEvaAgent` hook. The Vite dev server proxies the requests to avoid CORS issues.
|
|
260
|
+
|
|
261
|
+
### ChatBar
|
|
262
|
+
|
|
263
|
+
A command-palette strip below the hero map. NOT in the header. NOT a FAB or sliding panel.
|
|
260
264
|
|
|
261
265
|
```tsx
|
|
262
266
|
<div className="px-4 pt-4">
|
|
@@ -271,20 +275,35 @@ Two components work together:
|
|
|
271
275
|
|
|
272
276
|
Suggestions: "Storm warning in the Midwest — which travelers are affected?", "What's our severe weather rebooking policy?", "Notify Anna and Sofia about the Chicago delays", "Create a support case for Anna Johansson's flight"
|
|
273
277
|
|
|
274
|
-
|
|
278
|
+
### useEvaAgent Hook
|
|
275
279
|
|
|
276
|
-
|
|
280
|
+
The hook manages the full Agent API lifecycle — OAuth, session, messaging, cleanup.
|
|
277
281
|
|
|
278
282
|
```tsx
|
|
279
|
-
import
|
|
283
|
+
import useEvaAgent from "@/hooks/useEvaAgent";
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
+
// Inside the component:
|
|
286
|
+
const { messages, isReady, isSending, connect, sendMessage } = useEvaAgent();
|
|
287
|
+
|
|
288
|
+
// Connect on mount (or on first ChatBar focus):
|
|
289
|
+
useEffect(() => { connect(); }, [connect]);
|
|
290
|
+
|
|
291
|
+
// Wire to ChatBar:
|
|
292
|
+
const handleChat = (text: string) => {
|
|
293
|
+
sendMessage(text);
|
|
294
|
+
return { text: `Looking into: "${text}"…` };
|
|
295
|
+
};
|
|
285
296
|
```
|
|
286
297
|
|
|
287
|
-
The
|
|
298
|
+
The hook:
|
|
299
|
+
1. **Authenticates** via OAuth client-credentials (`POST /sf-oauth/services/oauth2/token`)
|
|
300
|
+
2. **Creates a session** (`POST /sf-agent/einstein/ai-agent/v1/agents/{agentId}/sessions`)
|
|
301
|
+
3. **Sends messages** (`POST /sf-agent/…/messages`) — returns agent response text
|
|
302
|
+
4. **Ends the session** (`DELETE /sf-agent/…`) on unmount
|
|
303
|
+
|
|
304
|
+
Config is in `src/config/agentApi.ts`. Proxy rules are in `vite.config.ts` (`/sf-oauth` and `/sf-agent`).
|
|
305
|
+
|
|
306
|
+
Define `CHAT_SUGGESTIONS` at module scope.
|
|
288
307
|
|
|
289
308
|
---
|
|
290
309
|
|
|
@@ -382,11 +401,13 @@ Build incrementally in 3 prompts. Each prompt builds on the previous result. The
|
|
|
382
401
|
|
|
383
402
|
1. Make **surgical edits** to the existing `EngineDashboard.tsx` — do NOT rewrite the entire file
|
|
384
403
|
2. Add `ChatBar` with suggestions from section 9, placed between map and data panels
|
|
385
|
-
3. Add `
|
|
386
|
-
4.
|
|
387
|
-
5. After
|
|
404
|
+
3. Add `useEvaAgent` hook — import from `@/hooks/useEvaAgent`, call `connect()` on mount, wire `sendMessage` to `handleChat`
|
|
405
|
+
4. Do NOT use `AgentforceConversationClient` (the iframe widget) — we use the Agent REST API via the `useEvaAgent` hook
|
|
406
|
+
5. After adding Eva, **STOP and suggest** the platform event — do NOT create it until the user confirms
|
|
407
|
+
6. After user confirms and platform event is created, **STOP and suggest** the Apex class — do NOT create it until the user confirms
|
|
388
408
|
|
|
389
409
|
**What the agent must NOT do in this prompt:**
|
|
410
|
+
- Do NOT use `AgentforceConversationClient` — use `useEvaAgent` hook instead (Agent REST API)
|
|
390
411
|
- Do NOT rewrite `EngineDashboard.tsx` from scratch — make targeted edits to the existing Phase 2 file
|
|
391
412
|
- Do NOT create platform events or Apex classes without user confirmation — each is a scripted demo moment where you suggest, wait, then create
|
|
392
413
|
- Do NOT read library component source files
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEvaAgent — React hook for the Agentforce Agent REST API
|
|
3
|
+
*
|
|
4
|
+
* Handles the full lifecycle:
|
|
5
|
+
* 1. OAuth client-credentials → access_token
|
|
6
|
+
* 2. POST …/sessions → sessionId + message href
|
|
7
|
+
* 3. POST …/messages → agent response text
|
|
8
|
+
* 4. DELETE …/sessions → cleanup on unmount
|
|
9
|
+
*
|
|
10
|
+
* All HTTP calls go through the Vite proxy (see vite.config.ts):
|
|
11
|
+
* /sf-oauth/* → myDomainUrl
|
|
12
|
+
* /sf-agent/* → agentApiBaseUrl
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
16
|
+
import { AGENT_API_CONFIG } from "@/config/agentApi";
|
|
17
|
+
|
|
18
|
+
export interface AgentMessage {
|
|
19
|
+
role: "user" | "agent";
|
|
20
|
+
text: string;
|
|
21
|
+
id: string;
|
|
22
|
+
timestamp: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SessionState {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
messagesHref: string;
|
|
29
|
+
endHref: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function useEvaAgent() {
|
|
33
|
+
const [messages, setMessages] = useState<AgentMessage[]>([]);
|
|
34
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
35
|
+
const [isReady, setIsReady] = useState(false);
|
|
36
|
+
const [isSending, setIsSending] = useState(false);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const sessionRef = useRef<SessionState | null>(null);
|
|
40
|
+
const sequenceRef = useRef(1);
|
|
41
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
42
|
+
|
|
43
|
+
/* ── Step 1: OAuth token ──────────────────────────────── */
|
|
44
|
+
async function authenticate(signal: AbortSignal): Promise<string> {
|
|
45
|
+
const { clientId, clientSecret } = AGENT_API_CONFIG;
|
|
46
|
+
|
|
47
|
+
const body = new URLSearchParams({
|
|
48
|
+
grant_type: "client_credentials",
|
|
49
|
+
client_id: clientId,
|
|
50
|
+
client_secret: clientSecret,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const res = await fetch("/sf-oauth/services/oauth2/token", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
56
|
+
body,
|
|
57
|
+
signal,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
throw new Error(`OAuth failed (${res.status}): ${text}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
return data.access_token;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ── Step 2: Create session ───────────────────────────── */
|
|
70
|
+
async function createSession(
|
|
71
|
+
accessToken: string,
|
|
72
|
+
signal: AbortSignal
|
|
73
|
+
): Promise<Omit<SessionState, "accessToken">> {
|
|
74
|
+
const { agentId, bypassUser, demoTraveler, myDomainUrl } =
|
|
75
|
+
AGENT_API_CONFIG;
|
|
76
|
+
|
|
77
|
+
const payload = {
|
|
78
|
+
externalSessionKey: crypto.randomUUID(),
|
|
79
|
+
instanceConfig: { endpoint: myDomainUrl },
|
|
80
|
+
streamingCapabilities: { chunkTypes: ["Text"] },
|
|
81
|
+
bypassUser,
|
|
82
|
+
variables: [
|
|
83
|
+
{
|
|
84
|
+
name: "$Context.EndUserId",
|
|
85
|
+
type: "Text",
|
|
86
|
+
value: demoTraveler.contactId,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "$Context.EndUserEmail",
|
|
90
|
+
type: "Text",
|
|
91
|
+
value: demoTraveler.email,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "$Context.EndUserFirstName",
|
|
95
|
+
type: "Text",
|
|
96
|
+
value: demoTraveler.firstName,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "$Context.EndUserLastName",
|
|
100
|
+
type: "Text",
|
|
101
|
+
value: demoTraveler.lastName,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const res = await fetch(
|
|
107
|
+
`/sf-agent/einstein/ai-agent/v1/agents/${agentId}/sessions`,
|
|
108
|
+
{
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
Authorization: `Bearer ${accessToken}`,
|
|
112
|
+
"Content-Type": "application/json",
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(payload),
|
|
115
|
+
signal,
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
const text = await res.text();
|
|
121
|
+
throw new Error(`Session creation failed (${res.status}): ${text}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const data = await res.json();
|
|
125
|
+
return {
|
|
126
|
+
sessionId: data.sessionId,
|
|
127
|
+
messagesHref: data._links?.messages?.href ?? "",
|
|
128
|
+
endHref: data._links?.end?.href ?? "",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Step 3: Send message ─────────────────────────────── */
|
|
133
|
+
const sendMessage = useCallback(async (text: string) => {
|
|
134
|
+
const session = sessionRef.current;
|
|
135
|
+
if (!session) return;
|
|
136
|
+
|
|
137
|
+
const userMsg: AgentMessage = {
|
|
138
|
+
role: "user",
|
|
139
|
+
text,
|
|
140
|
+
id: `user-${Date.now()}`,
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
144
|
+
setIsSending(true);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const seqId = sequenceRef.current++;
|
|
148
|
+
|
|
149
|
+
const messagesUrl = session.messagesHref.startsWith("http")
|
|
150
|
+
? `/sf-agent${new URL(session.messagesHref).pathname}`
|
|
151
|
+
: `/sf-agent${session.messagesHref}`;
|
|
152
|
+
|
|
153
|
+
const res = await fetch(messagesUrl, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: `Bearer ${session.accessToken}`,
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
Accept: "application/json",
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
message: { sequenceId: seqId, type: "Text", text },
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
const errText = await res.text();
|
|
167
|
+
throw new Error(`Agent message failed (${res.status}): ${errText}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const data = await res.json();
|
|
171
|
+
|
|
172
|
+
const agentTexts: string[] = [];
|
|
173
|
+
if (Array.isArray(data.messages)) {
|
|
174
|
+
for (const m of data.messages) {
|
|
175
|
+
if (m.type === "Inform" && m.message) {
|
|
176
|
+
agentTexts.push(m.message);
|
|
177
|
+
} else if (m.type === "Text" && m.message) {
|
|
178
|
+
agentTexts.push(m.message);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const responseText =
|
|
184
|
+
agentTexts.length > 0
|
|
185
|
+
? agentTexts.join("\n\n")
|
|
186
|
+
: data.message ?? "No response from agent.";
|
|
187
|
+
|
|
188
|
+
const agentMsg: AgentMessage = {
|
|
189
|
+
role: "agent",
|
|
190
|
+
text: responseText,
|
|
191
|
+
id: `agent-${Date.now()}`,
|
|
192
|
+
timestamp: new Date().toISOString(),
|
|
193
|
+
};
|
|
194
|
+
setMessages((prev) => [...prev, agentMsg]);
|
|
195
|
+
} catch (err: unknown) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
197
|
+
setError(msg);
|
|
198
|
+
const errMsg: AgentMessage = {
|
|
199
|
+
role: "agent",
|
|
200
|
+
text: `Error: ${msg}`,
|
|
201
|
+
id: `error-${Date.now()}`,
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
};
|
|
204
|
+
setMessages((prev) => [...prev, errMsg]);
|
|
205
|
+
} finally {
|
|
206
|
+
setIsSending(false);
|
|
207
|
+
}
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
210
|
+
/* ── Step 4: End session ──────────────────────────────── */
|
|
211
|
+
const endSession = useCallback(async () => {
|
|
212
|
+
const session = sessionRef.current;
|
|
213
|
+
if (!session) return;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const endUrl = session.endHref.startsWith("http")
|
|
217
|
+
? `/sf-agent${new URL(session.endHref).pathname}`
|
|
218
|
+
: `/sf-agent${session.endHref}`;
|
|
219
|
+
|
|
220
|
+
await fetch(endUrl, {
|
|
221
|
+
method: "DELETE",
|
|
222
|
+
headers: { Authorization: `Bearer ${session.accessToken}` },
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
/* best-effort cleanup */
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
sessionRef.current = null;
|
|
229
|
+
setIsReady(false);
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
/* ── Init: authenticate + create session on mount ─────── */
|
|
233
|
+
const connect = useCallback(async () => {
|
|
234
|
+
if (sessionRef.current) return;
|
|
235
|
+
|
|
236
|
+
abortRef.current = new AbortController();
|
|
237
|
+
const { signal } = abortRef.current;
|
|
238
|
+
|
|
239
|
+
setIsConnecting(true);
|
|
240
|
+
setError(null);
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const accessToken = await authenticate(signal);
|
|
244
|
+
const session = await createSession(accessToken, signal);
|
|
245
|
+
|
|
246
|
+
sessionRef.current = { accessToken, ...session };
|
|
247
|
+
sequenceRef.current = 1;
|
|
248
|
+
setIsReady(true);
|
|
249
|
+
} catch (err: unknown) {
|
|
250
|
+
if ((err as Error).name !== "AbortError") {
|
|
251
|
+
const msg = err instanceof Error ? err.message : "Connection failed";
|
|
252
|
+
setError(msg);
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
setIsConnecting(false);
|
|
256
|
+
}
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
/* ── Cleanup on unmount ───────────────────────────────── */
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
return () => {
|
|
262
|
+
abortRef.current?.abort();
|
|
263
|
+
if (sessionRef.current) {
|
|
264
|
+
const session = sessionRef.current;
|
|
265
|
+
const endUrl = session.endHref.startsWith("http")
|
|
266
|
+
? `/sf-agent${new URL(session.endHref).pathname}`
|
|
267
|
+
: `/sf-agent${session.endHref}`;
|
|
268
|
+
|
|
269
|
+
fetch(endUrl, {
|
|
270
|
+
method: "DELETE",
|
|
271
|
+
headers: { Authorization: `Bearer ${session.accessToken}` },
|
|
272
|
+
}).catch(() => {});
|
|
273
|
+
sessionRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
messages,
|
|
280
|
+
isConnecting,
|
|
281
|
+
isReady,
|
|
282
|
+
isSending,
|
|
283
|
+
error,
|
|
284
|
+
connect,
|
|
285
|
+
sendMessage,
|
|
286
|
+
endSession,
|
|
287
|
+
};
|
|
288
|
+
}
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -315,6 +315,41 @@ if (fs.existsSync(dataSourceDir)) {
|
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// Copy useEvaAgent.ts (Agentforce Agent API hook)
|
|
319
|
+
const useEvaAgentSource = path.join(dataSourceDir, 'useEvaAgent.ts');
|
|
320
|
+
const useEvaAgentTarget = path.join(targetHooksDir, 'useEvaAgent.ts');
|
|
321
|
+
|
|
322
|
+
if (fs.existsSync(useEvaAgentSource)) {
|
|
323
|
+
try {
|
|
324
|
+
if (!fs.existsSync(targetHooksDir)) {
|
|
325
|
+
fs.mkdirSync(targetHooksDir, { recursive: true });
|
|
326
|
+
}
|
|
327
|
+
fs.copyFileSync(useEvaAgentSource, useEvaAgentTarget);
|
|
328
|
+
console.log(' ✓ Installed useEvaAgent.ts');
|
|
329
|
+
dataFilesInstalled++;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error(` ✗ Failed to install useEvaAgent.ts: ${error.message}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Copy agentApiConfig.ts → src/config/agentApi.ts
|
|
336
|
+
const agentApiConfigSource = path.join(dataSourceDir, 'agentApiConfig.ts');
|
|
337
|
+
const targetConfigDir = path.join(cwd, 'src/config');
|
|
338
|
+
const agentApiConfigTarget = path.join(targetConfigDir, 'agentApi.ts');
|
|
339
|
+
|
|
340
|
+
if (fs.existsSync(agentApiConfigSource)) {
|
|
341
|
+
try {
|
|
342
|
+
if (!fs.existsSync(targetConfigDir)) {
|
|
343
|
+
fs.mkdirSync(targetConfigDir, { recursive: true });
|
|
344
|
+
}
|
|
345
|
+
fs.copyFileSync(agentApiConfigSource, agentApiConfigTarget);
|
|
346
|
+
console.log(' ✓ Installed src/config/agentApi.ts');
|
|
347
|
+
dataFilesInstalled++;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error(` ✗ Failed to install agentApiConfig.ts: ${error.message}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
318
353
|
// Copy engine_logo.png
|
|
319
354
|
const assetsSourceDir = path.join(packageRoot, 'assets/images');
|
|
320
355
|
const targetAssetsDir = path.join(cwd, 'src/assets/images');
|
|
@@ -771,35 +771,36 @@ fi
|
|
|
771
771
|
if [ -n "$SFDX_DEFAULT" ]; then
|
|
772
772
|
metadata_cleaned=0
|
|
773
773
|
|
|
774
|
-
# Remove
|
|
774
|
+
# Remove Apex classes (.cls and .cls-meta.xml), preserving permanent classes
|
|
775
|
+
KEEP_CLASSES=("TCC_TravelMetricsController")
|
|
775
776
|
CLASSES_DIR="$SFDX_DEFAULT/classes"
|
|
776
777
|
if [ -d "$CLASSES_DIR" ]; then
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
[ "$
|
|
780
|
-
|
|
781
|
-
|
|
778
|
+
cls_removed=0
|
|
779
|
+
for f in "$CLASSES_DIR"/*.cls "$CLASSES_DIR"/*.cls-meta.xml; do
|
|
780
|
+
[ -f "$f" ] || continue
|
|
781
|
+
base="$(basename "$f")"
|
|
782
|
+
skip=false
|
|
783
|
+
for keep in "${KEEP_CLASSES[@]}"; do
|
|
784
|
+
if [[ "$base" == "$keep".cls || "$base" == "$keep".cls-meta.xml ]]; then
|
|
785
|
+
skip=true
|
|
786
|
+
break
|
|
787
|
+
fi
|
|
788
|
+
done
|
|
789
|
+
if [ "$skip" = false ]; then
|
|
790
|
+
[ "$metadata_cleaned" -eq 0 ] && [ "$cls_removed" -eq 0 ] && echo "→ Cleaning Salesforce metadata…"
|
|
791
|
+
rm "$f"
|
|
792
|
+
cls_removed=$((cls_removed + 1))
|
|
793
|
+
fi
|
|
794
|
+
done
|
|
795
|
+
if [ "$cls_removed" -gt 0 ]; then
|
|
796
|
+
echo " ✓ Removed $cls_removed Apex class files (kept ${KEEP_CLASSES[*]})"
|
|
782
797
|
metadata_cleaned=1
|
|
783
798
|
fi
|
|
784
799
|
fi
|
|
785
800
|
|
|
786
|
-
# Remove custom fields
|
|
801
|
+
# Remove platform event directories (but NOT custom fields — those persist across resets)
|
|
787
802
|
OBJECTS_DIR="$SFDX_DEFAULT/objects"
|
|
788
803
|
if [ -d "$OBJECTS_DIR" ]; then
|
|
789
|
-
for fields_dir in "$OBJECTS_DIR"/*/fields; do
|
|
790
|
-
if [ -d "$fields_dir" ]; then
|
|
791
|
-
for f in "$fields_dir"/*.field-meta.xml; do
|
|
792
|
-
if [ -f "$f" ]; then
|
|
793
|
-
[ "$metadata_cleaned" -eq 0 ] && echo "→ Cleaning Salesforce metadata…"
|
|
794
|
-
rm "$f"
|
|
795
|
-
echo " ✓ Removed $(basename "$f")"
|
|
796
|
-
metadata_cleaned=1
|
|
797
|
-
fi
|
|
798
|
-
done
|
|
799
|
-
fi
|
|
800
|
-
done
|
|
801
|
-
|
|
802
|
-
# Remove platform event directories
|
|
803
804
|
for evt_dir in "$OBJECTS_DIR"/*__e; do
|
|
804
805
|
if [ -d "$evt_dir" ]; then
|
|
805
806
|
[ "$metadata_cleaned" -eq 0 ] && echo "→ Cleaning Salesforce metadata…"
|
|
@@ -827,7 +828,6 @@ echo "║ • Component library + theme providers ║"
|
|
|
827
828
|
echo "║ ║"
|
|
828
829
|
echo "║ Cleaned: ║"
|
|
829
830
|
echo "║ • Apex classes (force-app classes/) ║"
|
|
830
|
-
echo "║ • Custom fields (force-app objects/) ║"
|
|
831
831
|
echo "║ • Platform events (force-app *__e/) ║"
|
|
832
832
|
echo "║ ║"
|
|
833
833
|
echo "║ Layout: ║"
|
|
@@ -34,6 +34,24 @@ export default defineConfig(({ mode }) => {
|
|
|
34
34
|
: []),
|
|
35
35
|
] as import('vite').PluginOption[],
|
|
36
36
|
|
|
37
|
+
// Proxy Agentforce Agent API calls through the dev server to avoid CORS.
|
|
38
|
+
// /sf-oauth/* → Salesforce org (for OAuth client-credentials token)
|
|
39
|
+
// /sf-agent/* → Agent API base (for session + message calls)
|
|
40
|
+
server: {
|
|
41
|
+
proxy: {
|
|
42
|
+
'/sf-oauth': {
|
|
43
|
+
target: 'https://tdx26-keynote-org-1com.my.salesforce.com',
|
|
44
|
+
changeOrigin: true,
|
|
45
|
+
rewrite: (p: string) => p.replace(/^\/sf-oauth/, ''),
|
|
46
|
+
},
|
|
47
|
+
'/sf-agent': {
|
|
48
|
+
target: 'https://api.salesforce.com',
|
|
49
|
+
changeOrigin: true,
|
|
50
|
+
rewrite: (p: string) => p.replace(/^\/sf-agent/, ''),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
37
55
|
// Build configuration for MPA
|
|
38
56
|
build: {
|
|
39
57
|
outDir: resolve(__dirname, 'dist'),
|