@theseam/ui-common 1.0.2-beta.84 → 1.0.2-beta.86
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/ai/index.d.ts +170 -17
- package/fesm2022/theseam-ui-common-ai.mjs +433 -103
- package/fesm2022/theseam-ui-common-ai.mjs.map +1 -1
- package/fesm2022/theseam-ui-common-datatable.mjs +8 -8
- package/fesm2022/theseam-ui-common-datatable.mjs.map +1 -1
- package/fesm2022/theseam-ui-common-popover.mjs +2 -2
- package/fesm2022/theseam-ui-common-popover.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { defer, Observable, of, throwError, BehaviorSubject, Subject, EMPTY, switchMap as switchMap$1, startWith, map, shareReplay, firstValueFrom } from 'rxjs';
|
|
2
|
+
import { delay, tap, switchMap, catchError, takeUntil } from 'rxjs/operators';
|
|
1
3
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, InjectionToken, inject, Injector, Input, ChangeDetectionStrategy, Component, EventEmitter, Output, ChangeDetectorRef, NgZone, ViewChild } from '@angular/core';
|
|
4
|
+
import { Injectable, InjectionToken, inject, Injector, Input, ChangeDetectionStrategy, Component, EventEmitter, Output, ChangeDetectorRef, NgZone, input, output, effect, ViewChild } from '@angular/core';
|
|
3
5
|
import { print } from 'graphql';
|
|
4
6
|
import { processGql } from '@theseam/ui-common/graphql';
|
|
5
7
|
import { NgForOf, NgIf, NgComponentOutlet, AsyncPipe, JsonPipe } from '@angular/common';
|
|
6
|
-
import { BehaviorSubject, switchMap, of, startWith, map, shareReplay } from 'rxjs';
|
|
7
8
|
import { TheSeamOverlayScrollbarDirective } from '@theseam/ui-common/scrollbar';
|
|
8
9
|
import { MarkdownComponent } from 'ngx-markdown';
|
|
9
10
|
import * as i1 from '@angular/forms';
|
|
@@ -20,69 +21,217 @@ import * as i2$1 from '@theseam/ui-common/loading';
|
|
|
20
21
|
import { TheSeamLoadingModule } from '@theseam/ui-common/loading';
|
|
21
22
|
import { AlterationsDiffComponent } from '@theseam/ui-common/datatable-alterations-display';
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Errored by `TheSeamAiProvider.chat()` when the server reports the
|
|
26
|
+
* session's leaf has advanced since the client last observed it (HTTP 409).
|
|
27
|
+
* The chat component catches this, reloads the session, and emits
|
|
28
|
+
* `(staleSession)`.
|
|
29
|
+
*/
|
|
30
|
+
class ChatSessionStaleError extends Error {
|
|
31
|
+
sessionId;
|
|
32
|
+
currentLeafMessageId;
|
|
33
|
+
constructor(sessionId, currentLeafMessageId) {
|
|
34
|
+
super('Chat session leaf is stale');
|
|
35
|
+
this.sessionId = sessionId;
|
|
36
|
+
this.currentLeafMessageId = currentLeafMessageId;
|
|
37
|
+
this.name = 'ChatSessionStaleError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
class LmStudioAiProvider {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
chat(request) {
|
|
43
|
+
return defer(() => {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const promise = (async () => {
|
|
46
|
+
const url = 'http://localhost:1234/v1/chat/completions';
|
|
47
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
48
|
+
const model = 'model-identifier';
|
|
49
|
+
const response = await fetch(url, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
model,
|
|
54
|
+
messages: request.messages.map((m) => ({
|
|
55
|
+
role: m.role,
|
|
56
|
+
content: m.content,
|
|
57
|
+
})),
|
|
58
|
+
}),
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
const content = data.choices[0].message.content;
|
|
63
|
+
return {
|
|
64
|
+
content,
|
|
65
|
+
// LM Studio is provider-only; no real session backend.
|
|
66
|
+
sessionId: request.sessionId ?? 'lm-studio-local',
|
|
67
|
+
label: 'LM Studio',
|
|
68
|
+
leafMessageId: crypto.randomUUID(),
|
|
69
|
+
};
|
|
70
|
+
})();
|
|
71
|
+
promise.finally(() => {
|
|
72
|
+
/* avoid unhandled-rejection on unsubscribe */
|
|
73
|
+
});
|
|
74
|
+
return new Observable((subscriber) => {
|
|
75
|
+
promise.then((value) => {
|
|
76
|
+
subscriber.next(value);
|
|
77
|
+
subscriber.complete();
|
|
78
|
+
}, (err) => subscriber.error(err));
|
|
79
|
+
return () => controller.abort();
|
|
80
|
+
});
|
|
40
81
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return
|
|
82
|
+
}
|
|
83
|
+
getInitialSession() {
|
|
84
|
+
return of(null);
|
|
85
|
+
}
|
|
86
|
+
getRecentSession() {
|
|
87
|
+
return of(null);
|
|
88
|
+
}
|
|
89
|
+
getSession(_uid) {
|
|
90
|
+
return throwError(() => new Error('LmStudioAiProvider does not support session persistence.'));
|
|
91
|
+
}
|
|
92
|
+
listSessions() {
|
|
93
|
+
return of([]);
|
|
94
|
+
}
|
|
95
|
+
renameSession(_uid, _label) {
|
|
96
|
+
return throwError(() => new Error('LmStudioAiProvider does not support session persistence.'));
|
|
97
|
+
}
|
|
98
|
+
deleteSession(_uid) {
|
|
99
|
+
return throwError(() => new Error('LmStudioAiProvider does not support session persistence.'));
|
|
44
100
|
}
|
|
45
101
|
}
|
|
46
102
|
|
|
47
103
|
class OpenRouterAiProvider {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
104
|
+
chat(request) {
|
|
105
|
+
return defer(() => {
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const promise = (async () => {
|
|
108
|
+
const defaultApiKey = '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';
|
|
109
|
+
const url = 'https://openrouter.ai/api/v1/chat/completions';
|
|
110
|
+
const apiKey = localStorage.getItem('openrouter-api-key') || defaultApiKey;
|
|
111
|
+
const headers = {
|
|
112
|
+
Authorization: `Bearer ${apiKey}`,
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
};
|
|
115
|
+
const model = 'google/gemini-2.5-flash';
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers,
|
|
119
|
+
body: JSON.stringify({
|
|
120
|
+
model,
|
|
121
|
+
messages: request.messages.map((m) => ({
|
|
122
|
+
role: m.role,
|
|
123
|
+
content: m.content,
|
|
124
|
+
})),
|
|
125
|
+
response_format: { type: 'json_object' },
|
|
126
|
+
}),
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
});
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
const content = data.choices[0].message.content;
|
|
131
|
+
return {
|
|
132
|
+
content,
|
|
133
|
+
sessionId: request.sessionId ?? 'openrouter-remote',
|
|
134
|
+
label: 'OpenRouter',
|
|
135
|
+
leafMessageId: crypto.randomUUID(),
|
|
136
|
+
};
|
|
137
|
+
})();
|
|
138
|
+
promise.finally(() => {
|
|
139
|
+
/* avoid unhandled-rejection on unsubscribe */
|
|
140
|
+
});
|
|
141
|
+
return new Observable((subscriber) => {
|
|
142
|
+
promise.then((value) => {
|
|
143
|
+
subscriber.next(value);
|
|
144
|
+
subscriber.complete();
|
|
145
|
+
}, (err) => subscriber.error(err));
|
|
146
|
+
return () => controller.abort();
|
|
147
|
+
});
|
|
68
148
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return
|
|
149
|
+
}
|
|
150
|
+
getInitialSession() {
|
|
151
|
+
return of(null);
|
|
152
|
+
}
|
|
153
|
+
getRecentSession() {
|
|
154
|
+
return of(null);
|
|
155
|
+
}
|
|
156
|
+
getSession(_uid) {
|
|
157
|
+
return throwError(() => new Error('OpenRouterAiProvider does not support session persistence.'));
|
|
158
|
+
}
|
|
159
|
+
listSessions() {
|
|
160
|
+
return of([]);
|
|
161
|
+
}
|
|
162
|
+
renameSession(_uid, _label) {
|
|
163
|
+
return throwError(() => new Error('OpenRouterAiProvider does not support session persistence.'));
|
|
164
|
+
}
|
|
165
|
+
deleteSession(_uid) {
|
|
166
|
+
return throwError(() => new Error('OpenRouterAiProvider does not support session persistence.'));
|
|
72
167
|
}
|
|
73
168
|
}
|
|
74
169
|
|
|
75
170
|
class MockAiProvider {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
171
|
+
_config;
|
|
172
|
+
_throwOnNextChat;
|
|
173
|
+
constructor(configOrLegacy) {
|
|
174
|
+
if (typeof configOrLegacy === 'string' ||
|
|
175
|
+
typeof configOrLegacy === 'function') {
|
|
176
|
+
this._config = { response: configOrLegacy };
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
this._config = configOrLegacy ?? {};
|
|
180
|
+
}
|
|
181
|
+
this._throwOnNextChat = this._config.throwOnFirstChat;
|
|
182
|
+
}
|
|
183
|
+
chat(request) {
|
|
184
|
+
return defer(() => {
|
|
185
|
+
if (this._throwOnNextChat) {
|
|
186
|
+
const err = this._throwOnNextChat;
|
|
187
|
+
this._throwOnNextChat = undefined;
|
|
188
|
+
return throwError(() => err);
|
|
189
|
+
}
|
|
190
|
+
const content = typeof this._config.response === 'function'
|
|
191
|
+
? this._config.response(request.messages)
|
|
192
|
+
: (this._config.response ?? 'Mock response');
|
|
193
|
+
const response = {
|
|
194
|
+
content,
|
|
195
|
+
sessionId: request.sessionId ?? `mock-session-${cryptoRandomId()}`,
|
|
196
|
+
label: 'Mock',
|
|
197
|
+
leafMessageId: cryptoRandomId(),
|
|
198
|
+
};
|
|
199
|
+
return this._withDelay('chat', of(response));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
getInitialSession() {
|
|
203
|
+
return defer(() => this._withDelay('getInitialSession', of(this._config.initialSession ?? null)));
|
|
79
204
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
? this._response(request.messages)
|
|
83
|
-
: this._response;
|
|
84
|
-
return { content };
|
|
205
|
+
getRecentSession() {
|
|
206
|
+
return defer(() => this._withDelay('getRecentSession', of(this._config.initialSession ?? null)));
|
|
85
207
|
}
|
|
208
|
+
getSession(uid) {
|
|
209
|
+
return defer(() => {
|
|
210
|
+
const found = this._config.sessionsByUid?.get(uid);
|
|
211
|
+
if (!found) {
|
|
212
|
+
return throwError(() => new Error(`MockAiProvider: session not found for uid "${uid}"`));
|
|
213
|
+
}
|
|
214
|
+
return this._withDelay('getSession', of(found));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
listSessions() {
|
|
218
|
+
return defer(() => this._withDelay('listSessions', of(this._config.sessionsList ?? [])));
|
|
219
|
+
}
|
|
220
|
+
renameSession(_uid, _label) {
|
|
221
|
+
return defer(() => this._withDelay('renameSession', of(undefined)));
|
|
222
|
+
}
|
|
223
|
+
deleteSession(_uid) {
|
|
224
|
+
return defer(() => this._withDelay('deleteSession', of(undefined)));
|
|
225
|
+
}
|
|
226
|
+
_withDelay(method, source$) {
|
|
227
|
+
const ms = this._config.delayMsByMethod?.[method] ?? this._config.delayMs ?? 0;
|
|
228
|
+
return ms > 0 ? source$.pipe(delay(ms)) : source$;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function cryptoRandomId() {
|
|
232
|
+
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
233
|
+
? crypto.randomUUID()
|
|
234
|
+
: `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
86
235
|
}
|
|
87
236
|
|
|
88
237
|
class TheSeamChatContextRegistry {
|
|
@@ -309,6 +458,14 @@ class SeamChatInputComponent {
|
|
|
309
458
|
this.messageSent.emit(value);
|
|
310
459
|
this._control.reset();
|
|
311
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Restores the given text into the input control. Called by the parent
|
|
463
|
+
* chat component during stale-leaf recovery so the user can edit and
|
|
464
|
+
* resend their message after the conversation is refreshed.
|
|
465
|
+
*/
|
|
466
|
+
restoreText(text) {
|
|
467
|
+
this._control.setValue(text);
|
|
468
|
+
}
|
|
312
469
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SeamChatInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
313
470
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SeamChatInputComponent, isStandalone: true, selector: "seam-chat-input", inputs: { placeholder: "placeholder", disabled: "disabled" }, outputs: { messageSent: "messageSent" }, ngImport: i0, template: `
|
|
314
471
|
<div class="seam-chat-input">
|
|
@@ -379,16 +536,78 @@ class TheSeamChatComponent {
|
|
|
379
536
|
});
|
|
380
537
|
_cdr = inject(ChangeDetectorRef);
|
|
381
538
|
_ngZone = inject(NgZone);
|
|
382
|
-
placeholder = 'Type a message...';
|
|
383
539
|
_messageList;
|
|
384
540
|
_messageListScrollbar;
|
|
385
|
-
|
|
541
|
+
_chatInput;
|
|
542
|
+
/**
|
|
543
|
+
* The session this chat should display.
|
|
544
|
+
*
|
|
545
|
+
* - `null` on first init: the component asks the provider for an initial
|
|
546
|
+
* session via `getInitialSession()` (default app behavior: prefers
|
|
547
|
+
* `?chatSession=<uid>`, falls back to the user's most recent session).
|
|
548
|
+
* - `null` after init: resets to a new empty chat. Equivalent to calling
|
|
549
|
+
* `newSession()`.
|
|
550
|
+
* - A session uid: loads and displays that session.
|
|
551
|
+
*
|
|
552
|
+
* Pair with `(sessionIdChange)` for two-way binding.
|
|
553
|
+
*/
|
|
554
|
+
sessionId = input(null, ...(ngDevMode ? [{ debugName: "sessionId" }] : []));
|
|
555
|
+
placeholder = input('Type a message...', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
556
|
+
/**
|
|
557
|
+
* Emits whenever the chat's active session changes — after the initial
|
|
558
|
+
* load resolves, after a send creates a new session, after the input is
|
|
559
|
+
* reassigned, or after `newSession()` clears the chat.
|
|
560
|
+
*/
|
|
561
|
+
sessionIdChange = output();
|
|
562
|
+
/**
|
|
563
|
+
* Emits after the chat has recovered from a server-reported stale-leaf
|
|
564
|
+
* 409. The component has already reloaded the session and restored the
|
|
565
|
+
* user's typed text; consuming apps typically respond by surfacing a toast.
|
|
566
|
+
*/
|
|
567
|
+
staleSession = output();
|
|
568
|
+
// Internal conversation state — same as before, just relocated for clarity.
|
|
386
569
|
_messages = [];
|
|
387
570
|
_displayMessages = [];
|
|
388
571
|
// Pixels of slack allowed when deciding if the viewport is "at the bottom".
|
|
389
572
|
_pinnedThreshold = 32;
|
|
390
573
|
_isPinnedToBottom = true;
|
|
391
574
|
_forceScrollOnNextResize = false;
|
|
575
|
+
_loadingSubject = new BehaviorSubject(false);
|
|
576
|
+
loading$ = this._loadingSubject.asObservable();
|
|
577
|
+
_currentSessionId = null;
|
|
578
|
+
_currentLeafMessageId = null;
|
|
579
|
+
_initialized = false;
|
|
580
|
+
_sessionLoadRequest$ = new Subject();
|
|
581
|
+
_destroy$ = new Subject();
|
|
582
|
+
_initialLoadingSubject = new BehaviorSubject(false);
|
|
583
|
+
initialLoading$ = this._initialLoadingSubject.asObservable();
|
|
584
|
+
constructor() {
|
|
585
|
+
this._sessionLoadRequest$
|
|
586
|
+
.pipe(tap(() => this._initialLoadingSubject.next(this._messages.length === 0)), switchMap((load$) => load$.pipe(catchError((err) => {
|
|
587
|
+
console.error('Chat session load failed:', err);
|
|
588
|
+
return of(null);
|
|
589
|
+
}))), takeUntil(this._destroy$))
|
|
590
|
+
.subscribe((session) => {
|
|
591
|
+
this._initialLoadingSubject.next(false);
|
|
592
|
+
if (session) {
|
|
593
|
+
const wasNoSession = this._currentSessionId === null;
|
|
594
|
+
this._applySession(session);
|
|
595
|
+
if (wasNoSession) {
|
|
596
|
+
this.sessionIdChange.emit(session.uid);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
this._cdr.markForCheck();
|
|
600
|
+
});
|
|
601
|
+
effect(() => {
|
|
602
|
+
const incoming = this.sessionId();
|
|
603
|
+
if (!this._initialized) {
|
|
604
|
+
this._initialize(incoming);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
this._reactToSessionInputChange(incoming);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
392
611
|
ngAfterViewInit() {
|
|
393
612
|
const scrollInstance = this._messageListScrollbar?.instance;
|
|
394
613
|
if (!scrollInstance) {
|
|
@@ -403,11 +622,32 @@ class TheSeamChatComponent {
|
|
|
403
622
|
});
|
|
404
623
|
});
|
|
405
624
|
}
|
|
625
|
+
ngOnDestroy() {
|
|
626
|
+
this._destroy$.next();
|
|
627
|
+
this._destroy$.complete();
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Resets the chat to a new empty session. Idempotent — safe to call when
|
|
631
|
+
* the chat has no session loaded. Emits `(sessionIdChange)` with `null`.
|
|
632
|
+
*/
|
|
633
|
+
newSession() {
|
|
634
|
+
// Push EMPTY through the load pipeline so switchMap cancels any
|
|
635
|
+
// in-flight session load before we clear state.
|
|
636
|
+
this._sessionLoadRequest$.next(EMPTY);
|
|
637
|
+
this._currentSessionId = null;
|
|
638
|
+
this._currentLeafMessageId = null;
|
|
639
|
+
this._messages = [];
|
|
640
|
+
this._displayMessages = [];
|
|
641
|
+
this._loadingSubject.next(false);
|
|
642
|
+
this._initialLoadingSubject.next(false);
|
|
643
|
+
this.sessionIdChange.emit(null);
|
|
644
|
+
this._cdr.markForCheck();
|
|
645
|
+
}
|
|
406
646
|
async _onMessageSent(text) {
|
|
407
|
-
if (this._loadingSubject.value
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
647
|
+
if (this._loadingSubject.value)
|
|
648
|
+
return;
|
|
649
|
+
if (!this._provider) {
|
|
650
|
+
console.error('No chat provider configured.');
|
|
411
651
|
return;
|
|
412
652
|
}
|
|
413
653
|
const userMessage = { role: 'user', content: text };
|
|
@@ -421,35 +661,48 @@ class TheSeamChatComponent {
|
|
|
421
661
|
},
|
|
422
662
|
];
|
|
423
663
|
this._forceScrollOnNextResize = true;
|
|
424
|
-
this._cdr.markForCheck();
|
|
425
664
|
this._loadingSubject.next(true);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
665
|
+
this._cdr.markForCheck();
|
|
666
|
+
const contexts = (await this._chatContextRegistry?.snapshot()) ?? [];
|
|
667
|
+
this._provider
|
|
668
|
+
.chat({
|
|
669
|
+
messages: this._messages,
|
|
670
|
+
contexts: contexts.length === 0 ? undefined : contexts,
|
|
671
|
+
sessionId: this._currentSessionId,
|
|
672
|
+
expectedLeafMessageId: this._currentLeafMessageId,
|
|
673
|
+
})
|
|
674
|
+
.pipe(takeUntil(this._destroy$))
|
|
675
|
+
.subscribe({
|
|
676
|
+
next: (response) => {
|
|
677
|
+
this._messages.push({ role: 'assistant', content: response.content });
|
|
678
|
+
this._displayMessages = [
|
|
679
|
+
...this._displayMessages,
|
|
680
|
+
{
|
|
681
|
+
role: 'assistant',
|
|
682
|
+
segments: parseChatResponse(response.content),
|
|
683
|
+
timestamp: new Date(),
|
|
684
|
+
},
|
|
685
|
+
];
|
|
686
|
+
const wasNoSession = this._currentSessionId === null;
|
|
687
|
+
this._currentSessionId = response.sessionId;
|
|
688
|
+
this._currentLeafMessageId = response.leafMessageId;
|
|
689
|
+
if (wasNoSession) {
|
|
690
|
+
this.sessionIdChange.emit(response.sessionId);
|
|
691
|
+
}
|
|
692
|
+
this._loadingSubject.next(false);
|
|
693
|
+
this._cdr.markForCheck();
|
|
694
|
+
},
|
|
695
|
+
error: (err) => {
|
|
696
|
+
if (err instanceof ChatSessionStaleError) {
|
|
697
|
+
this._handleStaleSession(text);
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
console.error('Chat provider error:', err);
|
|
701
|
+
this._loadingSubject.next(false);
|
|
702
|
+
this._cdr.markForCheck();
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
});
|
|
453
706
|
}
|
|
454
707
|
_updatePinnedState() {
|
|
455
708
|
const scrollInstance = this._messageListScrollbar?.instance;
|
|
@@ -473,28 +726,97 @@ class TheSeamChatComponent {
|
|
|
473
726
|
scrollInstance.scroll({ y: state.contentScrollSize.height });
|
|
474
727
|
}
|
|
475
728
|
}
|
|
729
|
+
_initialize(incoming) {
|
|
730
|
+
this._initialized = true;
|
|
731
|
+
if (!this._provider) {
|
|
732
|
+
console.error('No chat provider configured.');
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (incoming) {
|
|
736
|
+
this._sessionLoadRequest$.next(this._provider.getSession(incoming));
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
this._sessionLoadRequest$.next(this._provider.getInitialSession());
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
_reactToSessionInputChange(incoming) {
|
|
743
|
+
if (incoming === this._currentSessionId)
|
|
744
|
+
return;
|
|
745
|
+
if (!this._provider) {
|
|
746
|
+
console.error('No chat provider configured.');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (incoming === null) {
|
|
750
|
+
this.newSession();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
this._sessionLoadRequest$.next(this._provider.getSession(incoming));
|
|
754
|
+
}
|
|
755
|
+
_applySession(session) {
|
|
756
|
+
this._currentSessionId = session.uid;
|
|
757
|
+
this._currentLeafMessageId = session.leafMessageId;
|
|
758
|
+
this._messages = session.messages.map((m) => ({
|
|
759
|
+
role: m.role,
|
|
760
|
+
content: m.content,
|
|
761
|
+
}));
|
|
762
|
+
this._displayMessages = session.messages.map((m) => ({
|
|
763
|
+
uid: m.uid,
|
|
764
|
+
role: m.role,
|
|
765
|
+
segments: m.role === 'assistant'
|
|
766
|
+
? parseChatResponse(m.content)
|
|
767
|
+
: [{ type: 'markdown', content: m.content }],
|
|
768
|
+
timestamp: new Date(m.created),
|
|
769
|
+
}));
|
|
770
|
+
this._forceScrollOnNextResize = true;
|
|
771
|
+
}
|
|
772
|
+
_handleStaleSession(originalText) {
|
|
773
|
+
const sessionId = this._currentSessionId;
|
|
774
|
+
if (!sessionId || !this._provider) {
|
|
775
|
+
this._loadingSubject.next(false);
|
|
776
|
+
this._cdr.markForCheck();
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
this._provider
|
|
780
|
+
.getSession(sessionId)
|
|
781
|
+
.pipe(takeUntil(this._destroy$))
|
|
782
|
+
.subscribe({
|
|
783
|
+
next: (reloaded) => {
|
|
784
|
+
this._applySession(reloaded);
|
|
785
|
+
this._chatInput?.restoreText(originalText);
|
|
786
|
+
this._loadingSubject.next(false);
|
|
787
|
+
this.staleSession.emit();
|
|
788
|
+
this._cdr.markForCheck();
|
|
789
|
+
},
|
|
790
|
+
error: (reloadErr) => {
|
|
791
|
+
console.error('Chat session reload failed during stale-leaf recovery:', reloadErr);
|
|
792
|
+
this._chatInput?.restoreText(originalText);
|
|
793
|
+
this._loadingSubject.next(false);
|
|
794
|
+
this.staleSession.emit();
|
|
795
|
+
this._cdr.markForCheck();
|
|
796
|
+
},
|
|
797
|
+
});
|
|
798
|
+
}
|
|
476
799
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
477
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "
|
|
800
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TheSeamChatComponent, isStandalone: true, selector: "seam-chat", inputs: { sessionId: { classPropertyName: "sessionId", publicName: "sessionId", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sessionIdChange: "sessionIdChange", staleSession: "staleSession" }, viewQueries: [{ propertyName: "_messageList", first: true, predicate: ["messageList"], descendants: true }, { propertyName: "_messageListScrollbar", first: true, predicate: TheSeamOverlayScrollbarDirective, descendants: true }, { propertyName: "_chatInput", first: true, predicate: SeamChatInputComponent, descendants: true }], ngImport: i0, template: "<div class=\"seam-chat\">\n <div class=\"seam-chat__messages\" #messageList seamOverlayScrollbar>\n @if (initialLoading$ | async) {\n <div class=\"seam-chat__initial-loading\">\n <span>Loading\u2026</span>\n </div>\n } @else {\n @for (msg of _displayMessages; track $index) {\n <seam-chat-message [message]=\"msg\"></seam-chat-message>\n }\n\n @if (loading$ | async) {\n <div class=\"seam-chat__loading\">\n <span>Thinking...</span>\n </div>\n }\n }\n </div>\n\n <seam-chat-input\n [placeholder]=\"placeholder()\"\n [disabled]=\"!!(loading$ | async) || !!(initialLoading$ | async)\"\n (messageSent)=\"_onMessageSent($event)\"\n ></seam-chat-input>\n</div>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.seam-chat{display:flex;flex-direction:column;height:100%;overflow:hidden}.seam-chat__messages{flex:1;overflow-y:auto;padding:12px}.seam-chat__loading{padding:8px 12px;color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "component", type: SeamChatMessageComponent, selector: "seam-chat-message", inputs: ["message"] }, { kind: "component", type: SeamChatInputComponent, selector: "seam-chat-input", inputs: ["placeholder", "disabled"], outputs: ["messageSent"] }, { kind: "directive", type: TheSeamOverlayScrollbarDirective, selector: "[seamOverlayScrollbar]", inputs: ["seamOverlayScrollbar", "overlayScrollbarEnabled"], exportAs: ["seamOverlayScrollbar"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
478
801
|
}
|
|
479
802
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamChatComponent, decorators: [{
|
|
480
803
|
type: Component,
|
|
481
804
|
args: [{ selector: 'seam-chat', imports: [
|
|
482
805
|
AsyncPipe,
|
|
483
|
-
NgForOf,
|
|
484
|
-
NgIf,
|
|
485
806
|
SeamChatMessageComponent,
|
|
486
807
|
SeamChatInputComponent,
|
|
487
808
|
TheSeamOverlayScrollbarDirective,
|
|
488
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"seam-chat\">\n <div class=\"seam-chat__messages\" #messageList seamOverlayScrollbar>\n <seam-
|
|
489
|
-
}], propDecorators: {
|
|
490
|
-
type: Input
|
|
491
|
-
}], _messageList: [{
|
|
809
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"seam-chat\">\n <div class=\"seam-chat__messages\" #messageList seamOverlayScrollbar>\n @if (initialLoading$ | async) {\n <div class=\"seam-chat__initial-loading\">\n <span>Loading\u2026</span>\n </div>\n } @else {\n @for (msg of _displayMessages; track $index) {\n <seam-chat-message [message]=\"msg\"></seam-chat-message>\n }\n\n @if (loading$ | async) {\n <div class=\"seam-chat__loading\">\n <span>Thinking...</span>\n </div>\n }\n }\n </div>\n\n <seam-chat-input\n [placeholder]=\"placeholder()\"\n [disabled]=\"!!(loading$ | async) || !!(initialLoading$ | async)\"\n (messageSent)=\"_onMessageSent($event)\"\n ></seam-chat-input>\n</div>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.seam-chat{display:flex;flex-direction:column;height:100%;overflow:hidden}.seam-chat__messages{flex:1;overflow-y:auto;padding:12px}.seam-chat__loading{padding:8px 12px;color:#6c757d;font-style:italic}\n"] }]
|
|
810
|
+
}], ctorParameters: () => [], propDecorators: { _messageList: [{
|
|
492
811
|
type: ViewChild,
|
|
493
812
|
args: ['messageList']
|
|
494
813
|
}], _messageListScrollbar: [{
|
|
495
814
|
type: ViewChild,
|
|
496
815
|
args: [TheSeamOverlayScrollbarDirective]
|
|
497
|
-
}]
|
|
816
|
+
}], _chatInput: [{
|
|
817
|
+
type: ViewChild,
|
|
818
|
+
args: [SeamChatInputComponent]
|
|
819
|
+
}], sessionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "sessionId", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], sessionIdChange: [{ type: i0.Output, args: ["sessionIdChange"] }], staleSession: [{ type: i0.Output, args: ["staleSession"] }] } });
|
|
498
820
|
|
|
499
821
|
class TheSeamChatMessageHarness extends ComponentHarness {
|
|
500
822
|
static hostSelector = 'seam-chat-message';
|
|
@@ -525,15 +847,22 @@ class TheSeamChatHarness extends ComponentHarness {
|
|
|
525
847
|
_messages = this.locatorForAll(TheSeamChatMessageHarness);
|
|
526
848
|
_input = this.locatorFor(TheSeamChatInputHarness);
|
|
527
849
|
_loading = this.locatorForOptional('.seam-chat__loading');
|
|
850
|
+
_initialLoading = this.locatorForOptional('.seam-chat__initial-loading');
|
|
528
851
|
async getMessages() {
|
|
529
852
|
return this._messages();
|
|
530
853
|
}
|
|
854
|
+
async getMessageCount() {
|
|
855
|
+
return (await this._messages()).length;
|
|
856
|
+
}
|
|
531
857
|
async getInput() {
|
|
532
858
|
return this._input();
|
|
533
859
|
}
|
|
534
860
|
async isLoading() {
|
|
535
861
|
return (await this._loading()) !== null;
|
|
536
862
|
}
|
|
863
|
+
async isInitialLoading() {
|
|
864
|
+
return (await this._initialLoading()) !== null;
|
|
865
|
+
}
|
|
537
866
|
}
|
|
538
867
|
|
|
539
868
|
const assistantPrompt = `You are a helpful assistant that provides formatting json code for a datatable.
|
|
@@ -860,12 +1189,12 @@ class TheSeamDatatablePrompterComponent {
|
|
|
860
1189
|
});
|
|
861
1190
|
_alterations$ = this._datatableSubject
|
|
862
1191
|
.asObservable()
|
|
863
|
-
.pipe(switchMap((dt) => {
|
|
1192
|
+
.pipe(switchMap$1((dt) => {
|
|
864
1193
|
if (!dt) {
|
|
865
1194
|
return of(dt);
|
|
866
1195
|
}
|
|
867
1196
|
return dt._columnsAlterationsManager.changes.pipe(startWith(undefined), map(() => dt));
|
|
868
|
-
}), switchMap((datatable) => {
|
|
1197
|
+
}), switchMap$1((datatable) => {
|
|
869
1198
|
if (!datatable) {
|
|
870
1199
|
return of([]);
|
|
871
1200
|
}
|
|
@@ -875,7 +1204,7 @@ class TheSeamDatatablePrompterComponent {
|
|
|
875
1204
|
console.warn('No preferences key set on datatable, returning empty alterations.');
|
|
876
1205
|
return of([]);
|
|
877
1206
|
}
|
|
878
|
-
return (this._dtPrefsService.preferences(key).pipe(switchMap((prefs) => {
|
|
1207
|
+
return (this._dtPrefsService.preferences(key).pipe(switchMap$1((prefs) => {
|
|
879
1208
|
// console.log('~~~~Current preferences:', prefs)
|
|
880
1209
|
if (!prefs) {
|
|
881
1210
|
return of(JSON.parse(JSON.stringify(EMPTY_DATATABLE_PREFERENCES))
|
|
@@ -885,7 +1214,7 @@ class TheSeamDatatablePrompterComponent {
|
|
|
885
1214
|
return of(prefs.alterations);
|
|
886
1215
|
})) ?? of([]));
|
|
887
1216
|
}));
|
|
888
|
-
_alterationsDisplayItems$ = this._alterations$.pipe(switchMap((alterations) => {
|
|
1217
|
+
_alterationsDisplayItems$ = this._alterations$.pipe(switchMap$1((alterations) => {
|
|
889
1218
|
console.log('~~~~~Current alterations:', alterations);
|
|
890
1219
|
if (!alterations || alterations.length === 0) {
|
|
891
1220
|
return of([]);
|
|
@@ -895,7 +1224,7 @@ class TheSeamDatatablePrompterComponent {
|
|
|
895
1224
|
return of(alts.map((a) => a.toDisplayItem()));
|
|
896
1225
|
}), shareReplay({ bufferSize: 1, refCount: true }));
|
|
897
1226
|
_pendingAlterationsSubject = new BehaviorSubject([]);
|
|
898
|
-
_pendingAlterationsDisplayItems$ = this._pendingAlterationsSubject.asObservable().pipe(switchMap((pending) => {
|
|
1227
|
+
_pendingAlterationsDisplayItems$ = this._pendingAlterationsSubject.asObservable().pipe(switchMap$1((pending) => {
|
|
899
1228
|
if (!pending || pending.length === 0) {
|
|
900
1229
|
return of([]);
|
|
901
1230
|
}
|
|
@@ -936,15 +1265,14 @@ class TheSeamDatatablePrompterComponent {
|
|
|
936
1265
|
this._loadingSubject.next(false);
|
|
937
1266
|
return;
|
|
938
1267
|
}
|
|
939
|
-
this._aiProvider
|
|
940
|
-
.chat({
|
|
1268
|
+
firstValueFrom(this._aiProvider.chat({
|
|
941
1269
|
messages: [
|
|
942
1270
|
{
|
|
943
1271
|
role: 'user',
|
|
944
1272
|
content: `${assistantPrompt}\n\n---\n\n${userPrompt}`,
|
|
945
1273
|
},
|
|
946
1274
|
],
|
|
947
|
-
})
|
|
1275
|
+
}))
|
|
948
1276
|
.then(async (response) => {
|
|
949
1277
|
const alterations = parseResponse(response.content, undefined);
|
|
950
1278
|
// this._form.reset()
|
|
@@ -1035,9 +1363,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1035
1363
|
type: Input
|
|
1036
1364
|
}] } });
|
|
1037
1365
|
|
|
1366
|
+
// Shared providers
|
|
1367
|
+
|
|
1038
1368
|
/**
|
|
1039
1369
|
* Generated bundle index. Do not edit.
|
|
1040
1370
|
*/
|
|
1041
1371
|
|
|
1042
|
-
export { LmStudioAiProvider, MockAiProvider, OpenRouterAiProvider, THESEAM_CHAT_BLOCK_REGISTRY, THESEAM_CHAT_PROVIDER, THESEAM_DATATABLE_PROMPTER_PROVIDER, TheSeamChatComponent, TheSeamChatContextRegistry, TheSeamChatHarness, TheSeamDatatableChatContext, TheSeamDatatablePrompterComponent, assistantPrompt, getUserPrompt, parseChatResponse, parseResponse };
|
|
1372
|
+
export { ChatSessionStaleError, LmStudioAiProvider, MockAiProvider, OpenRouterAiProvider, THESEAM_CHAT_BLOCK_REGISTRY, THESEAM_CHAT_PROVIDER, THESEAM_DATATABLE_PROMPTER_PROVIDER, TheSeamChatComponent, TheSeamChatContextRegistry, TheSeamChatHarness, TheSeamDatatableChatContext, TheSeamDatatablePrompterComponent, assistantPrompt, getUserPrompt, parseChatResponse, parseResponse };
|
|
1043
1373
|
//# sourceMappingURL=theseam-ui-common-ai.mjs.map
|