lemma-sdk 0.2.20 → 0.2.22
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/README.md +14 -1
- package/dist/auth.d.ts +49 -1
- package/dist/auth.js +109 -0
- package/dist/browser/lemma-client.js +115 -1
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/namespaces/assistants.d.ts +2 -0
- package/dist/namespaces/assistants.js +3 -0
- package/dist/openapi_client/index.d.ts +2 -0
- package/dist/openapi_client/models/AvailableModelInfo.d.ts +8 -0
- package/dist/openapi_client/models/AvailableModelInfo.js +1 -0
- package/dist/openapi_client/models/AvailableModels.d.ts +3 -4
- package/dist/openapi_client/models/AvailableModels.js +2 -3
- package/dist/openapi_client/models/AvailableModelsListResponse.d.ts +7 -0
- package/dist/openapi_client/models/AvailableModelsListResponse.js +1 -0
- package/dist/openapi_client/models/FunctionRunResponse.d.ts +2 -0
- package/dist/openapi_client/services/ConversationsService.d.ts +8 -0
- package/dist/openapi_client/services/ConversationsService.js +12 -0
- package/dist/react/components/AssistantExperience.js +10 -2
- package/dist/react/components/assistant-types.d.ts +2 -0
- package/dist/react/useAssistantController.d.ts +2 -1
- package/dist/react/useAssistantController.js +34 -1
- package/dist/react/useAssistantRuntime.js +25 -4
- package/dist/react/useAssistantSession.js +14 -5
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,12 @@ const assistantPayload: CreateAssistantInput = {
|
|
|
108
108
|
## Auth Helpers
|
|
109
109
|
|
|
110
110
|
```ts
|
|
111
|
-
import {
|
|
111
|
+
import {
|
|
112
|
+
LemmaClient,
|
|
113
|
+
buildAuthUrl,
|
|
114
|
+
buildFederatedLogoutUrl,
|
|
115
|
+
resolveSafeRedirectUri,
|
|
116
|
+
} from "lemma-sdk";
|
|
112
117
|
|
|
113
118
|
const client = new LemmaClient({
|
|
114
119
|
apiUrl: "https://api-next.asur.work",
|
|
@@ -131,6 +136,14 @@ await client.auth.signOut();
|
|
|
131
136
|
const token = await client.auth.getAccessToken();
|
|
132
137
|
const refreshed = await client.auth.refreshAccessToken();
|
|
133
138
|
client.auth.redirectToAuth({ mode: "signup", redirectUri: safeRedirect });
|
|
139
|
+
|
|
140
|
+
// Build upstream logout URL (server/client)
|
|
141
|
+
const federatedLogoutUrl = buildFederatedLogoutUrl(client.authUrl, {
|
|
142
|
+
redirectUri: safeRedirect,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Browser: sign out locally, then clear upstream SSO and return to app
|
|
146
|
+
await client.auth.redirectToFederatedLogout({ redirectUri: safeRedirect });
|
|
134
147
|
```
|
|
135
148
|
|
|
136
149
|
### Browser Testing With Injected Token
|
package/dist/auth.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface AuthState {
|
|
|
28
28
|
}
|
|
29
29
|
export type AuthListener = (state: AuthState) => void;
|
|
30
30
|
export type AuthRedirectMode = "login" | "signup";
|
|
31
|
+
type AuthQueryParams = Record<string, string | number | boolean | Array<string | number | boolean> | null | undefined>;
|
|
31
32
|
export interface BuildAuthUrlOptions {
|
|
32
33
|
/** Optional auth path segment relative to authUrl pathname, e.g. "callback" -> /auth/callback. */
|
|
33
34
|
path?: string;
|
|
@@ -36,7 +37,35 @@ export interface BuildAuthUrlOptions {
|
|
|
36
37
|
/** Redirect URI passed to auth service. */
|
|
37
38
|
redirectUri?: string;
|
|
38
39
|
/** Additional query parameters appended to auth URL. */
|
|
39
|
-
params?:
|
|
40
|
+
params?: AuthQueryParams;
|
|
41
|
+
}
|
|
42
|
+
export interface BuildFederatedLogoutUrlOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Optional auth path segment for logout, relative to authUrl pathname.
|
|
45
|
+
* Defaults to "logout" (for example: https://auth.example.com/auth/logout).
|
|
46
|
+
*/
|
|
47
|
+
path?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Post-logout redirect URI passed to the auth service.
|
|
50
|
+
*/
|
|
51
|
+
redirectUri?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Query parameter name used for redirect URI. Defaults to "redirect_uri".
|
|
54
|
+
*/
|
|
55
|
+
redirectParam?: string;
|
|
56
|
+
/** Additional query parameters appended to logout URL. */
|
|
57
|
+
params?: AuthQueryParams;
|
|
58
|
+
}
|
|
59
|
+
export interface RedirectToFederatedLogoutOptions extends Omit<BuildFederatedLogoutUrlOptions, "redirectUri"> {
|
|
60
|
+
/**
|
|
61
|
+
* Post-logout redirect URI. Defaults to current location.
|
|
62
|
+
*/
|
|
63
|
+
redirectUri?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Whether to clear the local session before redirecting upstream.
|
|
66
|
+
* Defaults to true.
|
|
67
|
+
*/
|
|
68
|
+
localSignOut?: boolean;
|
|
40
69
|
}
|
|
41
70
|
export interface ResolveSafeRedirectUriOptions {
|
|
42
71
|
/** Origin for resolving relative paths. */
|
|
@@ -50,6 +79,7 @@ export declare function setTestingToken(token: string): void;
|
|
|
50
79
|
export declare function getTestingToken(): string | null;
|
|
51
80
|
export declare function clearTestingToken(): void;
|
|
52
81
|
export declare function buildAuthUrl(authUrl: string, options?: BuildAuthUrlOptions): string;
|
|
82
|
+
export declare function buildFederatedLogoutUrl(authUrl: string, options?: BuildFederatedLogoutUrlOptions): string;
|
|
53
83
|
export declare function resolveSafeRedirectUri(rawValue: string | null | undefined, options: ResolveSafeRedirectUriOptions): string;
|
|
54
84
|
export declare class AuthManager {
|
|
55
85
|
private readonly apiUrl;
|
|
@@ -72,6 +102,13 @@ export declare class AuthManager {
|
|
|
72
102
|
private setState;
|
|
73
103
|
private assertBrowserContext;
|
|
74
104
|
private getCookie;
|
|
105
|
+
private getCookieDomainCandidates;
|
|
106
|
+
private expireCookie;
|
|
107
|
+
/**
|
|
108
|
+
* Defensive cleanup for stale SuperTokens frontend marker cookies/storage.
|
|
109
|
+
* This helps recover when signout/session-expiry paths leave local markers behind.
|
|
110
|
+
*/
|
|
111
|
+
private clearFrontendSessionMarkers;
|
|
75
112
|
private clearInjectedToken;
|
|
76
113
|
private rawSignOutViaBackend;
|
|
77
114
|
/**
|
|
@@ -112,6 +149,10 @@ export declare class AuthManager {
|
|
|
112
149
|
* Build auth URL for login/signup/custom auth sub-path.
|
|
113
150
|
*/
|
|
114
151
|
getAuthUrl(options?: BuildAuthUrlOptions): string;
|
|
152
|
+
/**
|
|
153
|
+
* Build upstream/federated logout URL.
|
|
154
|
+
*/
|
|
155
|
+
getFederatedLogoutUrl(options?: BuildFederatedLogoutUrlOptions): string;
|
|
115
156
|
/**
|
|
116
157
|
* Redirect to the auth service, passing the current URL as redirect_uri.
|
|
117
158
|
* After the user authenticates, the auth service should redirect back to
|
|
@@ -120,4 +161,11 @@ export declare class AuthManager {
|
|
|
120
161
|
redirectToAuth(options?: Omit<BuildAuthUrlOptions, "redirectUri"> & {
|
|
121
162
|
redirectUri?: string;
|
|
122
163
|
}): void;
|
|
164
|
+
/**
|
|
165
|
+
* Optional full logout flow:
|
|
166
|
+
* 1. clear local SDK/session cookies
|
|
167
|
+
* 2. redirect to auth service logout endpoint to terminate upstream SSO
|
|
168
|
+
*/
|
|
169
|
+
redirectToFederatedLogout(options?: RedirectToFederatedLogoutOptions): Promise<void>;
|
|
123
170
|
}
|
|
171
|
+
export {};
|
package/dist/auth.js
CHANGED
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
import Session from "supertokens-web-js/recipe/session";
|
|
19
19
|
import { ensureCookieSessionSupport } from "./supertokens.js";
|
|
20
20
|
const DEFAULT_BLOCKED_REDIRECT_PATHS = ["/login", "/signup", "/auth"];
|
|
21
|
+
const SUPERTOKENS_FRONTEND_MARKER_KEYS = [
|
|
22
|
+
"sFrontToken",
|
|
23
|
+
"st-last-access-token-update",
|
|
24
|
+
"sIRTFrontend",
|
|
25
|
+
"sAntiCsrf",
|
|
26
|
+
"st-access-token",
|
|
27
|
+
"st-refresh-token",
|
|
28
|
+
];
|
|
21
29
|
const LOCALSTORAGE_TOKEN_KEY = "lemma_token";
|
|
22
30
|
function readStorageToken() {
|
|
23
31
|
if (typeof window === "undefined")
|
|
@@ -121,6 +129,26 @@ export function buildAuthUrl(authUrl, options = {}) {
|
|
|
121
129
|
}
|
|
122
130
|
return url.toString();
|
|
123
131
|
}
|
|
132
|
+
export function buildFederatedLogoutUrl(authUrl, options = {}) {
|
|
133
|
+
const url = new URL(authUrl);
|
|
134
|
+
url.pathname = resolveAuthPath(url.pathname, options.path ?? "logout");
|
|
135
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
136
|
+
if (value === null || value === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
url.searchParams.delete(key);
|
|
140
|
+
for (const item of value) {
|
|
141
|
+
url.searchParams.append(key, String(item));
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
url.searchParams.set(key, String(value));
|
|
146
|
+
}
|
|
147
|
+
if (options.redirectUri && options.redirectUri.trim()) {
|
|
148
|
+
url.searchParams.set(options.redirectParam ?? "redirect_uri", options.redirectUri);
|
|
149
|
+
}
|
|
150
|
+
return url.toString();
|
|
151
|
+
}
|
|
124
152
|
export function resolveSafeRedirectUri(rawValue, options) {
|
|
125
153
|
const siteOrigin = normalizeOrigin(options.siteOrigin);
|
|
126
154
|
const blockedPaths = options.blockedPaths ?? DEFAULT_BLOCKED_REDIRECT_PATHS;
|
|
@@ -194,6 +222,62 @@ export class AuthManager {
|
|
|
194
222
|
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
|
|
195
223
|
return match ? decodeURIComponent(match[1]) : undefined;
|
|
196
224
|
}
|
|
225
|
+
getCookieDomainCandidates() {
|
|
226
|
+
if (typeof window === "undefined") {
|
|
227
|
+
return [undefined];
|
|
228
|
+
}
|
|
229
|
+
const host = window.location.hostname;
|
|
230
|
+
const isIpv4 = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host);
|
|
231
|
+
const isIpv6 = host.includes(":");
|
|
232
|
+
if (!host || host === "localhost" || isIpv4 || isIpv6) {
|
|
233
|
+
return [undefined];
|
|
234
|
+
}
|
|
235
|
+
const domains = new Set();
|
|
236
|
+
const parts = host.split(".").filter(Boolean);
|
|
237
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
238
|
+
const candidate = parts.slice(i).join(".");
|
|
239
|
+
if (!candidate)
|
|
240
|
+
continue;
|
|
241
|
+
domains.add(candidate);
|
|
242
|
+
domains.add(`.${candidate}`);
|
|
243
|
+
}
|
|
244
|
+
return [undefined, ...domains];
|
|
245
|
+
}
|
|
246
|
+
expireCookie(name, domain) {
|
|
247
|
+
if (typeof document === "undefined")
|
|
248
|
+
return;
|
|
249
|
+
const domainPart = domain ? `;domain=${domain}` : "";
|
|
250
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/${domainPart};samesite=lax`;
|
|
251
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/${domainPart}`;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Defensive cleanup for stale SuperTokens frontend marker cookies/storage.
|
|
255
|
+
* This helps recover when signout/session-expiry paths leave local markers behind.
|
|
256
|
+
*/
|
|
257
|
+
clearFrontendSessionMarkers() {
|
|
258
|
+
if (typeof window === "undefined")
|
|
259
|
+
return;
|
|
260
|
+
for (const key of SUPERTOKENS_FRONTEND_MARKER_KEYS) {
|
|
261
|
+
try {
|
|
262
|
+
window.localStorage.removeItem(key);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// ignore storage errors
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
window.sessionStorage.removeItem(key);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// ignore storage errors
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const domains = this.getCookieDomainCandidates();
|
|
275
|
+
for (const key of SUPERTOKENS_FRONTEND_MARKER_KEYS) {
|
|
276
|
+
for (const domain of domains) {
|
|
277
|
+
this.expireCookie(key, domain);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
197
281
|
clearInjectedToken() {
|
|
198
282
|
this.injectedToken = null;
|
|
199
283
|
clearTestingToken();
|
|
@@ -302,6 +386,7 @@ export class AuthManager {
|
|
|
302
386
|
const response = await fetch(`${this.apiUrl}/users/me`, this.getRequestInit({ method: "GET" }));
|
|
303
387
|
// Only 401 means not authenticated — 403 means authenticated but forbidden
|
|
304
388
|
if (response.status === 401) {
|
|
389
|
+
this.clearFrontendSessionMarkers();
|
|
305
390
|
const next = { status: "unauthenticated", user: null };
|
|
306
391
|
this.setState(next);
|
|
307
392
|
return next;
|
|
@@ -328,6 +413,7 @@ export class AuthManager {
|
|
|
328
413
|
* Does NOT redirect — call redirectToAuth() explicitly if desired.
|
|
329
414
|
*/
|
|
330
415
|
markUnauthenticated() {
|
|
416
|
+
this.clearFrontendSessionMarkers();
|
|
331
417
|
this.setState({ status: "unauthenticated", user: null });
|
|
332
418
|
}
|
|
333
419
|
/**
|
|
@@ -368,6 +454,12 @@ export class AuthManager {
|
|
|
368
454
|
getAuthUrl(options = {}) {
|
|
369
455
|
return buildAuthUrl(this.authUrl, options);
|
|
370
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Build upstream/federated logout URL.
|
|
459
|
+
*/
|
|
460
|
+
getFederatedLogoutUrl(options = {}) {
|
|
461
|
+
return buildFederatedLogoutUrl(this.authUrl, options);
|
|
462
|
+
}
|
|
371
463
|
/**
|
|
372
464
|
* Redirect to the auth service, passing the current URL as redirect_uri.
|
|
373
465
|
* After the user authenticates, the auth service should redirect back to
|
|
@@ -380,4 +472,21 @@ export class AuthManager {
|
|
|
380
472
|
const redirectUri = options.redirectUri ?? window.location.href;
|
|
381
473
|
window.location.href = this.getAuthUrl({ ...options, redirectUri });
|
|
382
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* Optional full logout flow:
|
|
477
|
+
* 1. clear local SDK/session cookies
|
|
478
|
+
* 2. redirect to auth service logout endpoint to terminate upstream SSO
|
|
479
|
+
*/
|
|
480
|
+
async redirectToFederatedLogout(options = {}) {
|
|
481
|
+
this.assertBrowserContext();
|
|
482
|
+
const redirectUri = options.redirectUri ?? window.location.href;
|
|
483
|
+
const localSignOut = options.localSignOut ?? true;
|
|
484
|
+
if (localSignOut) {
|
|
485
|
+
await this.signOut();
|
|
486
|
+
}
|
|
487
|
+
window.location.href = this.getFederatedLogoutUrl({
|
|
488
|
+
...options,
|
|
489
|
+
redirectUri,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
383
492
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"./browser.js": function (module, exports, require) {
|
|
4
4
|
"use strict";
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.ApiError = exports.setTestingToken = exports.resolveSafeRedirectUri = exports.getTestingToken = exports.clearTestingToken = exports.buildAuthUrl = exports.AuthManager = exports.LemmaClient = void 0;
|
|
6
|
+
exports.ApiError = exports.setTestingToken = exports.resolveSafeRedirectUri = exports.getTestingToken = exports.clearTestingToken = exports.buildFederatedLogoutUrl = exports.buildAuthUrl = exports.AuthManager = exports.LemmaClient = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* Browser bundle entry point.
|
|
9
9
|
* Exposes LemmaClient as globalThis.LemmaClient.LemmaClient
|
|
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "LemmaClient", { enumerable: true, get: function
|
|
|
19
19
|
var auth_js_1 = require("./auth.js");
|
|
20
20
|
Object.defineProperty(exports, "AuthManager", { enumerable: true, get: function () { return auth_js_1.AuthManager; } });
|
|
21
21
|
Object.defineProperty(exports, "buildAuthUrl", { enumerable: true, get: function () { return auth_js_1.buildAuthUrl; } });
|
|
22
|
+
Object.defineProperty(exports, "buildFederatedLogoutUrl", { enumerable: true, get: function () { return auth_js_1.buildFederatedLogoutUrl; } });
|
|
22
23
|
Object.defineProperty(exports, "clearTestingToken", { enumerable: true, get: function () { return auth_js_1.clearTestingToken; } });
|
|
23
24
|
Object.defineProperty(exports, "getTestingToken", { enumerable: true, get: function () { return auth_js_1.getTestingToken; } });
|
|
24
25
|
Object.defineProperty(exports, "resolveSafeRedirectUri", { enumerable: true, get: function () { return auth_js_1.resolveSafeRedirectUri; } });
|
|
@@ -198,10 +199,19 @@ exports.setTestingToken = setTestingToken;
|
|
|
198
199
|
exports.getTestingToken = getTestingToken;
|
|
199
200
|
exports.clearTestingToken = clearTestingToken;
|
|
200
201
|
exports.buildAuthUrl = buildAuthUrl;
|
|
202
|
+
exports.buildFederatedLogoutUrl = buildFederatedLogoutUrl;
|
|
201
203
|
exports.resolveSafeRedirectUri = resolveSafeRedirectUri;
|
|
202
204
|
const session_1 = require("supertokens-web-js/recipe/session");
|
|
203
205
|
const supertokens_js_1 = require("./supertokens.js");
|
|
204
206
|
const DEFAULT_BLOCKED_REDIRECT_PATHS = ["/login", "/signup", "/auth"];
|
|
207
|
+
const SUPERTOKENS_FRONTEND_MARKER_KEYS = [
|
|
208
|
+
"sFrontToken",
|
|
209
|
+
"st-last-access-token-update",
|
|
210
|
+
"sIRTFrontend",
|
|
211
|
+
"sAntiCsrf",
|
|
212
|
+
"st-access-token",
|
|
213
|
+
"st-refresh-token",
|
|
214
|
+
];
|
|
205
215
|
const LOCALSTORAGE_TOKEN_KEY = "lemma_token";
|
|
206
216
|
function readStorageToken() {
|
|
207
217
|
if (typeof window === "undefined")
|
|
@@ -305,6 +315,26 @@ function buildAuthUrl(authUrl, options = {}) {
|
|
|
305
315
|
}
|
|
306
316
|
return url.toString();
|
|
307
317
|
}
|
|
318
|
+
function buildFederatedLogoutUrl(authUrl, options = {}) {
|
|
319
|
+
const url = new URL(authUrl);
|
|
320
|
+
url.pathname = resolveAuthPath(url.pathname, options.path ?? "logout");
|
|
321
|
+
for (const [key, value] of Object.entries(options.params ?? {})) {
|
|
322
|
+
if (value === null || value === undefined)
|
|
323
|
+
continue;
|
|
324
|
+
if (Array.isArray(value)) {
|
|
325
|
+
url.searchParams.delete(key);
|
|
326
|
+
for (const item of value) {
|
|
327
|
+
url.searchParams.append(key, String(item));
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
url.searchParams.set(key, String(value));
|
|
332
|
+
}
|
|
333
|
+
if (options.redirectUri && options.redirectUri.trim()) {
|
|
334
|
+
url.searchParams.set(options.redirectParam ?? "redirect_uri", options.redirectUri);
|
|
335
|
+
}
|
|
336
|
+
return url.toString();
|
|
337
|
+
}
|
|
308
338
|
function resolveSafeRedirectUri(rawValue, options) {
|
|
309
339
|
const siteOrigin = normalizeOrigin(options.siteOrigin);
|
|
310
340
|
const blockedPaths = options.blockedPaths ?? DEFAULT_BLOCKED_REDIRECT_PATHS;
|
|
@@ -375,6 +405,62 @@ class AuthManager {
|
|
|
375
405
|
const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`));
|
|
376
406
|
return match ? decodeURIComponent(match[1]) : undefined;
|
|
377
407
|
}
|
|
408
|
+
getCookieDomainCandidates() {
|
|
409
|
+
if (typeof window === "undefined") {
|
|
410
|
+
return [undefined];
|
|
411
|
+
}
|
|
412
|
+
const host = window.location.hostname;
|
|
413
|
+
const isIpv4 = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host);
|
|
414
|
+
const isIpv6 = host.includes(":");
|
|
415
|
+
if (!host || host === "localhost" || isIpv4 || isIpv6) {
|
|
416
|
+
return [undefined];
|
|
417
|
+
}
|
|
418
|
+
const domains = new Set();
|
|
419
|
+
const parts = host.split(".").filter(Boolean);
|
|
420
|
+
for (let i = 0; i < parts.length - 1; i += 1) {
|
|
421
|
+
const candidate = parts.slice(i).join(".");
|
|
422
|
+
if (!candidate)
|
|
423
|
+
continue;
|
|
424
|
+
domains.add(candidate);
|
|
425
|
+
domains.add(`.${candidate}`);
|
|
426
|
+
}
|
|
427
|
+
return [undefined, ...domains];
|
|
428
|
+
}
|
|
429
|
+
expireCookie(name, domain) {
|
|
430
|
+
if (typeof document === "undefined")
|
|
431
|
+
return;
|
|
432
|
+
const domainPart = domain ? `;domain=${domain}` : "";
|
|
433
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/${domainPart};samesite=lax`;
|
|
434
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;max-age=0;path=/${domainPart}`;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Defensive cleanup for stale SuperTokens frontend marker cookies/storage.
|
|
438
|
+
* This helps recover when signout/session-expiry paths leave local markers behind.
|
|
439
|
+
*/
|
|
440
|
+
clearFrontendSessionMarkers() {
|
|
441
|
+
if (typeof window === "undefined")
|
|
442
|
+
return;
|
|
443
|
+
for (const key of SUPERTOKENS_FRONTEND_MARKER_KEYS) {
|
|
444
|
+
try {
|
|
445
|
+
window.localStorage.removeItem(key);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// ignore storage errors
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
window.sessionStorage.removeItem(key);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// ignore storage errors
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const domains = this.getCookieDomainCandidates();
|
|
458
|
+
for (const key of SUPERTOKENS_FRONTEND_MARKER_KEYS) {
|
|
459
|
+
for (const domain of domains) {
|
|
460
|
+
this.expireCookie(key, domain);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
378
464
|
clearInjectedToken() {
|
|
379
465
|
this.injectedToken = null;
|
|
380
466
|
clearTestingToken();
|
|
@@ -483,6 +569,7 @@ class AuthManager {
|
|
|
483
569
|
const response = await fetch(`${this.apiUrl}/users/me`, this.getRequestInit({ method: "GET" }));
|
|
484
570
|
// Only 401 means not authenticated — 403 means authenticated but forbidden
|
|
485
571
|
if (response.status === 401) {
|
|
572
|
+
this.clearFrontendSessionMarkers();
|
|
486
573
|
const next = { status: "unauthenticated", user: null };
|
|
487
574
|
this.setState(next);
|
|
488
575
|
return next;
|
|
@@ -509,6 +596,7 @@ class AuthManager {
|
|
|
509
596
|
* Does NOT redirect — call redirectToAuth() explicitly if desired.
|
|
510
597
|
*/
|
|
511
598
|
markUnauthenticated() {
|
|
599
|
+
this.clearFrontendSessionMarkers();
|
|
512
600
|
this.setState({ status: "unauthenticated", user: null });
|
|
513
601
|
}
|
|
514
602
|
/**
|
|
@@ -549,6 +637,12 @@ class AuthManager {
|
|
|
549
637
|
getAuthUrl(options = {}) {
|
|
550
638
|
return buildAuthUrl(this.authUrl, options);
|
|
551
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Build upstream/federated logout URL.
|
|
642
|
+
*/
|
|
643
|
+
getFederatedLogoutUrl(options = {}) {
|
|
644
|
+
return buildFederatedLogoutUrl(this.authUrl, options);
|
|
645
|
+
}
|
|
552
646
|
/**
|
|
553
647
|
* Redirect to the auth service, passing the current URL as redirect_uri.
|
|
554
648
|
* After the user authenticates, the auth service should redirect back to
|
|
@@ -561,6 +655,23 @@ class AuthManager {
|
|
|
561
655
|
const redirectUri = options.redirectUri ?? window.location.href;
|
|
562
656
|
window.location.href = this.getAuthUrl({ ...options, redirectUri });
|
|
563
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Optional full logout flow:
|
|
660
|
+
* 1. clear local SDK/session cookies
|
|
661
|
+
* 2. redirect to auth service logout endpoint to terminate upstream SSO
|
|
662
|
+
*/
|
|
663
|
+
async redirectToFederatedLogout(options = {}) {
|
|
664
|
+
this.assertBrowserContext();
|
|
665
|
+
const redirectUri = options.redirectUri ?? window.location.href;
|
|
666
|
+
const localSignOut = options.localSignOut ?? true;
|
|
667
|
+
if (localSignOut) {
|
|
668
|
+
await this.signOut();
|
|
669
|
+
}
|
|
670
|
+
window.location.href = this.getFederatedLogoutUrl({
|
|
671
|
+
...options,
|
|
672
|
+
redirectUri,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
564
675
|
}
|
|
565
676
|
exports.AuthManager = AuthManager;
|
|
566
677
|
|
|
@@ -1538,6 +1649,9 @@ class ConversationsNamespace {
|
|
|
1538
1649
|
listByAssistant(assistantId, options = {}) {
|
|
1539
1650
|
return this.list({ ...options, assistant_id: assistantId });
|
|
1540
1651
|
}
|
|
1652
|
+
listModels() {
|
|
1653
|
+
return this.http.request("GET", "/models");
|
|
1654
|
+
}
|
|
1541
1655
|
create(payload) {
|
|
1542
1656
|
return this.http.request("POST", "/conversations", {
|
|
1543
1657
|
body: {
|
package/dist/browser.d.ts
CHANGED
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
* </script>
|
|
10
10
|
*/
|
|
11
11
|
export { LemmaClient } from "./client.js";
|
|
12
|
-
export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
12
|
+
export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
13
13
|
export { ApiError } from "./http.js";
|
package/dist/browser.js
CHANGED
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
* </script>
|
|
10
10
|
*/
|
|
11
11
|
export { LemmaClient } from "./client.js";
|
|
12
|
-
export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
12
|
+
export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
13
13
|
export { ApiError } from "./http.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { LemmaClient } from "./client.js";
|
|
2
2
|
export type { LemmaConfig } from "./client.js";
|
|
3
|
-
export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
4
|
-
export type { AuthState, AuthListener, AuthStatus, UserInfo, AuthRedirectMode, BuildAuthUrlOptions, ResolveSafeRedirectUriOptions, } from "./auth.js";
|
|
3
|
+
export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
4
|
+
export type { AuthState, AuthListener, AuthStatus, UserInfo, AuthRedirectMode, BuildAuthUrlOptions, BuildFederatedLogoutUrlOptions, RedirectToFederatedLogoutOptions, ResolveSafeRedirectUriOptions, } from "./auth.js";
|
|
5
5
|
export { ApiError } from "./http.js";
|
|
6
6
|
export * from "./types.js";
|
|
7
7
|
export { readSSE, parseSSEJson } from "./streams.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { LemmaClient } from "./client.js";
|
|
2
|
-
export { AuthManager, buildAuthUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
2
|
+
export { AuthManager, buildAuthUrl, buildFederatedLogoutUrl, clearTestingToken, getTestingToken, resolveSafeRedirectUri, setTestingToken, } from "./auth.js";
|
|
3
3
|
export { ApiError } from "./http.js";
|
|
4
4
|
export * from "./types.js";
|
|
5
5
|
export { readSSE, parseSSEJson } from "./streams.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from "../http.js";
|
|
2
|
+
import type { AvailableModelsListResponse } from "../openapi_client/models/AvailableModelsListResponse.js";
|
|
2
3
|
import type { AssistantListResponse } from "../openapi_client/models/AssistantListResponse.js";
|
|
3
4
|
import type { AssistantResponse } from "../openapi_client/models/AssistantResponse.js";
|
|
4
5
|
import type { ConversationListResponse } from "../openapi_client/models/ConversationListResponse.js";
|
|
@@ -41,6 +42,7 @@ export declare class ConversationsNamespace {
|
|
|
41
42
|
limit?: number;
|
|
42
43
|
page_token?: string;
|
|
43
44
|
}): Promise<ConversationListResponse>;
|
|
45
|
+
listModels(): Promise<AvailableModelsListResponse>;
|
|
44
46
|
create(payload: CreateConversationRequest): Promise<ConversationResponse>;
|
|
45
47
|
createForAssistant(assistantId: string, payload?: Omit<CreateConversationRequest, "assistant_id">): Promise<ConversationResponse>;
|
|
46
48
|
get(conversationId: string, options?: {
|
|
@@ -67,6 +67,9 @@ export class ConversationsNamespace {
|
|
|
67
67
|
listByAssistant(assistantId, options = {}) {
|
|
68
68
|
return this.list({ ...options, assistant_id: assistantId });
|
|
69
69
|
}
|
|
70
|
+
listModels() {
|
|
71
|
+
return this.http.request("GET", "/models");
|
|
72
|
+
}
|
|
70
73
|
create(payload) {
|
|
71
74
|
return this.http.request("POST", "/conversations", {
|
|
72
75
|
body: {
|
|
@@ -26,7 +26,9 @@ export type { AssistantListResponse } from './models/AssistantListResponse.js';
|
|
|
26
26
|
export type { AssistantResponse } from './models/AssistantResponse.js';
|
|
27
27
|
export type { AssistantSurfaceListResponse } from './models/AssistantSurfaceListResponse.js';
|
|
28
28
|
export type { AssistantSurfaceResponse } from './models/AssistantSurfaceResponse.js';
|
|
29
|
+
export type { AvailableModelInfo } from './models/AvailableModelInfo.js';
|
|
29
30
|
export { AvailableModels } from './models/AvailableModels.js';
|
|
31
|
+
export type { AvailableModelsListResponse } from './models/AvailableModelsListResponse.js';
|
|
30
32
|
export { BillingInterval } from './models/BillingInterval.js';
|
|
31
33
|
export type { Body_upload_file_files__resource_type___resource_id__upload_post } from './models/Body_upload_file_files__resource_type___resource_id__upload_post.js';
|
|
32
34
|
export type { BulkCreateRecordsRequest } from './models/BulkCreateRecordsRequest.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -4,8 +4,7 @@ export declare enum AvailableModels {
|
|
|
4
4
|
GEMINI_FLASH_LITE = "GEMINI_FLASH_LITE",
|
|
5
5
|
KIMI_K2 = "KIMI_K2",
|
|
6
6
|
GPT_OSS = "GPT_OSS",
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
GLM_5 = "GLM_5"
|
|
7
|
+
DEEPSEEK_V32 = "DEEPSEEK_V32",
|
|
8
|
+
GLM_5 = "GLM_5",
|
|
9
|
+
QWEN3_6 = "QWEN3_6"
|
|
11
10
|
}
|
|
@@ -9,8 +9,7 @@ export var AvailableModels;
|
|
|
9
9
|
AvailableModels["GEMINI_FLASH_LITE"] = "GEMINI_FLASH_LITE";
|
|
10
10
|
AvailableModels["KIMI_K2"] = "KIMI_K2";
|
|
11
11
|
AvailableModels["GPT_OSS"] = "GPT_OSS";
|
|
12
|
-
AvailableModels["
|
|
13
|
-
AvailableModels["CLAUDE_SONNET_4"] = "CLAUDE_SONNET_4";
|
|
14
|
-
AvailableModels["QWEN3_235B"] = "QWEN3_235B";
|
|
12
|
+
AvailableModels["DEEPSEEK_V32"] = "DEEPSEEK_V32";
|
|
15
13
|
AvailableModels["GLM_5"] = "GLM_5";
|
|
14
|
+
AvailableModels["QWEN3_6"] = "QWEN3_6";
|
|
16
15
|
})(AvailableModels || (AvailableModels = {}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AvailableModelsListResponse } from '../models/AvailableModelsListResponse.js';
|
|
1
2
|
import type { ConversationListResponse } from '../models/ConversationListResponse.js';
|
|
2
3
|
import type { ConversationMessageListResponse } from '../models/ConversationMessageListResponse.js';
|
|
3
4
|
import type { ConversationResponse } from '../models/ConversationResponse.js';
|
|
@@ -76,4 +77,11 @@ export declare class ConversationsService {
|
|
|
76
77
|
* @throws ApiError
|
|
77
78
|
*/
|
|
78
79
|
static conversationStreamResume(conversationId: string, podId?: (string | null)): CancelablePromise<any>;
|
|
80
|
+
/**
|
|
81
|
+
* List Available Models
|
|
82
|
+
* Get list of all available models in the system.
|
|
83
|
+
* @returns AvailableModelsListResponse Successful Response
|
|
84
|
+
* @throws ApiError
|
|
85
|
+
*/
|
|
86
|
+
static conversationModelsList(): CancelablePromise<AvailableModelsListResponse>;
|
|
79
87
|
}
|
|
@@ -185,4 +185,16 @@ export class ConversationsService {
|
|
|
185
185
|
},
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* List Available Models
|
|
190
|
+
* Get list of all available models in the system.
|
|
191
|
+
* @returns AvailableModelsListResponse Successful Response
|
|
192
|
+
* @throws ApiError
|
|
193
|
+
*/
|
|
194
|
+
static conversationModelsList() {
|
|
195
|
+
return __request(OpenAPI, {
|
|
196
|
+
method: 'GET',
|
|
197
|
+
url: '/models',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
188
200
|
}
|
|
@@ -773,7 +773,15 @@ export function AssistantExperienceView({ controller, title = "Lemma Assistant",
|
|
|
773
773
|
const isPinnedToBottomRef = useRef(true);
|
|
774
774
|
const loadingOlderFromScrollRef = useRef(false);
|
|
775
775
|
const isConversationBusy = controller.isLoading || controller.isActiveConversationRunning;
|
|
776
|
-
const availableModels = useMemo(() =>
|
|
776
|
+
const availableModels = useMemo(() => {
|
|
777
|
+
const dynamicModels = controller.availableModels
|
|
778
|
+
.map((model) => model.id)
|
|
779
|
+
.filter((model) => model.trim().length > 0);
|
|
780
|
+
return dynamicModels.length > 0
|
|
781
|
+
? dynamicModels
|
|
782
|
+
: Object.values(AvailableModels);
|
|
783
|
+
}, [controller.availableModels]);
|
|
784
|
+
const availableModelLabels = useMemo(() => new Map(controller.availableModels.map((model) => [model.id, model.name])), [controller.availableModels]);
|
|
777
785
|
const resizeComposer = useCallback(() => {
|
|
778
786
|
const textarea = inputRef.current;
|
|
779
787
|
if (!textarea)
|
|
@@ -1027,7 +1035,7 @@ export function AssistantExperienceView({ controller, title = "Lemma Assistant",
|
|
|
1027
1035
|
return (_jsxs("div", { className: "lemma-assistant-experience", "data-chrome-style": chromeStyle, "data-status-placement": statusPlacement, "data-radius": radius, "data-show-model-picker": showModelPicker ? "true" : "false", "data-busy": isConversationBusy ? "true" : "false", "data-has-plan": planSummary ? "true" : "false", "data-has-pending-files": controller.pendingFiles.length > 0 ? "true" : "false", "data-show-conversation-list": showConversationList ? "true" : "false", children: [showConversationList ? (_jsxs("aside", { className: "lemma-assistant-experience-sidebar", children: [_jsx("div", { className: "lemma-assistant-experience-sidebar-header", children: _jsxs("div", { className: "lemma-assistant-experience-sidebar-header-row", children: [_jsxs("div", { className: "lemma-assistant-experience-sidebar-copy", children: [_jsx("div", { className: "lemma-assistant-experience-sidebar-title", children: "Conversations" }), _jsxs("div", { className: "lemma-assistant-experience-sidebar-meta", children: [controller.conversations.length, " total"] })] }), showNewConversationButton ? (_jsx("button", { type: "button", onClick: controller.clearMessages, className: "lemma-assistant-experience-sidebar-new", children: "New" })) : null] }) }), _jsx("div", { className: "lemma-assistant-experience-sidebar-items", children: controller.conversations.map((conversation) => {
|
|
1028
1036
|
const isActive = conversation.id === controller.activeConversationId;
|
|
1029
1037
|
return (_jsxs("button", { type: "button", onClick: () => controller.selectConversation(conversation.id), className: cx("lemma-assistant-experience-sidebar-item", isActive && "lemma-assistant-experience-sidebar-item-active"), children: [_jsx("div", { className: "lemma-assistant-experience-sidebar-item-title", children: renderConversationLabel({ conversation, isActive }) }), _jsx("div", { className: "lemma-assistant-experience-sidebar-item-status", children: (conversation.status || "waiting").toLowerCase() })] }, conversation.id));
|
|
1030
|
-
}) })] })) : null, _jsxs("div", { className: "lemma-assistant-experience-main", children: [_jsxs("div", { className: "lemma-assistant-experience-card", children: [_jsx(AssistantHeader, { className: "lemma-assistant-experience-header", tone: headerTone, title: title, subtitle: subtitle, badge: _jsx("span", { className: "lemma-assistant-experience-header-badge-icon", children: "\u2728" }), controls: showModelPicker || showNewConversationButton ? (_jsxs(_Fragment, { children: [showModelPicker ? (_jsx(AssistantModelPicker, { value: controller.conversationModel, options: availableModels, onChange: (nextModel) => { void handleModelChange(nextModel); }, disabled: isConversationBusy || isUpdatingModel, autoLabel: "Auto", className: "lemma-assistant-experience-model-picker" })) : null, showNewConversationButton ? (_jsx("button", { type: "button", onClick: controller.clearMessages, title: "New conversation", className: "lemma-assistant-experience-new", children: "\u21BA" })) : null] })) : undefined }), _jsxs(AssistantMessageViewport, { className: "lemma-assistant-experience-viewport", ref: messagesContainerRef, onScroll: updatePinnedState, children: [controller.messages.length === 0 && !isConversationBusy ? (emptyState || (_jsx(EmptyState, { onSendMessage: (message) => { void controller.sendMessage(message); }, suggestions: emptyStateSuggestions }))) : null, (controller.isLoadingMessages && controller.messages.length === 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading", children: _jsx("span", { className: "lemma-assistant-experience-loading-text", children: "Loading\u2026" }) })) : null, (controller.isLoadingOlderMessages && controller.messages.length > 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading-older", children: _jsx("span", { className: "lemma-assistant-experience-loading-older-text", children: "Loading older\u2026" }) })) : null, displayMessageRows.map((row, index) => {
|
|
1038
|
+
}) })] })) : null, _jsxs("div", { className: "lemma-assistant-experience-main", children: [_jsxs("div", { className: "lemma-assistant-experience-card", children: [_jsx(AssistantHeader, { className: "lemma-assistant-experience-header", tone: headerTone, title: title, subtitle: subtitle, badge: _jsx("span", { className: "lemma-assistant-experience-header-badge-icon", children: "\u2728" }), controls: showModelPicker || showNewConversationButton ? (_jsxs(_Fragment, { children: [showModelPicker ? (_jsx(AssistantModelPicker, { value: controller.conversationModel, options: availableModels, getOptionLabel: (model) => availableModelLabels.get(model) ?? model, onChange: (nextModel) => { void handleModelChange(nextModel); }, disabled: isConversationBusy || isUpdatingModel, autoLabel: "Auto", className: "lemma-assistant-experience-model-picker" })) : null, showNewConversationButton ? (_jsx("button", { type: "button", onClick: controller.clearMessages, title: "New conversation", className: "lemma-assistant-experience-new", children: "\u21BA" })) : null] })) : undefined }), _jsxs(AssistantMessageViewport, { className: "lemma-assistant-experience-viewport", ref: messagesContainerRef, onScroll: updatePinnedState, children: [controller.messages.length === 0 && !isConversationBusy ? (emptyState || (_jsx(EmptyState, { onSendMessage: (message) => { void controller.sendMessage(message); }, suggestions: emptyStateSuggestions }))) : null, (controller.isLoadingMessages && controller.messages.length === 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading", children: _jsx("span", { className: "lemma-assistant-experience-loading-text", children: "Loading\u2026" }) })) : null, (controller.isLoadingOlderMessages && controller.messages.length > 0) ? (_jsx("div", { className: "lemma-assistant-experience-loading-older", children: _jsx("span", { className: "lemma-assistant-experience-loading-older-text", children: "Loading older\u2026" }) })) : null, displayMessageRows.map((row, index) => {
|
|
1031
1039
|
const previousRow = index > 0 ? displayMessageRows[index - 1] : null;
|
|
1032
1040
|
const showAssistantHeader = row.message.role !== "assistant"
|
|
1033
1041
|
? false
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { AvailableModelInfo } from "../../types.js";
|
|
2
3
|
import type { AssistantRenderableMessage, AssistantToolInvocation } from "../useAssistantController.js";
|
|
3
4
|
export interface AssistantConversationListItem {
|
|
4
5
|
id: string;
|
|
@@ -11,6 +12,7 @@ export interface AssistantControllerView {
|
|
|
11
12
|
messages: AssistantRenderableMessage[];
|
|
12
13
|
conversations: AssistantConversationListItem[];
|
|
13
14
|
activeConversationId: string | null;
|
|
15
|
+
availableModels: AvailableModelInfo[];
|
|
14
16
|
conversationModel: string | null;
|
|
15
17
|
setConversationModel(model: string | null): Promise<void>;
|
|
16
18
|
isActiveConversationRunning: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LemmaClient } from "../client.js";
|
|
2
|
-
import type { Conversation, ConversationModel } from "../types.js";
|
|
2
|
+
import type { AvailableModelInfo, Conversation, ConversationModel } from "../types.js";
|
|
3
3
|
export interface AssistantConversationScope {
|
|
4
4
|
podId?: string | null;
|
|
5
5
|
assistantId?: string | null;
|
|
@@ -54,6 +54,7 @@ export interface UseAssistantControllerResult {
|
|
|
54
54
|
messages: AssistantRenderableMessage[];
|
|
55
55
|
conversations: Conversation[];
|
|
56
56
|
activeConversationId: string | null;
|
|
57
|
+
availableModels: AvailableModelInfo[];
|
|
57
58
|
conversationModel: ConversationModel | null;
|
|
58
59
|
isActiveConversationRunning: boolean;
|
|
59
60
|
isLoading: boolean;
|
|
@@ -543,6 +543,7 @@ export function useAssistantController({ client, podId, assistantId, organizatio
|
|
|
543
543
|
const [messages, setMessages] = useState([]);
|
|
544
544
|
const [conversations, setConversations] = useState([]);
|
|
545
545
|
const [activeConversationId, setActiveConversationId] = useState(null);
|
|
546
|
+
const [availableModels, setAvailableModels] = useState([]);
|
|
546
547
|
const [conversationModel, setConversationModelState] = useState(null);
|
|
547
548
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
548
549
|
const [isLoadingConversations, setIsLoadingConversations] = useState(false);
|
|
@@ -649,6 +650,15 @@ export function useAssistantController({ client, podId, assistantId, organizatio
|
|
|
649
650
|
setIsLoadingConversations(false);
|
|
650
651
|
}
|
|
651
652
|
}, [scope, sessionListConversations]);
|
|
653
|
+
const loadAvailableModels = useCallback(async () => {
|
|
654
|
+
try {
|
|
655
|
+
const response = await client.conversations.listModels();
|
|
656
|
+
return response.items ?? [];
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return [];
|
|
660
|
+
}
|
|
661
|
+
}, [client]);
|
|
652
662
|
const loadConversationMessages = useCallback(async (conversationId) => {
|
|
653
663
|
setIsLoadingMessages(true);
|
|
654
664
|
try {
|
|
@@ -706,6 +716,23 @@ export function useAssistantController({ client, podId, assistantId, organizatio
|
|
|
706
716
|
useEffect(() => {
|
|
707
717
|
conversationsRef.current = conversations;
|
|
708
718
|
}, [conversations]);
|
|
719
|
+
useEffect(() => {
|
|
720
|
+
if (!enabled) {
|
|
721
|
+
setAvailableModels([]);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
let cancelled = false;
|
|
725
|
+
void loadAvailableModels()
|
|
726
|
+
.then((models) => {
|
|
727
|
+
if (cancelled)
|
|
728
|
+
return;
|
|
729
|
+
setAvailableModels(models);
|
|
730
|
+
})
|
|
731
|
+
.catch(() => undefined);
|
|
732
|
+
return () => {
|
|
733
|
+
cancelled = true;
|
|
734
|
+
};
|
|
735
|
+
}, [enabled, loadAvailableModels]);
|
|
709
736
|
useEffect(() => {
|
|
710
737
|
const conversationId = activeConversationIdRef.current;
|
|
711
738
|
if (!conversationId) {
|
|
@@ -759,6 +786,7 @@ export function useAssistantController({ client, podId, assistantId, organizatio
|
|
|
759
786
|
loadingConversationIdRef.current = null;
|
|
760
787
|
skipInitialLoadConversationIdsRef.current.clear();
|
|
761
788
|
setActiveConversationId(null);
|
|
789
|
+
setAvailableModels([]);
|
|
762
790
|
setConversationModelState(null);
|
|
763
791
|
setConversations([]);
|
|
764
792
|
setMessages([]);
|
|
@@ -842,8 +870,12 @@ export function useAssistantController({ client, podId, assistantId, organizatio
|
|
|
842
870
|
const conversationIsRunning = isConversationRunning(activeConversation?.status);
|
|
843
871
|
if (!hadActiveStream && !conversationIsRunning)
|
|
844
872
|
return;
|
|
873
|
+
const previousStatus = activeConversation?.status;
|
|
845
874
|
touchConversation(conversationId, { status: "waiting" });
|
|
846
|
-
void sessionStop(conversationId).catch(() =>
|
|
875
|
+
void sessionStop(conversationId).catch((error) => {
|
|
876
|
+
touchConversation(conversationId, { status: previousStatus });
|
|
877
|
+
setLocalError((prev) => prev || (error instanceof Error ? error.message : "Failed to stop conversation"));
|
|
878
|
+
});
|
|
847
879
|
}, [isStreaming, sessionCancel, sessionIsStreaming, sessionStop, touchConversation]);
|
|
848
880
|
const selectConversation = useCallback((conversationId) => {
|
|
849
881
|
if (sessionIsStreaming || isStreaming) {
|
|
@@ -1066,6 +1098,7 @@ export function useAssistantController({ client, podId, assistantId, organizatio
|
|
|
1066
1098
|
messages,
|
|
1067
1099
|
conversations,
|
|
1068
1100
|
activeConversationId,
|
|
1101
|
+
availableModels,
|
|
1069
1102
|
conversationModel,
|
|
1070
1103
|
isActiveConversationRunning,
|
|
1071
1104
|
isLoading,
|
|
@@ -21,6 +21,7 @@ function messageTime(message) {
|
|
|
21
21
|
function isOptimisticId(messageId) {
|
|
22
22
|
return messageId.startsWith("optimistic-user-");
|
|
23
23
|
}
|
|
24
|
+
const OPTIMISTIC_MATCH_WINDOW_MS = 2 * 60 * 1000;
|
|
24
25
|
function upsertRuntimeMessage(previous, incoming) {
|
|
25
26
|
const next = [...previous];
|
|
26
27
|
const directIndex = next.findIndex((message) => message.id === incoming.id);
|
|
@@ -31,9 +32,22 @@ function upsertRuntimeMessage(previous, incoming) {
|
|
|
31
32
|
if (incoming.role === "user") {
|
|
32
33
|
const incomingText = messageText(incoming.content);
|
|
33
34
|
if (incomingText) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
const incomingTimestamp = messageTime(incoming);
|
|
36
|
+
let optimisticIndex = -1;
|
|
37
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
38
|
+
next.forEach((message, index) => {
|
|
39
|
+
if (message.role !== "user"
|
|
40
|
+
|| !isOptimisticId(message.id)
|
|
41
|
+
|| messageText(message.content) !== incomingText) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const distance = Math.abs(messageTime(message) - incomingTimestamp);
|
|
45
|
+
if (distance > OPTIMISTIC_MATCH_WINDOW_MS || distance >= bestDistance) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
optimisticIndex = index;
|
|
49
|
+
bestDistance = distance;
|
|
50
|
+
});
|
|
37
51
|
if (optimisticIndex >= 0) {
|
|
38
52
|
next[optimisticIndex] = incoming;
|
|
39
53
|
return next;
|
|
@@ -71,7 +85,14 @@ export function useAssistantRuntime({ conversationId = null, sessionMessages = [
|
|
|
71
85
|
const normalized = messages
|
|
72
86
|
.map((message) => toRuntimeMessage(message, conversationId))
|
|
73
87
|
.filter((message) => !conversationId || message.conversation_id === conversationId);
|
|
74
|
-
setRuntimeMessages(
|
|
88
|
+
setRuntimeMessages((previous) => {
|
|
89
|
+
const scopedPrevious = previous.filter((message) => !conversationId || message.conversation_id === conversationId);
|
|
90
|
+
// Loads can complete after optimistic appends or stream events. Merge the
|
|
91
|
+
// loaded snapshot into the current runtime state so newer local messages
|
|
92
|
+
// are not temporarily dropped while the server catches up.
|
|
93
|
+
const merged = normalized.reduce((accumulator, message) => upsertRuntimeMessage(accumulator, message), scopedPrevious);
|
|
94
|
+
return [...merged].sort((a, b) => messageTime(a) - messageTime(b));
|
|
95
|
+
});
|
|
75
96
|
}, [conversationId]);
|
|
76
97
|
const appendOptimisticUserMessage = useCallback((content, options) => {
|
|
77
98
|
const trimmed = content.trim();
|
|
@@ -393,12 +393,21 @@ export function useAssistantSession(options) {
|
|
|
393
393
|
return false;
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
+
const previousResumeKey = autoResumedKeyRef.current;
|
|
396
397
|
autoResumedKeyRef.current = resumeKey;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
398
|
+
try {
|
|
399
|
+
await resume({
|
|
400
|
+
conversationId: id,
|
|
401
|
+
onlyIfRunning: true,
|
|
402
|
+
});
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (autoResumedKeyRef.current === resumeKey) {
|
|
407
|
+
autoResumedKeyRef.current = previousResumeKey;
|
|
408
|
+
}
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
402
411
|
}, [conversationId, isStreaming, refreshConversation, resume]);
|
|
403
412
|
const stop = useCallback(async (explicitConversationId) => {
|
|
404
413
|
const id = requireConversationId(explicitConversationId ?? conversationId);
|
package/dist/types.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export type CreateAssistantInput = CreateAssistantRequest;
|
|
|
40
40
|
export type UpdateAssistantInput = UpdateAssistantRequest;
|
|
41
41
|
export type Conversation = ConversationResponse;
|
|
42
42
|
export type ConversationMessage = ConversationMessageResponse;
|
|
43
|
-
export type ConversationModel = `${AvailableModels}
|
|
43
|
+
export type ConversationModel = `${AvailableModels}` | (string & {});
|
|
44
44
|
export type Task = TaskResponse;
|
|
45
45
|
export type TaskMessage = TaskMessageResponse;
|
|
46
46
|
export type FunctionRun = FunctionRunResponse;
|