@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.
@@ -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
- async chat(request) {
25
- const url = 'http://localhost:1234/v1/chat/completions';
26
- const headers = {
27
- 'Content-Type': 'application/json',
28
- };
29
- const model = 'model-identifier';
30
- const response = await fetch(url, {
31
- method: 'POST',
32
- headers,
33
- body: JSON.stringify({
34
- model,
35
- messages: request.messages.map((m) => ({
36
- role: m.role,
37
- content: m.content,
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
- const data = await response.json();
42
- const content = data.choices[0].message.content;
43
- return { content };
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
- async chat(request) {
49
- const defaultApiKey = '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';
50
- const url = 'https://openrouter.ai/api/v1/chat/completions';
51
- const apiKey = localStorage.getItem('openrouter-api-key') || defaultApiKey;
52
- const headers = {
53
- Authorization: `Bearer ${apiKey}`,
54
- 'Content-Type': 'application/json',
55
- };
56
- const model = 'google/gemini-2.5-flash';
57
- const response = await fetch(url, {
58
- method: 'POST',
59
- headers,
60
- body: JSON.stringify({
61
- model,
62
- messages: request.messages.map((m) => ({
63
- role: m.role,
64
- content: m.content,
65
- })),
66
- response_format: { type: 'json_object' },
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
- const data = await response.json();
70
- const content = data.choices[0].message.content;
71
- return { content };
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
- _response;
77
- constructor(_response = 'Mock response') {
78
- this._response = _response;
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
- async chat(request) {
81
- const content = typeof this._response === 'function'
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
- _loadingSubject = new BehaviorSubject(false);
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 || !this._provider) {
408
- if (!this._provider) {
409
- console.error('No chat provider configured.');
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
- try {
427
- const contexts = (await this._chatContextRegistry?.snapshot()) ?? [];
428
- const response = await this._provider.chat({
429
- messages: this._messages,
430
- contexts: contexts.length === 0 ? undefined : contexts,
431
- });
432
- const assistantMessage = {
433
- role: 'assistant',
434
- content: response.content,
435
- };
436
- this._messages.push(assistantMessage);
437
- this._displayMessages = [
438
- ...this._displayMessages,
439
- {
440
- role: 'assistant',
441
- segments: parseChatResponse(response.content),
442
- timestamp: new Date(),
443
- },
444
- ];
445
- }
446
- catch (err) {
447
- console.error('Chat provider error:', err);
448
- }
449
- finally {
450
- this._loadingSubject.next(false);
451
- this._cdr.markForCheck();
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: "14.0.0", version: "20.3.15", type: TheSeamChatComponent, isStandalone: true, selector: "seam-chat", inputs: { placeholder: "placeholder" }, viewQueries: [{ propertyName: "_messageList", first: true, predicate: ["messageList"], descendants: true }, { propertyName: "_messageListScrollbar", first: true, predicate: TheSeamOverlayScrollbarDirective, descendants: true }], ngImport: i0, template: "<div class=\"seam-chat\">\n <div class=\"seam-chat__messages\" #messageList seamOverlayScrollbar>\n <seam-chat-message\n *ngFor=\"let msg of _displayMessages\"\n [message]=\"msg\"\n ></seam-chat-message>\n\n <div *ngIf=\"_loadingSubject | async\" class=\"seam-chat__loading\">\n <span>Thinking...</span>\n </div>\n </div>\n\n <seam-chat-input\n [placeholder]=\"placeholder\"\n [disabled]=\"!!(_loadingSubject | 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: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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 });
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-chat-message\n *ngFor=\"let msg of _displayMessages\"\n [message]=\"msg\"\n ></seam-chat-message>\n\n <div *ngIf=\"_loadingSubject | async\" class=\"seam-chat__loading\">\n <span>Thinking...</span>\n </div>\n </div>\n\n <seam-chat-input\n [placeholder]=\"placeholder\"\n [disabled]=\"!!(_loadingSubject | 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"] }]
489
- }], propDecorators: { placeholder: [{
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