@genesislcap/ai-assistant 14.458.1-GENC-0.3 → 14.458.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/ai-assistant",
3
3
  "description": "Genesis AI Assistant micro-frontend",
4
- "version": "14.458.1-GENC-0.3",
4
+ "version": "14.458.2",
5
5
  "license": "SEE LICENSE IN license.txt",
6
6
  "main": "dist/esm/index.js",
7
7
  "types": "dist/ai-assistant.d.ts",
@@ -64,24 +64,24 @@
64
64
  }
65
65
  },
66
66
  "devDependencies": {
67
- "@genesislcap/foundation-testing": "14.458.1-GENC-0.3",
68
- "@genesislcap/genx": "14.458.1-GENC-0.3",
69
- "@genesislcap/rollup-builder": "14.458.1-GENC-0.3",
70
- "@genesislcap/ts-builder": "14.458.1-GENC-0.3",
71
- "@genesislcap/uvu-playwright-builder": "14.458.1-GENC-0.3",
72
- "@genesislcap/vite-builder": "14.458.1-GENC-0.3",
73
- "@genesislcap/webpack-builder": "14.458.1-GENC-0.3",
67
+ "@genesislcap/foundation-testing": "14.458.2",
68
+ "@genesislcap/genx": "14.458.2",
69
+ "@genesislcap/rollup-builder": "14.458.2",
70
+ "@genesislcap/ts-builder": "14.458.2",
71
+ "@genesislcap/uvu-playwright-builder": "14.458.2",
72
+ "@genesislcap/vite-builder": "14.458.2",
73
+ "@genesislcap/webpack-builder": "14.458.2",
74
74
  "@types/dompurify": "^3.0.5",
75
75
  "@types/marked": "^5.0.2"
76
76
  },
77
77
  "dependencies": {
78
- "@genesislcap/foundation-ai": "14.458.1-GENC-0.3",
79
- "@genesislcap/foundation-logger": "14.458.1-GENC-0.3",
80
- "@genesislcap/foundation-redux": "14.458.1-GENC-0.3",
81
- "@genesislcap/foundation-ui": "14.458.1-GENC-0.3",
82
- "@genesislcap/foundation-utils": "14.458.1-GENC-0.3",
83
- "@genesislcap/rapid-design-system": "14.458.1-GENC-0.3",
84
- "@genesislcap/web-core": "14.458.1-GENC-0.3",
78
+ "@genesislcap/foundation-ai": "14.458.2",
79
+ "@genesislcap/foundation-logger": "14.458.2",
80
+ "@genesislcap/foundation-redux": "14.458.2",
81
+ "@genesislcap/foundation-ui": "14.458.2",
82
+ "@genesislcap/foundation-utils": "14.458.2",
83
+ "@genesislcap/rapid-design-system": "14.458.2",
84
+ "@genesislcap/web-core": "14.458.2",
85
85
  "dompurify": "^3.3.1",
86
86
  "marked": "^17.0.3"
87
87
  },
@@ -93,5 +93,5 @@
93
93
  "publishConfig": {
94
94
  "access": "public"
95
95
  },
96
- "gitHead": "8c0abb0de4dbbe5069d5456c3d8e7a52b5854133"
96
+ "gitHead": "d2445453c001d9ab1377d1c0a6d90e2b65adb448"
97
97
  }
@@ -101,22 +101,14 @@ export const styles = css`
101
101
  animation: settings-slide-out 0.2s ease-in forwards;
102
102
  }
103
103
 
104
- /* Collapsed control footprint stays compact (100px)... */
105
- rapid-categorized-multiselect::part(root) {
106
- min-width: 0;
107
- width: 100px;
104
+ rapid-multiselect::part(root) {
105
+ min-width: 80px;
106
+ width: 300%;
108
107
  }
109
108
 
110
- /* ...while the dropdown panel, being absolutely positioned, can be wider
111
- (200px) without affecting the root's layout footprint. The control sits on
112
- a different side of the settings panel depending on the container width
113
- (see the layout rules below), so the dropdown must anchor to whichever side
114
- keeps it inside the panel. Default (narrow) layout puts the control on the
115
- LEFT, so the dropdown grows rightward. */
116
- rapid-categorized-multiselect::part(options) {
117
- width: 200px;
118
- left: 0;
119
- right: auto;
109
+ rapid-multiselect::part(control),
110
+ .settings-panel > [part='download-button'] {
111
+ width: fit-content;
120
112
  }
121
113
 
122
114
  .settings-panel > [part='toggle-tool-calls'] {
@@ -135,7 +127,6 @@ export const styles = css`
135
127
  }
136
128
 
137
129
  .settings-panel > [part='download-button'] {
138
- width: fit-content;
139
130
  grid-column: 2;
140
131
  grid-row: 2;
141
132
  }
@@ -185,13 +176,6 @@ export const styles = css`
185
176
  .settings-panel > .settings-animations {
186
177
  grid-row: 2;
187
178
  }
188
-
189
- /* Control now sits on the RIGHT (justify-self: end / margin-left: auto in
190
- the wider layouts), so the dropdown grows leftward to stay in the panel. */
191
- rapid-categorized-multiselect::part(options) {
192
- left: auto;
193
- right: 0;
194
- }
195
179
  }
196
180
 
197
181
  @container (min-width: 750px) {
@@ -825,22 +809,6 @@ export const styles = css`
825
809
  }
826
810
  }
827
811
 
828
- .thinking-waves {
829
- display: flex;
830
- flex-direction: column;
831
- align-items: center;
832
- align-self: center;
833
- gap: 6px;
834
- padding: calc(var(--design-unit) * 2px) calc(var(--design-unit) * 3px);
835
- }
836
-
837
- .thinking-caption {
838
- font-size: var(--type-ramp-minus-1-font-size, 12px);
839
- line-height: var(--type-ramp-minus-1-line-height, 16px);
840
- color: var(--neutral-foreground-rest);
841
- opacity: 70%;
842
- }
843
-
844
812
  .attachment-chips {
845
813
  display: flex;
846
814
  flex-wrap: wrap;
@@ -40,6 +40,10 @@ function unknownToolPayload(tc: ChatToolCall): string {
40
40
  return lines.join('\n');
41
41
  }
42
42
 
43
+ const animationItemRenderer = (option: any): ViewTemplate => html`
44
+ <span part="option-label" title="${() => option.tooltip}">${() => option.label}</span>
45
+ `;
46
+
43
47
  const HALO_SPEED_DEFAULT = 1.5;
44
48
  const HALO_SPEED_ORCHESTRATING = 0.4;
45
49
  const HALO_BORDER_SIZE_DEFAULT = 3;
@@ -48,10 +52,9 @@ const HALO_BORDER_SIZE_DEFAULT = 3;
48
52
  const SESSION_COST_DECIMALS = 4;
49
53
 
50
54
  const animationOptions = Object.entries(ANIMATION_DEFS).map(([value, def]) => ({
51
- type: def.category,
52
55
  value,
53
56
  label: def.label,
54
- description: def.description,
57
+ tooltip: def.tooltip,
55
58
  }));
56
59
 
57
60
  // Avatar markup is owned by the component (`assistantIconSafe` / `userIconSafe`),
@@ -173,30 +176,6 @@ const liveSubAgentTraceTemplate = html<FoundationAiAssistant>`
173
176
  </div>
174
177
  `;
175
178
 
176
- // The two interchangeable loading indicators. These MUST be stable module-level
177
- // instances: the binding that selects between them reads `enabledAnimations`,
178
- // which is backed by the redux store proxy and re-evaluates on every change to
179
- // the aiAssistant slice (i.e. every new message, including hidden tool-call and
180
- // thinking-step messages). Returning a fresh `html` instance from that binding
181
- // would make FAST tear down and rebuild the indicator DOM each time, restarting
182
- // the dots' CSS animation and the waves' rAF loop. Stable references let FAST
183
- // reuse the existing view so the animation runs uninterrupted.
184
- const thinkingDotsTemplate = html<FoundationAiAssistant>`
185
- <div class="thinking-dots">
186
- <div class="dot dot-1"></div>
187
- <div class="dot dot-2"></div>
188
- <div class="dot dot-3"></div>
189
- <div class="dot dot-4"></div>
190
- </div>
191
- `;
192
-
193
- const thinkingWavesTemplate = html<FoundationAiAssistant>`
194
- <div class="thinking-waves" part="thinking-waves">
195
- <ai-waves-indicator></ai-waves-indicator>
196
- <span class="thinking-caption">Thinking...</span>
197
- </div>
198
- `;
199
-
200
179
  // ─── Public factory ───────────────────────────────────────────────────────────
201
180
 
202
181
  /** @internal */
@@ -205,7 +184,7 @@ export const FoundationAiAssistantTemplate = (
205
184
  ): ViewTemplate<FoundationAiAssistant> => {
206
185
  const buttonTag = `${designSystemPrefix}-button`;
207
186
  const switchTag = `${designSystemPrefix}-switch`;
208
- const categorizedMultiselectTag = `${designSystemPrefix}-categorized-multiselect`;
187
+ const multiselectTag = `${designSystemPrefix}-multiselect`;
209
188
  const textareaTag = `${designSystemPrefix}-text-area`;
210
189
  const iconTag = `${designSystemPrefix}-icon`;
211
190
  const progressTag = `${designSystemPrefix}-progress`;
@@ -494,14 +473,16 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
494
473
  html<FoundationAiAssistant>`
495
474
  <div class="settings-animations">
496
475
  <span class="settings-label">Animations</span>
497
- <${categorizedMultiselectTag}
476
+ <${multiselectTag}
498
477
  part="toggle-animations"
499
478
  :selectedOptions=${(x) => x.enabledAnimations}
500
479
  :options=${() => animationOptions}
480
+ :itemRenderer=${() => animationItemRenderer}
501
481
  @selectionChange=${(x, c) =>
502
482
  x.setEnabledAnimations((c.event as CustomEvent).detail)}
503
483
  search="false"
504
- ></${categorizedMultiselectTag}>
484
+ all="false"
485
+ ></${multiselectTag}>
505
486
  </div>
506
487
  `,
507
488
  )}
@@ -609,9 +590,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
609
590
  ${when(
610
591
  (x) =>
611
592
  x.showLoadingIndicator &&
612
- (x.chatConfig.ui?.animations == null ||
613
- x.enabledAnimations.includes('loading') ||
614
- x.enabledAnimations.includes('waves')),
593
+ (x.chatConfig.ui?.animations == null || x.enabledAnimations.includes('loading')),
615
594
  html<FoundationAiAssistant>`
616
595
  <div class="message-row ai" part="thinking">
617
596
  <div class="avatar">
@@ -624,10 +603,12 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
624
603
  <span class="avatar-icon" :innerHTML="${() => x.assistantIconSafe}"></span>
625
604
  `}
626
605
  </div>
627
- ${(x) =>
628
- x.enabledAnimations.includes('waves')
629
- ? thinkingWavesTemplate
630
- : thinkingDotsTemplate}
606
+ <div class="thinking-dots">
607
+ <div class="dot dot-1"></div>
608
+ <div class="dot dot-2"></div>
609
+ <div class="dot dot-3"></div>
610
+ <div class="dot dot-4"></div>
611
+ </div>
631
612
  </div>
632
613
  `,
633
614
  )}
package/src/main/main.ts CHANGED
@@ -49,7 +49,6 @@ import { AiChatInteractionWrapper } from '../components/chat-interaction-wrapper
49
49
  import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
50
50
  import { AiHaloOverlay } from '../components/halo-overlay';
51
51
  import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
52
- import { AiWavesIndicator } from '../components/waves-indicator';
53
52
  import type { AgentConfig } from '../config/config';
54
53
  import {
55
54
  recordMetaEvent,
@@ -67,7 +66,6 @@ import {
67
66
  } from '../styles/ai-colours';
68
67
  import { ChatSuggestions } from '../suggestions/chat-suggestions';
69
68
  import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
70
- import { resolveExclusiveLoadingStyle } from '../utils/animation-exclusivity';
71
69
  import { logger } from '../utils/logger';
72
70
  import { filterVisibleMessages, trailingInteractionRow } from '../utils/message-partition';
73
71
  import { sumCosts } from '../utils/sum-costs';
@@ -82,7 +80,7 @@ import type {
82
80
  SubmitMessageResult,
83
81
  SuggestionsState,
84
82
  } from './main.types';
85
- import { DEFAULT_ANIMATIONS } from './main.types';
83
+ import { ALL_ANIMATIONS } from './main.types';
86
84
 
87
85
  /** Context window sizes (in tokens) for known models. */
88
86
  /**
@@ -139,7 +137,6 @@ avoidTreeShaking(
139
137
  AiChatMarkdown,
140
138
  AiChatInteractionWrapper,
141
139
  AiHaloOverlay,
142
- AiWavesIndicator,
143
140
  AiChatBubble,
144
141
  AiActivityHalo,
145
142
  ChatSuggestions,
@@ -659,9 +656,7 @@ export class FoundationAiAssistant extends GenesisElement {
659
656
  return;
660
657
  }
661
658
  const last = this.messages[this.messages.length - 1];
662
- // Hide only while a pending interaction blocks on the user; a resolved
663
- // interaction means the assistant is computing again (keep the halo).
664
- if (last?.interaction && !last.interaction.resolved) {
659
+ if (last?.interaction) {
665
660
  this.showHalo = 'no';
666
661
  } else if (this.showHalo !== 'orchestrating') {
667
662
  this.showHalo = 'agent';
@@ -1052,10 +1047,9 @@ export class FoundationAiAssistant extends GenesisElement {
1052
1047
  this.showToolCalls = ui.showToolCalls === true;
1053
1048
  this.showThinkingSteps = ui.showThinkingSteps === true;
1054
1049
  this.showAgentSwitchIndicator = ui.showAgentSwitchIndicator === true;
1055
- this.enabledAnimations = resolveExclusiveLoadingStyle(
1050
+ this.enabledAnimations =
1056
1051
  (ui.animations?.enabled as AiAssistantAnimation[]) ??
1057
- (ui.animations ? [...DEFAULT_ANIMATIONS] : []),
1058
- );
1052
+ (ui.animations ? [...ALL_ANIMATIONS] : []);
1059
1053
 
1060
1054
  const defaultAgent = this.chatConfig.picker?.defaultAgent;
1061
1055
  if (defaultAgent && (this.agents ?? []).some((a) => a.name === defaultAgent)) {
@@ -1222,13 +1216,9 @@ export class FoundationAiAssistant extends GenesisElement {
1222
1216
  // waiting for the user, not computing.
1223
1217
  if (this.busy) {
1224
1218
  const last = this.messages[this.messages.length - 1];
1225
- // Only a *pending* interaction means the assistant is blocked waiting on
1226
- // the user. Once it's resolved — or for any normal step — the assistant is
1227
- // computing again, so the indicator should resume (e.g. while it works out
1228
- // the next planning question after the user answers a widget).
1229
- if (last?.interaction && !last.interaction.resolved) {
1219
+ if (last?.interaction) {
1230
1220
  this.stopLoadingTimer();
1231
- } else {
1221
+ } else if (last?.role === 'assistant') {
1232
1222
  this.startLoadingTimer();
1233
1223
  }
1234
1224
  }
@@ -1312,7 +1302,7 @@ export class FoundationAiAssistant extends GenesisElement {
1312
1302
  });
1313
1303
  }
1314
1304
 
1315
- private static readonly DEFAULT_LOADING_DELAY_S = 0;
1305
+ private static readonly DEFAULT_LOADING_DELAY_S = 5;
1316
1306
  private static readonly DEFAULT_SUGGESTION_COUNT = 3;
1317
1307
  private static readonly MS_PER_SECOND = 1000;
1318
1308
 
@@ -1516,9 +1506,7 @@ export class FoundationAiAssistant extends GenesisElement {
1516
1506
  }
1517
1507
 
1518
1508
  setEnabledAnimations(animations: AiAssistantAnimation[]) {
1519
- // The dots and waves loading styles are mutually exclusive — enabling one
1520
- // disables the other (see resolveExclusiveLoadingStyle).
1521
- this.enabledAnimations = resolveExclusiveLoadingStyle(animations, this.enabledAnimations);
1509
+ this.enabledAnimations = animations;
1522
1510
  }
1523
1511
 
1524
1512
  getDebugLog() {
@@ -53,41 +53,24 @@ export type SuggestionsState =
53
53
  export interface AiAssistantAnimationDef {
54
54
  /** Display label shown in the settings multiselect. */
55
55
  label: string;
56
- /** Short description shown beneath the label in the categorized multiselect. */
57
- description: string;
58
- /** Group heading the option is listed under in the settings multiselect. */
59
- category: string;
56
+ /** Short description shown as a tooltip on the option. */
57
+ tooltip: string;
60
58
  }
61
59
 
62
60
  /**
63
61
  * Registry of all available animations with their display metadata.
64
62
  * Adding an entry here automatically extends the {@link AiAssistantAnimation} type.
65
63
  *
66
- * @remarks
67
- * `loading` (dots) and `waves` are two interchangeable styles of the same
68
- * "is the assistant working" indicator and are therefore grouped under the same
69
- * {@link AiAssistantAnimationDef.category}. They are mutually exclusive — see
70
- * `LOADING_STYLE_ANIMATIONS`.
71
- *
72
64
  * @beta
73
65
  */
74
66
  export const ANIMATION_DEFS = {
75
67
  loading: {
76
- label: 'Dots',
77
- description: 'Shows pulsing dots while the assistant is generating a response.',
78
- category: 'Loading style',
79
- },
80
- waves: {
81
- label: 'Waves',
82
- description:
83
- 'Shows glowing sine waves inside a circle while the assistant is generating a response.',
84
- category: 'Loading style',
68
+ label: 'Loading indicator',
69
+ tooltip: 'Shows a pulsing animation while the assistant is generating a response.',
85
70
  },
86
71
  halo: {
87
72
  label: 'Halo',
88
- description:
89
- 'Displays a glowing halo around the assistant avatar while a response is streaming.',
90
- category: 'Effects',
73
+ tooltip: 'Displays a glowing halo around the assistant avatar while a response is streaming.',
91
74
  },
92
75
  } satisfies Record<string, AiAssistantAnimationDef>;
93
76
 
@@ -104,23 +87,3 @@ export type AiAssistantAnimation = keyof typeof ANIMATION_DEFS;
104
87
  * @internal
105
88
  */
106
89
  export const ALL_ANIMATIONS = Object.keys(ANIMATION_DEFS) as AiAssistantAnimation[];
107
-
108
- /**
109
- * The interchangeable "assistant is working" loading-indicator styles. At most
110
- * one of these may be enabled at a time — enabling one disables the other.
111
- *
112
- * @internal
113
- */
114
- export const LOADING_STYLE_ANIMATIONS = [
115
- 'loading',
116
- 'waves',
117
- ] as const satisfies readonly AiAssistantAnimation[];
118
-
119
- /**
120
- * Animations enabled by default when a consumer opts into the animations
121
- * feature without specifying an explicit `enabled` list. Keeps the dots loading
122
- * style (the long-standing default); the waves style is opt-in.
123
- *
124
- * @internal
125
- */
126
- export const DEFAULT_ANIMATIONS: AiAssistantAnimation[] = ['loading', 'halo'];
@@ -1,30 +0,0 @@
1
- import { GenesisElement } from '@genesislcap/web-core';
2
- /**
3
- * Animated "waves inside a circle" loading indicator — coloured sine waves that
4
- * slosh like glowing liquid inside a circular window, ringed by a rotating
5
- * gradient halo (the same effect as `<ai-halo-overlay>`).
6
- *
7
- * Visual sibling of the dots loading indicator; the two are interchangeable
8
- * styles of the same "assistant is working" state.
9
- *
10
- * @example
11
- * ```html
12
- * <ai-waves-indicator size="56"></ai-waves-indicator>
13
- * ```
14
- *
15
- * @beta
16
- */
17
- export declare class AiWavesIndicator extends GenesisElement {
18
- /** Diameter of the circular window in px. Default: 56. */
19
- size: number;
20
- sizeChanged(): void;
21
- private frame;
22
- private animFrame?;
23
- private wavePaths?;
24
- connectedCallback(): void;
25
- disconnectedCallback(): void;
26
- private tick;
27
- /** Trace one wave's polyline `d` attribute for the given frame. */
28
- private static buildWavePath;
29
- }
30
- //# sourceMappingURL=waves-indicator.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"waves-indicator.d.ts","sourceRoot":"","sources":["../../../src/components/waves-indicator.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,cAAc,EAAQ,MAAM,uBAAuB,CAAC;AAwFvF;;;;;;;;;;;;;;GAcG;AACH,qBAkDa,gBAAiB,SAAQ,cAAc;IAClD,0DAA0D;IACC,IAAI,EAAE,MAAM,CAAsB;IAE7F,WAAW;IAQX,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,SAAS,CAAC,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAC,CAAmB;IAErC,iBAAiB;IAKjB,oBAAoB;IAQpB,OAAO,CAAC,IAAI;IAYZ,mEAAmE;IACnE,OAAO,CAAC,MAAM,CAAC,aAAa;CAY7B"}
@@ -1,23 +0,0 @@
1
- import type { AiAssistantAnimation } from '../main/main.types';
2
- /**
3
- * Enforces that at most one loading-indicator style (dots vs. waves) is enabled
4
- * at a time. The two are alternative presentations of the same "assistant is
5
- * working" state, so selecting one must deselect the other.
6
- *
7
- * The settings control is a multiselect (checkboxes), which permits selecting
8
- * both; this resolver makes the loading-style group behave like a radio group
9
- * by dropping the previously-selected style whenever a new one is added.
10
- *
11
- * @param next - The freshly-selected animation list (e.g. emitted by the
12
- * multiselect, or read from consumer config).
13
- * @param previous - The animation list in effect before this change. Used to
14
- * work out which loading style was just added so the other can be dropped.
15
- * Pass `[]` when resolving an initial/config value with no prior state.
16
- * @returns `next` with at most one loading style retained. When both are
17
- * present and neither is newly added (e.g. a misconfigured `enabled` list),
18
- * the first entry of `LOADING_STYLE_ANIMATIONS` (dots) wins.
19
- *
20
- * @internal
21
- */
22
- export declare function resolveExclusiveLoadingStyle(next: AiAssistantAnimation[], previous?: AiAssistantAnimation[]): AiAssistantAnimation[];
23
- //# sourceMappingURL=animation-exclusivity.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"animation-exclusivity.d.ts","sourceRoot":"","sources":["../../../src/utils/animation-exclusivity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAG/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,oBAAoB,EAAE,EAC5B,QAAQ,GAAE,oBAAoB,EAAO,GACpC,oBAAoB,EAAE,CAaxB"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=animation-exclusivity.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"animation-exclusivity.test.d.ts","sourceRoot":"","sources":["../../../src/utils/animation-exclusivity.test.ts"],"names":[],"mappings":""}
@@ -1,180 +0,0 @@
1
- var AiWavesIndicator_1;
2
- import { __decorate } from "tslib";
3
- import { avoidTreeShaking } from '@genesislcap/foundation-utils';
4
- import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core';
5
- import { AI_COLOUR_AMBER, AI_COLOUR_CYAN, AI_COLOUR_PINK, AI_COLOUR_VIOLET, } from '../styles/ai-colours';
6
- import { AiHaloOverlay } from './halo-overlay';
7
- const WAVES_DEFAULT_SIZE = 56;
8
- /** CSS-ready form of `WAVES_DEFAULT_SIZE` (the `css` tag rejects raw numbers). */
9
- const WAVES_DEFAULT_SIZE_CSS = `${WAVES_DEFAULT_SIZE}px`;
10
- /** SVG coordinate space the waves are drawn in (square; the circle fills it). */
11
- const VIEWBOX = 120;
12
- const CENTRE = VIEWBOX / 2;
13
- /** Horizontal sampling step when tracing each wave path. Lower = smoother. */
14
- const SAMPLE_STEP = 6;
15
- const WAVES = [
16
- {
17
- colour: AI_COLOUR_AMBER,
18
- amplitude: 11,
19
- frequency: 0.085,
20
- phaseSpeed: 0.05,
21
- verticalOffset: -6,
22
- slosh: 6,
23
- sloshFrequency: 0.018,
24
- sloshSpeed: 0.021,
25
- },
26
- {
27
- colour: AI_COLOUR_PINK,
28
- amplitude: 14,
29
- frequency: 0.07,
30
- phaseSpeed: -0.043,
31
- verticalOffset: -2,
32
- slosh: 7,
33
- sloshFrequency: 0.022,
34
- sloshSpeed: -0.017,
35
- },
36
- {
37
- colour: AI_COLOUR_CYAN,
38
- amplitude: 13,
39
- frequency: 0.095,
40
- phaseSpeed: 0.037,
41
- verticalOffset: 2,
42
- slosh: 5,
43
- sloshFrequency: 0.015,
44
- sloshSpeed: 0.025,
45
- },
46
- {
47
- colour: AI_COLOUR_VIOLET,
48
- amplitude: 10,
49
- frequency: 0.06,
50
- phaseSpeed: -0.055,
51
- verticalOffset: 6,
52
- slosh: 8,
53
- sloshFrequency: 0.025,
54
- sloshSpeed: -0.013,
55
- },
56
- ];
57
- const wavePathsMarkup = WAVES.map((w, i) => `<path class="wave" data-wave="${i}" stroke="${w.colour}" />`).join('');
58
- /**
59
- * Animated "waves inside a circle" loading indicator — coloured sine waves that
60
- * slosh like glowing liquid inside a circular window, ringed by a rotating
61
- * gradient halo (the same effect as `<ai-halo-overlay>`).
62
- *
63
- * Visual sibling of the dots loading indicator; the two are interchangeable
64
- * styles of the same "assistant is working" state.
65
- *
66
- * @example
67
- * ```html
68
- * <ai-waves-indicator size="56"></ai-waves-indicator>
69
- * ```
70
- *
71
- * @beta
72
- */
73
- let AiWavesIndicator = AiWavesIndicator_1 = class AiWavesIndicator extends GenesisElement {
74
- constructor() {
75
- super(...arguments);
76
- /** Diameter of the circular window in px. Default: 56. */
77
- this.size = WAVES_DEFAULT_SIZE;
78
- // A rAF loop drives the wave paths for the same reason `<ai-halo-overlay>`
79
- // hand-drives its rotation: a pure-CSS approach can't produce per-frame sine
80
- // geometry, and SMIL/`<animate>` can't express the combined travel + slosh.
81
- this.frame = 0;
82
- }
83
- sizeChanged() {
84
- this.style.setProperty('--waves-size', `${this.size}px`);
85
- }
86
- connectedCallback() {
87
- super.connectedCallback();
88
- this.tick();
89
- }
90
- disconnectedCallback() {
91
- super.disconnectedCallback();
92
- if (this.animFrame !== undefined) {
93
- cancelAnimationFrame(this.animFrame);
94
- this.animFrame = undefined;
95
- }
96
- }
97
- tick() {
98
- var _a, _b;
99
- if (!this.wavePaths) {
100
- const paths = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('.wave');
101
- if (paths === null || paths === void 0 ? void 0 : paths.length)
102
- this.wavePaths = Array.from(paths);
103
- }
104
- (_b = this.wavePaths) === null || _b === void 0 ? void 0 : _b.forEach((path, i) => {
105
- path.setAttribute('d', AiWavesIndicator_1.buildWavePath(WAVES[i], this.frame));
106
- });
107
- this.frame += 1;
108
- this.animFrame = requestAnimationFrame(() => this.tick());
109
- }
110
- /** Trace one wave's polyline `d` attribute for the given frame. */
111
- static buildWavePath(cfg, frame) {
112
- const segments = [];
113
- for (let x = 0; x <= VIEWBOX; x += SAMPLE_STEP) {
114
- const y = CENTRE +
115
- cfg.verticalOffset +
116
- cfg.amplitude * Math.sin(x * cfg.frequency + frame * cfg.phaseSpeed) +
117
- cfg.slosh * Math.sin(x * cfg.sloshFrequency + frame * cfg.sloshSpeed);
118
- segments.push(`${x},${y.toFixed(2)}`);
119
- }
120
- return `M ${segments.join(' L ')}`;
121
- }
122
- };
123
- __decorate([
124
- attr({ converter: { fromView: Number, toView: String } })
125
- ], AiWavesIndicator.prototype, "size", void 0);
126
- AiWavesIndicator = AiWavesIndicator_1 = __decorate([
127
- customElement({
128
- name: 'ai-waves-indicator',
129
- template: html `
130
- <div class="window" role="img" aria-label="Assistant is working">
131
- <svg class="waves" viewBox="0 0 ${VIEWBOX} ${VIEWBOX}" preserveAspectRatio="none">
132
- <defs>
133
- <filter id="wave-glow" x="-20%" y="-20%" width="140%" height="140%">
134
- <feGaussianBlur stdDeviation="1.6" result="blur" />
135
- <feMerge>
136
- <feMergeNode in="blur" />
137
- <feMergeNode in="SourceGraphic" />
138
- </feMerge>
139
- </filter>
140
- </defs>
141
- <g filter="url(#wave-glow)">${wavePathsMarkup}</g>
142
- </svg>
143
- <ai-halo-overlay active border-size="2" glow-opacity="0.5" glow-spread="55"></ai-halo-overlay>
144
- </div>
145
- `,
146
- styles: css `
147
- :host {
148
- display: inline-block;
149
- width: var(--waves-size, ${WAVES_DEFAULT_SIZE_CSS});
150
- height: var(--waves-size, ${WAVES_DEFAULT_SIZE_CSS});
151
- }
152
-
153
- .window {
154
- position: relative;
155
- width: 100%;
156
- height: 100%;
157
- border-radius: 50%;
158
- overflow: hidden;
159
- background: radial-gradient(circle at 50% 32%, #2b3140 0%, #11141c 55%, #05070c 100%);
160
- }
161
-
162
- .waves {
163
- position: absolute;
164
- inset: 0;
165
- width: 100%;
166
- height: 100%;
167
- }
168
-
169
- .wave {
170
- fill: none;
171
- stroke-width: 2;
172
- stroke-linecap: round;
173
- stroke-linejoin: round;
174
- }
175
- `,
176
- })
177
- ], AiWavesIndicator);
178
- export { AiWavesIndicator };
179
- // Ensure the halo overlay used for the ring is registered alongside this component.
180
- avoidTreeShaking(AiHaloOverlay);