@genesislcap/ai-assistant 14.458.0 → 14.458.1-GENC-0.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/dist/ai-assistant.api.json +37 -10
- package/dist/ai-assistant.d.ts +36 -4
- 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 +34 -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/waves-indicator.js +180 -0
- package/dist/esm/main/main.js +18 -9
- package/dist/esm/main/main.styles.js +38 -6
- package/dist/esm/main/main.template.js +23 -16
- package/dist/esm/main/main.types.js +34 -3
- package/dist/esm/utils/animation-exclusivity.js +33 -0
- package/dist/esm/utils/animation-exclusivity.test.js +41 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/waves-indicator.ts +212 -0
- package/src/main/main.styles.ts +38 -6
- package/src/main/main.template.ts +24 -17
- package/src/main/main.ts +20 -8
- package/src/main/main.types.ts +42 -5
- package/src/utils/animation-exclusivity.test.ts +53 -0
- package/src/utils/animation-exclusivity.ts +40 -0
|
@@ -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`),
|
|
@@ -184,7 +181,7 @@ export const FoundationAiAssistantTemplate = (
|
|
|
184
181
|
): ViewTemplate<FoundationAiAssistant> => {
|
|
185
182
|
const buttonTag = `${designSystemPrefix}-button`;
|
|
186
183
|
const switchTag = `${designSystemPrefix}-switch`;
|
|
187
|
-
const
|
|
184
|
+
const categorizedMultiselectTag = `${designSystemPrefix}-categorized-multiselect`;
|
|
188
185
|
const textareaTag = `${designSystemPrefix}-text-area`;
|
|
189
186
|
const iconTag = `${designSystemPrefix}-icon`;
|
|
190
187
|
const progressTag = `${designSystemPrefix}-progress`;
|
|
@@ -473,16 +470,14 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
473
470
|
html<FoundationAiAssistant>`
|
|
474
471
|
<div class="settings-animations">
|
|
475
472
|
<span class="settings-label">Animations</span>
|
|
476
|
-
<${
|
|
473
|
+
<${categorizedMultiselectTag}
|
|
477
474
|
part="toggle-animations"
|
|
478
475
|
:selectedOptions=${(x) => x.enabledAnimations}
|
|
479
476
|
:options=${() => animationOptions}
|
|
480
|
-
:itemRenderer=${() => animationItemRenderer}
|
|
481
477
|
@selectionChange=${(x, c) =>
|
|
482
478
|
x.setEnabledAnimations((c.event as CustomEvent).detail)}
|
|
483
479
|
search="false"
|
|
484
|
-
|
|
485
|
-
></${multiselectTag}>
|
|
480
|
+
></${categorizedMultiselectTag}>
|
|
486
481
|
</div>
|
|
487
482
|
`,
|
|
488
483
|
)}
|
|
@@ -590,7 +585,9 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
590
585
|
${when(
|
|
591
586
|
(x) =>
|
|
592
587
|
x.showLoadingIndicator &&
|
|
593
|
-
(x.chatConfig.ui?.animations == null ||
|
|
588
|
+
(x.chatConfig.ui?.animations == null ||
|
|
589
|
+
x.enabledAnimations.includes('loading') ||
|
|
590
|
+
x.enabledAnimations.includes('waves')),
|
|
594
591
|
html<FoundationAiAssistant>`
|
|
595
592
|
<div class="message-row ai" part="thinking">
|
|
596
593
|
<div class="avatar">
|
|
@@ -603,12 +600,22 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
603
600
|
<span class="avatar-icon" :innerHTML="${() => x.assistantIconSafe}"></span>
|
|
604
601
|
`}
|
|
605
602
|
</div>
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
603
|
+
${(x) =>
|
|
604
|
+
x.enabledAnimations.includes('waves')
|
|
605
|
+
? html<FoundationAiAssistant>`
|
|
606
|
+
<div class="thinking-waves" part="thinking-waves">
|
|
607
|
+
<ai-waves-indicator></ai-waves-indicator>
|
|
608
|
+
<span class="thinking-caption">Thinking...</span>
|
|
609
|
+
</div>
|
|
610
|
+
`
|
|
611
|
+
: html<FoundationAiAssistant>`
|
|
612
|
+
<div class="thinking-dots">
|
|
613
|
+
<div class="dot dot-1"></div>
|
|
614
|
+
<div class="dot dot-2"></div>
|
|
615
|
+
<div class="dot dot-3"></div>
|
|
616
|
+
<div class="dot dot-4"></div>
|
|
617
|
+
</div>
|
|
618
|
+
`}
|
|
612
619
|
</div>
|
|
613
620
|
`,
|
|
614
621
|
)}
|
package/src/main/main.ts
CHANGED
|
@@ -49,6 +49,7 @@ 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';
|
|
52
53
|
import type { AgentConfig } from '../config/config';
|
|
53
54
|
import {
|
|
54
55
|
recordMetaEvent,
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
} from '../styles/ai-colours';
|
|
67
68
|
import { ChatSuggestions } from '../suggestions/chat-suggestions';
|
|
68
69
|
import { AnimatedPanelToggle } from '../utils/animated-panel-toggle';
|
|
70
|
+
import { resolveExclusiveLoadingStyle } from '../utils/animation-exclusivity';
|
|
69
71
|
import { logger } from '../utils/logger';
|
|
70
72
|
import { filterVisibleMessages, trailingInteractionRow } from '../utils/message-partition';
|
|
71
73
|
import { sumCosts } from '../utils/sum-costs';
|
|
@@ -80,7 +82,7 @@ import type {
|
|
|
80
82
|
SubmitMessageResult,
|
|
81
83
|
SuggestionsState,
|
|
82
84
|
} from './main.types';
|
|
83
|
-
import {
|
|
85
|
+
import { DEFAULT_ANIMATIONS } from './main.types';
|
|
84
86
|
|
|
85
87
|
/** Context window sizes (in tokens) for known models. */
|
|
86
88
|
/**
|
|
@@ -137,6 +139,7 @@ avoidTreeShaking(
|
|
|
137
139
|
AiChatMarkdown,
|
|
138
140
|
AiChatInteractionWrapper,
|
|
139
141
|
AiHaloOverlay,
|
|
142
|
+
AiWavesIndicator,
|
|
140
143
|
AiChatBubble,
|
|
141
144
|
AiActivityHalo,
|
|
142
145
|
ChatSuggestions,
|
|
@@ -656,7 +659,9 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
656
659
|
return;
|
|
657
660
|
}
|
|
658
661
|
const last = this.messages[this.messages.length - 1];
|
|
659
|
-
|
|
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) {
|
|
660
665
|
this.showHalo = 'no';
|
|
661
666
|
} else if (this.showHalo !== 'orchestrating') {
|
|
662
667
|
this.showHalo = 'agent';
|
|
@@ -1047,9 +1052,10 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1047
1052
|
this.showToolCalls = ui.showToolCalls === true;
|
|
1048
1053
|
this.showThinkingSteps = ui.showThinkingSteps === true;
|
|
1049
1054
|
this.showAgentSwitchIndicator = ui.showAgentSwitchIndicator === true;
|
|
1050
|
-
this.enabledAnimations =
|
|
1055
|
+
this.enabledAnimations = resolveExclusiveLoadingStyle(
|
|
1051
1056
|
(ui.animations?.enabled as AiAssistantAnimation[]) ??
|
|
1052
|
-
|
|
1057
|
+
(ui.animations ? [...DEFAULT_ANIMATIONS] : []),
|
|
1058
|
+
);
|
|
1053
1059
|
|
|
1054
1060
|
const defaultAgent = this.chatConfig.picker?.defaultAgent;
|
|
1055
1061
|
if (defaultAgent && (this.agents ?? []).some((a) => a.name === defaultAgent)) {
|
|
@@ -1216,9 +1222,13 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1216
1222
|
// waiting for the user, not computing.
|
|
1217
1223
|
if (this.busy) {
|
|
1218
1224
|
const last = this.messages[this.messages.length - 1];
|
|
1219
|
-
|
|
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) {
|
|
1220
1230
|
this.stopLoadingTimer();
|
|
1221
|
-
} else
|
|
1231
|
+
} else {
|
|
1222
1232
|
this.startLoadingTimer();
|
|
1223
1233
|
}
|
|
1224
1234
|
}
|
|
@@ -1302,7 +1312,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1302
1312
|
});
|
|
1303
1313
|
}
|
|
1304
1314
|
|
|
1305
|
-
private static readonly DEFAULT_LOADING_DELAY_S =
|
|
1315
|
+
private static readonly DEFAULT_LOADING_DELAY_S = 0;
|
|
1306
1316
|
private static readonly DEFAULT_SUGGESTION_COUNT = 3;
|
|
1307
1317
|
private static readonly MS_PER_SECOND = 1000;
|
|
1308
1318
|
|
|
@@ -1506,7 +1516,9 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1506
1516
|
}
|
|
1507
1517
|
|
|
1508
1518
|
setEnabledAnimations(animations: AiAssistantAnimation[]) {
|
|
1509
|
-
|
|
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);
|
|
1510
1522
|
}
|
|
1511
1523
|
|
|
1512
1524
|
getDebugLog() {
|
package/src/main/main.types.ts
CHANGED
|
@@ -53,24 +53,41 @@ 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) 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
|
+
*
|
|
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',
|
|
70
85
|
},
|
|
71
86
|
halo: {
|
|
72
87
|
label: 'Halo',
|
|
73
|
-
|
|
88
|
+
description:
|
|
89
|
+
'Displays a glowing halo around the assistant avatar while a response is streaming.',
|
|
90
|
+
category: 'Effects',
|
|
74
91
|
},
|
|
75
92
|
} satisfies Record<string, AiAssistantAnimationDef>;
|
|
76
93
|
|
|
@@ -87,3 +104,23 @@ export type AiAssistantAnimation = keyof typeof ANIMATION_DEFS;
|
|
|
87
104
|
* @internal
|
|
88
105
|
*/
|
|
89
106
|
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'];
|
|
@@ -0,0 +1,53 @@
|
|
|
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();
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|