@frontera-sdk/chat 1.52.44 → 1.52.46
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/dist/auth.d.ts +20 -0
- package/dist/auth.js +11 -0
- package/dist/callback-page.d.ts +49 -0
- package/dist/callback-page.js +84 -0
- package/dist/chat-client.d.ts +221 -0
- package/dist/chat-client.js +5 -0
- package/dist/chat-errors.d.ts +30 -0
- package/dist/chat-errors.js +3 -0
- package/dist/chunk-CMMWTCYS.js +116 -0
- package/dist/chunk-KZEGEV3P.js +35 -0
- package/dist/chunk-LS26G2N6.js +120 -0
- package/dist/chunk-RLHHRJWS.js +371 -0
- package/dist/chunk-UCUVHMJC.js +62 -0
- package/dist/chunk-VWUR3ECR.js +115 -0
- package/dist/chunk-W25Q2DQA.js +32 -0
- package/dist/chunk-YKQ5Z5EK.js +32 -0
- package/dist/connect-popup.d.ts +150 -0
- package/dist/connect-popup.js +138 -0
- package/dist/doctor.d.ts +71 -0
- package/dist/doctor.js +3 -0
- package/dist/element.d.ts +23 -0
- package/dist/element.js +175 -0
- package/dist/frontera-chat.iife.js +27 -0
- package/dist/hooks.d.ts +88 -0
- package/dist/hooks.js +3 -0
- package/dist/provider.d.ts +20 -0
- package/dist/provider.js +13 -0
- package/dist/public-chat.d.ts +50 -0
- package/dist/public-chat.js +6 -0
- package/dist/public-token.d.ts +46 -0
- package/dist/public-token.js +3 -0
- package/dist/reply-text.d.ts +25 -0
- package/dist/reply-text.js +3 -0
- package/dist/stream.d.ts +25 -0
- package/dist/stream.js +3 -0
- package/dist/token-session.d.ts +66 -0
- package/dist/token-session.js +99 -0
- package/dist/transport.d.ts +60 -0
- package/dist/transport.js +103 -0
- package/dist/types.d.ts +361 -0
- package/dist/types.js +3 -0
- package/dist/ui-stream.d.ts +25 -0
- package/dist/ui-stream.js +26 -0
- package/package.json +70 -30
- package/src/chat-errors.ts +43 -0
- package/src/element.ts +201 -0
- package/src/frontera-chat.global.ts +13 -0
- package/src/public-chat.ts +154 -0
- package/src/public-token.ts +36 -4
- package/src/reply-text.ts +49 -0
- package/src/transport.ts +7 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { mintPublicChatToken } from './chunk-UCUVHMJC.js';
|
|
2
|
+
import { startChat } from './chunk-KZEGEV3P.js';
|
|
3
|
+
import { chatErrorCode } from './chunk-YKQ5Z5EK.js';
|
|
4
|
+
import { FronteraClient } from '@frontera-sdk/core/client';
|
|
5
|
+
import { FronteraError } from '@frontera-sdk/core/errors';
|
|
6
|
+
|
|
7
|
+
function newId() {
|
|
8
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `m-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
9
|
+
}
|
|
10
|
+
async function* chatEvents(response) {
|
|
11
|
+
if (!response.body) return;
|
|
12
|
+
const reader = response.body.getReader();
|
|
13
|
+
const decoder = new TextDecoder();
|
|
14
|
+
let buffer = "";
|
|
15
|
+
const parse = (block) => {
|
|
16
|
+
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
17
|
+
if (!data || data === "[DONE]") return null;
|
|
18
|
+
try {
|
|
19
|
+
const value = JSON.parse(data);
|
|
20
|
+
return value && typeof value === "object" ? value : null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
for (; ; ) {
|
|
26
|
+
const { done, value } = await reader.read();
|
|
27
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
28
|
+
let end = buffer.indexOf("\n\n");
|
|
29
|
+
while (end !== -1) {
|
|
30
|
+
const event = parse(buffer.slice(0, end));
|
|
31
|
+
buffer = buffer.slice(end + 2);
|
|
32
|
+
if (event) yield event;
|
|
33
|
+
end = buffer.indexOf("\n\n");
|
|
34
|
+
}
|
|
35
|
+
if (done) break;
|
|
36
|
+
}
|
|
37
|
+
const last = parse(buffer);
|
|
38
|
+
if (last) yield last;
|
|
39
|
+
}
|
|
40
|
+
var PublicChat = class {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
}
|
|
44
|
+
options;
|
|
45
|
+
token = null;
|
|
46
|
+
conversationId = null;
|
|
47
|
+
/** Shown before the first message; sent back with it, so the conversation keeps it. */
|
|
48
|
+
openingMessage = null;
|
|
49
|
+
opened = false;
|
|
50
|
+
/** Mint the visitor's token, and learn the Agent's opening message. Called by `send` when needed. */
|
|
51
|
+
async open() {
|
|
52
|
+
const minted = await mintPublicChatToken({ ...this.options, ...this.options.fetchImpl ? { fetchImpl: this.options.fetchImpl } : {} });
|
|
53
|
+
this.token = minted.token;
|
|
54
|
+
if (!this.opened) this.openingMessage = minted.openingMessage ?? null;
|
|
55
|
+
this.opened = true;
|
|
56
|
+
return { openingMessage: this.openingMessage };
|
|
57
|
+
}
|
|
58
|
+
client() {
|
|
59
|
+
return new FronteraClient(
|
|
60
|
+
{ apiBaseUrl: this.options.apiBaseUrl, credential: { kind: "token", token: this.token } },
|
|
61
|
+
this.options.fetchImpl ?? fetch
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
messagesFor(text) {
|
|
65
|
+
const user = { id: newId(), role: "user", parts: [{ type: "text", text }] };
|
|
66
|
+
if (this.conversationId || !this.openingMessage) return [user];
|
|
67
|
+
return [{ id: newId(), role: "assistant", parts: [{ type: "text", text: this.openingMessage }] }, user];
|
|
68
|
+
}
|
|
69
|
+
/** Send one message and read the reply: `waiting`, then the reply's text as it grows, then `done` (or `error`). */
|
|
70
|
+
async *send(text, signal) {
|
|
71
|
+
yield { type: "waiting" };
|
|
72
|
+
let response;
|
|
73
|
+
try {
|
|
74
|
+
if (!this.token) await this.open();
|
|
75
|
+
const start = () => startChat(this.client(), { messages: this.messagesFor(text), ...this.conversationId ? { conversationId: this.conversationId } : {} }, { signal });
|
|
76
|
+
let run;
|
|
77
|
+
try {
|
|
78
|
+
run = await start();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (chatErrorCode(err) !== "UNAUTHORIZED") throw err;
|
|
81
|
+
await this.open();
|
|
82
|
+
run = await start();
|
|
83
|
+
}
|
|
84
|
+
this.conversationId = run.conversationId;
|
|
85
|
+
response = run.response;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
yield { type: "error", error: asFronteraError(err) };
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const parts = /* @__PURE__ */ new Map();
|
|
91
|
+
const order = [];
|
|
92
|
+
const whole = () => order.map((id) => parts.get(id) ?? "").filter(Boolean).join("\n\n");
|
|
93
|
+
try {
|
|
94
|
+
for await (const event of chatEvents(response)) {
|
|
95
|
+
if (event.type === "text-delta" && typeof event.delta === "string") {
|
|
96
|
+
const id = typeof event.id === "string" ? event.id : "text";
|
|
97
|
+
if (!parts.has(id)) order.push(id);
|
|
98
|
+
parts.set(id, (parts.get(id) ?? "") + event.delta);
|
|
99
|
+
yield { type: "text", text: whole() };
|
|
100
|
+
} else if (event.type === "error") {
|
|
101
|
+
const message = typeof event.errorText === "string" ? event.errorText : "The reply failed.";
|
|
102
|
+
yield { type: "error", error: new FronteraError(message, { code: "INTERNAL_ERROR" }) };
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (err) {
|
|
107
|
+
yield { type: "error", error: asFronteraError(err) };
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
yield { type: "done" };
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function asFronteraError(err) {
|
|
114
|
+
if (err instanceof FronteraError) return err;
|
|
115
|
+
return new FronteraError(err instanceof Error ? err.message : String(err), { code: "INTERNAL_ERROR" });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { PublicChat, chatEvents };
|
|
119
|
+
//# sourceMappingURL=chunk-LS26G2N6.js.map
|
|
120
|
+
//# sourceMappingURL=chunk-LS26G2N6.js.map
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { chatDoctor } from './chunk-VWUR3ECR.js';
|
|
2
|
+
import { startChat, resumeStream } from './chunk-KZEGEV3P.js';
|
|
3
|
+
import { FronteraError } from '@frontera-sdk/core/errors';
|
|
4
|
+
|
|
5
|
+
var ChatClient = class {
|
|
6
|
+
constructor(client, options = {}) {
|
|
7
|
+
this.client = client;
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
client;
|
|
11
|
+
options;
|
|
12
|
+
async authorize(conversationId) {
|
|
13
|
+
const gate = this.options.authorizeConversation;
|
|
14
|
+
if (!gate) return;
|
|
15
|
+
if (!await gate(conversationId)) {
|
|
16
|
+
throw new FronteraError(`access to conversation ${conversationId} denied locally`, {
|
|
17
|
+
code: "FORBIDDEN_LOCAL"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// --- chat runs ---
|
|
22
|
+
/** Start (or continue) a run. See `stream.ts` for stream semantics. */
|
|
23
|
+
async startChat(request, options) {
|
|
24
|
+
if (request.conversationId) await this.authorize(request.conversationId);
|
|
25
|
+
return startChat(this.client, request, options);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Re-attach to a run's stream. Run ids are capabilities: they only leave the
|
|
29
|
+
* server through a response the caller was already authorized to receive, so
|
|
30
|
+
* there is no conversation gate here — treat them accordingly.
|
|
31
|
+
*/
|
|
32
|
+
resumeStream(runId, options) {
|
|
33
|
+
return resumeStream(this.client, runId, options);
|
|
34
|
+
}
|
|
35
|
+
cancelRun(runId) {
|
|
36
|
+
return this.client.request(`/v1/chat/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
|
|
37
|
+
}
|
|
38
|
+
steerRun(runId, message) {
|
|
39
|
+
return this.client.request(`/v1/chat/${encodeURIComponent(runId)}/steer`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
body: { message }
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// --- approvals ---
|
|
45
|
+
/**
|
|
46
|
+
* The tool calls this conversation has parked, waiting on a person.
|
|
47
|
+
*
|
|
48
|
+
* Poll or call this on mount: a run blocked on an approval stays blocked
|
|
49
|
+
* across a page reload, and the `tool-approval-request` chunk that announced
|
|
50
|
+
* it is not replayed by a fresh `useChat`. Without reading them back, the
|
|
51
|
+
* conversation looks like an agent that stopped for no reason.
|
|
52
|
+
*
|
|
53
|
+
* End-user (`cht_`) tokens only — see `respondToApproval`.
|
|
54
|
+
*/
|
|
55
|
+
async listPendingApprovals(conversationId) {
|
|
56
|
+
chatDoctor.resolved("approval");
|
|
57
|
+
chatDoctor.resolved("connect");
|
|
58
|
+
await this.authorize(conversationId);
|
|
59
|
+
const envelope = await this.client.requestEnvelope(
|
|
60
|
+
"/v1/chat/approvals",
|
|
61
|
+
{ query: { conversationId } }
|
|
62
|
+
);
|
|
63
|
+
return envelope.data;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Approve or deny a parked tool call, releasing (or refusing) the run.
|
|
67
|
+
*
|
|
68
|
+
* Requires an end-user `cht_` token: the decision is recorded against the
|
|
69
|
+
* person who made it, and an `sak_` key is one credential shared by every
|
|
70
|
+
* user of your product — there is nobody it could be deciding as, so the
|
|
71
|
+
* server answers 403 (`FORBIDDEN`). An integration that wants its agent to
|
|
72
|
+
* act unattended sets the agent's approval mode to `auto` instead.
|
|
73
|
+
*
|
|
74
|
+
* Deciding twice is not an error: the first decision wins and this reports
|
|
75
|
+
* the settled status. A lapsed request answers `expired`.
|
|
76
|
+
*/
|
|
77
|
+
respondToApproval(approvalId, decision) {
|
|
78
|
+
chatDoctor.resolved("approval");
|
|
79
|
+
return this.client.request(`/v1/chat/approvals/${encodeURIComponent(approvalId)}`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: { decision }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// --- app connections ---
|
|
85
|
+
/**
|
|
86
|
+
* Begin connecting the end user's own account for a parked `needs_connect`
|
|
87
|
+
* call, and get the URL to send them to.
|
|
88
|
+
*
|
|
89
|
+
* Three things must line up before this answers, and all three are one-time
|
|
90
|
+
* setup rather than per-user work:
|
|
91
|
+
* 1. the workspace has ITS OWN OAuth client for the provider (the
|
|
92
|
+
* deployment-managed one cannot serve this — your users are authorising
|
|
93
|
+
* your application, and only its owner can register your callback);
|
|
94
|
+
* 2. `redirectUri` is registered with that client AND on the workspace's
|
|
95
|
+
* embed allowlist;
|
|
96
|
+
* 3. the install allows external users, and this user's token carries the
|
|
97
|
+
* `plugin-connect` capability you granted when you minted it.
|
|
98
|
+
*
|
|
99
|
+
* Open the returned `authUrl` in a popup. Your callback receives `code` and
|
|
100
|
+
* `state` — hand them to `completeAppConnection`, then `resumeToolCall`.
|
|
101
|
+
*/
|
|
102
|
+
startAppConnection(request) {
|
|
103
|
+
chatDoctor.resolved("connect");
|
|
104
|
+
return this.client.request("/v1/chat/app-connections/start", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: request
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Finish a connect from the code your own callback received.
|
|
111
|
+
*
|
|
112
|
+
* `state` is signed by us and names the install, the user and the redirect
|
|
113
|
+
* URI, so none of the three can be swapped between the authorize request and
|
|
114
|
+
* this call. Pass it back exactly as it arrived.
|
|
115
|
+
*/
|
|
116
|
+
completeAppConnection(request) {
|
|
117
|
+
return this.client.request("/v1/chat/app-connections/exchange", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: request
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Re-run the tool call that was waiting on the connection.
|
|
124
|
+
*
|
|
125
|
+
* Not optional: the agent is blocked on a tool RESULT, not on an account, so
|
|
126
|
+
* a connect without this leaves the conversation exactly as stuck as before.
|
|
127
|
+
*/
|
|
128
|
+
resumeToolCall(pendingToolCallId) {
|
|
129
|
+
return this.client.request("/v1/chat/app-connections/resume", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
body: { pendingToolCallId }
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The agents this credential may talk to — what an agent picker renders.
|
|
136
|
+
*
|
|
137
|
+
* Scoped to the credential, so a token bound to one agent lists exactly that
|
|
138
|
+
* agent while a workspace-wide token lists them all. The Console listing is
|
|
139
|
+
* session-only and returns configuration an end user has no business seeing,
|
|
140
|
+
* which is why this exists separately.
|
|
141
|
+
*/
|
|
142
|
+
listAgents() {
|
|
143
|
+
return this.client.request("/v1/chat-agents");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Upload a file for this user, then reference the result in
|
|
147
|
+
* `startChat({ fileAttachments: [uploaded] })`.
|
|
148
|
+
*
|
|
149
|
+
* A chat-scoped route, not the platform's `/v1/upload`: that one authorises
|
|
150
|
+
* through workspace membership, which an end user of a customer's product
|
|
151
|
+
* does not have and should not need.
|
|
152
|
+
*/
|
|
153
|
+
async upload(file, filename) {
|
|
154
|
+
const form = new FormData();
|
|
155
|
+
form.append("file", file, filename ?? file.name ?? "upload");
|
|
156
|
+
const response = await this.client.requestRaw("/v1/chat-uploads", {
|
|
157
|
+
method: "POST",
|
|
158
|
+
body: form
|
|
159
|
+
});
|
|
160
|
+
const payload = await response.json();
|
|
161
|
+
return payload.data ?? payload;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A live URL for a stored upload.
|
|
165
|
+
*
|
|
166
|
+
* The URL saved on a message is presigned and expires about an hour after
|
|
167
|
+
* upload, so anything rendered later re-signs from the stored path — without
|
|
168
|
+
* this, attachments in yesterday's messages become broken images.
|
|
169
|
+
*/
|
|
170
|
+
async signUpload(storagePath) {
|
|
171
|
+
const { url } = await this.client.request("/v1/chat-uploads/sign", {
|
|
172
|
+
method: "POST",
|
|
173
|
+
body: { path: storagePath }
|
|
174
|
+
});
|
|
175
|
+
return url;
|
|
176
|
+
}
|
|
177
|
+
/** The artifacts this user owns — documents, decks and images the agent made. */
|
|
178
|
+
async listArtifacts(query = {}) {
|
|
179
|
+
const envelope = await this.client.requestEnvelope("/v1/chat-artifacts", { query });
|
|
180
|
+
return { data: envelope.data, nextCursor: envelope.nextCursor ?? void 0 };
|
|
181
|
+
}
|
|
182
|
+
getArtifact(id) {
|
|
183
|
+
return this.client.request(`/v1/chat-artifacts/${encodeURIComponent(id)}`);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The artifact's contents as TEXT.
|
|
187
|
+
*
|
|
188
|
+
* Only for artifacts that are text — markdown, csv, code. Reading a PNG or a
|
|
189
|
+
* spreadsheet through here destroys it: `.text()` decodes the bytes as UTF-8,
|
|
190
|
+
* and every byte that is not valid UTF-8 becomes a replacement character on
|
|
191
|
+
* the way. Use `getArtifactFile` for anything binary.
|
|
192
|
+
*/
|
|
193
|
+
async getArtifactContent(id) {
|
|
194
|
+
const response = await this.client.requestRaw(
|
|
195
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}/file`
|
|
196
|
+
);
|
|
197
|
+
return response.text();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The artifact's raw bytes, with the server's own content type.
|
|
201
|
+
*
|
|
202
|
+
* The Blob carries the type the route reported rather than one guessed from
|
|
203
|
+
* the format, so an <img> or a PDF reader gets what it needs without the
|
|
204
|
+
* caller restating it — and a wrong guess here is an image that silently
|
|
205
|
+
* will not display.
|
|
206
|
+
*/
|
|
207
|
+
async getArtifactFile(id) {
|
|
208
|
+
const response = await this.client.requestRaw(
|
|
209
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}/file`
|
|
210
|
+
);
|
|
211
|
+
return response.blob();
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* A PDF rendering of an artifact a browser cannot open itself.
|
|
215
|
+
*
|
|
216
|
+
* `.docx` and `.pptx` are bytes with no source to fall back on, so the
|
|
217
|
+
* service renders a PDF beside them and this returns it — displaying one
|
|
218
|
+
* without shipping an OOXML reader.
|
|
219
|
+
*
|
|
220
|
+
* Only artifacts with `previewKey` set have one; anything else answers 404
|
|
221
|
+
* as `NOT_FOUND`. Check the field before asking, or catch it.
|
|
222
|
+
*/
|
|
223
|
+
/**
|
|
224
|
+
* Save an edit made in the viewer — a sheet cell, a document's text.
|
|
225
|
+
*
|
|
226
|
+
* Produces a new REVISION rather than overwriting: the service re-renders to
|
|
227
|
+
* a versioned storage key and bumps the version, so the artifact keeps the
|
|
228
|
+
* history an edit implies.
|
|
229
|
+
*/
|
|
230
|
+
async updateArtifact(id, patch) {
|
|
231
|
+
return this.client.request(
|
|
232
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}`,
|
|
233
|
+
{ method: "PATCH", body: patch }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
/** One revision's content — what the inline "viewing v1" panel shows. */
|
|
237
|
+
async getArtifactVersion(id, version) {
|
|
238
|
+
return this.client.request(
|
|
239
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}/versions/${encodeURIComponent(String(version))}`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Roll back to a revision.
|
|
244
|
+
*
|
|
245
|
+
* Writes a new revision holding the old content — nothing is deleted, so a
|
|
246
|
+
* restore made in error is itself undoable.
|
|
247
|
+
*/
|
|
248
|
+
async restoreArtifactVersion(id, version) {
|
|
249
|
+
return this.client.request(
|
|
250
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}/versions/${encodeURIComponent(String(version))}/restore`,
|
|
251
|
+
{ method: "POST" }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
/** Revisions of one artifact, newest first. Metadata only. */
|
|
255
|
+
async listArtifactVersions(id) {
|
|
256
|
+
return this.client.request(
|
|
257
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}/versions`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The rendition of a PAST version, for the viewer's version picker.
|
|
262
|
+
*
|
|
263
|
+
* Separate from `getArtifactPreview` rather than an optional argument: the
|
|
264
|
+
* current preview lives at a different path, and a version of `0` or `NaN`
|
|
265
|
+
* silently falling back to "current" is how a reader ends up looking at
|
|
266
|
+
* today's document under yesterday's heading.
|
|
267
|
+
*/
|
|
268
|
+
async getArtifactVersionPreview(id, version) {
|
|
269
|
+
const response = await this.client.requestRaw(
|
|
270
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}/versions/${encodeURIComponent(String(version))}/preview`
|
|
271
|
+
);
|
|
272
|
+
return response.blob();
|
|
273
|
+
}
|
|
274
|
+
async getArtifactPreview(id) {
|
|
275
|
+
const response = await this.client.requestRaw(
|
|
276
|
+
`/v1/chat-artifacts/${encodeURIComponent(id)}/preview`
|
|
277
|
+
);
|
|
278
|
+
return response.blob();
|
|
279
|
+
}
|
|
280
|
+
// --- sessions ---
|
|
281
|
+
async listSessions(query = {}) {
|
|
282
|
+
const envelope = await this.client.requestEnvelope("/v1/sessions", {
|
|
283
|
+
query: {
|
|
284
|
+
cursor: query.cursor,
|
|
285
|
+
limit: query.limit,
|
|
286
|
+
status: query.status,
|
|
287
|
+
label: query.label,
|
|
288
|
+
group: query.group,
|
|
289
|
+
search: query.search,
|
|
290
|
+
pinned: query.pinned,
|
|
291
|
+
source: query.source,
|
|
292
|
+
agentName: query.agentName,
|
|
293
|
+
agentId: query.agentId,
|
|
294
|
+
updatedAfter: query.updatedAfter,
|
|
295
|
+
updatedBefore: query.updatedBefore,
|
|
296
|
+
surface: query.surface
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return { data: envelope.data, nextCursor: envelope.nextCursor ?? void 0 };
|
|
300
|
+
}
|
|
301
|
+
createSession(request = {}) {
|
|
302
|
+
return this.client.request("/v1/sessions", { method: "POST", body: request });
|
|
303
|
+
}
|
|
304
|
+
async getSession(id) {
|
|
305
|
+
await this.authorize(id);
|
|
306
|
+
return this.client.request(`/v1/sessions/${encodeURIComponent(id)}`);
|
|
307
|
+
}
|
|
308
|
+
async getMessages(id, options = {}) {
|
|
309
|
+
await this.authorize(id);
|
|
310
|
+
return this.client.request(`/v1/sessions/${encodeURIComponent(id)}/messages`, {
|
|
311
|
+
query: { limit: options.limit }
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
// The four below act on ONE person's own session, so they need a credential
|
|
315
|
+
// that names one: a `cht_` end-user token works, an `sak_` key answers 403
|
|
316
|
+
// (`FORBIDDEN`) because it stands for every user of your product at once.
|
|
317
|
+
//
|
|
318
|
+
// `getShare`/`createShare` are deliberately absent: a share link works with
|
|
319
|
+
// no credential at all, and whether your end users may publish a
|
|
320
|
+
// conversation is your product's decision to implement, not a default they
|
|
321
|
+
// inherit from us. Those routes are platform-only and answer 403.
|
|
322
|
+
/**
|
|
323
|
+
* Rename, pin, or restatus a conversation.
|
|
324
|
+
*
|
|
325
|
+
* Answers `{ success: true }` rather than the updated row — the service
|
|
326
|
+
* confirms the write and nothing more. Typed as what actually arrives: a
|
|
327
|
+
* `Session` here would be a promise the server never made, and the first
|
|
328
|
+
* caller to read `.title` off it would get `undefined` with no explanation.
|
|
329
|
+
* Re-read with `getSession` when you need the new state.
|
|
330
|
+
*/
|
|
331
|
+
async updateSession(id, patch) {
|
|
332
|
+
await this.authorize(id);
|
|
333
|
+
return this.client.request(`/v1/sessions/${encodeURIComponent(id)}`, {
|
|
334
|
+
method: "PATCH",
|
|
335
|
+
body: patch
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async deleteSession(id) {
|
|
339
|
+
await this.authorize(id);
|
|
340
|
+
await this.client.request(`/v1/sessions/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
341
|
+
}
|
|
342
|
+
async forkSession(id, messageId) {
|
|
343
|
+
await this.authorize(id);
|
|
344
|
+
return this.client.request(`/v1/sessions/${encodeURIComponent(id)}/fork`, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
body: { messageId }
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/** Returns the export as a raw string — this route has no JSON envelope. */
|
|
350
|
+
async exportSession(id, format) {
|
|
351
|
+
await this.authorize(id);
|
|
352
|
+
const response = await this.client.requestRaw(
|
|
353
|
+
`/v1/sessions/${encodeURIComponent(id)}/export`,
|
|
354
|
+
{ query: { format } }
|
|
355
|
+
);
|
|
356
|
+
return response.text();
|
|
357
|
+
}
|
|
358
|
+
// --- aux ---
|
|
359
|
+
async getFollowUps(conversationId, messageId) {
|
|
360
|
+
await this.authorize(conversationId);
|
|
361
|
+
return this.client.request("/v1/follow-ups", { query: { conversationId, messageId } });
|
|
362
|
+
}
|
|
363
|
+
async getMessageMetadata(conversationId, messageId) {
|
|
364
|
+
await this.authorize(conversationId);
|
|
365
|
+
return this.client.request("/v1/message-metadata", { query: { conversationId, messageId } });
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export { ChatClient };
|
|
370
|
+
//# sourceMappingURL=chunk-RLHHRJWS.js.map
|
|
371
|
+
//# sourceMappingURL=chunk-RLHHRJWS.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { errorFromResponse } from '@frontera-sdk/core/errors';
|
|
2
|
+
|
|
3
|
+
// src/public-token.ts
|
|
4
|
+
function browserLanguage() {
|
|
5
|
+
try {
|
|
6
|
+
return typeof navigator === "undefined" ? void 0 : navigator.language || void 0;
|
|
7
|
+
} catch {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function visitorPassStorageKey(publishableKey) {
|
|
12
|
+
return `frontera:visitor:${publishableKey.slice(0, 12)}`;
|
|
13
|
+
}
|
|
14
|
+
function defaultStorage() {
|
|
15
|
+
try {
|
|
16
|
+
return typeof localStorage === "undefined" ? null : localStorage;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function read(storage, key) {
|
|
22
|
+
try {
|
|
23
|
+
return storage?.getItem(key) ?? null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function write(storage, key, value) {
|
|
29
|
+
try {
|
|
30
|
+
storage?.setItem(key, value);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function mintPublicChatToken(options) {
|
|
35
|
+
const storage = options.storage === void 0 ? defaultStorage() : options.storage;
|
|
36
|
+
const passKey = visitorPassStorageKey(options.publishableKey);
|
|
37
|
+
const visitorPass = read(storage, passKey);
|
|
38
|
+
const language = options.language ?? browserLanguage();
|
|
39
|
+
const response = await (options.fetchImpl ?? fetch)(`${options.apiBaseUrl}/v1/chat-tokens/public`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { "content-type": "application/json" },
|
|
42
|
+
// Never `include`: this call carries the key, not anyone's cookies.
|
|
43
|
+
credentials: "omit",
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
publishableKey: options.publishableKey,
|
|
46
|
+
...visitorPass ? { visitorPass } : {},
|
|
47
|
+
...language ? { language } : {}
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
const body = await response.json().catch(() => null);
|
|
51
|
+
if (!response.ok || !body?.data) throw errorFromResponse(response.status, body);
|
|
52
|
+
if (body.data.visitorPass) write(storage, passKey, body.data.visitorPass);
|
|
53
|
+
return {
|
|
54
|
+
token: body.data.token,
|
|
55
|
+
expiresAt: body.data.expiresAt,
|
|
56
|
+
...body.data.openingMessage ? { openingMessage: body.data.openingMessage } : {}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { mintPublicChatToken, visitorPassStorageKey };
|
|
61
|
+
//# sourceMappingURL=chunk-UCUVHMJC.js.map
|
|
62
|
+
//# sourceMappingURL=chunk-UCUVHMJC.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/doctor.ts
|
|
2
|
+
var DEFAULT_GRACE_MS = 1e4;
|
|
3
|
+
function defaultEnabled() {
|
|
4
|
+
const mode = typeof process !== "undefined" ? "development" : void 0;
|
|
5
|
+
return mode !== "production";
|
|
6
|
+
}
|
|
7
|
+
var ADVICE = {
|
|
8
|
+
approval: [
|
|
9
|
+
"A tool call is parked waiting for a person to approve it, and nothing has answered.",
|
|
10
|
+
"The run is alive and BLOCKED \u2014 it will not continue until it is settled or expires.",
|
|
11
|
+
"",
|
|
12
|
+
" const pending = await chat.listPendingApprovals(conversationId)",
|
|
13
|
+
" await chat.respondToApproval(pending[0].id, 'approve')",
|
|
14
|
+
"",
|
|
15
|
+
"Using @frontera-sdk/chat-ui, render <PendingCards conversationId={\u2026} />.",
|
|
16
|
+
"Read the pending list on mount too: a reload loses the stream chunk, not the block."
|
|
17
|
+
].join("\n"),
|
|
18
|
+
connect: [
|
|
19
|
+
"A tool needs this user's own account and returned `needs_connect`; nothing has started a connection.",
|
|
20
|
+
"The agent has been told NOT to ask in prose, so if your UI stays silent, nothing is said.",
|
|
21
|
+
"",
|
|
22
|
+
" await runAppConnection(chat, { pendingToolCallId, installId, redirectUri })",
|
|
23
|
+
"",
|
|
24
|
+
"Using @frontera-sdk/chat-ui, pass `connectRedirectUri` to <FronteraChat>."
|
|
25
|
+
].join("\n"),
|
|
26
|
+
unauthorized: [
|
|
27
|
+
"A request was refused with UNAUTHORIZED and no refresh is wired, so the next send will fail too.",
|
|
28
|
+
"",
|
|
29
|
+
" const session = createTokenSession({ apiBaseUrl, getToken })",
|
|
30
|
+
" createChatTransport(session.getClient, { onUnauthorized: session.refresh })"
|
|
31
|
+
].join("\n")
|
|
32
|
+
};
|
|
33
|
+
var ChatDoctor = class {
|
|
34
|
+
options = {};
|
|
35
|
+
timers = /* @__PURE__ */ new Map();
|
|
36
|
+
configure(options) {
|
|
37
|
+
this.options = { ...this.options, ...options };
|
|
38
|
+
}
|
|
39
|
+
get enabled() {
|
|
40
|
+
return this.options.enabled ?? defaultEnabled();
|
|
41
|
+
}
|
|
42
|
+
warn(concern) {
|
|
43
|
+
const message = `[frontera-chat] ${ADVICE[concern]}`;
|
|
44
|
+
if (this.options.onWarn) this.options.onWarn(concern, message);
|
|
45
|
+
else console.warn(message);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Something parked. Start counting; `resolved` before the grace period ends
|
|
49
|
+
* cancels it.
|
|
50
|
+
*/
|
|
51
|
+
parked(concern) {
|
|
52
|
+
if (!this.enabled || this.timers.has(concern)) return;
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
this.timers.delete(concern);
|
|
55
|
+
this.warn(concern);
|
|
56
|
+
}, this.options.graceMs ?? DEFAULT_GRACE_MS);
|
|
57
|
+
timer.unref?.();
|
|
58
|
+
this.timers.set(concern, timer);
|
|
59
|
+
}
|
|
60
|
+
/** The integration did the thing. Nothing to warn about. */
|
|
61
|
+
resolved(concern) {
|
|
62
|
+
const timer = this.timers.get(concern);
|
|
63
|
+
if (!timer) return;
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
this.timers.delete(concern);
|
|
66
|
+
}
|
|
67
|
+
/** Immediate, not timed: nothing about a missing refresh improves by waiting. */
|
|
68
|
+
noticed(concern) {
|
|
69
|
+
if (this.enabled) this.warn(concern);
|
|
70
|
+
}
|
|
71
|
+
/** For tests, and for a host that unmounts a chat mid-run. */
|
|
72
|
+
reset() {
|
|
73
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
74
|
+
this.timers.clear();
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Watch a chat stream for the two things that park a run.
|
|
78
|
+
*
|
|
79
|
+
* The body is teed rather than parsed: this reads a copy as text and matches
|
|
80
|
+
* two markers, leaving the real stream untouched for the AI SDK. A full parse
|
|
81
|
+
* here would be a second implementation of the protocol, kept in step with
|
|
82
|
+
* the first by hope.
|
|
83
|
+
*/
|
|
84
|
+
observe(response) {
|
|
85
|
+
if (!this.enabled || !response.body) return response;
|
|
86
|
+
const [forCaller, forDoctor] = response.body.tee();
|
|
87
|
+
void this.scan(forDoctor);
|
|
88
|
+
return new Response(forCaller, {
|
|
89
|
+
status: response.status,
|
|
90
|
+
statusText: response.statusText,
|
|
91
|
+
headers: response.headers
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async scan(stream) {
|
|
95
|
+
const reader = stream.getReader();
|
|
96
|
+
const decoder = new TextDecoder();
|
|
97
|
+
try {
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done) break;
|
|
101
|
+
const text = decoder.decode(value, { stream: true });
|
|
102
|
+
if (text.includes("tool-approval-request")) this.parked("approval");
|
|
103
|
+
if (text.includes("needs_connect")) this.parked("connect");
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
} finally {
|
|
107
|
+
reader.releaseLock();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var chatDoctor = new ChatDoctor();
|
|
112
|
+
|
|
113
|
+
export { chatDoctor };
|
|
114
|
+
//# sourceMappingURL=chunk-VWUR3ECR.js.map
|
|
115
|
+
//# sourceMappingURL=chunk-VWUR3ECR.js.map
|