@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.
@@ -1,33 +0,0 @@
1
- import { LOADING_STYLE_ANIMATIONS } 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 function resolveExclusiveLoadingStyle(next, previous = []) {
23
- var _a;
24
- const selectedStyles = LOADING_STYLE_ANIMATIONS.filter((style) => next.includes(style));
25
- if (selectedStyles.length <= 1) {
26
- return next;
27
- }
28
- // Both styles selected — keep whichever was just added (absent from
29
- // `previous`); fall back to the first declared style on ambiguity.
30
- const justAdded = (_a = selectedStyles.find((style) => !previous.includes(style))) !== null && _a !== void 0 ? _a : selectedStyles[0];
31
- return next.filter((animation) => animation === justAdded ||
32
- !LOADING_STYLE_ANIMATIONS.includes(animation));
33
- }
@@ -1,41 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import { resolveExclusiveLoadingStyle } from './animation-exclusivity';
3
- const suite = createLogicSuite('resolveExclusiveLoadingStyle');
4
- suite('leaves a selection with only the dots loading style untouched', () => {
5
- const next = ['loading', 'halo'];
6
- assert.equal(resolveExclusiveLoadingStyle(next, ['halo']), ['loading', 'halo']);
7
- });
8
- suite('leaves a selection with only the waves loading style untouched', () => {
9
- const next = ['waves', 'halo'];
10
- assert.equal(resolveExclusiveLoadingStyle(next, ['halo']), ['waves', 'halo']);
11
- });
12
- suite('leaves a selection with no loading style untouched', () => {
13
- assert.equal(resolveExclusiveLoadingStyle(['halo'], []), ['halo']);
14
- });
15
- suite('drops dots when waves was just added on top of dots', () => {
16
- // Previously dots was on; user ticks waves → waves wins, dots removed.
17
- const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], ['loading', 'halo']);
18
- assert.equal(result, ['waves', 'halo']);
19
- });
20
- suite('drops waves when dots was just added on top of waves', () => {
21
- // Previously waves was on; user ticks dots → dots wins, waves removed.
22
- const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], ['waves', 'halo']);
23
- assert.equal(result, ['loading', 'halo']);
24
- });
25
- suite('preserves the order of the surviving selection', () => {
26
- const result = resolveExclusiveLoadingStyle(['halo', 'loading', 'waves'], ['halo', 'loading']);
27
- // waves was just added → loading dropped; halo and waves keep their order.
28
- assert.equal(result, ['halo', 'waves']);
29
- });
30
- suite('falls back to dots when both are present with no prior state (e.g. bad config)', () => {
31
- const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], []);
32
- assert.equal(result, ['loading', 'halo']);
33
- });
34
- suite('falls back to dots when both were already present (neither newly added)', () => {
35
- const result = resolveExclusiveLoadingStyle(['loading', 'waves'], ['loading', 'waves', 'halo']);
36
- assert.equal(result, ['loading']);
37
- });
38
- suite('handles an empty selection', () => {
39
- assert.equal(resolveExclusiveLoadingStyle([], ['loading']), []);
40
- });
41
- suite.run();
@@ -1,212 +0,0 @@
1
- import { avoidTreeShaking } from '@genesislcap/foundation-utils';
2
- import { attr, css, customElement, GenesisElement, html } from '@genesislcap/web-core';
3
- import {
4
- AI_COLOUR_AMBER,
5
- AI_COLOUR_CYAN,
6
- AI_COLOUR_PINK,
7
- AI_COLOUR_VIOLET,
8
- } from '../styles/ai-colours';
9
- import { AiHaloOverlay } from './halo-overlay';
10
-
11
- const WAVES_DEFAULT_SIZE = 56;
12
- /** CSS-ready form of `WAVES_DEFAULT_SIZE` (the `css` tag rejects raw numbers). */
13
- const WAVES_DEFAULT_SIZE_CSS = `${WAVES_DEFAULT_SIZE}px`;
14
-
15
- /** SVG coordinate space the waves are drawn in (square; the circle fills it). */
16
- const VIEWBOX = 120;
17
- const CENTRE = VIEWBOX / 2;
18
- /** Horizontal sampling step when tracing each wave path. Lower = smoother. */
19
- const SAMPLE_STEP = 6;
20
-
21
- /**
22
- * Per-wave parameters. Each wave is a travelling sine (amplitude/frequency/phase)
23
- * plus a slower, longer-wavelength term that makes the line "slosh" like liquid.
24
- */
25
- interface WaveConfig {
26
- colour: string;
27
- /** Peak height of the travelling wave, in viewBox units. */
28
- amplitude: number;
29
- /** Angular frequency of the travelling wave (radians per viewBox unit). */
30
- frequency: number;
31
- /** How fast the travelling wave scrolls (radians per frame). */
32
- phaseSpeed: number;
33
- /** Vertical centre offset so the waves stack rather than overlap exactly. */
34
- verticalOffset: number;
35
- /** Amplitude of the slow sloshing term. */
36
- slosh: number;
37
- /** Angular frequency of the sloshing term (radians per viewBox unit). */
38
- sloshFrequency: number;
39
- /** How fast the sloshing term evolves (radians per frame). */
40
- sloshSpeed: number;
41
- }
42
-
43
- const WAVES: WaveConfig[] = [
44
- {
45
- colour: AI_COLOUR_AMBER,
46
- amplitude: 11,
47
- frequency: 0.085,
48
- phaseSpeed: 0.05,
49
- verticalOffset: -6,
50
- slosh: 6,
51
- sloshFrequency: 0.018,
52
- sloshSpeed: 0.021,
53
- },
54
- {
55
- colour: AI_COLOUR_PINK,
56
- amplitude: 14,
57
- frequency: 0.07,
58
- phaseSpeed: -0.043,
59
- verticalOffset: -2,
60
- slosh: 7,
61
- sloshFrequency: 0.022,
62
- sloshSpeed: -0.017,
63
- },
64
- {
65
- colour: AI_COLOUR_CYAN,
66
- amplitude: 13,
67
- frequency: 0.095,
68
- phaseSpeed: 0.037,
69
- verticalOffset: 2,
70
- slosh: 5,
71
- sloshFrequency: 0.015,
72
- sloshSpeed: 0.025,
73
- },
74
- {
75
- colour: AI_COLOUR_VIOLET,
76
- amplitude: 10,
77
- frequency: 0.06,
78
- phaseSpeed: -0.055,
79
- verticalOffset: 6,
80
- slosh: 8,
81
- sloshFrequency: 0.025,
82
- sloshSpeed: -0.013,
83
- },
84
- ];
85
-
86
- const wavePathsMarkup = WAVES.map(
87
- (w, i) => `<path class="wave" data-wave="${i}" stroke="${w.colour}" />`,
88
- ).join('');
89
-
90
- /**
91
- * Animated "waves inside a circle" loading indicator — coloured sine waves that
92
- * slosh like glowing liquid inside a circular window, ringed by a rotating
93
- * gradient halo (the same effect as `<ai-halo-overlay>`).
94
- *
95
- * Visual sibling of the dots loading indicator; the two are interchangeable
96
- * styles of the same "assistant is working" state.
97
- *
98
- * @example
99
- * ```html
100
- * <ai-waves-indicator size="56"></ai-waves-indicator>
101
- * ```
102
- *
103
- * @beta
104
- */
105
- @customElement({
106
- name: 'ai-waves-indicator',
107
- template: html<AiWavesIndicator>`
108
- <div class="window" role="img" aria-label="Assistant is working">
109
- <svg class="waves" viewBox="0 0 ${VIEWBOX} ${VIEWBOX}" preserveAspectRatio="none">
110
- <defs>
111
- <filter id="wave-glow" x="-20%" y="-20%" width="140%" height="140%">
112
- <feGaussianBlur stdDeviation="1.6" result="blur" />
113
- <feMerge>
114
- <feMergeNode in="blur" />
115
- <feMergeNode in="SourceGraphic" />
116
- </feMerge>
117
- </filter>
118
- </defs>
119
- <g filter="url(#wave-glow)">${wavePathsMarkup}</g>
120
- </svg>
121
- <ai-halo-overlay active border-size="2" glow-opacity="0.5" glow-spread="55"></ai-halo-overlay>
122
- </div>
123
- `,
124
- styles: css`
125
- :host {
126
- display: inline-block;
127
- width: var(--waves-size, ${WAVES_DEFAULT_SIZE_CSS});
128
- height: var(--waves-size, ${WAVES_DEFAULT_SIZE_CSS});
129
- }
130
-
131
- .window {
132
- position: relative;
133
- width: 100%;
134
- height: 100%;
135
- border-radius: 50%;
136
- overflow: hidden;
137
- background: radial-gradient(circle at 50% 32%, #2b3140 0%, #11141c 55%, #05070c 100%);
138
- }
139
-
140
- .waves {
141
- position: absolute;
142
- inset: 0;
143
- width: 100%;
144
- height: 100%;
145
- }
146
-
147
- .wave {
148
- fill: none;
149
- stroke-width: 2;
150
- stroke-linecap: round;
151
- stroke-linejoin: round;
152
- }
153
- `,
154
- })
155
- export class AiWavesIndicator extends GenesisElement {
156
- /** Diameter of the circular window in px. Default: 56. */
157
- @attr({ converter: { fromView: Number, toView: String } }) size: number = WAVES_DEFAULT_SIZE;
158
-
159
- sizeChanged() {
160
- this.style.setProperty('--waves-size', `${this.size}px`);
161
- }
162
-
163
- // A rAF loop drives the wave paths for the same reason `<ai-halo-overlay>`
164
- // hand-drives its rotation: a pure-CSS approach can't produce per-frame sine
165
- // geometry, and SMIL/`<animate>` can't express the combined travel + slosh.
166
-
167
- private frame = 0;
168
- private animFrame?: number;
169
- private wavePaths?: SVGPathElement[];
170
-
171
- connectedCallback() {
172
- super.connectedCallback();
173
- this.tick();
174
- }
175
-
176
- disconnectedCallback() {
177
- super.disconnectedCallback();
178
- if (this.animFrame !== undefined) {
179
- cancelAnimationFrame(this.animFrame);
180
- this.animFrame = undefined;
181
- }
182
- }
183
-
184
- private tick() {
185
- if (!this.wavePaths) {
186
- const paths = this.shadowRoot?.querySelectorAll<SVGPathElement>('.wave');
187
- if (paths?.length) this.wavePaths = Array.from(paths);
188
- }
189
- this.wavePaths?.forEach((path, i) => {
190
- path.setAttribute('d', AiWavesIndicator.buildWavePath(WAVES[i], this.frame));
191
- });
192
- this.frame += 1;
193
- this.animFrame = requestAnimationFrame(() => this.tick());
194
- }
195
-
196
- /** Trace one wave's polyline `d` attribute for the given frame. */
197
- private static buildWavePath(cfg: WaveConfig, frame: number): string {
198
- const segments: string[] = [];
199
- for (let x = 0; x <= VIEWBOX; x += SAMPLE_STEP) {
200
- const y =
201
- CENTRE +
202
- cfg.verticalOffset +
203
- cfg.amplitude * Math.sin(x * cfg.frequency + frame * cfg.phaseSpeed) +
204
- cfg.slosh * Math.sin(x * cfg.sloshFrequency + frame * cfg.sloshSpeed);
205
- segments.push(`${x},${y.toFixed(2)}`);
206
- }
207
- return `M ${segments.join(' L ')}`;
208
- }
209
- }
210
-
211
- // Ensure the halo overlay used for the ring is registered alongside this component.
212
- avoidTreeShaking(AiHaloOverlay);
@@ -1,53 +0,0 @@
1
- import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
- import type { AiAssistantAnimation } from '../main/main.types';
3
- import { resolveExclusiveLoadingStyle } from './animation-exclusivity';
4
-
5
- const suite = createLogicSuite('resolveExclusiveLoadingStyle');
6
-
7
- suite('leaves a selection with only the dots loading style untouched', () => {
8
- const next: AiAssistantAnimation[] = ['loading', 'halo'];
9
- assert.equal(resolveExclusiveLoadingStyle(next, ['halo']), ['loading', 'halo']);
10
- });
11
-
12
- suite('leaves a selection with only the waves loading style untouched', () => {
13
- const next: AiAssistantAnimation[] = ['waves', 'halo'];
14
- assert.equal(resolveExclusiveLoadingStyle(next, ['halo']), ['waves', 'halo']);
15
- });
16
-
17
- suite('leaves a selection with no loading style untouched', () => {
18
- assert.equal(resolveExclusiveLoadingStyle(['halo'], []), ['halo']);
19
- });
20
-
21
- suite('drops dots when waves was just added on top of dots', () => {
22
- // Previously dots was on; user ticks waves → waves wins, dots removed.
23
- const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], ['loading', 'halo']);
24
- assert.equal(result, ['waves', 'halo']);
25
- });
26
-
27
- suite('drops waves when dots was just added on top of waves', () => {
28
- // Previously waves was on; user ticks dots → dots wins, waves removed.
29
- const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], ['waves', 'halo']);
30
- assert.equal(result, ['loading', 'halo']);
31
- });
32
-
33
- suite('preserves the order of the surviving selection', () => {
34
- const result = resolveExclusiveLoadingStyle(['halo', 'loading', 'waves'], ['halo', 'loading']);
35
- // waves was just added → loading dropped; halo and waves keep their order.
36
- assert.equal(result, ['halo', 'waves']);
37
- });
38
-
39
- suite('falls back to dots when both are present with no prior state (e.g. bad config)', () => {
40
- const result = resolveExclusiveLoadingStyle(['loading', 'waves', 'halo'], []);
41
- assert.equal(result, ['loading', 'halo']);
42
- });
43
-
44
- suite('falls back to dots when both were already present (neither newly added)', () => {
45
- const result = resolveExclusiveLoadingStyle(['loading', 'waves'], ['loading', 'waves', 'halo']);
46
- assert.equal(result, ['loading']);
47
- });
48
-
49
- suite('handles an empty selection', () => {
50
- assert.equal(resolveExclusiveLoadingStyle([], ['loading']), []);
51
- });
52
-
53
- suite.run();
@@ -1,40 +0,0 @@
1
- import type { AiAssistantAnimation } from '../main/main.types';
2
- import { LOADING_STYLE_ANIMATIONS } from '../main/main.types';
3
-
4
- /**
5
- * Enforces that at most one loading-indicator style (dots vs. waves) is enabled
6
- * at a time. The two are alternative presentations of the same "assistant is
7
- * working" state, so selecting one must deselect the other.
8
- *
9
- * The settings control is a multiselect (checkboxes), which permits selecting
10
- * both; this resolver makes the loading-style group behave like a radio group
11
- * by dropping the previously-selected style whenever a new one is added.
12
- *
13
- * @param next - The freshly-selected animation list (e.g. emitted by the
14
- * multiselect, or read from consumer config).
15
- * @param previous - The animation list in effect before this change. Used to
16
- * work out which loading style was just added so the other can be dropped.
17
- * Pass `[]` when resolving an initial/config value with no prior state.
18
- * @returns `next` with at most one loading style retained. When both are
19
- * present and neither is newly added (e.g. a misconfigured `enabled` list),
20
- * the first entry of `LOADING_STYLE_ANIMATIONS` (dots) wins.
21
- *
22
- * @internal
23
- */
24
- export function resolveExclusiveLoadingStyle(
25
- next: AiAssistantAnimation[],
26
- previous: AiAssistantAnimation[] = [],
27
- ): AiAssistantAnimation[] {
28
- const selectedStyles = LOADING_STYLE_ANIMATIONS.filter((style) => next.includes(style));
29
- if (selectedStyles.length <= 1) {
30
- return next;
31
- }
32
- // Both styles selected — keep whichever was just added (absent from
33
- // `previous`); fall back to the first declared style on ambiguity.
34
- const justAdded = selectedStyles.find((style) => !previous.includes(style)) ?? selectedStyles[0];
35
- return next.filter(
36
- (animation) =>
37
- animation === justAdded ||
38
- !(LOADING_STYLE_ANIMATIONS as readonly AiAssistantAnimation[]).includes(animation),
39
- );
40
- }