@theseam/ui-common 1.0.2-beta.87 → 2.0.1

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,1373 +1,6 @@
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';
3
- import * as i0 from '@angular/core';
4
- import { Injectable, InjectionToken, inject, Injector, Input, ChangeDetectionStrategy, Component, EventEmitter, Output, ChangeDetectorRef, NgZone, input, output, effect, ViewChild } from '@angular/core';
5
- import { print } from 'graphql';
6
- import { processGql } from '@theseam/ui-common/graphql';
7
- import { NgForOf, NgIf, NgComponentOutlet, AsyncPipe, JsonPipe } from '@angular/common';
8
- import { TheSeamOverlayScrollbarDirective } from '@theseam/ui-common/scrollbar';
9
- import { MarkdownComponent } from 'ngx-markdown';
10
- import * as i1 from '@angular/forms';
11
- import { FormControl, Validators, ReactiveFormsModule, FormGroup } from '@angular/forms';
12
- import * as i2 from '@theseam/ui-common/rich-text';
13
- import { TheSeamRichTextModule } from '@theseam/ui-common/rich-text';
14
- import * as i4 from '@theseam/ui-common/form-field';
15
- import { TheSeamFormFieldModule } from '@theseam/ui-common/form-field';
16
- import * as i4$1 from '@theseam/ui-common/buttons';
17
- import { TheSeamButtonsModule } from '@theseam/ui-common/buttons';
18
- import { ComponentHarness } from '@angular/cdk/testing';
19
- import { THESEAM_DATATABLE_PREFERENCES_ACCESSOR, DatatablePreferencesService, EMPTY_DATATABLE_PREFERENCES, mapColumnsAlterationsStates } from '@theseam/ui-common/datatable';
20
- import * as i2$1 from '@theseam/ui-common/loading';
21
- import { TheSeamLoadingModule } from '@theseam/ui-common/loading';
22
- import { AlterationsDiffComponent } from '@theseam/ui-common/datatable-alterations-display';
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
-
41
- class LmStudioAiProvider {
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
- });
81
- });
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.'));
100
- }
101
- }
102
-
103
- class OpenRouterAiProvider {
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
- });
148
- });
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.'));
167
- }
168
- }
169
-
170
- class MockAiProvider {
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)));
204
- }
205
- getRecentSession() {
206
- return defer(() => this._withDelay('getRecentSession', of(this._config.initialSession ?? null)));
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)}`;
235
- }
236
-
237
- class TheSeamChatContextRegistry {
238
- _contexts = new Set();
239
- /**
240
- * Register a context. Returns an unregister function — pair it with DestroyRef:
241
- * inject(DestroyRef).onDestroy(registry.register(ctx))
242
- */
243
- register(ctx) {
244
- this._contexts.add(ctx);
245
- return () => this._contexts.delete(ctx);
246
- }
247
- unregister(ctx) {
248
- this._contexts.delete(ctx);
249
- }
250
- /**
251
- * Resolve registered contexts to a wire-ready payload list. Applies the modal-mask
252
- * rule (a `modal`-typed context hides others unless they set `alwaysVisible`) and
253
- * drops entries whose `getContext()` returns null/undefined.
254
- */
255
- async snapshot() {
256
- const all = [...this._contexts];
257
- const masked = all.some((c) => c.type === 'modal');
258
- const visible = masked
259
- ? all.filter((c) => c.type === 'modal' || c.alwaysVisible)
260
- : all;
261
- const resolved = await Promise.all(visible.map(async (c) => ({ type: c.type, data: await c.getContext() })));
262
- return resolved.filter((p) => p.data != null);
263
- }
264
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamChatContextRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
265
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamChatContextRegistry, providedIn: 'root' });
266
- }
267
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamChatContextRegistry, decorators: [{
268
- type: Injectable,
269
- args: [{ providedIn: 'root' }]
270
- }] });
271
-
272
- class TheSeamDatatableChatContext {
273
- _queryRef;
274
- _columns;
275
- _options;
276
- type = 'datatable';
277
- constructor(_queryRef, _columns, _options = {}) {
278
- this._queryRef = _queryRef;
279
- this._columns = _columns;
280
- this._options = _options;
281
- }
282
- getContext() {
283
- const opts = this._queryRef.getOptions();
284
- if (!opts)
285
- return null;
286
- const { query, variables } = processGql(opts.query, this._queryRef.getVariables(), this._queryRef.getQueryProcessingConfig() ?? { variables: {} });
287
- const operationName = query.definitions.find((d) => d.kind === 'OperationDefinition')?.name?.value ?? '';
288
- return {
289
- label: this._options.label,
290
- operationName,
291
- query: print(query),
292
- variables,
293
- columns: this._columns.map((c) => ({ prop: c.prop, name: c.name })),
294
- };
295
- }
296
- }
297
-
298
- const THESEAM_CHAT_PROVIDER = new InjectionToken('TheSeamChatProvider');
299
-
300
- /**
301
- * Splits a raw AI response string into an ordered array of segments.
302
- * Fenced code blocks with `seam-` prefixed language tags become custom-block
303
- * segments; everything else stays as markdown.
304
- */
305
- function parseChatResponse(input) {
306
- if (!input) {
307
- return [];
308
- }
309
- const segments = [];
310
- const pattern = /^```(seam-[\w-]+)\n([\s\S]*?)^```/gm;
311
- let lastIndex = 0;
312
- let match;
313
- while ((match = pattern.exec(input)) !== null) {
314
- if (match.index > lastIndex) {
315
- segments.push({
316
- type: 'markdown',
317
- content: input.slice(lastIndex, match.index),
318
- });
319
- }
320
- segments.push({
321
- type: 'custom-block',
322
- tag: match[1],
323
- content: match[2].replace(/\n$/, ''),
324
- });
325
- lastIndex = match.index + match[0].length;
326
- }
327
- if (lastIndex < input.length) {
328
- segments.push({ type: 'markdown', content: input.slice(lastIndex) });
329
- }
330
- return segments;
331
- }
332
-
333
- const THESEAM_CHAT_BLOCK_REGISTRY = new InjectionToken('TheSeamChatBlockRegistry');
334
-
335
- class SeamChatMessageComponent {
336
- message;
337
- _blockRegistry = inject(THESEAM_CHAT_BLOCK_REGISTRY, {
338
- optional: true,
339
- });
340
- _injector = inject(Injector);
341
- _getBlockComponent(tag) {
342
- return this._blockRegistry?.get(tag) ?? null;
343
- }
344
- _createBlockInjector(content) {
345
- return Injector.create({
346
- providers: [{ provide: 'CHAT_BLOCK_CONTENT', useValue: content }],
347
- parent: this._injector,
348
- });
349
- }
350
- _buildFallbackMarkdown(tag, content) {
351
- return '```' + tag + '\n' + content + '\n```';
352
- }
353
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SeamChatMessageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
354
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: SeamChatMessageComponent, isStandalone: true, selector: "seam-chat-message", inputs: { message: "message" }, ngImport: i0, template: `
355
- <div
356
- class="seam-chat-message"
357
- [class.seam-chat-message--user]="message.role === 'user'"
358
- [class.seam-chat-message--assistant]="message.role === 'assistant'"
359
- >
360
- <div class="seam-chat-message__role">
361
- {{ message.role === 'user' ? 'You' : 'Assistant' }}
362
- </div>
363
- <div class="seam-chat-message__content">
364
- <ng-container *ngFor="let segment of message.segments">
365
- <markdown
366
- *ngIf="segment.type === 'markdown'"
367
- [data]="segment.content"
368
- ></markdown>
369
- <ng-container *ngIf="segment.type === 'custom-block'">
370
- <ng-container
371
- *ngIf="
372
- _getBlockComponent(segment.tag) as blockComponent;
373
- else fallbackBlock
374
- "
375
- >
376
- <ng-container
377
- *ngComponentOutlet="
378
- blockComponent;
379
- injector: _createBlockInjector(segment.content)
380
- "
381
- ></ng-container>
382
- </ng-container>
383
- <ng-template #fallbackBlock>
384
- <markdown
385
- [data]="_buildFallbackMarkdown(segment.tag, segment.content)"
386
- ></markdown>
387
- </ng-template>
388
- </ng-container>
389
- </ng-container>
390
- </div>
391
- </div>
392
- `, isInline: true, styles: [".seam-chat-message{display:flex;flex-direction:column;padding:8px 12px;margin-bottom:8px}.seam-chat-message--user{align-items:flex-end}.seam-chat-message--user .seam-chat-message__content{background-color:#e8f5e9;border-radius:8px;padding:8px 12px;max-width:80%}.seam-chat-message--assistant .seam-chat-message__content{background-color:#f1f3f5;border-radius:8px;padding:8px 12px;max-width:80%}.seam-chat-message__role{font-size:.75rem;color:#6c757d;margin-bottom:4px}\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: MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"], exportAs: ["ngComponentOutlet"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
393
- }
394
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SeamChatMessageComponent, decorators: [{
395
- type: Component,
396
- args: [{ selector: 'seam-chat-message', imports: [NgForOf, NgIf, MarkdownComponent, NgComponentOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: `
397
- <div
398
- class="seam-chat-message"
399
- [class.seam-chat-message--user]="message.role === 'user'"
400
- [class.seam-chat-message--assistant]="message.role === 'assistant'"
401
- >
402
- <div class="seam-chat-message__role">
403
- {{ message.role === 'user' ? 'You' : 'Assistant' }}
404
- </div>
405
- <div class="seam-chat-message__content">
406
- <ng-container *ngFor="let segment of message.segments">
407
- <markdown
408
- *ngIf="segment.type === 'markdown'"
409
- [data]="segment.content"
410
- ></markdown>
411
- <ng-container *ngIf="segment.type === 'custom-block'">
412
- <ng-container
413
- *ngIf="
414
- _getBlockComponent(segment.tag) as blockComponent;
415
- else fallbackBlock
416
- "
417
- >
418
- <ng-container
419
- *ngComponentOutlet="
420
- blockComponent;
421
- injector: _createBlockInjector(segment.content)
422
- "
423
- ></ng-container>
424
- </ng-container>
425
- <ng-template #fallbackBlock>
426
- <markdown
427
- [data]="_buildFallbackMarkdown(segment.tag, segment.content)"
428
- ></markdown>
429
- </ng-template>
430
- </ng-container>
431
- </ng-container>
432
- </div>
433
- </div>
434
- `, styles: [".seam-chat-message{display:flex;flex-direction:column;padding:8px 12px;margin-bottom:8px}.seam-chat-message--user{align-items:flex-end}.seam-chat-message--user .seam-chat-message__content{background-color:#e8f5e9;border-radius:8px;padding:8px 12px;max-width:80%}.seam-chat-message--assistant .seam-chat-message__content{background-color:#f1f3f5;border-radius:8px;padding:8px 12px;max-width:80%}.seam-chat-message__role{font-size:.75rem;color:#6c757d;margin-bottom:4px}\n"] }]
435
- }], propDecorators: { message: [{
436
- type: Input,
437
- args: [{ required: true }]
438
- }] } });
439
-
440
- class SeamChatInputComponent {
441
- placeholder = 'Type a message...';
442
- disabled = false;
443
- messageSent = new EventEmitter();
444
- _control = new FormControl('', [Validators.required]);
445
- _onEnterKey(event) {
446
- const keyEvent = event;
447
- if (keyEvent.shiftKey) {
448
- return;
449
- }
450
- keyEvent.preventDefault();
451
- this._onSend();
452
- }
453
- _onSend() {
454
- const value = this._control.value?.trim();
455
- if (!value || this.disabled) {
456
- return;
457
- }
458
- this.messageSent.emit(value);
459
- this._control.reset();
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
- }
469
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SeamChatInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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: `
471
- <div class="seam-chat-input">
472
- <seam-form-field>
473
- <seam-rich-text
474
- [formControl]="_control"
475
- [placeholder]="placeholder"
476
- [disableRichText]="true"
477
- [rows]="2.7"
478
- [resizable]="false"
479
- (keydown.enter)="_onEnterKey($event)"
480
- ></seam-rich-text>
481
- </seam-form-field>
482
- <button
483
- seamButton
484
- theme="primary"
485
- class="seam-chat-send-btn"
486
- [disabled]="disabled || _control.invalid"
487
- (click)="_onSend()"
488
- >
489
- Send
490
- </button>
491
- </div>
492
- `, isInline: true, styles: [".seam-chat-input{display:flex;align-items:flex-end;gap:8px;padding:8px;border-top:1px solid #dee2e6}seam-form-field{flex:1}.seam-chat-send-btn{flex-shrink:0}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: TheSeamRichTextModule }, { kind: "component", type: i2.RichTextComponent, selector: "seam-rich-text", inputs: ["value", "required", "placeholder", "rows", "resizable", "disableRichText", "displayCharacterCounter", "minLength", "maxLength", "characterCounterTpl", "characterCounterFn", "useMentions", "mentionItems", "mentionSearchFn", "mentionRenderListFn", "mentionListLoadingText", "mentionListEmptyText"], outputs: ["quillEditorCreated", "quillEditorChanged", "quillContentChanged", "quillSelectionChanged", "quillFocus", "quillBlur", "mentionsUpdated"] }, { kind: "ngmodule", type: TheSeamFormFieldModule }, { kind: "component", type: i4.TheSeamFormFieldComponent, selector: "seam-form-field", inputs: ["inline", "label", "labelPosition", "labelClass", "maxErrors", "numPaddingErrors", "labelId", "helpText", "helpTextId"] }, { kind: "ngmodule", type: TheSeamButtonsModule }, { kind: "component", type: i4$1.TheSeamButtonComponent, selector: "button[seamButton]", inputs: ["disabled", "theme", "size", "type"], exportAs: ["seamButton"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
493
- }
494
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SeamChatInputComponent, decorators: [{
495
- type: Component,
496
- args: [{ selector: 'seam-chat-input', imports: [
497
- ReactiveFormsModule,
498
- TheSeamRichTextModule,
499
- TheSeamFormFieldModule,
500
- TheSeamButtonsModule,
501
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
502
- <div class="seam-chat-input">
503
- <seam-form-field>
504
- <seam-rich-text
505
- [formControl]="_control"
506
- [placeholder]="placeholder"
507
- [disableRichText]="true"
508
- [rows]="2.7"
509
- [resizable]="false"
510
- (keydown.enter)="_onEnterKey($event)"
511
- ></seam-rich-text>
512
- </seam-form-field>
513
- <button
514
- seamButton
515
- theme="primary"
516
- class="seam-chat-send-btn"
517
- [disabled]="disabled || _control.invalid"
518
- (click)="_onSend()"
519
- >
520
- Send
521
- </button>
522
- </div>
523
- `, styles: [".seam-chat-input{display:flex;align-items:flex-end;gap:8px;padding:8px;border-top:1px solid #dee2e6}seam-form-field{flex:1}.seam-chat-send-btn{flex-shrink:0}\n"] }]
524
- }], propDecorators: { placeholder: [{
525
- type: Input
526
- }], disabled: [{
527
- type: Input
528
- }], messageSent: [{
529
- type: Output
530
- }] } });
531
-
532
- class TheSeamChatComponent {
533
- _provider = inject(THESEAM_CHAT_PROVIDER, { optional: true });
534
- _chatContextRegistry = inject(TheSeamChatContextRegistry, {
535
- optional: true,
536
- });
537
- _cdr = inject(ChangeDetectorRef);
538
- _ngZone = inject(NgZone);
539
- _messageList;
540
- _messageListScrollbar;
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.
569
- _messages = [];
570
- _displayMessages = [];
571
- // Pixels of slack allowed when deciding if the viewport is "at the bottom".
572
- _pinnedThreshold = 32;
573
- _isPinnedToBottom = true;
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
- }
611
- ngAfterViewInit() {
612
- const scrollInstance = this._messageListScrollbar?.instance;
613
- if (!scrollInstance) {
614
- return;
615
- }
616
- this._ngZone.runOutsideAngular(() => {
617
- scrollInstance.options({
618
- callbacks: {
619
- onScroll: () => this._updatePinnedState(),
620
- onContentSizeChanged: () => this._maybeScrollToBottom(),
621
- },
622
- });
623
- });
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
- }
646
- async _onMessageSent(text) {
647
- if (this._loadingSubject.value)
648
- return;
649
- if (!this._provider) {
650
- console.error('No chat provider configured.');
651
- return;
652
- }
653
- const userMessage = { role: 'user', content: text };
654
- this._messages.push(userMessage);
655
- this._displayMessages = [
656
- ...this._displayMessages,
657
- {
658
- role: 'user',
659
- segments: [{ type: 'markdown', content: text }],
660
- timestamp: new Date(),
661
- },
662
- ];
663
- this._forceScrollOnNextResize = true;
664
- this._loadingSubject.next(true);
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
- });
706
- }
707
- _updatePinnedState() {
708
- const scrollInstance = this._messageListScrollbar?.instance;
709
- if (!scrollInstance) {
710
- return;
711
- }
712
- const info = scrollInstance.scroll();
713
- this._isPinnedToBottom =
714
- info.position.y >= info.max.y - this._pinnedThreshold;
715
- }
716
- _maybeScrollToBottom() {
717
- if (this._forceScrollOnNextResize || this._isPinnedToBottom) {
718
- this._scrollToBottom();
719
- this._forceScrollOnNextResize = false;
720
- }
721
- }
722
- _scrollToBottom() {
723
- const scrollInstance = this._messageListScrollbar?.instance;
724
- if (scrollInstance) {
725
- const state = scrollInstance.getState();
726
- scrollInstance.scroll({ y: state.contentScrollSize.height });
727
- }
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
- }
799
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamChatComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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 });
801
- }
802
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamChatComponent, decorators: [{
803
- type: Component,
804
- args: [{ selector: 'seam-chat', imports: [
805
- AsyncPipe,
806
- SeamChatMessageComponent,
807
- SeamChatInputComponent,
808
- TheSeamOverlayScrollbarDirective,
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: [{
811
- type: ViewChild,
812
- args: ['messageList']
813
- }], _messageListScrollbar: [{
814
- type: ViewChild,
815
- args: [TheSeamOverlayScrollbarDirective]
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"] }] } });
820
-
821
- class TheSeamChatMessageHarness extends ComponentHarness {
822
- static hostSelector = 'seam-chat-message';
823
- _role = this.locatorForOptional('.seam-chat-message__role');
824
- _content = this.locatorForOptional('.seam-chat-message__content');
825
- async getRole() {
826
- const roleEl = await this._role();
827
- const text = await roleEl?.text();
828
- return text?.trim().toLowerCase() ?? '';
829
- }
830
- async getText() {
831
- const contentEl = await this._content();
832
- return (await contentEl?.text())?.trim() ?? '';
833
- }
834
- }
835
- class TheSeamChatInputHarness extends ComponentHarness {
836
- static hostSelector = 'seam-chat-input';
837
- async getSendButton() {
838
- return this.locatorFor('button')();
839
- }
840
- async isSendDisabled() {
841
- const btn = await this.getSendButton();
842
- return (await btn.getAttribute('disabled')) !== null;
843
- }
844
- }
845
- class TheSeamChatHarness extends ComponentHarness {
846
- static hostSelector = 'seam-chat';
847
- _messages = this.locatorForAll(TheSeamChatMessageHarness);
848
- _input = this.locatorFor(TheSeamChatInputHarness);
849
- _loading = this.locatorForOptional('.seam-chat__loading');
850
- _initialLoading = this.locatorForOptional('.seam-chat__initial-loading');
851
- async getMessages() {
852
- return this._messages();
853
- }
854
- async getMessageCount() {
855
- return (await this._messages()).length;
856
- }
857
- async getInput() {
858
- return this._input();
859
- }
860
- async isLoading() {
861
- return (await this._loading()) !== null;
862
- }
863
- async isInitialLoading() {
864
- return (await this._initialLoading()) !== null;
865
- }
866
- }
867
-
868
- const assistantPrompt = `You are a helpful assistant that provides formatting json code for a datatable.
869
- A datatable is a table that displays data in rows and columns, similar to a spreadsheet, with column sorting and data filtering.
870
-
871
- Your job is not to provide a descriptive analysis of the request or any additional information. The user will ignore anything that is not a JSON object.
872
-
873
- The user will provide a request, and you will respond with a JSON object that contains an array of table alterations.
874
- The following is the typescript interface for a datatable column and the alterations you can make to it:
875
-
876
- \`\`\`typescript
877
- interface TableColumn {
878
- /** Column property */
879
- prop: string,
880
- /** Column name */
881
- name: string,
882
- /** Column cell type - determines filter type */
883
- cellType?: 'string' | 'integer' | 'decimal' | 'currency' | 'date' | 'phone',
884
- /** Whether the column is sortable */
885
- sortable?: boolean,
886
- /** Whether the column is filterable */
887
- filterable?: boolean,
888
- /** Whether the column is visible */
889
- visible?: boolean,
890
- /** Whether the column is resizable */
891
- resizable?: boolean,
892
- /** Whether the column is draggable */
893
- draggable?: boolean,
894
- /** Column width */
895
- width?: number,
896
- /** Column index */
897
- index?: number,
898
- }
899
-
900
- interface SortItem {
901
- /** Column property */
902
- prop: string,
903
- /** Sort direction */
904
- dir: 'asc' | 'desc'
905
- }
906
-
907
- interface SortState {
908
- /** The list of sorts */
909
- sorts: SortItem[]
910
- }
911
-
912
- interface OrderRecord {
913
- /** Column property */
914
- columnProp: string,
915
- /** Column order, which is the index that it will be placed in the columns array. */
916
- index: number
917
- }
918
-
919
- interface OrderState {
920
- /** The list of column order records */
921
- columns: OrderRecord[]
922
- }
923
-
924
- interface WidthState {
925
- /** The column property that this width alteration applies to */
926
- columnProp: string
927
- /** The width of the column. Number is in pixels. */
928
- width?: number
929
- /** Whether the column can auto resize. Needs to be false to guarantee a specific width. */
930
- canAutoResize: boolean
931
- }
932
-
933
- interface HideColumnState {
934
- /** The column property that this alteration applies to */
935
- columnProp: string
936
- /** Whether the column is hidden */
937
- hidden: boolean
938
- }
939
-
940
- interface FilterState {
941
- /** The column property that this filter applies to */
942
- columnProp: string,
943
- /** The filter type based on column cellType */
944
- filterType: 'text' | 'numeric' | 'date',
945
- /** The filter operation */
946
- operation: string,
947
- /** The filter value (for single value operations) */
948
- value?: any,
949
- /** The from value (for range operations like 'between') */
950
- fromValue?: any,
951
- /** The to value (for range operations like 'between') */
952
- toValue?: any
953
- }
954
-
955
- interface TableAlteration<TType extends string, TState> {
956
- /**
957
- * Unique identifier for the alteration.
958
- */
959
- id: string
960
- /**
961
- * The type of alteration.
962
- */
963
- type: TType
964
- /** The alteration state */
965
- state: TState
966
- }
967
-
968
- /**
969
- * Sort alteration for a datatable.
970
- * "id" should always be "sort" for this alteration.
971
- */
972
- type SortAlteration = TableAlteration<'sort', SortState>
973
-
974
- /**
975
- * Order alteration for a datatable column.
976
- *
977
- * "id" should always be "order" for this alteration.
978
- */
979
- type OrderAlteration = TableAlteration<'order', OrderState>
980
-
981
- /**
982
- * Width alteration for a datatable column.
983
- *
984
- * "id" should always be "width-<prop>" for this alteration. So, for example, if the column property is "name", the id would be "width-name".
985
- */
986
- type WidthAlteration = TableAlteration<'width', WidthState>
987
-
988
- /**
989
- * Hide column alteration for a datatable column.
990
- *
991
- * "id" should always be "hide-column-<prop>" for this alteration. So, for example, if the column property is "name", the id would be "hide-column-name".
992
- */
993
- type HideColumnAlteration = TableAlteration<'hide-column', HideColumnState>
994
-
995
- /**
996
- * Filter alteration for a datatable column.
997
- * "id" should be "filter--<columnProp>" for this alteration.
998
- * For example, if filtering the "age" column, the id would be "filter--age".
999
- */
1000
- type FilterAlteration = TableAlteration<'filter', FilterState>
1001
- \`\`\`
1002
-
1003
- ## Filter Operations by Type
1004
-
1005
- ### Text Filters (cellType: 'string', 'phone')
1006
- - 'contains': Text contains the value (case-insensitive)
1007
- - 'eq': Text equals the value exactly
1008
- - 'neq': Text does not equal the value
1009
- - 'ncontains': Text does not contain the value
1010
- - 'blank': Field is empty/null
1011
- - 'not-blank': Field is not empty/null
1012
-
1013
- ### Numeric Filters (cellType: 'integer', 'decimal', 'currency')
1014
- - 'eq': Equals the value
1015
- - 'gt': Greater than the value
1016
- - 'gte': Greater than or equal to the value
1017
- - 'lt': Less than the value
1018
- - 'lte': Less than or equal to the value
1019
- - 'between': Between fromValue and toValue (inclusive)
1020
- - 'not-between': Not between fromValue and toValue
1021
- - 'blank': Field is empty/null
1022
- - 'not-blank': Field is not empty/null
1023
-
1024
- ### Date Filters (cellType: 'date')
1025
- - 'eq': Date equals the value
1026
- - 'gt': Date is after the value
1027
- - 'gte': Date is on or after the value
1028
- - 'lt': Date is before the value
1029
- - 'lte': Date is on or before the value
1030
- - 'between': Date is between fromValue and toValue (inclusive)
1031
- - 'not-between': Date is not between fromValue and toValue
1032
- - 'blank': Field is empty/null
1033
- - 'not-blank': Field is not empty/null
1034
-
1035
- ## Examples
1036
-
1037
- Filter age greater than 30:
1038
- \`\`\`json
1039
- {
1040
- "id": "filter--age",
1041
- "type": "filter",
1042
- "state": {
1043
- "columnProp": "age",
1044
- "filterType": "numeric",
1045
- "operation": "gt",
1046
- "value": 30
1047
- }
1048
- }
1049
- \`\`\`
1050
-
1051
- Filter color contains "red":
1052
- \`\`\`json
1053
- {
1054
- "id": "filter--color",
1055
- "type": "filter",
1056
- "state": {
1057
- "columnProp": "color",
1058
- "filterType": "text",
1059
- "operation": "contains",
1060
- "value": "red"
1061
- }
1062
- }
1063
- \`\`\`
1064
-
1065
- Filter age between 25 and 65:
1066
- \`\`\`json
1067
- {
1068
- "id": "filter--age",
1069
- "type": "filter",
1070
- "state": {
1071
- "columnProp": "age",
1072
- "filterType": "numeric",
1073
- "operation": "between",
1074
- "fromValue": 25,
1075
- "toValue": 65
1076
- }
1077
- }
1078
- \`\`\`
1079
-
1080
- Sort by name ascending:
1081
- \`\`\`json
1082
- {
1083
- "id": "sort",
1084
- "type": "sort",
1085
- "state": {
1086
- "sorts": [
1087
- {
1088
- "prop": "name",
1089
- "dir": "asc"
1090
- }
1091
- ]
1092
- }
1093
- }
1094
- \`\`\`
1095
-
1096
- Hide the age column:
1097
- \`\`\`json
1098
- {
1099
- "id": "hide-column-age",
1100
- "type": "hide-column",
1101
- "state": {
1102
- "columnProp": "age",
1103
- "hidden": true
1104
- }
1105
- }
1106
- \`\`\`
1107
-
1108
- Set name column width to 300 pixels:
1109
- \`\`\`json
1110
- {
1111
- "id": "width-name",
1112
- "type": "width",
1113
- "state": {
1114
- "columnProp": "name",
1115
- "width": 300,
1116
- "canAutoResize": false
1117
- }
1118
- }
1119
- \`\`\`
1120
-
1121
- Reorder columns (name first, age second, color third):
1122
- \`\`\`json
1123
- {
1124
- "id": "order",
1125
- "type": "order",
1126
- "state": {
1127
- "columns": [
1128
- { "columnProp": "name", "index": 0 },
1129
- { "columnProp": "age", "index": 1 },
1130
- { "columnProp": "color", "index": 2 }
1131
- ]
1132
- }
1133
- }
1134
- \`\`\`
1135
- `;
1136
- const getUserPrompt = (columns, request) => `
1137
- Columns:
1138
- \`\`\`json
1139
- ${JSON.stringify(columns, null, 2)}
1140
- \`\`\`
1141
- Request: "${request}"
1142
- `;
1143
- function parseResponse(responseContent, responseFormat) {
1144
- if (responseFormat?.type === 'json_object') {
1145
- return JSON.parse(responseContent);
1146
- }
1147
- // Parse the JSON string to an object, which is in the string between the code blocks.
1148
- // So, need to find the first and last code block markers.
1149
- const startIndex = responseContent.indexOf('```json') + '```json'.length;
1150
- const endIndex = responseContent.lastIndexOf('```');
1151
- const alterations = responseContent.substring(startIndex, endIndex).trim();
1152
- // console.log('Alterations:', alterations)
1153
- return JSON.parse(alterations);
1154
- }
1155
- const THESEAM_DATATABLE_PROMPTER_PROVIDER = new InjectionToken('TheSeamDatatablePrompterProvider');
1156
-
1157
- class TheSeamDatatablePrompterComponent {
1158
- // cdr = inject(ChangeDetectorRef)
1159
- _prefsAccessor = inject(THESEAM_DATATABLE_PREFERENCES_ACCESSOR, { optional: true });
1160
- _dtPrefsService = inject(DatatablePreferencesService);
1161
- _aiProvider = inject(THESEAM_DATATABLE_PROMPTER_PROVIDER, {
1162
- optional: true,
1163
- });
1164
- _loadingSubject = new BehaviorSubject(false);
1165
- _altsDataSubject = new BehaviorSubject(undefined);
1166
- loading$ = this._loadingSubject.asObservable();
1167
- diffMode = 'auto';
1168
- compact = true;
1169
- set prompt(value) {
1170
- if (value) {
1171
- this._form.controls.prompt.setValue(value);
1172
- }
1173
- else {
1174
- this._form.controls.prompt.setValue('Sort color descending order');
1175
- }
1176
- }
1177
- set datatable(value) {
1178
- this._datatableSubject.next(value);
1179
- }
1180
- get datatable() {
1181
- return this._datatableSubject.value;
1182
- }
1183
- _datatableSubject = new BehaviorSubject(null);
1184
- showAlts = true;
1185
- _form = new FormGroup({
1186
- prompt: new FormControl('Sort color descending order', [
1187
- Validators.required,
1188
- ]),
1189
- });
1190
- _alterations$ = this._datatableSubject
1191
- .asObservable()
1192
- .pipe(switchMap$1((dt) => {
1193
- if (!dt) {
1194
- return of(dt);
1195
- }
1196
- return dt._columnsAlterationsManager.changes.pipe(startWith(undefined), map(() => dt));
1197
- }), switchMap$1((datatable) => {
1198
- if (!datatable) {
1199
- return of([]);
1200
- }
1201
- const key = datatable.preferencesKey;
1202
- if (!key) {
1203
- // eslint-disable-next-line no-console
1204
- console.warn('No preferences key set on datatable, returning empty alterations.');
1205
- return of([]);
1206
- }
1207
- return (this._dtPrefsService.preferences(key).pipe(switchMap$1((prefs) => {
1208
- // console.log('~~~~Current preferences:', prefs)
1209
- if (!prefs) {
1210
- return of(JSON.parse(JSON.stringify(EMPTY_DATATABLE_PREFERENCES))
1211
- .alterations);
1212
- }
1213
- // return of(JSON.parse(prefs).alterations as ColumnsAlterationState[])
1214
- return of(prefs.alterations);
1215
- })) ?? of([]));
1216
- }));
1217
- _alterationsDisplayItems$ = this._alterations$.pipe(switchMap$1((alterations) => {
1218
- console.log('~~~~~Current alterations:', alterations);
1219
- if (!alterations || alterations.length === 0) {
1220
- return of([]);
1221
- }
1222
- const alts = mapColumnsAlterationsStates(alterations);
1223
- console.log('~~~~~Mapped alterations:', alts);
1224
- return of(alts.map((a) => a.toDisplayItem()));
1225
- }), shareReplay({ bufferSize: 1, refCount: true }));
1226
- _pendingAlterationsSubject = new BehaviorSubject([]);
1227
- _pendingAlterationsDisplayItems$ = this._pendingAlterationsSubject.asObservable().pipe(switchMap$1((pending) => {
1228
- if (!pending || pending.length === 0) {
1229
- return of([]);
1230
- }
1231
- const alts = mapColumnsAlterationsStates(pending);
1232
- console.log('~~~~~Mapped alterations2:', alts);
1233
- return of(alts.map((a) => a.toDisplayItem()));
1234
- }), shareReplay({ bufferSize: 1, refCount: true }));
1235
- _onSubmit() {
1236
- console.log('Submitting prompt:', this._form.value);
1237
- if (this._form.invalid) {
1238
- return;
1239
- }
1240
- if (this._loadingSubject.value) {
1241
- console.warn('Already loading, ignoring submit.');
1242
- return;
1243
- }
1244
- const prompt = this._form.value.prompt;
1245
- if (!prompt) {
1246
- return;
1247
- }
1248
- console.log('datatable', this._datatableSubject.value);
1249
- const columns = (this._datatableSubject.value?.ngxDatatable?.columns || []).map((col) => ({
1250
- prop: col.prop,
1251
- name: col.name,
1252
- cellType: col.cellType || 'string',
1253
- sortable: col.sortable,
1254
- filterable: true,
1255
- visible: true,
1256
- resizable: col.resizeable,
1257
- draggable: col.draggable,
1258
- }));
1259
- console.log('columns', columns);
1260
- const userPrompt = getUserPrompt(columns, prompt);
1261
- console.log('userPrompt', userPrompt);
1262
- this._loadingSubject.next(true);
1263
- if (!this._aiProvider) {
1264
- console.error('No AI provider configured, cannot submit prompt.');
1265
- this._loadingSubject.next(false);
1266
- return;
1267
- }
1268
- firstValueFrom(this._aiProvider.chat({
1269
- messages: [
1270
- {
1271
- role: 'user',
1272
- content: `${assistantPrompt}\n\n---\n\n${userPrompt}`,
1273
- },
1274
- ],
1275
- }))
1276
- .then(async (response) => {
1277
- const alterations = parseResponse(response.content, undefined);
1278
- // this._form.reset()
1279
- console.log('Received alterations:', alterations);
1280
- const datatable = this._datatableSubject.value;
1281
- if (!datatable) {
1282
- console.error('No datatable found to apply alterations to.');
1283
- return;
1284
- }
1285
- const key = this.datatable.preferencesKey;
1286
- const before = await this._prefsAccessor?.get(key).toPromise();
1287
- console.log('Current preferences before update:', before);
1288
- const _apply = async () => {
1289
- console.log('Preferences updated successfully.');
1290
- const _cols = this.datatable.ngxDatatable.columns;
1291
- const cols = [..._cols];
1292
- console.log('this.datatable!.columns', cols);
1293
- const after = await this._prefsAccessor?.get(key).toPromise();
1294
- let _after = (JSON.parse(after || '{}').alterations ||
1295
- []);
1296
- if (!Array.isArray(_after)) {
1297
- _after = [_after];
1298
- }
1299
- const mgr = this.datatable._columnsAlterationsManager;
1300
- console.log('_columnsAlterationsManager', mgr, mgr.get());
1301
- const alts = mapColumnsAlterationsStates(_after);
1302
- console.log('Mapped alterations:', alts);
1303
- const columnsBefore = JSON.parse(JSON.stringify(this.datatable.ngxDatatable.columns.map((x) => x.prop)));
1304
- console.log('Columns before applying alterations:', columnsBefore);
1305
- for (const a of alts) {
1306
- console.log('Applying alteration:', a);
1307
- a.apply(cols, this.datatable);
1308
- }
1309
- console.log('Current preferences after update:', after);
1310
- console.log(_after);
1311
- this.datatable.columns = [...cols];
1312
- const columnsAfter = JSON.parse(JSON.stringify(this.datatable.ngxDatatable.columns.map((x) => x.prop)));
1313
- console.log('Columns after applying alterations:', columnsAfter);
1314
- mgr.add(alts);
1315
- datatable._cdr.detectChanges();
1316
- this._pendingAlterationsSubject.next(_after);
1317
- };
1318
- this._prefsAccessor
1319
- ?.update(key, JSON.stringify({
1320
- version: 2,
1321
- alterations,
1322
- }))
1323
- .subscribe(async () => {
1324
- // TODO: Cleanup. This is a hack to ensure the datatable updates after the preferences are set.
1325
- await _apply();
1326
- datatable.rows = [...datatable.rows];
1327
- datatable._cdr.detectChanges();
1328
- await _apply();
1329
- this._loadingSubject.next(false);
1330
- });
1331
- })
1332
- .catch((err) => {
1333
- console.error('Error submitting prompt:', err);
1334
- this._loadingSubject.next(false);
1335
- });
1336
- }
1337
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamDatatablePrompterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1338
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TheSeamDatatablePrompterComponent, isStandalone: true, selector: "seam-datatable-prompter", inputs: { diffMode: "diffMode", compact: "compact", prompt: "prompt", datatable: "datatable", showAlts: "showAlts" }, ngImport: i0, template: "<div class=\"d-block border rounded border-lightgray\">\n <div\n [formGroup]=\"_form\"\n (ngSubmit)=\"_onSubmit()\"\n class=\"d-block position-relative\"\n >\n <div>\n <div class=\"p-1\">\n <!-- <textarea formControlName=\"prompt\" style=\"width: 800px; height: 100px;\"></textarea> -->\n <seam-form-field [numPaddingErrors]=\"0\">\n <seam-rich-text\n seamInput\n formControlName=\"prompt\"\n placeholder=\"Describe what you want to apply to the datatable.\"\n [rows]=\"3\"\n [resizable]=\"false\"\n [disableRichText]=\"true\"\n ></seam-rich-text>\n </seam-form-field>\n </div>\n <div class=\"p-1\">\n <button\n class=\"mr-1\"\n type=\"submit\"\n seamButton\n theme=\"primary\"\n size=\"sm\"\n (click)=\"_onSubmit()\"\n >\n Submit\n </button>\n <button seamButton theme=\"lightgray\" size=\"sm\" (click)=\"_form.reset()\">\n Reset\n </button>\n </div>\n </div>\n <div\n *ngIf=\"loading$ | async\"\n class=\"d-block position-absolute\"\n style=\"top: 0; left: 0; right: 0; bottom: 0\"\n >\n <seam-loading></seam-loading>\n </div>\n </div>\n\n <div class=\"p-2\" *ngIf=\"showAlts\">\n <div>Active Alterations</div>\n <div class=\"p-2 border rounded bg-lightgray\">\n <!-- {{ _alterations$ | async | json }} -->\n <div *ngFor=\"let alteration of _alterations$ | async; let last = last\">\n <div\n class=\"d-flex align-items-center border border-dark rounded p-1\"\n [class.mb-1]=\"!last\"\n >\n <span class=\"badge bg-secondary\">{{ alteration.type }}</span>\n <!-- <span class=\"badge bg-primary\">{{ alteration.label }}</span> -->\n <!-- <div>\n {{ alteration.value | json }}\n </div> -->\n <div *ngIf=\"alteration.type === 'sort'\">\n <div class=\"d-flex flex-column\">\n <div\n *ngFor=\"let sort of alteration.state.sorts; let last = last\"\n [class.mb-1]=\"!last\"\n >\n <span class=\"badge bg-lightgray\"\n >{{ sort.prop }} ({{ sort.dir }})</span\n >\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <seam-alterations-diff\n [currentItems]=\"(_alterationsDisplayItems$ | async) ?? []\"\n [pendingItems]=\"(_pendingAlterationsDisplayItems$ | async) ?? []\"\n [compact]=\"compact\"\n [diffMode]=\"diffMode\"\n ></seam-alterations-diff>\n </div>\n</div>\n", styles: [":host{display:block;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: TheSeamLoadingModule }, { kind: "component", type: i2$1.TheSeamLoadingComponent, selector: "seam-loading", inputs: ["theme"] }, { kind: "ngmodule", type: TheSeamRichTextModule }, { kind: "component", type: i2.RichTextComponent, selector: "seam-rich-text", inputs: ["value", "required", "placeholder", "rows", "resizable", "disableRichText", "displayCharacterCounter", "minLength", "maxLength", "characterCounterTpl", "characterCounterFn", "useMentions", "mentionItems", "mentionSearchFn", "mentionRenderListFn", "mentionListLoadingText", "mentionListEmptyText"], outputs: ["quillEditorCreated", "quillEditorChanged", "quillContentChanged", "quillSelectionChanged", "quillFocus", "quillBlur", "mentionsUpdated"] }, { kind: "ngmodule", type: TheSeamFormFieldModule }, { kind: "component", type: i4.TheSeamFormFieldComponent, selector: "seam-form-field", inputs: ["inline", "label", "labelPosition", "labelClass", "maxErrors", "numPaddingErrors", "labelId", "helpText", "helpTextId"] }, { kind: "directive", type: i4.InputDirective, selector: "input[seamInput], textarea[seamInput], ng-select[seamInput], seam-tel-input[seamInput], quill-editor[seamInput], seam-google-maps[seamInput], seam-rich-text[seamInput]", inputs: ["seamInputSize", "id", "type", "placeholder", "required", "disabled", "readonly"], exportAs: ["seamInput"] }, { kind: "ngmodule", type: TheSeamButtonsModule }, { kind: "component", type: i4$1.TheSeamButtonComponent, selector: "button[seamButton]", inputs: ["disabled", "theme", "size", "type"], exportAs: ["seamButton"] }, { kind: "component", type: AlterationsDiffComponent, selector: "seam-alterations-diff", inputs: ["currentItems", "pendingItems", "diffMode", "initialDiffState", "compact"] }, { kind: "pipe", type: AsyncPipe, name: "async" }] });
1339
- }
1340
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TheSeamDatatablePrompterComponent, decorators: [{
1341
- type: Component,
1342
- args: [{ selector: 'seam-datatable-prompter', imports: [
1343
- ReactiveFormsModule,
1344
- AsyncPipe,
1345
- JsonPipe,
1346
- NgForOf,
1347
- NgIf,
1348
- TheSeamLoadingModule,
1349
- TheSeamRichTextModule,
1350
- TheSeamFormFieldModule,
1351
- TheSeamButtonsModule,
1352
- AlterationsDiffComponent,
1353
- ], template: "<div class=\"d-block border rounded border-lightgray\">\n <div\n [formGroup]=\"_form\"\n (ngSubmit)=\"_onSubmit()\"\n class=\"d-block position-relative\"\n >\n <div>\n <div class=\"p-1\">\n <!-- <textarea formControlName=\"prompt\" style=\"width: 800px; height: 100px;\"></textarea> -->\n <seam-form-field [numPaddingErrors]=\"0\">\n <seam-rich-text\n seamInput\n formControlName=\"prompt\"\n placeholder=\"Describe what you want to apply to the datatable.\"\n [rows]=\"3\"\n [resizable]=\"false\"\n [disableRichText]=\"true\"\n ></seam-rich-text>\n </seam-form-field>\n </div>\n <div class=\"p-1\">\n <button\n class=\"mr-1\"\n type=\"submit\"\n seamButton\n theme=\"primary\"\n size=\"sm\"\n (click)=\"_onSubmit()\"\n >\n Submit\n </button>\n <button seamButton theme=\"lightgray\" size=\"sm\" (click)=\"_form.reset()\">\n Reset\n </button>\n </div>\n </div>\n <div\n *ngIf=\"loading$ | async\"\n class=\"d-block position-absolute\"\n style=\"top: 0; left: 0; right: 0; bottom: 0\"\n >\n <seam-loading></seam-loading>\n </div>\n </div>\n\n <div class=\"p-2\" *ngIf=\"showAlts\">\n <div>Active Alterations</div>\n <div class=\"p-2 border rounded bg-lightgray\">\n <!-- {{ _alterations$ | async | json }} -->\n <div *ngFor=\"let alteration of _alterations$ | async; let last = last\">\n <div\n class=\"d-flex align-items-center border border-dark rounded p-1\"\n [class.mb-1]=\"!last\"\n >\n <span class=\"badge bg-secondary\">{{ alteration.type }}</span>\n <!-- <span class=\"badge bg-primary\">{{ alteration.label }}</span> -->\n <!-- <div>\n {{ alteration.value | json }}\n </div> -->\n <div *ngIf=\"alteration.type === 'sort'\">\n <div class=\"d-flex flex-column\">\n <div\n *ngFor=\"let sort of alteration.state.sorts; let last = last\"\n [class.mb-1]=\"!last\"\n >\n <span class=\"badge bg-lightgray\"\n >{{ sort.prop }} ({{ sort.dir }})</span\n >\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n <seam-alterations-diff\n [currentItems]=\"(_alterationsDisplayItems$ | async) ?? []\"\n [pendingItems]=\"(_pendingAlterationsDisplayItems$ | async) ?? []\"\n [compact]=\"compact\"\n [diffMode]=\"diffMode\"\n ></seam-alterations-diff>\n </div>\n</div>\n", styles: [":host{display:block;overflow:hidden}\n"] }]
1354
- }], propDecorators: { diffMode: [{
1355
- type: Input
1356
- }], compact: [{
1357
- type: Input
1358
- }], prompt: [{
1359
- type: Input
1360
- }], datatable: [{
1361
- type: Input
1362
- }], showAlts: [{
1363
- type: Input
1364
- }] } });
1365
-
1366
- // Shared providers
1
+ var publicApi = {};
1367
2
 
1368
3
  /**
1369
4
  * Generated bundle index. Do not edit.
1370
5
  */
1371
-
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 };
1373
6
  //# sourceMappingURL=theseam-ui-common-ai.mjs.map