@tylertech/forge-ai 0.13.13 → 0.14.0-dev.0

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.
@@ -11,6 +11,8 @@ declare global {
11
11
  'forge-ai-threads-search-new-chat': CustomEvent;
12
12
  'forge-ai-threads-search-rename': CustomEvent<ForgeAiThreadsSearchRenameEventData>;
13
13
  'forge-ai-threads-search-delete': CustomEvent<ForgeAiThreadsSearchDeleteEventData>;
14
+ 'forge-ai-threads-search-delete-confirm': CustomEvent<ForgeAiThreadsSearchDeleteConfirmEventData>;
15
+ 'forge-ai-threads-search-back': CustomEvent<void>;
14
16
  }
15
17
  }
16
18
  export interface ForgeAiThreadsSearchQueryEventData {
@@ -38,16 +40,23 @@ export interface ForgeAiThreadsSearchDeleteEventData {
38
40
  onSuccess: () => void;
39
41
  onError: (error?: string) => void;
40
42
  }
43
+ export interface ForgeAiThreadsSearchDeleteConfirmEventData {
44
+ thread: Thread;
45
+ }
41
46
  export declare const AiThreadsSearchComponentTagName: keyof HTMLElementTagNameMap;
42
47
  /**
43
48
  * @tag forge-ai-threads-search
44
49
  *
50
+ * @slot header-actions - Slot for a persistent action pinned to the top-right of the header, alongside the "New chat" button (e.g. a "View all" button).
51
+ *
45
52
  * @event {CustomEvent<ForgeAiThreadsSearchQueryEventData>} forge-ai-threads-search-query - Fired when search query changes (debounced).
46
53
  * @event {CustomEvent<ForgeAiThreadsSearchLoadMoreEventData>} forge-ai-threads-search-load-more - Fired when user scrolls near bottom for pagination.
47
54
  * @event {CustomEvent<ForgeAiThreadsSearchSelectEventData>} forge-ai-threads-search-select - Fired when a thread is selected.
48
55
  * @event {CustomEvent} forge-ai-threads-search-new-chat - Fired when new chat button clicked.
49
56
  * @event {CustomEvent<ForgeAiThreadsSearchRenameEventData>} forge-ai-threads-search-rename - Fired when thread renamed. Cancelable - if prevented, call onSuccess() to commit or onError() to revert.
50
57
  * @event {CustomEvent<ForgeAiThreadsSearchDeleteEventData>} forge-ai-threads-search-delete - Fired when thread delete confirmed. Cancelable - if prevented, call onSuccess() to commit deletion or onError() to revert.
58
+ * @event {CustomEvent<ForgeAiThreadsSearchDeleteConfirmEventData>} forge-ai-threads-search-delete-confirm - Fired before showing the built-in delete confirmation. Cancelable - if prevented, this component shows no confirmation UI; the host must show its own and call confirmThreadDelete() once accepted.
59
+ * @event {CustomEvent<void>} forge-ai-threads-search-back - Fired when the back button (shown via showBackButton) is clicked.
51
60
  *
52
61
  * @description A standalone search component for conversations/threads. Can be slotted into forge-ai-threads
53
62
  * or used independently. Supports both local and external/async search patterns via event callbacks.
@@ -57,6 +66,22 @@ export declare class AiThreadsSearchComponent extends LitElement {
57
66
  #private;
58
67
  static styles: CSSResult;
59
68
  threads: Thread[];
69
+ /**
70
+ * The title text shown in the header.
71
+ */
72
+ headerTitle: string;
73
+ /**
74
+ * Whether to show the "New chat" button in the header.
75
+ */
76
+ showNewChatButton: boolean;
77
+ /**
78
+ * Whether to show a back button before the header title.
79
+ */
80
+ showBackButton: boolean;
81
+ /**
82
+ * Whether to show the search field.
83
+ */
84
+ showSearch: boolean;
60
85
  /**
61
86
  * Total number of threads available. When set to a positive number and fewer threads
62
87
  * are loaded than the total, infinite scroll is enabled. Leave at 0 (default) to disable
@@ -67,15 +92,26 @@ export declare class AiThreadsSearchComponent extends LitElement {
67
92
  emptyMessage: string;
68
93
  showThreadRename: boolean;
69
94
  showThreadDelete: boolean;
95
+ /**
96
+ * The id of the currently selected/active thread, highlighted in the list.
97
+ */
98
+ selectedThreadId: string | null;
70
99
  private _resultsContainer;
71
100
  private _searchQuery;
72
101
  private _isSearching;
73
102
  private _searchResults;
74
103
  private _editingThreadId;
75
104
  private _hiddenThreadIds;
105
+ private _openMenuThreadId;
76
106
  private _searchTimeout?;
77
107
  firstUpdated(): void;
78
108
  updated(changedProperties: Map<string, unknown>): void;
79
109
  disconnectedCallback(): void;
110
+ /**
111
+ * Completes a delete that a host intercepted via `forge-ai-threads-search-delete-confirm`
112
+ * (calling preventDefault() to show its own confirmation instead of this component's
113
+ * built-in one). Call once the host's own confirmation has been accepted.
114
+ */
115
+ confirmThreadDelete(thread: Thread): void;
80
116
  render(): TemplateResult;
81
117
  }
@@ -1,6 +1,7 @@
1
1
  import { unsafeCSS, LitElement, html } from "lit";
2
2
  import { property, query, state, customElement } from "lit/decorators.js";
3
3
  import { when } from "lit/directives/when.js";
4
+ import { formatRelativeTime } from "../utils.mjs";
4
5
  import { DeleteThreadController } from "../utils/delete-thread-controller.mjs";
5
6
  import { InfiniteScrollController } from "../utils/infinite-scroll-controller.mjs";
6
7
  import "../ai-thread-actions-menu/ai-thread-actions-menu.mjs";
@@ -25,48 +26,31 @@ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot
25
26
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
26
27
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
27
28
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
28
- var _deleteThreadController, _infiniteScrollController, _AiThreadsSearchComponent_instances, shouldEnablePagination_get, handleSearchInput_fn, performSearch_fn, loadMore_fn, handleThreadSelect_fn, handleNewChatClick_fn, handleMenuRename_fn, handleMenuDeleteClick_fn, handleEditSave_fn, handleEditCancel_fn, formatRelativeTime_fn, handleClearSearch_fn, displayedThreads_get, header_get, searchField_get, renderThreadItem_fn, resultsList_get;
29
+ var _deleteThreadController, _infiniteScrollController, _AiThreadsSearchComponent_instances, shouldEnablePagination_get, handleSearchInput_fn, performSearch_fn, loadMore_fn, handleThreadSelect_fn, handleNewChatClick_fn, handleBackClick_fn, handleMenuRename_fn, handleMenuOpen_fn, handleMenuClose_fn, handleMenuDeleteClick_fn, confirmDelete_fn, handleEditSave_fn, handleEditCancel_fn, handleClearSearch_fn, displayedThreads_get, header_get, searchField_get, renderThreadItem_fn, resultsList_get;
29
30
  const AiThreadsSearchComponentTagName = "forge-ai-threads-search";
30
31
  let AiThreadsSearchComponent = class extends LitElement {
31
32
  constructor() {
32
33
  super(...arguments);
33
34
  __privateAdd(this, _AiThreadsSearchComponent_instances);
34
35
  this.threads = [];
36
+ this.headerTitle = "All chats";
37
+ this.showNewChatButton = true;
38
+ this.showBackButton = false;
39
+ this.showSearch = true;
35
40
  this.totalChats = 0;
36
41
  this.placeholder = "Search chats...";
37
42
  this.emptyMessage = "No chats found";
38
43
  this.showThreadRename = false;
39
44
  this.showThreadDelete = false;
45
+ this.selectedThreadId = null;
40
46
  this._searchQuery = "";
41
47
  this._isSearching = false;
42
48
  this._searchResults = [];
43
49
  this._editingThreadId = null;
44
50
  this._hiddenThreadIds = /* @__PURE__ */ new Set();
51
+ this._openMenuThreadId = null;
45
52
  __privateAdd(this, _deleteThreadController, new DeleteThreadController(this, {
46
- onConfirm: (thread) => {
47
- const onSuccess = () => {
48
- this._hiddenThreadIds.add(thread.id);
49
- this.requestUpdate();
50
- };
51
- const onError = (error) => {
52
- this._hiddenThreadIds.delete(thread.id);
53
- this.requestUpdate();
54
- if (error) {
55
- console.error("Delete failed:", error);
56
- }
57
- };
58
- const event = new CustomEvent("forge-ai-threads-search-delete", {
59
- detail: { id: thread.id, thread, onSuccess, onError },
60
- bubbles: true,
61
- composed: true,
62
- cancelable: true
63
- });
64
- const dispatched = this.dispatchEvent(event);
65
- if (dispatched) {
66
- this._hiddenThreadIds.add(thread.id);
67
- this.requestUpdate();
68
- }
69
- }
53
+ onConfirm: (thread) => __privateMethod(this, _AiThreadsSearchComponent_instances, confirmDelete_fn).call(this, thread)
70
54
  }));
71
55
  __privateAdd(this, _infiniteScrollController, new InfiniteScrollController(this, {
72
56
  onLoadMore: () => __privateMethod(this, _AiThreadsSearchComponent_instances, loadMore_fn).call(this)
@@ -93,9 +77,19 @@ let AiThreadsSearchComponent = class extends LitElement {
93
77
  super.disconnectedCallback();
94
78
  clearTimeout(this._searchTimeout);
95
79
  }
80
+ /**
81
+ * Completes a delete that a host intercepted via `forge-ai-threads-search-delete-confirm`
82
+ * (calling preventDefault() to show its own confirmation instead of this component's
83
+ * built-in one). Call once the host's own confirmation has been accepted.
84
+ */
85
+ confirmThreadDelete(thread) {
86
+ __privateMethod(this, _AiThreadsSearchComponent_instances, confirmDelete_fn).call(this, thread);
87
+ }
96
88
  render() {
97
89
  return html`
98
- <div class="threads-search-container">${__privateGet(this, _AiThreadsSearchComponent_instances, header_get)} ${__privateGet(this, _AiThreadsSearchComponent_instances, searchField_get)} ${__privateGet(this, _AiThreadsSearchComponent_instances, resultsList_get)}</div>
90
+ <div class="threads-search-container">
91
+ ${__privateGet(this, _AiThreadsSearchComponent_instances, header_get)} ${when(this.showSearch, () => __privateGet(this, _AiThreadsSearchComponent_instances, searchField_get))} ${__privateGet(this, _AiThreadsSearchComponent_instances, resultsList_get)}
92
+ </div>
99
93
  ${__privateGet(this, _deleteThreadController).template}
100
94
  `;
101
95
  }
@@ -180,8 +174,22 @@ handleNewChatClick_fn = function() {
180
174
  });
181
175
  this.dispatchEvent(event);
182
176
  };
177
+ handleBackClick_fn = function() {
178
+ const event = new CustomEvent("forge-ai-threads-search-back", {
179
+ bubbles: true,
180
+ composed: true
181
+ });
182
+ this.dispatchEvent(event);
183
+ };
183
184
  handleMenuRename_fn = function(e) {
184
185
  this._editingThreadId = e.detail.id;
186
+ this._openMenuThreadId = null;
187
+ };
188
+ handleMenuOpen_fn = function(e) {
189
+ this._openMenuThreadId = e.detail.id;
190
+ };
191
+ handleMenuClose_fn = function() {
192
+ this._openMenuThreadId = null;
185
193
  };
186
194
  handleMenuDeleteClick_fn = function(e) {
187
195
  const { id } = e.detail;
@@ -189,7 +197,43 @@ handleMenuDeleteClick_fn = function(e) {
189
197
  if (!thread) {
190
198
  return;
191
199
  }
192
- __privateGet(this, _deleteThreadController).show(thread);
200
+ const event = new CustomEvent(
201
+ "forge-ai-threads-search-delete-confirm",
202
+ {
203
+ detail: { thread },
204
+ bubbles: true,
205
+ composed: true,
206
+ cancelable: true
207
+ }
208
+ );
209
+ const dispatched = this.dispatchEvent(event);
210
+ if (dispatched) {
211
+ __privateGet(this, _deleteThreadController).show(thread);
212
+ }
213
+ };
214
+ confirmDelete_fn = function(thread) {
215
+ const onSuccess = () => {
216
+ this._hiddenThreadIds.add(thread.id);
217
+ this.requestUpdate();
218
+ };
219
+ const onError = (error) => {
220
+ this._hiddenThreadIds.delete(thread.id);
221
+ this.requestUpdate();
222
+ if (error) {
223
+ console.error("Delete failed:", error);
224
+ }
225
+ };
226
+ const event = new CustomEvent("forge-ai-threads-search-delete", {
227
+ detail: { id: thread.id, thread, onSuccess, onError },
228
+ bubbles: true,
229
+ composed: true,
230
+ cancelable: true
231
+ });
232
+ const dispatched = this.dispatchEvent(event);
233
+ if (dispatched) {
234
+ this._hiddenThreadIds.add(thread.id);
235
+ this.requestUpdate();
236
+ }
193
237
  };
194
238
  handleEditSave_fn = function(e) {
195
239
  const { id, oldTitle, newTitle } = e.detail;
@@ -224,32 +268,6 @@ handleEditSave_fn = function(e) {
224
268
  handleEditCancel_fn = function() {
225
269
  this._editingThreadId = null;
226
270
  };
227
- formatRelativeTime_fn = function(dateString) {
228
- const date = new Date(dateString);
229
- const now = /* @__PURE__ */ new Date();
230
- const diffMs = now.getTime() - date.getTime();
231
- const diffMinutes = Math.floor(diffMs / (1e3 * 60));
232
- const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
233
- const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
234
- const diffWeeks = Math.floor(diffDays / 7);
235
- const diffMonths = Math.floor(diffDays / 30);
236
- const diffYears = Math.floor(diffDays / 365);
237
- if (diffYears >= 1) {
238
- return `${diffYears}y ago`;
239
- } else if (diffMonths >= 1) {
240
- return `${diffMonths}mo ago`;
241
- } else if (diffWeeks >= 1) {
242
- return `${diffWeeks}w ago`;
243
- } else if (diffDays >= 1) {
244
- return `${diffDays}d ago`;
245
- } else if (diffHours >= 1) {
246
- return `${diffHours}h ago`;
247
- } else if (diffMinutes >= 1) {
248
- return `${diffMinutes}m ago`;
249
- } else {
250
- return "Just now";
251
- }
252
- };
253
271
  handleClearSearch_fn = function() {
254
272
  this._searchQuery = "";
255
273
  this._searchResults = [];
@@ -263,21 +281,45 @@ displayedThreads_get = function() {
263
281
  header_get = function() {
264
282
  return html`
265
283
  <div class="header">
266
- <div class="header-title">
267
- <span class="title">All chats</span>
284
+ <div class="header-start">
285
+ ${when(
286
+ this.showBackButton,
287
+ () => html`
288
+ <button
289
+ class="back-button forge-icon-button forge-icon-button--medium"
290
+ @click=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleBackClick_fn)}
291
+ aria-label="Back">
292
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
293
+ <path fill="none" d="M0 0h24v24H0z" />
294
+ <path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z" />
295
+ </svg>
296
+ </button>
297
+ `
298
+ )}
299
+ <div class="header-title">
300
+ <span class="title">${this.headerTitle}</span>
301
+ </div>
302
+ </div>
303
+ <div class="header-end">
304
+ ${when(
305
+ this.showNewChatButton,
306
+ () => html`
307
+ <button class="forge-button new-chat-button" @click=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleNewChatClick_fn)}>
308
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="forge-icon" aria-hidden="true">
309
+ <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
310
+ </svg>
311
+ New chat
312
+ </button>
313
+ `
314
+ )}
315
+ <slot name="header-actions"></slot>
268
316
  </div>
269
- <button class="forge-button new-chat-button" @click=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleNewChatClick_fn)}>
270
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="forge-icon" aria-hidden="true">
271
- <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
272
- </svg>
273
- New chat
274
- </button>
275
317
  </div>
276
318
  `;
277
319
  };
278
320
  searchField_get = function() {
279
321
  return html`
280
- <div class="forge-field forge-field--rounded forge-field--small search-field">
322
+ <div class="forge-field forge-field--small search-field">
281
323
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="forge-icon" aria-hidden="true">
282
324
  <path fill="none" d="M0 0h24v24H0z" />
283
325
  <path
@@ -289,7 +331,7 @@ searchField_get = function() {
289
331
  .value=${this._searchQuery}
290
332
  placeholder=${this.placeholder}
291
333
  @input=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleSearchInput_fn)} />
292
- ${when(this._isSearching, () => html`<forge-ai-spinner size="small"></forge-ai-spinner>`)}
334
+ ${when(this._isSearching, () => html`<forge-ai-spinner size="extra-small"></forge-ai-spinner>`)}
293
335
  ${when(
294
336
  this._searchQuery.length,
295
337
  () => html`
@@ -306,8 +348,12 @@ searchField_get = function() {
306
348
  };
307
349
  renderThreadItem_fn = function(thread) {
308
350
  const isEditing = this._editingThreadId === thread.id;
351
+ const hasOpenMenu = this._openMenuThreadId === thread.id;
352
+ const isSelected = this.selectedThreadId === thread.id;
309
353
  return html`
310
- <li class="forge-list-item" role="listitem">
354
+ <li
355
+ class="forge-list-item ${isSelected ? "forge-list-item--selected" : ""} ${hasOpenMenu ? "forge-list-item--menu-open" : ""}"
356
+ role="listitem">
311
357
  ${when(
312
358
  isEditing,
313
359
  () => html`
@@ -319,8 +365,10 @@ renderThreadItem_fn = function(thread) {
319
365
  </forge-ai-edit-thread>
320
366
  `,
321
367
  () => html`
322
- <button type="button" @click=${() => __privateMethod(this, _AiThreadsSearchComponent_instances, handleThreadSelect_fn).call(this, thread)}>${thread.title}</button>
323
- <span class="thread-time">${__privateMethod(this, _AiThreadsSearchComponent_instances, formatRelativeTime_fn).call(this, thread.createdAt)}</span>
368
+ <button type="button" aria-selected=${isSelected} @click=${() => __privateMethod(this, _AiThreadsSearchComponent_instances, handleThreadSelect_fn).call(this, thread)}>
369
+ ${thread.title}
370
+ </button>
371
+ <span class="thread-time">${formatRelativeTime(thread.createdAt)}</span>
324
372
  ${when(
325
373
  this.showThreadRename || this.showThreadDelete,
326
374
  () => html`
@@ -330,7 +378,9 @@ renderThreadItem_fn = function(thread) {
330
378
  ?show-rename=${this.showThreadRename}
331
379
  ?show-delete=${this.showThreadDelete}
332
380
  @forge-ai-thread-actions-menu-rename=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleMenuRename_fn)}
333
- @forge-ai-thread-actions-menu-delete-click=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleMenuDeleteClick_fn)}>
381
+ @forge-ai-thread-actions-menu-delete-click=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleMenuDeleteClick_fn)}
382
+ @forge-ai-thread-actions-menu-open=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleMenuOpen_fn)}
383
+ @forge-ai-thread-actions-menu-close=${__privateMethod(this, _AiThreadsSearchComponent_instances, handleMenuClose_fn)}>
334
384
  </forge-ai-thread-actions-menu>
335
385
  </div>
336
386
  `
@@ -372,6 +422,18 @@ AiThreadsSearchComponent.styles = unsafeCSS(styles);
372
422
  __decorateClass([
373
423
  property({ type: Array })
374
424
  ], AiThreadsSearchComponent.prototype, "threads", 2);
425
+ __decorateClass([
426
+ property({ type: String, attribute: "header-title" })
427
+ ], AiThreadsSearchComponent.prototype, "headerTitle", 2);
428
+ __decorateClass([
429
+ property({ type: Boolean, attribute: "show-new-chat-button" })
430
+ ], AiThreadsSearchComponent.prototype, "showNewChatButton", 2);
431
+ __decorateClass([
432
+ property({ type: Boolean, attribute: "show-back-button" })
433
+ ], AiThreadsSearchComponent.prototype, "showBackButton", 2);
434
+ __decorateClass([
435
+ property({ type: Boolean, attribute: "show-search" })
436
+ ], AiThreadsSearchComponent.prototype, "showSearch", 2);
375
437
  __decorateClass([
376
438
  property({ type: Number, attribute: "total-chats" })
377
439
  ], AiThreadsSearchComponent.prototype, "totalChats", 2);
@@ -387,6 +449,9 @@ __decorateClass([
387
449
  __decorateClass([
388
450
  property({ type: Boolean, attribute: "show-thread-delete" })
389
451
  ], AiThreadsSearchComponent.prototype, "showThreadDelete", 2);
452
+ __decorateClass([
453
+ property({ type: String, attribute: "selected-thread-id" })
454
+ ], AiThreadsSearchComponent.prototype, "selectedThreadId", 2);
390
455
  __decorateClass([
391
456
  query(".results-container")
392
457
  ], AiThreadsSearchComponent.prototype, "_resultsContainer", 2);
@@ -405,6 +470,9 @@ __decorateClass([
405
470
  __decorateClass([
406
471
  state()
407
472
  ], AiThreadsSearchComponent.prototype, "_hiddenThreadIds", 2);
473
+ __decorateClass([
474
+ state()
475
+ ], AiThreadsSearchComponent.prototype, "_openMenuThreadId", 2);
408
476
  AiThreadsSearchComponent = __decorateClass([
409
477
  customElement(AiThreadsSearchComponentTagName)
410
478
  ], AiThreadsSearchComponent);