@astra-code/astra-ai 0.1.6 → 0.1.8
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/app/App.js +223 -58
- package/dist/app/App.js.map +1 -1
- package/dist/lib/backendClient.js +87 -17
- package/dist/lib/backendClient.js.map +1 -1
- package/dist/lib/voice.js +3 -0
- package/dist/lib/voice.js.map +1 -1
- package/package.json +1 -1
- package/src/app/App.tsx +277 -92
- package/src/lib/backendClient.ts +88 -17
- package/src/lib/voice.ts +2 -0
- package/src/types/events.ts +1 -0
package/src/lib/backendClient.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./config.js";
|
|
8
8
|
import type {AgentEvent, AuthSession, ChatMessage} from "../types/events.js";
|
|
9
9
|
import type {WorkspaceFile} from "./workspaceScanner.js";
|
|
10
|
-
import {loadSession} from "./sessionStore.js";
|
|
10
|
+
import {loadSession, saveSession} from "./sessionStore.js";
|
|
11
11
|
|
|
12
12
|
type JsonRecord = Record<string, unknown>;
|
|
13
13
|
|
|
@@ -22,14 +22,55 @@ export type SessionSummary = {
|
|
|
22
22
|
export class BackendClient {
|
|
23
23
|
private readonly baseUrl: string;
|
|
24
24
|
private authToken: string | null;
|
|
25
|
+
private refreshToken: string | null;
|
|
26
|
+
private onTokenRefreshed: ((session: AuthSession) => void) | null = null;
|
|
25
27
|
|
|
26
28
|
public constructor(baseUrl = getBackendUrl()) {
|
|
27
29
|
this.baseUrl = baseUrl;
|
|
28
|
-
|
|
30
|
+
const stored = loadSession();
|
|
31
|
+
this.authToken = stored?.access_token?.trim() || null;
|
|
32
|
+
this.refreshToken = stored?.refresh_token?.trim() || null;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
public setAuthSession(session: AuthSession | null): void {
|
|
32
36
|
this.authToken = session?.access_token?.trim() || null;
|
|
37
|
+
this.refreshToken = session?.refresh_token?.trim() || null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public setOnTokenRefreshed(cb: (session: AuthSession) => void): void {
|
|
41
|
+
this.onTokenRefreshed = cb;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async tryRefreshToken(): Promise<boolean> {
|
|
45
|
+
if (!this.refreshToken) return false;
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${this.baseUrl}/api/auth/refresh`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {"content-type": "application/json"},
|
|
50
|
+
body: JSON.stringify({refresh_token: this.refreshToken})
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) return false;
|
|
53
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
54
|
+
const newAccess = typeof data.access_token === "string" ? data.access_token.trim() : null;
|
|
55
|
+
if (!newAccess) return false;
|
|
56
|
+
const newRefresh = typeof data.refresh_token === "string" ? data.refresh_token.trim() : this.refreshToken;
|
|
57
|
+
this.authToken = newAccess;
|
|
58
|
+
this.refreshToken = newRefresh;
|
|
59
|
+
// Merge with stored session and persist
|
|
60
|
+
const stored = loadSession();
|
|
61
|
+
if (stored) {
|
|
62
|
+
const refreshed: AuthSession = {
|
|
63
|
+
...stored,
|
|
64
|
+
access_token: newAccess,
|
|
65
|
+
refresh_token: newRefresh ?? undefined
|
|
66
|
+
};
|
|
67
|
+
saveSession(refreshed);
|
|
68
|
+
this.onTokenRefreshed?.(refreshed);
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
33
74
|
}
|
|
34
75
|
|
|
35
76
|
public async get(path: string, params?: Record<string, string>): Promise<JsonRecord> {
|
|
@@ -209,6 +250,7 @@ export class BackendClient {
|
|
|
209
250
|
workspaceTree?: string[];
|
|
210
251
|
workspaceFiles?: WorkspaceFile[];
|
|
211
252
|
model?: string;
|
|
253
|
+
signal?: AbortSignal;
|
|
212
254
|
}): AsyncGenerator<AgentEvent, void, void> {
|
|
213
255
|
const model = payload.model ?? getDefaultModel();
|
|
214
256
|
const body = {
|
|
@@ -230,20 +272,33 @@ export class BackendClient {
|
|
|
230
272
|
user_id: payload.user.user_id,
|
|
231
273
|
org_id: payload.user.org_id
|
|
232
274
|
};
|
|
233
|
-
return this.streamSse("/api/agent/chat/stream", body);
|
|
275
|
+
return this.streamSse("/api/agent/chat/stream", body, payload.signal);
|
|
234
276
|
}
|
|
235
277
|
|
|
236
278
|
private async request(url: string, method: string, payload?: JsonRecord): Promise<JsonRecord> {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
279
|
+
const makeHeaders = (): Record<string, string> => {
|
|
280
|
+
const h: Record<string, string> = {"content-type": "application/json"};
|
|
281
|
+
if (this.authToken) h.authorization = `Bearer ${this.authToken}`;
|
|
282
|
+
return h;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
let response = await fetch(url, {
|
|
242
286
|
method,
|
|
243
|
-
headers,
|
|
287
|
+
headers: makeHeaders(),
|
|
244
288
|
body: payload ? JSON.stringify(payload) : null
|
|
245
289
|
});
|
|
246
290
|
|
|
291
|
+
if (response.status === 401) {
|
|
292
|
+
const refreshed = await this.tryRefreshToken();
|
|
293
|
+
if (refreshed) {
|
|
294
|
+
response = await fetch(url, {
|
|
295
|
+
method,
|
|
296
|
+
headers: makeHeaders(),
|
|
297
|
+
body: payload ? JSON.stringify(payload) : null
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
247
302
|
if (!response.ok) {
|
|
248
303
|
const detail = (await response.text()).trim();
|
|
249
304
|
throw new Error(`Backend error ${response.status}: ${detail || response.statusText}`);
|
|
@@ -256,17 +311,33 @@ export class BackendClient {
|
|
|
256
311
|
return JSON.parse(text) as JsonRecord;
|
|
257
312
|
}
|
|
258
313
|
|
|
259
|
-
private async *streamSse(path: string, payload: JsonRecord): AsyncGenerator<AgentEvent> {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
314
|
+
private async *streamSse(path: string, payload: JsonRecord, signal?: AbortSignal): AsyncGenerator<AgentEvent> {
|
|
315
|
+
const makeHeaders = (): Record<string, string> => {
|
|
316
|
+
const h: Record<string, string> = {"content-type": "application/json"};
|
|
317
|
+
if (this.authToken) h.authorization = `Bearer ${this.authToken}`;
|
|
318
|
+
return h;
|
|
319
|
+
};
|
|
320
|
+
const sig = signal ?? null;
|
|
321
|
+
|
|
322
|
+
let response = await fetch(`${this.baseUrl}${path}`, {
|
|
265
323
|
method: "POST",
|
|
266
|
-
headers,
|
|
267
|
-
body: JSON.stringify(payload)
|
|
324
|
+
headers: makeHeaders(),
|
|
325
|
+
body: JSON.stringify(payload),
|
|
326
|
+
signal: sig
|
|
268
327
|
});
|
|
269
328
|
|
|
329
|
+
if (response.status === 401) {
|
|
330
|
+
const refreshed = await this.tryRefreshToken();
|
|
331
|
+
if (refreshed) {
|
|
332
|
+
response = await fetch(`${this.baseUrl}${path}`, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: makeHeaders(),
|
|
335
|
+
body: JSON.stringify(payload),
|
|
336
|
+
signal: sig
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
270
341
|
if (!response.ok) {
|
|
271
342
|
const detail = (await response.text()).trim();
|
|
272
343
|
throw new Error(`Backend error ${response.status}: ${detail || response.statusText}`);
|
package/src/lib/voice.ts
CHANGED
|
@@ -280,6 +280,8 @@ export const startLiveTranscription = (handlers: {
|
|
|
280
280
|
audioPath = await captureAudioChunk(DEFAULT_CHUNK_SECONDS);
|
|
281
281
|
const piece = await transcribeAudioFile(audioPath);
|
|
282
282
|
consecutiveErrors = 0;
|
|
283
|
+
// Discard result if stop() was called while we were capturing/transcribing.
|
|
284
|
+
if (!running) break;
|
|
283
285
|
if (piece) {
|
|
284
286
|
transcript = transcript ? `${transcript} ${piece}` : piece;
|
|
285
287
|
handlers.onPartial(transcript);
|