@memberjunction/ng-conversations 6.2.0-edge.6 → 6.2.0-edge.7
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/lib/components/collection/collection-view.component.d.ts.map +1 -1
- package/dist/lib/components/collection/collection-view.component.js +1 -0
- package/dist/lib/components/collection/collection-view.component.js.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +174 -2
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.js +1266 -443
- package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
- package/dist/lib/components/conversation/conversation-mode-picker.component.js +2 -2
- package/dist/lib/components/conversation/conversation-mode-picker.component.js.map +1 -1
- package/dist/lib/components/conversation/conversation-remote-browser-controller.d.ts +109 -0
- package/dist/lib/components/conversation/conversation-remote-browser-controller.d.ts.map +1 -0
- package/dist/lib/components/conversation/conversation-remote-browser-controller.js +298 -0
- package/dist/lib/components/conversation/conversation-remote-browser-controller.js.map +1 -0
- package/dist/lib/components/message/agent-handoff-card.component.d.ts +102 -0
- package/dist/lib/components/message/agent-handoff-card.component.d.ts.map +1 -0
- package/dist/lib/components/message/agent-handoff-card.component.js +439 -0
- package/dist/lib/components/message/agent-handoff-card.component.js.map +1 -0
- package/dist/lib/components/message/message-input.component.d.ts +80 -53
- package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-input.component.js +381 -277
- package/dist/lib/components/message/message-input.component.js.map +1 -1
- package/dist/lib/components/message/message-item.component.d.ts +18 -1
- package/dist/lib/components/message/message-item.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-item.component.js +548 -426
- package/dist/lib/components/message/message-item.component.js.map +1 -1
- package/dist/lib/components/message/message-list.component.d.ts +8 -1
- package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-list.component.js +40 -2
- package/dist/lib/components/message/message-list.component.js.map +1 -1
- package/dist/lib/components/realtime/interactive-component/run-view-component-artifact-source.js +1 -1
- package/dist/lib/components/realtime/interactive-component/run-view-component-artifact-source.js.map +1 -1
- package/dist/lib/components/realtime/media/media-channel-state.d.ts +5 -0
- package/dist/lib/components/realtime/media/media-channel-state.d.ts.map +1 -1
- package/dist/lib/components/realtime/media/media-channel-state.js +27 -1
- package/dist/lib/components/realtime/media/media-channel-state.js.map +1 -1
- package/dist/lib/components/realtime/media/media-channel.d.ts.map +1 -1
- package/dist/lib/components/realtime/media/media-channel.js +7 -2
- package/dist/lib/components/realtime/media/media-channel.js.map +1 -1
- package/dist/lib/components/realtime/media/realtime-media-surface.component.d.ts +19 -2
- package/dist/lib/components/realtime/media/realtime-media-surface.component.d.ts.map +1 -1
- package/dist/lib/components/realtime/media/realtime-media-surface.component.js +59 -10
- package/dist/lib/components/realtime/media/realtime-media-surface.component.js.map +1 -1
- package/dist/lib/components/realtime/realtime-agent-banner.component.d.ts +17 -6
- package/dist/lib/components/realtime/realtime-agent-banner.component.d.ts.map +1 -1
- package/dist/lib/components/realtime/realtime-agent-banner.component.js +61 -27
- package/dist/lib/components/realtime/realtime-agent-banner.component.js.map +1 -1
- package/dist/lib/components/realtime/realtime-composer.component.d.ts +26 -3
- package/dist/lib/components/realtime/realtime-composer.component.d.ts.map +1 -1
- package/dist/lib/components/realtime/realtime-composer.component.js +196 -102
- package/dist/lib/components/realtime/realtime-composer.component.js.map +1 -1
- package/dist/lib/components/realtime/realtime-session-overlay.component.d.ts +53 -15
- package/dist/lib/components/realtime/realtime-session-overlay.component.d.ts.map +1 -1
- package/dist/lib/components/realtime/realtime-session-overlay.component.js +125 -51
- package/dist/lib/components/realtime/realtime-session-overlay.component.js.map +1 -1
- package/dist/lib/components/realtime/realtime-ui-config.d.ts +9 -12
- package/dist/lib/components/realtime/realtime-ui-config.d.ts.map +1 -1
- package/dist/lib/components/realtime/realtime-ui-config.js +38 -18
- package/dist/lib/components/realtime/realtime-ui-config.js.map +1 -1
- package/dist/lib/components/realtime/remote-browser/remote-browser-channel.d.ts.map +1 -1
- package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js +27 -4
- package/dist/lib/components/realtime/remote-browser/remote-browser-channel.js.map +1 -1
- package/dist/lib/components/realtime/remote-browser/remote-browser-tools.d.ts +8 -2
- package/dist/lib/components/realtime/remote-browser/remote-browser-tools.d.ts.map +1 -1
- package/dist/lib/components/realtime/remote-browser/remote-browser-tools.js +51 -10
- package/dist/lib/components/realtime/remote-browser/remote-browser-tools.js.map +1 -1
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.d.ts +46 -0
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.d.ts.map +1 -0
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.js +64 -0
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel-config.js.map +1 -0
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel.d.ts +131 -9
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel.d.ts.map +1 -1
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel.js +347 -38
- package/dist/lib/components/realtime/whiteboard/whiteboard-channel.js.map +1 -1
- package/dist/lib/conversations.module.d.ts +2 -1
- package/dist/lib/conversations.module.d.ts.map +1 -1
- package/dist/lib/conversations.module.js +9 -4
- package/dist/lib/conversations.module.js.map +1 -1
- package/dist/lib/events/chat-events.d.ts +5 -4
- package/dist/lib/events/chat-events.d.ts.map +1 -1
- package/dist/lib/events/chat-events.js +2 -2
- package/dist/lib/events/chat-events.js.map +1 -1
- package/dist/lib/models/agent-turn.model.d.ts +11 -7
- package/dist/lib/models/agent-turn.model.d.ts.map +1 -1
- package/dist/lib/models/agent-turn.model.js.map +1 -1
- package/dist/lib/services/active-tasks.service.d.ts +5 -0
- package/dist/lib/services/active-tasks.service.d.ts.map +1 -1
- package/dist/lib/services/active-tasks.service.js +16 -0
- package/dist/lib/services/active-tasks.service.js.map +1 -1
- package/dist/lib/services/agent-state.service.d.ts.map +1 -1
- package/dist/lib/services/agent-state.service.js +7 -1
- package/dist/lib/services/agent-state.service.js.map +1 -1
- package/dist/lib/services/artifact-permission.service.d.ts.map +1 -1
- package/dist/lib/services/artifact-permission.service.js +2 -0
- package/dist/lib/services/artifact-permission.service.js.map +1 -1
- package/dist/lib/services/artifact-state.service.d.ts.map +1 -1
- package/dist/lib/services/artifact-state.service.js +1 -0
- package/dist/lib/services/artifact-state.service.js.map +1 -1
- package/dist/lib/services/collection-permission.service.d.ts.map +1 -1
- package/dist/lib/services/collection-permission.service.js +1 -0
- package/dist/lib/services/collection-permission.service.js.map +1 -1
- package/dist/lib/services/conversation-agent.service.d.ts +1 -4
- package/dist/lib/services/conversation-agent.service.d.ts.map +1 -1
- package/dist/lib/services/conversation-agent.service.js +7 -45
- package/dist/lib/services/conversation-agent.service.js.map +1 -1
- package/dist/lib/services/realtime-session-review.service.d.ts +2 -1
- package/dist/lib/services/realtime-session-review.service.d.ts.map +1 -1
- package/dist/lib/services/realtime-session-review.service.js.map +1 -1
- package/dist/lib/services/search.service.d.ts.map +1 -1
- package/dist/lib/services/search.service.js +1 -0
- package/dist/lib/services/search.service.js.map +1 -1
- package/dist/lib/utils/agent-handoff-map.d.ts +52 -0
- package/dist/lib/utils/agent-handoff-map.d.ts.map +1 -0
- package/dist/lib/utils/agent-handoff-map.js +270 -0
- package/dist/lib/utils/agent-handoff-map.js.map +1 -0
- package/dist/lib/utils/agent-turn-routing.d.ts +62 -7
- package/dist/lib/utils/agent-turn-routing.d.ts.map +1 -1
- package/dist/lib/utils/agent-turn-routing.js +147 -6
- package/dist/lib/utils/agent-turn-routing.js.map +1 -1
- package/dist/lib/utils/decision-routing.d.ts +17 -9
- package/dist/lib/utils/decision-routing.d.ts.map +1 -1
- package/dist/lib/utils/decision-routing.js +23 -14
- package/dist/lib/utils/decision-routing.js.map +1 -1
- package/dist/lib/utils/thread-replies.d.ts +34 -0
- package/dist/lib/utils/thread-replies.d.ts.map +1 -0
- package/dist/lib/utils/thread-replies.js +49 -0
- package/dist/lib/utils/thread-replies.js.map +1 -0
- package/dist/public-api.d.ts +5 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +5 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +36 -36
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostListener, Input, Output, inject, } from '@angular/core';
|
|
2
2
|
import { LogError } from '@memberjunction/core';
|
|
3
|
-
import { AIEngineBase } from '@memberjunction/ai-engine-base';
|
|
3
|
+
import { AIEngineBase, AgentModeSettingKey } from '@memberjunction/ai-engine-base';
|
|
4
4
|
import { UserInfoEngine } from '@memberjunction/core-entities';
|
|
5
5
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
6
6
|
import * as i0 from "@angular/core";
|
|
@@ -183,7 +183,7 @@ export class ConversationModePickerComponent {
|
|
|
183
183
|
return this.Presets.find(p => p.IsDefault) ?? this.Presets[0];
|
|
184
184
|
}
|
|
185
185
|
storageKey(agentID) {
|
|
186
|
-
return
|
|
186
|
+
return AgentModeSettingKey(agentID);
|
|
187
187
|
}
|
|
188
188
|
OnDocumentClick(_event) {
|
|
189
189
|
if (this.IsOpen) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-mode-picker.component.js","sourceRoot":"","sources":["../../../../src/lib/components/conversation/conversation-mode-picker.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n HostListener,\n Input,\n OnInit,\n Output,\n inject,\n} from '@angular/core';\nimport { LogError } from '@memberjunction/core';\nimport { AIEngineBase } from '@memberjunction/ai-engine-base';\nimport type { MJAIAgentConfigurationEntity } from '@memberjunction/core-entities';\nimport { UserInfoEngine } from '@memberjunction/core-entities';\nimport { UUIDsEqual } from '@memberjunction/global';\n\n/**\n * Per-agent quality/speed mode picker. Sits next to the agent picker in\n * the chat-area header. Lists the **active agent's** configured\n * presets (e.g. Draft / Standard / High) from\n * `AIEngineBase.Instance.GetAgentConfigurationPresets(agentId)` and lets\n * the user pick the one applied to all SUBSEQUENT requests in this\n * conversation. Past messages are not retroactively re-routed.\n *\n * Choice persists per-user, per-agent via `UserInfoEngine` (key\n * `mj.agentMode.<agentId>` = preset name). Cross-device, cross-browser\n * — same model the form-variant preference uses. No DB schema changes.\n *\n * Auto-hides when the active agent has fewer than 2 presets — a\n * single-mode agent doesn't need a switcher.\n */\n@Component({\n standalone: false,\n selector: 'mj-conversation-mode-picker',\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (Presets.length >= 2) {\n <div class=\"mj-cv-mode-picker\" [class.mj-cv-mode-picker--open]=\"IsOpen\">\n <button\n type=\"button\"\n class=\"mj-cv-mode-picker__trigger\"\n [title]=\"ButtonTitle\"\n [disabled]=\"Disabled\"\n (click)=\"Toggle($event)\">\n <span class=\"mj-cv-mode-picker__label\">{{ CurrentLabel }}</span>\n <i class=\"fa-solid fa-caret-down mj-cv-mode-picker__caret\"></i>\n </button>\n @if (IsOpen) {\n <div class=\"mj-cv-mode-picker__menu\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-cv-mode-picker__menu-header\">Mode for the next message</div>\n @for (p of Presets; track p.ID) {\n <button\n type=\"button\"\n class=\"mj-cv-mode-picker__item\"\n [class.mj-cv-mode-picker__item--selected]=\"IsSelected(p)\"\n [title]=\"p.Description || p.Name\"\n (click)=\"PickPreset(p)\">\n <div class=\"mj-cv-mode-picker__item-text\">\n <span class=\"mj-cv-mode-picker__item-name\">{{ p.DisplayName || p.Name }}</span>\n @if (p.Description) {\n <span class=\"mj-cv-mode-picker__item-desc\">{{ p.Description }}</span>\n }\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n `,\n styles: [`\n .mj-cv-mode-picker { position: relative; display: inline-flex; align-items: center; }\n .mj-cv-mode-picker__trigger {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 6px 12px; min-height: 32px; box-sizing: border-box;\n background: var(--mj-bg-surface-card); border: 1px solid var(--mj-border-default);\n border-radius: 4px; cursor: pointer;\n color: var(--mj-text-primary); font-size: 0.8125rem; line-height: 1.5;\n transition: background 120ms, border-color 120ms;\n }\n .mj-cv-mode-picker__trigger:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover); border-color: var(--mj-border-strong);\n }\n .mj-cv-mode-picker__trigger:disabled { opacity: 0.5; cursor: not-allowed; }\n .mj-cv-mode-picker--open .mj-cv-mode-picker__trigger { border-color: var(--mj-border-focus); }\n .mj-cv-mode-picker__label { max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .mj-cv-mode-picker__caret { font-size: 10px; opacity: 0.7; }\n .mj-cv-mode-picker__menu {\n position: absolute; top: calc(100% + 4px); right: 0; z-index: 50;\n min-width: 280px; max-height: 360px; overflow-y: auto;\n background: var(--mj-bg-surface-elevated); border: 1px solid var(--mj-border-default);\n border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);\n padding: 4px 0;\n }\n .mj-cv-mode-picker__menu-header {\n padding: 6px 12px 4px; font-size: 11px; text-transform: uppercase;\n letter-spacing: 0.04em; color: var(--mj-text-muted);\n }\n .mj-cv-mode-picker__item {\n display: flex; align-items: flex-start; gap: 10px; width: 100%;\n padding: 8px 12px; background: transparent; border: none; cursor: pointer;\n color: var(--mj-text-primary); font-size: 13px; text-align: left;\n }\n .mj-cv-mode-picker__item:hover { background: var(--mj-bg-surface-hover); }\n .mj-cv-mode-picker__item--selected { background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent); }\n .mj-cv-mode-picker__item-text { flex: 1; display: flex; flex-direction: column; gap: 2px; }\n .mj-cv-mode-picker__item-name { font-weight: 600; }\n .mj-cv-mode-picker__item-desc { font-size: 11px; color: var(--mj-text-muted); }\n `]\n})\nexport class ConversationModePickerComponent implements OnInit {\n /**\n * The agent the picker should show presets for. When this changes,\n * the picker re-resolves the available presets and the user's saved\n * choice for that agent. Null hides the picker entirely.\n */\n @Input()\n public set AgentID(value: string | null) {\n if (value === this._agentID) return;\n this._agentID = value;\n this.refresh();\n }\n public get AgentID(): string | null { return this._agentID; }\n\n /** Disable interactions (e.g. during agent execution). */\n @Input() Disabled: boolean = false;\n\n /**\n * Emitted whenever the user picks a preset. Payload is the\n * `MJ: AI Agent Configurations.ID` — exactly what\n * `MessageInputComponent` forwards as `agentConfigurationPresetId`\n * on the next message route. Null when the picker is cleared or\n * the active agent has no presets.\n */\n @Output() PresetChanged = new EventEmitter<string | null>();\n\n public Presets: MJAIAgentConfigurationEntity[] = [];\n public CurrentPreset: MJAIAgentConfigurationEntity | null = null;\n public IsOpen = false;\n\n private _agentID: string | null = null;\n private readonly cdr = inject(ChangeDetectorRef);\n\n public async ngOnInit(): Promise<void> {\n await this.refresh();\n }\n\n public get CurrentLabel(): string {\n return this.CurrentPreset?.DisplayName ?? this.CurrentPreset?.Name ?? 'Mode';\n }\n\n public get ButtonTitle(): string {\n if (!this.CurrentPreset) return 'Pick a mode';\n const base = `Mode: ${this.CurrentLabel}`;\n const desc = this.CurrentPreset.Description ?? '';\n return desc ? `${base} — ${desc}\\n\\nClick to change. Applies to the NEXT message.` : `${base} — click to change`;\n }\n\n public IsSelected(p: MJAIAgentConfigurationEntity): boolean {\n return !!this.CurrentPreset && UUIDsEqual(p.ID, this.CurrentPreset.ID);\n }\n\n public Toggle(event: MouseEvent): void {\n event.stopPropagation();\n if (this.Disabled || this.Presets.length < 2) return;\n this.IsOpen = !this.IsOpen;\n this.cdr.markForCheck();\n }\n\n public PickPreset(p: MJAIAgentConfigurationEntity): void {\n this.IsOpen = false;\n if (this.CurrentPreset && UUIDsEqual(p.ID, this.CurrentPreset.ID)) {\n this.cdr.markForCheck();\n return;\n }\n this.CurrentPreset = p;\n // Persist by NAME (not ID) so the preference survives a\n // metadata refresh that re-stamps preset IDs. Resolution back to\n // the actual config row on read happens via\n // GetAgentConfigurationPresetByName at refresh time.\n if (this._agentID) {\n UserInfoEngine.Instance.SetSettingDebounced(this.storageKey(this._agentID), p.Name);\n }\n this.PresetChanged.emit(p.ID);\n this.cdr.markForCheck();\n }\n\n /** Re-load presets for the active agent and apply the saved choice. */\n private async refresh(): Promise<void> {\n if (!this._agentID) {\n this.Presets = [];\n this.CurrentPreset = null;\n this.PresetChanged.emit(null);\n this.cdr.markForCheck();\n return;\n }\n try {\n await AIEngineBase.Instance.Config(false);\n this.Presets = AIEngineBase.Instance.GetAgentConfigurationPresets(this._agentID, true) ?? [];\n this.CurrentPreset = this.resolveSavedOrDefault();\n this.PresetChanged.emit(this.CurrentPreset?.ID ?? null);\n this.cdr.markForCheck();\n } catch (err) {\n LogError(`ConversationModePicker.refresh: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n /** User's saved pick (by Name) → preset, or the agent's IsDefault row. */\n private resolveSavedOrDefault(): MJAIAgentConfigurationEntity | null {\n if (!this._agentID || this.Presets.length === 0) return null;\n const savedName = UserInfoEngine.Instance.GetSetting(this.storageKey(this._agentID));\n if (savedName) {\n const match = AIEngineBase.Instance.GetAgentConfigurationPresetByName(this._agentID, savedName);\n if (match) return match;\n }\n return this.Presets.find(p => p.IsDefault) ?? this.Presets[0];\n }\n\n private storageKey(agentID: string): string {\n return `mj.agentMode.${agentID.toLowerCase()}`;\n }\n\n @HostListener('document:click', ['$event'])\n public OnDocumentClick(_event: MouseEvent): void {\n if (this.IsOpen) {\n this.IsOpen = false;\n this.cdr.markForCheck();\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"conversation-mode-picker.component.js","sourceRoot":"","sources":["../../../../src/lib/components/conversation/conversation-mode-picker.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n HostListener,\n Input,\n OnInit,\n Output,\n inject,\n} from '@angular/core';\nimport { LogError } from '@memberjunction/core';\nimport { AIEngineBase, AgentModeSettingKey } from '@memberjunction/ai-engine-base';\nimport type { MJAIAgentConfigurationEntity } from '@memberjunction/core-entities';\nimport { UserInfoEngine } from '@memberjunction/core-entities';\nimport { UUIDsEqual } from '@memberjunction/global';\n\n/**\n * Per-agent quality/speed mode picker. Sits next to the agent picker in\n * the chat-area header. Lists the **active agent's** configured\n * presets (e.g. Draft / Standard / High) from\n * `AIEngineBase.Instance.GetAgentConfigurationPresets(agentId)` and lets\n * the user pick the one applied to all SUBSEQUENT requests in this\n * conversation. Past messages are not retroactively re-routed.\n *\n * Choice persists per-user, per-agent via `UserInfoEngine` (key\n * `mj.agentMode.<agentId>` = preset name). Cross-device, cross-browser\n * — same model the form-variant preference uses. No DB schema changes.\n *\n * Auto-hides when the active agent has fewer than 2 presets — a\n * single-mode agent doesn't need a switcher.\n */\n@Component({\n standalone: false,\n selector: 'mj-conversation-mode-picker',\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n @if (Presets.length >= 2) {\n <div class=\"mj-cv-mode-picker\" [class.mj-cv-mode-picker--open]=\"IsOpen\">\n <button\n type=\"button\"\n class=\"mj-cv-mode-picker__trigger\"\n [title]=\"ButtonTitle\"\n [disabled]=\"Disabled\"\n (click)=\"Toggle($event)\">\n <span class=\"mj-cv-mode-picker__label\">{{ CurrentLabel }}</span>\n <i class=\"fa-solid fa-caret-down mj-cv-mode-picker__caret\"></i>\n </button>\n @if (IsOpen) {\n <div class=\"mj-cv-mode-picker__menu\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-cv-mode-picker__menu-header\">Mode for the next message</div>\n @for (p of Presets; track p.ID) {\n <button\n type=\"button\"\n class=\"mj-cv-mode-picker__item\"\n [class.mj-cv-mode-picker__item--selected]=\"IsSelected(p)\"\n [title]=\"p.Description || p.Name\"\n (click)=\"PickPreset(p)\">\n <div class=\"mj-cv-mode-picker__item-text\">\n <span class=\"mj-cv-mode-picker__item-name\">{{ p.DisplayName || p.Name }}</span>\n @if (p.Description) {\n <span class=\"mj-cv-mode-picker__item-desc\">{{ p.Description }}</span>\n }\n </div>\n </button>\n }\n </div>\n }\n </div>\n }\n `,\n styles: [`\n .mj-cv-mode-picker { position: relative; display: inline-flex; align-items: center; }\n .mj-cv-mode-picker__trigger {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 6px 12px; min-height: 32px; box-sizing: border-box;\n background: var(--mj-bg-surface-card); border: 1px solid var(--mj-border-default);\n border-radius: 4px; cursor: pointer;\n color: var(--mj-text-primary); font-size: 0.8125rem; line-height: 1.5;\n transition: background 120ms, border-color 120ms;\n }\n .mj-cv-mode-picker__trigger:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover); border-color: var(--mj-border-strong);\n }\n .mj-cv-mode-picker__trigger:disabled { opacity: 0.5; cursor: not-allowed; }\n .mj-cv-mode-picker--open .mj-cv-mode-picker__trigger { border-color: var(--mj-border-focus); }\n .mj-cv-mode-picker__label { max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .mj-cv-mode-picker__caret { font-size: 10px; opacity: 0.7; }\n .mj-cv-mode-picker__menu {\n position: absolute; top: calc(100% + 4px); right: 0; z-index: 50;\n min-width: 280px; max-height: 360px; overflow-y: auto;\n background: var(--mj-bg-surface-elevated); border: 1px solid var(--mj-border-default);\n border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.12);\n padding: 4px 0;\n }\n .mj-cv-mode-picker__menu-header {\n padding: 6px 12px 4px; font-size: 11px; text-transform: uppercase;\n letter-spacing: 0.04em; color: var(--mj-text-muted);\n }\n .mj-cv-mode-picker__item {\n display: flex; align-items: flex-start; gap: 10px; width: 100%;\n padding: 8px 12px; background: transparent; border: none; cursor: pointer;\n color: var(--mj-text-primary); font-size: 13px; text-align: left;\n }\n .mj-cv-mode-picker__item:hover { background: var(--mj-bg-surface-hover); }\n .mj-cv-mode-picker__item--selected { background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent); }\n .mj-cv-mode-picker__item-text { flex: 1; display: flex; flex-direction: column; gap: 2px; }\n .mj-cv-mode-picker__item-name { font-weight: 600; }\n .mj-cv-mode-picker__item-desc { font-size: 11px; color: var(--mj-text-muted); }\n `]\n})\nexport class ConversationModePickerComponent implements OnInit {\n /**\n * The agent the picker should show presets for. When this changes,\n * the picker re-resolves the available presets and the user's saved\n * choice for that agent. Null hides the picker entirely.\n */\n @Input()\n public set AgentID(value: string | null) {\n if (value === this._agentID) return;\n this._agentID = value;\n this.refresh();\n }\n public get AgentID(): string | null { return this._agentID; }\n\n /** Disable interactions (e.g. during agent execution). */\n @Input() Disabled: boolean = false;\n\n /**\n * Emitted whenever the user picks a preset. Payload is the\n * `MJ: AI Agent Configurations.ID` — exactly what\n * `MessageInputComponent` forwards as `agentConfigurationPresetId`\n * on the next message route. Null when the picker is cleared or\n * the active agent has no presets.\n */\n @Output() PresetChanged = new EventEmitter<string | null>();\n\n public Presets: MJAIAgentConfigurationEntity[] = [];\n public CurrentPreset: MJAIAgentConfigurationEntity | null = null;\n public IsOpen = false;\n\n private _agentID: string | null = null;\n private readonly cdr = inject(ChangeDetectorRef);\n\n public async ngOnInit(): Promise<void> {\n await this.refresh();\n }\n\n public get CurrentLabel(): string {\n return this.CurrentPreset?.DisplayName ?? this.CurrentPreset?.Name ?? 'Mode';\n }\n\n public get ButtonTitle(): string {\n if (!this.CurrentPreset) return 'Pick a mode';\n const base = `Mode: ${this.CurrentLabel}`;\n const desc = this.CurrentPreset.Description ?? '';\n return desc ? `${base} — ${desc}\\n\\nClick to change. Applies to the NEXT message.` : `${base} — click to change`;\n }\n\n public IsSelected(p: MJAIAgentConfigurationEntity): boolean {\n return !!this.CurrentPreset && UUIDsEqual(p.ID, this.CurrentPreset.ID);\n }\n\n public Toggle(event: MouseEvent): void {\n event.stopPropagation();\n if (this.Disabled || this.Presets.length < 2) return;\n this.IsOpen = !this.IsOpen;\n this.cdr.markForCheck();\n }\n\n public PickPreset(p: MJAIAgentConfigurationEntity): void {\n this.IsOpen = false;\n if (this.CurrentPreset && UUIDsEqual(p.ID, this.CurrentPreset.ID)) {\n this.cdr.markForCheck();\n return;\n }\n this.CurrentPreset = p;\n // Persist by NAME (not ID) so the preference survives a\n // metadata refresh that re-stamps preset IDs. Resolution back to\n // the actual config row on read happens via\n // GetAgentConfigurationPresetByName at refresh time.\n if (this._agentID) {\n UserInfoEngine.Instance.SetSettingDebounced(this.storageKey(this._agentID), p.Name);\n }\n this.PresetChanged.emit(p.ID);\n this.cdr.markForCheck();\n }\n\n /** Re-load presets for the active agent and apply the saved choice. */\n private async refresh(): Promise<void> {\n if (!this._agentID) {\n this.Presets = [];\n this.CurrentPreset = null;\n this.PresetChanged.emit(null);\n this.cdr.markForCheck();\n return;\n }\n try {\n await AIEngineBase.Instance.Config(false);\n this.Presets = AIEngineBase.Instance.GetAgentConfigurationPresets(this._agentID, true) ?? [];\n this.CurrentPreset = this.resolveSavedOrDefault();\n this.PresetChanged.emit(this.CurrentPreset?.ID ?? null);\n this.cdr.markForCheck();\n } catch (err) {\n LogError(`ConversationModePicker.refresh: ${err instanceof Error ? err.message : String(err)}`);\n }\n }\n\n /** User's saved pick (by Name) → preset, or the agent's IsDefault row. */\n private resolveSavedOrDefault(): MJAIAgentConfigurationEntity | null {\n if (!this._agentID || this.Presets.length === 0) return null;\n const savedName = UserInfoEngine.Instance.GetSetting(this.storageKey(this._agentID));\n if (savedName) {\n const match = AIEngineBase.Instance.GetAgentConfigurationPresetByName(this._agentID, savedName);\n if (match) return match;\n }\n return this.Presets.find(p => p.IsDefault) ?? this.Presets[0];\n }\n\n private storageKey(agentID: string): string {\n return AgentModeSettingKey(agentID);\n }\n\n @HostListener('document:click', ['$event'])\n public OnDocumentClick(_event: MouseEvent): void {\n if (this.IsOpen) {\n this.IsOpen = false;\n this.cdr.markForCheck();\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { JSONValue } from '@memberjunction/ai';
|
|
2
|
+
import { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';
|
|
3
|
+
import { AgentClientService } from '@memberjunction/ng-agent-client';
|
|
4
|
+
import { RemoteBrowserSurfaceComponent } from '../realtime/remote-browser/remote-browser-surface.component';
|
|
5
|
+
/** What a text agent is told about the conversation's browser, as `appContext.AdditionalContext.RemoteBrowser`. */
|
|
6
|
+
export interface ConversationRemoteBrowserContextSnapshot {
|
|
7
|
+
Open: boolean;
|
|
8
|
+
CurrentUrl: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Options for constructing a {@link ConversationRemoteBrowserController}.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConversationRemoteBrowserControllerOptions {
|
|
14
|
+
/** Invoked when an agent's `browser_*` tool runs, so the UI can show the browser drawer. */
|
|
15
|
+
OnAutoOpenSurface?: () => void;
|
|
16
|
+
/** Invoked when the browser opens or closes, or its URL changes. */
|
|
17
|
+
OnStateChange?: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Invoked once when the server refuses this conversation a browser (not enabled, not the caller's, over
|
|
20
|
+
* the cap). The controller has already closed it, which stops the live view's polling.
|
|
21
|
+
*/
|
|
22
|
+
OnSessionRefused?: (message: string) => void;
|
|
23
|
+
/** Optional custom GraphQL executor (used by unit tests; defaults to the provider's `ExecuteGQL`). */
|
|
24
|
+
ExecuteServerAction?: <T>(query: string, variables: Record<string, JSONValue>) => Promise<T | null>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Gives one text conversation (`MJ: Conversations`) a live Remote Browser its agents can drive.
|
|
28
|
+
*
|
|
29
|
+
* The browser belongs to the conversation that opened it ({@link Open}); the controller captures that
|
|
30
|
+
* ID, so a later conversation switch still stops and ends the right browser ({@link Close}).
|
|
31
|
+
*
|
|
32
|
+
* Agents reach the browser through the `browser_*` client tools. Their handlers live in
|
|
33
|
+
* `AgentClientService`, whose registry is app-wide, so they are registered only while this
|
|
34
|
+
* controller's browser is open, and removed on close only if this controller still owns them (a chat
|
|
35
|
+
* area that opened a browser later takes them over). The tools are advertised to this conversation's
|
|
36
|
+
* agent runs alone, through `appContext.Capabilities.Tools` ({@link WithBrowserContext}).
|
|
37
|
+
*/
|
|
38
|
+
export declare class ConversationRemoteBrowserController {
|
|
39
|
+
private readonly channel;
|
|
40
|
+
private readonly options;
|
|
41
|
+
private readonly handlers;
|
|
42
|
+
private agentClient;
|
|
43
|
+
private provider;
|
|
44
|
+
private pushSub;
|
|
45
|
+
private conversationId;
|
|
46
|
+
private currentUrl;
|
|
47
|
+
private stateVersion;
|
|
48
|
+
private disposed;
|
|
49
|
+
constructor(options?: ConversationRemoteBrowserControllerOptions);
|
|
50
|
+
/** The conversation whose browser is open, or `null` when none is. */
|
|
51
|
+
get ConversationId(): string | null;
|
|
52
|
+
/** The browser's last known URL. */
|
|
53
|
+
get CurrentUrl(): string | null;
|
|
54
|
+
/** Bumps on every open, close and URL change, so a host can cache what it derives from the state. */
|
|
55
|
+
get StateVersion(): number;
|
|
56
|
+
/** Whether the app-wide registry holds THIS controller's `browser_*` handlers. */
|
|
57
|
+
get OwnsTools(): boolean;
|
|
58
|
+
/** Connects the controller to the agent client (tool dispatch) and the provider (server calls, pushed frames). */
|
|
59
|
+
Initialize(agentClient: AgentClientService, provider: GraphQLDataProvider | null): void;
|
|
60
|
+
/**
|
|
61
|
+
* Opens the browser for `conversationId` and claims the `browser_*` tools. A browser this controller
|
|
62
|
+
* held for a different conversation is ended first.
|
|
63
|
+
*/
|
|
64
|
+
Open(conversationId: string): void;
|
|
65
|
+
/**
|
|
66
|
+
* Ends the conversation's browser: stops its streams, gives up the tools this controller still owns,
|
|
67
|
+
* and ends the server browser so no Chromium process (or billed hosted browser) is left running.
|
|
68
|
+
*/
|
|
69
|
+
Close(): Promise<void>;
|
|
70
|
+
/** Binds the drawer's live surface (starts the screencast, audio and human takeover). */
|
|
71
|
+
BindSurface(surface: RemoteBrowserSurfaceComponent): void;
|
|
72
|
+
/** Unbinds the surface when the drawer is hidden; the browser itself stays open. */
|
|
73
|
+
UnbindSurface(): void;
|
|
74
|
+
/** Navigates the browser, e.g. from the drawer's URL bar; a refusal or failure comes back in `ErrorMessage`. */
|
|
75
|
+
Navigate(url: string): Promise<{
|
|
76
|
+
Success: boolean;
|
|
77
|
+
ErrorMessage?: string;
|
|
78
|
+
}>;
|
|
79
|
+
/** Runs one `browser_*` tool against this conversation's browser. */
|
|
80
|
+
ExecuteBrowserTool(toolName: string, params: Record<string, unknown>): Promise<{
|
|
81
|
+
Success: boolean;
|
|
82
|
+
Data?: JSONValue;
|
|
83
|
+
ErrorMessage?: string;
|
|
84
|
+
}>;
|
|
85
|
+
/** What the agent is told about the browser. */
|
|
86
|
+
GetAppContextSnapshot(): ConversationRemoteBrowserContextSnapshot;
|
|
87
|
+
/**
|
|
88
|
+
* Adds the browser to an app context while it is open: its state under
|
|
89
|
+
* `AdditionalContext.RemoteBrowser`, and, while this controller owns them, the `browser_*` tools
|
|
90
|
+
* under `Capabilities.Tools`, the tier an agent run reads a surface's tools from. Returns `base`
|
|
91
|
+
* unchanged while no browser is open.
|
|
92
|
+
*/
|
|
93
|
+
WithBrowserContext(base: Record<string, unknown> | null): Record<string, unknown> | null;
|
|
94
|
+
/** Ends the browser and releases the tools, push subscription and channel. */
|
|
95
|
+
Dispose(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Routes a pushed `RemoteBrowserScreencastFrame` or `RemoteBrowserAudioChunk` to the channel when it
|
|
98
|
+
* belongs to this controller's browser.
|
|
99
|
+
*/
|
|
100
|
+
HandlePushMessage(rawMessage: string): void;
|
|
101
|
+
/** Records a new URL; frames repeat the same one, so only a real change notifies the host. */
|
|
102
|
+
private setUrl;
|
|
103
|
+
private changed;
|
|
104
|
+
private registerBrowserTools;
|
|
105
|
+
private unregisterBrowserTools;
|
|
106
|
+
private executeServerAction;
|
|
107
|
+
private createChannelContext;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=conversation-remote-browser-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-remote-browser-controller.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/conversation/conversation-remote-browser-controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
2
|
+
import { SafeGraphQLError } from '@memberjunction/graphql-dataprovider';
|
|
3
|
+
import { RemoteBrowserChannel } from '../realtime/remote-browser/remote-browser-channel';
|
|
4
|
+
import { REMOTE_BROWSER_TOOL_DEFINITIONS, REMOTE_BROWSER_TOOL_NAMES } from '../realtime/remote-browser/remote-browser-tools';
|
|
5
|
+
/**
|
|
6
|
+
* The `extensions.code` the server sets on a refusal that asking again cannot change (browsers not enabled,
|
|
7
|
+
* not the caller's, over a cap): MJServer's `RemoteBrowserRefusedError`.
|
|
8
|
+
*/
|
|
9
|
+
const REMOTE_BROWSER_REFUSED = 'REMOTE_BROWSER_REFUSED';
|
|
10
|
+
const END_REMOTE_BROWSER_SESSION_MUTATION = `
|
|
11
|
+
mutation EndRemoteBrowserSession($agentSessionID: String!) {
|
|
12
|
+
EndRemoteBrowserSession(agentSessionID: $agentSessionID)
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
/**
|
|
16
|
+
* The `browser_*` tools a text conversation's agents get: all but AchieveGoal, whose goal loop clicks
|
|
17
|
+
* and navigates on its own, past the click guard (the server refuses goal runs for a conversation too).
|
|
18
|
+
*/
|
|
19
|
+
const TEXT_BROWSER_TOOLS = REMOTE_BROWSER_TOOL_DEFINITIONS.filter((def) => def.Name !== REMOTE_BROWSER_TOOL_NAMES.AchieveGoal);
|
|
20
|
+
/** The text browser tools in the shape an agent run reads from `appContext.Capabilities.Tools`. */
|
|
21
|
+
const BROWSER_TOOL_METADATA = TEXT_BROWSER_TOOLS.map((def) => ({
|
|
22
|
+
Name: def.Name,
|
|
23
|
+
Description: def.Description,
|
|
24
|
+
InputSchema: (def.ParametersSchema ?? {}),
|
|
25
|
+
}));
|
|
26
|
+
/**
|
|
27
|
+
* Gives one text conversation (`MJ: Conversations`) a live Remote Browser its agents can drive.
|
|
28
|
+
*
|
|
29
|
+
* The browser belongs to the conversation that opened it ({@link Open}); the controller captures that
|
|
30
|
+
* ID, so a later conversation switch still stops and ends the right browser ({@link Close}).
|
|
31
|
+
*
|
|
32
|
+
* Agents reach the browser through the `browser_*` client tools. Their handlers live in
|
|
33
|
+
* `AgentClientService`, whose registry is app-wide, so they are registered only while this
|
|
34
|
+
* controller's browser is open, and removed on close only if this controller still owns them (a chat
|
|
35
|
+
* area that opened a browser later takes them over). The tools are advertised to this conversation's
|
|
36
|
+
* agent runs alone, through `appContext.Capabilities.Tools` ({@link WithBrowserContext}).
|
|
37
|
+
*/
|
|
38
|
+
export class ConversationRemoteBrowserController {
|
|
39
|
+
channel = new RemoteBrowserChannel();
|
|
40
|
+
options;
|
|
41
|
+
handlers = new Map();
|
|
42
|
+
agentClient = null;
|
|
43
|
+
provider = null;
|
|
44
|
+
pushSub = null;
|
|
45
|
+
conversationId = null;
|
|
46
|
+
currentUrl = null;
|
|
47
|
+
stateVersion = 0;
|
|
48
|
+
disposed = false;
|
|
49
|
+
constructor(options = {}) {
|
|
50
|
+
this.options = options;
|
|
51
|
+
this.channel.Initialize(this.createChannelContext());
|
|
52
|
+
}
|
|
53
|
+
/** The conversation whose browser is open, or `null` when none is. */
|
|
54
|
+
get ConversationId() {
|
|
55
|
+
return this.conversationId;
|
|
56
|
+
}
|
|
57
|
+
/** The browser's last known URL. */
|
|
58
|
+
get CurrentUrl() {
|
|
59
|
+
return this.currentUrl;
|
|
60
|
+
}
|
|
61
|
+
/** Bumps on every open, close and URL change, so a host can cache what it derives from the state. */
|
|
62
|
+
get StateVersion() {
|
|
63
|
+
return this.stateVersion;
|
|
64
|
+
}
|
|
65
|
+
/** Whether the app-wide registry holds THIS controller's `browser_*` handlers. */
|
|
66
|
+
get OwnsTools() {
|
|
67
|
+
const registered = this.agentClient?.GetRegisteredTools() ?? [];
|
|
68
|
+
return this.handlers.size > 0 && Array.from(this.handlers).every(([name, handler]) => registered.find((t) => t.Name === name)?.Handler === handler);
|
|
69
|
+
}
|
|
70
|
+
/** Connects the controller to the agent client (tool dispatch) and the provider (server calls, pushed frames). */
|
|
71
|
+
Initialize(agentClient, provider) {
|
|
72
|
+
this.agentClient = agentClient;
|
|
73
|
+
this.provider = provider;
|
|
74
|
+
this.pushSub?.unsubscribe();
|
|
75
|
+
this.pushSub = provider?.PushStatusUpdates().subscribe((raw) => this.HandlePushMessage(raw)) ?? null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Opens the browser for `conversationId` and claims the `browser_*` tools. A browser this controller
|
|
79
|
+
* held for a different conversation is ended first.
|
|
80
|
+
*/
|
|
81
|
+
Open(conversationId) {
|
|
82
|
+
if (this.conversationId && UUIDsEqual(this.conversationId, conversationId)) {
|
|
83
|
+
this.registerBrowserTools();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
void this.Close();
|
|
87
|
+
this.conversationId = conversationId;
|
|
88
|
+
this.registerBrowserTools();
|
|
89
|
+
this.changed();
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Ends the conversation's browser: stops its streams, gives up the tools this controller still owns,
|
|
93
|
+
* and ends the server browser so no Chromium process (or billed hosted browser) is left running.
|
|
94
|
+
*/
|
|
95
|
+
async Close() {
|
|
96
|
+
const conversationId = this.conversationId;
|
|
97
|
+
if (!conversationId) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Unbinding reads the session ID synchronously, so it must run before the ID is cleared.
|
|
101
|
+
this.channel.UnbindSurface();
|
|
102
|
+
this.unregisterBrowserTools();
|
|
103
|
+
this.conversationId = null;
|
|
104
|
+
this.currentUrl = null;
|
|
105
|
+
this.changed();
|
|
106
|
+
await this.executeServerAction(END_REMOTE_BROWSER_SESSION_MUTATION, { agentSessionID: conversationId });
|
|
107
|
+
}
|
|
108
|
+
/** Binds the drawer's live surface (starts the screencast, audio and human takeover). */
|
|
109
|
+
BindSurface(surface) {
|
|
110
|
+
this.channel.BindSurface(surface);
|
|
111
|
+
if (this.currentUrl) {
|
|
112
|
+
surface.SetCurrentUrl(this.currentUrl);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Unbinds the surface when the drawer is hidden; the browser itself stays open. */
|
|
116
|
+
UnbindSurface() {
|
|
117
|
+
this.channel.UnbindSurface();
|
|
118
|
+
}
|
|
119
|
+
/** Navigates the browser, e.g. from the drawer's URL bar; a refusal or failure comes back in `ErrorMessage`. */
|
|
120
|
+
async Navigate(url) {
|
|
121
|
+
const result = await this.ExecuteBrowserTool(REMOTE_BROWSER_TOOL_NAMES.OpenUrl, { url });
|
|
122
|
+
return { Success: result.Success, ErrorMessage: result.ErrorMessage };
|
|
123
|
+
}
|
|
124
|
+
/** Runs one `browser_*` tool against this conversation's browser. */
|
|
125
|
+
async ExecuteBrowserTool(toolName, params) {
|
|
126
|
+
if (!this.conversationId) {
|
|
127
|
+
return { Success: false, ErrorMessage: 'No browser is open for this conversation.' };
|
|
128
|
+
}
|
|
129
|
+
this.options.OnAutoOpenSurface?.();
|
|
130
|
+
const raw = await this.channel.ApplyAgentTool(toolName, JSON.stringify(params ?? {}));
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = JSON.parse(raw);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { Success: true, Data: { raw } };
|
|
137
|
+
}
|
|
138
|
+
this.setUrl(result.currentUrl);
|
|
139
|
+
return result.success === false
|
|
140
|
+
? { Success: false, Data: result, ErrorMessage: result.error ?? result.detail ?? 'Browser tool execution failed.' }
|
|
141
|
+
: { Success: true, Data: result };
|
|
142
|
+
}
|
|
143
|
+
/** What the agent is told about the browser. */
|
|
144
|
+
GetAppContextSnapshot() {
|
|
145
|
+
return { Open: this.conversationId !== null, CurrentUrl: this.currentUrl };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Adds the browser to an app context while it is open: its state under
|
|
149
|
+
* `AdditionalContext.RemoteBrowser`, and, while this controller owns them, the `browser_*` tools
|
|
150
|
+
* under `Capabilities.Tools`, the tier an agent run reads a surface's tools from. Returns `base`
|
|
151
|
+
* unchanged while no browser is open.
|
|
152
|
+
*/
|
|
153
|
+
WithBrowserContext(base) {
|
|
154
|
+
if (!this.conversationId) {
|
|
155
|
+
return base;
|
|
156
|
+
}
|
|
157
|
+
const context = base ?? {};
|
|
158
|
+
const additional = asRecord(context['AdditionalContext']);
|
|
159
|
+
const capabilities = asRecord(context['Capabilities']);
|
|
160
|
+
const tools = Array.isArray(capabilities['Tools']) ? capabilities['Tools'] : [];
|
|
161
|
+
return {
|
|
162
|
+
...context,
|
|
163
|
+
AdditionalContext: { ...additional, RemoteBrowser: this.GetAppContextSnapshot() },
|
|
164
|
+
...(this.OwnsTools ? { Capabilities: { ...capabilities, Tools: [...tools, ...BROWSER_TOOL_METADATA] } } : {}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** Ends the browser and releases the tools, push subscription and channel. */
|
|
168
|
+
Dispose() {
|
|
169
|
+
this.disposed = true;
|
|
170
|
+
void this.Close();
|
|
171
|
+
this.pushSub?.unsubscribe();
|
|
172
|
+
this.pushSub = null;
|
|
173
|
+
this.channel.Dispose();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Routes a pushed `RemoteBrowserScreencastFrame` or `RemoteBrowserAudioChunk` to the channel when it
|
|
177
|
+
* belongs to this controller's browser.
|
|
178
|
+
*/
|
|
179
|
+
HandlePushMessage(rawMessage) {
|
|
180
|
+
if (!this.conversationId || !rawMessage) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let message;
|
|
184
|
+
try {
|
|
185
|
+
const outer = JSON.parse(rawMessage);
|
|
186
|
+
message = typeof outer['message'] === 'string' ? JSON.parse(outer['message']) : outer;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return; // Not JSON: an unrelated status update.
|
|
190
|
+
}
|
|
191
|
+
if (message.resolver !== 'RemoteBrowserActionResolver' || !message.agentSessionID || !UUIDsEqual(message.agentSessionID, this.conversationId)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (typeof message.dataBase64 !== 'string') {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (message.type === 'RemoteBrowserScreencastFrame') {
|
|
198
|
+
this.channel.OnScreencastFrame(message.dataBase64, message.currentUrl ?? null);
|
|
199
|
+
this.setUrl(message.currentUrl);
|
|
200
|
+
}
|
|
201
|
+
else if (message.type === 'RemoteBrowserAudioChunk') {
|
|
202
|
+
this.channel.OnAudioChunk({
|
|
203
|
+
dataBase64: message.dataBase64,
|
|
204
|
+
codec: 'webm-opus',
|
|
205
|
+
sampleRate: message.sampleRate ?? 48000,
|
|
206
|
+
channels: message.channels ?? 2,
|
|
207
|
+
seq: message.seq ?? 0,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** Records a new URL; frames repeat the same one, so only a real change notifies the host. */
|
|
212
|
+
setUrl(url) {
|
|
213
|
+
const next = url?.trim();
|
|
214
|
+
if (next && next !== this.currentUrl) {
|
|
215
|
+
this.currentUrl = next;
|
|
216
|
+
this.changed();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
changed() {
|
|
220
|
+
this.stateVersion++;
|
|
221
|
+
if (!this.disposed) {
|
|
222
|
+
this.options.OnStateChange?.();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
registerBrowserTools() {
|
|
226
|
+
const agentClient = this.agentClient;
|
|
227
|
+
if (!agentClient) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
for (const def of TEXT_BROWSER_TOOLS) {
|
|
231
|
+
let handler = this.handlers.get(def.Name);
|
|
232
|
+
if (!handler) {
|
|
233
|
+
handler = (params) => this.ExecuteBrowserTool(def.Name, params);
|
|
234
|
+
this.handlers.set(def.Name, handler);
|
|
235
|
+
}
|
|
236
|
+
agentClient.RegisterTool({ Name: def.Name, Description: def.Description, ParameterSchema: (def.ParametersSchema ?? {}), Handler: handler });
|
|
237
|
+
}
|
|
238
|
+
const sessionId = this.provider?.sessionId;
|
|
239
|
+
if (sessionId && (!agentClient.IsActive || agentClient.SessionId !== sessionId)) {
|
|
240
|
+
agentClient.StartSession(sessionId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
unregisterBrowserTools() {
|
|
244
|
+
const registered = this.agentClient?.GetRegisteredTools() ?? [];
|
|
245
|
+
for (const [name, handler] of this.handlers) {
|
|
246
|
+
if (registered.find((t) => t.Name === name)?.Handler === handler) {
|
|
247
|
+
this.agentClient?.UnregisterTool(name);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async executeServerAction(query, variables) {
|
|
252
|
+
try {
|
|
253
|
+
if (this.options.ExecuteServerAction) {
|
|
254
|
+
return await this.options.ExecuteServerAction(query, variables);
|
|
255
|
+
}
|
|
256
|
+
return this.provider ? ((await this.provider.ExecuteGQL(query, variables)) ?? null) : null;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
if (err instanceof SafeGraphQLError && err.Code === REMOTE_BROWSER_REFUSED) {
|
|
260
|
+
// Asking again gets the same answer: close, so the live view stops polling, and say why once. Only
|
|
261
|
+
// for the browser still open: a refusal for one already left (its End, after a switch) changes nothing.
|
|
262
|
+
if (UUIDsEqual(variables['agentSessionID'], this.conversationId)) {
|
|
263
|
+
void this.Close();
|
|
264
|
+
this.options.OnSessionRefused?.(err.message);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
console.warn('[ConversationRemoteBrowserController] GraphQL execution failed:', err);
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
createChannelContext() {
|
|
274
|
+
const self = this;
|
|
275
|
+
return {
|
|
276
|
+
AgentName: 'ConversationAgent',
|
|
277
|
+
get Provider() {
|
|
278
|
+
return self.provider;
|
|
279
|
+
},
|
|
280
|
+
get AgentSessionID() {
|
|
281
|
+
return self.conversationId;
|
|
282
|
+
},
|
|
283
|
+
// The channel narrates page changes as notes; a text conversation only needs the URL out of them.
|
|
284
|
+
SendContextNote: (text) => {
|
|
285
|
+
const match = /^\[browser\] (?:current page|the page changed to): (\S+)/.exec(text);
|
|
286
|
+
self.setUrl(match?.[1]);
|
|
287
|
+
},
|
|
288
|
+
RequestSave: () => undefined,
|
|
289
|
+
SetFocusMode: () => undefined,
|
|
290
|
+
SaveAsArtifact: async () => null,
|
|
291
|
+
ExecuteServerAction: (query, variables) => self.executeServerAction(query, variables),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function asRecord(value) {
|
|
296
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=conversation-remote-browser-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-remote-browser-controller.js","sourceRoot":"","sources":["../../../../src/lib/components/conversation/conversation-remote-browser-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { Subscription } from 'rxjs';\nimport { JSONObject, JSONValue } from '@memberjunction/ai';\nimport { ClientToolMetadata } from '@memberjunction/ai-core-plus';\nimport { UUIDsEqual } from '@memberjunction/global';\nimport { GraphQLDataProvider, SafeGraphQLError } from '@memberjunction/graphql-dataprovider';\nimport { AgentClientService } from '@memberjunction/ng-agent-client';\nimport { RealtimeChannelContext } from '@memberjunction/realtime-runtime';\nimport { RemoteBrowserChannel } from '../realtime/remote-browser/remote-browser-channel';\nimport { RemoteBrowserSurfaceComponent } from '../realtime/remote-browser/remote-browser-surface.component';\nimport { REMOTE_BROWSER_TOOL_DEFINITIONS, REMOTE_BROWSER_TOOL_NAMES } from '../realtime/remote-browser/remote-browser-tools';\n\n/** What a text agent is told about the conversation's browser, as `appContext.AdditionalContext.RemoteBrowser`. */\nexport interface ConversationRemoteBrowserContextSnapshot {\n Open: boolean;\n CurrentUrl: string | null;\n}\n\n/**\n * Options for constructing a {@link ConversationRemoteBrowserController}.\n */\nexport interface ConversationRemoteBrowserControllerOptions {\n /** Invoked when an agent's `browser_*` tool runs, so the UI can show the browser drawer. */\n OnAutoOpenSurface?: () => void;\n /** Invoked when the browser opens or closes, or its URL changes. */\n OnStateChange?: () => void;\n /**\n * Invoked once when the server refuses this conversation a browser (not enabled, not the caller's, over\n * the cap). The controller has already closed it, which stops the live view's polling.\n */\n OnSessionRefused?: (message: string) => void;\n /** Optional custom GraphQL executor (used by unit tests; defaults to the provider's `ExecuteGQL`). */\n ExecuteServerAction?: <T>(query: string, variables: Record<string, JSONValue>) => Promise<T | null>;\n}\n\n/**\n * The `extensions.code` the server sets on a refusal that asking again cannot change (browsers not enabled,\n * not the caller's, over a cap): MJServer's `RemoteBrowserRefusedError`.\n */\nconst REMOTE_BROWSER_REFUSED = 'REMOTE_BROWSER_REFUSED';\n\nconst END_REMOTE_BROWSER_SESSION_MUTATION = `\n mutation EndRemoteBrowserSession($agentSessionID: String!) {\n EndRemoteBrowserSession(agentSessionID: $agentSessionID)\n }\n`;\n\n/**\n * The `browser_*` tools a text conversation's agents get: all but AchieveGoal, whose goal loop clicks\n * and navigates on its own, past the click guard (the server refuses goal runs for a conversation too).\n */\nconst TEXT_BROWSER_TOOLS = REMOTE_BROWSER_TOOL_DEFINITIONS.filter((def) => def.Name !== REMOTE_BROWSER_TOOL_NAMES.AchieveGoal);\n\n/** The text browser tools in the shape an agent run reads from `appContext.Capabilities.Tools`. */\nconst BROWSER_TOOL_METADATA: ClientToolMetadata[] = TEXT_BROWSER_TOOLS.map((def) => ({\n Name: def.Name,\n Description: def.Description,\n InputSchema: (def.ParametersSchema ?? {}) as Record<string, unknown>,\n}));\n\ntype ClientToolHandler = ReturnType<AgentClientService['GetRegisteredTools']>[number]['Handler'];\n\n/**\n * Gives one text conversation (`MJ: Conversations`) a live Remote Browser its agents can drive.\n *\n * The browser belongs to the conversation that opened it ({@link Open}); the controller captures that\n * ID, so a later conversation switch still stops and ends the right browser ({@link Close}).\n *\n * Agents reach the browser through the `browser_*` client tools. Their handlers live in\n * `AgentClientService`, whose registry is app-wide, so they are registered only while this\n * controller's browser is open, and removed on close only if this controller still owns them (a chat\n * area that opened a browser later takes them over). The tools are advertised to this conversation's\n * agent runs alone, through `appContext.Capabilities.Tools` ({@link WithBrowserContext}).\n */\nexport class ConversationRemoteBrowserController {\n private readonly channel = new RemoteBrowserChannel();\n private readonly options: ConversationRemoteBrowserControllerOptions;\n private readonly handlers = new Map<string, ClientToolHandler>();\n private agentClient: AgentClientService | null = null;\n private provider: GraphQLDataProvider | null = null;\n private pushSub: Subscription | null = null;\n private conversationId: string | null = null;\n private currentUrl: string | null = null;\n private stateVersion = 0;\n private disposed = false;\n\n constructor(options: ConversationRemoteBrowserControllerOptions = {}) {\n this.options = options;\n this.channel.Initialize(this.createChannelContext());\n }\n\n /** The conversation whose browser is open, or `null` when none is. */\n public get ConversationId(): string | null {\n return this.conversationId;\n }\n\n /** The browser's last known URL. */\n public get CurrentUrl(): string | null {\n return this.currentUrl;\n }\n\n /** Bumps on every open, close and URL change, so a host can cache what it derives from the state. */\n public get StateVersion(): number {\n return this.stateVersion;\n }\n\n /** Whether the app-wide registry holds THIS controller's `browser_*` handlers. */\n public get OwnsTools(): boolean {\n const registered = this.agentClient?.GetRegisteredTools() ?? [];\n return this.handlers.size > 0 && Array.from(this.handlers).every(([name, handler]) => registered.find((t) => t.Name === name)?.Handler === handler);\n }\n\n /** Connects the controller to the agent client (tool dispatch) and the provider (server calls, pushed frames). */\n public Initialize(agentClient: AgentClientService, provider: GraphQLDataProvider | null): void {\n this.agentClient = agentClient;\n this.provider = provider;\n this.pushSub?.unsubscribe();\n this.pushSub = provider?.PushStatusUpdates().subscribe((raw: string) => this.HandlePushMessage(raw)) ?? null;\n }\n\n /**\n * Opens the browser for `conversationId` and claims the `browser_*` tools. A browser this controller\n * held for a different conversation is ended first.\n */\n public Open(conversationId: string): void {\n if (this.conversationId && UUIDsEqual(this.conversationId, conversationId)) {\n this.registerBrowserTools();\n return;\n }\n void this.Close();\n this.conversationId = conversationId;\n this.registerBrowserTools();\n this.changed();\n }\n\n /**\n * Ends the conversation's browser: stops its streams, gives up the tools this controller still owns,\n * and ends the server browser so no Chromium process (or billed hosted browser) is left running.\n */\n public async Close(): Promise<void> {\n const conversationId = this.conversationId;\n if (!conversationId) {\n return;\n }\n // Unbinding reads the session ID synchronously, so it must run before the ID is cleared.\n this.channel.UnbindSurface();\n this.unregisterBrowserTools();\n this.conversationId = null;\n this.currentUrl = null;\n this.changed();\n await this.executeServerAction(END_REMOTE_BROWSER_SESSION_MUTATION, { agentSessionID: conversationId });\n }\n\n /** Binds the drawer's live surface (starts the screencast, audio and human takeover). */\n public BindSurface(surface: RemoteBrowserSurfaceComponent): void {\n this.channel.BindSurface(surface);\n if (this.currentUrl) {\n surface.SetCurrentUrl(this.currentUrl);\n }\n }\n\n /** Unbinds the surface when the drawer is hidden; the browser itself stays open. */\n public UnbindSurface(): void {\n this.channel.UnbindSurface();\n }\n\n /** Navigates the browser, e.g. from the drawer's URL bar; a refusal or failure comes back in `ErrorMessage`. */\n public async Navigate(url: string): Promise<{ Success: boolean; ErrorMessage?: string }> {\n const result = await this.ExecuteBrowserTool(REMOTE_BROWSER_TOOL_NAMES.OpenUrl, { url });\n return { Success: result.Success, ErrorMessage: result.ErrorMessage };\n }\n\n /** Runs one `browser_*` tool against this conversation's browser. */\n public async ExecuteBrowserTool(toolName: string, params: Record<string, unknown>): Promise<{ Success: boolean; Data?: JSONValue; ErrorMessage?: string }> {\n if (!this.conversationId) {\n return { Success: false, ErrorMessage: 'No browser is open for this conversation.' };\n }\n this.options.OnAutoOpenSurface?.();\n const raw = await this.channel.ApplyAgentTool(toolName, JSON.stringify(params ?? {}));\n let result: { success?: boolean; currentUrl?: string; detail?: string; error?: string };\n try {\n result = JSON.parse(raw);\n } catch {\n return { Success: true, Data: { raw } };\n }\n this.setUrl(result.currentUrl);\n return result.success === false\n ? { Success: false, Data: result as JSONObject, ErrorMessage: result.error ?? result.detail ?? 'Browser tool execution failed.' }\n : { Success: true, Data: result as JSONObject };\n }\n\n /** What the agent is told about the browser. */\n public GetAppContextSnapshot(): ConversationRemoteBrowserContextSnapshot {\n return { Open: this.conversationId !== null, CurrentUrl: this.currentUrl };\n }\n\n /**\n * Adds the browser to an app context while it is open: its state under\n * `AdditionalContext.RemoteBrowser`, and, while this controller owns them, the `browser_*` tools\n * under `Capabilities.Tools`, the tier an agent run reads a surface's tools from. Returns `base`\n * unchanged while no browser is open.\n */\n public WithBrowserContext(base: Record<string, unknown> | null): Record<string, unknown> | null {\n if (!this.conversationId) {\n return base;\n }\n const context = base ?? {};\n const additional = asRecord(context['AdditionalContext']);\n const capabilities = asRecord(context['Capabilities']);\n const tools = Array.isArray(capabilities['Tools']) ? (capabilities['Tools'] as ClientToolMetadata[]) : [];\n return {\n ...context,\n AdditionalContext: { ...additional, RemoteBrowser: this.GetAppContextSnapshot() },\n ...(this.OwnsTools ? { Capabilities: { ...capabilities, Tools: [...tools, ...BROWSER_TOOL_METADATA] } } : {}),\n };\n }\n\n /** Ends the browser and releases the tools, push subscription and channel. */\n public Dispose(): void {\n this.disposed = true;\n void this.Close();\n this.pushSub?.unsubscribe();\n this.pushSub = null;\n this.channel.Dispose();\n }\n\n /**\n * Routes a pushed `RemoteBrowserScreencastFrame` or `RemoteBrowserAudioChunk` to the channel when it\n * belongs to this controller's browser.\n */\n public HandlePushMessage(rawMessage: string): void {\n if (!this.conversationId || !rawMessage) {\n return;\n }\n let message: {\n resolver?: string;\n type?: string;\n agentSessionID?: string;\n currentUrl?: string | null;\n dataBase64?: string;\n sampleRate?: number;\n channels?: number;\n seq?: number;\n };\n try {\n const outer = JSON.parse(rawMessage) as Record<string, unknown>;\n message = typeof outer['message'] === 'string' ? JSON.parse(outer['message']) : outer;\n } catch {\n return; // Not JSON: an unrelated status update.\n }\n if (message.resolver !== 'RemoteBrowserActionResolver' || !message.agentSessionID || !UUIDsEqual(message.agentSessionID, this.conversationId)) {\n return;\n }\n if (typeof message.dataBase64 !== 'string') {\n return;\n }\n if (message.type === 'RemoteBrowserScreencastFrame') {\n this.channel.OnScreencastFrame(message.dataBase64, message.currentUrl ?? null);\n this.setUrl(message.currentUrl);\n } else if (message.type === 'RemoteBrowserAudioChunk') {\n this.channel.OnAudioChunk({\n dataBase64: message.dataBase64,\n codec: 'webm-opus',\n sampleRate: message.sampleRate ?? 48000,\n channels: message.channels ?? 2,\n seq: message.seq ?? 0,\n });\n }\n }\n\n /** Records a new URL; frames repeat the same one, so only a real change notifies the host. */\n private setUrl(url: string | null | undefined): void {\n const next = url?.trim();\n if (next && next !== this.currentUrl) {\n this.currentUrl = next;\n this.changed();\n }\n }\n\n private changed(): void {\n this.stateVersion++;\n if (!this.disposed) {\n this.options.OnStateChange?.();\n }\n }\n\n private registerBrowserTools(): void {\n const agentClient = this.agentClient;\n if (!agentClient) {\n return;\n }\n for (const def of TEXT_BROWSER_TOOLS) {\n let handler = this.handlers.get(def.Name);\n if (!handler) {\n handler = (params: Record<string, unknown>) => this.ExecuteBrowserTool(def.Name, params);\n this.handlers.set(def.Name, handler);\n }\n agentClient.RegisterTool({ Name: def.Name, Description: def.Description, ParameterSchema: (def.ParametersSchema ?? {}) as JSONObject, Handler: handler });\n }\n const sessionId = this.provider?.sessionId;\n if (sessionId && (!agentClient.IsActive || agentClient.SessionId !== sessionId)) {\n agentClient.StartSession(sessionId);\n }\n }\n\n private unregisterBrowserTools(): void {\n const registered = this.agentClient?.GetRegisteredTools() ?? [];\n for (const [name, handler] of this.handlers) {\n if (registered.find((t) => t.Name === name)?.Handler === handler) {\n this.agentClient?.UnregisterTool(name);\n }\n }\n }\n\n private async executeServerAction<T>(query: string, variables: Record<string, JSONValue>): Promise<T | null> {\n try {\n if (this.options.ExecuteServerAction) {\n return await this.options.ExecuteServerAction<T>(query, variables);\n }\n return this.provider ? (((await this.provider.ExecuteGQL(query, variables)) as T) ?? null) : null;\n } catch (err) {\n if (err instanceof SafeGraphQLError && err.Code === REMOTE_BROWSER_REFUSED) {\n // Asking again gets the same answer: close, so the live view stops polling, and say why once. Only\n // for the browser still open: a refusal for one already left (its End, after a switch) changes nothing.\n if (UUIDsEqual(variables['agentSessionID'] as string | null, this.conversationId)) {\n void this.Close();\n this.options.OnSessionRefused?.(err.message);\n }\n } else {\n console.warn('[ConversationRemoteBrowserController] GraphQL execution failed:', err);\n }\n return null;\n }\n }\n\n private createChannelContext(): RealtimeChannelContext {\n const self = this;\n return {\n AgentName: 'ConversationAgent',\n get Provider(): GraphQLDataProvider | null {\n return self.provider;\n },\n get AgentSessionID(): string | null {\n return self.conversationId;\n },\n // The channel narrates page changes as notes; a text conversation only needs the URL out of them.\n SendContextNote: (text: string) => {\n const match = /^\\[browser\\] (?:current page|the page changed to): (\\S+)/.exec(text);\n self.setUrl(match?.[1]);\n },\n RequestSave: () => undefined,\n SetFocusMode: () => undefined,\n SaveAsArtifact: async () => null,\n ExecuteServerAction: <T>(query: string, variables: Record<string, JSONValue>) => self.executeServerAction<T>(query, variables),\n };\n }\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};\n}\n"]}
|