@genesislcap/ai-assistant 15.19.0 → 15.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -18
- package/api-extractor.json +0 -11
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.compact.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.invocation-scope.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.thinking-policy.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.trace-capture.test.d.ts.map +0 -1
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts +0 -2
- package/dist/dts/components/chat-driver/chat-driver.turn-usage.test.d.ts.map +0 -1
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts +0 -2
- package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.budget.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.cost.test.d.ts.map +0 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts +0 -2
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.pin.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.styles.test.d.ts.map +0 -1
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts +0 -2
- package/dist/dts/components/settings-modal/settings-modal.template.test.d.ts.map +0 -1
- package/dist/dts/config/define-stateful-agent.test.d.ts +0 -2
- package/dist/dts/config/define-stateful-agent.test.d.ts.map +0 -1
- package/dist/dts/config/validate-providers.test.d.ts +0 -2
- package/dist/dts/config/validate-providers.test.d.ts.map +0 -1
- package/dist/dts/main/blocked-state.test.d.ts +0 -2
- package/dist/dts/main/blocked-state.test.d.ts.map +0 -1
- package/dist/dts/main/budget-meter.test.d.ts +0 -2
- package/dist/dts/main/budget-meter.test.d.ts.map +0 -1
- package/dist/dts/main/cost-session-banking.test.d.ts +0 -2
- package/dist/dts/main/cost-session-banking.test.d.ts.map +0 -1
- package/dist/dts/main/file-attachments.test.d.ts +0 -2
- package/dist/dts/main/file-attachments.test.d.ts.map +0 -1
- package/dist/dts/main/interaction-cost.test.d.ts +0 -2
- package/dist/dts/main/interaction-cost.test.d.ts.map +0 -1
- package/dist/dts/main/main.styles.test.d.ts +0 -2
- package/dist/dts/main/main.styles.test.d.ts.map +0 -1
- package/dist/dts/main/persistence-broken-sources.test.d.ts +0 -2
- package/dist/dts/main/persistence-broken-sources.test.d.ts.map +0 -1
- package/dist/dts/main/popout-interaction-gate.test.d.ts +0 -2
- package/dist/dts/main/popout-interaction-gate.test.d.ts.map +0 -1
- package/dist/dts/state/ai-assistant-slice.test.d.ts +0 -2
- package/dist/dts/state/ai-assistant-slice.test.d.ts.map +0 -1
- package/dist/dts/state/debug-event-log.test.d.ts +0 -2
- package/dist/dts/state/debug-event-log.test.d.ts.map +0 -1
- package/dist/dts/state/driver-registry.test.d.ts +0 -2
- package/dist/dts/state/driver-registry.test.d.ts.map +0 -1
- package/dist/dts/state/interaction-context.test.d.ts +0 -2
- package/dist/dts/state/interaction-context.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics-cursors.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/diagnostics.test.d.ts +0 -2
- package/dist/dts/state/persistence/diagnostics.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence-provider.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persistence.integration.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-persister.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-persister.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/session-snapshot.test.d.ts +0 -2
- package/dist/dts/state/persistence/session-snapshot.test.d.ts.map +0 -1
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts +0 -2
- package/dist/dts/state/persistence/stateful-restore.e2e.test.d.ts.map +0 -1
- package/dist/dts/utils/animation-exclusivity.test.d.ts +0 -2
- package/dist/dts/utils/animation-exclusivity.test.d.ts.map +0 -1
- package/dist/dts/utils/collect-session-models.test.d.ts +0 -2
- package/dist/dts/utils/collect-session-models.test.d.ts.map +0 -1
- package/dist/dts/utils/condense-history.test.d.ts +0 -2
- package/dist/dts/utils/condense-history.test.d.ts.map +0 -1
- package/dist/dts/utils/cost-session-history.test.d.ts +0 -2
- package/dist/dts/utils/cost-session-history.test.d.ts.map +0 -1
- package/dist/dts/utils/derive-cost-session-title.test.d.ts +0 -2
- package/dist/dts/utils/derive-cost-session-title.test.d.ts.map +0 -1
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts +0 -2
- package/dist/dts/utils/flatten-sub-agent-messages.test.d.ts.map +0 -1
- package/dist/dts/utils/history-transform.test.d.ts +0 -2
- package/dist/dts/utils/history-transform.test.d.ts.map +0 -1
- package/dist/dts/utils/message-partition.test.d.ts +0 -2
- package/dist/dts/utils/message-partition.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts +0 -2
- package/dist/dts/utils/resolve-cost-history-config.test.d.ts.map +0 -1
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts +0 -2
- package/dist/dts/utils/resolve-preference-baseline.test.d.ts.map +0 -1
- package/dist/dts/utils/strip-agent-handlers.test.d.ts +0 -2
- package/dist/dts/utils/strip-agent-handlers.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-costs.test.d.ts +0 -2
- package/dist/dts/utils/sum-costs.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-tokens.test.d.ts +0 -2
- package/dist/dts/utils/sum-tokens.test.d.ts.map +0 -1
- package/dist/dts/utils/sum-usage.test.d.ts +0 -2
- package/dist/dts/utils/sum-usage.test.d.ts.map +0 -1
- package/dist/dts/utils/usage-rows.test.d.ts +0 -2
- package/dist/dts/utils/usage-rows.test.d.ts.map +0 -1
- package/dist/esm/components/chat-driver/chat-driver.compact.test.js +0 -93
- package/dist/esm/components/chat-driver/chat-driver.invocation-scope.test.js +0 -238
- package/dist/esm/components/chat-driver/chat-driver.test.js +0 -2770
- package/dist/esm/components/chat-driver/chat-driver.thinking-policy.test.js +0 -137
- package/dist/esm/components/chat-driver/chat-driver.trace-capture.test.js +0 -200
- package/dist/esm/components/chat-driver/chat-driver.turn-usage.test.js +0 -268
- package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.test.js +0 -104
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.budget.test.js +0 -312
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.cost.test.js +0 -76
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.pin.test.js +0 -61
- package/dist/esm/components/settings-modal/settings-modal.styles.test.js +0 -80
- package/dist/esm/components/settings-modal/settings-modal.template.test.js +0 -91
- package/dist/esm/config/define-stateful-agent.test.js +0 -53
- package/dist/esm/config/validate-providers.test.js +0 -100
- package/dist/esm/main/blocked-state.test.js +0 -1065
- package/dist/esm/main/budget-meter.test.js +0 -359
- package/dist/esm/main/cost-session-banking.test.js +0 -538
- package/dist/esm/main/file-attachments.test.js +0 -164
- package/dist/esm/main/interaction-cost.test.js +0 -107
- package/dist/esm/main/main.styles.test.js +0 -161
- package/dist/esm/main/persistence-broken-sources.test.js +0 -180
- package/dist/esm/main/popout-interaction-gate.test.js +0 -54
- package/dist/esm/state/ai-assistant-slice.test.js +0 -229
- package/dist/esm/state/debug-event-log.test.js +0 -117
- package/dist/esm/state/driver-registry.test.js +0 -55
- package/dist/esm/state/interaction-context.test.js +0 -78
- package/dist/esm/state/persistence/diagnostics-cursors.test.js +0 -30
- package/dist/esm/state/persistence/diagnostics.test.js +0 -280
- package/dist/esm/state/persistence/session-persistence-provider.test.js +0 -110
- package/dist/esm/state/persistence/session-persistence.integration.test.js +0 -113
- package/dist/esm/state/persistence/session-persister.test.js +0 -613
- package/dist/esm/state/persistence/session-snapshot.test.js +0 -256
- package/dist/esm/state/persistence/stateful-restore.e2e.test.js +0 -158
- package/dist/esm/utils/animation-exclusivity.test.js +0 -52
- package/dist/esm/utils/collect-session-models.test.js +0 -53
- package/dist/esm/utils/condense-history.test.js +0 -547
- package/dist/esm/utils/cost-session-history.test.js +0 -174
- package/dist/esm/utils/derive-cost-session-title.test.js +0 -37
- package/dist/esm/utils/flatten-sub-agent-messages.test.js +0 -139
- package/dist/esm/utils/history-transform.test.js +0 -158
- package/dist/esm/utils/message-partition.test.js +0 -90
- package/dist/esm/utils/resolve-cost-history-config.test.js +0 -32
- package/dist/esm/utils/resolve-preference-baseline.test.js +0 -69
- package/dist/esm/utils/strip-agent-handlers.test.js +0 -81
- package/dist/esm/utils/sum-costs.test.js +0 -114
- package/dist/esm/utils/sum-tokens.test.js +0 -51
- package/dist/esm/utils/sum-usage.test.js +0 -164
- package/dist/esm/utils/usage-rows.test.js +0 -189
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/license.txt +0 -46
- package/sandbox/tsconfig.json +0 -8
- package/src/channel/ai-activity-bus.ts +0 -209
- package/src/channel/ai-activity-channel.ts +0 -146
- package/src/chat-driver-node.ts +0 -159
- package/src/components/activity-halo/activity-halo.ts +0 -106
- package/src/components/agent-picker/agent-picker.constants.ts +0 -2
- package/src/components/agent-picker/agent-picker.styles.ts +0 -24
- package/src/components/agent-picker/agent-picker.template.ts +0 -54
- package/src/components/agent-picker/agent-picker.ts +0 -99
- package/src/components/agent-picker/index.ts +0 -1
- package/src/components/ai-driver/ai-driver.ts +0 -166
- package/src/components/ai-driver/index.ts +0 -1
- package/src/components/chat-bubble/chat-bubble.styles.ts +0 -194
- package/src/components/chat-bubble/chat-bubble.template.ts +0 -67
- package/src/components/chat-bubble/chat-bubble.ts +0 -436
- package/src/components/chat-bubble/index.ts +0 -1
- package/src/components/chat-driver/align-event-globals.ts +0 -23
- package/src/components/chat-driver/chat-driver.compact.test.ts +0 -131
- package/src/components/chat-driver/chat-driver.invocation-scope.test.ts +0 -285
- package/src/components/chat-driver/chat-driver.test.ts +0 -3679
- package/src/components/chat-driver/chat-driver.thinking-policy.test.ts +0 -185
- package/src/components/chat-driver/chat-driver.trace-capture.test.ts +0 -251
- package/src/components/chat-driver/chat-driver.ts +0 -3925
- package/src/components/chat-driver/chat-driver.turn-usage.test.ts +0 -362
- package/src/components/chat-driver/index.ts +0 -1
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.styles.ts +0 -7
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.template.ts +0 -6
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.test.ts +0 -144
- package/src/components/chat-interaction-wrapper/chat-interaction-wrapper.ts +0 -243
- package/src/components/chat-interaction-wrapper/index.ts +0 -3
- package/src/components/chat-markdown/chat-markdown.ts +0 -89
- package/src/components/chat-markdown/index.ts +0 -1
- package/src/components/flowing-waves-indicator.ts +0 -286
- package/src/components/halo-overlay.ts +0 -184
- package/src/components/orchestrating-driver/index.ts +0 -1
- package/src/components/orchestrating-driver/orchestrating-driver.budget.test.ts +0 -438
- package/src/components/orchestrating-driver/orchestrating-driver.cost.test.ts +0 -107
- package/src/components/orchestrating-driver/orchestrating-driver.pin.test.ts +0 -96
- package/src/components/orchestrating-driver/orchestrating-driver.ts +0 -813
- package/src/components/plasma-orb-indicator.ts +0 -281
- package/src/components/popout-manager/index.ts +0 -1
- package/src/components/popout-manager/popout-manager.ts +0 -212
- package/src/components/settings-modal/index.ts +0 -2
- package/src/components/settings-modal/settings-modal.styles.test.ts +0 -94
- package/src/components/settings-modal/settings-modal.styles.ts +0 -853
- package/src/components/settings-modal/settings-modal.template.test.ts +0 -124
- package/src/components/settings-modal/settings-modal.template.ts +0 -943
- package/src/components/waves-indicator.ts +0 -241
- package/src/config/config.ts +0 -608
- package/src/config/define-stateful-agent.test.ts +0 -64
- package/src/config/define-stateful-agent.ts +0 -642
- package/src/config/fallback-agents.ts +0 -29
- package/src/config/index.ts +0 -2
- package/src/config/validate-providers.test.ts +0 -148
- package/src/config/validate-providers.ts +0 -58
- package/src/index.ts +0 -29
- package/src/main/blocked-state.test.ts +0 -1444
- package/src/main/budget-meter.test.ts +0 -477
- package/src/main/cost-session-banking.test.ts +0 -679
- package/src/main/file-attachments.test.ts +0 -215
- package/src/main/index.ts +0 -3
- package/src/main/interaction-cost.test.ts +0 -140
- package/src/main/main.styles.test.ts +0 -216
- package/src/main/main.styles.ts +0 -995
- package/src/main/main.template.ts +0 -1119
- package/src/main/main.ts +0 -5432
- package/src/main/main.types.ts +0 -225
- package/src/main/persistence-broken-sources.test.ts +0 -219
- package/src/main/popout-interaction-gate.test.ts +0 -87
- package/src/provider/ai-provider-switcher.ts +0 -40
- package/src/provider/assistant-app-settings.ts +0 -84
- package/src/state/ai-assistant-slice.test.ts +0 -336
- package/src/state/ai-assistant-slice.ts +0 -604
- package/src/state/debug-event-log.test.ts +0 -154
- package/src/state/debug-event-log.ts +0 -358
- package/src/state/driver-registry.test.ts +0 -70
- package/src/state/driver-registry.ts +0 -79
- package/src/state/interaction-context.test.ts +0 -90
- package/src/state/interaction-context.ts +0 -101
- package/src/state/persistence/build-timeline-entries.ts +0 -66
- package/src/state/persistence/diagnostics-cursors.test.ts +0 -53
- package/src/state/persistence/diagnostics-cursors.ts +0 -80
- package/src/state/persistence/diagnostics.test.ts +0 -346
- package/src/state/persistence/diagnostics.ts +0 -220
- package/src/state/persistence/index.ts +0 -10
- package/src/state/persistence/persister-registry.ts +0 -55
- package/src/state/persistence/session-persistence-provider.test.ts +0 -136
- package/src/state/persistence/session-persistence-provider.ts +0 -226
- package/src/state/persistence/session-persistence.integration.test.ts +0 -141
- package/src/state/persistence/session-persister.test.ts +0 -787
- package/src/state/persistence/session-persister.ts +0 -917
- package/src/state/persistence/session-snapshot.test.ts +0 -352
- package/src/state/persistence/session-snapshot.ts +0 -344
- package/src/state/persistence/stateful-restore.e2e.test.ts +0 -229
- package/src/state/session-store.ts +0 -56
- package/src/styles/ai-colours.ts +0 -30
- package/src/styles/menu.ts +0 -187
- package/src/styles/settings-section.ts +0 -40
- package/src/suggestions/chat-suggestions.ts +0 -186
- package/src/tags/index.ts +0 -1
- package/src/types/ai-chat-widget.ts +0 -67
- package/src/types/interaction-context.ts +0 -82
- package/src/utils/animated-panel-toggle.ts +0 -62
- package/src/utils/animation-exclusivity.test.ts +0 -72
- package/src/utils/animation-exclusivity.ts +0 -40
- package/src/utils/banked-usage-baselines.ts +0 -90
- package/src/utils/collect-session-models.test.ts +0 -82
- package/src/utils/collect-session-models.ts +0 -65
- package/src/utils/condense-history.test.ts +0 -746
- package/src/utils/condense-history.ts +0 -448
- package/src/utils/cost-session-history.test.ts +0 -226
- package/src/utils/cost-session-history.ts +0 -218
- package/src/utils/derive-cost-session-title.test.ts +0 -63
- package/src/utils/derive-cost-session-title.ts +0 -76
- package/src/utils/flatten-sub-agent-messages.test.ts +0 -163
- package/src/utils/flatten-sub-agent-messages.ts +0 -88
- package/src/utils/format-usd.ts +0 -26
- package/src/utils/history-transform.test.ts +0 -228
- package/src/utils/history-transform.ts +0 -163
- package/src/utils/index.ts +0 -3
- package/src/utils/logger.ts +0 -3
- package/src/utils/message-partition.test.ts +0 -129
- package/src/utils/message-partition.ts +0 -115
- package/src/utils/resolve-cost-history-config.test.ts +0 -38
- package/src/utils/resolve-cost-history-config.ts +0 -38
- package/src/utils/resolve-preference-baseline.test.ts +0 -84
- package/src/utils/resolve-preference-baseline.ts +0 -80
- package/src/utils/strip-agent-handlers.test.ts +0 -99
- package/src/utils/strip-agent-handlers.ts +0 -53
- package/src/utils/sum-costs.test.ts +0 -136
- package/src/utils/sum-costs.ts +0 -18
- package/src/utils/sum-tokens.test.ts +0 -93
- package/src/utils/sum-tokens.ts +0 -68
- package/src/utils/sum-usage.test.ts +0 -191
- package/src/utils/sum-usage.ts +0 -208
- package/src/utils/tool-fold.ts +0 -181
- package/src/utils/usage-rows.test.ts +0 -237
- package/src/utils/usage-rows.ts +0 -187
- package/src/utils/with-timeout.ts +0 -24
- package/tsconfig.json +0 -9
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { html, ref, repeat, slotted, ViewTemplate, when } from '@genesislcap/web-core';
|
|
2
|
-
import type { AiChatBubble } from './chat-bubble';
|
|
3
|
-
|
|
4
|
-
const genesisIconTemplate = html`
|
|
5
|
-
<svg
|
|
6
|
-
viewBox="-4 0 30 26"
|
|
7
|
-
fill="none"
|
|
8
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
-
style="width:60%;height:60%"
|
|
10
|
-
>
|
|
11
|
-
<path
|
|
12
|
-
d="M18.7702 23.0437C18.7702 22.4675 18.8526 21.8089 19.0995 21.3149C19.3465 20.7386 19.6758 20.2447 20.0874 19.833C20.4991 19.4214 20.993 19.0921 21.5693 18.8451C22.1456 18.5982 22.7219 18.5158 23.2981 18.5158H25.9326V11.6828H19.1819V14.3172C19.1819 15.5521 18.6879 16.7047 17.8647 17.5279C17.0414 18.3512 15.8888 18.8451 14.7363 18.9275H6.7507V6.99025H16.4651V0.157227H3.54C2.63442 0.157227 1.72884 0.568855 1.07023 1.22746C0.411628 1.88606 0 2.79165 0 3.77955V22.2205C0 23.1261 0.411628 24.0316 1.07023 24.7726C1.72884 25.4312 2.63442 25.8428 3.54 25.8428H18.7702V23.0437Z"
|
|
13
|
-
fill="currentColor"
|
|
14
|
-
/>
|
|
15
|
-
</svg>
|
|
16
|
-
`;
|
|
17
|
-
|
|
18
|
-
export const AiChatBubbleTemplate = (): ViewTemplate<AiChatBubble> => {
|
|
19
|
-
return html<AiChatBubble>`
|
|
20
|
-
${when(
|
|
21
|
-
(x) => !x.dialogOpen,
|
|
22
|
-
html<AiChatBubble>`
|
|
23
|
-
<div
|
|
24
|
-
class="bubble ${(x) => (x.hovered ? 'hovered' : '')}"
|
|
25
|
-
style="${(x) => `translate: ${x.bubbleLeft}px ${x.bubbleTop}px`}"
|
|
26
|
-
${ref('bubbleEl')}
|
|
27
|
-
@mousedown="${(x, c) => x.onBubbleMouseDown(c.event as MouseEvent)}"
|
|
28
|
-
@mouseenter="${(x) => x.onBubbleMouseEnter()}"
|
|
29
|
-
@mouseleave="${(x) => x.onBubbleMouseLeave()}"
|
|
30
|
-
>
|
|
31
|
-
${when(
|
|
32
|
-
(x) => !!x.imageSrc,
|
|
33
|
-
html<AiChatBubble>`
|
|
34
|
-
<img src="${(x) => x.imageSrc}" alt="AI" draggable="false" />
|
|
35
|
-
`,
|
|
36
|
-
)}
|
|
37
|
-
${when((x) => !x.imageSrc, genesisIconTemplate)}
|
|
38
|
-
<div class="pulse-ring"></div>
|
|
39
|
-
<div class="tooltip" style="${(x) => x.tooltipStyle}" ${ref('tooltipEl')}>
|
|
40
|
-
${(x) => x.title}
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
`,
|
|
44
|
-
)}
|
|
45
|
-
${repeat(
|
|
46
|
-
// Pass the component instance as the sole array item so inner bindings
|
|
47
|
-
// can use x directly. recycle: false ensures a fresh element on every
|
|
48
|
-
// open, preventing stale .closing class or animationend listeners from
|
|
49
|
-
// a previous close persisting on the reused view.
|
|
50
|
-
(x) => (x.dialogOpen ? [x] : []),
|
|
51
|
-
html<AiChatBubble>`
|
|
52
|
-
<div
|
|
53
|
-
class="dialog"
|
|
54
|
-
style="${(x) =>
|
|
55
|
-
`translate: ${x.dialogLeft}px ${x.dialogTop}px; width: ${x.dialogWidth}px; height: ${x.dialogHeight}px`}"
|
|
56
|
-
@chat-header-mousedown="${(x, c) => x.onDialogHeaderMouseDown(c.event as CustomEvent)}"
|
|
57
|
-
${ref('dialogEl')}
|
|
58
|
-
>
|
|
59
|
-
<div class="dialog-content">
|
|
60
|
-
<slot name="dialog-content" ${slotted('dialogContent')}></slot>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
`,
|
|
64
|
-
{ recycle: false },
|
|
65
|
-
)}
|
|
66
|
-
`;
|
|
67
|
-
};
|
|
@@ -1,436 +0,0 @@
|
|
|
1
|
-
import { attr, customElement, GenesisElement, html, observable } from '@genesislcap/web-core';
|
|
2
|
-
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
3
|
-
import { styles } from './chat-bubble.styles';
|
|
4
|
-
import { AiChatBubbleTemplate } from './chat-bubble.template';
|
|
5
|
-
|
|
6
|
-
/** Default dimensions of the dialog panel */
|
|
7
|
-
const DIALOG_DEFAULT_WIDTH = 400;
|
|
8
|
-
const DIALOG_DEFAULT_HEIGHT = 520;
|
|
9
|
-
|
|
10
|
-
/** Initial position: bubble starts this far from the right/bottom viewport edges */
|
|
11
|
-
const BUBBLE_INITIAL_RIGHT_OFFSET = 90;
|
|
12
|
-
|
|
13
|
-
/** Initial position: dialog starts this far from the right/bottom viewport edges */
|
|
14
|
-
const DIALOG_INITIAL_RIGHT_OFFSET = 440;
|
|
15
|
-
const DIALOG_INITIAL_BOTTOM_OFFSET = 570;
|
|
16
|
-
|
|
17
|
-
/** Minimum distance the dialog is allowed from any viewport edge */
|
|
18
|
-
const VIEWPORT_EDGE_MARGIN = 20;
|
|
19
|
-
|
|
20
|
-
/** Minimum pointer movement (px) to classify an interaction as a drag rather than a click */
|
|
21
|
-
const DRAG_THRESHOLD = 4;
|
|
22
|
-
|
|
23
|
-
/** Keeps the bubble fully inside the viewport when dragged (accounts for bubble size + border) */
|
|
24
|
-
const BUBBLE_DRAG_MARGIN = 64;
|
|
25
|
-
|
|
26
|
-
/** Horizontal offset between bubble center and dialog left edge when toggling open/close */
|
|
27
|
-
const DIALOG_BUBBLE_HORIZONTAL_OFFSET = 340;
|
|
28
|
-
|
|
29
|
-
/** How far above the bubble the dialog opens */
|
|
30
|
-
const DIALOG_OPEN_TOP_OFFSET = 470;
|
|
31
|
-
|
|
32
|
-
/** How far below the dialog top the bubble snaps when closing via the X button */
|
|
33
|
-
const BUBBLE_SNAP_BOTTOM_OFFSET = 458;
|
|
34
|
-
|
|
35
|
-
/** Fallback tooltip width used before the element has been measured in the DOM */
|
|
36
|
-
const TOOLTIP_FALLBACK_WIDTH = 155;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Floating chat bubble component that provides a draggable orb and dialog panel
|
|
40
|
-
* for embedding an AI assistant without a full-page layout.
|
|
41
|
-
*
|
|
42
|
-
* @remarks
|
|
43
|
-
* This component is optional — apps can use `foundation-ai-assistant` directly
|
|
44
|
-
* in a layout panel instead, depending on their AI interaction flow.
|
|
45
|
-
*
|
|
46
|
-
* @beta
|
|
47
|
-
*/
|
|
48
|
-
@customElement({
|
|
49
|
-
name: 'foundation-ai-chat-bubble',
|
|
50
|
-
template: html`
|
|
51
|
-
${() => AiChatBubbleTemplate()}
|
|
52
|
-
`,
|
|
53
|
-
styles,
|
|
54
|
-
})
|
|
55
|
-
export class AiChatBubble extends GenesisElement {
|
|
56
|
-
@attr({ attribute: 'title' }) title: string = 'AI Assistant';
|
|
57
|
-
@attr({ attribute: 'image-src' }) imageSrc?: string;
|
|
58
|
-
@attr({ attribute: 'design-system-prefix' }) designSystemPrefix: string = 'rapid';
|
|
59
|
-
|
|
60
|
-
/** @internal */
|
|
61
|
-
@observable dialogContent: HTMLElement[] = [];
|
|
62
|
-
/** @internal */
|
|
63
|
-
@observable tooltipMeasuredWidth: number = 0;
|
|
64
|
-
/** Current left position of the bubble orb (px from viewport left). @internal */
|
|
65
|
-
@observable bubbleLeft: number = 0;
|
|
66
|
-
/** Current top position of the bubble orb (px from viewport top). @internal */
|
|
67
|
-
@observable bubbleTop: number = 0;
|
|
68
|
-
/** Current left position of the dialog panel (px from viewport left). @internal */
|
|
69
|
-
@observable dialogLeft: number = 0;
|
|
70
|
-
/** Current top position of the dialog panel (px from viewport top). @internal */
|
|
71
|
-
@observable dialogTop: number = 0;
|
|
72
|
-
/** Current rendered width of the dialog panel (px). @internal */
|
|
73
|
-
@observable dialogWidth: number = DIALOG_DEFAULT_WIDTH;
|
|
74
|
-
/** Current rendered height of the dialog panel (px). @internal */
|
|
75
|
-
@observable dialogHeight: number = DIALOG_DEFAULT_HEIGHT;
|
|
76
|
-
/** Whether the dialog panel is currently open. @internal */
|
|
77
|
-
@observable dialogOpen: boolean = false;
|
|
78
|
-
/** Whether the bubble orb is currently hovered. @internal */
|
|
79
|
-
@observable hovered: boolean = false;
|
|
80
|
-
|
|
81
|
-
/** @internal */
|
|
82
|
-
bubbleEl?: HTMLElement;
|
|
83
|
-
/** @internal */
|
|
84
|
-
tooltipEl?: HTMLElement;
|
|
85
|
-
/** @internal */
|
|
86
|
-
dialogEl?: HTMLElement;
|
|
87
|
-
|
|
88
|
-
private unsubBus?: () => void;
|
|
89
|
-
private injectedCloseBtn?: Element;
|
|
90
|
-
private resizeObserver?: ResizeObserver;
|
|
91
|
-
private dragging: boolean = false;
|
|
92
|
-
private dragTarget: 'bubble' | 'dialog' | null = null;
|
|
93
|
-
private startMouseX: number = 0;
|
|
94
|
-
private startMouseY: number = 0;
|
|
95
|
-
private startElemLeft: number = 0;
|
|
96
|
-
private startElemTop: number = 0;
|
|
97
|
-
private hasDragged: boolean = false;
|
|
98
|
-
private latestMouseX: number = 0;
|
|
99
|
-
private latestMouseY: number = 0;
|
|
100
|
-
private rafHandle?: number;
|
|
101
|
-
private closeAbort?: AbortController;
|
|
102
|
-
|
|
103
|
-
connectedCallback() {
|
|
104
|
-
super.connectedCallback();
|
|
105
|
-
this.bubbleLeft = window.innerWidth - BUBBLE_INITIAL_RIGHT_OFFSET;
|
|
106
|
-
this.bubbleTop = window.innerHeight - 100;
|
|
107
|
-
this.dialogLeft = Math.max(
|
|
108
|
-
VIEWPORT_EDGE_MARGIN,
|
|
109
|
-
window.innerWidth - DIALOG_INITIAL_RIGHT_OFFSET,
|
|
110
|
-
);
|
|
111
|
-
this.dialogTop = Math.max(
|
|
112
|
-
VIEWPORT_EDGE_MARGIN,
|
|
113
|
-
window.innerHeight - DIALOG_INITIAL_BOTTOM_OFFSET,
|
|
114
|
-
);
|
|
115
|
-
document.addEventListener('mousemove', this.onDocMouseMove);
|
|
116
|
-
document.addEventListener('mouseup', this.onDocMouseUp);
|
|
117
|
-
const unsubPopout = agenticActivityBus.subscribe('chat-popout', () => {
|
|
118
|
-
this.startCloseDialog();
|
|
119
|
-
this.hidden = true;
|
|
120
|
-
});
|
|
121
|
-
const unsubPopin = agenticActivityBus.subscribe('chat-popin', () => {
|
|
122
|
-
this.cancelClose();
|
|
123
|
-
this.hidden = false;
|
|
124
|
-
this.dialogOpen = true;
|
|
125
|
-
});
|
|
126
|
-
this.unsubBus = () => {
|
|
127
|
-
unsubPopout();
|
|
128
|
-
unsubPopin();
|
|
129
|
-
};
|
|
130
|
-
// Measure tooltip width once after first render. The bubble is visible on connect
|
|
131
|
-
// (dialogOpen starts false), so the element is in the DOM by the time rAF fires.
|
|
132
|
-
// This value persists through open/close cycles — no re-measurement needed unless
|
|
133
|
-
// the title attribute changes.
|
|
134
|
-
requestAnimationFrame(() => {
|
|
135
|
-
if (this.tooltipEl) this.tooltipMeasuredWidth = this.tooltipEl.offsetWidth;
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
onBubbleMouseEnter() {
|
|
140
|
-
this.hovered = true;
|
|
141
|
-
// Ensure we have a real measured width even on first-ever hover.
|
|
142
|
-
// The ref may not be available yet during connectedCallback's first rAF.
|
|
143
|
-
if (!this.tooltipMeasuredWidth) {
|
|
144
|
-
requestAnimationFrame(() => {
|
|
145
|
-
if (this.tooltipEl) this.tooltipMeasuredWidth = this.tooltipEl.offsetWidth;
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
onBubbleMouseLeave() {
|
|
151
|
-
this.hovered = false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
disconnectedCallback() {
|
|
155
|
-
super.disconnectedCallback();
|
|
156
|
-
document.removeEventListener('mousemove', this.onDocMouseMove);
|
|
157
|
-
document.removeEventListener('mouseup', this.onDocMouseUp);
|
|
158
|
-
this.unsubBus?.();
|
|
159
|
-
this.unsubBus = undefined;
|
|
160
|
-
this.injectedCloseBtn?.removeEventListener('click', this.onCloseBtnClick);
|
|
161
|
-
this.injectedCloseBtn?.remove();
|
|
162
|
-
this.injectedCloseBtn = undefined;
|
|
163
|
-
this.resizeObserver?.disconnect();
|
|
164
|
-
this.resizeObserver = undefined;
|
|
165
|
-
if (this.rafHandle !== undefined) {
|
|
166
|
-
cancelAnimationFrame(this.rafHandle);
|
|
167
|
-
this.rafHandle = undefined;
|
|
168
|
-
}
|
|
169
|
-
this.closeAbort?.abort();
|
|
170
|
-
this.closeAbort = undefined;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
dialogOpenChanged(_: boolean, next: boolean) {
|
|
174
|
-
if (next) {
|
|
175
|
-
// Ensure we never observe a stale element from a prior open.
|
|
176
|
-
this.resizeObserver?.disconnect();
|
|
177
|
-
this.resizeObserver = undefined;
|
|
178
|
-
requestAnimationFrame(() => {
|
|
179
|
-
const el = this.dialogEl;
|
|
180
|
-
if (!el || !el.isConnected) return;
|
|
181
|
-
|
|
182
|
-
// Immediately sync actual size on open. This prevents cases where a stale
|
|
183
|
-
// ResizeObserver entry (or a not-yet-laid-out element) would temporarily
|
|
184
|
-
// drive the observables to 0 and shrink the dialog.
|
|
185
|
-
const w = el.offsetWidth;
|
|
186
|
-
const h = el.offsetHeight;
|
|
187
|
-
if (w > 0) this.dialogWidth = w;
|
|
188
|
-
if (h > 0) this.dialogHeight = h;
|
|
189
|
-
|
|
190
|
-
this.resizeObserver = new ResizeObserver((entries) => {
|
|
191
|
-
const entry = entries[0];
|
|
192
|
-
if (!entry) return;
|
|
193
|
-
const borderBox = (entry as ResizeObserverEntry).borderBoxSize?.[0] as
|
|
194
|
-
| { inlineSize: number; blockSize: number }
|
|
195
|
-
| undefined;
|
|
196
|
-
const nextW = Math.round(borderBox?.inlineSize ?? entry.contentRect.width);
|
|
197
|
-
const nextH = Math.round(borderBox?.blockSize ?? entry.contentRect.height);
|
|
198
|
-
if (nextW > 0) this.dialogWidth = nextW;
|
|
199
|
-
if (nextH > 0) this.dialogHeight = nextH;
|
|
200
|
-
});
|
|
201
|
-
this.resizeObserver.observe(el);
|
|
202
|
-
});
|
|
203
|
-
} else {
|
|
204
|
-
this.resizeObserver?.disconnect();
|
|
205
|
-
this.resizeObserver = undefined;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
dialogContentChanged(_: HTMLElement[], next: HTMLElement[]) {
|
|
210
|
-
if (!next?.length || this.injectedCloseBtn) return;
|
|
211
|
-
for (const el of next) {
|
|
212
|
-
const assistant =
|
|
213
|
-
el.tagName.toLowerCase() === 'foundation-ai-assistant'
|
|
214
|
-
? el
|
|
215
|
-
: el.shadowRoot?.querySelector('foundation-ai-assistant');
|
|
216
|
-
if (assistant) {
|
|
217
|
-
this.injectCloseButton(assistant);
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private injectCloseButton(assistant: Element) {
|
|
224
|
-
const prefix = this.designSystemPrefix;
|
|
225
|
-
const btn = document.createElement(`${prefix}-button`);
|
|
226
|
-
btn.setAttribute('slot', 'header-actions-extra');
|
|
227
|
-
btn.setAttribute('appearance', 'stealth');
|
|
228
|
-
btn.setAttribute('title', 'Close');
|
|
229
|
-
const icon = document.createElement(`${prefix}-icon`);
|
|
230
|
-
icon.setAttribute('name', 'times');
|
|
231
|
-
btn.appendChild(icon);
|
|
232
|
-
btn.addEventListener('click', this.onCloseBtnClick);
|
|
233
|
-
assistant.appendChild(btn);
|
|
234
|
-
this.injectedCloseBtn = btn;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
onBubbleMouseDown(e: MouseEvent) {
|
|
238
|
-
e.preventDefault();
|
|
239
|
-
this.dragging = true;
|
|
240
|
-
this.dragTarget = 'bubble';
|
|
241
|
-
this.startMouseX = e.clientX;
|
|
242
|
-
this.startMouseY = e.clientY;
|
|
243
|
-
this.latestMouseX = e.clientX;
|
|
244
|
-
this.latestMouseY = e.clientY;
|
|
245
|
-
this.startElemLeft = this.bubbleLeft;
|
|
246
|
-
this.startElemTop = this.bubbleTop;
|
|
247
|
-
this.hasDragged = false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
onDialogHeaderMouseDown(e: CustomEvent<{ clientX: number; clientY: number }>) {
|
|
251
|
-
// Sync actual rendered size before dragging — the browser's CSS resize handle
|
|
252
|
-
// updates the element's size but our observables may not have caught up yet.
|
|
253
|
-
if (this.dialogEl) {
|
|
254
|
-
this.dialogWidth = this.dialogEl.offsetWidth;
|
|
255
|
-
this.dialogHeight = this.dialogEl.offsetHeight;
|
|
256
|
-
}
|
|
257
|
-
this.dragging = true;
|
|
258
|
-
this.dragTarget = 'dialog';
|
|
259
|
-
this.startMouseX = e.detail.clientX;
|
|
260
|
-
this.startMouseY = e.detail.clientY;
|
|
261
|
-
this.latestMouseX = e.detail.clientX;
|
|
262
|
-
this.latestMouseY = e.detail.clientY;
|
|
263
|
-
this.startElemLeft = this.dialogLeft;
|
|
264
|
-
this.startElemTop = this.dialogTop;
|
|
265
|
-
this.hasDragged = false;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private readonly onCloseBtnClick = () => this.onCloseDialog();
|
|
269
|
-
|
|
270
|
-
// Cancels an in-flight close animation and restores the dialog to its open state.
|
|
271
|
-
// Needed for the chat-popin case: startCloseDialog() may have been called (adding
|
|
272
|
-
// .closing and an animationend listener) but the host was hidden before the animation
|
|
273
|
-
// finished. When the host reappears via chat-popin, we cancel the pending close so
|
|
274
|
-
// the dialog stays open rather than disappearing once animationend eventually fires.
|
|
275
|
-
private cancelClose() {
|
|
276
|
-
this.closeAbort?.abort();
|
|
277
|
-
this.closeAbort = undefined;
|
|
278
|
-
this.dialogEl?.classList.remove('closing');
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Plays the closing animation before removing the dialog from the DOM.
|
|
282
|
-
// dialogOpen must stay true while the animation runs — setting it to false
|
|
283
|
-
// immediately would cause the repeat directive to remove the element before
|
|
284
|
-
// the animation has a chance to play. We only set dialogOpen = false inside
|
|
285
|
-
// the animationend callback, at which point FAST removes the element cleanly.
|
|
286
|
-
// If the user prefers reduced motion we skip the animation and close immediately.
|
|
287
|
-
private startCloseDialog() {
|
|
288
|
-
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
289
|
-
if (!this.dialogEl || reducedMotion) {
|
|
290
|
-
this.dialogOpen = false;
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
this.closeAbort = new AbortController();
|
|
294
|
-
this.dialogEl.classList.add('closing');
|
|
295
|
-
this.dialogEl.addEventListener(
|
|
296
|
-
'animationend',
|
|
297
|
-
() => {
|
|
298
|
-
this.dialogOpen = false;
|
|
299
|
-
},
|
|
300
|
-
{
|
|
301
|
-
once: true,
|
|
302
|
-
signal: this.closeAbort.signal,
|
|
303
|
-
},
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Capture the latest pointer coords and schedule one rAF per display frame.
|
|
308
|
-
// Pointer events can fire far faster than the display refreshes (up to 1000Hz on
|
|
309
|
-
// gaming mice), so we only store the coords here and let applyDragFrame do the
|
|
310
|
-
// actual work. If a frame is already scheduled we just overwrite the coords —
|
|
311
|
-
// the pending frame will pick up the freshest values when it runs.
|
|
312
|
-
private onDocMouseMove = (e: MouseEvent) => {
|
|
313
|
-
if (!this.dragging) return;
|
|
314
|
-
this.latestMouseX = e.clientX;
|
|
315
|
-
this.latestMouseY = e.clientY;
|
|
316
|
-
if (this.rafHandle === undefined) {
|
|
317
|
-
this.rafHandle = requestAnimationFrame(this.applyDragFrame);
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
// Runs once per display frame during a drag. Writes the new position directly to
|
|
322
|
-
// the element's style.translate rather than updating FAST observables, which keeps
|
|
323
|
-
// the compositor thread doing the work instead of triggering FAST's reactivity
|
|
324
|
-
// pipeline on every frame. This means the element's visual position is temporarily
|
|
325
|
-
// ahead of the observables — they are only synced back on mouseup (see onDocMouseUp).
|
|
326
|
-
// Note: style.translate is an independent CSS property and does not conflict with
|
|
327
|
-
// the transform used for the bubble's hover scale or the dialog's open animation.
|
|
328
|
-
private applyDragFrame = () => {
|
|
329
|
-
this.rafHandle = undefined;
|
|
330
|
-
const dx = this.latestMouseX - this.startMouseX;
|
|
331
|
-
const dy = this.latestMouseY - this.startMouseY;
|
|
332
|
-
if (Math.abs(dx) > DRAG_THRESHOLD || Math.abs(dy) > DRAG_THRESHOLD) this.hasDragged = true;
|
|
333
|
-
if (this.dragTarget === 'bubble') {
|
|
334
|
-
const left = Math.max(
|
|
335
|
-
0,
|
|
336
|
-
Math.min(window.innerWidth - BUBBLE_DRAG_MARGIN, this.startElemLeft + dx),
|
|
337
|
-
);
|
|
338
|
-
const top = Math.max(
|
|
339
|
-
0,
|
|
340
|
-
Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.startElemTop + dy),
|
|
341
|
-
);
|
|
342
|
-
if (this.bubbleEl) this.bubbleEl.style.translate = `${left}px ${top}px`;
|
|
343
|
-
} else if (this.dragTarget === 'dialog') {
|
|
344
|
-
const left = Math.max(
|
|
345
|
-
0,
|
|
346
|
-
Math.min(window.innerWidth - DIALOG_DEFAULT_WIDTH, this.startElemLeft + dx),
|
|
347
|
-
);
|
|
348
|
-
const top = Math.max(0, Math.min(window.innerHeight - 100, this.startElemTop + dy));
|
|
349
|
-
if (this.dialogEl) this.dialogEl.style.translate = `${left}px ${top}px`;
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
// Commits the final drag position back to the FAST observables, bringing them back
|
|
354
|
-
// in sync with what the user sees. This triggers a single FAST re-render which writes
|
|
355
|
-
// the same value already on the element — no visible jump. Any pending rAF is
|
|
356
|
-
// cancelled first; its work is redundant since we recompute from latestMouseX/Y here.
|
|
357
|
-
private onDocMouseUp = () => {
|
|
358
|
-
if (this.rafHandle !== undefined) {
|
|
359
|
-
cancelAnimationFrame(this.rafHandle);
|
|
360
|
-
this.rafHandle = undefined;
|
|
361
|
-
}
|
|
362
|
-
if (this.dragging) {
|
|
363
|
-
const dx = this.latestMouseX - this.startMouseX;
|
|
364
|
-
const dy = this.latestMouseY - this.startMouseY;
|
|
365
|
-
if (this.dragTarget === 'bubble') {
|
|
366
|
-
this.bubbleLeft = Math.max(
|
|
367
|
-
0,
|
|
368
|
-
Math.min(window.innerWidth - BUBBLE_DRAG_MARGIN, this.startElemLeft + dx),
|
|
369
|
-
);
|
|
370
|
-
this.bubbleTop = Math.max(
|
|
371
|
-
0,
|
|
372
|
-
Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.startElemTop + dy),
|
|
373
|
-
);
|
|
374
|
-
if (!this.hasDragged) {
|
|
375
|
-
if (this.dialogOpen) {
|
|
376
|
-
this.startCloseDialog();
|
|
377
|
-
} else {
|
|
378
|
-
this.dialogLeft = Math.max(
|
|
379
|
-
VIEWPORT_EDGE_MARGIN,
|
|
380
|
-
this.bubbleLeft - DIALOG_BUBBLE_HORIZONTAL_OFFSET,
|
|
381
|
-
);
|
|
382
|
-
this.dialogTop = Math.max(
|
|
383
|
-
VIEWPORT_EDGE_MARGIN,
|
|
384
|
-
this.bubbleTop - DIALOG_OPEN_TOP_OFFSET,
|
|
385
|
-
);
|
|
386
|
-
this.dialogOpen = true;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
} else if (this.dragTarget === 'dialog') {
|
|
390
|
-
this.dialogLeft = Math.max(
|
|
391
|
-
0,
|
|
392
|
-
Math.min(window.innerWidth - DIALOG_DEFAULT_WIDTH, this.startElemLeft + dx),
|
|
393
|
-
);
|
|
394
|
-
this.dialogTop = Math.max(0, Math.min(window.innerHeight - 100, this.startElemTop + dy));
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
this.dragging = false;
|
|
398
|
-
this.dragTarget = null;
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
onPopout() {
|
|
402
|
-
this.startCloseDialog();
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
onCloseDialog() {
|
|
406
|
-
// Snap the bubble orb to the bottom-right of wherever the dialog currently is,
|
|
407
|
-
// so it reappears near where the user last positioned the window.
|
|
408
|
-
this.bubbleLeft = Math.max(
|
|
409
|
-
0,
|
|
410
|
-
Math.min(
|
|
411
|
-
window.innerWidth - BUBBLE_DRAG_MARGIN,
|
|
412
|
-
this.dialogLeft + DIALOG_BUBBLE_HORIZONTAL_OFFSET,
|
|
413
|
-
),
|
|
414
|
-
);
|
|
415
|
-
this.bubbleTop = Math.max(
|
|
416
|
-
0,
|
|
417
|
-
Math.min(window.innerHeight - BUBBLE_DRAG_MARGIN, this.dialogTop + BUBBLE_SNAP_BOTTOM_OFFSET),
|
|
418
|
-
);
|
|
419
|
-
this.startCloseDialog();
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
get tooltipStyle(): string {
|
|
423
|
-
const tooltipWidth = this.tooltipMeasuredWidth || TOOLTIP_FALLBACK_WIDTH;
|
|
424
|
-
const bubbleWidth = 62; // Fixed in CSS
|
|
425
|
-
const margin = 8;
|
|
426
|
-
const naturalLeft = (bubbleWidth - tooltipWidth) / 2;
|
|
427
|
-
const vpLeft = this.bubbleLeft + naturalLeft;
|
|
428
|
-
const vpRight = vpLeft + tooltipWidth;
|
|
429
|
-
let shift = 0;
|
|
430
|
-
if (vpLeft < margin) shift = margin - vpLeft;
|
|
431
|
-
else if (vpRight > window.innerWidth - margin) shift = window.innerWidth - margin - vpRight;
|
|
432
|
-
const tooltipLeft = naturalLeft + shift;
|
|
433
|
-
const arrowLeft = bubbleWidth / 2 - tooltipLeft;
|
|
434
|
-
return `left: ${tooltipLeft}px; --arrow-left: ${arrowLeft}px`;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './chat-bubble';
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test-only side effect: align the global `EventTarget` with jsdom's before any
|
|
3
|
-
* module that `extends EventTarget` is evaluated.
|
|
4
|
-
*
|
|
5
|
-
* The node test runner's jsdom setup installs `globalThis.CustomEvent` from
|
|
6
|
-
* jsdom but leaves `globalThis.EventTarget` as Node's native class. A class that
|
|
7
|
-
* `extends EventTarget` (e.g. {@link ChatDriver}) then inherits Node's native
|
|
8
|
-
* `dispatchEvent`, which rejects the jsdom `CustomEvent` instances it is handed
|
|
9
|
-
* ("The 'event' argument must be an instance of Event. Received an instance of
|
|
10
|
-
* CustomEvent"). Pointing `EventTarget` at jsdom's keeps the whole event family
|
|
11
|
-
* in one realm.
|
|
12
|
-
*
|
|
13
|
-
* No-op in a real browser, where `window.EventTarget === globalThis.EventTarget`
|
|
14
|
-
* already. Import this BEFORE importing anything that subclasses `EventTarget`.
|
|
15
|
-
*/
|
|
16
|
-
const jsdomWindow = (globalThis as { window?: { EventTarget?: typeof EventTarget } }).window;
|
|
17
|
-
if (jsdomWindow?.EventTarget && globalThis.EventTarget !== jsdomWindow.EventTarget) {
|
|
18
|
-
Object.defineProperty(globalThis, 'EventTarget', {
|
|
19
|
-
value: jsdomWindow.EventTarget,
|
|
20
|
-
configurable: true,
|
|
21
|
-
writable: true,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { AIProviderRegistry, ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
|
-
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
// Side-effect import — MUST come before `./chat-driver` so the driver subclasses
|
|
4
|
-
// jsdom's EventTarget rather than Node's native one (CustomEvent dispatch then
|
|
5
|
-
// works). Mirrors `chat-driver.test.ts`.
|
|
6
|
-
import './align-event-globals';
|
|
7
|
-
import { ChatDriver } from './chat-driver';
|
|
8
|
-
|
|
9
|
-
// GENC-1351 §5.7 — driver-level compaction: the atomic, destructive replace.
|
|
10
|
-
// A minimal fake registry stands in for a provider, so no network is involved.
|
|
11
|
-
|
|
12
|
-
const asMsg = (o: Record<string, unknown>): ChatMessage => o as unknown as ChatMessage;
|
|
13
|
-
|
|
14
|
-
/** A registry whose default provider summarizes via `prompt` (undefined = unsupported). */
|
|
15
|
-
function registryWith(prompt: ((m: string) => Promise<string>) | undefined): AIProviderRegistry {
|
|
16
|
-
const provider = { prompt, getStatus: async () => ({ model: 'test-model' }) };
|
|
17
|
-
return { default: () => provider } as unknown as AIProviderRegistry;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** 10 messages alternating user/assistant (user at even indices); last carries usage. */
|
|
21
|
-
function tenTurnHistory(): ChatMessage[] {
|
|
22
|
-
return Array.from({ length: 10 }, (_, i) =>
|
|
23
|
-
asMsg({
|
|
24
|
-
role: i % 2 === 0 ? 'user' : 'assistant',
|
|
25
|
-
content: `m${i}`,
|
|
26
|
-
timestamp: `2026-01-01T00:00:${String(i).padStart(2, '0')}.000Z`,
|
|
27
|
-
...(i === 9 ? { inputTokens: 1234 } : {}),
|
|
28
|
-
}),
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const Suite = createLogicSuite('chat-driver-compact');
|
|
33
|
-
|
|
34
|
-
Suite('compact() destructively replaces older turns with one head summary + metadata', async () => {
|
|
35
|
-
const driver = new ChatDriver(registryWith(async () => 'THE SUMMARY'));
|
|
36
|
-
driver.loadHistory(tenTurnHistory());
|
|
37
|
-
const before = driver.getHistory().length;
|
|
38
|
-
|
|
39
|
-
const summary = await driver.compact();
|
|
40
|
-
assert.ok(summary, 'returns the created summary message');
|
|
41
|
-
assert.is(summary!.role, 'compacted-summary');
|
|
42
|
-
assert.is(summary!.content, 'THE SUMMARY');
|
|
43
|
-
assert.is(summary!.compaction?.tokensBefore, 1234, 'captures pre-compaction input tokens');
|
|
44
|
-
assert.is(summary!.compaction?.model, 'test-model', 'records the summarizing model');
|
|
45
|
-
|
|
46
|
-
const after = driver.getHistory();
|
|
47
|
-
assert.ok(after.length < before, 'history shrank (destructive)');
|
|
48
|
-
assert.is(after[0].role, 'compacted-summary', 'summary sits at the head');
|
|
49
|
-
assert.is(
|
|
50
|
-
after[0].timestamp,
|
|
51
|
-
'2026-01-01T00:00:00.000Z',
|
|
52
|
-
'summary inherits the covered region start → sorts to the head in time-ordered views',
|
|
53
|
-
);
|
|
54
|
-
assert.is(after[0].compaction?.compactedCount, before - (after.length - 1));
|
|
55
|
-
assert.ok(
|
|
56
|
-
after.slice(1).every((m) => m.role !== 'compacted-summary'),
|
|
57
|
-
'exactly one summary — no marker chain',
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
Suite('compact() is atomic on failure — no splice, surfaces a "sorry" message', async () => {
|
|
62
|
-
const driver = new ChatDriver(
|
|
63
|
-
registryWith(async () => {
|
|
64
|
-
throw new Error('network');
|
|
65
|
-
}),
|
|
66
|
-
);
|
|
67
|
-
const original = tenTurnHistory();
|
|
68
|
-
driver.loadHistory(original);
|
|
69
|
-
|
|
70
|
-
const result = await driver.compact();
|
|
71
|
-
assert.is(result, null, 'returns null on failure');
|
|
72
|
-
const after = driver.getHistory();
|
|
73
|
-
assert.ok(
|
|
74
|
-
after.every((m) => m.role !== 'compacted-summary'),
|
|
75
|
-
'nothing compacted',
|
|
76
|
-
);
|
|
77
|
-
// The covered turns are untouched (atomic); only a brief apology is appended so
|
|
78
|
-
// the user knows it failed (GENC-1351 follow-up — surfaced like a turn error).
|
|
79
|
-
assert.is(after.length, original.length + 1, 'only an apology appended');
|
|
80
|
-
assert.is(after[after.length - 1].role, 'assistant');
|
|
81
|
-
assert.ok(/sorry/i.test(after[after.length - 1].content), 'apology is an agent message');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
Suite('compact() rolls a prior summary into a single fresh one', async () => {
|
|
85
|
-
const driver = new ChatDriver(registryWith(async () => 'COMBINED'));
|
|
86
|
-
driver.loadHistory([
|
|
87
|
-
asMsg({ role: 'compacted-summary', content: 'old', compaction: { compactedCount: 5 } }),
|
|
88
|
-
...tenTurnHistory(),
|
|
89
|
-
]);
|
|
90
|
-
const summary = await driver.compact();
|
|
91
|
-
assert.ok(summary);
|
|
92
|
-
const after = driver.getHistory();
|
|
93
|
-
assert.is(
|
|
94
|
-
after.filter((m) => m.role === 'compacted-summary').length,
|
|
95
|
-
1,
|
|
96
|
-
'still exactly one summary after re-compaction',
|
|
97
|
-
);
|
|
98
|
-
assert.is(after[0].content, 'COMBINED');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
Suite('compact() no-ops when there is nothing worth compacting', async () => {
|
|
102
|
-
const driver = new ChatDriver(registryWith(async () => 'unused'));
|
|
103
|
-
driver.loadHistory([
|
|
104
|
-
asMsg({ role: 'user', content: 'hi' }),
|
|
105
|
-
asMsg({ role: 'assistant', content: 'yo' }),
|
|
106
|
-
]);
|
|
107
|
-
assert.is(await driver.compact(), null);
|
|
108
|
-
assert.is(driver.getHistory().length, 2, 'short history untouched');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
Suite('compact() no-ops when the default provider cannot summarize', async () => {
|
|
112
|
-
const driver = new ChatDriver(registryWith(undefined));
|
|
113
|
-
driver.loadHistory(tenTurnHistory());
|
|
114
|
-
assert.is(await driver.compact(), null);
|
|
115
|
-
assert.is(driver.getHistory().length, 10, 'history untouched without prompt support');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
Suite("canCompact reflects compact()-would-act, keyed on the driver's own history", () => {
|
|
119
|
-
const rich = new ChatDriver(registryWith(async () => 'x'));
|
|
120
|
-
rich.loadHistory(tenTurnHistory());
|
|
121
|
-
assert.is(rich.canCompact(), true, 'enough history → gate open');
|
|
122
|
-
|
|
123
|
-
const thin = new ChatDriver(registryWith(async () => 'x'));
|
|
124
|
-
thin.loadHistory([
|
|
125
|
-
asMsg({ role: 'user', content: 'hi' }),
|
|
126
|
-
asMsg({ role: 'assistant', content: 'yo' }),
|
|
127
|
-
]);
|
|
128
|
-
assert.is(thin.canCompact(), false, 'too short → gate closed (same source compact() uses)');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
Suite.run();
|