@trismegisto/spine 3.3.5 → 3.3.6
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/README.md +4 -2
- package/fesm2022/trismegisto-spine-audit.mjs +20 -2
- package/fesm2022/trismegisto-spine-audit.mjs.map +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs → trismegisto-spine-auto-service-auto-service-audit.component-CszXNvSs.mjs} +2 -2
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-audit.component-D8HUpZtA.mjs.map → trismegisto-spine-auto-service-auto-service-audit.component-CszXNvSs.mjs.map} +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs → trismegisto-spine-auto-service-auto-service-client-picker.component-CknHYd_P.mjs} +7 -3
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-CknHYd_P.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs → trismegisto-spine-auto-service-auto-service-client.component-Dg3mJrpz.mjs} +24 -5
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-Dg3mJrpz.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-delete-CV_t9vxm.mjs → trismegisto-spine-auto-service-auto-service-delete-BWY5G-xe.mjs} +2 -2
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-BWY5G-xe.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs → trismegisto-spine-auto-service-auto-service-groups.component-DQqzfq0M.mjs} +21 -5
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-DQqzfq0M.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs → trismegisto-spine-auto-service-auto-service-login-attempts.component-CjxKYv-y.mjs} +2 -2
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-login-attempts.component-DoyhkSYc.mjs.map → trismegisto-spine-auto-service-auto-service-login-attempts.component-CjxKYv-y.mjs.map} +1 -1
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs → trismegisto-spine-auto-service-auto-service-news-page.component-Cbaszx4F.mjs} +21 -5
- package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-Cbaszx4F.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs → trismegisto-spine-auto-service-auto-service-ratings-page.component-CJRIDWXi.mjs} +22 -4
- package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-CJRIDWXi.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs → trismegisto-spine-auto-service-auto-service-suggestions-page.component-Bu8YX554.mjs} +46 -4
- package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-Bu8YX554.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs → trismegisto-spine-auto-service-auto-service.component-BLkLHp6J.mjs} +23 -5
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-BLkLHp6J.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs → trismegisto-spine-auto-service-home.component-BYMiTZp6.mjs} +6 -3
- package/fesm2022/trismegisto-spine-auto-service-home.component-BYMiTZp6.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-auto-service.mjs +10 -10
- package/fesm2022/trismegisto-spine-auto-service.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-layout-ai-agent-chat.component-C5JljPYD.mjs +294 -0
- package/fesm2022/trismegisto-spine-layout-ai-agent-chat.component-C5JljPYD.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs → trismegisto-spine-layout-suggestions-page.component-Bh3VGlOR.mjs} +18 -4
- package/fesm2022/trismegisto-spine-layout-suggestions-page.component-Bh3VGlOR.mjs.map +1 -0
- package/fesm2022/{trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs → trismegisto-spine-layout-trismegisto-spine-layout-B47sMo_4.mjs} +1012 -57
- package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-B47sMo_4.mjs.map +1 -0
- package/fesm2022/trismegisto-spine-layout.mjs +1 -1
- package/fesm2022/trismegisto-spine-modal.mjs +4 -1
- package/fesm2022/trismegisto-spine-modal.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-notification.mjs +1 -1
- package/fesm2022/trismegisto-spine-notification.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-routing.mjs +2 -2
- package/fesm2022/trismegisto-spine-routing.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-shell.mjs +2 -1
- package/fesm2022/trismegisto-spine-shell.mjs.map +1 -1
- package/fesm2022/trismegisto-spine-side-panel.mjs +4 -1
- package/fesm2022/trismegisto-spine-side-panel.mjs.map +1 -1
- package/fesm2022/trismegisto-spine.mjs +200 -69
- package/fesm2022/trismegisto-spine.mjs.map +1 -1
- package/package.json +6 -6
- package/types/trismegisto-spine-audit.d.ts +11 -4
- package/types/trismegisto-spine-audit.d.ts.map +1 -1
- package/types/trismegisto-spine-auto-service.d.ts.map +1 -1
- package/types/trismegisto-spine-layout.d.ts +124 -105
- package/types/trismegisto-spine-layout.d.ts.map +1 -1
- package/types/trismegisto-spine-modal.d.ts +2 -1
- package/types/trismegisto-spine-modal.d.ts.map +1 -1
- package/types/trismegisto-spine-shell.d.ts.map +1 -1
- package/types/trismegisto-spine-side-panel.d.ts +2 -1
- package/types/trismegisto-spine-side-panel.d.ts.map +1 -1
- package/types/trismegisto-spine.d.ts +104 -28
- package/types/trismegisto-spine.d.ts.map +1 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client-picker.component-NawbfdLW.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-client.component-kAXaCM5W.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-delete-CV_t9vxm.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-groups.component-DE2QIGRH.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-news-page.component-BW7BJ0oD.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-ratings-page.component-CHPU-QLJ.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service-suggestions-page.component-CkiCTmXD.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-auto-service.component-D58Ht9PH.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-auto-service-home.component-9WvXOH3l.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-layout-suggestions-page.component-CEKMqera.mjs.map +0 -1
- package/fesm2022/trismegisto-spine-layout-trismegisto-spine-layout-DiAfxIGP.mjs.map +0 -1
|
@@ -1,28 +1,84 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, ChangeDetectorRef, ChangeDetectionStrategy, Component, InjectionToken, input, signal, computed, Injectable, PLATFORM_ID, effect, untracked, DestroyRef, Injector, model, ElementRef, output, Optional, Inject, viewChild, NgZone, isDevMode, Directive, makeEnvironmentProviders, provideEnvironmentInitializer, TemplateRef, ViewContainerRef, afterNextRender, ViewEncapsulation } from '@angular/core';
|
|
2
|
+
import { inject, ChangeDetectorRef, ChangeDetectionStrategy, Component, InjectionToken, input, signal, computed, Injectable, PLATFORM_ID, effect, untracked, DestroyRef, Injector, model, ElementRef, output, Optional, Inject, viewChild, NgZone, isDevMode, Directive, makeEnvironmentProviders, provideEnvironmentInitializer, TemplateRef, ViewContainerRef, afterNextRender, ViewEncapsulation, DOCUMENT } from '@angular/core';
|
|
3
3
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
4
4
|
import * as i3 from '@angular/router';
|
|
5
5
|
import { RouterLink, Router, NavigationEnd, NavigationSkipped, RouterOutlet } from '@angular/router';
|
|
6
6
|
import { firstValueFrom, filter, Subject, take, Subscription, debounceTime, distinctUntilChanged } from 'rxjs';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { SkipLinkComponent } from '@trismegisto/angular/skip-link';
|
|
8
|
+
import { HermesToastService, ToastStackComponent } from '@trismegisto/angular/toast';
|
|
9
|
+
import { WorkspaceComponent } from '@trismegisto/angular/workspace';
|
|
10
|
+
import { HermesSidebarService, SidebarComponent as SidebarComponent$1, SidebarNodeComponent, HERMES_SIDEBAR_CONFIG } from '@trismegisto/angular/sidebar';
|
|
9
11
|
import { ModalComponent as ModalComponent$1 } from '@trismegisto/spine/modal';
|
|
10
12
|
import { SidePanelComponent } from '@trismegisto/spine/side-panel';
|
|
11
13
|
import * as i2$1 from '@trismegisto/spine';
|
|
12
|
-
import { SPINE_MESSAGES, SPINE_CONFIG, ThemeService, resolveSpineLogo, SpinePreferencesService, SpineQuickAccessService, DensityService, ThemePresetService, SPINE_ADMIN_API, SpineUserStorage, SPINE_USER_STORAGE_KEYS, spineRoutesOf, routerIdOf, SPINE_IDLE_OPTIONS, APP_PACKAGE_INFO, registerSpineBuiltinPage } from '@trismegisto/spine';
|
|
14
|
+
import { SPINE_MESSAGES, SPINE_CONFIG, ThemeService, resolveSpineLogo, SpinePreferencesService, SpineQuickAccessService, DensityService, ThemePresetService, SPINE_ADMIN_API, resolveSpineImage, SPINE_DEFAULT_AGENT_AVATAR, SPINE_DEFAULT_AGENT_IMAGE, SpineUserStorage, SPINE_USER_STORAGE_KEYS, spineRoutesOf, routerIdOf, SPINE_IDLE_OPTIONS, APP_PACKAGE_INFO, registerSpineBuiltinPage } from '@trismegisto/spine';
|
|
13
15
|
import * as i2 from '@angular/common';
|
|
14
16
|
import { CommonModule, NgTemplateOutlet, NgComponentOutlet, isPlatformBrowser, Location } from '@angular/common';
|
|
17
|
+
import { AccountAvatarComponent } from '@trismegisto/angular/account-avatar';
|
|
18
|
+
import { CommandPaletteComponent } from '@trismegisto/angular/command-palette';
|
|
19
|
+
import { HeaderComponent as HeaderComponent$1 } from '@trismegisto/angular/header';
|
|
20
|
+
import { IconComponent } from '@trismegisto/angular/icon';
|
|
21
|
+
import { IconButtonComponent } from '@trismegisto/angular/icon-button';
|
|
22
|
+
import { MenuButtonComponent } from '@trismegisto/angular/menu-button';
|
|
15
23
|
import * as i1 from '@orfeu/angular/core';
|
|
16
24
|
import { AuthorizationService, AUTH_CONFIG, ViewService, RealmAppsService } from '@orfeu/angular/core';
|
|
17
25
|
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
18
|
-
import {
|
|
26
|
+
import { HermesThemeService, HERMES_THEME_CONFIG } from '@trismegisto/angular/theme';
|
|
27
|
+
import { formatMessage, pluralMessage } from '@trismegisto/primitives/i18n';
|
|
28
|
+
import { urlPath, formatMessageHtml } from '@trismegisto/primitives/html';
|
|
29
|
+
import { FloatingWindowComponent } from '@trismegisto/angular/floating-window';
|
|
30
|
+
import { ButtonComponent } from '@trismegisto/angular/button';
|
|
31
|
+
import { SkeletonComponent } from '@trismegisto/angular/skeleton';
|
|
32
|
+
import { TextareaComponent } from '@trismegisto/angular/textarea';
|
|
33
|
+
import { BadgeComponent } from '@trismegisto/angular/badge';
|
|
34
|
+
import { PopoverComponent, PopoverTriggerDirective } from '@trismegisto/angular/popover';
|
|
19
35
|
import { NotificationService, AlertType } from '@trismegisto/spine/notification';
|
|
36
|
+
import { HermesShortcutService, ShortcutListComponent } from '@trismegisto/angular/shortcut-list';
|
|
37
|
+
import { detectShortcutPlatform, formatShortcut } from '@trismegisto/primitives/shortcuts';
|
|
38
|
+
import { ContextMenuDirective } from '@trismegisto/angular/menu';
|
|
39
|
+
import { createPersistedWorkspaceStore, HERMES_WORKSPACE_PERSISTENCE_CONFIG } from '@trismegisto/angular/workspace-persistence';
|
|
40
|
+
import { panelIdsInOrder, findPanelTabs, canSplitPanel, cyclicPanelNeighbor } from '@trismegisto/primitives/workspace';
|
|
41
|
+
import { tabUrl } from '@trismegisto/primitives/tab-bar';
|
|
20
42
|
import { resolveEffectiveRouteChain, ComponentDistributorService, componentRouterId, LoggerService } from '@trismegisto/spine/routing';
|
|
43
|
+
import { HermesI18nService } from '@trismegisto/angular/i18n';
|
|
44
|
+
import { SegmentedControlComponent } from '@trismegisto/angular/segmented-control';
|
|
45
|
+
import { ThemePickerComponent } from '@trismegisto/angular/theme-picker';
|
|
46
|
+
import { HERMES_FONTS, FALLBACK_FAMILY } from '@trismegisto/primitives/theme';
|
|
47
|
+
import { HERMES_TEXT_SCALES } from '@trismegisto/primitives/zoom';
|
|
48
|
+
import { AlertComponent } from '@trismegisto/angular/alert';
|
|
49
|
+
import { BreadcrumbComponent } from '@trismegisto/angular/breadcrumb';
|
|
50
|
+
import { DrawerComponent } from '@trismegisto/angular/drawer';
|
|
51
|
+
import { ModalComponent } from '@trismegisto/angular/modal';
|
|
52
|
+
import { PaginatorComponent } from '@trismegisto/angular/paginator';
|
|
53
|
+
import { RichTextComponent } from '@trismegisto/angular/rich-text';
|
|
54
|
+
import { SearchFieldComponent } from '@trismegisto/angular/search-field';
|
|
55
|
+
import { CheckboxComponent } from '@trismegisto/angular/checkbox';
|
|
56
|
+
import { ComboboxComponent } from '@trismegisto/angular/combobox';
|
|
57
|
+
import { SelectComponent } from '@trismegisto/angular/select';
|
|
58
|
+
import { SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent } from '@trismegisto/angular/settings';
|
|
59
|
+
import { ToggletipComponent } from '@trismegisto/angular/toggletip';
|
|
60
|
+
import { weekdayNames } from '@trismegisto/primitives/date';
|
|
61
|
+
import { TagComponent } from '@trismegisto/angular/tag';
|
|
21
62
|
import * as i1$1 from '@angular/forms';
|
|
22
63
|
import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
64
|
+
import { CodeInputComponent } from '@trismegisto/angular/code-input';
|
|
65
|
+
import { InputComponent } from '@trismegisto/angular/input';
|
|
66
|
+
import { RadioComponent } from '@trismegisto/angular/radio';
|
|
67
|
+
import { CodeViewerComponent } from '@trismegisto/angular/code-viewer';
|
|
68
|
+
import { AvatarGroupComponent } from '@trismegisto/angular/avatar-group';
|
|
23
69
|
import { Title, DomSanitizer } from '@angular/platform-browser';
|
|
70
|
+
import { LoadingSquareComponent } from '@trismegisto/angular/loading-square';
|
|
71
|
+
import { TooltipDirective } from '@trismegisto/angular/tooltip';
|
|
24
72
|
import { Marked } from 'marked';
|
|
25
73
|
import DOMPurify from 'dompurify';
|
|
74
|
+
import { FabComponent } from '@trismegisto/angular/fab';
|
|
75
|
+
import { PromptSuggestionsComponent } from '@trismegisto/angular/prompt-suggestions';
|
|
76
|
+
import { registerUnsavedChanges, HermesUnsavedChangesService } from '@trismegisto/angular/unsaved-changes';
|
|
77
|
+
import { BubbleComponent } from '@trismegisto/angular/bubble';
|
|
78
|
+
import { MessageComponent } from '@trismegisto/angular/message';
|
|
79
|
+
import { MessageDividerComponent } from '@trismegisto/angular/message-divider';
|
|
80
|
+
import { MessageScrollerComponent } from '@trismegisto/angular/message-scroller';
|
|
81
|
+
import { isMessageContinuation } from '@trismegisto/primitives/conversation';
|
|
26
82
|
|
|
27
83
|
/**
|
|
28
84
|
* Seletor de aplicações do realm: um `hm-menu-button` próprio, só com o ícone de grade, que abre a lista das
|
|
@@ -1856,6 +1912,511 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
1856
1912
|
`, styles: [":host{display:contents}.spine-inbox{position:relative;display:inline-flex}.spine-inbox__badge{position:absolute;inset-block-start:0;inset-inline-end:0;pointer-events:none;transform:translate(25%,-15%)}.spine-inbox__list{display:flex;flex-direction:column;inline-size:min(21rem,calc(100vw - 4rem));max-block-size:min(28rem,70dvh);margin:0;padding:0;overflow-x:hidden;overflow-y:auto;list-style:none}.spine-inbox__item{display:flex;gap:var(--hm-space-sm);inline-size:100%;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:0;border-radius:var(--hm-radius);background:none;color:var(--hm-text);font:inherit;text-align:start;cursor:pointer}.spine-inbox__item:hover{background:var(--hm-hover)}.spine-inbox__item:focus-visible{outline:var(--hm-border-thin) solid var(--hm-focus-color);outline-offset:calc(var(--hm-border-thin) * -1)}.spine-inbox__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;inline-size:2rem;block-size:2rem;border-radius:50%;background:var(--hm-accent-subtle);color:var(--hm-accent)}.spine-inbox__text{display:flex;flex:1;flex-direction:column;gap:var(--hm-space-micro);min-inline-size:0}.spine-inbox__source{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.spine-inbox__title{overflow:hidden;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold);text-overflow:ellipsis;white-space:nowrap}.spine-inbox__body{display:-webkit-box;overflow:hidden;color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}.spine-inbox__meta{display:flex;gap:var(--hm-space-micro);color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.spine-inbox__empty{margin:0;padding-block:var(--hm-space-sm);color:var(--hm-muted);font-size:var(--hm-font-size-sm)}\n"] }]
|
|
1857
1913
|
}], ctorParameters: () => [] });
|
|
1858
1914
|
|
|
1915
|
+
const TOKEN_HEADER = 'x-access-token';
|
|
1916
|
+
const WS_PROTOCOL = 'graphql-transport-ws';
|
|
1917
|
+
const SUBSCRIPTION_ID = '1';
|
|
1918
|
+
const EVENTS_QUERY = `subscription ($agentId: ID!, $conversationId: ID!) {
|
|
1919
|
+
agentEvents(agentId: $agentId, conversationId: $conversationId) { type text tool title ok detail message }
|
|
1920
|
+
}`;
|
|
1921
|
+
const SEND_MUTATION = `mutation ($agentId: ID!, $input: SendAgentMessageInput!) {
|
|
1922
|
+
sendAgentMessage(agentId: $agentId, input: $input) { status message }
|
|
1923
|
+
}`;
|
|
1924
|
+
const RATE_MUTATION = `mutation ($agentId: ID!, $input: RateAgentMessageInput!) {
|
|
1925
|
+
rateAgentMessage(agentId: $agentId, input: $input) { status message }
|
|
1926
|
+
}`;
|
|
1927
|
+
const CONVERSATION_QUERY = `query ($agentId: ID!, $conversationId: ID!) {
|
|
1928
|
+
agentConversation(agentId: $agentId, conversationId: $conversationId) { status data { role text steps { tool title ok detail } } }
|
|
1929
|
+
}`;
|
|
1930
|
+
const AGENTS_QUERY = `query { agents { status data { id suggestions } } }`;
|
|
1931
|
+
/**
|
|
1932
|
+
* Assina os eventos de uma conversa e devolve a função que encerra a assinatura. Usa o WebSocket do navegador, com o
|
|
1933
|
+
* protocolo do graphql-ws: o shell também atende apps sem GraphQL, que não têm o pacote `graphql-ws` instalado.
|
|
1934
|
+
* `onClosed` avisa que a conexão caiu sem que quem assinou tenha pedido.
|
|
1935
|
+
*/
|
|
1936
|
+
function subscribeAgentEvents(url, token, variables, onEvent, onClosed) {
|
|
1937
|
+
const socket = new WebSocket(url, WS_PROTOCOL);
|
|
1938
|
+
let stopped = false;
|
|
1939
|
+
const send = (message) => socket.send(JSON.stringify(message));
|
|
1940
|
+
socket.onopen = () => send({ type: 'connection_init', payload: { [TOKEN_HEADER]: token, headers: { [TOKEN_HEADER]: token } } });
|
|
1941
|
+
socket.onmessage = ({ data }) => {
|
|
1942
|
+
const message = JSON.parse(String(data));
|
|
1943
|
+
if (message.type === 'connection_ack') {
|
|
1944
|
+
send({ id: SUBSCRIPTION_ID, type: 'subscribe', payload: { query: EVENTS_QUERY, variables } });
|
|
1945
|
+
}
|
|
1946
|
+
else if (message.type === 'ping') {
|
|
1947
|
+
send({ type: 'pong' });
|
|
1948
|
+
}
|
|
1949
|
+
else if (message.type === 'next' && message.payload?.data?.agentEvents) {
|
|
1950
|
+
onEvent(message.payload.data.agentEvents);
|
|
1951
|
+
}
|
|
1952
|
+
else if (message.type === 'next' || message.type === 'error' || message.type === 'complete') {
|
|
1953
|
+
// Recusa do gateway (agente de outro client, credencial vencida) ou fim inesperado: não há mais o que esperar.
|
|
1954
|
+
stop();
|
|
1955
|
+
onClosed();
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
socket.onclose = () => {
|
|
1959
|
+
if (!stopped) {
|
|
1960
|
+
stopped = true;
|
|
1961
|
+
onClosed();
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
function stop() {
|
|
1965
|
+
stopped = true;
|
|
1966
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
1967
|
+
send({ id: SUBSCRIPTION_ID, type: 'complete' });
|
|
1968
|
+
socket.close();
|
|
1969
|
+
}
|
|
1970
|
+
return stop;
|
|
1971
|
+
}
|
|
1972
|
+
/** Envia a mensagem. Falha de rede sobe como exceção; recusa do backend volta no envelope `{ status, message }`. */
|
|
1973
|
+
async function sendAgentMessage(url, token, variables, text) {
|
|
1974
|
+
const response = await fetch(url, {
|
|
1975
|
+
method: 'POST',
|
|
1976
|
+
headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
|
|
1977
|
+
body: JSON.stringify({
|
|
1978
|
+
query: SEND_MUTATION,
|
|
1979
|
+
variables: { agentId: variables.agentId, input: { conversationId: variables.conversationId, text } },
|
|
1980
|
+
}),
|
|
1981
|
+
});
|
|
1982
|
+
const body = await response.json();
|
|
1983
|
+
return body?.data?.sendAgentMessage ?? { status: response.status >= 400 ? response.status : 500, message: '' };
|
|
1984
|
+
}
|
|
1985
|
+
/** Avalia uma resposta, com uma correção opcional. `messageIndex` é a posição dela na conversa guardada. */
|
|
1986
|
+
async function rateAgentMessage(url, token, variables, messageIndex, rating, correction) {
|
|
1987
|
+
const response = await fetch(url, {
|
|
1988
|
+
method: 'POST',
|
|
1989
|
+
headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
|
|
1990
|
+
body: JSON.stringify({
|
|
1991
|
+
query: RATE_MUTATION,
|
|
1992
|
+
variables: { agentId: variables.agentId, input: { conversationId: variables.conversationId, messageIndex, rating, correction } },
|
|
1993
|
+
}),
|
|
1994
|
+
});
|
|
1995
|
+
const body = await response.json();
|
|
1996
|
+
return body?.data?.rateAgentMessage ?? { status: response.status >= 400 ? response.status : 500, message: '' };
|
|
1997
|
+
}
|
|
1998
|
+
/** Perguntas prontas que o agente oferece. Agente que não aparece na lista não tem sugestões. */
|
|
1999
|
+
async function fetchAgentSuggestions(url, token, agentId) {
|
|
2000
|
+
const response = await fetch(url, {
|
|
2001
|
+
method: 'POST',
|
|
2002
|
+
headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
|
|
2003
|
+
body: JSON.stringify({ query: AGENTS_QUERY }),
|
|
2004
|
+
});
|
|
2005
|
+
const body = await response.json();
|
|
2006
|
+
const agents = body?.data?.agents?.data ?? [];
|
|
2007
|
+
return agents.find((agent) => agent.id === agentId)?.suggestions ?? [];
|
|
2008
|
+
}
|
|
2009
|
+
/** As perguntas e respostas de uma conversa que já existe; vazia se ela expirou. */
|
|
2010
|
+
async function fetchAgentConversation(url, token, variables) {
|
|
2011
|
+
const response = await fetch(url, {
|
|
2012
|
+
method: 'POST',
|
|
2013
|
+
headers: { 'content-type': 'application/json', [TOKEN_HEADER]: token },
|
|
2014
|
+
body: JSON.stringify({ query: CONVERSATION_QUERY, variables }),
|
|
2015
|
+
});
|
|
2016
|
+
const body = await response.json();
|
|
2017
|
+
return body?.data?.agentConversation?.data ?? [];
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Teto para o agente dar sinal de vida; acima do limite de uma execução no backend.
|
|
2021
|
+
const SILENCE_LIMIT_MS = 200_000;
|
|
2022
|
+
const STORAGE_PREFIX = 'spine-ai-agent:conversation:';
|
|
2023
|
+
const PLACEMENT_KEY = 'spine-ai-agent:placement';
|
|
2024
|
+
// O gateway fecha a conexão quando o token vence, e a rede também cai: a resposta continua no backend. Reconecta com o token atual.
|
|
2025
|
+
const RECONNECT_ATTEMPTS = 5;
|
|
2026
|
+
const RECONNECT_DELAY_MS = 1_000;
|
|
2027
|
+
const RECOVERY_POLL_MS = 5_000;
|
|
2028
|
+
function readPlacement() {
|
|
2029
|
+
try {
|
|
2030
|
+
return JSON.parse(readStored(PLACEMENT_KEY) ?? '{}');
|
|
2031
|
+
}
|
|
2032
|
+
catch {
|
|
2033
|
+
return {};
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function readStored(key) {
|
|
2037
|
+
try {
|
|
2038
|
+
return localStorage.getItem(key);
|
|
2039
|
+
}
|
|
2040
|
+
catch {
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function writeStored(key, value) {
|
|
2045
|
+
try {
|
|
2046
|
+
localStorage.setItem(key, value);
|
|
2047
|
+
}
|
|
2048
|
+
catch {
|
|
2049
|
+
// Navegador sem armazenamento: a conversa só não volta depois de recarregar.
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Conversa com o agente do app (`SpineFeatures.aiAgent`). Fica no serviço para sobreviver a fechar e abrir o painel. O id
|
|
2054
|
+
* da conversa fica no navegador, só ele: as mensagens são do backend e voltam de lá ao recarregar a tela, e a chave lá
|
|
2055
|
+
* leva o usuário do token, então quem entrar depois nesse navegador não vê a conversa. A cada mensagem abre uma assinatura, espera o `ready` do gateway,
|
|
2056
|
+
* envia e acompanha até `done` ou `error`.
|
|
2057
|
+
*
|
|
2058
|
+
* Se a conexão cair depois do envio, o backend segue trabalhando: reconecta com o token atual, sem reenviar, e busca a resposta salva, porque os
|
|
2059
|
+
* eventos do intervalo se perderam e a resposta pode até já ter terminado.
|
|
2060
|
+
*/
|
|
2061
|
+
class SpineAiAgentService {
|
|
2062
|
+
constructor() {
|
|
2063
|
+
this.config = inject(SPINE_CONFIG, { optional: true })?.features?.aiAgent ?? null;
|
|
2064
|
+
this.auth = inject(AuthorizationService);
|
|
2065
|
+
this.text = inject(SPINE_MESSAGES).aiAgentChat;
|
|
2066
|
+
this.enabled = !!this.config;
|
|
2067
|
+
this.title = this.config?.title || this.text.title;
|
|
2068
|
+
this.theme = inject(ThemeService);
|
|
2069
|
+
this.avatarSources = resolveSpineImage(this.config?.avatar, SPINE_DEFAULT_AGENT_AVATAR);
|
|
2070
|
+
this.thinkingSources = resolveSpineImage(this.config?.avatarThinking, null);
|
|
2071
|
+
this.imageSources = resolveSpineImage(this.config?.image, SPINE_DEFAULT_AGENT_IMAGE);
|
|
2072
|
+
/** Arquivos que não carregaram: saem, em vez de virar imagem quebrada na tela. */
|
|
2073
|
+
this.failedImages = signal(new Set(), ...(ngDevMode ? [{ debugName: "failedImages" }] : /* istanbul ignore next */ []));
|
|
2074
|
+
/** O rosto do agente, para o tema atual. */
|
|
2075
|
+
this.avatar = computed(() => this.pickImage(this.avatarSources), ...(ngDevMode ? [{ debugName: "avatar" }] : /* istanbul ignore next */ []));
|
|
2076
|
+
/** A figura do agente, para o tema atual. */
|
|
2077
|
+
this.image = computed(() => this.pickImage(this.imageSources), ...(ngDevMode ? [{ debugName: "image" }] : /* istanbul ignore next */ []));
|
|
2078
|
+
this.greetingTitle = this.config?.greeting?.title || this.text.greetingTitle;
|
|
2079
|
+
this.greetingText = this.config?.greeting?.text || this.text.greetingText;
|
|
2080
|
+
this.entries = signal([], ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
|
|
2081
|
+
this.running = signal(false, ...(ngDevMode ? [{ debugName: "running" }] : /* istanbul ignore next */ []));
|
|
2082
|
+
/** Perguntas prontas que o agente oferece, mostradas na conversa vazia. */
|
|
2083
|
+
this.suggestions = signal([], ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
2084
|
+
/** O rosto de agora, que muda enquanto o agente trabalha. */
|
|
2085
|
+
this.avatarNow = computed(() => (this.running() ? this.pickImage(this.thinkingSources) : null) ?? this.avatar(), ...(ngDevMode ? [{ debugName: "avatarNow" }] : /* istanbul ignore next */ []));
|
|
2086
|
+
this.panelOpen = signal(false, ...(ngDevMode ? [{ debugName: "panelOpen" }] : /* istanbul ignore next */ []));
|
|
2087
|
+
/** O balão saiu da tela e virou um ícone no cabeçalho. */
|
|
2088
|
+
this.docked = signal(readPlacement().docked === true, ...(ngDevMode ? [{ debugName: "docked" }] : /* istanbul ignore next */ []));
|
|
2089
|
+
this.position = signal(readPlacement().position ?? null, ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
2090
|
+
/** A pessoa pediu o painel agora: a conversa recebe o foco. Ao recarregar a tela, não. */
|
|
2091
|
+
this.focusOnOpen = false;
|
|
2092
|
+
this.storageKey = `${STORAGE_PREFIX}${this.config?.agent ?? ''}`;
|
|
2093
|
+
this.storedConversation = readStored(this.storageKey);
|
|
2094
|
+
this.conversationId = this.storedConversation ?? this.newConversationId();
|
|
2095
|
+
this.restored = !this.storedConversation;
|
|
2096
|
+
this.nextId = 0;
|
|
2097
|
+
this.stop = null;
|
|
2098
|
+
this.silence = null;
|
|
2099
|
+
this.reconnectTimer = null;
|
|
2100
|
+
this.recoveryTimer = null;
|
|
2101
|
+
/** A mensagem já foi enviada: daqui em diante, a conexão pode cair sem perder a resposta. */
|
|
2102
|
+
this.messageSent = false;
|
|
2103
|
+
this.reconnects = 0;
|
|
2104
|
+
this.resumed = false;
|
|
2105
|
+
/** Mensagens que o backend já tinha guardado antes desta pergunta, para reconhecer a resposta nova. */
|
|
2106
|
+
this.savedBefore = 0;
|
|
2107
|
+
this.suggestionsLoaded = false;
|
|
2108
|
+
}
|
|
2109
|
+
/** Como no logo do header: no escuro vale `dark`, e se ele falhar volta para `light`. */
|
|
2110
|
+
pickImage(sources) {
|
|
2111
|
+
if (!this.config || !sources)
|
|
2112
|
+
return null;
|
|
2113
|
+
const failed = this.failedImages();
|
|
2114
|
+
if (this.theme.current === 'dark' && sources.dark && !failed.has(sources.dark))
|
|
2115
|
+
return sources.dark;
|
|
2116
|
+
return failed.has(sources.light) ? null : sources.light;
|
|
2117
|
+
}
|
|
2118
|
+
imageFailed(src) {
|
|
2119
|
+
if (!this.failedImages().has(src))
|
|
2120
|
+
this.failedImages.update((failed) => new Set(failed).add(src));
|
|
2121
|
+
}
|
|
2122
|
+
togglePanel() {
|
|
2123
|
+
this.focusOnOpen = !this.panelOpen();
|
|
2124
|
+
this.panelOpen.set(this.focusOnOpen);
|
|
2125
|
+
}
|
|
2126
|
+
dock() {
|
|
2127
|
+
this.docked.set(true);
|
|
2128
|
+
this.savePlacement();
|
|
2129
|
+
}
|
|
2130
|
+
undock() {
|
|
2131
|
+
this.docked.set(false);
|
|
2132
|
+
this.savePlacement();
|
|
2133
|
+
}
|
|
2134
|
+
savePlacement() {
|
|
2135
|
+
writeStored(PLACEMENT_KEY, JSON.stringify({ docked: this.docked(), position: this.position() }));
|
|
2136
|
+
}
|
|
2137
|
+
/** Abre o painel e pergunta, para quem escolheu uma sugestão no convite. */
|
|
2138
|
+
askFromGreeting(question) {
|
|
2139
|
+
this.focusOnOpen = true;
|
|
2140
|
+
this.panelOpen.set(true);
|
|
2141
|
+
this.restored = true;
|
|
2142
|
+
this.send(question);
|
|
2143
|
+
}
|
|
2144
|
+
/** Busca as sugestões uma vez. Sem elas o chat funciona igual, então a falha não aparece para a pessoa. */
|
|
2145
|
+
loadSuggestions() {
|
|
2146
|
+
const token = this.auth.getToken();
|
|
2147
|
+
if (!this.config || !token || this.suggestionsLoaded)
|
|
2148
|
+
return;
|
|
2149
|
+
this.suggestionsLoaded = true;
|
|
2150
|
+
fetchAgentSuggestions(this.config.http, token, this.config.agent).then((suggestions) => this.suggestions.set(suggestions), () => { this.suggestionsLoaded = false; });
|
|
2151
|
+
}
|
|
2152
|
+
/** Traz de volta a conversa guardada, uma vez. Se falhar, ou se ela expirou, o chat só começa vazio. */
|
|
2153
|
+
restoreConversation() {
|
|
2154
|
+
const token = this.auth.getToken();
|
|
2155
|
+
if (!this.config || !token || this.restored)
|
|
2156
|
+
return;
|
|
2157
|
+
this.restored = true;
|
|
2158
|
+
const conversationId = this.conversationId;
|
|
2159
|
+
fetchAgentConversation(this.config.http, token, { agentId: this.config.agent, conversationId }).then((messages) => {
|
|
2160
|
+
// A pessoa pode ter começado a escrever antes de a resposta chegar: o que está na tela vence.
|
|
2161
|
+
if (this.entries().length || conversationId !== this.conversationId)
|
|
2162
|
+
return;
|
|
2163
|
+
for (const message of messages) {
|
|
2164
|
+
const id = this.append(message.role, message.text);
|
|
2165
|
+
const steps = this.storedSteps(message.steps);
|
|
2166
|
+
if (steps.length)
|
|
2167
|
+
this.update(id, (entry) => ({ ...entry, steps }));
|
|
2168
|
+
}
|
|
2169
|
+
}, () => { this.restored = false; });
|
|
2170
|
+
}
|
|
2171
|
+
/** Começa outra conversa; a anterior expira sozinha no backend. */
|
|
2172
|
+
reset() {
|
|
2173
|
+
this.finish();
|
|
2174
|
+
this.entries.set([]);
|
|
2175
|
+
this.restored = true;
|
|
2176
|
+
this.conversationId = this.newConversationId();
|
|
2177
|
+
}
|
|
2178
|
+
/** Avalia uma resposta. A posição conta só o que o backend guardou: resposta que falhou não entra no histórico dele. */
|
|
2179
|
+
async rate(entryId, rating, correction = '') {
|
|
2180
|
+
const token = this.auth.getToken();
|
|
2181
|
+
const saved = this.entries().filter((entry) => !entry.error);
|
|
2182
|
+
const index = saved.findIndex((entry) => entry.id === entryId);
|
|
2183
|
+
if (!this.config || !token || index < 1)
|
|
2184
|
+
return false;
|
|
2185
|
+
const variables = { agentId: this.config.agent, conversationId: this.conversationId };
|
|
2186
|
+
try {
|
|
2187
|
+
const { status } = await rateAgentMessage(this.config.http, token, variables, index, rating, correction);
|
|
2188
|
+
if (status >= 400)
|
|
2189
|
+
return false;
|
|
2190
|
+
}
|
|
2191
|
+
catch {
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
this.update(entryId, (entry) => ({ ...entry, rating }));
|
|
2195
|
+
return true;
|
|
2196
|
+
}
|
|
2197
|
+
/** Repete a última pergunta depois de um erro. Resposta que falhou não entra no histórico do backend. */
|
|
2198
|
+
retry() {
|
|
2199
|
+
const entries = this.entries();
|
|
2200
|
+
const question = entries[entries.length - 2];
|
|
2201
|
+
if (this.running() || !entries[entries.length - 1]?.error || question?.role !== 'person')
|
|
2202
|
+
return;
|
|
2203
|
+
this.entries.set(entries.slice(0, -2));
|
|
2204
|
+
this.send(question.text);
|
|
2205
|
+
}
|
|
2206
|
+
send(raw) {
|
|
2207
|
+
const text = raw.trim();
|
|
2208
|
+
const token = this.auth.getToken();
|
|
2209
|
+
if (!this.config || !text || this.running())
|
|
2210
|
+
return;
|
|
2211
|
+
this.savedBefore = this.entries().filter((entry) => !entry.error).length;
|
|
2212
|
+
this.append('person', text);
|
|
2213
|
+
const answerId = this.append('agent', '');
|
|
2214
|
+
if (!token) {
|
|
2215
|
+
this.fail(answerId, this.text.sendError);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
this.running.set(true);
|
|
2219
|
+
this.listen(answerId, { agentId: this.config.agent, conversationId: this.conversationId }, text);
|
|
2220
|
+
}
|
|
2221
|
+
listen(answerId, variables, question) {
|
|
2222
|
+
const token = this.auth.getToken();
|
|
2223
|
+
if (!this.config || !token) {
|
|
2224
|
+
this.fail(answerId, this.text.connectionError);
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
const http = this.config.http;
|
|
2228
|
+
this.watchSilence(answerId);
|
|
2229
|
+
this.stop = subscribeAgentEvents(this.config.ws, token, variables, (event) => {
|
|
2230
|
+
this.watchSilence(answerId);
|
|
2231
|
+
if (event.type === 'ready') {
|
|
2232
|
+
this.onReady(answerId, variables, question, http, token);
|
|
2233
|
+
}
|
|
2234
|
+
else if (event.type === 'done' && this.resumed) {
|
|
2235
|
+
void this.loadAnswer(answerId, variables, question).finally(() => this.finish());
|
|
2236
|
+
}
|
|
2237
|
+
else {
|
|
2238
|
+
this.apply(answerId, event);
|
|
2239
|
+
}
|
|
2240
|
+
}, () => this.onDisconnected(answerId, variables, question));
|
|
2241
|
+
}
|
|
2242
|
+
onReady(answerId, variables, question, http, token) {
|
|
2243
|
+
if (this.messageSent) {
|
|
2244
|
+
this.reconnects = 0;
|
|
2245
|
+
this.resumed = true;
|
|
2246
|
+
void this.pollAnswer(answerId, variables, question);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
this.messageSent = true;
|
|
2250
|
+
sendAgentMessage(http, token, variables, question).then(({ status, message }) => { if (status >= 400)
|
|
2251
|
+
this.fail(answerId, message || this.text.sendError); }, () => this.fail(answerId, this.text.sendError));
|
|
2252
|
+
}
|
|
2253
|
+
onDisconnected(answerId, variables, question) {
|
|
2254
|
+
if (!this.running())
|
|
2255
|
+
return;
|
|
2256
|
+
// Antes do envio, ou depois de esgotar as tentativas, ninguém está trabalhando nesta resposta ou não há como acompanhar.
|
|
2257
|
+
if (!this.messageSent || this.reconnects >= RECONNECT_ATTEMPTS) {
|
|
2258
|
+
this.fail(answerId, this.text.connectionError);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
this.reconnects += 1;
|
|
2262
|
+
this.stop = null;
|
|
2263
|
+
this.reconnectTimer = setTimeout(() => this.listen(answerId, variables, question), RECONNECT_DELAY_MS * this.reconnects);
|
|
2264
|
+
}
|
|
2265
|
+
async pollAnswer(answerId, variables, question) {
|
|
2266
|
+
if (await this.loadAnswer(answerId, variables, question)) {
|
|
2267
|
+
this.finish();
|
|
2268
|
+
}
|
|
2269
|
+
else if (this.running()) {
|
|
2270
|
+
this.recoveryTimer = setTimeout(() => void this.pollAnswer(answerId, variables, question), RECOVERY_POLL_MS);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
/** Traz a resposta salva, se ela já terminou. Serve também para repor o que se perdeu enquanto a conexão estava fora. */
|
|
2274
|
+
async loadAnswer(answerId, variables, question) {
|
|
2275
|
+
const token = this.auth.getToken();
|
|
2276
|
+
if (!this.config || !token)
|
|
2277
|
+
return false;
|
|
2278
|
+
try {
|
|
2279
|
+
const messages = await fetchAgentConversation(this.config.http, token, variables);
|
|
2280
|
+
const answer = messages[messages.length - 1];
|
|
2281
|
+
const asked = messages[messages.length - 2];
|
|
2282
|
+
if (answer?.role !== 'agent' || asked?.text !== question || messages.length <= this.savedBefore)
|
|
2283
|
+
return false;
|
|
2284
|
+
const steps = this.storedSteps(answer.steps);
|
|
2285
|
+
this.update(answerId, (entry) => ({ ...entry, text: answer.text, steps: steps.length ? steps : entry.steps }));
|
|
2286
|
+
return true;
|
|
2287
|
+
}
|
|
2288
|
+
catch {
|
|
2289
|
+
return false;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
storedSteps(steps) {
|
|
2293
|
+
return (steps ?? []).map((step) => ({
|
|
2294
|
+
id: ++this.nextId, tool: step.tool, title: step.title, status: step.ok ? 'done' : 'error', detail: step.detail ?? undefined,
|
|
2295
|
+
}));
|
|
2296
|
+
}
|
|
2297
|
+
newConversationId() {
|
|
2298
|
+
const id = crypto.randomUUID();
|
|
2299
|
+
writeStored(this.storageKey, id);
|
|
2300
|
+
return id;
|
|
2301
|
+
}
|
|
2302
|
+
apply(answerId, event) {
|
|
2303
|
+
switch (event.type) {
|
|
2304
|
+
case 'text_delta':
|
|
2305
|
+
this.update(answerId, (entry) => ({ ...entry, text: entry.text + (event.text ?? '') }));
|
|
2306
|
+
break;
|
|
2307
|
+
case 'text_replace':
|
|
2308
|
+
this.update(answerId, (entry) => ({ ...entry, text: event.text ?? '' }));
|
|
2309
|
+
break;
|
|
2310
|
+
case 'tool_started': {
|
|
2311
|
+
const tool = event.tool ?? '';
|
|
2312
|
+
const step = { id: ++this.nextId, tool, title: event.title || tool, status: 'running' };
|
|
2313
|
+
this.update(answerId, (entry) => ({ ...entry, steps: [...entry.steps, step] }));
|
|
2314
|
+
break;
|
|
2315
|
+
}
|
|
2316
|
+
case 'tool_finished':
|
|
2317
|
+
this.finishStep(answerId, event.tool ?? '', event.ok === false ? 'error' : 'done', event.detail ?? undefined);
|
|
2318
|
+
break;
|
|
2319
|
+
case 'done':
|
|
2320
|
+
this.finish();
|
|
2321
|
+
break;
|
|
2322
|
+
case 'error':
|
|
2323
|
+
this.fail(answerId, event.message || this.text.sendError);
|
|
2324
|
+
break;
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
/** A mesma ferramenta pode rodar mais de uma vez: encerra a chamada mais antiga que ainda está em andamento. */
|
|
2328
|
+
finishStep(answerId, tool, status, detail) {
|
|
2329
|
+
this.update(answerId, (entry) => {
|
|
2330
|
+
const index = entry.steps.findIndex((step) => step.tool === tool && step.status === 'running');
|
|
2331
|
+
return { ...entry, steps: entry.steps.map((step, i) => (i === index ? { ...step, status, detail } : step)) };
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
fail(answerId, message) {
|
|
2335
|
+
this.update(answerId, (entry) => ({
|
|
2336
|
+
...entry,
|
|
2337
|
+
text: message,
|
|
2338
|
+
error: true,
|
|
2339
|
+
steps: entry.steps.map((step) => (step.status === 'running' ? { ...step, status: 'error' } : step)),
|
|
2340
|
+
}));
|
|
2341
|
+
this.finish();
|
|
2342
|
+
}
|
|
2343
|
+
finish() {
|
|
2344
|
+
if (this.silence)
|
|
2345
|
+
clearTimeout(this.silence);
|
|
2346
|
+
if (this.reconnectTimer)
|
|
2347
|
+
clearTimeout(this.reconnectTimer);
|
|
2348
|
+
if (this.recoveryTimer)
|
|
2349
|
+
clearTimeout(this.recoveryTimer);
|
|
2350
|
+
this.silence = this.reconnectTimer = this.recoveryTimer = null;
|
|
2351
|
+
this.messageSent = this.resumed = false;
|
|
2352
|
+
this.reconnects = 0;
|
|
2353
|
+
this.running.set(false);
|
|
2354
|
+
const stop = this.stop;
|
|
2355
|
+
this.stop = null;
|
|
2356
|
+
stop?.();
|
|
2357
|
+
}
|
|
2358
|
+
watchSilence(answerId) {
|
|
2359
|
+
if (this.silence)
|
|
2360
|
+
clearTimeout(this.silence);
|
|
2361
|
+
this.silence = setTimeout(() => this.fail(answerId, this.text.connectionError), SILENCE_LIMIT_MS);
|
|
2362
|
+
}
|
|
2363
|
+
append(role, text) {
|
|
2364
|
+
const id = ++this.nextId;
|
|
2365
|
+
this.entries.update((entries) => [...entries, { id, role, text, steps: [], error: false }]);
|
|
2366
|
+
return id;
|
|
2367
|
+
}
|
|
2368
|
+
update(id, change) {
|
|
2369
|
+
this.entries.update((entries) => entries.map((entry) => (entry.id === id ? change(entry) : entry)));
|
|
2370
|
+
}
|
|
2371
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2372
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentService, providedIn: 'root' }); }
|
|
2373
|
+
}
|
|
2374
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentService, decorators: [{
|
|
2375
|
+
type: Injectable,
|
|
2376
|
+
args: [{ providedIn: 'root' }]
|
|
2377
|
+
}] });
|
|
2378
|
+
|
|
2379
|
+
const SPINE_AGENT_DOCK_CLASS = 'spine-agent-dock';
|
|
2380
|
+
/** O agente no cabeçalho, ao lado do sino: só aparece depois que a pessoa tira o balão da tela. */
|
|
2381
|
+
class SpineAiAgentDockButtonComponent {
|
|
2382
|
+
constructor() {
|
|
2383
|
+
this.agent = inject(SpineAiAgentService);
|
|
2384
|
+
this.text = inject(SPINE_MESSAGES).aiAgentChat;
|
|
2385
|
+
this.dockClass = SPINE_AGENT_DOCK_CLASS;
|
|
2386
|
+
}
|
|
2387
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentDockButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2388
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAiAgentDockButtonComponent, isStandalone: true, selector: "spine-ai-agent-dock-button", ngImport: i0, template: `
|
|
2389
|
+
@if (agent.enabled && agent.docked()) {
|
|
2390
|
+
<hm-icon-button
|
|
2391
|
+
[class]="dockClass"
|
|
2392
|
+
emphasis="subtle"
|
|
2393
|
+
icon="chat-bubble"
|
|
2394
|
+
[label]="agent.panelOpen() ? text.close : agent.title"
|
|
2395
|
+
(clicked)="agent.togglePanel()"
|
|
2396
|
+
/>
|
|
2397
|
+
}
|
|
2398
|
+
`, isInline: true, dependencies: [{ kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2399
|
+
}
|
|
2400
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentDockButtonComponent, decorators: [{
|
|
2401
|
+
type: Component,
|
|
2402
|
+
args: [{
|
|
2403
|
+
selector: 'spine-ai-agent-dock-button',
|
|
2404
|
+
imports: [IconButtonComponent],
|
|
2405
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2406
|
+
template: `
|
|
2407
|
+
@if (agent.enabled && agent.docked()) {
|
|
2408
|
+
<hm-icon-button
|
|
2409
|
+
[class]="dockClass"
|
|
2410
|
+
emphasis="subtle"
|
|
2411
|
+
icon="chat-bubble"
|
|
2412
|
+
[label]="agent.panelOpen() ? text.close : agent.title"
|
|
2413
|
+
(clicked)="agent.togglePanel()"
|
|
2414
|
+
/>
|
|
2415
|
+
}
|
|
2416
|
+
`,
|
|
2417
|
+
}]
|
|
2418
|
+
}] });
|
|
2419
|
+
|
|
1859
2420
|
/** Tempo do aviso de falha na tela (segundos). */
|
|
1860
2421
|
const LOGOUT_FAILED_SECONDS = 8;
|
|
1861
2422
|
/**
|
|
@@ -2415,7 +2976,7 @@ class HeaderComponent {
|
|
|
2415
2976
|
catch { /* storage cheio/indisponível */ }
|
|
2416
2977
|
}
|
|
2417
2978
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HeaderComponent, deps: [{ token: i1.AuthorizationService }, { token: i2$1.ThemeService }, { token: i2$1.ThemePresetService }, { token: i2$1.DensityService }, { token: i1.ViewService }, { token: i3.Router }, { token: ShortcutService }, { token: SPINE_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2418
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: HeaderComponent, isStandalone: true, selector: "spine-header", ngImport: i0, template: "<hm-header [class.header-blocked]=\"headerBlocked\" (searchRequested)=\"onSearchRequested()\">\n <div *ngIf=\"headerBlocked\" class=\"header-overlay\"></div>\n\n <spine-app-launcher></spine-app-launcher>\n\n <!-- Marca: logo do app (SpineConfig.logo, padr\u00E3o assets/images/logo.svg) + SpineConfig.appName,\n levando \u00E0 primeira rota naveg\u00E1vel. Ver SpineBrandComponent. -->\n <spine-brand [homeRoute]=\"homeRoute\" [label]=\"text.home\" />\n\n <div actions class=\"h-right\">\n <!-- central de notifica\u00E7\u00F5es: o sino, com as novidades das fontes registradas (sem fonte, n\u00E3o aparece) -->\n <spine-inbox-button />\n\n <!-- tema claro/escuro -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setTheme(themeService.current == 'light' ? 'dark' : 'light')\"\n [icon]=\"themeService.current == 'light' ? 'moon' : 'sun'\"\n [label]=\"themeService.current == 'light' ? text.themeDark : text.themeLight\">\n </hm-icon-button>\n\n <!-- densidade -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setDensity()\"\n [icon]=\"isCompactDensity ? 'density-comfortable' : 'density-compact'\"\n [label]=\"isCompactDensity ? text.densityComfortable : text.densityCompact\">\n </hm-icon-button>\n\n <!-- Avaliar o {app} (SpineFeatures.ratings): a estrela abre o di\u00E1logo e fica cheia depois de avaliar. Toma o\n lugar do bot\u00E3o de tela cheia (o atalho de teclado continua); sem avalia\u00E7\u00E3o, a tela cheia volta aqui. -->\n @if (showFullscreenButton) {\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"toggleFullscreen()\"\n [icon]=\"isFullscreen ? 'shrink' : 'resize'\"\n [label]=\"isFullscreen ? text.exitFullscreen : text.fullscreen\">\n </hm-icon-button>\n } @else {\n <spine-rate-app-button />\n }\n\n <span class=\"h-divider\" aria-hidden=\"true\"></span>\n\n <!-- perfil: crach\u00E1, ajuda, tema e sair ficam dentro do menu -->\n <hm-menu-button class=\"h-profile\" [label]=\"profile.name ?? text.user\" align=\"end\" [items]=\"profileMenuItems\" (chosen)=\"onProfileMenuChosen($event)\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\"></hm-account-avatar>\n <span class=\"h-profile-info\">\n <span class=\"h-profile-name\">{{ profile.name ?? text.user }}</span>\n <span class=\"h-profile-role\" *ngIf=\"badge\">{{ badge?.name }}</span>\n </span>\n </hm-menu-button>\n </div>\n</hm-header>\n\n<!-- Busca global: paleta ancorada logo abaixo do cabe\u00E7alho. Campo vazio = favoritos, abertos h\u00E1 pouco e\n m\u00F3dulos; com texto, busca em todas as telas. Setas/Enter navegam, Esc fecha. -->\n<hm-command-palette\n [(open)]=\"quickOpen\"\n [commands]=\"searchCommands\"\n [browseGroups]=\"searchBrowse\"\n [groupIcons]=\"searchIcons\"\n [label]=\"text.search\"\n [placeholder]=\"text.searchPlaceholder\"\n/>\n\n<!-- A janela de avalia\u00E7\u00E3o (flutuante, canto inferior direito): \"Avaliar o {app}\" da estrela do cabe\u00E7alho e de\n Configura\u00E7\u00F5es \u203A Sobre, e \"Avaliar esta tela\" da barra da p\u00E1gina (SpineFeatures.ratings). S\u00F3 desenha com o recurso\n ligado e aberta. -->\n<spine-rating-window />\n", styles: ["@charset \"UTF-8\";:host{display:contents}.header-overlay{position:absolute;inset:0;z-index:9999;cursor:not-allowed}.h-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:40px;min-width:40px;padding:0 10px;border:none;background:transparent;cursor:pointer;border-radius:9px;text-decoration:none;color:var(--hm-navbar-text);outline:none}.h-btn:focus-visible,.h-btn:hover{background:var(--hm-navbar-hover)}.h-btn.active{background:var(--hm-navbar-hover)}.h-right{display:flex;align-items:center;gap:var(--hm-header-action-gap)}.h-divider{width:1px;height:24px;margin:0 4px;background:var(--hm-navbar-hover)}.h-profile{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 4px 0 2px}.h-profile hm-account-avatar{background:color-mix(in srgb,var(--hm-navbar-text) 18%,transparent);color:var(--hm-navbar-text)}.h-profile-info{display:flex;flex-direction:column;min-width:0;max-width:140px;line-height:1.2;text-align:left}.h-profile-name{font-size:var(--hm-font-size-label);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.h-profile-role{color:color-mix(in srgb,var(--hm-navbar-text) 65%,transparent);font-size:var(--hm-font-size-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: HeaderComponent$1, selector: "hm-header", inputs: ["searchPlaceholder", "searchLabel"], outputs: ["searchRequested"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: CommandPaletteComponent, selector: "hm-command-palette", inputs: ["open", "commands", "shortcut", "label", "placeholder", "recentLimit", "recentIds", "closeOnRun", "browseGroups", "groupIcons"], outputs: ["openChange", "recentIdsChange", "executed"] }, { kind: "component", type: AppLauncherComponent, selector: "spine-app-launcher" }, { kind: "component", type: SpineBrandComponent, selector: "spine-brand", inputs: ["homeRoute", "label"] }, { kind: "component", type: SpineRatingWindowComponent, selector: "spine-rating-window" }, { kind: "component", type: SpineRateAppButtonComponent, selector: "spine-rate-app-button" }, { kind: "component", type: SpineInboxButtonComponent, selector: "spine-inbox-button" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2979
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: HeaderComponent, isStandalone: true, selector: "spine-header", ngImport: i0, template: "<hm-header [class.header-blocked]=\"headerBlocked\" (searchRequested)=\"onSearchRequested()\">\n <div *ngIf=\"headerBlocked\" class=\"header-overlay\"></div>\n\n <spine-app-launcher></spine-app-launcher>\n\n <!-- Marca: logo do app (SpineConfig.logo, padr\u00E3o assets/images/logo.svg) + SpineConfig.appName,\n levando \u00E0 primeira rota naveg\u00E1vel. Ver SpineBrandComponent. -->\n <spine-brand [homeRoute]=\"homeRoute\" [label]=\"text.home\" />\n\n <div actions class=\"h-right\">\n <!-- central de notifica\u00E7\u00F5es: o sino, com as novidades das fontes registradas (sem fonte, n\u00E3o aparece) -->\n <spine-inbox-button />\n\n <!-- o agente de IA, depois que a pessoa tira o bal\u00E3o da tela -->\n <spine-ai-agent-dock-button />\n\n <!-- tema claro/escuro -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setTheme(themeService.current == 'light' ? 'dark' : 'light')\"\n [icon]=\"themeService.current == 'light' ? 'moon' : 'sun'\"\n [label]=\"themeService.current == 'light' ? text.themeDark : text.themeLight\">\n </hm-icon-button>\n\n <!-- densidade -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setDensity()\"\n [icon]=\"isCompactDensity ? 'density-comfortable' : 'density-compact'\"\n [label]=\"isCompactDensity ? text.densityComfortable : text.densityCompact\">\n </hm-icon-button>\n\n <!-- Avaliar o {app} (SpineFeatures.ratings): a estrela abre o di\u00E1logo e fica cheia depois de avaliar. Toma o\n lugar do bot\u00E3o de tela cheia (o atalho de teclado continua); sem avalia\u00E7\u00E3o, a tela cheia volta aqui. -->\n @if (showFullscreenButton) {\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"toggleFullscreen()\"\n [icon]=\"isFullscreen ? 'shrink' : 'resize'\"\n [label]=\"isFullscreen ? text.exitFullscreen : text.fullscreen\">\n </hm-icon-button>\n } @else {\n <spine-rate-app-button />\n }\n\n <span class=\"h-divider\" aria-hidden=\"true\"></span>\n\n <!-- perfil: crach\u00E1, ajuda, tema e sair ficam dentro do menu -->\n <hm-menu-button class=\"h-profile\" [label]=\"profile.name ?? text.user\" align=\"end\" [items]=\"profileMenuItems\" (chosen)=\"onProfileMenuChosen($event)\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\"></hm-account-avatar>\n <span class=\"h-profile-info\">\n <span class=\"h-profile-name\">{{ profile.name ?? text.user }}</span>\n <span class=\"h-profile-role\" *ngIf=\"badge\">{{ badge?.name }}</span>\n </span>\n </hm-menu-button>\n </div>\n</hm-header>\n\n<!-- Busca global: paleta ancorada logo abaixo do cabe\u00E7alho. Campo vazio = favoritos, abertos h\u00E1 pouco e\n m\u00F3dulos; com texto, busca em todas as telas. Setas/Enter navegam, Esc fecha. -->\n<hm-command-palette\n [(open)]=\"quickOpen\"\n [commands]=\"searchCommands\"\n [browseGroups]=\"searchBrowse\"\n [groupIcons]=\"searchIcons\"\n [label]=\"text.search\"\n [placeholder]=\"text.searchPlaceholder\"\n/>\n\n<!-- A janela de avalia\u00E7\u00E3o (flutuante, canto inferior direito): \"Avaliar o {app}\" da estrela do cabe\u00E7alho e de\n Configura\u00E7\u00F5es \u203A Sobre, e \"Avaliar esta tela\" da barra da p\u00E1gina (SpineFeatures.ratings). S\u00F3 desenha com o recurso\n ligado e aberta. -->\n<spine-rating-window />\n", styles: ["@charset \"UTF-8\";:host{display:contents}.header-overlay{position:absolute;inset:0;z-index:9999;cursor:not-allowed}.h-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:40px;min-width:40px;padding:0 10px;border:none;background:transparent;cursor:pointer;border-radius:9px;text-decoration:none;color:var(--hm-navbar-text);outline:none}.h-btn:focus-visible,.h-btn:hover{background:var(--hm-navbar-hover)}.h-btn.active{background:var(--hm-navbar-hover)}.h-right{display:flex;align-items:center;gap:var(--hm-header-action-gap)}.h-divider{width:1px;height:24px;margin:0 4px;background:var(--hm-navbar-hover)}.h-profile{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 4px 0 2px}.h-profile hm-account-avatar{background:color-mix(in srgb,var(--hm-navbar-text) 18%,transparent);color:var(--hm-navbar-text)}.h-profile-info{display:flex;flex-direction:column;min-width:0;max-width:140px;line-height:1.2;text-align:left}.h-profile-name{font-size:var(--hm-font-size-label);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.h-profile-role{color:color-mix(in srgb,var(--hm-navbar-text) 65%,transparent);font-size:var(--hm-font-size-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: HeaderComponent$1, selector: "hm-header", inputs: ["searchPlaceholder", "searchLabel"], outputs: ["searchRequested"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "component", type: MenuButtonComponent, selector: "hm-menu-button", inputs: ["label", "icon", "items", "showArrow", "align", "side", "size", "shape", "layer", "closeOnCheck", "disabled"], outputs: ["chosen", "checkedChange"] }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: CommandPaletteComponent, selector: "hm-command-palette", inputs: ["open", "commands", "shortcut", "label", "placeholder", "recentLimit", "recentIds", "closeOnRun", "browseGroups", "groupIcons"], outputs: ["openChange", "recentIdsChange", "executed"] }, { kind: "component", type: AppLauncherComponent, selector: "spine-app-launcher" }, { kind: "component", type: SpineBrandComponent, selector: "spine-brand", inputs: ["homeRoute", "label"] }, { kind: "component", type: SpineRatingWindowComponent, selector: "spine-rating-window" }, { kind: "component", type: SpineRateAppButtonComponent, selector: "spine-rate-app-button" }, { kind: "component", type: SpineInboxButtonComponent, selector: "spine-inbox-button" }, { kind: "component", type: SpineAiAgentDockButtonComponent, selector: "spine-ai-agent-dock-button" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2419
2980
|
}
|
|
2420
2981
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: HeaderComponent, decorators: [{
|
|
2421
2982
|
type: Component,
|
|
@@ -2423,7 +2984,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
2423
2984
|
CommonModule,
|
|
2424
2985
|
HeaderComponent$1, IconButtonComponent, IconComponent, MenuButtonComponent, AccountAvatarComponent, CommandPaletteComponent,
|
|
2425
2986
|
AppLauncherComponent, SpineBrandComponent, SpineRatingWindowComponent, SpineRateAppButtonComponent, SpineInboxButtonComponent,
|
|
2426
|
-
|
|
2987
|
+
SpineAiAgentDockButtonComponent,
|
|
2988
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<hm-header [class.header-blocked]=\"headerBlocked\" (searchRequested)=\"onSearchRequested()\">\n <div *ngIf=\"headerBlocked\" class=\"header-overlay\"></div>\n\n <spine-app-launcher></spine-app-launcher>\n\n <!-- Marca: logo do app (SpineConfig.logo, padr\u00E3o assets/images/logo.svg) + SpineConfig.appName,\n levando \u00E0 primeira rota naveg\u00E1vel. Ver SpineBrandComponent. -->\n <spine-brand [homeRoute]=\"homeRoute\" [label]=\"text.home\" />\n\n <div actions class=\"h-right\">\n <!-- central de notifica\u00E7\u00F5es: o sino, com as novidades das fontes registradas (sem fonte, n\u00E3o aparece) -->\n <spine-inbox-button />\n\n <!-- o agente de IA, depois que a pessoa tira o bal\u00E3o da tela -->\n <spine-ai-agent-dock-button />\n\n <!-- tema claro/escuro -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setTheme(themeService.current == 'light' ? 'dark' : 'light')\"\n [icon]=\"themeService.current == 'light' ? 'moon' : 'sun'\"\n [label]=\"themeService.current == 'light' ? text.themeDark : text.themeLight\">\n </hm-icon-button>\n\n <!-- densidade -->\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"setDensity()\"\n [icon]=\"isCompactDensity ? 'density-comfortable' : 'density-compact'\"\n [label]=\"isCompactDensity ? text.densityComfortable : text.densityCompact\">\n </hm-icon-button>\n\n <!-- Avaliar o {app} (SpineFeatures.ratings): a estrela abre o di\u00E1logo e fica cheia depois de avaliar. Toma o\n lugar do bot\u00E3o de tela cheia (o atalho de teclado continua); sem avalia\u00E7\u00E3o, a tela cheia volta aqui. -->\n @if (showFullscreenButton) {\n <hm-icon-button emphasis=\"subtle\" (clicked)=\"toggleFullscreen()\"\n [icon]=\"isFullscreen ? 'shrink' : 'resize'\"\n [label]=\"isFullscreen ? text.exitFullscreen : text.fullscreen\">\n </hm-icon-button>\n } @else {\n <spine-rate-app-button />\n }\n\n <span class=\"h-divider\" aria-hidden=\"true\"></span>\n\n <!-- perfil: crach\u00E1, ajuda, tema e sair ficam dentro do menu -->\n <hm-menu-button class=\"h-profile\" [label]=\"profile.name ?? text.user\" align=\"end\" [items]=\"profileMenuItems\" (chosen)=\"onProfileMenuChosen($event)\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\"></hm-account-avatar>\n <span class=\"h-profile-info\">\n <span class=\"h-profile-name\">{{ profile.name ?? text.user }}</span>\n <span class=\"h-profile-role\" *ngIf=\"badge\">{{ badge?.name }}</span>\n </span>\n </hm-menu-button>\n </div>\n</hm-header>\n\n<!-- Busca global: paleta ancorada logo abaixo do cabe\u00E7alho. Campo vazio = favoritos, abertos h\u00E1 pouco e\n m\u00F3dulos; com texto, busca em todas as telas. Setas/Enter navegam, Esc fecha. -->\n<hm-command-palette\n [(open)]=\"quickOpen\"\n [commands]=\"searchCommands\"\n [browseGroups]=\"searchBrowse\"\n [groupIcons]=\"searchIcons\"\n [label]=\"text.search\"\n [placeholder]=\"text.searchPlaceholder\"\n/>\n\n<!-- A janela de avalia\u00E7\u00E3o (flutuante, canto inferior direito): \"Avaliar o {app}\" da estrela do cabe\u00E7alho e de\n Configura\u00E7\u00F5es \u203A Sobre, e \"Avaliar esta tela\" da barra da p\u00E1gina (SpineFeatures.ratings). S\u00F3 desenha com o recurso\n ligado e aberta. -->\n<spine-rating-window />\n", styles: ["@charset \"UTF-8\";:host{display:contents}.header-overlay{position:absolute;inset:0;z-index:9999;cursor:not-allowed}.h-btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:40px;min-width:40px;padding:0 10px;border:none;background:transparent;cursor:pointer;border-radius:9px;text-decoration:none;color:var(--hm-navbar-text);outline:none}.h-btn:focus-visible,.h-btn:hover{background:var(--hm-navbar-hover)}.h-btn.active{background:var(--hm-navbar-hover)}.h-right{display:flex;align-items:center;gap:var(--hm-header-action-gap)}.h-divider{width:1px;height:24px;margin:0 4px;background:var(--hm-navbar-hover)}.h-profile{display:inline-flex;align-items:center;gap:6px;height:40px;padding:0 4px 0 2px}.h-profile hm-account-avatar{background:color-mix(in srgb,var(--hm-navbar-text) 18%,transparent);color:var(--hm-navbar-text)}.h-profile-info{display:flex;flex-direction:column;min-width:0;max-width:140px;line-height:1.2;text-align:left}.h-profile-name{font-size:var(--hm-font-size-label);font-weight:var(--hm-font-weight-semibold);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.h-profile-role{color:color-mix(in srgb,var(--hm-navbar-text) 65%,transparent);font-size:var(--hm-font-size-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
|
|
2427
2989
|
}], ctorParameters: () => [{ type: i1.AuthorizationService }, { type: i2$1.ThemeService }, { type: i2$1.ThemePresetService }, { type: i2$1.DensityService }, { type: i1.ViewService }, { type: i3.Router }, { type: ShortcutService }, { type: undefined, decorators: [{
|
|
2428
2990
|
type: Optional
|
|
2429
2991
|
}, {
|
|
@@ -4690,13 +5252,13 @@ function buildMcpConfig(servers, token, write, origin) {
|
|
|
4690
5252
|
return JSON.stringify({ mcpServers }, null, 2);
|
|
4691
5253
|
}
|
|
4692
5254
|
/**
|
|
4693
|
-
* Sessões › "Conectar um agente de IA" (`SpineFeatures.
|
|
5255
|
+
* Sessões › "Conectar um agente de IA" (`SpineFeatures.mcpConnect`): em um passo, cria um token de aplicação pessoal
|
|
4694
5256
|
* (`x-app-token`, a mesma API de "Tokens de aplicação") e monta a configuração MCP pronta para o Claude Code, com os
|
|
4695
5257
|
* servidores que o app declarou e os escopos do acesso escolhido (só leitura ou leitura e escrita). O token aparece
|
|
4696
5258
|
* uma única vez, dentro da configuração; nunca vai para o console nem para o armazenamento do navegador, e some ao
|
|
4697
5259
|
* concluir ou fechar o painel.
|
|
4698
5260
|
*/
|
|
4699
|
-
class
|
|
5261
|
+
class McpConnectComponent {
|
|
4700
5262
|
constructor() {
|
|
4701
5263
|
this.messages = inject(SPINE_MESSAGES);
|
|
4702
5264
|
this.t = this.messages.accountFeatures;
|
|
@@ -4706,7 +5268,7 @@ class AiAgentComponent {
|
|
|
4706
5268
|
this.toast = inject(HermesToastService);
|
|
4707
5269
|
this.preferences = inject(SpinePreferencesService);
|
|
4708
5270
|
this.available = this.service.available;
|
|
4709
|
-
this.servers = inject(SPINE_CONFIG, { optional: true })?.features?.
|
|
5271
|
+
this.servers = inject(SPINE_CONFIG, { optional: true })?.features?.mcpConnect?.servers ?? [];
|
|
4710
5272
|
this.open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
|
|
4711
5273
|
this.starting = signal(false, ...(ngDevMode ? [{ debugName: "starting" }] : /* istanbul ignore next */ []));
|
|
4712
5274
|
this.submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : /* istanbul ignore next */ []));
|
|
@@ -4721,8 +5283,8 @@ class AiAgentComponent {
|
|
|
4721
5283
|
this.noBadges = computed(() => !this.badges().length, ...(ngDevMode ? [{ debugName: "noBadges" }] : /* istanbul ignore next */ []));
|
|
4722
5284
|
this.canSubmit = computed(() => !!this.selected().size && !!this.badgeId() && !this.submitting(), ...(ngDevMode ? [{ debugName: "canSubmit" }] : /* istanbul ignore next */ []));
|
|
4723
5285
|
this.accessOptions = computed(() => [
|
|
4724
|
-
{ value: 'read', label: this.t.
|
|
4725
|
-
{ value: 'write', label: this.t.
|
|
5286
|
+
{ value: 'read', label: this.t.mcpConnectReadOnly },
|
|
5287
|
+
{ value: 'write', label: this.t.mcpConnectReadWrite },
|
|
4726
5288
|
], ...(ngDevMode ? [{ debugName: "accessOptions" }] : /* istanbul ignore next */ []));
|
|
4727
5289
|
this.badgeOptions = computed(() => this.badges().map((b) => ({ value: b.id, label: b.name })), ...(ngDevMode ? [{ debugName: "badgeOptions" }] : /* istanbul ignore next */ []));
|
|
4728
5290
|
this.validityOptions = computed(() => [
|
|
@@ -4734,7 +5296,7 @@ class AiAgentComponent {
|
|
|
4734
5296
|
}
|
|
4735
5297
|
serverLabel(server) {
|
|
4736
5298
|
const label = server.label ?? server.name;
|
|
4737
|
-
return server.writeScopes ? label : formatMessage(this.t.
|
|
5299
|
+
return server.writeScopes ? label : formatMessage(this.t.mcpConnectReadOnlyServer, { name: label });
|
|
4738
5300
|
}
|
|
4739
5301
|
toggleServer(name, checked) {
|
|
4740
5302
|
const next = new Set(this.selected());
|
|
@@ -4772,7 +5334,7 @@ class AiAgentComponent {
|
|
|
4772
5334
|
const date = new Intl.DateTimeFormat(this.messages.meta.locale, { dateStyle: 'short', timeZone: this.preferences.effectiveTimeZone() }).format(new Date());
|
|
4773
5335
|
const created = await this.service.create({
|
|
4774
5336
|
badge_id: this.badgeId(),
|
|
4775
|
-
name: formatMessage(this.t.
|
|
5337
|
+
name: formatMessage(this.t.mcpConnectTokenName, { date }).slice(0, 80),
|
|
4776
5338
|
validity_days: Number(this.validity()),
|
|
4777
5339
|
});
|
|
4778
5340
|
const servers = this.servers.filter((s) => this.selected().has(s.name));
|
|
@@ -4785,7 +5347,7 @@ class AiAgentComponent {
|
|
|
4785
5347
|
this.open.set(false);
|
|
4786
5348
|
}
|
|
4787
5349
|
catch (err) {
|
|
4788
|
-
this.toast.error(errorDetail(err) || this.t.
|
|
5350
|
+
this.toast.error(errorDetail(err) || this.t.mcpConnectCreateError);
|
|
4789
5351
|
}
|
|
4790
5352
|
finally {
|
|
4791
5353
|
this.submitting.set(false);
|
|
@@ -4797,7 +5359,7 @@ class AiAgentComponent {
|
|
|
4797
5359
|
return;
|
|
4798
5360
|
try {
|
|
4799
5361
|
await navigator.clipboard.writeText(config);
|
|
4800
|
-
this.toast.success(this.t.
|
|
5362
|
+
this.toast.success(this.t.mcpConnectCopied);
|
|
4801
5363
|
}
|
|
4802
5364
|
catch {
|
|
4803
5365
|
this.toast.error(this.tokens.copyFailed);
|
|
@@ -4806,23 +5368,23 @@ class AiAgentComponent {
|
|
|
4806
5368
|
done() {
|
|
4807
5369
|
this.result.set(null);
|
|
4808
5370
|
}
|
|
4809
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type:
|
|
4810
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type:
|
|
5371
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: McpConnectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
5372
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: McpConnectComponent, isStandalone: true, selector: "spine-mcp-connect", ngImport: i0, template: `
|
|
4811
5373
|
@if (!available) {
|
|
4812
|
-
<p class="sp-account__note">{{ t.
|
|
5374
|
+
<p class="sp-account__note">{{ t.mcpConnectUnavailable }}</p>
|
|
4813
5375
|
} @else if (result(); as r) {
|
|
4814
5376
|
<div class="sp-account__panel sp-account__panel--success" role="status">
|
|
4815
5377
|
<div class="sp-account__panel-head">
|
|
4816
5378
|
<hm-icon shape="check-circle" />
|
|
4817
5379
|
<div class="sp-account__panel-text">
|
|
4818
|
-
<p class="sp-account__title">{{ t.
|
|
4819
|
-
<p class="sp-account__note">{{ fmt(t.
|
|
5380
|
+
<p class="sp-account__title">{{ t.mcpConnectResult }}</p>
|
|
5381
|
+
<p class="sp-account__note">{{ fmt(t.mcpConnectResultHint, { date: r.expires }) }}</p>
|
|
4820
5382
|
</div>
|
|
4821
5383
|
</div>
|
|
4822
|
-
<hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.
|
|
5384
|
+
<hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.mcpConnectResult" />
|
|
4823
5385
|
<div class="sp-account__actions">
|
|
4824
|
-
<hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.
|
|
4825
|
-
<hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.
|
|
5386
|
+
<hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.mcpConnectCopy }}</hm-button>
|
|
5387
|
+
<hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.mcpConnectDone }}</hm-button>
|
|
4826
5388
|
</div>
|
|
4827
5389
|
</div>
|
|
4828
5390
|
} @else if (open()) {
|
|
@@ -4830,7 +5392,7 @@ class AiAgentComponent {
|
|
|
4830
5392
|
<div class="sp-account__form">
|
|
4831
5393
|
@if (servers.length > 1) {
|
|
4832
5394
|
<fieldset class="sp-account__choices">
|
|
4833
|
-
<legend class="sp-account__label">{{ t.
|
|
5395
|
+
<legend class="sp-account__label">{{ t.mcpConnectServers }}</legend>
|
|
4834
5396
|
@for (s of servers; track s.name) {
|
|
4835
5397
|
<hm-checkbox [label]="serverLabel(s)" [checked]="selected().has(s.name)" (checkedChange)="toggleServer(s.name, $event)" />
|
|
4836
5398
|
}
|
|
@@ -4838,9 +5400,9 @@ class AiAgentComponent {
|
|
|
4838
5400
|
}
|
|
4839
5401
|
@if (canWrite()) {
|
|
4840
5402
|
<div class="sp-account__field">
|
|
4841
|
-
<span class="sp-account__label">{{ t.
|
|
4842
|
-
<hm-segmented-control size="small" emphasis="action" [label]="t.
|
|
4843
|
-
@if (access() === 'write') { <p class="sp-account__note">{{ t.
|
|
5403
|
+
<span class="sp-account__label">{{ t.mcpConnectAccess }}</span>
|
|
5404
|
+
<hm-segmented-control size="small" emphasis="action" [label]="t.mcpConnectAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
|
|
5405
|
+
@if (access() === 'write') { <p class="sp-account__note">{{ t.mcpConnectWriteHint }}</p> }
|
|
4844
5406
|
</div>
|
|
4845
5407
|
}
|
|
4846
5408
|
@if (badgeOptions().length > 1) {
|
|
@@ -4853,39 +5415,39 @@ class AiAgentComponent {
|
|
|
4853
5415
|
<span class="sp-account__label">{{ tokens.validity }}</span>
|
|
4854
5416
|
<hm-segmented-control size="small" emphasis="action" [label]="tokens.validity" [options]="validityOptions()" [value]="validity()" (valueChange)="validity.set($event)" />
|
|
4855
5417
|
</div>
|
|
4856
|
-
@if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.
|
|
5418
|
+
@if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.mcpConnectSelectServer }}</p> }
|
|
4857
5419
|
@if (noBadges()) { <p class="sp-account__note sp-account__note--error">{{ tokens.noBadges }}</p> }
|
|
4858
5420
|
</div>
|
|
4859
5421
|
<div class="sp-account__actions">
|
|
4860
5422
|
<hm-button size="small" emphasis="medium" [disabled]="submitting()" (clicked)="cancel()">{{ tokens.cancel }}</hm-button>
|
|
4861
|
-
<hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.
|
|
5423
|
+
<hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.mcpConnectGenerate }}</hm-button>
|
|
4862
5424
|
</div>
|
|
4863
5425
|
</div>
|
|
4864
5426
|
} @else {
|
|
4865
5427
|
<div class="sp-account__actions">
|
|
4866
|
-
<hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.
|
|
5428
|
+
<hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.mcpConnectStart }}</hm-button>
|
|
4867
5429
|
</div>
|
|
4868
5430
|
}
|
|
4869
5431
|
`, isInline: true, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"], dependencies: [{ kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: CodeViewerComponent, selector: "hm-code-viewer", inputs: ["code", "language", "filename", "label", "showLineNumbers", "firstLineNumber", "highlightLines", "wrap", "tabSize", "copyable", "header", "virtualize", "virtualizeThreshold", "revealInvisibles"], outputs: ["copied", "tokenized"] }, { kind: "component", type: IconComponent, selector: "hm-icon", inputs: ["shape", "direction", "size", "solid", "badge", "status", "flip", "inverse", "label"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
4870
5432
|
}
|
|
4871
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type:
|
|
5433
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: McpConnectComponent, decorators: [{
|
|
4872
5434
|
type: Component,
|
|
4873
|
-
args: [{ selector: 'spine-
|
|
5435
|
+
args: [{ selector: 'spine-mcp-connect', imports: [ButtonComponent, CheckboxComponent, CodeViewerComponent, IconComponent, SegmentedControlComponent], template: `
|
|
4874
5436
|
@if (!available) {
|
|
4875
|
-
<p class="sp-account__note">{{ t.
|
|
5437
|
+
<p class="sp-account__note">{{ t.mcpConnectUnavailable }}</p>
|
|
4876
5438
|
} @else if (result(); as r) {
|
|
4877
5439
|
<div class="sp-account__panel sp-account__panel--success" role="status">
|
|
4878
5440
|
<div class="sp-account__panel-head">
|
|
4879
5441
|
<hm-icon shape="check-circle" />
|
|
4880
5442
|
<div class="sp-account__panel-text">
|
|
4881
|
-
<p class="sp-account__title">{{ t.
|
|
4882
|
-
<p class="sp-account__note">{{ fmt(t.
|
|
5443
|
+
<p class="sp-account__title">{{ t.mcpConnectResult }}</p>
|
|
5444
|
+
<p class="sp-account__note">{{ fmt(t.mcpConnectResultHint, { date: r.expires }) }}</p>
|
|
4883
5445
|
</div>
|
|
4884
5446
|
</div>
|
|
4885
|
-
<hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.
|
|
5447
|
+
<hm-code-viewer [code]="r.config" language="json" filename=".mcp.json" [label]="t.mcpConnectResult" />
|
|
4886
5448
|
<div class="sp-account__actions">
|
|
4887
|
-
<hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.
|
|
4888
|
-
<hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.
|
|
5449
|
+
<hm-button size="small" emphasis="high" icon="copy" (clicked)="copy()">{{ t.mcpConnectCopy }}</hm-button>
|
|
5450
|
+
<hm-button size="small" emphasis="medium" (clicked)="done()">{{ t.mcpConnectDone }}</hm-button>
|
|
4889
5451
|
</div>
|
|
4890
5452
|
</div>
|
|
4891
5453
|
} @else if (open()) {
|
|
@@ -4893,7 +5455,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
4893
5455
|
<div class="sp-account__form">
|
|
4894
5456
|
@if (servers.length > 1) {
|
|
4895
5457
|
<fieldset class="sp-account__choices">
|
|
4896
|
-
<legend class="sp-account__label">{{ t.
|
|
5458
|
+
<legend class="sp-account__label">{{ t.mcpConnectServers }}</legend>
|
|
4897
5459
|
@for (s of servers; track s.name) {
|
|
4898
5460
|
<hm-checkbox [label]="serverLabel(s)" [checked]="selected().has(s.name)" (checkedChange)="toggleServer(s.name, $event)" />
|
|
4899
5461
|
}
|
|
@@ -4901,9 +5463,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
4901
5463
|
}
|
|
4902
5464
|
@if (canWrite()) {
|
|
4903
5465
|
<div class="sp-account__field">
|
|
4904
|
-
<span class="sp-account__label">{{ t.
|
|
4905
|
-
<hm-segmented-control size="small" emphasis="action" [label]="t.
|
|
4906
|
-
@if (access() === 'write') { <p class="sp-account__note">{{ t.
|
|
5466
|
+
<span class="sp-account__label">{{ t.mcpConnectAccess }}</span>
|
|
5467
|
+
<hm-segmented-control size="small" emphasis="action" [label]="t.mcpConnectAccess" [options]="accessOptions()" [value]="access()" (valueChange)="access.set($event === 'write' ? 'write' : 'read')" />
|
|
5468
|
+
@if (access() === 'write') { <p class="sp-account__note">{{ t.mcpConnectWriteHint }}</p> }
|
|
4907
5469
|
</div>
|
|
4908
5470
|
}
|
|
4909
5471
|
@if (badgeOptions().length > 1) {
|
|
@@ -4916,17 +5478,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
4916
5478
|
<span class="sp-account__label">{{ tokens.validity }}</span>
|
|
4917
5479
|
<hm-segmented-control size="small" emphasis="action" [label]="tokens.validity" [options]="validityOptions()" [value]="validity()" (valueChange)="validity.set($event)" />
|
|
4918
5480
|
</div>
|
|
4919
|
-
@if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.
|
|
5481
|
+
@if (!selected().size) { <p class="sp-account__note sp-account__note--error">{{ t.mcpConnectSelectServer }}</p> }
|
|
4920
5482
|
@if (noBadges()) { <p class="sp-account__note sp-account__note--error">{{ tokens.noBadges }}</p> }
|
|
4921
5483
|
</div>
|
|
4922
5484
|
<div class="sp-account__actions">
|
|
4923
5485
|
<hm-button size="small" emphasis="medium" [disabled]="submitting()" (clicked)="cancel()">{{ tokens.cancel }}</hm-button>
|
|
4924
|
-
<hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.
|
|
5486
|
+
<hm-button size="small" emphasis="high" icon="plugin" [loading]="submitting()" [disabled]="!canSubmit()" (clicked)="generate()">{{ t.mcpConnectGenerate }}</hm-button>
|
|
4925
5487
|
</div>
|
|
4926
5488
|
</div>
|
|
4927
5489
|
} @else {
|
|
4928
5490
|
<div class="sp-account__actions">
|
|
4929
|
-
<hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.
|
|
5491
|
+
<hm-button emphasis="medium" icon="plugin" [loading]="starting()" (clicked)="start()">{{ t.mcpConnectStart }}</hm-button>
|
|
4930
5492
|
</div>
|
|
4931
5493
|
}
|
|
4932
5494
|
`, changeDetection: ChangeDetectionStrategy.OnPush, styles: ["@charset \"UTF-8\";:host{display:flex;flex-direction:column;gap:var(--hm-space-xs);width:100%;min-width:0}.sp-account__note{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-xs);line-height:1.45}.sp-account__note--error{color:var(--hm-danger)}.sp-account__note code{font-family:var(--hm-font-mono)}.sp-account__list{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-account__items{display:flex;flex-direction:column;min-width:0;margin:0;padding:0;list-style:none}.sp-account__item{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-sm)}.sp-account__item+.sp-account__item{border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-account__item--empty{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-account__item--muted .sp-account__item-title strong{color:var(--hm-muted)}.sp-account__icon{display:inline-flex;flex:none;align-items:center;justify-content:center;width:var(--hm-control-height-large);height:var(--hm-control-height-large);border-radius:var(--hm-radius-control);background:var(--hm-hover);color:var(--hm-muted)}.sp-account__icon--accent{background:var(--hm-accent-subtle);color:var(--hm-accent-strong)}.sp-account__item-main{display:flex;min-width:0;flex:1 1 14rem;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);font-size:var(--hm-font-size-sm);overflow-wrap:anywhere}.sp-account__item-title{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-nano)}.sp-account__item-title strong{font-weight:var(--hm-font-weight-semibold)}.sp-account__meta{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-account__meta code{font-family:var(--hm-font-mono)}.sp-account__meta--danger{color:var(--hm-danger)}.sp-account__item-actions,.sp-account__actions{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__item-actions{margin-inline-start:auto}.sp-account__list-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--hm-space-xs) var(--hm-space-sm);padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);background:var(--hm-surface-raised)}.sp-account__list-footer:first-child{border-top:0}.sp-account__panel{display:flex;flex-direction:column;gap:var(--hm-space-sm);min-width:0;padding:var(--hm-space-sm) var(--hm-space-md) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-inline-start:var(--hm-border-thin) solid var(--hm-accent);border-radius:var(--hm-radius);background:var(--hm-surface-raised)}.sp-account__panel--success{border-color:var(--hm-success);background:var(--hm-success-bg)}.sp-account__panel--success .sp-account__panel-head hm-icon{color:var(--hm-success)}.sp-account__panel--danger{border-inline-start-color:var(--hm-danger)}.sp-account__panel--danger .sp-account__panel-head hm-icon{color:var(--hm-danger)}.sp-account__panel-head{display:flex;align-items:flex-start;gap:var(--hm-space-xs)}.sp-account__panel-head hm-icon{flex:none;margin-block-start:var(--hm-space-micro);color:var(--hm-accent-strong)}.sp-account__panel-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro)}.sp-account__title{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__form,.sp-account__code,.sp-account__field{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0}.sp-account__form{gap:var(--hm-space-sm)}.sp-account__label{color:var(--hm-text);font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.sp-account__qr{align-self:flex-start;padding:var(--hm-space-xxxs);border-radius:var(--hm-radius-control);background:var(--hm-surface)}.sp-account__qr img{display:block;width:10rem;max-width:100%;height:auto}.sp-account__secret,.sp-account__codes code{font-family:var(--hm-font-mono);overflow-wrap:anywhere}.sp-account__secret{-webkit-user-select:all;user-select:all}.sp-account__secret--block{display:block;padding:var(--hm-space-xs) var(--hm-space-xxxs);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-xs)}.sp-account__codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(9rem,100%),1fr));gap:var(--hm-space-nano) var(--hm-space-sm);margin:0;padding:var(--hm-space-xxxs) var(--hm-space-xs) var(--hm-space-xxxs) var(--hm-space-md);border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius-control);background:var(--hm-surface);color:var(--hm-text);font-size:var(--hm-font-size-sm)}@media(forced-colors:active){.sp-account__panel{border-inline-start-color:Highlight}.sp-account__icon{border:var(--hm-border-hairline) solid CanvasText}}.sp-account__toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--hm-space-xs)}.sp-account__choices{display:flex;flex-direction:column;gap:var(--hm-space-xs);min-width:0;margin:0;padding:0;border:0}\n"] }]
|
|
@@ -6288,7 +6850,7 @@ class SettingsPanelComponent {
|
|
|
6288
6850
|
this.sessionsItems = {
|
|
6289
6851
|
accessToken: this.showAccessToken,
|
|
6290
6852
|
appTokens: true,
|
|
6291
|
-
|
|
6853
|
+
mcpConnect: !!this.config?.features?.mcpConnect?.servers?.length,
|
|
6292
6854
|
devices: this.accountSecurity,
|
|
6293
6855
|
};
|
|
6294
6856
|
/** Sem nenhuma linha visível a seção não aparece. */
|
|
@@ -6508,17 +7070,17 @@ class SettingsPanelComponent {
|
|
|
6508
7070
|
this.settings.close();
|
|
6509
7071
|
}
|
|
6510
7072
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
6511
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SettingsPanelComponent, isStandalone: true, selector: "spine-settings-panel", ngImport: i0, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.aiAgent). -->\n @if (sessionsItems.aiAgent) {\n <hm-settings-row [label]=\"featureText.aiAgent\" [description]=\"featureText.aiAgentDescription\" [keywords]=\"featureText.aiAgentKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.aiAgent })\" [text]=\"featureText.aiAgentHelp\" />\n <spine-ai-agent />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: AppTokensComponent, selector: "spine-app-tokens" }, { kind: "component", type: MyGroupsComponent, selector: "spine-my-groups", inputs: ["groups"], outputs: ["openChange"] }, { kind: "component", type: AiAgentComponent, selector: "spine-ai-agent" }, { kind: "component", type: AccountActivityComponent, selector: "spine-account-activity" }, { kind: "component", type: DefaultBadgeComponent, selector: "spine-default-badge" }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: AccountMfaComponent, selector: "spine-account-mfa" }, { kind: "component", type: AccountRecoveryCodesComponent, selector: "spine-account-recovery-codes" }, { kind: "component", type: AccountSessionsComponent, selector: "spine-account-sessions" }, { kind: "directive", type: SpineAccountSecurityScopeDirective, selector: "[spineAccountSecurity]" }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SettingsDialogComponent, selector: "hm-settings-dialog", inputs: ["open", "heading", "activeSection", "closeHandler", "actions", "submitLabel", "cancelLabel"], outputs: ["openChange", "activeSectionChange", "closed", "submitted"] }, { kind: "component", type: SettingsRowComponent, selector: "hm-settings-row", inputs: ["label", "description", "keywords", "stacked"] }, { kind: "component", type: SettingsSectionComponent, selector: "hm-settings-section", inputs: ["key", "label", "icon", "description", "keywords", "control", "invalid"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: ShortcutListComponent, selector: "hm-shortcut-list", inputs: ["items", "registered", "searchable", "characterKeysToggle", "headingLevel"] }, { kind: "component", type: ThemePickerComponent, selector: "hm-theme-picker", inputs: ["families", "family", "theme", "showMode"], outputs: ["familyChosen", "themeChosen"] }, { kind: "component", type: ToggletipComponent, selector: "hm-toggletip", inputs: ["label", "text", "side", "intent", "icon"] }, { kind: "component", type: SpineAppRatingSummaryComponent, selector: "spine-app-rating-summary" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
7073
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SettingsPanelComponent, isStandalone: true, selector: "spine-settings-panel", ngImport: i0, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.mcpConnect). -->\n @if (sessionsItems.mcpConnect) {\n <hm-settings-row [label]=\"featureText.mcpConnect\" [description]=\"featureText.mcpConnectDescription\" [keywords]=\"featureText.mcpConnectKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.mcpConnect })\" [text]=\"featureText.mcpConnectHelp\" />\n <spine-mcp-connect />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"], dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: AppTokensComponent, selector: "spine-app-tokens" }, { kind: "component", type: MyGroupsComponent, selector: "spine-my-groups", inputs: ["groups"], outputs: ["openChange"] }, { kind: "component", type: McpConnectComponent, selector: "spine-mcp-connect" }, { kind: "component", type: AccountActivityComponent, selector: "spine-account-activity" }, { kind: "component", type: DefaultBadgeComponent, selector: "spine-default-badge" }, { kind: "component", type: CheckboxComponent, selector: "hm-checkbox", inputs: ["label", "labelHidden", "checked", "indeterminate"], outputs: ["checkedChange"] }, { kind: "component", type: AccountMfaComponent, selector: "spine-account-mfa" }, { kind: "component", type: AccountRecoveryCodesComponent, selector: "spine-account-recovery-codes" }, { kind: "component", type: AccountSessionsComponent, selector: "spine-account-sessions" }, { kind: "directive", type: SpineAccountSecurityScopeDirective, selector: "[spineAccountSecurity]" }, { kind: "component", type: AccountAvatarComponent, selector: "hm-account-avatar", inputs: ["name", "imageUrl", "size", "loading", "presence", "texts"] }, { kind: "component", type: ButtonComponent, selector: "hm-button", inputs: ["emphasis", "intent", "size", "type", "icon", "iconDirection", "iconEnd", "disabled", "loading", "loadingState", "block", "link", "newTab", "caret", "inMenu", "toggle", "pressed"], outputs: ["loadingStateChange", "pressedChange", "clicked"] }, { kind: "component", type: ComboboxComponent, selector: "hm-combobox", inputs: ["label", "options", "value", "placeholder", "icon", "hint", "size", "labelHidden", "clearable", "loading", "allowCustomValue"], outputs: ["valueChange"] }, { kind: "component", type: SegmentedControlComponent, selector: "hm-segmented-control", inputs: ["options", "value", "label", "size", "emphasis"], outputs: ["valueChange"] }, { kind: "component", type: SettingsDialogComponent, selector: "hm-settings-dialog", inputs: ["open", "heading", "activeSection", "closeHandler", "actions", "submitLabel", "cancelLabel"], outputs: ["openChange", "activeSectionChange", "closed", "submitted"] }, { kind: "component", type: SettingsRowComponent, selector: "hm-settings-row", inputs: ["label", "description", "keywords", "stacked"] }, { kind: "component", type: SettingsSectionComponent, selector: "hm-settings-section", inputs: ["key", "label", "icon", "description", "keywords", "control", "invalid"] }, { kind: "component", type: SelectComponent, selector: "hm-select", inputs: ["value", "label", "labelHidden", "hint"], outputs: ["valueChange"] }, { kind: "component", type: ShortcutListComponent, selector: "hm-shortcut-list", inputs: ["items", "registered", "searchable", "characterKeysToggle", "headingLevel"] }, { kind: "component", type: ThemePickerComponent, selector: "hm-theme-picker", inputs: ["families", "family", "theme", "showMode"], outputs: ["familyChosen", "themeChosen"] }, { kind: "component", type: ToggletipComponent, selector: "hm-toggletip", inputs: ["label", "text", "side", "intent", "icon"] }, { kind: "component", type: SpineAppRatingSummaryComponent, selector: "spine-app-rating-summary" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
6512
7074
|
}
|
|
6513
7075
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SettingsPanelComponent, decorators: [{
|
|
6514
7076
|
type: Component,
|
|
6515
7077
|
args: [{ selector: 'spine-settings-panel', imports: [
|
|
6516
|
-
NgComponentOutlet, AppTokensComponent, MyGroupsComponent,
|
|
7078
|
+
NgComponentOutlet, AppTokensComponent, MyGroupsComponent, McpConnectComponent, AccountActivityComponent, DefaultBadgeComponent, CheckboxComponent,
|
|
6517
7079
|
AccountMfaComponent, AccountRecoveryCodesComponent, AccountSessionsComponent, SpineAccountSecurityScopeDirective,
|
|
6518
7080
|
AccountAvatarComponent, ButtonComponent, ComboboxComponent,
|
|
6519
7081
|
SegmentedControlComponent, SettingsDialogComponent, SettingsRowComponent, SettingsSectionComponent,
|
|
6520
7082
|
SelectComponent, ShortcutListComponent, ThemePickerComponent, ToggletipComponent, SpineAppRatingSummaryComponent,
|
|
6521
|
-
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.aiAgent). -->\n @if (sessionsItems.aiAgent) {\n <hm-settings-row [label]=\"featureText.aiAgent\" [description]=\"featureText.aiAgentDescription\" [keywords]=\"featureText.aiAgentKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.aiAgent })\" [text]=\"featureText.aiAgentHelp\" />\n <spine-ai-agent />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"] }]
|
|
7083
|
+
], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Montado s\u00F3 enquanto aberto: nada disto (lista de fusos, leitura do token...) custa no\n carregamento de toda p\u00E1gina. -->\n@if (settings.isOpen()) {\n <hm-settings-dialog\n [heading]=\"text.heading\"\n [open]=\"true\"\n (openChange)=\"onOpenChange($event)\"\n [activeSection]=\"activeSection()\"\n (activeSectionChange)=\"settings.section.set($event)\"\n >\n <!-- \u2500\u2500 Conta \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.account\" [label]=\"text.account\" icon=\"user\" [keywords]=\"text.accountKeywords\">\n <div class=\"sp-settings__profile\">\n <hm-account-avatar [name]=\"profile.name ?? text.user\" [imageUrl]=\"profile.picture\" />\n <span class=\"sp-settings__profile-text\">\n <strong>{{ profile.name ?? text.user }}</strong>\n @if (profile.role) { <span>{{ profile.role }}</span> }\n </span>\n </div>\n\n <!-- S\u00F3 informativo: a troca de n\u00EDvel fica no menu do perfil, perto de onde se trabalha. -->\n @if (badge) {\n <hm-settings-row [label]=\"text.accessLevel\" [keywords]=\"text.accessLevelKeywords\">\n <span class=\"sp-settings__value\">{{ badge.name }}</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.session\" [description]=\"text.sessionDescription\" [keywords]=\"text.sessionKeywords\">\n <span class=\"sp-settings__value\">{{ sessionExpiry }}</span>\n </hm-settings-row>\n\n <!-- Recursos opcionais (SpineConfig.features): crach\u00E1 padr\u00E3o (Argos), inatividade e tela inicial. -->\n @if (showDefaultBadge) {\n <hm-settings-row [label]=\"featureText.defaultBadge\" [description]=\"featureText.defaultBadgeDescription\" [keywords]=\"featureText.defaultBadgeKeywords\">\n <spine-default-badge />\n </hm-settings-row>\n }\n\n @if (idleOptions().length) {\n <hm-settings-row [label]=\"featureText.idle\" [description]=\"idleHint || featureText.idleDescription\" [keywords]=\"featureText.idleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"featureText.idle\" [options]=\"idleOptions()\" [value]=\"idleValue()\" (valueChange)=\"setIdle($event)\" />\n </hm-settings-row>\n }\n\n @if (features.startPage) {\n <hm-settings-row [label]=\"featureText.startPage\" [description]=\"quickAccessHasFavorites() ? featureText.startPageDescription : featureText.startPageHint\" [keywords]=\"featureText.startPageKeywords\">\n <hm-select class=\"sp-settings__language\" [label]=\"featureText.startPage\" [labelHidden]=\"true\" [value]=\"startPageValue()\" (valueChange)=\"setStartPage($event ?? 'home')\">\n @for (option of startPageOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.logout\" [description]=\"text.logoutDescription\" [keywords]=\"text.logoutKeywords\">\n <hm-button emphasis=\"medium\" intent=\"danger\" icon=\"logout\" [loading]=\"logoutPending()\" (clicked)=\"logout()\">{{ text.logout }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 MFA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- API da conta do Argos com o token da sess\u00E3o (Bearer): MFA e c\u00F3digos de recupera\u00E7\u00E3o aqui mesmo, sem\n abrir o Argos. O spineAccountSecurity d\u00E1 \u00E0s linhas o mesmo estado (reautentica\u00E7\u00E3o, cadastro em\n andamento), que morre quando o painel fecha. `?settings=seguranca` (chave antiga) abre esta se\u00E7\u00E3o. -->\n @if (accountSecurity) {\n <hm-settings-section spineAccountSecurity [key]=\"keys.mfa\" [label]=\"text.mfa\" icon=\"shield-check\" [description]=\"text.mfaDescription\" [keywords]=\"text.mfaKeywords\">\n <!-- Realm s\u00F3 pelo nome: o id (UUID) n\u00E3o diz nada a quem l\u00EA, ent\u00E3o some. -->\n @if (accountLogin || accountRealm) {\n <hm-settings-row [label]=\"text.signedInAccount\" [description]=\"accountRealm ? fmt(text.signedInRealm, { realm: accountRealm }) : ''\">\n <span class=\"sp-settings__value\">{{ profile.name ?? text.user }}@if (accountLogin) { <span class=\"sp-settings__muted\"> \u00B7 {{ accountLogin }}</span> }</span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.twoFactor\" [description]=\"text.twoFactorDescription\" [keywords]=\"text.twoFactorKeywords\" [stacked]=\"true\">\n <spine-account-mfa />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.recoveryCodes\" [description]=\"text.recoveryCodesDescription\" [keywords]=\"text.recoveryCodesKeywords\" [stacked]=\"true\">\n <spine-account-recovery-codes />\n </hm-settings-row>\n\n <!-- Entradas e tentativas desta conta (SpineFeatures.accountActivity), \u00FAltimos 90 dias. -->\n @if (features.accountActivity) {\n <hm-settings-row [label]=\"featureText.activity\" [description]=\"featureText.activityDescription\" [keywords]=\"featureText.activityKeywords\" [stacked]=\"true\">\n <spine-account-activity />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sess\u00F5es \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tokens e onde a conta est\u00E1 conectada (a validade desta sess\u00E3o fica em Conta). Cada linha tem a\n pr\u00F3pria condi\u00E7\u00E3o; a se\u00E7\u00E3o some se nenhuma sobrar (hasSessionsSection). -->\n @if (hasSessionsSection) {\n <hm-settings-section [key]=\"keys.sessions\" [label]=\"text.sessionsSection\" icon=\"devices\" [description]=\"text.sessionsSectionDescription\" [keywords]=\"text.sessionsSectionKeywords\">\n <!-- S\u00F3 com SpineConfig.technicalDocs (padr\u00E3o: isDevMode()): token da sess\u00E3o no clipboard \u00E9 ferramenta de dev. -->\n @if (sessionsItems.accessToken) {\n <hm-settings-row [label]=\"text.accessToken\" [description]=\"text.accessTokenDescription\" [keywords]=\"text.accessTokenKeywords\">\n <hm-button emphasis=\"medium\" icon=\"copy\" (clicked)=\"copyToken()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n }\n\n @if (sessionsItems.appTokens) {\n <hm-settings-row [label]=\"text.appTokens\" [stacked]=\"true\"\n [description]=\"text.appTokensDescription\"\n [keywords]=\"text.appTokensKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.appTokens })\" [text]=\"text.appTokensHelp\" />\n <spine-app-tokens />\n </hm-settings-row>\n }\n\n <!-- Token + .mcp.json prontos para um agente de IA (SpineFeatures.mcpConnect). -->\n @if (sessionsItems.mcpConnect) {\n <hm-settings-row [label]=\"featureText.mcpConnect\" [description]=\"featureText.mcpConnectDescription\" [keywords]=\"featureText.mcpConnectKeywords\" [stacked]=\"true\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.mcpConnect })\" [text]=\"featureText.mcpConnectHelp\" />\n <spine-mcp-connect />\n </hm-settings-row>\n }\n\n <!-- Sess\u00F5es e dispositivos: API da conta do Argos, mesma condi\u00E7\u00E3o da se\u00E7\u00E3o MFA. -->\n @if (sessionsItems.devices) {\n <hm-settings-row [label]=\"text.sessions\" [description]=\"text.sessionsDescription\" [keywords]=\"text.sessionsKeywords\" [stacked]=\"true\">\n <spine-account-sessions />\n </hm-settings-row>\n }\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Grupos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os grupos desta aplica\u00E7\u00E3o de que a pessoa \u00E9 membro (argos-admin /me/groups, relidos a cada abertura): a lista,\n e escolhendo um, a tela de membros dele. Sem grupo nenhum, a se\u00E7\u00E3o n\u00E3o aparece. -->\n @if (myGroups().length) {\n <hm-settings-section [key]=\"keys.groups\" [label]=\"text.groups\" icon=\"employee-group\" [keywords]=\"text.groupsKeywords\"\n [class.sp-settings__section--group-open]=\"groupOpen()\" [description]=\"groupOpen() ? '' : text.groupsDescription\">\n <spine-my-groups [groups]=\"myGroups()\" (openChange)=\"groupOpen.set($event)\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Apar\u00EAncia \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.appearance\" [label]=\"text.appearance\" icon=\"color-palette\" [keywords]=\"text.appearanceKeywords\">\n <hm-settings-row [label]=\"text.theme\" [description]=\"text.themeDescription\" [keywords]=\"text.themeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.theme\" [options]=\"themeOptions()\" [value]=\"themeMode()\" (valueChange)=\"setThemeMode($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.accent\" [description]=\"text.accentDescription\" [keywords]=\"text.accentKeywords\" [stacked]=\"true\">\n <hm-theme-picker\n class=\"sp-settings__picker\"\n [families]=\"families\"\n [family]=\"family()\"\n [theme]=\"appliedTheme()\"\n [showMode]=\"false\"\n (familyChosen)=\"setFamily($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.density\" [description]=\"text.densityDescription\" [keywords]=\"text.densityKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.density\" [options]=\"densityOptions()\" [value]=\"densityValue()\" (valueChange)=\"setDensity($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.font\" [description]=\"text.fontDescription\" [keywords]=\"text.fontKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.font\" [options]=\"fontOptions\" [value]=\"fontValue()\" (valueChange)=\"setFont($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.sidebar\" [description]=\"text.sidebarDescription\" [keywords]=\"text.sidebarKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.sidebar })\" [text]=\"text.sidebarHelp\" />\n <hm-segmented-control emphasis=\"action\" [label]=\"text.sidebar\" [options]=\"sidebarOptions()\" [value]=\"sidebar.mode()\" (valueChange)=\"setSidebarMode($event)\" />\n </hm-settings-row>\n\n <!-- Dividir abas: com ela desligada (o padr\u00E3o) a \u00E1rea de abas fica num painel s\u00F3 (SpineWorkspaceNavService). -->\n <hm-settings-row [label]=\"text.splitTabs\" [description]=\"text.splitTabsDescription\" [keywords]=\"text.splitTabsKeywords\">\n <hm-checkbox [label]=\"text.splitTabs\" [labelHidden]=\"true\" [checked]=\"preferences.splitTabs()\" (checkedChange)=\"preferences.setSplitTabs($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Acessibilidade \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Tamanho do texto (--hm-text-scale no <html>, HermesZoomService) e movimento (data-motion no <html>,\n HermesMotionService), guardados pelo SpinePreferencesService. -->\n <hm-settings-section [key]=\"keys.accessibility\" [label]=\"text.accessibility\" icon=\"accessibility-1\" [keywords]=\"text.accessibilityKeywords\">\n <hm-settings-row [label]=\"text.textSize\" [description]=\"text.textSizeDescription\" [keywords]=\"text.textSizeKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.textSize\" [options]=\"textSizeOptions()\" [value]=\"textSizeValue()\" (valueChange)=\"setTextSize($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.motion\" [description]=\"text.motionDescription\" [keywords]=\"text.motionKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.motion\" [options]=\"motionOptions()\" [value]=\"preferences.motion()\" (valueChange)=\"setMotion($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Regi\u00E3o \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.region\" [label]=\"text.region\" icon=\"clock\" [keywords]=\"text.regionKeywords\">\n <hm-settings-row [label]=\"text.language\" [description]=\"text.languageDescription\" [keywords]=\"text.languageKeywords\">\n <hm-select\n class=\"sp-settings__language\"\n [label]=\"text.language\"\n [labelHidden]=\"true\"\n [value]=\"preferences.language()\"\n (valueChange)=\"preferences.setLanguage($event ?? '')\"\n >\n @for (option of languageOptions(); track option.value) {\n <option [value]=\"option.value\" [attr.lang]=\"option.value || null\">{{ option.label }}</option>\n }\n </hm-select>\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.timeZone\" [description]=\"fmt(text.timeZoneHint, { now: timeZonePreview() })\" [keywords]=\"text.timeZoneKeywords\">\n <hm-combobox\n class=\"sp-settings__timezone\"\n [label]=\"text.timeZone\"\n [labelHidden]=\"true\"\n icon=\"world\"\n [options]=\"timeZoneOptions()\"\n [value]=\"preferences.timeZone()\"\n (valueChange)=\"preferences.setTimeZone($event)\"\n />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.hourCycle\" [description]=\"fmt(text.hourCycleHint, { now: hourCyclePreview() })\" [keywords]=\"text.hourCycleKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.hourCycle\" [options]=\"hourCycleOptions()\" [value]=\"preferences.hourCycle()\" (valueChange)=\"setHourCycle($event)\" />\n </hm-settings-row>\n\n <hm-settings-row [label]=\"text.weekStart\" [description]=\"text.weekStartDescription\" [keywords]=\"text.weekStartKeywords\">\n <hm-segmented-control emphasis=\"action\" [label]=\"text.weekStart\" [options]=\"weekStartOptions()\" [value]=\"weekStartValue()\" (valueChange)=\"setWeekStart($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Notifica\u00E7\u00F5es (SpineFeatures.notices) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Os tipos de aviso que o app declarou (SpineNoticesService) e o aviso do navegador com a aba em segundo plano. -->\n @if (notices.enabled) {\n <hm-settings-section [key]=\"keys.notices\" [label]=\"featureText.notices\" icon=\"bell\" [description]=\"featureText.noticesDescription\" [keywords]=\"featureText.noticesKeywords\">\n @for (type of notices.types; track type.id) {\n <hm-settings-row [label]=\"type.label\" [description]=\"type.description ?? ''\">\n <hm-checkbox [label]=\"type.label\" [labelHidden]=\"true\" [checked]=\"notices.isEnabled(type.id)\" (checkedChange)=\"notices.setEnabled(type.id, $event)\" />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"featureText.browserNotices\" [description]=\"notices.permission() === 'denied' ? featureText.browserNoticesDenied : notices.permission() === 'unsupported' ? featureText.browserNoticesUnsupported : featureText.browserNoticesDescription\">\n <hm-checkbox [label]=\"featureText.browserNotices\" [labelHidden]=\"true\" [checked]=\"notices.browserActive()\" (checkedChange)=\"setBrowserNotices($event)\" />\n </hm-settings-row>\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Atalhos \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.shortcuts\" [label]=\"text.shortcuts\" icon=\"keyboard\" [description]=\"text.shortcutsDescription\" [keywords]=\"text.shortcutsKeywords\">\n <!-- Mesma lista do painel \"?\", sem busca (a do di\u00E1logo de configura\u00E7\u00F5es j\u00E1 cobre a se\u00E7\u00E3o);\n o interruptor de tecla \u00FAnica (WCAG 2.1.4) persiste no SpinePreferencesService. -->\n <hm-shortcut-list class=\"sp-settings__shortcuts\" characterKeysToggle [registered]=\"false\" [items]=\"shortcutItems()\" [headingLevel]=\"4\" />\n </hm-settings-section>\n\n <!-- \u2500\u2500 Dados do navegador \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Listas por usu\u00E1rio do SpineUserStorage e as prefer\u00EAncias deste painel. Destrutivo: o 1\u00BA clique pede\n confirma\u00E7\u00E3o (bot\u00E3o vira \"Confirmar\" + Cancelar), como nas listas de tokens e sess\u00F5es. -->\n <hm-settings-section [key]=\"keys.data\" [label]=\"text.browserData\" icon=\"browser-setting\" [description]=\"text.browserDataDescription\" [keywords]=\"text.browserDataKeywords\">\n <!-- SpineFeatures.accountPreferences: as prefer\u00EAncias tamb\u00E9m na conta do Argos (as telas acima n\u00E3o mudam). -->\n @if (features.accountPreferences) {\n <hm-settings-row [label]=\"featureText.sync\" [description]=\"syncStatus() || featureText.syncDescription\" [keywords]=\"featureText.syncKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: featureText.sync })\" [text]=\"featureText.syncDescription\" />\n <hm-button emphasis=\"medium\" icon=\"refresh\" [disabled]=\"accountPreferences.status() === 'local'\" [loading]=\"accountPreferences.status() === 'saving' || accountPreferences.status() === 'loading'\" (clicked)=\"syncNow()\">{{ featureText.syncNow }}</hm-button>\n </hm-settings-row>\n }\n @for (list of browserLists(); track list.id) {\n <hm-settings-row [label]=\"list.label\" [description]=\"list.count\" [keywords]=\"list.keywords\">\n <span class=\"sp-settings__actions\">\n @if (confirming() === list.id) {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button\n [emphasis]=\"confirming() === list.id ? 'high' : 'medium'\"\n intent=\"danger\"\n [disabled]=\"list.empty && confirming() !== list.id\"\n (clicked)=\"clearList(list.id)\"\n >{{ confirming() === list.id ? text.confirmClear : text.clear }}</hm-button>\n </span>\n </hm-settings-row>\n }\n\n <hm-settings-row [label]=\"text.restorePreferences\" [description]=\"text.restorePreferencesDescription\" [keywords]=\"text.restorePreferencesKeywords\">\n <hm-toggletip rowHelp [label]=\"fmt(text.moreAbout, { label: text.restorePreferences })\" [text]=\"text.restorePreferencesHelp\" />\n <span class=\"sp-settings__actions\">\n @if (confirming() === 'preferences') {\n <hm-button emphasis=\"medium\" (clicked)=\"cancelConfirm()\">{{ text.cancel }}</hm-button>\n }\n <hm-button [emphasis]=\"confirming() === 'preferences' ? 'high' : 'medium'\" intent=\"danger\" (clicked)=\"restorePreferences()\">\n {{ confirming() === 'preferences' ? text.confirmRestore : text.restore }}\n </hm-button>\n </span>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Ajuda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <!-- Novidades (s\u00F3 com alguma vigente), configura\u00E7\u00E3o inicial e o essencial: fecham o painel antes de abrir. -->\n <hm-settings-section [key]=\"keys.help\" [label]=\"text.help\" icon=\"help\" [keywords]=\"text.helpKeywords\">\n @if (hasNews()) {\n <hm-settings-row [label]=\"text.news\" [description]=\"text.newsDescription\" [keywords]=\"text.newsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"star\" (clicked)=\"showNews()\">{{ text.showNews }}</hm-button>\n </hm-settings-row>\n }\n @if (setupEnabled) {\n <hm-settings-row [label]=\"text.setup\" [description]=\"text.setupDescription\" [keywords]=\"text.setupKeywords\">\n <hm-button emphasis=\"medium\" icon=\"color-palette\" (clicked)=\"redoSetup()\">{{ text.redoSetup }}</hm-button>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.essentials\" [description]=\"text.essentialsDescription\" [keywords]=\"text.essentialsKeywords\">\n <hm-button emphasis=\"medium\" icon=\"help\" (clicked)=\"showEssentials()\">{{ text.showEssentials }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n\n <!-- \u2500\u2500 Se\u00E7\u00F5es do app (provideSettingsSection) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @for (s of appSections; track s.key) {\n <hm-settings-section [key]=\"s.key\" [label]=\"s.label\" [icon]=\"s.icon ?? 'cog'\" [description]=\"s.description ?? ''\" [keywords]=\"s.keywords ?? ''\">\n <ng-container *ngComponentOutlet=\"s.component\" />\n </hm-settings-section>\n }\n\n <!-- \u2500\u2500 Sobre \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <hm-settings-section [key]=\"keys.about\" [label]=\"text.about\" icon=\"info-circle\" [keywords]=\"text.aboutKeywords\">\n <hm-settings-row [label]=\"text.application\">\n <span class=\"sp-settings__value\">{{ appName }}@if (packageInfo.name) { <span class=\"sp-settings__muted\"> \u00B7 {{ packageInfo.name }}</span> }</span>\n </hm-settings-row>\n <!-- M\u00E9dia da ferramenta (SpineFeatures.ratings) e \"Avaliar\", que abre o di\u00E1logo da estrela do cabe\u00E7alho. -->\n @if (ratingsEnabled) {\n <hm-settings-row [label]=\"ratingText.aboutRating\" [keywords]=\"ratingText.aboutRatingKeywords\">\n <spine-app-rating-summary />\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.version\" [keywords]=\"text.versionKeywords\">\n <span class=\"sp-settings__value sp-settings__mono\">{{ packageInfo.version ?? '\u2014' }}</span>\n </hm-settings-row>\n @if (buildDate) {\n <hm-settings-row [label]=\"text.buildDate\">\n <span class=\"sp-settings__value\">{{ buildDate }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.environment) {\n <hm-settings-row [label]=\"text.environment\" [keywords]=\"text.environmentKeywords\">\n <span class=\"sp-settings__value\">{{ packageInfo.environment }}</span>\n </hm-settings-row>\n }\n @if (packageInfo.repository?.url || packageInfo.docs || packageInfo.issues) {\n <hm-settings-row [label]=\"text.links\" [keywords]=\"text.linksKeywords\">\n <span class=\"sp-settings__links\">\n @if (packageInfo.docs) { <hm-button emphasis=\"medium\" icon=\"book\" [link]=\"packageInfo.docs\" [newTab]=\"true\">{{ text.docs }}</hm-button> }\n @if (packageInfo.issues) { <hm-button emphasis=\"medium\" icon=\"bug\" [link]=\"packageInfo.issues\" [newTab]=\"true\">{{ text.reportIssue }}</hm-button> }\n @if (packageInfo.repository?.url) { <hm-button emphasis=\"medium\" icon=\"link\" [link]=\"packageInfo.repository!.url!\" [newTab]=\"true\">{{ text.repository }}</hm-button> }\n </span>\n </hm-settings-row>\n }\n @if (dependencies.length) {\n <hm-settings-row [label]=\"text.dependencies\" [description]=\"text.dependenciesDescription\" [keywords]=\"text.dependenciesKeywords\" [stacked]=\"true\">\n <dl class=\"sp-settings__deps\">\n @for (d of dependencies; track d.name) {\n <div><dt>{{ d.label }}</dt><dd class=\"sp-settings__mono\">{{ d.version }}</dd></div>\n }\n </dl>\n </hm-settings-row>\n }\n <hm-settings-row [label]=\"text.supportInfo\" [description]=\"text.supportInfoDescription\" [keywords]=\"text.supportInfoKeywords\">\n <hm-button emphasis=\"medium\" icon=\"clipboard\" (clicked)=\"copySupportInfo()\">{{ text.copy }}</hm-button>\n </hm-settings-row>\n </hm-settings-section>\n </hm-settings-dialog>\n}\n", styles: ["@charset \"UTF-8\";.sp-settings__timezone{display:block;width:min(320px,100%)}.sp-settings__profile{display:flex;align-items:center;gap:var(--hm-space-sm);padding:var(--hm-space-xs) 0 var(--hm-space-sm)}.sp-settings__profile-text{display:flex;min-width:0;flex-direction:column;gap:var(--hm-space-micro);color:var(--hm-text);overflow-wrap:anywhere}.sp-settings__profile-text span{color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.sp-settings__value{color:var(--hm-text);font-size:var(--hm-font-size-sm);text-align:end;overflow-wrap:anywhere}.sp-settings__muted{color:var(--hm-muted)}.sp-settings__mono{font-family:var(--hm-font-mono)}.sp-settings__shortcuts{display:block;padding-block:var(--hm-space-xxxs);border-top:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__links,.sp-settings__actions{display:inline-flex;flex-wrap:wrap;justify-content:flex-end;gap:var(--hm-space-xs)}.sp-settings__section--group-open ::ng-deep .hm-settings-section__heading{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__picker ::ng-deep .hm-theme-picker__title{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}.sp-settings__deps{display:grid;flex:1;min-width:0;grid-template-columns:repeat(auto-fill,minmax(min(12rem,100%),1fr));margin:0;overflow:hidden;border:var(--hm-border-hairline) solid var(--hm-border);border-radius:var(--hm-radius);background:var(--hm-surface)}.sp-settings__deps div{display:flex;justify-content:space-between;gap:var(--hm-space-xs);margin-block-start:calc(-1 * var(--hm-border-hairline));margin-inline-start:calc(-1 * var(--hm-border-hairline));padding:var(--hm-space-xs) var(--hm-space-sm);border-top:var(--hm-border-hairline) solid var(--hm-divider);border-inline-start:var(--hm-border-hairline) solid var(--hm-divider)}.sp-settings__deps dt{color:var(--hm-muted);font-size:var(--hm-font-size-xs)}.sp-settings__deps dd{margin:0;color:var(--hm-text);font-size:var(--hm-font-size-xs)}\n"] }]
|
|
6522
7084
|
}], ctorParameters: () => [] });
|
|
6523
7085
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6524
7086
|
/** "UTC−03:00" para um fuso IANA. */
|
|
@@ -6996,7 +7558,7 @@ function registerPage() {
|
|
|
6996
7558
|
route_redirect: SPINE_SUGGESTIONS_PAGE_PATH,
|
|
6997
7559
|
description: text.description,
|
|
6998
7560
|
},
|
|
6999
|
-
load: () => import('./trismegisto-spine-layout-suggestions-page.component-
|
|
7561
|
+
load: () => import('./trismegisto-spine-layout-suggestions-page.component-Bh3VGlOR.mjs').then((m) => m.SpineSuggestionsPageComponent),
|
|
7000
7562
|
});
|
|
7001
7563
|
});
|
|
7002
7564
|
}
|
|
@@ -8101,6 +8663,394 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
8101
8663
|
`, styles: [":host{display:block;flex:none}\n"] }]
|
|
8102
8664
|
}] });
|
|
8103
8665
|
|
|
8666
|
+
const PANEL_ID = 'spine-ai-agent-panel';
|
|
8667
|
+
const TITLE_ID = 'spine-ai-agent-title';
|
|
8668
|
+
const PANEL_STATE_KEY = 'spine-ai-agent:panel';
|
|
8669
|
+
// Em px, medidas do ponteiro: até aqui é um clique, e o balão não encosta na borda da tela.
|
|
8670
|
+
const DRAG_THRESHOLD = 4;
|
|
8671
|
+
const DRAG_MARGIN = 8;
|
|
8672
|
+
const GREETING_KEY = 'spine-ai-agent:greeting-closed';
|
|
8673
|
+
const GREETING_MS = 15_000;
|
|
8674
|
+
const GREETING_SUGGESTIONS = 3;
|
|
8675
|
+
function greetingClosed() {
|
|
8676
|
+
try {
|
|
8677
|
+
return sessionStorage.getItem(GREETING_KEY) === '1';
|
|
8678
|
+
}
|
|
8679
|
+
catch {
|
|
8680
|
+
return false;
|
|
8681
|
+
}
|
|
8682
|
+
}
|
|
8683
|
+
function readPanelState() {
|
|
8684
|
+
try {
|
|
8685
|
+
return JSON.parse(sessionStorage.getItem(PANEL_STATE_KEY) ?? '{}');
|
|
8686
|
+
}
|
|
8687
|
+
catch {
|
|
8688
|
+
return {};
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
/**
|
|
8692
|
+
* O agente de IA do app: um balão flutuante, que abre a conversa num painel ao lado dele. O painel não é modal, para a
|
|
8693
|
+
* pessoa continuar vendo e usando a tela enquanto pergunta. Esc fecha e devolve o foco a quem abriu, e o painel pode ser
|
|
8694
|
+
* expandido para respostas longas. Aberto e tamanho voltam como estavam ao recarregar a aba. A tela da conversa fica em chunk
|
|
8695
|
+
* próprio, carregado na primeira abertura, e continua montada depois disso.
|
|
8696
|
+
*
|
|
8697
|
+
* O balão pode ser arrastado e, pelo X dele, sair da tela: vira um ícone no cabeçalho (`spine-ai-agent-dock-button`), e o
|
|
8698
|
+
* painel passa a abrir logo abaixo do cabeçalho. Onde ele está vale neste navegador. Ao lado do balão aparece um convite com
|
|
8699
|
+
* o que dá para perguntar, uma vez por aba, que sai sozinho ou quando a pessoa o fecha. Sem `SpineFeatures.aiAgent`, não
|
|
8700
|
+
* desenha nada.
|
|
8701
|
+
*/
|
|
8702
|
+
class SpineAiAgentLauncherComponent {
|
|
8703
|
+
constructor() {
|
|
8704
|
+
this.agent = inject(SpineAiAgentService);
|
|
8705
|
+
this.text = inject(SPINE_MESSAGES).aiAgentChat;
|
|
8706
|
+
this.panelId = PANEL_ID;
|
|
8707
|
+
this.titleId = TITLE_ID;
|
|
8708
|
+
/** Tamanho do painel: continua como a pessoa deixou ao fechar e abrir. */
|
|
8709
|
+
this.expanded = signal(readPanelState().expanded === true, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
8710
|
+
this.chat = signal(null, ...(ngDevMode ? [{ debugName: "chat" }] : /* istanbul ignore next */ []));
|
|
8711
|
+
this.dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
|
|
8712
|
+
/** Com o balão na metade de cima da tela, o painel abre para baixo, onde há espaço. */
|
|
8713
|
+
this.below = computed(() => !this.agent.docked() && (this.agent.position()?.bottom ?? 0) > window.innerHeight / 2, ...(ngDevMode ? [{ debugName: "below" }] : /* istanbul ignore next */ []));
|
|
8714
|
+
/** O balão é um círculo: o rosto cabe nele, e uma imagem de corpo inteiro cortaria onde não deve. */
|
|
8715
|
+
this.fabImage = computed(() => this.agent.avatarNow() ?? this.agent.image(), ...(ngDevMode ? [{ debugName: "fabImage" }] : /* istanbul ignore next */ []));
|
|
8716
|
+
/** Com o balão na metade inicial da tela, o convite fica depois dele. */
|
|
8717
|
+
this.greetingAfter = computed(() => (this.agent.position()?.end ?? 0) > window.innerWidth / 2, ...(ngDevMode ? [{ debugName: "greetingAfter" }] : /* istanbul ignore next */ []));
|
|
8718
|
+
this.greetingOpen = signal(!greetingClosed(), ...(ngDevMode ? [{ debugName: "greetingOpen" }] : /* istanbul ignore next */ []));
|
|
8719
|
+
this.greetingVisible = computed(() => this.greetingOpen() && !this.agent.panelOpen() && !this.agent.docked() && !this.dragging(), ...(ngDevMode ? [{ debugName: "greetingVisible" }] : /* istanbul ignore next */ []));
|
|
8720
|
+
this.greetingSuggestions = computed(() => this.agent.suggestions().slice(0, GREETING_SUGGESTIONS), ...(ngDevMode ? [{ debugName: "greetingSuggestions" }] : /* istanbul ignore next */ []));
|
|
8721
|
+
this.host = inject(ElementRef);
|
|
8722
|
+
this.injector = inject(Injector);
|
|
8723
|
+
this.document = inject(DOCUMENT);
|
|
8724
|
+
/** O clique que encerra um arraste não abre o painel. */
|
|
8725
|
+
this.dragged = false;
|
|
8726
|
+
this.stopDrag = null;
|
|
8727
|
+
this.greetingTimer = null;
|
|
8728
|
+
inject(DestroyRef).onDestroy(() => {
|
|
8729
|
+
this.stopDrag?.();
|
|
8730
|
+
this.keepGreeting();
|
|
8731
|
+
});
|
|
8732
|
+
effect(() => {
|
|
8733
|
+
const state = JSON.stringify({ open: this.agent.panelOpen(), expanded: this.expanded() });
|
|
8734
|
+
try {
|
|
8735
|
+
sessionStorage.setItem(PANEL_STATE_KEY, state);
|
|
8736
|
+
}
|
|
8737
|
+
catch {
|
|
8738
|
+
// Navegador sem armazenamento: o painel só não volta aberto.
|
|
8739
|
+
}
|
|
8740
|
+
});
|
|
8741
|
+
// O painel também abre pelo ícone do cabeçalho, que está em outro componente.
|
|
8742
|
+
effect(() => {
|
|
8743
|
+
if (this.agent.panelOpen())
|
|
8744
|
+
untracked(() => void this.prepare());
|
|
8745
|
+
});
|
|
8746
|
+
}
|
|
8747
|
+
ngOnInit() {
|
|
8748
|
+
// Sem roubar o foco: a pessoa acabou de recarregar a tela, não pediu o chat agora.
|
|
8749
|
+
if (this.agent.enabled && readPanelState().open === true)
|
|
8750
|
+
this.agent.panelOpen.set(true);
|
|
8751
|
+
if (this.agent.enabled && this.greetingVisible()) {
|
|
8752
|
+
this.agent.loadSuggestions();
|
|
8753
|
+
this.greetingTimer = setTimeout(() => this.closeGreeting(), GREETING_MS);
|
|
8754
|
+
}
|
|
8755
|
+
}
|
|
8756
|
+
toggle() {
|
|
8757
|
+
if (!this.dragged)
|
|
8758
|
+
this.agent.togglePanel();
|
|
8759
|
+
}
|
|
8760
|
+
async prepare() {
|
|
8761
|
+
const focus = this.agent.focusOnOpen;
|
|
8762
|
+
this.agent.focusOnOpen = false;
|
|
8763
|
+
if (!this.chat()) {
|
|
8764
|
+
const { SpineAiAgentChatComponent } = await import('./trismegisto-spine-layout-ai-agent-chat.component-C5JljPYD.mjs');
|
|
8765
|
+
this.chat.set(SpineAiAgentChatComponent);
|
|
8766
|
+
}
|
|
8767
|
+
this.agent.loadSuggestions();
|
|
8768
|
+
this.agent.restoreConversation();
|
|
8769
|
+
if (focus)
|
|
8770
|
+
afterNextRender(() => this.host.nativeElement.querySelector('textarea')?.focus(), { injector: this.injector });
|
|
8771
|
+
}
|
|
8772
|
+
/** Quem está lendo ou usando o convite decide quando ele sai. */
|
|
8773
|
+
keepGreeting() {
|
|
8774
|
+
if (this.greetingTimer)
|
|
8775
|
+
clearTimeout(this.greetingTimer);
|
|
8776
|
+
this.greetingTimer = null;
|
|
8777
|
+
}
|
|
8778
|
+
closeGreeting() {
|
|
8779
|
+
this.keepGreeting();
|
|
8780
|
+
this.greetingOpen.set(false);
|
|
8781
|
+
try {
|
|
8782
|
+
sessionStorage.setItem(GREETING_KEY, '1');
|
|
8783
|
+
}
|
|
8784
|
+
catch {
|
|
8785
|
+
// Navegador sem armazenamento: o convite só volta ao recarregar.
|
|
8786
|
+
}
|
|
8787
|
+
}
|
|
8788
|
+
askFromGreeting(question) {
|
|
8789
|
+
this.closeGreeting();
|
|
8790
|
+
this.agent.askFromGreeting(question);
|
|
8791
|
+
}
|
|
8792
|
+
close() {
|
|
8793
|
+
this.agent.panelOpen.set(false);
|
|
8794
|
+
this.focusEntry();
|
|
8795
|
+
}
|
|
8796
|
+
dock() {
|
|
8797
|
+
this.agent.dock();
|
|
8798
|
+
afterNextRender(() => this.focusEntry(), { injector: this.injector });
|
|
8799
|
+
}
|
|
8800
|
+
undock() {
|
|
8801
|
+
this.agent.undock();
|
|
8802
|
+
afterNextRender(() => this.focusEntry(), { injector: this.injector });
|
|
8803
|
+
}
|
|
8804
|
+
/** O foco volta a quem abre o painel: o balão ou, com ele fora da tela, o ícone do cabeçalho. */
|
|
8805
|
+
focusEntry() {
|
|
8806
|
+
const entry = this.agent.docked()
|
|
8807
|
+
? this.document.querySelector(`.${SPINE_AGENT_DOCK_CLASS} button`)
|
|
8808
|
+
: this.host.nativeElement.querySelector('.hm-fab__button');
|
|
8809
|
+
entry?.focus();
|
|
8810
|
+
}
|
|
8811
|
+
/** Só passa a ser arraste depois de `DRAG_THRESHOLD`; antes disso, soltar é o clique normal do balão. */
|
|
8812
|
+
startDrag(event) {
|
|
8813
|
+
if (event.button !== 0)
|
|
8814
|
+
return;
|
|
8815
|
+
this.dragged = false;
|
|
8816
|
+
const box = event.currentTarget.getBoundingClientRect();
|
|
8817
|
+
const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
|
|
8818
|
+
const { clientX: startX, clientY: startY } = event;
|
|
8819
|
+
const view = this.document.defaultView;
|
|
8820
|
+
const move = (moved) => {
|
|
8821
|
+
const dx = moved.clientX - startX;
|
|
8822
|
+
const dy = moved.clientY - startY;
|
|
8823
|
+
if (!this.dragged && Math.hypot(dx, dy) < DRAG_THRESHOLD)
|
|
8824
|
+
return;
|
|
8825
|
+
this.dragged = true;
|
|
8826
|
+
this.dragging.set(true);
|
|
8827
|
+
const left = Math.min(Math.max(box.left + dx, DRAG_MARGIN), view.innerWidth - box.width - DRAG_MARGIN);
|
|
8828
|
+
const top = Math.min(Math.max(box.top + dy, DRAG_MARGIN), view.innerHeight - box.height - DRAG_MARGIN);
|
|
8829
|
+
this.agent.position.set({ end: rtl ? left : view.innerWidth - left - box.width, bottom: view.innerHeight - top - box.height });
|
|
8830
|
+
};
|
|
8831
|
+
const stop = () => {
|
|
8832
|
+
this.stopDrag = null;
|
|
8833
|
+
this.document.removeEventListener('pointermove', move);
|
|
8834
|
+
this.document.removeEventListener('pointerup', stop);
|
|
8835
|
+
this.document.removeEventListener('pointercancel', stop);
|
|
8836
|
+
this.dragging.set(false);
|
|
8837
|
+
if (this.dragged)
|
|
8838
|
+
this.agent.savePlacement();
|
|
8839
|
+
};
|
|
8840
|
+
this.document.addEventListener('pointermove', move);
|
|
8841
|
+
this.document.addEventListener('pointerup', stop);
|
|
8842
|
+
this.document.addEventListener('pointercancel', stop);
|
|
8843
|
+
this.stopDrag = stop;
|
|
8844
|
+
}
|
|
8845
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentLauncherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
8846
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.24", type: SpineAiAgentLauncherComponent, isStandalone: true, selector: "spine-ai-agent-launcher", host: { properties: { "class.spine-agent--docked": "agent.docked()", "class.spine-agent--below": "below()", "style.--spine-agent-end": "agent.position() ? agent.position()!.end + 'px' : null", "style.--spine-agent-bottom": "agent.position() ? agent.position()!.bottom + 'px' : null" } }, ngImport: i0, template: `
|
|
8847
|
+
@if (agent.enabled) {
|
|
8848
|
+
@if (chat(); as component) {
|
|
8849
|
+
<section
|
|
8850
|
+
class="spine-agent-panel"
|
|
8851
|
+
role="dialog"
|
|
8852
|
+
aria-modal="false"
|
|
8853
|
+
[attr.id]="panelId"
|
|
8854
|
+
[attr.aria-labelledby]="titleId"
|
|
8855
|
+
[class.spine-agent-panel--open]="agent.panelOpen()"
|
|
8856
|
+
[class.spine-agent-panel--expanded]="expanded()"
|
|
8857
|
+
[hidden]="!agent.panelOpen()"
|
|
8858
|
+
(keydown.escape)="close()"
|
|
8859
|
+
>
|
|
8860
|
+
<header class="spine-agent-panel__header">
|
|
8861
|
+
@if (agent.avatarNow(); as avatar) {
|
|
8862
|
+
<img class="spine-agent-panel__avatar" [src]="avatar" alt="" (error)="agent.imageFailed(avatar)" />
|
|
8863
|
+
}
|
|
8864
|
+
<h2 class="spine-agent-panel__title" [attr.id]="titleId">{{ agent.title }}</h2>
|
|
8865
|
+
<hm-icon-button
|
|
8866
|
+
emphasis="subtle"
|
|
8867
|
+
size="small"
|
|
8868
|
+
icon="plus"
|
|
8869
|
+
[label]="text.newConversation"
|
|
8870
|
+
[disabled]="agent.running() || !agent.entries().length"
|
|
8871
|
+
(clicked)="agent.reset()"
|
|
8872
|
+
/>
|
|
8873
|
+
<hm-icon-button
|
|
8874
|
+
emphasis="subtle"
|
|
8875
|
+
size="small"
|
|
8876
|
+
[icon]="expanded() ? 'window-restore' : 'window-max'"
|
|
8877
|
+
[label]="expanded() ? text.collapse : text.expand"
|
|
8878
|
+
(clicked)="expanded.set(!expanded())"
|
|
8879
|
+
/>
|
|
8880
|
+
@if (agent.docked()) {
|
|
8881
|
+
<hm-icon-button emphasis="subtle" size="small" icon="unpin" [label]="text.undock" (clicked)="undock()" />
|
|
8882
|
+
}
|
|
8883
|
+
<hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.close" (clicked)="close()" />
|
|
8884
|
+
</header>
|
|
8885
|
+
<div class="spine-agent-panel__body">
|
|
8886
|
+
<ng-container *ngComponentOutlet="component" />
|
|
8887
|
+
</div>
|
|
8888
|
+
</section>
|
|
8889
|
+
}
|
|
8890
|
+
@if (!agent.docked()) {
|
|
8891
|
+
<div class="spine-agent-fab" [class.spine-agent-fab--dragging]="dragging()" (pointerdown)="startDrag($event)">
|
|
8892
|
+
@if (greetingVisible()) {
|
|
8893
|
+
<section
|
|
8894
|
+
class="spine-agent-greeting"
|
|
8895
|
+
[class.spine-agent-greeting--after]="greetingAfter()"
|
|
8896
|
+
[class.spine-agent-greeting--below]="below()"
|
|
8897
|
+
[class.spine-agent-greeting--figure]="agent.image()"
|
|
8898
|
+
[attr.aria-label]="agent.title"
|
|
8899
|
+
(pointerdown)="$event.stopPropagation()"
|
|
8900
|
+
(mouseenter)="keepGreeting()"
|
|
8901
|
+
(focusin)="keepGreeting()"
|
|
8902
|
+
>
|
|
8903
|
+
@if (agent.image(); as figure) {
|
|
8904
|
+
<img class="spine-agent-greeting__figure" [src]="figure" alt="" (error)="agent.imageFailed(figure)" />
|
|
8905
|
+
}
|
|
8906
|
+
<div class="spine-agent-greeting__content">
|
|
8907
|
+
<div class="spine-agent-greeting__head">
|
|
8908
|
+
<p class="spine-agent-greeting__title">{{ agent.greetingTitle }}</p>
|
|
8909
|
+
<hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.greetingClose" (clicked)="closeGreeting()" />
|
|
8910
|
+
</div>
|
|
8911
|
+
<p class="spine-agent-greeting__text">{{ agent.greetingText }}</p>
|
|
8912
|
+
@if (greetingSuggestions().length) {
|
|
8913
|
+
<hm-prompt-suggestions [suggestions]="greetingSuggestions()" (chosen)="askFromGreeting($event)" />
|
|
8914
|
+
}
|
|
8915
|
+
</div>
|
|
8916
|
+
</section>
|
|
8917
|
+
}
|
|
8918
|
+
<hm-fab
|
|
8919
|
+
[fixed]="false"
|
|
8920
|
+
[label]="agent.panelOpen() ? text.close : agent.title"
|
|
8921
|
+
[icon]="agent.panelOpen() ? 'times' : fabImage() ? '' : 'chat-bubble'"
|
|
8922
|
+
[expanded]="agent.panelOpen()"
|
|
8923
|
+
[controls]="panelId"
|
|
8924
|
+
avoid=""
|
|
8925
|
+
(triggered)="toggle()"
|
|
8926
|
+
/>
|
|
8927
|
+
@if (!agent.panelOpen() && fabImage(); as image) {
|
|
8928
|
+
<img class="spine-agent-fab__image" [src]="image" alt="" (error)="agent.imageFailed(image)" />
|
|
8929
|
+
}
|
|
8930
|
+
@if (!agent.panelOpen()) {
|
|
8931
|
+
<hm-icon-button
|
|
8932
|
+
class="spine-agent-fab__dock"
|
|
8933
|
+
emphasis="outlined"
|
|
8934
|
+
size="small"
|
|
8935
|
+
icon="times"
|
|
8936
|
+
[label]="text.dock"
|
|
8937
|
+
(pointerdown)="$event.stopPropagation()"
|
|
8938
|
+
(clicked)="dock()"
|
|
8939
|
+
/>
|
|
8940
|
+
}
|
|
8941
|
+
</div>
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
`, isInline: true, styles: [":host{--spine-agent-fab-size: calc(var(--hm-control-height-large) * 1.75);--spine-agent-end-now: var(--spine-agent-end, var(--hm-space-lg));--spine-agent-bottom-now: var(--spine-agent-bottom, var(--hm-space-lg));--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 11), calc(100vw - 2 * var(--hm-space-md)));--spine-agent-offset: calc(var(--spine-agent-bottom-now) + var(--spine-agent-fab-size) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-header-height) - var(--hm-space-xs))}:host(.spine-agent--below){--spine-agent-offset: calc(100dvh - var(--spine-agent-bottom-now) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}:host(.spine-agent--docked){--spine-agent-offset: calc(var(--hm-header-height) + var(--hm-space-xs));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}.spine-agent-fab{position:fixed;inset-inline-end:min(var(--spine-agent-end-now),calc(100vw - var(--spine-agent-fab-size) - var(--hm-space-xs)));inset-block-end:calc(min(var(--spine-agent-bottom-now),calc(100dvh - var(--spine-agent-fab-size) - var(--hm-space-xs))) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);inline-size:var(--spine-agent-fab-size);block-size:var(--spine-agent-fab-size);cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.spine-agent-fab--dragging{cursor:grabbing}.spine-agent-fab__image{position:absolute;inset:0;inline-size:100%;block-size:100%;object-fit:cover;border-radius:var(--hm-radius-pill);pointer-events:none}.spine-agent-greeting{position:absolute;inset-block-end:0;inset-inline-end:calc(100% + var(--hm-space-sm));display:flex;align-items:flex-end;gap:var(--hm-space-sm);box-sizing:border-box;inline-size:min(calc(var(--hm-space-3xl) * 7),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)));overflow:hidden;padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay);cursor:auto;touch-action:auto;-webkit-user-select:text;user-select:text;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}.spine-agent-greeting--figure{inline-size:min(calc(var(--hm-space-3xl) * 11),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)))}.spine-agent-greeting__figure{flex:none;inline-size:calc(var(--hm-space-3xl) * 3.5);margin-block-end:calc(var(--hm-space-sm) * -1);object-fit:contain;object-position:bottom}.spine-agent-greeting__content{display:flex;flex:1;flex-direction:column;align-self:stretch;gap:var(--hm-space-xs);min-inline-size:0}.spine-agent-greeting--after{inset-inline:calc(100% + var(--hm-space-sm)) auto}.spine-agent-greeting--below{inset-block:0 auto}.spine-agent-greeting__head{display:flex;align-items:center;gap:var(--hm-space-xs)}.spine-agent-greeting__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-greeting__text{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.spine-agent-fab--dragging hm-fab{pointer-events:none}.spine-agent-fab__dock{--hm-radius-control: var(--hm-radius-pill);position:absolute;inset-block-start:calc(var(--hm-space-xs) * -1);inset-inline-end:calc(var(--hm-space-xs) * -1);background:var(--hm-surface-raised);border-radius:var(--hm-radius-pill);box-shadow:var(--hm-shadow-overlay);opacity:0;transition:opacity var(--hm-duration-quick) var(--hm-ease)}.spine-agent-fab:hover .spine-agent-fab__dock,.spine-agent-fab:focus-within .spine-agent-fab__dock{opacity:1}@media(hover:none){.spine-agent-fab__dock{opacity:1}}.spine-agent-panel{position:fixed;inset-inline-end:clamp(var(--hm-space-md),var(--spine-agent-end-now),calc(100vw - var(--spine-agent-panel-inline) - var(--hm-space-md)));inset-block-end:calc(var(--spine-agent-offset) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);display:flex;flex-direction:column;inline-size:var(--spine-agent-panel-inline);block-size:min(calc(var(--hm-space-3xl) * 16),var(--spine-agent-room));overflow:hidden;background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay)}.spine-agent-panel[hidden]{display:none}:host(.spine-agent--below) .spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-block-start:var(--spine-agent-offset);inset-block-end:auto}:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-md)}.spine-agent-panel--expanded{--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 24), calc(100vw - 2 * var(--hm-space-md)));block-size:var(--spine-agent-room)}.spine-agent-panel--open{transform-origin:bottom right;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}:host(.spine-agent--below) .spine-agent-panel--open,:host(.spine-agent--docked) .spine-agent-panel--open{transform-origin:top right}@keyframes spine-agent-panel-in{0%{opacity:0;transform:translateY(var(--hm-space-sm)) scale(.97)}}@media(prefers-reduced-motion:reduce){.spine-agent-panel--open,.spine-agent-greeting{animation:none}.spine-agent-fab__dock{transition:none}}.spine-agent-panel__header{display:flex;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}.spine-agent-panel__avatar{flex:none;inline-size:var(--hm-control-height);block-size:var(--hm-control-height);object-fit:cover;border-radius:var(--hm-radius-pill)}.spine-agent-panel__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-panel__body{flex:1;min-block-size:0;padding:var(--hm-space-md)}@media(max-width:600px){:host{--spine-agent-panel-inline: calc(100vw - 2 * var(--hm-space-xs))}.spine-agent-greeting__figure{display:none}.spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-xs)}}\n"], dependencies: [{ kind: "component", type: FabComponent, selector: "hm-fab", inputs: ["label", "icon", "extended", "position", "fixed", "size", "disabled", "highlight", "actions", "hideOnScroll", "avoid", "expanded", "controls"], outputs: ["triggered", "chosen"] }, { kind: "component", type: IconButtonComponent, selector: "hm-icon-button", inputs: ["label", "icon", "solid", "iconDirection", "emphasis", "intent", "size", "disabled", "toggle", "pressed", "keyShortcuts"], outputs: ["pressedChange", "clicked"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "component", type: PromptSuggestionsComponent, selector: "hm-prompt-suggestions", inputs: ["suggestions", "label", "disabled"], outputs: ["chosen"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
8945
|
+
}
|
|
8946
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: SpineAiAgentLauncherComponent, decorators: [{
|
|
8947
|
+
type: Component,
|
|
8948
|
+
args: [{ selector: 'spine-ai-agent-launcher', imports: [FabComponent, IconButtonComponent, NgComponentOutlet, PromptSuggestionsComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
8949
|
+
'[class.spine-agent--docked]': 'agent.docked()',
|
|
8950
|
+
'[class.spine-agent--below]': 'below()',
|
|
8951
|
+
'[style.--spine-agent-end]': "agent.position() ? agent.position()!.end + 'px' : null",
|
|
8952
|
+
'[style.--spine-agent-bottom]': "agent.position() ? agent.position()!.bottom + 'px' : null",
|
|
8953
|
+
}, template: `
|
|
8954
|
+
@if (agent.enabled) {
|
|
8955
|
+
@if (chat(); as component) {
|
|
8956
|
+
<section
|
|
8957
|
+
class="spine-agent-panel"
|
|
8958
|
+
role="dialog"
|
|
8959
|
+
aria-modal="false"
|
|
8960
|
+
[attr.id]="panelId"
|
|
8961
|
+
[attr.aria-labelledby]="titleId"
|
|
8962
|
+
[class.spine-agent-panel--open]="agent.panelOpen()"
|
|
8963
|
+
[class.spine-agent-panel--expanded]="expanded()"
|
|
8964
|
+
[hidden]="!agent.panelOpen()"
|
|
8965
|
+
(keydown.escape)="close()"
|
|
8966
|
+
>
|
|
8967
|
+
<header class="spine-agent-panel__header">
|
|
8968
|
+
@if (agent.avatarNow(); as avatar) {
|
|
8969
|
+
<img class="spine-agent-panel__avatar" [src]="avatar" alt="" (error)="agent.imageFailed(avatar)" />
|
|
8970
|
+
}
|
|
8971
|
+
<h2 class="spine-agent-panel__title" [attr.id]="titleId">{{ agent.title }}</h2>
|
|
8972
|
+
<hm-icon-button
|
|
8973
|
+
emphasis="subtle"
|
|
8974
|
+
size="small"
|
|
8975
|
+
icon="plus"
|
|
8976
|
+
[label]="text.newConversation"
|
|
8977
|
+
[disabled]="agent.running() || !agent.entries().length"
|
|
8978
|
+
(clicked)="agent.reset()"
|
|
8979
|
+
/>
|
|
8980
|
+
<hm-icon-button
|
|
8981
|
+
emphasis="subtle"
|
|
8982
|
+
size="small"
|
|
8983
|
+
[icon]="expanded() ? 'window-restore' : 'window-max'"
|
|
8984
|
+
[label]="expanded() ? text.collapse : text.expand"
|
|
8985
|
+
(clicked)="expanded.set(!expanded())"
|
|
8986
|
+
/>
|
|
8987
|
+
@if (agent.docked()) {
|
|
8988
|
+
<hm-icon-button emphasis="subtle" size="small" icon="unpin" [label]="text.undock" (clicked)="undock()" />
|
|
8989
|
+
}
|
|
8990
|
+
<hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.close" (clicked)="close()" />
|
|
8991
|
+
</header>
|
|
8992
|
+
<div class="spine-agent-panel__body">
|
|
8993
|
+
<ng-container *ngComponentOutlet="component" />
|
|
8994
|
+
</div>
|
|
8995
|
+
</section>
|
|
8996
|
+
}
|
|
8997
|
+
@if (!agent.docked()) {
|
|
8998
|
+
<div class="spine-agent-fab" [class.spine-agent-fab--dragging]="dragging()" (pointerdown)="startDrag($event)">
|
|
8999
|
+
@if (greetingVisible()) {
|
|
9000
|
+
<section
|
|
9001
|
+
class="spine-agent-greeting"
|
|
9002
|
+
[class.spine-agent-greeting--after]="greetingAfter()"
|
|
9003
|
+
[class.spine-agent-greeting--below]="below()"
|
|
9004
|
+
[class.spine-agent-greeting--figure]="agent.image()"
|
|
9005
|
+
[attr.aria-label]="agent.title"
|
|
9006
|
+
(pointerdown)="$event.stopPropagation()"
|
|
9007
|
+
(mouseenter)="keepGreeting()"
|
|
9008
|
+
(focusin)="keepGreeting()"
|
|
9009
|
+
>
|
|
9010
|
+
@if (agent.image(); as figure) {
|
|
9011
|
+
<img class="spine-agent-greeting__figure" [src]="figure" alt="" (error)="agent.imageFailed(figure)" />
|
|
9012
|
+
}
|
|
9013
|
+
<div class="spine-agent-greeting__content">
|
|
9014
|
+
<div class="spine-agent-greeting__head">
|
|
9015
|
+
<p class="spine-agent-greeting__title">{{ agent.greetingTitle }}</p>
|
|
9016
|
+
<hm-icon-button emphasis="subtle" size="small" icon="times" [label]="text.greetingClose" (clicked)="closeGreeting()" />
|
|
9017
|
+
</div>
|
|
9018
|
+
<p class="spine-agent-greeting__text">{{ agent.greetingText }}</p>
|
|
9019
|
+
@if (greetingSuggestions().length) {
|
|
9020
|
+
<hm-prompt-suggestions [suggestions]="greetingSuggestions()" (chosen)="askFromGreeting($event)" />
|
|
9021
|
+
}
|
|
9022
|
+
</div>
|
|
9023
|
+
</section>
|
|
9024
|
+
}
|
|
9025
|
+
<hm-fab
|
|
9026
|
+
[fixed]="false"
|
|
9027
|
+
[label]="agent.panelOpen() ? text.close : agent.title"
|
|
9028
|
+
[icon]="agent.panelOpen() ? 'times' : fabImage() ? '' : 'chat-bubble'"
|
|
9029
|
+
[expanded]="agent.panelOpen()"
|
|
9030
|
+
[controls]="panelId"
|
|
9031
|
+
avoid=""
|
|
9032
|
+
(triggered)="toggle()"
|
|
9033
|
+
/>
|
|
9034
|
+
@if (!agent.panelOpen() && fabImage(); as image) {
|
|
9035
|
+
<img class="spine-agent-fab__image" [src]="image" alt="" (error)="agent.imageFailed(image)" />
|
|
9036
|
+
}
|
|
9037
|
+
@if (!agent.panelOpen()) {
|
|
9038
|
+
<hm-icon-button
|
|
9039
|
+
class="spine-agent-fab__dock"
|
|
9040
|
+
emphasis="outlined"
|
|
9041
|
+
size="small"
|
|
9042
|
+
icon="times"
|
|
9043
|
+
[label]="text.dock"
|
|
9044
|
+
(pointerdown)="$event.stopPropagation()"
|
|
9045
|
+
(clicked)="dock()"
|
|
9046
|
+
/>
|
|
9047
|
+
}
|
|
9048
|
+
</div>
|
|
9049
|
+
}
|
|
9050
|
+
}
|
|
9051
|
+
`, styles: [":host{--spine-agent-fab-size: calc(var(--hm-control-height-large) * 1.75);--spine-agent-end-now: var(--spine-agent-end, var(--hm-space-lg));--spine-agent-bottom-now: var(--spine-agent-bottom, var(--hm-space-lg));--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 11), calc(100vw - 2 * var(--hm-space-md)));--spine-agent-offset: calc(var(--spine-agent-bottom-now) + var(--spine-agent-fab-size) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-header-height) - var(--hm-space-xs))}:host(.spine-agent--below){--spine-agent-offset: calc(100dvh - var(--spine-agent-bottom-now) + var(--hm-space-sm));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}:host(.spine-agent--docked){--spine-agent-offset: calc(var(--hm-header-height) + var(--hm-space-xs));--spine-agent-room: calc(100dvh - var(--spine-agent-offset) - var(--hm-space-md))}.spine-agent-fab{position:fixed;inset-inline-end:min(var(--spine-agent-end-now),calc(100vw - var(--spine-agent-fab-size) - var(--hm-space-xs)));inset-block-end:calc(min(var(--spine-agent-bottom-now),calc(100dvh - var(--spine-agent-fab-size) - var(--hm-space-xs))) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);inline-size:var(--spine-agent-fab-size);block-size:var(--spine-agent-fab-size);cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none}.spine-agent-fab--dragging{cursor:grabbing}.spine-agent-fab__image{position:absolute;inset:0;inline-size:100%;block-size:100%;object-fit:cover;border-radius:var(--hm-radius-pill);pointer-events:none}.spine-agent-greeting{position:absolute;inset-block-end:0;inset-inline-end:calc(100% + var(--hm-space-sm));display:flex;align-items:flex-end;gap:var(--hm-space-sm);box-sizing:border-box;inline-size:min(calc(var(--hm-space-3xl) * 7),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)));overflow:hidden;padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay);cursor:auto;touch-action:auto;-webkit-user-select:text;user-select:text;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}.spine-agent-greeting--figure{inline-size:min(calc(var(--hm-space-3xl) * 11),calc(100vw - var(--spine-agent-fab-size) - 2 * var(--hm-space-lg)))}.spine-agent-greeting__figure{flex:none;inline-size:calc(var(--hm-space-3xl) * 3.5);margin-block-end:calc(var(--hm-space-sm) * -1);object-fit:contain;object-position:bottom}.spine-agent-greeting__content{display:flex;flex:1;flex-direction:column;align-self:stretch;gap:var(--hm-space-xs);min-inline-size:0}.spine-agent-greeting--after{inset-inline:calc(100% + var(--hm-space-sm)) auto}.spine-agent-greeting--below{inset-block:0 auto}.spine-agent-greeting__head{display:flex;align-items:center;gap:var(--hm-space-xs)}.spine-agent-greeting__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-greeting__text{margin:0;color:var(--hm-muted);font-size:var(--hm-font-size-sm)}.spine-agent-fab--dragging hm-fab{pointer-events:none}.spine-agent-fab__dock{--hm-radius-control: var(--hm-radius-pill);position:absolute;inset-block-start:calc(var(--hm-space-xs) * -1);inset-inline-end:calc(var(--hm-space-xs) * -1);background:var(--hm-surface-raised);border-radius:var(--hm-radius-pill);box-shadow:var(--hm-shadow-overlay);opacity:0;transition:opacity var(--hm-duration-quick) var(--hm-ease)}.spine-agent-fab:hover .spine-agent-fab__dock,.spine-agent-fab:focus-within .spine-agent-fab__dock{opacity:1}@media(hover:none){.spine-agent-fab__dock{opacity:1}}.spine-agent-panel{position:fixed;inset-inline-end:clamp(var(--hm-space-md),var(--spine-agent-end-now),calc(100vw - var(--spine-agent-panel-inline) - var(--hm-space-md)));inset-block-end:calc(var(--spine-agent-offset) + env(safe-area-inset-bottom,0px));z-index:var(--hm-z-fixed);display:flex;flex-direction:column;inline-size:var(--spine-agent-panel-inline);block-size:min(calc(var(--hm-space-3xl) * 16),var(--spine-agent-room));overflow:hidden;background:var(--hm-surface-raised);color:var(--hm-text);border:var(--hm-border-hairline) solid var(--hm-divider);border-radius:calc(var(--hm-radius) * 2);box-shadow:var(--hm-shadow-overlay)}.spine-agent-panel[hidden]{display:none}:host(.spine-agent--below) .spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-block-start:var(--spine-agent-offset);inset-block-end:auto}:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-md)}.spine-agent-panel--expanded{--spine-agent-panel-inline: min(calc(var(--hm-space-3xl) * 24), calc(100vw - 2 * var(--hm-space-md)));block-size:var(--spine-agent-room)}.spine-agent-panel--open{transform-origin:bottom right;animation:spine-agent-panel-in var(--hm-duration-quick) var(--hm-ease-decelerate)}:host(.spine-agent--below) .spine-agent-panel--open,:host(.spine-agent--docked) .spine-agent-panel--open{transform-origin:top right}@keyframes spine-agent-panel-in{0%{opacity:0;transform:translateY(var(--hm-space-sm)) scale(.97)}}@media(prefers-reduced-motion:reduce){.spine-agent-panel--open,.spine-agent-greeting{animation:none}.spine-agent-fab__dock{transition:none}}.spine-agent-panel__header{display:flex;align-items:center;gap:var(--hm-space-xs);padding:var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-sm) var(--hm-space-md);border-block-end:var(--hm-border-hairline) solid var(--hm-divider)}.spine-agent-panel__avatar{flex:none;inline-size:var(--hm-control-height);block-size:var(--hm-control-height);object-fit:cover;border-radius:var(--hm-radius-pill)}.spine-agent-panel__title{flex:1;min-inline-size:0;margin:0;font-size:var(--hm-font-size-sm);font-weight:var(--hm-font-weight-semibold)}.spine-agent-panel__body{flex:1;min-block-size:0;padding:var(--hm-space-md)}@media(max-width:600px){:host{--spine-agent-panel-inline: calc(100vw - 2 * var(--hm-space-xs))}.spine-agent-greeting__figure{display:none}.spine-agent-panel,:host(.spine-agent--docked) .spine-agent-panel{inset-inline-end:var(--hm-space-xs)}}\n"] }]
|
|
9052
|
+
}], ctorParameters: () => [] });
|
|
9053
|
+
|
|
8104
9054
|
/**
|
|
8105
9055
|
* Aviso de "Encerrar por inatividade" (`SpineIdleService`): contagem regressiva, "Continuar conectado" (padrão, e o
|
|
8106
9056
|
* que fechar pelo X/Esc faz) e "Sair agora". Montado uma vez no `spine-layout`.
|
|
@@ -8185,11 +9135,11 @@ class SpineUnsavedChangesService {
|
|
|
8185
9135
|
canReplaceActive(panelId) {
|
|
8186
9136
|
if (!this.enabled)
|
|
8187
9137
|
return true;
|
|
8188
|
-
const active = findPanelTabs
|
|
9138
|
+
const active = findPanelTabs(this.nav.store.getState().root, panelId)?.activeId;
|
|
8189
9139
|
return active && this.unsaved.hasUnsavedChanges(active) ? this.unsaved.confirmDiscard() : true;
|
|
8190
9140
|
}
|
|
8191
9141
|
guard(store) {
|
|
8192
|
-
const tabsOf = (panelId) => findPanelTabs
|
|
9142
|
+
const tabsOf = (panelId) => findPanelTabs(store.getState().root, panelId)?.tabs.map((t) => t.id) ?? [];
|
|
8193
9143
|
// Sem alteração em nenhuma das abas, fecha na hora (mesmo comportamento de antes); com alteração, pergunta.
|
|
8194
9144
|
const closing = (ids, close) => {
|
|
8195
9145
|
if (!ids.some((id) => this.unsaved.hasUnsavedChanges(id))) {
|
|
@@ -8247,7 +9197,7 @@ class LayoutComponent {
|
|
|
8247
9197
|
this.preferences = inject(SpinePreferencesService);
|
|
8248
9198
|
this.workspace = inject(SpineWorkspaceNavService);
|
|
8249
9199
|
this.settings = inject(SpineSettingsService);
|
|
8250
|
-
this.sidebar = inject(HermesSidebarService
|
|
9200
|
+
this.sidebar = inject(HermesSidebarService);
|
|
8251
9201
|
/** Também liga, uma vez, o aviso de falha do logout (`logoutFailed$` do `@orfeu/angular`). */
|
|
8252
9202
|
this.logoutNotice = inject(SpineLogoutService);
|
|
8253
9203
|
/** Recursos opcionais (`SpineConfig.features`): cada serviço não faz nada com o seu recurso desligado. */
|
|
@@ -8351,6 +9301,8 @@ class LayoutComponent {
|
|
|
8351
9301
|
<!-- Todos os avisos (respostas do servidor, avisos do app, configurações, auto-serviço…). Se o app também montar
|
|
8352
9302
|
uma, só uma desenha (o serviço escolhe a pilha ativa): nunca em dobro. -->
|
|
8353
9303
|
<hm-toast-stack />
|
|
9304
|
+
<!-- Agente de IA do app (SpineFeatures.aiAgent): botão flutuante no canto, que abre a conversa. -->
|
|
9305
|
+
<spine-ai-agent-launcher />
|
|
8354
9306
|
<spine-side-panel />
|
|
8355
9307
|
<spine-modal />
|
|
8356
9308
|
<spine-shortcuts-cheatsheet></spine-shortcuts-cheatsheet>
|
|
@@ -8363,7 +9315,7 @@ class LayoutComponent {
|
|
|
8363
9315
|
}
|
|
8364
9316
|
<!-- Aviso de "Encerrar por inatividade" (SpineFeatures.idleTimeout): só desenha quando abre. -->
|
|
8365
9317
|
<spine-idle-warning />
|
|
8366
|
-
`, isInline: true, styles: [":host{display:block}.shell{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--hm-surface);color:var(--hm-text)}.body{position:relative;display:flex;flex:1;min-height:0;overflow:hidden}.main{display:flex;flex:1;min-width:0;flex-direction:column}.workspace-area{flex:1;min-height:0;display:flex;flex-direction:column;--hm-tab-strip-bg: var(--hm-shell-tab-strip-bg);--hm-tab-active-bg: var(--hm-surface-raised)}hm-workspace{flex:1;min-height:0}.panel-content{display:flex;flex-direction:column;height:100%;min-height:0}\n"], dependencies: [{ kind: "component", type: SpineNewsComponent, selector: "spine-news" }, { kind: "component", type: SpineDocsComponent, selector: "spine-docs" }, { kind: "component", type: HeaderComponent, selector: "spine-header" }, { kind: "component", type: SidebarComponent, selector: "spine-sidebar" }, { kind: "component", type: SpineConnectionBannerComponent, selector: "spine-connection-banner" }, { kind: "component", type: SpinePagePanelComponent, selector: "spine-page-panel", inputs: ["url", "isFocusedPanel", "tabId", "panelId"] }, { kind: "directive", type: PanelLocalLinkDirective, selector: "[appPanelLocalLink]", inputs: ["appPanelLocalLink"], outputs: ["localNavigate"] }, { kind: "component", type: WorkspaceComponent, selector: "hm-workspace", inputs: ["initialTabs", "initialActiveId", "keepAlive", "split", "maxColumns", "maxRows", "label", "providedStore"], outputs: ["stateChange", "newTabRequested"] }, { kind: "component", type: ToastStackComponent, selector: "hm-toast-stack", inputs: ["position", "expand", "visibleToasts"] }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: ModalComponent$1, selector: "spine-modal" }, { kind: "component", type: SidePanelComponent, selector: "spine-side-panel" }, { kind: "component", type: SpineInitialSetupComponent, selector: "spine-initial-setup" }, { kind: "component", type: ShortcutsCheatsheetComponent, selector: "spine-shortcuts-cheatsheet" }, { kind: "component", type: SettingsPanelComponent, selector: "spine-settings-panel" }, { kind: "component", type: SpineIdleWarningComponent, selector: "spine-idle-warning" }, { kind: "component", type: SkipLinkComponent, selector: "hm-skip-link", inputs: ["target", "label", "texts"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
9318
|
+
`, isInline: true, styles: [":host{display:block}.shell{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--hm-surface);color:var(--hm-text)}.body{position:relative;display:flex;flex:1;min-height:0;overflow:hidden}.main{display:flex;flex:1;min-width:0;flex-direction:column}.workspace-area{flex:1;min-height:0;display:flex;flex-direction:column;--hm-tab-strip-bg: var(--hm-shell-tab-strip-bg);--hm-tab-active-bg: var(--hm-surface-raised)}hm-workspace{flex:1;min-height:0}.panel-content{display:flex;flex-direction:column;height:100%;min-height:0}\n"], dependencies: [{ kind: "component", type: SpineNewsComponent, selector: "spine-news" }, { kind: "component", type: SpineDocsComponent, selector: "spine-docs" }, { kind: "component", type: HeaderComponent, selector: "spine-header" }, { kind: "component", type: SidebarComponent, selector: "spine-sidebar" }, { kind: "component", type: SpineConnectionBannerComponent, selector: "spine-connection-banner" }, { kind: "component", type: SpinePagePanelComponent, selector: "spine-page-panel", inputs: ["url", "isFocusedPanel", "tabId", "panelId"] }, { kind: "directive", type: PanelLocalLinkDirective, selector: "[appPanelLocalLink]", inputs: ["appPanelLocalLink"], outputs: ["localNavigate"] }, { kind: "component", type: WorkspaceComponent, selector: "hm-workspace", inputs: ["initialTabs", "initialActiveId", "keepAlive", "split", "maxColumns", "maxRows", "label", "providedStore"], outputs: ["stateChange", "newTabRequested"] }, { kind: "component", type: ToastStackComponent, selector: "hm-toast-stack", inputs: ["position", "expand", "visibleToasts"] }, { kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "component", type: ModalComponent$1, selector: "spine-modal" }, { kind: "component", type: SidePanelComponent, selector: "spine-side-panel" }, { kind: "component", type: SpineInitialSetupComponent, selector: "spine-initial-setup" }, { kind: "component", type: ShortcutsCheatsheetComponent, selector: "spine-shortcuts-cheatsheet" }, { kind: "component", type: SettingsPanelComponent, selector: "spine-settings-panel" }, { kind: "component", type: SpineIdleWarningComponent, selector: "spine-idle-warning" }, { kind: "component", type: SkipLinkComponent, selector: "hm-skip-link", inputs: ["target", "label", "texts"] }, { kind: "component", type: SpineAiAgentLauncherComponent, selector: "spine-ai-agent-launcher" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
8367
9319
|
}
|
|
8368
9320
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImport: i0, type: LayoutComponent, decorators: [{
|
|
8369
9321
|
type: Component,
|
|
@@ -8385,6 +9337,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
8385
9337
|
SettingsPanelComponent,
|
|
8386
9338
|
SpineIdleWarningComponent,
|
|
8387
9339
|
SkipLinkComponent,
|
|
9340
|
+
SpineAiAgentLauncherComponent,
|
|
8388
9341
|
], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
8389
9342
|
<!-- "Pular para o conteúdo" (SpineFeatures.skipLink): primeira parada do Tab. -->
|
|
8390
9343
|
@if (skipLink) {
|
|
@@ -8421,6 +9374,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
8421
9374
|
<!-- Todos os avisos (respostas do servidor, avisos do app, configurações, auto-serviço…). Se o app também montar
|
|
8422
9375
|
uma, só uma desenha (o serviço escolhe a pilha ativa): nunca em dobro. -->
|
|
8423
9376
|
<hm-toast-stack />
|
|
9377
|
+
<!-- Agente de IA do app (SpineFeatures.aiAgent): botão flutuante no canto, que abre a conversa. -->
|
|
9378
|
+
<spine-ai-agent-launcher />
|
|
8424
9379
|
<spine-side-panel />
|
|
8425
9380
|
<spine-modal />
|
|
8426
9381
|
<spine-shortcuts-cheatsheet></spine-shortcuts-cheatsheet>
|
|
@@ -8760,5 +9715,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.24", ngImpo
|
|
|
8760
9715
|
* Generated bundle index. Do not edit.
|
|
8761
9716
|
*/
|
|
8762
9717
|
|
|
8763
|
-
export {
|
|
8764
|
-
//# sourceMappingURL=trismegisto-spine-layout-trismegisto-spine-layout-
|
|
9718
|
+
export { SpineAppTokensService as $, AppLauncherComponent as A, SPINE_INBOX_POLL_MS as B, SPINE_INBOX_SOURCES as C, SPINE_MARKDOWN_PURIFY_CONFIG as D, SPINE_PREFERENCES_SAVE_DELAY_MS as E, SPINE_RATING_COMMENT_MAX as F, SPINE_RATING_SCORE_MAX as G, HeaderComponent as H, SPINE_SETTINGS_ALIASES as I, SPINE_SETTINGS_KEYS as J, SPINE_SETTINGS_SECTIONS as K, LayoutComponent as L, McpConnectComponent as M, NEW_SCREEN_VALUE as N, SPINE_SUGGESTIONS_PAGE_ID as O, SPINE_SUGGESTION_LIMITS as P, SPINE_SUGGESTION_PUBLIC_SORTS as Q, SPINE_SUGGESTION_PUBLIC_STATUSES as R, SpineSuggestionsService as S, SPINE_SUGGESTION_STATUS_ICONS as T, SPINE_WORKSPACE_LIMITS as U, SettingsPanelComponent as V, ShortcutService as W, ShortcutsCheatsheetComponent as X, SidebarComponent as Y, SpineAccountPreferencesService as Z, SpineAccountSecurityService as _, SpineInboxService as a, SpineBrandComponent as a0, SpineConnectionBannerComponent as a1, SpineDocsComponent as a2, SpineDocsService as a3, SpineIdleService as a4, SpineIdleWarningComponent as a5, SpineInboxButtonComponent as a6, SpineInitialSetupComponent as a7, SpineInitialSetupService as a8, SpineLogoutService as a9, spineParamsOf as aA, spinePathOf as aB, spineScoreBand as aC, spineStarFills as aD, spineSuggestionsEnabled as aE, spineTagColor as aF, stable as aG, SpineMyGroupsService as aa, SpineNewsComponent as ab, SpineNewsService as ac, SpineNoticesService as ad, SpinePageActionsDirective as ae, SpinePageActionsSlot as af, SpineRatingStarsComponent as ag, SpineRatingsService as ah, SpineSettingsService as ai, SpineStartPageService as aj, SpineUnsavedChangesService as ak, SpineWorkspaceNavService as al, accountErrorCode as am, argosAccountApi as an, buildMcpConfig as ao, effectiveIdleMinutes as ap, idleLimits as aq, isInternalPath as ar, provideSettingsSection as as, provideSpineInboxSource as at, provideSpineSuggestionsPage as au, ratingTargetKey as av, registerSpineUnsavedChanges as aw, resolveSettingsSection as ax, sanitizeSpineMarkdownHtml as ay, spineNormalizeUrl as az, SpineSuggestionChatComponent as b, SPINE_SUGGESTION_STATUS_COLORS as c, SPINE_SUGGESTION_KIND_COLORS as d, SPINE_SUGGESTION_KIND_ICONS as e, SPINE_SUGGESTION_WAITING_COLORS as f, SPINE_SUGGESTION_VISIBLE_SORTS as g, SPINE_SUGGESTION_REASONS as h, SPINE_SUGGESTION_STATUSES as i, SPINE_SUGGESTION_KINDS as j, SPINE_SUGGESTION_WAITING as k, SPINE_TAB as l, SPINE_SUGGESTION_PAGE_PARAM as m, SPINE_SUGGESTION_TAB_PARAM as n, SPINE_SUGGESTION_TAB_CONVERSATION as o, screenName as p, screenModule as q, SPINE_SUGGESTIONS_PAGE_PATH as r, suggestionScreens as s, SpineSuggestionsDialogComponent as t, SpineTagRowComponent as u, SpineAiAgentService as v, SpineMarkdownComponent as w, SPINE_BRAND as x, SPINE_GLOBAL_PREFERENCE_KEYS as y, SPINE_IDLE_WARNING_SECONDS as z };
|
|
9719
|
+
//# sourceMappingURL=trismegisto-spine-layout-trismegisto-spine-layout-B47sMo_4.mjs.map
|