@genesislcap/ai-assistant 14.458.3 → 14.459.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.
- package/dist/ai-assistant.api.json +37 -10
- package/dist/ai-assistant.d.ts +46 -4
- package/dist/dts/components/flowing-waves-indicator.d.ts +32 -0
- package/dist/dts/components/flowing-waves-indicator.d.ts.map +1 -0
- package/dist/dts/components/plasma-orb-indicator.d.ts +22 -0
- package/dist/dts/components/plasma-orb-indicator.d.ts.map +1 -0
- package/dist/dts/components/waves-indicator.d.ts +30 -0
- package/dist/dts/components/waves-indicator.d.ts.map +1 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +44 -4
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/utils/animation-exclusivity.d.ts +23 -0
- package/dist/dts/utils/animation-exclusivity.d.ts.map +1 -0
- package/dist/dts/utils/animation-exclusivity.test.d.ts +2 -0
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +1 -0
- package/dist/esm/components/flowing-waves-indicator.js +222 -0
- package/dist/esm/components/plasma-orb-indicator.js +280 -0
- package/dist/esm/components/waves-indicator.js +189 -0
- package/dist/esm/main/main.js +20 -9
- package/dist/esm/main/main.styles.js +40 -6
- package/dist/esm/main/main.template.js +58 -17
- package/dist/esm/main/main.types.js +46 -3
- package/dist/esm/utils/animation-exclusivity.js +33 -0
- package/dist/esm/utils/animation-exclusivity.test.js +52 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/flowing-waves-indicator.ts +260 -0
- package/src/components/plasma-orb-indicator.ts +281 -0
- package/src/components/waves-indicator.ts +221 -0
- package/src/main/main.styles.ts +40 -6
- package/src/main/main.template.ts +60 -18
- package/src/main/main.ts +24 -8
- package/src/main/main.types.ts +56 -5
- package/src/utils/animation-exclusivity.test.ts +72 -0
- package/src/utils/animation-exclusivity.ts +40 -0
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
// Guard against a reconnect starting a second concurrent loop.
|
|
174
|
+
if (this.animFrame === undefined) {
|
|
175
|
+
this.tick();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
disconnectedCallback() {
|
|
180
|
+
super.disconnectedCallback();
|
|
181
|
+
if (this.animFrame !== undefined) {
|
|
182
|
+
cancelAnimationFrame(this.animFrame);
|
|
183
|
+
this.animFrame = undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private tick() {
|
|
188
|
+
// Stop ticking once disconnected (belt-and-braces alongside the cancel in
|
|
189
|
+
// disconnectedCallback) so no frames are scheduled while detached.
|
|
190
|
+
if (!this.isConnected) {
|
|
191
|
+
this.animFrame = undefined;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!this.wavePaths) {
|
|
195
|
+
const paths = this.shadowRoot?.querySelectorAll<SVGPathElement>('.wave');
|
|
196
|
+
if (paths?.length) this.wavePaths = Array.from(paths);
|
|
197
|
+
}
|
|
198
|
+
this.wavePaths?.forEach((path, i) => {
|
|
199
|
+
path.setAttribute('d', AiWavesIndicator.buildWavePath(WAVES[i], this.frame));
|
|
200
|
+
});
|
|
201
|
+
this.frame += 1;
|
|
202
|
+
this.animFrame = requestAnimationFrame(() => this.tick());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Trace one wave's polyline `d` attribute for the given frame. */
|
|
206
|
+
private static buildWavePath(cfg: WaveConfig, frame: number): string {
|
|
207
|
+
const segments: string[] = [];
|
|
208
|
+
for (let x = 0; x <= VIEWBOX; x += SAMPLE_STEP) {
|
|
209
|
+
const y =
|
|
210
|
+
CENTRE +
|
|
211
|
+
cfg.verticalOffset +
|
|
212
|
+
cfg.amplitude * Math.sin(x * cfg.frequency + frame * cfg.phaseSpeed) +
|
|
213
|
+
cfg.slosh * Math.sin(x * cfg.sloshFrequency + frame * cfg.sloshSpeed);
|
|
214
|
+
segments.push(`${x},${y.toFixed(2)}`);
|
|
215
|
+
}
|
|
216
|
+
return `M ${segments.join(' L ')}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Ensure the halo overlay used for the ring is registered alongside this component.
|
|
221
|
+
avoidTreeShaking(AiHaloOverlay);
|
package/src/main/main.styles.ts
CHANGED
|
@@ -101,14 +101,22 @@ export const styles = css`
|
|
|
101
101
|
animation: settings-slide-out 0.2s ease-in forwards;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
width:
|
|
104
|
+
/* Collapsed control footprint stays compact (100px)... */
|
|
105
|
+
rapid-categorized-multiselect::part(root) {
|
|
106
|
+
min-width: 0;
|
|
107
|
+
width: 100px;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
.settings-panel > [part='toggle-tool-calls'] {
|
|
@@ -127,6 +135,7 @@ export const styles = css`
|
|
|
127
135
|
}
|
|
128
136
|
|
|
129
137
|
.settings-panel > [part='download-button'] {
|
|
138
|
+
width: fit-content;
|
|
130
139
|
grid-column: 2;
|
|
131
140
|
grid-row: 2;
|
|
132
141
|
}
|
|
@@ -176,6 +185,13 @@ export const styles = css`
|
|
|
176
185
|
.settings-panel > .settings-animations {
|
|
177
186
|
grid-row: 2;
|
|
178
187
|
}
|
|
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
|
+
}
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
@container (min-width: 750px) {
|
|
@@ -809,6 +825,24 @@ export const styles = css`
|
|
|
809
825
|
}
|
|
810
826
|
}
|
|
811
827
|
|
|
828
|
+
.thinking-waves,
|
|
829
|
+
.thinking-flowing-waves,
|
|
830
|
+
.thinking-plasma {
|
|
831
|
+
display: flex;
|
|
832
|
+
flex-direction: column;
|
|
833
|
+
align-items: center;
|
|
834
|
+
align-self: center;
|
|
835
|
+
gap: 6px;
|
|
836
|
+
padding: calc(var(--design-unit) * 2px) calc(var(--design-unit) * 3px);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.thinking-caption {
|
|
840
|
+
font-size: var(--type-ramp-minus-1-font-size, 12px);
|
|
841
|
+
line-height: var(--type-ramp-minus-1-line-height, 16px);
|
|
842
|
+
color: var(--neutral-foreground-rest);
|
|
843
|
+
opacity: 70%;
|
|
844
|
+
}
|
|
845
|
+
|
|
812
846
|
.attachment-chips {
|
|
813
847
|
display: flex;
|
|
814
848
|
flex-wrap: wrap;
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
25
25
|
import { classNames, html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
|
|
26
26
|
import type { FoundationAiAssistant } from './main';
|
|
27
|
-
import { ANIMATION_DEFS } from './main.types';
|
|
27
|
+
import { ANIMATION_DEFS, LOADING_STYLE_ANIMATIONS } from './main.types';
|
|
28
28
|
|
|
29
29
|
function unknownToolPayload(tc: ChatToolCall): string {
|
|
30
30
|
if (!isChatToolCallUnknown(tc)) return '';
|
|
@@ -40,10 +40,6 @@ 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
|
-
|
|
47
43
|
const HALO_SPEED_DEFAULT = 1.5;
|
|
48
44
|
const HALO_SPEED_ORCHESTRATING = 0.4;
|
|
49
45
|
const HALO_BORDER_SIZE_DEFAULT = 3;
|
|
@@ -52,9 +48,10 @@ const HALO_BORDER_SIZE_DEFAULT = 3;
|
|
|
52
48
|
const SESSION_COST_DECIMALS = 4;
|
|
53
49
|
|
|
54
50
|
const animationOptions = Object.entries(ANIMATION_DEFS).map(([value, def]) => ({
|
|
51
|
+
type: def.category,
|
|
55
52
|
value,
|
|
56
53
|
label: def.label,
|
|
57
|
-
|
|
54
|
+
description: def.description,
|
|
58
55
|
}));
|
|
59
56
|
|
|
60
57
|
// Avatar markup is owned by the component (`assistantIconSafe` / `userIconSafe`),
|
|
@@ -176,6 +173,57 @@ const liveSubAgentTraceTemplate = html<FoundationAiAssistant>`
|
|
|
176
173
|
</div>
|
|
177
174
|
`;
|
|
178
175
|
|
|
176
|
+
// The 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 CSS animations and rAF loops. Stable references let FAST reuse the
|
|
183
|
+
// 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
|
+
const thinkingFlowingWavesTemplate = html<FoundationAiAssistant>`
|
|
201
|
+
<div class="thinking-flowing-waves" part="thinking-flowing-waves">
|
|
202
|
+
<ai-flowing-waves-indicator></ai-flowing-waves-indicator>
|
|
203
|
+
<span class="thinking-caption">Thinking...</span>
|
|
204
|
+
</div>
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
const thinkingPlasmaTemplate = html<FoundationAiAssistant>`
|
|
208
|
+
<div class="thinking-plasma" part="thinking-plasma">
|
|
209
|
+
<ai-plasma-orb-indicator></ai-plasma-orb-indicator>
|
|
210
|
+
<span class="thinking-caption">Thinking...</span>
|
|
211
|
+
</div>
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Picks the loading indicator for the currently enabled style, falling back to
|
|
216
|
+
* the dots (also the default when no animations config is supplied). Returns a
|
|
217
|
+
* stable template instance per style — see the note above.
|
|
218
|
+
*/
|
|
219
|
+
const selectThinkingTemplate = (x: FoundationAiAssistant): ViewTemplate<FoundationAiAssistant> => {
|
|
220
|
+
const enabled = x.enabledAnimations;
|
|
221
|
+
if (enabled.includes('waves')) return thinkingWavesTemplate;
|
|
222
|
+
if (enabled.includes('flowingWaves')) return thinkingFlowingWavesTemplate;
|
|
223
|
+
if (enabled.includes('plasma')) return thinkingPlasmaTemplate;
|
|
224
|
+
return thinkingDotsTemplate;
|
|
225
|
+
};
|
|
226
|
+
|
|
179
227
|
// ─── Public factory ───────────────────────────────────────────────────────────
|
|
180
228
|
|
|
181
229
|
/** @internal */
|
|
@@ -184,7 +232,7 @@ export const FoundationAiAssistantTemplate = (
|
|
|
184
232
|
): ViewTemplate<FoundationAiAssistant> => {
|
|
185
233
|
const buttonTag = `${designSystemPrefix}-button`;
|
|
186
234
|
const switchTag = `${designSystemPrefix}-switch`;
|
|
187
|
-
const
|
|
235
|
+
const categorizedMultiselectTag = `${designSystemPrefix}-categorized-multiselect`;
|
|
188
236
|
const textareaTag = `${designSystemPrefix}-text-area`;
|
|
189
237
|
const iconTag = `${designSystemPrefix}-icon`;
|
|
190
238
|
const progressTag = `${designSystemPrefix}-progress`;
|
|
@@ -473,16 +521,14 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
473
521
|
html<FoundationAiAssistant>`
|
|
474
522
|
<div class="settings-animations">
|
|
475
523
|
<span class="settings-label">Animations</span>
|
|
476
|
-
<${
|
|
524
|
+
<${categorizedMultiselectTag}
|
|
477
525
|
part="toggle-animations"
|
|
478
526
|
:selectedOptions=${(x) => x.enabledAnimations}
|
|
479
527
|
:options=${() => animationOptions}
|
|
480
|
-
:itemRenderer=${() => animationItemRenderer}
|
|
481
528
|
@selectionChange=${(x, c) =>
|
|
482
529
|
x.setEnabledAnimations((c.event as CustomEvent).detail)}
|
|
483
530
|
search="false"
|
|
484
|
-
|
|
485
|
-
></${multiselectTag}>
|
|
531
|
+
></${categorizedMultiselectTag}>
|
|
486
532
|
</div>
|
|
487
533
|
`,
|
|
488
534
|
)}
|
|
@@ -590,7 +636,8 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
590
636
|
${when(
|
|
591
637
|
(x) =>
|
|
592
638
|
x.showLoadingIndicator &&
|
|
593
|
-
(x.chatConfig.ui?.animations == null ||
|
|
639
|
+
(x.chatConfig.ui?.animations == null ||
|
|
640
|
+
LOADING_STYLE_ANIMATIONS.some((s) => x.enabledAnimations.includes(s))),
|
|
594
641
|
html<FoundationAiAssistant>`
|
|
595
642
|
<div class="message-row ai" part="thinking">
|
|
596
643
|
<div class="avatar">
|
|
@@ -603,12 +650,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
603
650
|
<span class="avatar-icon" :innerHTML="${() => x.assistantIconSafe}"></span>
|
|
604
651
|
`}
|
|
605
652
|
</div>
|
|
606
|
-
|
|
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>
|
|
653
|
+
${(x) => selectThinkingTemplate(x)}
|
|
612
654
|
</div>
|
|
613
655
|
`,
|
|
614
656
|
)}
|
package/src/main/main.ts
CHANGED
|
@@ -47,8 +47,11 @@ import { AiChatBubble } from '../components/chat-bubble/chat-bubble';
|
|
|
47
47
|
import { ChatDriver } from '../components/chat-driver/chat-driver';
|
|
48
48
|
import { AiChatInteractionWrapper } from '../components/chat-interaction-wrapper/chat-interaction-wrapper';
|
|
49
49
|
import { AiChatMarkdown } from '../components/chat-markdown/chat-markdown';
|
|
50
|
+
import { AiFlowingWavesIndicator } from '../components/flowing-waves-indicator';
|
|
50
51
|
import { AiHaloOverlay } from '../components/halo-overlay';
|
|
51
52
|
import { OrchestratingDriver } from '../components/orchestrating-driver/orchestrating-driver';
|
|
53
|
+
import { AiPlasmaOrbIndicator } from '../components/plasma-orb-indicator';
|
|
54
|
+
import { AiWavesIndicator } from '../components/waves-indicator';
|
|
52
55
|
import type { AgentConfig } from '../config/config';
|
|
53
56
|
import {
|
|
54
57
|
recordMetaEvent,
|
|
@@ -66,6 +69,7 @@ import {
|
|
|
66
69
|
} from '../styles/ai-colours';
|
|
67
70
|
import { ChatSuggestions } from '../suggestions/chat-suggestions';
|
|
68
71
|
import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
|
|
72
|
+
import { resolveExclusiveLoadingStyle } from '../utils/animation-exclusivity';
|
|
69
73
|
import { logger } from '../utils/logger';
|
|
70
74
|
import { filterVisibleMessages, trailingInteractionRow } from '../utils/message-partition';
|
|
71
75
|
import { sumCosts } from '../utils/sum-costs';
|
|
@@ -80,7 +84,7 @@ import type {
|
|
|
80
84
|
SubmitMessageResult,
|
|
81
85
|
SuggestionsState,
|
|
82
86
|
} from './main.types';
|
|
83
|
-
import {
|
|
87
|
+
import { DEFAULT_ANIMATIONS } from './main.types';
|
|
84
88
|
|
|
85
89
|
/** Context window sizes (in tokens) for known models. */
|
|
86
90
|
/**
|
|
@@ -137,6 +141,9 @@ avoidTreeShaking(
|
|
|
137
141
|
AiChatMarkdown,
|
|
138
142
|
AiChatInteractionWrapper,
|
|
139
143
|
AiHaloOverlay,
|
|
144
|
+
AiWavesIndicator,
|
|
145
|
+
AiFlowingWavesIndicator,
|
|
146
|
+
AiPlasmaOrbIndicator,
|
|
140
147
|
AiChatBubble,
|
|
141
148
|
AiActivityHalo,
|
|
142
149
|
ChatSuggestions,
|
|
@@ -656,7 +663,9 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
656
663
|
return;
|
|
657
664
|
}
|
|
658
665
|
const last = this.messages[this.messages.length - 1];
|
|
659
|
-
|
|
666
|
+
// Hide only while a pending interaction blocks on the user; a resolved
|
|
667
|
+
// interaction means the assistant is computing again (keep the halo).
|
|
668
|
+
if (last?.interaction && !last.interaction.resolved) {
|
|
660
669
|
this.showHalo = 'no';
|
|
661
670
|
} else if (this.showHalo !== 'orchestrating') {
|
|
662
671
|
this.showHalo = 'agent';
|
|
@@ -1047,9 +1056,10 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1047
1056
|
this.showToolCalls = ui.showToolCalls === true;
|
|
1048
1057
|
this.showThinkingSteps = ui.showThinkingSteps === true;
|
|
1049
1058
|
this.showAgentSwitchIndicator = ui.showAgentSwitchIndicator === true;
|
|
1050
|
-
this.enabledAnimations =
|
|
1059
|
+
this.enabledAnimations = resolveExclusiveLoadingStyle(
|
|
1051
1060
|
(ui.animations?.enabled as AiAssistantAnimation[]) ??
|
|
1052
|
-
|
|
1061
|
+
(ui.animations ? [...DEFAULT_ANIMATIONS] : []),
|
|
1062
|
+
);
|
|
1053
1063
|
|
|
1054
1064
|
const defaultAgent = this.chatConfig.picker?.defaultAgent;
|
|
1055
1065
|
if (defaultAgent && (this.agents ?? []).some((a) => a.name === defaultAgent)) {
|
|
@@ -1216,9 +1226,13 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1216
1226
|
// waiting for the user, not computing.
|
|
1217
1227
|
if (this.busy) {
|
|
1218
1228
|
const last = this.messages[this.messages.length - 1];
|
|
1219
|
-
|
|
1229
|
+
// Only a *pending* interaction means the assistant is blocked waiting on
|
|
1230
|
+
// the user. Once it's resolved — or for any normal step — the assistant is
|
|
1231
|
+
// computing again, so the indicator should resume (e.g. while it works out
|
|
1232
|
+
// the next planning question after the user answers a widget).
|
|
1233
|
+
if (last?.interaction && !last.interaction.resolved) {
|
|
1220
1234
|
this.stopLoadingTimer();
|
|
1221
|
-
} else
|
|
1235
|
+
} else {
|
|
1222
1236
|
this.startLoadingTimer();
|
|
1223
1237
|
}
|
|
1224
1238
|
}
|
|
@@ -1302,7 +1316,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1302
1316
|
});
|
|
1303
1317
|
}
|
|
1304
1318
|
|
|
1305
|
-
private static readonly DEFAULT_LOADING_DELAY_S =
|
|
1319
|
+
private static readonly DEFAULT_LOADING_DELAY_S = 0;
|
|
1306
1320
|
private static readonly DEFAULT_SUGGESTION_COUNT = 3;
|
|
1307
1321
|
private static readonly MS_PER_SECOND = 1000;
|
|
1308
1322
|
|
|
@@ -1506,7 +1520,9 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1506
1520
|
}
|
|
1507
1521
|
|
|
1508
1522
|
setEnabledAnimations(animations: AiAssistantAnimation[]) {
|
|
1509
|
-
|
|
1523
|
+
// The dots and waves loading styles are mutually exclusive — enabling one
|
|
1524
|
+
// disables the other (see resolveExclusiveLoadingStyle).
|
|
1525
|
+
this.enabledAnimations = resolveExclusiveLoadingStyle(animations, this.enabledAnimations);
|
|
1510
1526
|
}
|
|
1511
1527
|
|
|
1512
1528
|
getDebugLog() {
|
package/src/main/main.types.ts
CHANGED
|
@@ -53,24 +53,53 @@ 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
|
|
57
|
-
|
|
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;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
63
|
* Registry of all available animations with their display metadata.
|
|
62
64
|
* Adding an entry here automatically extends the {@link AiAssistantAnimation} type.
|
|
63
65
|
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* `loading` (dots), `waves`, `flowingWaves` and `plasma` are interchangeable
|
|
68
|
+
* styles of the same "is the assistant working" indicator and are therefore
|
|
69
|
+
* grouped under the same {@link AiAssistantAnimationDef.category}. They are
|
|
70
|
+
* mutually exclusive — see `LOADING_STYLE_ANIMATIONS`.
|
|
71
|
+
*
|
|
64
72
|
* @beta
|
|
65
73
|
*/
|
|
66
74
|
export const ANIMATION_DEFS = {
|
|
67
75
|
loading: {
|
|
68
|
-
label: '
|
|
69
|
-
|
|
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',
|
|
85
|
+
},
|
|
86
|
+
flowingWaves: {
|
|
87
|
+
label: 'Flowing waves',
|
|
88
|
+
description:
|
|
89
|
+
'Shows coloured waves rising and settling from a line while the assistant is generating a response.',
|
|
90
|
+
category: 'Loading style',
|
|
91
|
+
},
|
|
92
|
+
plasma: {
|
|
93
|
+
label: 'Plasma orb',
|
|
94
|
+
description:
|
|
95
|
+
'Shows a glowing plasma sphere with drifting energy while the assistant is generating a response.',
|
|
96
|
+
category: 'Loading style',
|
|
70
97
|
},
|
|
71
98
|
halo: {
|
|
72
99
|
label: 'Halo',
|
|
73
|
-
|
|
100
|
+
description:
|
|
101
|
+
'Displays a glowing halo around the assistant avatar while a response is streaming.',
|
|
102
|
+
category: 'Effects',
|
|
74
103
|
},
|
|
75
104
|
} satisfies Record<string, AiAssistantAnimationDef>;
|
|
76
105
|
|
|
@@ -87,3 +116,25 @@ export type AiAssistantAnimation = keyof typeof ANIMATION_DEFS;
|
|
|
87
116
|
* @internal
|
|
88
117
|
*/
|
|
89
118
|
export const ALL_ANIMATIONS = Object.keys(ANIMATION_DEFS) as AiAssistantAnimation[];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The interchangeable "assistant is working" loading-indicator styles. At most
|
|
122
|
+
* one of these may be enabled at a time — enabling one disables the other.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
export const LOADING_STYLE_ANIMATIONS = [
|
|
127
|
+
'loading',
|
|
128
|
+
'waves',
|
|
129
|
+
'flowingWaves',
|
|
130
|
+
'plasma',
|
|
131
|
+
] as const satisfies readonly AiAssistantAnimation[];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Animations enabled by default when a consumer opts into the animations
|
|
135
|
+
* feature without specifying an explicit `enabled` list. Keeps the dots loading
|
|
136
|
+
* style (the long-standing default); the waves style is opt-in.
|
|
137
|
+
*
|
|
138
|
+
* @internal
|
|
139
|
+
*/
|
|
140
|
+
export const DEFAULT_ANIMATIONS: AiAssistantAnimation[] = ['loading', 'halo'];
|