@genesislcap/ai-assistant 14.458.3 → 14.460.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 +38 -11
- package/dist/ai-assistant.d.ts +49 -5
- package/dist/dts/components/chat-driver/chat-driver.d.ts +3 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- 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/chat-driver/chat-driver.js +7 -2
- package/dist/esm/components/chat-driver/chat-driver.test.js +24 -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 +62 -7
- package/dist/esm/main/main.template.js +75 -21
- 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/chat-driver/chat-driver.test.ts +36 -0
- package/src/components/chat-driver/chat-driver.ts +10 -2
- 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 +62 -7
- package/src/main/main.template.ts +88 -27
- 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) {
|
|
@@ -417,6 +433,13 @@ export const styles = css`
|
|
|
417
433
|
animation: slide-in-left 0.25s ease-out;
|
|
418
434
|
}
|
|
419
435
|
|
|
436
|
+
/* A 'bubble' interaction reads like a normal assistant message: avatar to the
|
|
437
|
+
left, bubble to the right — not the full-width column the other interaction
|
|
438
|
+
presentations use. */
|
|
439
|
+
.message-row.interaction.present-bubble {
|
|
440
|
+
flex-direction: row;
|
|
441
|
+
}
|
|
442
|
+
|
|
420
443
|
.avatar {
|
|
421
444
|
position: relative;
|
|
422
445
|
width: 32px;
|
|
@@ -525,7 +548,10 @@ export const styles = css`
|
|
|
525
548
|
border: 1px solid color-mix(in srgb, var(--ai-function-color, #86efac) 20%, transparent);
|
|
526
549
|
}
|
|
527
550
|
|
|
528
|
-
|
|
551
|
+
/* 'label' (default) and 'bare' interactions render the widget chrome-free and
|
|
552
|
+
full-width — the widget owns its body. */
|
|
553
|
+
.message-row.interaction.present-label .message,
|
|
554
|
+
.message-row.interaction.present-bare .message {
|
|
529
555
|
background: none;
|
|
530
556
|
border: none;
|
|
531
557
|
padding: 0;
|
|
@@ -533,6 +559,17 @@ export const styles = css`
|
|
|
533
559
|
width: 100%;
|
|
534
560
|
}
|
|
535
561
|
|
|
562
|
+
/* 'bubble' interactions reuse the standard assistant-message skin so the
|
|
563
|
+
widget sits inside a normal chat bubble. Padding, max-width and min-width
|
|
564
|
+
come from the base .message rule. */
|
|
565
|
+
.message-row.interaction.present-bubble .message {
|
|
566
|
+
background: linear-gradient(135deg, var(--neutral-layer-3) 0%, var(--neutral-layer-4) 100%);
|
|
567
|
+
color: var(--neutral-foreground-rest);
|
|
568
|
+
border-radius: 4px 18px 18px;
|
|
569
|
+
border: calc(var(--stroke-width) * 1px) solid var(--neutral-stroke-rest);
|
|
570
|
+
box-shadow: var(--ai-message-shadow, 0 2px 8px rgb(0 0 0 / 15%));
|
|
571
|
+
}
|
|
572
|
+
|
|
536
573
|
.sender {
|
|
537
574
|
font-weight: bold;
|
|
538
575
|
font-size: 0.9em;
|
|
@@ -809,6 +846,24 @@ export const styles = css`
|
|
|
809
846
|
}
|
|
810
847
|
}
|
|
811
848
|
|
|
849
|
+
.thinking-waves,
|
|
850
|
+
.thinking-flowing-waves,
|
|
851
|
+
.thinking-plasma {
|
|
852
|
+
display: flex;
|
|
853
|
+
flex-direction: column;
|
|
854
|
+
align-items: center;
|
|
855
|
+
align-self: center;
|
|
856
|
+
gap: 6px;
|
|
857
|
+
padding: calc(var(--design-unit) * 2px) calc(var(--design-unit) * 3px);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.thinking-caption {
|
|
861
|
+
font-size: var(--type-ramp-minus-1-font-size, 12px);
|
|
862
|
+
line-height: var(--type-ramp-minus-1-line-height, 16px);
|
|
863
|
+
color: var(--neutral-foreground-rest);
|
|
864
|
+
opacity: 70%;
|
|
865
|
+
}
|
|
866
|
+
|
|
812
867
|
.attachment-chips {
|
|
813
868
|
display: flex;
|
|
814
869
|
flex-wrap: wrap;
|
|
@@ -20,11 +20,12 @@ import type {
|
|
|
20
20
|
ChatAttachment,
|
|
21
21
|
ChatMessage,
|
|
22
22
|
ChatToolCall,
|
|
23
|
+
InteractionPresentation,
|
|
23
24
|
} from '@genesislcap/foundation-ai';
|
|
24
25
|
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
25
26
|
import { classNames, html, ref, repeat, when, ViewTemplate } from '@genesislcap/web-core';
|
|
26
27
|
import type { FoundationAiAssistant } from './main';
|
|
27
|
-
import { ANIMATION_DEFS } from './main.types';
|
|
28
|
+
import { ANIMATION_DEFS, LOADING_STYLE_ANIMATIONS } from './main.types';
|
|
28
29
|
|
|
29
30
|
function unknownToolPayload(tc: ChatToolCall): string {
|
|
30
31
|
if (!isChatToolCallUnknown(tc)) return '';
|
|
@@ -40,10 +41,6 @@ function unknownToolPayload(tc: ChatToolCall): string {
|
|
|
40
41
|
return lines.join('\n');
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const animationItemRenderer = (option: any): ViewTemplate => html`
|
|
44
|
-
<span part="option-label" title="${() => option.tooltip}">${() => option.label}</span>
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
44
|
const HALO_SPEED_DEFAULT = 1.5;
|
|
48
45
|
const HALO_SPEED_ORCHESTRATING = 0.4;
|
|
49
46
|
const HALO_BORDER_SIZE_DEFAULT = 3;
|
|
@@ -52,9 +49,10 @@ const HALO_BORDER_SIZE_DEFAULT = 3;
|
|
|
52
49
|
const SESSION_COST_DECIMALS = 4;
|
|
53
50
|
|
|
54
51
|
const animationOptions = Object.entries(ANIMATION_DEFS).map(([value, def]) => ({
|
|
52
|
+
type: def.category,
|
|
55
53
|
value,
|
|
56
54
|
label: def.label,
|
|
57
|
-
|
|
55
|
+
description: def.description,
|
|
58
56
|
}));
|
|
59
57
|
|
|
60
58
|
// Avatar markup is owned by the component (`assistantIconSafe` / `userIconSafe`),
|
|
@@ -120,6 +118,14 @@ const senderLabel: Record<string, string> = {
|
|
|
120
118
|
ai: 'Assistant',
|
|
121
119
|
};
|
|
122
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Resolves how the host frames an interaction widget, defaulting to `'label'`
|
|
123
|
+
* (the historical "Assistant" label, no bubble). Callers guard on
|
|
124
|
+
* `m.interaction` first; non-interaction messages never consult this.
|
|
125
|
+
*/
|
|
126
|
+
const interactionPresentation = (m: ChatMessage): InteractionPresentation =>
|
|
127
|
+
m.interaction?.presentation ?? 'label';
|
|
128
|
+
|
|
123
129
|
// ─── Sub-agent trace fragments ────────────────────────────────────────────────
|
|
124
130
|
|
|
125
131
|
const subAgentAssistantTemplate = html<ChatMessage>`
|
|
@@ -176,6 +182,57 @@ const liveSubAgentTraceTemplate = html<FoundationAiAssistant>`
|
|
|
176
182
|
</div>
|
|
177
183
|
`;
|
|
178
184
|
|
|
185
|
+
// The interchangeable loading indicators. These MUST be stable module-level
|
|
186
|
+
// instances: the binding that selects between them reads `enabledAnimations`,
|
|
187
|
+
// which is backed by the redux store proxy and re-evaluates on every change to
|
|
188
|
+
// the aiAssistant slice (i.e. every new message, including hidden tool-call and
|
|
189
|
+
// thinking-step messages). Returning a fresh `html` instance from that binding
|
|
190
|
+
// would make FAST tear down and rebuild the indicator DOM each time, restarting
|
|
191
|
+
// the CSS animations and rAF loops. Stable references let FAST reuse the
|
|
192
|
+
// existing view so the animation runs uninterrupted.
|
|
193
|
+
const thinkingDotsTemplate = html<FoundationAiAssistant>`
|
|
194
|
+
<div class="thinking-dots">
|
|
195
|
+
<div class="dot dot-1"></div>
|
|
196
|
+
<div class="dot dot-2"></div>
|
|
197
|
+
<div class="dot dot-3"></div>
|
|
198
|
+
<div class="dot dot-4"></div>
|
|
199
|
+
</div>
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
const thinkingWavesTemplate = html<FoundationAiAssistant>`
|
|
203
|
+
<div class="thinking-waves" part="thinking-waves">
|
|
204
|
+
<ai-waves-indicator></ai-waves-indicator>
|
|
205
|
+
<span class="thinking-caption">Thinking...</span>
|
|
206
|
+
</div>
|
|
207
|
+
`;
|
|
208
|
+
|
|
209
|
+
const thinkingFlowingWavesTemplate = html<FoundationAiAssistant>`
|
|
210
|
+
<div class="thinking-flowing-waves" part="thinking-flowing-waves">
|
|
211
|
+
<ai-flowing-waves-indicator></ai-flowing-waves-indicator>
|
|
212
|
+
<span class="thinking-caption">Thinking...</span>
|
|
213
|
+
</div>
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
const thinkingPlasmaTemplate = html<FoundationAiAssistant>`
|
|
217
|
+
<div class="thinking-plasma" part="thinking-plasma">
|
|
218
|
+
<ai-plasma-orb-indicator></ai-plasma-orb-indicator>
|
|
219
|
+
<span class="thinking-caption">Thinking...</span>
|
|
220
|
+
</div>
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Picks the loading indicator for the currently enabled style, falling back to
|
|
225
|
+
* the dots (also the default when no animations config is supplied). Returns a
|
|
226
|
+
* stable template instance per style — see the note above.
|
|
227
|
+
*/
|
|
228
|
+
const selectThinkingTemplate = (x: FoundationAiAssistant): ViewTemplate<FoundationAiAssistant> => {
|
|
229
|
+
const enabled = x.enabledAnimations;
|
|
230
|
+
if (enabled.includes('waves')) return thinkingWavesTemplate;
|
|
231
|
+
if (enabled.includes('flowingWaves')) return thinkingFlowingWavesTemplate;
|
|
232
|
+
if (enabled.includes('plasma')) return thinkingPlasmaTemplate;
|
|
233
|
+
return thinkingDotsTemplate;
|
|
234
|
+
};
|
|
235
|
+
|
|
179
236
|
// ─── Public factory ───────────────────────────────────────────────────────────
|
|
180
237
|
|
|
181
238
|
/** @internal */
|
|
@@ -184,7 +241,7 @@ export const FoundationAiAssistantTemplate = (
|
|
|
184
241
|
): ViewTemplate<FoundationAiAssistant> => {
|
|
185
242
|
const buttonTag = `${designSystemPrefix}-button`;
|
|
186
243
|
const switchTag = `${designSystemPrefix}-switch`;
|
|
187
|
-
const
|
|
244
|
+
const categorizedMultiselectTag = `${designSystemPrefix}-categorized-multiselect`;
|
|
188
245
|
const textareaTag = `${designSystemPrefix}-text-area`;
|
|
189
246
|
const iconTag = `${designSystemPrefix}-icon`;
|
|
190
247
|
const progressTag = `${designSystemPrefix}-progress`;
|
|
@@ -236,7 +293,10 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
236
293
|
${when(
|
|
237
294
|
(m) => m.role !== 'system-event',
|
|
238
295
|
html<ChatMessage, FoundationAiAssistant>`
|
|
239
|
-
<div
|
|
296
|
+
<div
|
|
297
|
+
class="message-row ${(m) => messageType(m)} ${(m) =>
|
|
298
|
+
m.interaction ? `present-${interactionPresentation(m)}` : ''}"
|
|
299
|
+
>
|
|
240
300
|
<div class="avatar ${(m) => messageType(m)}">
|
|
241
301
|
${when(
|
|
242
302
|
// Keyed on messageType (not raw role) so a synthetic-user message,
|
|
@@ -247,14 +307,21 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
247
307
|
)}${when((m) => messageType(m) !== 'user', assistantAvatarTemplate)}
|
|
248
308
|
</div>
|
|
249
309
|
<div class="message ${(m) => messageType(m)}">
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
310
|
+
${when(
|
|
311
|
+
// A 'bare' interaction owns its full presentation — suppress the
|
|
312
|
+
// host sender label. Every other message keeps it.
|
|
313
|
+
(m) => !(m.interaction && interactionPresentation(m) === 'bare'),
|
|
314
|
+
html<ChatMessage, FoundationAiAssistant>`
|
|
315
|
+
<div class="sender">
|
|
316
|
+
${(m, c) =>
|
|
317
|
+
messageType(m) === 'ai-function' &&
|
|
318
|
+
m.agentName &&
|
|
319
|
+
(c.parent as FoundationAiAssistant).showAgentSwitchIndicator
|
|
320
|
+
? `Tool Call · ${m.agentLabel ?? m.agentName}`
|
|
321
|
+
: senderLabel[messageType(m)]}
|
|
322
|
+
</div>
|
|
323
|
+
`,
|
|
324
|
+
)}
|
|
258
325
|
<div class="content">
|
|
259
326
|
${when(
|
|
260
327
|
(m) => m.content,
|
|
@@ -473,16 +540,14 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
473
540
|
html<FoundationAiAssistant>`
|
|
474
541
|
<div class="settings-animations">
|
|
475
542
|
<span class="settings-label">Animations</span>
|
|
476
|
-
<${
|
|
543
|
+
<${categorizedMultiselectTag}
|
|
477
544
|
part="toggle-animations"
|
|
478
545
|
:selectedOptions=${(x) => x.enabledAnimations}
|
|
479
546
|
:options=${() => animationOptions}
|
|
480
|
-
:itemRenderer=${() => animationItemRenderer}
|
|
481
547
|
@selectionChange=${(x, c) =>
|
|
482
548
|
x.setEnabledAnimations((c.event as CustomEvent).detail)}
|
|
483
549
|
search="false"
|
|
484
|
-
|
|
485
|
-
></${multiselectTag}>
|
|
550
|
+
></${categorizedMultiselectTag}>
|
|
486
551
|
</div>
|
|
487
552
|
`,
|
|
488
553
|
)}
|
|
@@ -590,7 +655,8 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
590
655
|
${when(
|
|
591
656
|
(x) =>
|
|
592
657
|
x.showLoadingIndicator &&
|
|
593
|
-
(x.chatConfig.ui?.animations == null ||
|
|
658
|
+
(x.chatConfig.ui?.animations == null ||
|
|
659
|
+
LOADING_STYLE_ANIMATIONS.some((s) => x.enabledAnimations.includes(s))),
|
|
594
660
|
html<FoundationAiAssistant>`
|
|
595
661
|
<div class="message-row ai" part="thinking">
|
|
596
662
|
<div class="avatar">
|
|
@@ -603,12 +669,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
603
669
|
<span class="avatar-icon" :innerHTML="${() => x.assistantIconSafe}"></span>
|
|
604
670
|
`}
|
|
605
671
|
</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>
|
|
672
|
+
${(x) => selectThinkingTemplate(x)}
|
|
612
673
|
</div>
|
|
613
674
|
`,
|
|
614
675
|
)}
|
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() {
|