@aparte/core 0.3.0-alpha.0 → 0.4.0-alpha.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.
Files changed (39) hide show
  1. package/README.md +21 -0
  2. package/dist/client/aparte-client.d.ts.map +1 -1
  3. package/dist/components/bubble/aparte-chat-bubble.d.ts +11 -0
  4. package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
  5. package/dist/components/chat/aparte-chat.d.ts +13 -0
  6. package/dist/components/chat/aparte-chat.d.ts.map +1 -1
  7. package/dist/components/viewport/aparte-chat-viewport.d.ts +13 -2
  8. package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
  9. package/dist/config/aparte-config.d.ts +10 -2
  10. package/dist/config/aparte-config.d.ts.map +1 -1
  11. package/dist/config/icon-provider.d.ts +21 -21
  12. package/dist/config/icon-provider.d.ts.map +1 -1
  13. package/dist/conversations/conversation-controller.d.ts.map +1 -1
  14. package/dist/custom-elements.json +7604 -7360
  15. package/dist/host/aparte-chat-host.d.ts +47 -1
  16. package/dist/host/aparte-chat-host.d.ts.map +1 -1
  17. package/dist/{index-CW2BCAqn.js → index-BBg6dsYx.js} +164 -41
  18. package/dist/index-BBg6dsYx.js.map +1 -0
  19. package/dist/index.css +60 -6
  20. package/dist/index.d.ts +10 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +122 -26
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.node.d.ts +2 -0
  25. package/dist/index.node.d.ts.map +1 -1
  26. package/dist/index.node.js +16 -14
  27. package/dist/index.node.js.map +1 -1
  28. package/dist/primitives/select/aparte-optgroup.d.ts +2 -0
  29. package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
  30. package/dist/primitives/select/aparte-select.d.ts +2 -0
  31. package/dist/primitives/select/aparte-select.d.ts.map +1 -1
  32. package/dist/types/imperative-api.d.ts +13 -0
  33. package/dist/types/imperative-api.d.ts.map +1 -1
  34. package/dist/utils/files-to-attachments.d.ts +27 -0
  35. package/dist/utils/files-to-attachments.d.ts.map +1 -0
  36. package/dist/utils/is-awaiting-reply.d.ts +22 -0
  37. package/dist/utils/is-awaiting-reply.d.ts.map +1 -0
  38. package/package.json +8 -1
  39. package/dist/index-CW2BCAqn.js.map +0 -1
@@ -78,6 +78,12 @@ export declare class AparteChatHost {
78
78
  private _pendingSiblings?;
79
79
  /** Abort handle for an in-flight {@link streamTokens} loop. */
80
80
  private _streamAbort?;
81
+ /** Segment chunks written to the bubble, awaiting one coalesced state sync. */
82
+ private readonly _segmentBuffer;
83
+ /** {@link streamTokens} text written to the bubble, awaiting the same sync. */
84
+ private _tokenBuffer;
85
+ /** Handle of the scheduled coalesced sync, or null when none is pending. */
86
+ private _flushHandle;
81
87
  private _conversationId;
82
88
  private _controller?;
83
89
  private _teardown;
@@ -129,8 +135,35 @@ export declare class AparteChatHost {
129
135
  updateSegment(segmentId: string, updates: Partial<AparteSegment>): void;
130
136
  /** Remove a transient segment (e.g. pipeline-waiting indicator). */
131
137
  removeSegment(segmentId: string): void;
132
- /** Append text to a segment's content in the last message. */
138
+ /**
139
+ * Append text to a segment's content in the last message — the streaming path
140
+ * for thinking blocks, tool pills and any segment that grows token by token.
141
+ *
142
+ * Two writes, deliberately asymmetric:
143
+ * 1. straight into the bubble, every chunk, so the text appears immediately —
144
+ * the same direct path `appendToken` uses;
145
+ * 2. into the framework's message list, **coalesced to once per frame**.
146
+ *
147
+ * It used to do only (2), synchronously: a full list rebuild plus
148
+ * `setMessages` + `onMessagesChange` for every chunk, i.e. one complete
149
+ * framework render per token. At 60 tok/s from a local model that is unusable,
150
+ * and nothing in the imperative API hinted that `appendToSegment` cost so much
151
+ * more than `injectTokenStream`. Consumers had to write their own rAF batcher.
152
+ *
153
+ * Any structural change flushes the buffer first (see
154
+ * {@link _flushStreamState}), so ordering is never observable.
155
+ */
133
156
  appendToSegment(segmentId: string, content: string): void;
157
+ /** Schedule the coalesced segment-state sync (idempotent within a frame). */
158
+ private _scheduleStreamFlush;
159
+ /**
160
+ * Write every buffered stream chunk — segment chunks and {@link streamTokens}
161
+ * text alike — into the framework's message list in **one** update. Called by
162
+ * the scheduled frame, and eagerly by any mutation that would otherwise observe
163
+ * a stale list (a new segment, a new message, a conversation swap): flushing
164
+ * first keeps the emitted order identical to the call order.
165
+ */
166
+ private _flushStreamState;
134
167
  /** Read the current message list. */
135
168
  getMessages(): AparteMessage[];
136
169
  /** Clear all messages + reset viewport state. */
@@ -148,8 +181,21 @@ export declare class AparteChatHost {
148
181
  * appends each token, completes the message at the end. Per-framework
149
182
  * wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
150
183
  * ReadableStream) into this.
184
+ *
185
+ * Each token reaches the bubble immediately and the framework's message list
186
+ * is synced **once per frame** (same discipline as {@link appendToSegment}),
187
+ * with a guaranteed flush before completion and on abort. Before that sync
188
+ * existed the two disagreed: the DOM had the reply, the framework list still
189
+ * held `content: ''` — so any re-render from state wiped it, and a custom
190
+ * bubble (which renders from that state) showed nothing at all.
151
191
  */
152
192
  streamTokens(messageId: string, tokens: AsyncIterable<string>): Promise<void>;
193
+ /**
194
+ * Buffer a streamed token for the coalesced state sync. A different message id
195
+ * (a second stream started without stopping the first) flushes the previous
196
+ * one, so text never migrates between messages.
197
+ */
198
+ private _bufferToken;
153
199
  /** Abort an in-flight {@link streamTokens} loop. */
154
200
  stopTokenStream(): void;
155
201
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"aparte-chat-host.d.ts","sourceRoot":"","sources":["../../src/host/aparte-chat-host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"aparte-chat-host.d.ts","sourceRoot":"","sources":["../../src/host/aparte-chat-host.ts"],"names":[],"mappings":"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"}
@@ -1165,13 +1165,24 @@ class AparteConfigClass {
1165
1165
  return this._artifactPreviewBuilder;
1166
1166
  }
1167
1167
  /**
1168
- * Get the current icon provider, or a proxy that falls back to DEFAULT_ICON_FALLBACKS
1168
+ * The icon set as a **complete** provider: every name resolves, falling back
1169
+ * to `DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
1170
+ * implement. Callers (bubble action bar, composer controls) can therefore
1171
+ * invoke `icons.copy()` unconditionally.
1172
+ *
1173
+ * It used to hand back the registered provider verbatim, so a provider that
1174
+ * implemented a subset — which `getIcon()` has always supported, and which
1175
+ * the type now states — crashed every other icon with
1176
+ * "icons.retry is not a function".
1169
1177
  */
1170
1178
  getIconProvider() {
1171
- if (this._iconProvider) return this._iconProvider;
1172
- return Object.fromEntries(
1173
- Object.entries(DEFAULT_ICON_FALLBACKS).map(([k, v]) => [k, () => v])
1174
- );
1179
+ const registered = this._iconProvider;
1180
+ const complete = {};
1181
+ for (const name of Object.keys(DEFAULT_ICON_FALLBACKS)) {
1182
+ const fn = registered?.[name];
1183
+ complete[name] = fn ?? (() => DEFAULT_ICON_FALLBACKS[name]);
1184
+ }
1185
+ return complete;
1175
1186
  }
1176
1187
  /**
1177
1188
  * Set a custom avatar renderer. Lets framework consumers (Angular,
@@ -1296,9 +1307,8 @@ class AparteConfigClass {
1296
1307
  * Falls back to textual representation if no provider is set
1297
1308
  */
1298
1309
  getIcon(name) {
1299
- if (this._iconProvider && this._iconProvider[name]) {
1300
- return this._iconProvider[name]();
1301
- }
1310
+ const icon = this._iconProvider?.[name];
1311
+ if (icon) return icon();
1302
1312
  return DEFAULT_ICON_FALLBACKS[name];
1303
1313
  }
1304
1314
  /**
@@ -3319,6 +3329,19 @@ class ConversationManager {
3319
3329
  return text.trim() || "New Chat";
3320
3330
  }
3321
3331
  }
3332
+ function attachmentId() {
3333
+ return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3334
+ }
3335
+ function filesToAttachments(files) {
3336
+ return files.map((file) => ({
3337
+ id: attachmentId(),
3338
+ name: file.name,
3339
+ type: file.type || "application/octet-stream",
3340
+ url: URL.createObjectURL(file),
3341
+ size: file.size,
3342
+ blob: file
3343
+ }));
3344
+ }
3322
3345
  class AparteConversationController {
3323
3346
  _binding;
3324
3347
  _options;
@@ -3363,16 +3386,7 @@ class AparteConversationController {
3363
3386
  if (targetId && targetId !== this._binding.hostId) return;
3364
3387
  const content = evt.detail?.content ?? "";
3365
3388
  const files = evt.detail?.files;
3366
- const attachments = files?.length ? files.map((f) => ({
3367
- id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
3368
- name: f.name,
3369
- type: f.type || "application/octet-stream",
3370
- url: URL.createObjectURL(f),
3371
- size: f.size,
3372
- // Carry the raw File so the storage adapter can persist it
3373
- // to its attachments table; reload reconstructs `url`.
3374
- blob: f
3375
- })) : void 0;
3389
+ const attachments = files?.length ? filesToAttachments(files) : void 0;
3376
3390
  const userMsg = {
3377
3391
  id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`,
3378
3392
  role: "user",
@@ -3622,6 +3636,12 @@ class AparteChatHost {
3622
3636
  _pendingSiblings;
3623
3637
  /** Abort handle for an in-flight {@link streamTokens} loop. */
3624
3638
  _streamAbort;
3639
+ /** Segment chunks written to the bubble, awaiting one coalesced state sync. */
3640
+ _segmentBuffer = /* @__PURE__ */ new Map();
3641
+ /** {@link streamTokens} text written to the bubble, awaiting the same sync. */
3642
+ _tokenBuffer = null;
3643
+ /** Handle of the scheduled coalesced sync, or null when none is pending. */
3644
+ _flushHandle = null;
3625
3645
  _conversationId = null;
3626
3646
  _controller;
3627
3647
  _teardown = [];
@@ -3734,6 +3754,9 @@ class AparteChatHost {
3734
3754
  }
3735
3755
  /** Drop transient streaming state so a new conversation starts clean. */
3736
3756
  _beginConversationSwap() {
3757
+ this._segmentBuffer.clear();
3758
+ this._tokenBuffer = null;
3759
+ this._flushStreamState();
3737
3760
  this.stopTokenStream();
3738
3761
  this._setStreamingId(null);
3739
3762
  this.binding.onTypingChange?.(false);
@@ -3755,12 +3778,14 @@ class AparteChatHost {
3755
3778
  * is the append-specific signal instead.
3756
3779
  */
3757
3780
  appendMessage(message) {
3781
+ this._flushStreamState();
3758
3782
  if (message.role === "user") this._vp()?.setAutoScroll?.(true);
3759
3783
  this.binding.setMessages([...this.binding.getMessages(), message]);
3760
3784
  this.binding.onMessageAppended?.(message);
3761
3785
  }
3762
3786
  /** Atomic partial update of a message by id. */
3763
3787
  updateMessage(messageId, updates) {
3788
+ this._flushStreamState();
3764
3789
  const msgs = this.binding.getMessages();
3765
3790
  const index = msgs.findIndex((m) => m.id === messageId);
3766
3791
  if (index === -1) return;
@@ -3786,6 +3811,7 @@ class AparteChatHost {
3786
3811
  }
3787
3812
  /** Add a segment to the last message (immediate bubble + state sync). */
3788
3813
  addSegment(segment) {
3814
+ this._flushStreamState();
3789
3815
  const msgs = this.binding.getMessages();
3790
3816
  const last = msgs[msgs.length - 1];
3791
3817
  if (last && this._isOrphan(last.id)) return;
@@ -3799,6 +3825,7 @@ class AparteChatHost {
3799
3825
  }
3800
3826
  /** Update a segment in the last message in place. */
3801
3827
  updateSegment(segmentId, updates) {
3828
+ this._flushStreamState();
3802
3829
  const msgs = this.binding.getMessages();
3803
3830
  const last = msgs[msgs.length - 1];
3804
3831
  if (last && this._isOrphan(last.id)) return;
@@ -3813,6 +3840,7 @@ class AparteChatHost {
3813
3840
  }
3814
3841
  /** Remove a transient segment (e.g. pipeline-waiting indicator). */
3815
3842
  removeSegment(segmentId) {
3843
+ this._flushStreamState();
3816
3844
  this._lastBubble()?.removeSegment?.(segmentId);
3817
3845
  const msgs = this.binding.getMessages();
3818
3846
  const last = msgs[msgs.length - 1];
@@ -3823,18 +3851,85 @@ class AparteChatHost {
3823
3851
  this.binding.setMessages(next);
3824
3852
  this.binding.onMessagesChange?.(next);
3825
3853
  }
3826
- /** Append text to a segment's content in the last message. */
3854
+ /**
3855
+ * Append text to a segment's content in the last message — the streaming path
3856
+ * for thinking blocks, tool pills and any segment that grows token by token.
3857
+ *
3858
+ * Two writes, deliberately asymmetric:
3859
+ * 1. straight into the bubble, every chunk, so the text appears immediately —
3860
+ * the same direct path `appendToken` uses;
3861
+ * 2. into the framework's message list, **coalesced to once per frame**.
3862
+ *
3863
+ * It used to do only (2), synchronously: a full list rebuild plus
3864
+ * `setMessages` + `onMessagesChange` for every chunk, i.e. one complete
3865
+ * framework render per token. At 60 tok/s from a local model that is unusable,
3866
+ * and nothing in the imperative API hinted that `appendToSegment` cost so much
3867
+ * more than `injectTokenStream`. Consumers had to write their own rAF batcher.
3868
+ *
3869
+ * Any structural change flushes the buffer first (see
3870
+ * {@link _flushStreamState}), so ordering is never observable.
3871
+ */
3827
3872
  appendToSegment(segmentId, content) {
3828
3873
  const msgs = this.binding.getMessages();
3829
3874
  const last = msgs[msgs.length - 1];
3830
3875
  if (!last || !last.segments) return;
3831
3876
  if (this._isOrphan(last.id)) return;
3832
- const segments = last.segments.map(
3833
- (s) => s.id === segmentId && "content" in s ? { ...s, content: s.content + content } : s
3834
- );
3835
- const next = [...msgs.slice(0, -1), { ...last, segments }];
3836
- this.binding.setMessages(next);
3837
- this.binding.onMessagesChange?.(next);
3877
+ this._vp()?.appendToSegment?.(last.id, segmentId, content);
3878
+ this._segmentBuffer.set(segmentId, (this._segmentBuffer.get(segmentId) ?? "") + content);
3879
+ this._scheduleStreamFlush();
3880
+ }
3881
+ /** Schedule the coalesced segment-state sync (idempotent within a frame). */
3882
+ _scheduleStreamFlush() {
3883
+ if (this._flushHandle !== null) return;
3884
+ const run = () => {
3885
+ this._flushHandle = null;
3886
+ this._flushStreamState();
3887
+ };
3888
+ this._flushHandle = typeof requestAnimationFrame === "function" ? requestAnimationFrame(run) : setTimeout(run, 0);
3889
+ }
3890
+ /**
3891
+ * Write every buffered stream chunk — segment chunks and {@link streamTokens}
3892
+ * text alike — into the framework's message list in **one** update. Called by
3893
+ * the scheduled frame, and eagerly by any mutation that would otherwise observe
3894
+ * a stale list (a new segment, a new message, a conversation swap): flushing
3895
+ * first keeps the emitted order identical to the call order.
3896
+ */
3897
+ _flushStreamState() {
3898
+ if (this._flushHandle !== null) {
3899
+ if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(this._flushHandle);
3900
+ else clearTimeout(this._flushHandle);
3901
+ this._flushHandle = null;
3902
+ }
3903
+ const tokens = this._tokenBuffer;
3904
+ const pending = this._segmentBuffer.size > 0 ? new Map(this._segmentBuffer) : null;
3905
+ if (!tokens && !pending) return;
3906
+ this._tokenBuffer = null;
3907
+ this._segmentBuffer.clear();
3908
+ let msgs = this.binding.getMessages();
3909
+ let changed = false;
3910
+ if (tokens) {
3911
+ const index = msgs.findIndex((m) => m.id === tokens.messageId);
3912
+ if (index !== -1) {
3913
+ const next = [...msgs];
3914
+ next[index] = { ...next[index], content: (next[index].content ?? "") + tokens.text };
3915
+ msgs = next;
3916
+ changed = true;
3917
+ }
3918
+ }
3919
+ if (pending) {
3920
+ const last = msgs[msgs.length - 1];
3921
+ if (last?.segments) {
3922
+ const segments = last.segments.map((s) => {
3923
+ const chunk = pending.get(s.id);
3924
+ return chunk !== void 0 && "content" in s ? { ...s, content: s.content + chunk } : s;
3925
+ });
3926
+ msgs = [...msgs.slice(0, -1), { ...last, segments }];
3927
+ changed = true;
3928
+ }
3929
+ }
3930
+ if (!changed) return;
3931
+ this.binding.setMessages(msgs);
3932
+ this.binding.onMessagesChange?.(msgs);
3838
3933
  }
3839
3934
  /** Read the current message list. */
3840
3935
  getMessages() {
@@ -3880,6 +3975,13 @@ class AparteChatHost {
3880
3975
  * appends each token, completes the message at the end. Per-framework
3881
3976
  * wrappers adapt their native stream type (RxJS Observable, AsyncIterable,
3882
3977
  * ReadableStream) into this.
3978
+ *
3979
+ * Each token reaches the bubble immediately and the framework's message list
3980
+ * is synced **once per frame** (same discipline as {@link appendToSegment}),
3981
+ * with a guaranteed flush before completion and on abort. Before that sync
3982
+ * existed the two disagreed: the DOM had the reply, the framework list still
3983
+ * held `content: ''` — so any re-render from state wiped it, and a custom
3984
+ * bubble (which renders from that state) showed nothing at all.
3883
3985
  */
3884
3986
  async streamTokens(messageId, tokens) {
3885
3987
  this.stopTokenStream();
@@ -3899,10 +4001,13 @@ class AparteChatHost {
3899
4001
  const res = await Promise.race([iterator.next(), aborted]);
3900
4002
  if (res === "aborted" || ac.signal.aborted || res.done) break;
3901
4003
  this._vp()?.appendToken?.(messageId, res.value);
4004
+ this._bufferToken(messageId, res.value);
3902
4005
  }
4006
+ this._flushStreamState();
3903
4007
  if (!ac.signal.aborted) this._vp()?.completeMessage?.(messageId);
3904
4008
  this._setStreamingId(null);
3905
4009
  } catch (err) {
4010
+ this._flushStreamState();
3906
4011
  this._setStreamingId(null);
3907
4012
  throw err;
3908
4013
  } finally {
@@ -3915,11 +4020,22 @@ class AparteChatHost {
3915
4020
  if (this._streamAbort === ac) this._streamAbort = void 0;
3916
4021
  }
3917
4022
  }
4023
+ /**
4024
+ * Buffer a streamed token for the coalesced state sync. A different message id
4025
+ * (a second stream started without stopping the first) flushes the previous
4026
+ * one, so text never migrates between messages.
4027
+ */
4028
+ _bufferToken(messageId, chunk) {
4029
+ if (this._tokenBuffer && this._tokenBuffer.messageId !== messageId) this._flushStreamState();
4030
+ this._tokenBuffer = this._tokenBuffer ? { messageId, text: this._tokenBuffer.text + chunk } : { messageId, text: chunk };
4031
+ this._scheduleStreamFlush();
4032
+ }
3918
4033
  /** Abort an in-flight {@link streamTokens} loop. */
3919
4034
  stopTokenStream() {
3920
4035
  if (this._streamAbort) {
3921
4036
  this._streamAbort.abort();
3922
4037
  this._streamAbort = void 0;
4038
+ this._flushStreamState();
3923
4039
  this._setStreamingId(null);
3924
4040
  }
3925
4041
  }
@@ -4864,8 +4980,9 @@ ${summary}`,
4864
4980
  const targetId = event.detail?.targetId;
4865
4981
  if (targetId) {
4866
4982
  const byId = document.getElementById(targetId);
4867
- if (byId && typeof byId.appendMessage === "function") {
4868
- targetElement = byId;
4983
+ const resolved = this._asRenderTarget(byId);
4984
+ if (resolved) {
4985
+ targetElement = resolved;
4869
4986
  } else {
4870
4987
  console.warn("[AparteClient] ⚠️ targetId present but element not found or missing appendMessage:", targetId);
4871
4988
  }
@@ -6093,6 +6210,10 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
6093
6210
  }
6094
6211
  }
6095
6212
  }
6213
+ function isAwaitingReply(message) {
6214
+ if (message.status === "streaming" || message.status === "pending") return true;
6215
+ return message.status === void 0 && message.role === "assistant" && !message.content?.trim() && !message.segments?.length;
6216
+ }
6096
6217
  function el(tag, className, text) {
6097
6218
  const node = document.createElement(tag);
6098
6219
  if (className) node.className = className;
@@ -6279,18 +6400,20 @@ export {
6279
6400
  BackendTransport as B,
6280
6401
  ConversationManager as C,
6281
6402
  DEFAULT_ICON_FALLBACKS as D,
6282
- isSafeUrl as E,
6283
- parseAparteEventStream as F,
6284
- parseMarkdownToSegments as G,
6285
- populateBubbleFromMessage as H,
6286
- readableToAsyncIterable as I,
6287
- registerDefaultRenderers as J,
6288
- registerSegmentRenderer as K,
6289
- requestUserInput as L,
6403
+ isAwaitingReply as E,
6404
+ isFormatAdapter as F,
6405
+ isSafeUrl as G,
6406
+ parseAparteEventStream as H,
6407
+ parseMarkdownToSegments as I,
6408
+ populateBubbleFromMessage as J,
6409
+ readableToAsyncIterable as K,
6410
+ registerDefaultRenderers as L,
6290
6411
  MessageRepository as M,
6291
- resolveConfig as N,
6292
- runWithConfig as O,
6293
- unregisterSegmentRenderer as P,
6412
+ registerSegmentRenderer as N,
6413
+ requestUserInput as O,
6414
+ resolveConfig as P,
6415
+ runWithConfig as Q,
6416
+ unregisterSegmentRenderer as R,
6294
6417
  APARTE_HOST_ATTR as a,
6295
6418
  AparteChatHost as b,
6296
6419
  AparteClient as c,
@@ -6315,7 +6438,7 @@ export {
6315
6438
  defaultSanitizer as v,
6316
6439
  deriveArtifactKind as w,
6317
6440
  detachConfig as x,
6318
- getSegmentRenderer as y,
6319
- isFormatAdapter as z
6441
+ filesToAttachments as y,
6442
+ getSegmentRenderer as z
6320
6443
  };
6321
- //# sourceMappingURL=index-CW2BCAqn.js.map
6444
+ //# sourceMappingURL=index-BBg6dsYx.js.map