@meshmakers/octo-ai-console 3.3.1190
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.
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, Injectable, InjectionToken, makeEnvironmentProviders, input, computed, ChangeDetectionStrategy, Component, output, signal } from '@angular/core';
|
|
3
|
+
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
4
|
+
import { ReplaySubject, Subject } from 'rxjs';
|
|
5
|
+
import * as i1 from '@angular/forms';
|
|
6
|
+
import { FormsModule } from '@angular/forms';
|
|
7
|
+
import { CommonModule } from '@angular/common';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Typed REST client for the AI Adapter's session endpoints. Every method maps
|
|
11
|
+
* 1:1 to a controller route on the C# side
|
|
12
|
+
* (`src/AiServices/TenantApi/v1/Controllers/SessionsController.cs`).
|
|
13
|
+
*
|
|
14
|
+
* The service does NOT cache responses or hold session state — each call is a
|
|
15
|
+
* pure pass-through to `HttpClient`. Components that need a derived view (e.g.
|
|
16
|
+
* a session list with status counts) should compose over the returned
|
|
17
|
+
* observables themselves; that keeps the client free of UI concerns and lets
|
|
18
|
+
* host apps mix Apollo / GraphQL state in the same component without dueling
|
|
19
|
+
* caches.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Not `providedIn: 'root'` — `AI_ADAPTER_OPTIONS` is provided per-route in host
|
|
23
|
+
* apps (the tenant id is read from the route param, which the root injector
|
|
24
|
+
* doesn't see). A root-provided service would resolve through the root injector
|
|
25
|
+
* and fail with NG0201 on `AI_ADAPTER_OPTIONS`. Standalone components that
|
|
26
|
+
* inject this service pick up the route-level provider via the element
|
|
27
|
+
* injector hierarchy.
|
|
28
|
+
*/
|
|
29
|
+
class AiAdapterClientService {
|
|
30
|
+
http = inject(HttpClient);
|
|
31
|
+
options = inject(AI_ADAPTER_OPTIONS);
|
|
32
|
+
/**
|
|
33
|
+
* `GET /{tenantId}/v1/sessions` — list all sessions for the configured tenant.
|
|
34
|
+
* Optional `status` filter narrows to one lifecycle state when present.
|
|
35
|
+
*/
|
|
36
|
+
listSessions(status) {
|
|
37
|
+
const url = `${this.tenantBase()}/sessions`;
|
|
38
|
+
let params = new HttpParams();
|
|
39
|
+
if (status) {
|
|
40
|
+
params = params.set('status', status);
|
|
41
|
+
}
|
|
42
|
+
return this.http.get(url, { params });
|
|
43
|
+
}
|
|
44
|
+
/** `GET /{tenantId}/v1/sessions/{sessionId}` — single session by rtId. */
|
|
45
|
+
getSession(sessionId) {
|
|
46
|
+
return this.http.get(`${this.tenantBase()}/sessions/${encodeURIComponent(sessionId)}`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `POST /{tenantId}/v1/sessions` — start a new session. Returns the persisted
|
|
50
|
+
* session plus the quota gate's decision so the caller can render either a
|
|
51
|
+
* "Running" pill or a "Queued (n)" pill without a second round-trip.
|
|
52
|
+
*/
|
|
53
|
+
createSession(request) {
|
|
54
|
+
return this.http.post(`${this.tenantBase()}/sessions`, request);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `POST /{tenantId}/v1/sessions/{sessionId}/cancel` — request graceful
|
|
58
|
+
* cancellation; the orchestrator transitions the session to `Cancelled`
|
|
59
|
+
* once the worker has acknowledged.
|
|
60
|
+
*/
|
|
61
|
+
cancelSession(sessionId) {
|
|
62
|
+
return this.http.post(`${this.tenantBase()}/sessions/${encodeURIComponent(sessionId)}/cancel`, null);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `GET /{tenantId}/v1/sessions/{sessionId}/events?sinceSequence=N` — replay
|
|
66
|
+
* persisted events for a session. UI uses this on reconnect to backfill any
|
|
67
|
+
* events SignalR missed while disconnected (drives the resume-from-sequence
|
|
68
|
+
* path in {@link AiSessionStreamService}).
|
|
69
|
+
*/
|
|
70
|
+
listEvents(sessionId, sinceSequence) {
|
|
71
|
+
const url = `${this.tenantBase()}/sessions/${encodeURIComponent(sessionId)}/events`;
|
|
72
|
+
let params = new HttpParams();
|
|
73
|
+
if (sinceSequence != null) {
|
|
74
|
+
params = params.set('sinceSequence', sinceSequence.toString());
|
|
75
|
+
}
|
|
76
|
+
return this.http.get(url, { params });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* `POST /{tenantId}/v1/sessions/{sessionId}/approvals/{requestId}` — submit
|
|
80
|
+
* the user's decision on a paused approval gate. Server fans
|
|
81
|
+
* `OnApprovalDecidedAsync` to every connection on the session.
|
|
82
|
+
*/
|
|
83
|
+
decideApproval(sessionId, requestId, decision) {
|
|
84
|
+
return this.http.post(`${this.tenantBase()}/sessions/${encodeURIComponent(sessionId)}` +
|
|
85
|
+
`/approvals/${encodeURIComponent(requestId)}`, decision);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `POST /{tenantId}/v1/credentials/tickets` — mint a one-time credential-
|
|
89
|
+
* registration ticket (#4133). The admin sees the plaintext code once in
|
|
90
|
+
* the response; the server stores only the SHA-256 hash. The matching
|
|
91
|
+
* redemption call (`POST /v1/credentials/tickets/redeem`) is anonymous and
|
|
92
|
+
* NOT in this client because the redeemer is by design a different person
|
|
93
|
+
* on a different machine — they use the bastion CLI, not the Studio.
|
|
94
|
+
*/
|
|
95
|
+
issueCredentialTicket(request) {
|
|
96
|
+
return this.http.post(`${this.tenantBase()}/credentials/tickets`, request);
|
|
97
|
+
}
|
|
98
|
+
tenantBase() {
|
|
99
|
+
return `${this.options.baseUrl}/${encodeURIComponent(this.options.tenantId)}/v1`;
|
|
100
|
+
}
|
|
101
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiAdapterClientService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
102
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiAdapterClientService });
|
|
103
|
+
}
|
|
104
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiAdapterClientService, decorators: [{
|
|
105
|
+
type: Injectable
|
|
106
|
+
}] });
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Internal wrapper around the SignalR connection. The library does not pin a
|
|
110
|
+
* SignalR version — host applications inject a `HubConnectionBuilder` they
|
|
111
|
+
* already use, which keeps the library agnostic of the
|
|
112
|
+
* `@microsoft/signalr` major version line (six and seven have diverged on the
|
|
113
|
+
* `withAutomaticReconnect()` defaults).
|
|
114
|
+
*
|
|
115
|
+
* For now the connection is constructed deferred — `streamSession` builds it on
|
|
116
|
+
* demand so the host can defer the SignalR JS bundle behind a route lazy-load.
|
|
117
|
+
* Phase-1 ships a stub backed by `ReplaySubject` so demo apps can drive
|
|
118
|
+
* components without a live hub; the real connection wiring lives behind a
|
|
119
|
+
* `connectionFactory` provider the host overrides.
|
|
120
|
+
*/
|
|
121
|
+
/**
|
|
122
|
+
* Not `providedIn: 'root'` — see <see cref="AiAdapterClientService"/> for the
|
|
123
|
+
* rationale: `AI_ADAPTER_OPTIONS` is a per-route token, the root injector
|
|
124
|
+
* cannot resolve it. Element-level injection picks up the route-level provider.
|
|
125
|
+
*/
|
|
126
|
+
class AiSessionStreamService {
|
|
127
|
+
options = inject(AI_ADAPTER_OPTIONS);
|
|
128
|
+
client = inject(AiAdapterClientService);
|
|
129
|
+
connectionFactory = inject(AI_SESSION_STREAM_CONNECTION_FACTORY, { optional: true });
|
|
130
|
+
/**
|
|
131
|
+
* Open a streaming session. Returns the per-stream subjects and a
|
|
132
|
+
* `disconnect()` closer. When the host has provided an
|
|
133
|
+
* {@link AI_SESSION_STREAM_CONNECTION_FACTORY}, a live SignalR connection is
|
|
134
|
+
* built against {@link hubUrl} and the five hub callbacks
|
|
135
|
+
* (`OnSessionEventAsync`, `OnSessionStatusChangedAsync`,
|
|
136
|
+
* `OnApprovalRequestedAsync`, `OnApprovalDecidedAsync`, `OnQuotaWarningAsync`)
|
|
137
|
+
* are routed into the matching subject. Without a factory, the service
|
|
138
|
+
* degrades to REST-only backfill — the original Phase-1 stub contract.
|
|
139
|
+
*
|
|
140
|
+
* Tracking the highest sequence we've emitted lets the host reconnect after a
|
|
141
|
+
* network blip by passing `sinceSequence` to {@link replay} (the SignalR
|
|
142
|
+
* `onreconnected` hook fires that with the highest seen sequence).
|
|
143
|
+
*/
|
|
144
|
+
streamSession(sessionId) {
|
|
145
|
+
const events$ = new ReplaySubject(256);
|
|
146
|
+
const statusChanges$ = new Subject();
|
|
147
|
+
const approvalsRequested$ = new Subject();
|
|
148
|
+
const approvalsDecided$ = new Subject();
|
|
149
|
+
const quotaWarnings$ = new Subject();
|
|
150
|
+
// Backfill any persisted events the hub-connect process would have missed
|
|
151
|
+
// — REST returns the full history up to "now", and the live channel takes
|
|
152
|
+
// it from there. The combination is at-least-once with deterministic
|
|
153
|
+
// ordering courtesy of the persisted `sequence` field.
|
|
154
|
+
let highestSequence = 0;
|
|
155
|
+
const backfill = this.client.listEvents(sessionId).subscribe({
|
|
156
|
+
next: (events) => {
|
|
157
|
+
for (const event of events) {
|
|
158
|
+
highestSequence = Math.max(highestSequence, event.sequence);
|
|
159
|
+
events$.next(event);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
let connection = null;
|
|
164
|
+
if (this.connectionFactory) {
|
|
165
|
+
connection = this.connectionFactory(this.hubUrl);
|
|
166
|
+
}
|
|
167
|
+
if (connection) {
|
|
168
|
+
const conn = connection;
|
|
169
|
+
conn.on('OnSessionEventAsync', (payload) => {
|
|
170
|
+
const event = payload;
|
|
171
|
+
highestSequence = Math.max(highestSequence, event.sequence ?? 0);
|
|
172
|
+
events$.next(event);
|
|
173
|
+
});
|
|
174
|
+
conn.on('OnSessionStatusChangedAsync', (payload) => {
|
|
175
|
+
statusChanges$.next(payload);
|
|
176
|
+
});
|
|
177
|
+
conn.on('OnApprovalRequestedAsync', (payload) => {
|
|
178
|
+
approvalsRequested$.next(payload);
|
|
179
|
+
});
|
|
180
|
+
conn.on('OnApprovalDecidedAsync', (payload) => {
|
|
181
|
+
approvalsDecided$.next(payload);
|
|
182
|
+
});
|
|
183
|
+
conn.on('OnQuotaWarningAsync', (payload) => {
|
|
184
|
+
quotaWarnings$.next(payload);
|
|
185
|
+
});
|
|
186
|
+
conn.onreconnected(() => {
|
|
187
|
+
// Pull anything we missed during the disconnect window so the
|
|
188
|
+
// events$ stream stays gap-free without the caller having to do
|
|
189
|
+
// their own bookkeeping.
|
|
190
|
+
this.replay(sessionId, highestSequence, events$);
|
|
191
|
+
});
|
|
192
|
+
// Start the connection, then ask the hub to scope this connection's
|
|
193
|
+
// group membership to the session. Failures are intentionally swallowed
|
|
194
|
+
// — backfill stays valid and the UI degrades to REST-only instead of
|
|
195
|
+
// crashing.
|
|
196
|
+
conn
|
|
197
|
+
.start()
|
|
198
|
+
.then(() => conn.invoke('SubscribeToSessionAsync', sessionId))
|
|
199
|
+
.catch(() => {
|
|
200
|
+
/* hub start / subscribe failed — REST backfill still serves the UI */
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
events$: events$.asObservable(),
|
|
205
|
+
statusChanges$: statusChanges$.asObservable(),
|
|
206
|
+
approvalsRequested$: approvalsRequested$.asObservable(),
|
|
207
|
+
approvalsDecided$: approvalsDecided$.asObservable(),
|
|
208
|
+
quotaWarnings$: quotaWarnings$.asObservable(),
|
|
209
|
+
disconnect: () => {
|
|
210
|
+
backfill.unsubscribe();
|
|
211
|
+
if (connection) {
|
|
212
|
+
// Best-effort — `stop()` may reject during teardown; the subjects
|
|
213
|
+
// get completed regardless so consumers see clean termination.
|
|
214
|
+
connection.stop().catch(() => undefined);
|
|
215
|
+
connection = null;
|
|
216
|
+
}
|
|
217
|
+
events$.complete();
|
|
218
|
+
statusChanges$.complete();
|
|
219
|
+
approvalsRequested$.complete();
|
|
220
|
+
approvalsDecided$.complete();
|
|
221
|
+
quotaWarnings$.complete();
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Backfill missing events from a known sequence number and push them into
|
|
227
|
+
* the host's existing stream. Used by a SignalR `onreconnected` hook the
|
|
228
|
+
* host wires up.
|
|
229
|
+
*/
|
|
230
|
+
replay(sessionId, sinceSequence, sink) {
|
|
231
|
+
this.client.listEvents(sessionId, sinceSequence).subscribe({
|
|
232
|
+
next: (events) => events.forEach((event) => sink.next(event)),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Tenant-scoped hub URL. Composition matches the AI Adapter's hub map —
|
|
237
|
+
* `app.MapHub<AiHub>("/{tenantId:tenantId}/aiHub")` — so the host's
|
|
238
|
+
* `hubPath` is appended after the tenant segment. Host apps that mount the
|
|
239
|
+
* hub at a different relative path override `hubPath` via
|
|
240
|
+
* `AI_ADAPTER_OPTIONS`.
|
|
241
|
+
*/
|
|
242
|
+
get hubUrl() {
|
|
243
|
+
const trimmed = this.options.hubPath.startsWith('/')
|
|
244
|
+
? this.options.hubPath
|
|
245
|
+
: `/${this.options.hubPath}`;
|
|
246
|
+
return `${this.options.baseUrl}/${this.options.tenantId}${trimmed}`;
|
|
247
|
+
}
|
|
248
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiSessionStreamService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
249
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiSessionStreamService });
|
|
250
|
+
}
|
|
251
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiSessionStreamService, decorators: [{
|
|
252
|
+
type: Injectable
|
|
253
|
+
}] });
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* DI token the services in this library read at construction time.
|
|
257
|
+
* Provided by {@link provideOctoAiConsole}.
|
|
258
|
+
*/
|
|
259
|
+
const AI_ADAPTER_OPTIONS = new InjectionToken('mm-octo-ai-console.adapter-options');
|
|
260
|
+
/**
|
|
261
|
+
* Optional DI token a host application provides to enable the live SignalR
|
|
262
|
+
* channel on {@link AiSessionStreamService}. When absent, the service degrades
|
|
263
|
+
* to REST-only backfill.
|
|
264
|
+
*/
|
|
265
|
+
const AI_SESSION_STREAM_CONNECTION_FACTORY = new InjectionToken('mm-octo-ai-console.session-stream-connection-factory');
|
|
266
|
+
/**
|
|
267
|
+
* Composable provider that surfaces the library's adapter configuration to the
|
|
268
|
+
* Angular DI container. Host applications call this once in `app.config.ts`:
|
|
269
|
+
*
|
|
270
|
+
* ```ts
|
|
271
|
+
* providers: [
|
|
272
|
+
* provideOctoAiConsole({
|
|
273
|
+
* baseUrl: 'https://ai.acme.cloud',
|
|
274
|
+
* tenantId: 'acme',
|
|
275
|
+
* hubPath: '/hubs/ai',
|
|
276
|
+
* }),
|
|
277
|
+
* ]
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
function provideOctoAiConsole(options) {
|
|
281
|
+
// Provide AiAdapterClientService + AiSessionStreamService alongside the
|
|
282
|
+
// options token. Both services drop `providedIn: 'root'` (6d0e7daf) because
|
|
283
|
+
// the options they read are route-scoped — a host with multiple tenants
|
|
284
|
+
// composes the provider with the right tenant id per route, and root-
|
|
285
|
+
// singleton services would freeze the first route's options. Bundling the
|
|
286
|
+
// services into the same provider chain means a host's `providers: [
|
|
287
|
+
// provideOctoAiConsole({...}) ]` is enough; no manual listing required.
|
|
288
|
+
return makeEnvironmentProviders([
|
|
289
|
+
{ provide: AI_ADAPTER_OPTIONS, useValue: options },
|
|
290
|
+
AiAdapterClientService,
|
|
291
|
+
AiSessionStreamService,
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Status pill that mirrors the eight values `AiSessionStatus` carries on the
|
|
297
|
+
* wire. The component is purely visual — it takes a status string in and emits
|
|
298
|
+
* a CSS class + display label that the host's stylesheet can theme via the
|
|
299
|
+
* `--mm-ai-status-*` custom properties.
|
|
300
|
+
*/
|
|
301
|
+
class AiJobStatusBadgeComponent {
|
|
302
|
+
status = input.required(...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
|
|
303
|
+
variant = computed(() => {
|
|
304
|
+
switch (this.status()) {
|
|
305
|
+
case 'Running':
|
|
306
|
+
return 'running';
|
|
307
|
+
case 'Queued':
|
|
308
|
+
return 'queued';
|
|
309
|
+
case 'Paused':
|
|
310
|
+
return 'paused';
|
|
311
|
+
case 'Completed':
|
|
312
|
+
return 'completed';
|
|
313
|
+
case 'Failed':
|
|
314
|
+
case 'Cancelled':
|
|
315
|
+
return 'failed';
|
|
316
|
+
case 'QuotaBlocked':
|
|
317
|
+
case 'RateLimited':
|
|
318
|
+
return 'blocked';
|
|
319
|
+
default:
|
|
320
|
+
return 'queued';
|
|
321
|
+
}
|
|
322
|
+
}, ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
323
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiJobStatusBadgeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
324
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: AiJobStatusBadgeComponent, isStandalone: true, selector: "mm-ai-job-status-badge", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<span class=\"mm-ai-status-badge mm-ai-status-badge--{{ variant() }}\">\n {{ status() }}\n</span>\n", styles: [":host{display:inline-block;--mm-ai-status-running-bg: #1f6feb;--mm-ai-status-running-fg: #ffffff;--mm-ai-status-queued-bg: #6e7681;--mm-ai-status-queued-fg: #ffffff;--mm-ai-status-paused-bg: #d29922;--mm-ai-status-paused-fg: #1c1c1c;--mm-ai-status-completed-bg: #2ea44f;--mm-ai-status-completed-fg: #ffffff;--mm-ai-status-failed-bg: #d73a49;--mm-ai-status-failed-fg: #ffffff;--mm-ai-status-blocked-bg: #8957e5;--mm-ai-status-blocked-fg: #ffffff}.mm-ai-status-badge{display:inline-block;padding:.15rem .65rem;border-radius:9999px;font-size:.8rem;font-weight:600;line-height:1.2;letter-spacing:.01em}.mm-ai-status-badge--running{background:var(--mm-ai-status-running-bg);color:var(--mm-ai-status-running-fg)}.mm-ai-status-badge--queued{background:var(--mm-ai-status-queued-bg);color:var(--mm-ai-status-queued-fg)}.mm-ai-status-badge--paused{background:var(--mm-ai-status-paused-bg);color:var(--mm-ai-status-paused-fg)}.mm-ai-status-badge--completed{background:var(--mm-ai-status-completed-bg);color:var(--mm-ai-status-completed-fg)}.mm-ai-status-badge--failed{background:var(--mm-ai-status-failed-bg);color:var(--mm-ai-status-failed-fg)}.mm-ai-status-badge--blocked{background:var(--mm-ai-status-blocked-bg);color:var(--mm-ai-status-blocked-fg)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
325
|
+
}
|
|
326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiJobStatusBadgeComponent, decorators: [{
|
|
327
|
+
type: Component,
|
|
328
|
+
args: [{ selector: 'mm-ai-job-status-badge', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<span class=\"mm-ai-status-badge mm-ai-status-badge--{{ variant() }}\">\n {{ status() }}\n</span>\n", styles: [":host{display:inline-block;--mm-ai-status-running-bg: #1f6feb;--mm-ai-status-running-fg: #ffffff;--mm-ai-status-queued-bg: #6e7681;--mm-ai-status-queued-fg: #ffffff;--mm-ai-status-paused-bg: #d29922;--mm-ai-status-paused-fg: #1c1c1c;--mm-ai-status-completed-bg: #2ea44f;--mm-ai-status-completed-fg: #ffffff;--mm-ai-status-failed-bg: #d73a49;--mm-ai-status-failed-fg: #ffffff;--mm-ai-status-blocked-bg: #8957e5;--mm-ai-status-blocked-fg: #ffffff}.mm-ai-status-badge{display:inline-block;padding:.15rem .65rem;border-radius:9999px;font-size:.8rem;font-weight:600;line-height:1.2;letter-spacing:.01em}.mm-ai-status-badge--running{background:var(--mm-ai-status-running-bg);color:var(--mm-ai-status-running-fg)}.mm-ai-status-badge--queued{background:var(--mm-ai-status-queued-bg);color:var(--mm-ai-status-queued-fg)}.mm-ai-status-badge--paused{background:var(--mm-ai-status-paused-bg);color:var(--mm-ai-status-paused-fg)}.mm-ai-status-badge--completed{background:var(--mm-ai-status-completed-bg);color:var(--mm-ai-status-completed-fg)}.mm-ai-status-badge--failed{background:var(--mm-ai-status-failed-bg);color:var(--mm-ai-status-failed-fg)}.mm-ai-status-badge--blocked{background:var(--mm-ai-status-blocked-bg);color:var(--mm-ai-status-blocked-fg)}\n"] }]
|
|
329
|
+
}], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: true }] }] } });
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* List view of an AI Adapter tenant's sessions. The host owns data fetching —
|
|
333
|
+
* the component takes the array as an input signal and emits the rtId on
|
|
334
|
+
* row click. Keeping it data-source-agnostic lets refinery wire its own
|
|
335
|
+
* Apollo cache while a bastion CLI could pass static fixtures in a Storybook.
|
|
336
|
+
*/
|
|
337
|
+
class AiSessionListComponent {
|
|
338
|
+
sessions = input.required(...(ngDevMode ? [{ debugName: "sessions" }] : /* istanbul ignore next */ []));
|
|
339
|
+
selectedSessionId = input(null, ...(ngDevMode ? [{ debugName: "selectedSessionId" }] : /* istanbul ignore next */ []));
|
|
340
|
+
sessionSelected = output();
|
|
341
|
+
orderedSessions = computed(() => [...this.sessions()].sort((a, b) => b.startedAt.localeCompare(a.startedAt)), ...(ngDevMode ? [{ debugName: "orderedSessions" }] : /* istanbul ignore next */ []));
|
|
342
|
+
onSelect(sessionRtId) {
|
|
343
|
+
this.sessionSelected.emit(sessionRtId);
|
|
344
|
+
}
|
|
345
|
+
age(startedAt) {
|
|
346
|
+
const started = Date.parse(startedAt);
|
|
347
|
+
if (Number.isNaN(started)) {
|
|
348
|
+
return '';
|
|
349
|
+
}
|
|
350
|
+
const now = Date.now();
|
|
351
|
+
const seconds = Math.floor((now - started) / 1000);
|
|
352
|
+
if (seconds < 60) {
|
|
353
|
+
return `${seconds}s ago`;
|
|
354
|
+
}
|
|
355
|
+
const minutes = Math.floor(seconds / 60);
|
|
356
|
+
if (minutes < 60) {
|
|
357
|
+
return `${minutes}m ago`;
|
|
358
|
+
}
|
|
359
|
+
const hours = Math.floor(minutes / 60);
|
|
360
|
+
if (hours < 24) {
|
|
361
|
+
return `${hours}h ago`;
|
|
362
|
+
}
|
|
363
|
+
const days = Math.floor(hours / 24);
|
|
364
|
+
return `${days}d ago`;
|
|
365
|
+
}
|
|
366
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiSessionListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
367
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiSessionListComponent, isStandalone: true, selector: "mm-ai-session-list", inputs: { sessions: { classPropertyName: "sessions", publicName: "sessions", isSignal: true, isRequired: true, transformFunction: null }, selectedSessionId: { classPropertyName: "selectedSessionId", publicName: "selectedSessionId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sessionSelected: "sessionSelected" }, ngImport: i0, template: "<ul class=\"mm-ai-session-list\">\n @for (session of orderedSessions(); track session.sessionRtId) {\n <li\n class=\"mm-ai-session-list__row\"\n [class.mm-ai-session-list__row--selected]=\"\n session.sessionRtId === selectedSessionId()\n \"\n >\n <button\n type=\"button\"\n class=\"mm-ai-session-list__button\"\n (click)=\"onSelect(session.sessionRtId)\"\n >\n <div class=\"mm-ai-session-list__main\">\n <div class=\"mm-ai-session-list__goal\">{{ session.goalSummary }}</div>\n <div class=\"mm-ai-session-list__meta\">\n <span class=\"mm-ai-session-list__owner\">{{ session.ownerUserId }}</span>\n <span class=\"mm-ai-session-list__age\">{{ age(session.startedAt) }}</span>\n <span class=\"mm-ai-session-list__tokens\">\n {{ session.tokensConsumed }} tok\n </span>\n @if (session.previewUrl) {\n <!--\n #4136 \u2014 preview-deploy URL the server surfaces when the worker\n pod's nginx sidecar has a build to serve. Opens in a new tab so\n the operator's session-list scroll position survives.\n stopPropagation prevents the row click from also firing.\n -->\n <a\n class=\"mm-ai-session-list__preview\"\n [href]=\"session.previewUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n (click)=\"$event.stopPropagation()\"\n >\n Open preview\n </a>\n }\n </div>\n </div>\n <mm-ai-job-status-badge [status]=\"session.status\" />\n </button>\n </li>\n } @empty {\n <li class=\"mm-ai-session-list__empty\">No sessions yet.</li>\n }\n</ul>\n", styles: [":host{display:block;--mm-ai-list-bg: #16191d;--mm-ai-list-row-bg: #1f2227;--mm-ai-list-row-selected: #2a3142;--mm-ai-list-border: #2f343b;--mm-ai-list-text: #e6e6e6;--mm-ai-list-muted: #9aa0a6;--mm-ai-list-accent: #64ceb9}.mm-ai-session-list{list-style:none;margin:0;padding:0;background:var(--mm-ai-list-bg);border-radius:.5rem;display:flex;flex-direction:column;gap:.25rem;padding:.25rem}.mm-ai-session-list__row{background:var(--mm-ai-list-row-bg);border:1px solid var(--mm-ai-list-border);border-radius:.4rem}.mm-ai-session-list__row--selected{background:var(--mm-ai-list-row-selected)}.mm-ai-session-list__button{width:100%;display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:transparent;color:var(--mm-ai-list-text);border:none;cursor:pointer;text-align:left;font-family:inherit}.mm-ai-session-list__button:hover{background:#ffffff08}.mm-ai-session-list__main{flex:1;min-width:0}.mm-ai-session-list__goal{font-size:.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mm-ai-session-list__meta{display:flex;gap:.75rem;margin-top:.15rem;font-size:.75rem;color:var(--mm-ai-list-muted);font-variant-numeric:tabular-nums}.mm-ai-session-list__preview{color:var(--mm-ai-list-accent);font-size:.75rem;text-decoration:underline;text-underline-offset:2px}.mm-ai-session-list__preview:hover{text-decoration-thickness:2px}.mm-ai-session-list__empty{padding:1rem;text-align:center;color:var(--mm-ai-list-muted);font-style:italic}\n"], dependencies: [{ kind: "component", type: AiJobStatusBadgeComponent, selector: "mm-ai-job-status-badge", inputs: ["status"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
368
|
+
}
|
|
369
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiSessionListComponent, decorators: [{
|
|
370
|
+
type: Component,
|
|
371
|
+
args: [{ selector: 'mm-ai-session-list', imports: [AiJobStatusBadgeComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ul class=\"mm-ai-session-list\">\n @for (session of orderedSessions(); track session.sessionRtId) {\n <li\n class=\"mm-ai-session-list__row\"\n [class.mm-ai-session-list__row--selected]=\"\n session.sessionRtId === selectedSessionId()\n \"\n >\n <button\n type=\"button\"\n class=\"mm-ai-session-list__button\"\n (click)=\"onSelect(session.sessionRtId)\"\n >\n <div class=\"mm-ai-session-list__main\">\n <div class=\"mm-ai-session-list__goal\">{{ session.goalSummary }}</div>\n <div class=\"mm-ai-session-list__meta\">\n <span class=\"mm-ai-session-list__owner\">{{ session.ownerUserId }}</span>\n <span class=\"mm-ai-session-list__age\">{{ age(session.startedAt) }}</span>\n <span class=\"mm-ai-session-list__tokens\">\n {{ session.tokensConsumed }} tok\n </span>\n @if (session.previewUrl) {\n <!--\n #4136 \u2014 preview-deploy URL the server surfaces when the worker\n pod's nginx sidecar has a build to serve. Opens in a new tab so\n the operator's session-list scroll position survives.\n stopPropagation prevents the row click from also firing.\n -->\n <a\n class=\"mm-ai-session-list__preview\"\n [href]=\"session.previewUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n (click)=\"$event.stopPropagation()\"\n >\n Open preview\n </a>\n }\n </div>\n </div>\n <mm-ai-job-status-badge [status]=\"session.status\" />\n </button>\n </li>\n } @empty {\n <li class=\"mm-ai-session-list__empty\">No sessions yet.</li>\n }\n</ul>\n", styles: [":host{display:block;--mm-ai-list-bg: #16191d;--mm-ai-list-row-bg: #1f2227;--mm-ai-list-row-selected: #2a3142;--mm-ai-list-border: #2f343b;--mm-ai-list-text: #e6e6e6;--mm-ai-list-muted: #9aa0a6;--mm-ai-list-accent: #64ceb9}.mm-ai-session-list{list-style:none;margin:0;padding:0;background:var(--mm-ai-list-bg);border-radius:.5rem;display:flex;flex-direction:column;gap:.25rem;padding:.25rem}.mm-ai-session-list__row{background:var(--mm-ai-list-row-bg);border:1px solid var(--mm-ai-list-border);border-radius:.4rem}.mm-ai-session-list__row--selected{background:var(--mm-ai-list-row-selected)}.mm-ai-session-list__button{width:100%;display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;background:transparent;color:var(--mm-ai-list-text);border:none;cursor:pointer;text-align:left;font-family:inherit}.mm-ai-session-list__button:hover{background:#ffffff08}.mm-ai-session-list__main{flex:1;min-width:0}.mm-ai-session-list__goal{font-size:.9rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mm-ai-session-list__meta{display:flex;gap:.75rem;margin-top:.15rem;font-size:.75rem;color:var(--mm-ai-list-muted);font-variant-numeric:tabular-nums}.mm-ai-session-list__preview{color:var(--mm-ai-list-accent);font-size:.75rem;text-decoration:underline;text-underline-offset:2px}.mm-ai-session-list__preview:hover{text-decoration-thickness:2px}.mm-ai-session-list__empty{padding:1rem;text-align:center;color:var(--mm-ai-list-muted);font-style:italic}\n"] }]
|
|
372
|
+
}], propDecorators: { sessions: [{ type: i0.Input, args: [{ isSignal: true, alias: "sessions", required: true }] }], selectedSessionId: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedSessionId", required: false }] }], sessionSelected: [{ type: i0.Output, args: ["sessionSelected"] }] } });
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Read-only transcript of a session, derived from the events stream. The
|
|
376
|
+
* component takes the events as an input signal so the host owns lifetime —
|
|
377
|
+
* a parent that's already wired `AiSessionStreamService.events$` into a
|
|
378
|
+
* `toSignal()` just hands the same signal in.
|
|
379
|
+
*
|
|
380
|
+
* Markdown rendering is deliberately minimal in Phase 1: the component shows
|
|
381
|
+
* the raw payload (already JSON or stream-json) in a `<pre>` block. The
|
|
382
|
+
* concept §10 calls for proper markdown + code-block highlighting; that
|
|
383
|
+
* upgrade lands behind a `markdown` Input once we've picked a renderer
|
|
384
|
+
* (markdown-it vs marked) and verified its CSP behaviour.
|
|
385
|
+
*/
|
|
386
|
+
class AiChatStreamComponent {
|
|
387
|
+
events = input.required(...(ngDevMode ? [{ debugName: "events" }] : /* istanbul ignore next */ []));
|
|
388
|
+
/**
|
|
389
|
+
* Sorted by sequence so out-of-order arrival (the SignalR + REST backfill
|
|
390
|
+
* union) renders deterministically.
|
|
391
|
+
*/
|
|
392
|
+
orderedEvents = computed(() => [...this.events()].sort((a, b) => a.sequence - b.sequence), ...(ngDevMode ? [{ debugName: "orderedEvents" }] : /* istanbul ignore next */ []));
|
|
393
|
+
variant(kind) {
|
|
394
|
+
switch (kind) {
|
|
395
|
+
case 'Message':
|
|
396
|
+
return 'message';
|
|
397
|
+
case 'ToolCall':
|
|
398
|
+
return 'tool-call';
|
|
399
|
+
case 'ToolResult':
|
|
400
|
+
return 'tool-result';
|
|
401
|
+
case 'StatusChange':
|
|
402
|
+
return 'status';
|
|
403
|
+
case 'Hook':
|
|
404
|
+
return 'hook';
|
|
405
|
+
case 'Error':
|
|
406
|
+
return 'error';
|
|
407
|
+
default:
|
|
408
|
+
return 'message';
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiChatStreamComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
412
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiChatStreamComponent, isStandalone: true, selector: "mm-ai-chat-stream", inputs: { events: { classPropertyName: "events", publicName: "events", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<div class=\"mm-ai-chat\">\n @for (event of orderedEvents(); track event.sequence) {\n <article class=\"mm-ai-chat__event mm-ai-chat__event--{{ variant(event.kind) }}\">\n <header class=\"mm-ai-chat__event-header\">\n <span class=\"mm-ai-chat__kind\">{{ event.kind }}</span>\n <span class=\"mm-ai-chat__actor\">{{ event.actorRef }}</span>\n <span class=\"mm-ai-chat__sequence\">#{{ event.sequence }}</span>\n <time class=\"mm-ai-chat__time\" [attr.datetime]=\"event.at\">\n {{ event.at }}\n </time>\n </header>\n <pre class=\"mm-ai-chat__payload\">{{ event.payload }}</pre>\n </article>\n } @empty {\n <div class=\"mm-ai-chat__empty\">\n No events yet \u2014 the worker hasn't emitted anything.\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-chat-bg: #16191d;--mm-ai-chat-card-bg: #1f2227;--mm-ai-chat-border: #2f343b;--mm-ai-chat-text: #e6e6e6;--mm-ai-chat-muted: #9aa0a6;--mm-ai-chat-message: #1f6feb;--mm-ai-chat-tool-call: #d29922;--mm-ai-chat-tool-result: #2ea44f;--mm-ai-chat-status: #6e7681;--mm-ai-chat-hook: #8957e5;--mm-ai-chat-error: #d73a49}.mm-ai-chat{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;background:var(--mm-ai-chat-bg);color:var(--mm-ai-chat-text);border-radius:.5rem}.mm-ai-chat__event{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left-width:3px;border-radius:.4rem;padding:.5rem .6rem}.mm-ai-chat__event--message{border-left-color:var(--mm-ai-chat-message)}.mm-ai-chat__event--tool-call{border-left-color:var(--mm-ai-chat-tool-call)}.mm-ai-chat__event--tool-result{border-left-color:var(--mm-ai-chat-tool-result)}.mm-ai-chat__event--status{border-left-color:var(--mm-ai-chat-status)}.mm-ai-chat__event--hook{border-left-color:var(--mm-ai-chat-hook)}.mm-ai-chat__event--error{border-left-color:var(--mm-ai-chat-error)}.mm-ai-chat__event-header{display:flex;gap:.5rem;align-items:baseline;font-size:.75rem;color:var(--mm-ai-chat-muted);margin-bottom:.3rem}.mm-ai-chat__kind{font-weight:700;color:var(--mm-ai-chat-text);text-transform:uppercase;letter-spacing:.04em}.mm-ai-chat__sequence,.mm-ai-chat__time{font-variant-numeric:tabular-nums}.mm-ai-chat__time{margin-left:auto}.mm-ai-chat__payload{margin:0;font-size:.8rem;white-space:pre-wrap;word-break:break-word}.mm-ai-chat__empty{color:var(--mm-ai-chat-muted);text-align:center;padding:1rem;font-style:italic}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
413
|
+
}
|
|
414
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiChatStreamComponent, decorators: [{
|
|
415
|
+
type: Component,
|
|
416
|
+
args: [{ selector: 'mm-ai-chat-stream', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mm-ai-chat\">\n @for (event of orderedEvents(); track event.sequence) {\n <article class=\"mm-ai-chat__event mm-ai-chat__event--{{ variant(event.kind) }}\">\n <header class=\"mm-ai-chat__event-header\">\n <span class=\"mm-ai-chat__kind\">{{ event.kind }}</span>\n <span class=\"mm-ai-chat__actor\">{{ event.actorRef }}</span>\n <span class=\"mm-ai-chat__sequence\">#{{ event.sequence }}</span>\n <time class=\"mm-ai-chat__time\" [attr.datetime]=\"event.at\">\n {{ event.at }}\n </time>\n </header>\n <pre class=\"mm-ai-chat__payload\">{{ event.payload }}</pre>\n </article>\n } @empty {\n <div class=\"mm-ai-chat__empty\">\n No events yet \u2014 the worker hasn't emitted anything.\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-chat-bg: #16191d;--mm-ai-chat-card-bg: #1f2227;--mm-ai-chat-border: #2f343b;--mm-ai-chat-text: #e6e6e6;--mm-ai-chat-muted: #9aa0a6;--mm-ai-chat-message: #1f6feb;--mm-ai-chat-tool-call: #d29922;--mm-ai-chat-tool-result: #2ea44f;--mm-ai-chat-status: #6e7681;--mm-ai-chat-hook: #8957e5;--mm-ai-chat-error: #d73a49}.mm-ai-chat{display:flex;flex-direction:column;gap:.5rem;padding:.5rem;background:var(--mm-ai-chat-bg);color:var(--mm-ai-chat-text);border-radius:.5rem}.mm-ai-chat__event{background:var(--mm-ai-chat-card-bg);border:1px solid var(--mm-ai-chat-border);border-left-width:3px;border-radius:.4rem;padding:.5rem .6rem}.mm-ai-chat__event--message{border-left-color:var(--mm-ai-chat-message)}.mm-ai-chat__event--tool-call{border-left-color:var(--mm-ai-chat-tool-call)}.mm-ai-chat__event--tool-result{border-left-color:var(--mm-ai-chat-tool-result)}.mm-ai-chat__event--status{border-left-color:var(--mm-ai-chat-status)}.mm-ai-chat__event--hook{border-left-color:var(--mm-ai-chat-hook)}.mm-ai-chat__event--error{border-left-color:var(--mm-ai-chat-error)}.mm-ai-chat__event-header{display:flex;gap:.5rem;align-items:baseline;font-size:.75rem;color:var(--mm-ai-chat-muted);margin-bottom:.3rem}.mm-ai-chat__kind{font-weight:700;color:var(--mm-ai-chat-text);text-transform:uppercase;letter-spacing:.04em}.mm-ai-chat__sequence,.mm-ai-chat__time{font-variant-numeric:tabular-nums}.mm-ai-chat__time{margin-left:auto}.mm-ai-chat__payload{margin:0;font-size:.8rem;white-space:pre-wrap;word-break:break-word}.mm-ai-chat__empty{color:var(--mm-ai-chat-muted);text-align:center;padding:1rem;font-style:italic}\n"] }]
|
|
417
|
+
}], propDecorators: { events: [{ type: i0.Input, args: [{ isSignal: true, alias: "events", required: true }] }] } });
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Expandable card surface for one tool invocation in a session transcript.
|
|
421
|
+
* Click the header to toggle arguments + result; the body is rendered as
|
|
422
|
+
* pretty-printed JSON so the host doesn't need its own json formatter.
|
|
423
|
+
*/
|
|
424
|
+
class AiToolCallComponent {
|
|
425
|
+
call = input.required(...(ngDevMode ? [{ debugName: "call" }] : /* istanbul ignore next */ []));
|
|
426
|
+
expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
427
|
+
statusVariant = computed(() => {
|
|
428
|
+
const status = this.call().status;
|
|
429
|
+
switch (status) {
|
|
430
|
+
case 'Succeeded':
|
|
431
|
+
return 'succeeded';
|
|
432
|
+
case 'Failed':
|
|
433
|
+
return 'failed';
|
|
434
|
+
case 'Rejected':
|
|
435
|
+
return 'rejected';
|
|
436
|
+
case 'Approved':
|
|
437
|
+
case 'Pending':
|
|
438
|
+
return 'pending';
|
|
439
|
+
default:
|
|
440
|
+
return 'pending';
|
|
441
|
+
}
|
|
442
|
+
}, ...(ngDevMode ? [{ debugName: "statusVariant" }] : /* istanbul ignore next */ []));
|
|
443
|
+
argumentsJson = computed(() => this.format(this.call().arguments), ...(ngDevMode ? [{ debugName: "argumentsJson" }] : /* istanbul ignore next */ []));
|
|
444
|
+
resultJson = computed(() => {
|
|
445
|
+
const result = this.call().result;
|
|
446
|
+
return result === undefined ? null : this.format(result);
|
|
447
|
+
}, ...(ngDevMode ? [{ debugName: "resultJson" }] : /* istanbul ignore next */ []));
|
|
448
|
+
toggleExpanded() {
|
|
449
|
+
this.expanded.update((v) => !v);
|
|
450
|
+
}
|
|
451
|
+
format(value) {
|
|
452
|
+
if (value === null || value === undefined) {
|
|
453
|
+
return '';
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
return JSON.stringify(value, null, 2);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return String(value);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiToolCallComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
463
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiToolCallComponent, isStandalone: true, selector: "mm-ai-tool-call", inputs: { call: { classPropertyName: "call", publicName: "call", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<div class=\"mm-ai-tool-call mm-ai-tool-call--{{ statusVariant() }}\">\n <button\n type=\"button\"\n class=\"mm-ai-tool-call__header\"\n (click)=\"toggleExpanded()\"\n [attr.aria-expanded]=\"expanded()\"\n >\n <span class=\"mm-ai-tool-call__chevron\">\n {{ expanded() ? '\u25BE' : '\u25B8' }}\n </span>\n <span class=\"mm-ai-tool-call__name\">{{ call().toolName }}</span>\n <span class=\"mm-ai-tool-call__status\">{{ call().status }}</span>\n @if (call().durationMs != null) {\n <span class=\"mm-ai-tool-call__duration\">\n {{ call().durationMs }}ms\n </span>\n }\n </button>\n\n @if (expanded()) {\n <div class=\"mm-ai-tool-call__body\">\n <div class=\"mm-ai-tool-call__section\">\n <h4 class=\"mm-ai-tool-call__heading\">Arguments</h4>\n <pre class=\"mm-ai-tool-call__code\">{{ argumentsJson() }}</pre>\n </div>\n\n @if (resultJson() !== null) {\n <div class=\"mm-ai-tool-call__section\">\n <h4 class=\"mm-ai-tool-call__heading\">Result</h4>\n <pre class=\"mm-ai-tool-call__code\">{{ resultJson() }}</pre>\n </div>\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-tool-bg: #1f2227;--mm-ai-tool-border: #2f343b;--mm-ai-tool-text: #e6e6e6;--mm-ai-tool-muted: #9aa0a6;--mm-ai-tool-success: #2ea44f;--mm-ai-tool-failed: #d73a49;--mm-ai-tool-rejected: #d29922;--mm-ai-tool-pending: #6e7681;--mm-ai-tool-code-bg: #16191d}.mm-ai-tool-call{border:1px solid var(--mm-ai-tool-border);border-radius:.5rem;background:var(--mm-ai-tool-bg);color:var(--mm-ai-tool-text);margin:.4rem 0;overflow:hidden}.mm-ai-tool-call--succeeded{border-left:3px solid var(--mm-ai-tool-success)}.mm-ai-tool-call--failed{border-left:3px solid var(--mm-ai-tool-failed)}.mm-ai-tool-call--rejected{border-left:3px solid var(--mm-ai-tool-rejected)}.mm-ai-tool-call--pending{border-left:3px solid var(--mm-ai-tool-pending)}.mm-ai-tool-call__header{width:100%;display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:transparent;color:inherit;border:none;cursor:pointer;text-align:left;font-family:inherit;font-size:.9rem}.mm-ai-tool-call__header:hover{background:#ffffff0a}.mm-ai-tool-call__chevron{width:1rem;text-align:center;color:var(--mm-ai-tool-muted)}.mm-ai-tool-call__name{font-weight:600;flex:1}.mm-ai-tool-call__status,.mm-ai-tool-call__duration{font-size:.75rem;color:var(--mm-ai-tool-muted);font-variant-numeric:tabular-nums}.mm-ai-tool-call__body{border-top:1px solid var(--mm-ai-tool-border);padding:.5rem .75rem}.mm-ai-tool-call__section+.mm-ai-tool-call__section{margin-top:.75rem}.mm-ai-tool-call__heading{margin:0 0 .25rem;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;color:var(--mm-ai-tool-muted)}.mm-ai-tool-call__code{margin:0;padding:.5rem .6rem;background:var(--mm-ai-tool-code-bg);border-radius:.3rem;font-size:.8rem;overflow-x:auto;white-space:pre-wrap;word-break:break-word}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
464
|
+
}
|
|
465
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiToolCallComponent, decorators: [{
|
|
466
|
+
type: Component,
|
|
467
|
+
args: [{ selector: 'mm-ai-tool-call', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mm-ai-tool-call mm-ai-tool-call--{{ statusVariant() }}\">\n <button\n type=\"button\"\n class=\"mm-ai-tool-call__header\"\n (click)=\"toggleExpanded()\"\n [attr.aria-expanded]=\"expanded()\"\n >\n <span class=\"mm-ai-tool-call__chevron\">\n {{ expanded() ? '\u25BE' : '\u25B8' }}\n </span>\n <span class=\"mm-ai-tool-call__name\">{{ call().toolName }}</span>\n <span class=\"mm-ai-tool-call__status\">{{ call().status }}</span>\n @if (call().durationMs != null) {\n <span class=\"mm-ai-tool-call__duration\">\n {{ call().durationMs }}ms\n </span>\n }\n </button>\n\n @if (expanded()) {\n <div class=\"mm-ai-tool-call__body\">\n <div class=\"mm-ai-tool-call__section\">\n <h4 class=\"mm-ai-tool-call__heading\">Arguments</h4>\n <pre class=\"mm-ai-tool-call__code\">{{ argumentsJson() }}</pre>\n </div>\n\n @if (resultJson() !== null) {\n <div class=\"mm-ai-tool-call__section\">\n <h4 class=\"mm-ai-tool-call__heading\">Result</h4>\n <pre class=\"mm-ai-tool-call__code\">{{ resultJson() }}</pre>\n </div>\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-tool-bg: #1f2227;--mm-ai-tool-border: #2f343b;--mm-ai-tool-text: #e6e6e6;--mm-ai-tool-muted: #9aa0a6;--mm-ai-tool-success: #2ea44f;--mm-ai-tool-failed: #d73a49;--mm-ai-tool-rejected: #d29922;--mm-ai-tool-pending: #6e7681;--mm-ai-tool-code-bg: #16191d}.mm-ai-tool-call{border:1px solid var(--mm-ai-tool-border);border-radius:.5rem;background:var(--mm-ai-tool-bg);color:var(--mm-ai-tool-text);margin:.4rem 0;overflow:hidden}.mm-ai-tool-call--succeeded{border-left:3px solid var(--mm-ai-tool-success)}.mm-ai-tool-call--failed{border-left:3px solid var(--mm-ai-tool-failed)}.mm-ai-tool-call--rejected{border-left:3px solid var(--mm-ai-tool-rejected)}.mm-ai-tool-call--pending{border-left:3px solid var(--mm-ai-tool-pending)}.mm-ai-tool-call__header{width:100%;display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background:transparent;color:inherit;border:none;cursor:pointer;text-align:left;font-family:inherit;font-size:.9rem}.mm-ai-tool-call__header:hover{background:#ffffff0a}.mm-ai-tool-call__chevron{width:1rem;text-align:center;color:var(--mm-ai-tool-muted)}.mm-ai-tool-call__name{font-weight:600;flex:1}.mm-ai-tool-call__status,.mm-ai-tool-call__duration{font-size:.75rem;color:var(--mm-ai-tool-muted);font-variant-numeric:tabular-nums}.mm-ai-tool-call__body{border-top:1px solid var(--mm-ai-tool-border);padding:.5rem .75rem}.mm-ai-tool-call__section+.mm-ai-tool-call__section{margin-top:.75rem}.mm-ai-tool-call__heading{margin:0 0 .25rem;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;color:var(--mm-ai-tool-muted)}.mm-ai-tool-call__code{margin:0;padding:.5rem .6rem;background:var(--mm-ai-tool-code-bg);border-radius:.3rem;font-size:.8rem;overflow-x:auto;white-space:pre-wrap;word-break:break-word}\n"] }]
|
|
468
|
+
}], propDecorators: { call: [{ type: i0.Input, args: [{ isSignal: true, alias: "call", required: true }] }] } });
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Approval modal. The host opens it when a `AiApprovalRequestedDto` arrives on
|
|
472
|
+
* the SignalR stream, fills the `request` input, and listens for the `decide`
|
|
473
|
+
* output to call `AiAdapterClientService.decideApproval`. The modal itself is
|
|
474
|
+
* presentation-only: no networking, no global state, no portal magic.
|
|
475
|
+
*
|
|
476
|
+
* The host is expected to wrap the modal in its own backdrop / focus-trap so
|
|
477
|
+
* the library doesn't pull in a positioning dependency (CDK overlay, Kendo
|
|
478
|
+
* window, ng-bootstrap, …) — each host shop already has one.
|
|
479
|
+
*/
|
|
480
|
+
class AiApprovalModalComponent {
|
|
481
|
+
request = input.required(...(ngDevMode ? [{ debugName: "request" }] : /* istanbul ignore next */ []));
|
|
482
|
+
decide = output();
|
|
483
|
+
dismiss = output();
|
|
484
|
+
comment = signal('', ...(ngDevMode ? [{ debugName: "comment" }] : /* istanbul ignore next */ []));
|
|
485
|
+
prettyPayload = computed(() => {
|
|
486
|
+
try {
|
|
487
|
+
return JSON.stringify(JSON.parse(this.request().payload), null, 2);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
return this.request().payload;
|
|
491
|
+
}
|
|
492
|
+
}, ...(ngDevMode ? [{ debugName: "prettyPayload" }] : /* istanbul ignore next */ []));
|
|
493
|
+
approve() {
|
|
494
|
+
this.decide.emit({
|
|
495
|
+
outcome: 'Approved',
|
|
496
|
+
comment: this.commentOrUndefined(),
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
reject() {
|
|
500
|
+
this.decide.emit({
|
|
501
|
+
outcome: 'Rejected',
|
|
502
|
+
comment: this.commentOrUndefined(),
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
onDismiss() {
|
|
506
|
+
this.dismiss.emit();
|
|
507
|
+
}
|
|
508
|
+
commentOrUndefined() {
|
|
509
|
+
const trimmed = this.comment().trim();
|
|
510
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
511
|
+
}
|
|
512
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiApprovalModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
513
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: AiApprovalModalComponent, isStandalone: true, selector: "mm-ai-approval-modal", inputs: { request: { classPropertyName: "request", publicName: "request", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { decide: "decide", dismiss: "dismiss" }, ngImport: i0, template: "<div class=\"mm-ai-approval\">\n <header class=\"mm-ai-approval__header\">\n <h3 class=\"mm-ai-approval__title\">Approve tool call</h3>\n <button\n type=\"button\"\n class=\"mm-ai-approval__close\"\n (click)=\"onDismiss()\"\n aria-label=\"Dismiss\"\n >\n \u00D7\n </button>\n </header>\n\n <div class=\"mm-ai-approval__meta\">\n <div><span>Tool:</span> <code>{{ request().toolName }}</code></div>\n <div><span>Reason:</span> <code>{{ request().reason }}</code></div>\n <div><span>Session:</span> <code>{{ request().sessionId }}</code></div>\n </div>\n\n <div class=\"mm-ai-approval__section\">\n <h4 class=\"mm-ai-approval__heading\">Payload</h4>\n <pre class=\"mm-ai-approval__code\">{{ prettyPayload() }}</pre>\n </div>\n\n <div class=\"mm-ai-approval__section\">\n <h4 class=\"mm-ai-approval__heading\">Comment <span>(optional)</span></h4>\n <textarea\n class=\"mm-ai-approval__comment\"\n [ngModel]=\"comment()\"\n (ngModelChange)=\"comment.set($event)\"\n rows=\"3\"\n placeholder=\"Why is this approved or rejected?\"\n ></textarea>\n </div>\n\n <footer class=\"mm-ai-approval__actions\">\n <button\n type=\"button\"\n class=\"mm-ai-approval__btn mm-ai-approval__btn--reject\"\n (click)=\"reject()\"\n >\n Reject\n </button>\n <button\n type=\"button\"\n class=\"mm-ai-approval__btn mm-ai-approval__btn--approve\"\n (click)=\"approve()\"\n >\n Approve\n </button>\n </footer>\n</div>\n", styles: [":host{display:block;--mm-ai-approval-bg: #1f2227;--mm-ai-approval-border: #2f343b;--mm-ai-approval-text: #e6e6e6;--mm-ai-approval-muted: #9aa0a6;--mm-ai-approval-approve: #2ea44f;--mm-ai-approval-reject: #d73a49;--mm-ai-approval-code-bg: #16191d}.mm-ai-approval{background:var(--mm-ai-approval-bg);color:var(--mm-ai-approval-text);border:1px solid var(--mm-ai-approval-border);border-radius:.6rem;padding:1rem 1.25rem;width:min(36rem,100%)}.mm-ai-approval__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.mm-ai-approval__title{margin:0;font-size:1rem}.mm-ai-approval__close{background:transparent;color:var(--mm-ai-approval-muted);border:none;font-size:1.4rem;cursor:pointer;line-height:1;padding:0 .25rem}.mm-ai-approval__close:hover{color:var(--mm-ai-approval-text)}.mm-ai-approval__meta{display:grid;gap:.25rem;font-size:.85rem;margin-bottom:.75rem}.mm-ai-approval__meta span{color:var(--mm-ai-approval-muted);margin-right:.35rem}.mm-ai-approval__meta code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.mm-ai-approval__section{margin-bottom:.75rem}.mm-ai-approval__heading{margin:0 0 .3rem;font-size:.75rem;color:var(--mm-ai-approval-muted);text-transform:uppercase;letter-spacing:.04em}.mm-ai-approval__heading span{text-transform:none;letter-spacing:0;opacity:.6}.mm-ai-approval__code{margin:0;padding:.5rem .6rem;background:var(--mm-ai-approval-code-bg);border-radius:.3rem;font-size:.8rem;overflow-x:auto;white-space:pre-wrap;word-break:break-word;max-height:14rem}.mm-ai-approval__comment{width:100%;box-sizing:border-box;padding:.5rem .6rem;background:var(--mm-ai-approval-code-bg);color:inherit;border:1px solid var(--mm-ai-approval-border);border-radius:.3rem;font-family:inherit;font-size:.85rem;resize:vertical}.mm-ai-approval__actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem}.mm-ai-approval__btn{padding:.45rem 1rem;border-radius:.3rem;font-weight:600;border:none;cursor:pointer;font-size:.85rem}.mm-ai-approval__btn--approve{background:var(--mm-ai-approval-approve);color:#fff}.mm-ai-approval__btn--reject{background:var(--mm-ai-approval-reject);color:#fff}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
514
|
+
}
|
|
515
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiApprovalModalComponent, decorators: [{
|
|
516
|
+
type: Component,
|
|
517
|
+
args: [{ selector: 'mm-ai-approval-modal', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mm-ai-approval\">\n <header class=\"mm-ai-approval__header\">\n <h3 class=\"mm-ai-approval__title\">Approve tool call</h3>\n <button\n type=\"button\"\n class=\"mm-ai-approval__close\"\n (click)=\"onDismiss()\"\n aria-label=\"Dismiss\"\n >\n \u00D7\n </button>\n </header>\n\n <div class=\"mm-ai-approval__meta\">\n <div><span>Tool:</span> <code>{{ request().toolName }}</code></div>\n <div><span>Reason:</span> <code>{{ request().reason }}</code></div>\n <div><span>Session:</span> <code>{{ request().sessionId }}</code></div>\n </div>\n\n <div class=\"mm-ai-approval__section\">\n <h4 class=\"mm-ai-approval__heading\">Payload</h4>\n <pre class=\"mm-ai-approval__code\">{{ prettyPayload() }}</pre>\n </div>\n\n <div class=\"mm-ai-approval__section\">\n <h4 class=\"mm-ai-approval__heading\">Comment <span>(optional)</span></h4>\n <textarea\n class=\"mm-ai-approval__comment\"\n [ngModel]=\"comment()\"\n (ngModelChange)=\"comment.set($event)\"\n rows=\"3\"\n placeholder=\"Why is this approved or rejected?\"\n ></textarea>\n </div>\n\n <footer class=\"mm-ai-approval__actions\">\n <button\n type=\"button\"\n class=\"mm-ai-approval__btn mm-ai-approval__btn--reject\"\n (click)=\"reject()\"\n >\n Reject\n </button>\n <button\n type=\"button\"\n class=\"mm-ai-approval__btn mm-ai-approval__btn--approve\"\n (click)=\"approve()\"\n >\n Approve\n </button>\n </footer>\n</div>\n", styles: [":host{display:block;--mm-ai-approval-bg: #1f2227;--mm-ai-approval-border: #2f343b;--mm-ai-approval-text: #e6e6e6;--mm-ai-approval-muted: #9aa0a6;--mm-ai-approval-approve: #2ea44f;--mm-ai-approval-reject: #d73a49;--mm-ai-approval-code-bg: #16191d}.mm-ai-approval{background:var(--mm-ai-approval-bg);color:var(--mm-ai-approval-text);border:1px solid var(--mm-ai-approval-border);border-radius:.6rem;padding:1rem 1.25rem;width:min(36rem,100%)}.mm-ai-approval__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}.mm-ai-approval__title{margin:0;font-size:1rem}.mm-ai-approval__close{background:transparent;color:var(--mm-ai-approval-muted);border:none;font-size:1.4rem;cursor:pointer;line-height:1;padding:0 .25rem}.mm-ai-approval__close:hover{color:var(--mm-ai-approval-text)}.mm-ai-approval__meta{display:grid;gap:.25rem;font-size:.85rem;margin-bottom:.75rem}.mm-ai-approval__meta span{color:var(--mm-ai-approval-muted);margin-right:.35rem}.mm-ai-approval__meta code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.mm-ai-approval__section{margin-bottom:.75rem}.mm-ai-approval__heading{margin:0 0 .3rem;font-size:.75rem;color:var(--mm-ai-approval-muted);text-transform:uppercase;letter-spacing:.04em}.mm-ai-approval__heading span{text-transform:none;letter-spacing:0;opacity:.6}.mm-ai-approval__code{margin:0;padding:.5rem .6rem;background:var(--mm-ai-approval-code-bg);border-radius:.3rem;font-size:.8rem;overflow-x:auto;white-space:pre-wrap;word-break:break-word;max-height:14rem}.mm-ai-approval__comment{width:100%;box-sizing:border-box;padding:.5rem .6rem;background:var(--mm-ai-approval-code-bg);color:inherit;border:1px solid var(--mm-ai-approval-border);border-radius:.3rem;font-family:inherit;font-size:.85rem;resize:vertical}.mm-ai-approval__actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.75rem}.mm-ai-approval__btn{padding:.45rem 1rem;border-radius:.3rem;font-weight:600;border:none;cursor:pointer;font-size:.85rem}.mm-ai-approval__btn--approve{background:var(--mm-ai-approval-approve);color:#fff}.mm-ai-approval__btn--reject{background:var(--mm-ai-approval-reject);color:#fff}\n"] }]
|
|
518
|
+
}], propDecorators: { request: [{ type: i0.Input, args: [{ isSignal: true, alias: "request", required: true }] }], decide: [{ type: i0.Output, args: ["decide"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }] } });
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Two-bar usage indicator (daily + monthly) with threshold colour state.
|
|
522
|
+
* Inputs are unit-agnostic counts and caps; the component does not parse the
|
|
523
|
+
* tenant's `AiQuotaLimit` itself — the host is expected to feed numbers the
|
|
524
|
+
* adapter already aggregated.
|
|
525
|
+
*/
|
|
526
|
+
class AiQuotaIndicatorComponent {
|
|
527
|
+
dailyConsumed = input.required(...(ngDevMode ? [{ debugName: "dailyConsumed" }] : /* istanbul ignore next */ []));
|
|
528
|
+
dailyCap = input.required(...(ngDevMode ? [{ debugName: "dailyCap" }] : /* istanbul ignore next */ []));
|
|
529
|
+
monthlyConsumed = input(null, ...(ngDevMode ? [{ debugName: "monthlyConsumed" }] : /* istanbul ignore next */ []));
|
|
530
|
+
monthlyCap = input(null, ...(ngDevMode ? [{ debugName: "monthlyCap" }] : /* istanbul ignore next */ []));
|
|
531
|
+
dailyPercent = computed(() => this.percent(this.dailyConsumed(), this.dailyCap()), ...(ngDevMode ? [{ debugName: "dailyPercent" }] : /* istanbul ignore next */ []));
|
|
532
|
+
monthlyPercent = computed(() => {
|
|
533
|
+
const consumed = this.monthlyConsumed();
|
|
534
|
+
const cap = this.monthlyCap();
|
|
535
|
+
if (consumed == null || cap == null) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
return this.percent(consumed, cap);
|
|
539
|
+
}, ...(ngDevMode ? [{ debugName: "monthlyPercent" }] : /* istanbul ignore next */ []));
|
|
540
|
+
dailyState = computed(() => this.toState(this.dailyPercent()), ...(ngDevMode ? [{ debugName: "dailyState" }] : /* istanbul ignore next */ []));
|
|
541
|
+
monthlyState = computed(() => {
|
|
542
|
+
const pct = this.monthlyPercent();
|
|
543
|
+
return pct == null ? null : this.toState(pct);
|
|
544
|
+
}, ...(ngDevMode ? [{ debugName: "monthlyState" }] : /* istanbul ignore next */ []));
|
|
545
|
+
percent(consumed, cap) {
|
|
546
|
+
if (cap <= 0) {
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
return Math.min(100, Math.round((consumed / cap) * 100));
|
|
550
|
+
}
|
|
551
|
+
toState(percent) {
|
|
552
|
+
if (percent >= 100) {
|
|
553
|
+
return 'critical';
|
|
554
|
+
}
|
|
555
|
+
if (percent >= 80) {
|
|
556
|
+
return 'warning';
|
|
557
|
+
}
|
|
558
|
+
return 'ok';
|
|
559
|
+
}
|
|
560
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiQuotaIndicatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
561
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiQuotaIndicatorComponent, isStandalone: true, selector: "mm-ai-quota-indicator", inputs: { dailyConsumed: { classPropertyName: "dailyConsumed", publicName: "dailyConsumed", isSignal: true, isRequired: true, transformFunction: null }, dailyCap: { classPropertyName: "dailyCap", publicName: "dailyCap", isSignal: true, isRequired: true, transformFunction: null }, monthlyConsumed: { classPropertyName: "monthlyConsumed", publicName: "monthlyConsumed", isSignal: true, isRequired: false, transformFunction: null }, monthlyCap: { classPropertyName: "monthlyCap", publicName: "monthlyCap", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"mm-ai-quota\">\n <div class=\"mm-ai-quota__row\">\n <span class=\"mm-ai-quota__label\">Daily</span>\n <div class=\"mm-ai-quota__bar mm-ai-quota__bar--{{ dailyState() }}\">\n <div class=\"mm-ai-quota__fill\" [style.width.%]=\"dailyPercent()\"></div>\n </div>\n <span class=\"mm-ai-quota__count\">\n {{ dailyConsumed() }} / {{ dailyCap() }} ({{ dailyPercent() }}%)\n </span>\n </div>\n\n @if (monthlyPercent() !== null) {\n <div class=\"mm-ai-quota__row\">\n <span class=\"mm-ai-quota__label\">Monthly</span>\n <div class=\"mm-ai-quota__bar mm-ai-quota__bar--{{ monthlyState() }}\">\n <div class=\"mm-ai-quota__fill\" [style.width.%]=\"monthlyPercent()\"></div>\n </div>\n <span class=\"mm-ai-quota__count\">\n {{ monthlyConsumed() }} / {{ monthlyCap() }} ({{ monthlyPercent() }}%)\n </span>\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-quota-track-bg: #2a2a2a;--mm-ai-quota-ok: #2ea44f;--mm-ai-quota-warning: #d29922;--mm-ai-quota-critical: #d73a49}.mm-ai-quota{display:flex;flex-direction:column;gap:.5rem;font-size:.85rem}.mm-ai-quota__row{display:grid;grid-template-columns:4.5rem 1fr auto;align-items:center;gap:.5rem}.mm-ai-quota__label{font-weight:600;opacity:.7}.mm-ai-quota__bar{height:.5rem;border-radius:9999px;background:var(--mm-ai-quota-track-bg);overflow:hidden}.mm-ai-quota__bar--ok .mm-ai-quota__fill{background:var(--mm-ai-quota-ok)}.mm-ai-quota__bar--warning .mm-ai-quota__fill{background:var(--mm-ai-quota-warning)}.mm-ai-quota__bar--critical .mm-ai-quota__fill{background:var(--mm-ai-quota-critical)}.mm-ai-quota__fill{height:100%;transition:width .2s ease}.mm-ai-quota__count{font-variant-numeric:tabular-nums;opacity:.8}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
562
|
+
}
|
|
563
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiQuotaIndicatorComponent, decorators: [{
|
|
564
|
+
type: Component,
|
|
565
|
+
args: [{ selector: 'mm-ai-quota-indicator', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"mm-ai-quota\">\n <div class=\"mm-ai-quota__row\">\n <span class=\"mm-ai-quota__label\">Daily</span>\n <div class=\"mm-ai-quota__bar mm-ai-quota__bar--{{ dailyState() }}\">\n <div class=\"mm-ai-quota__fill\" [style.width.%]=\"dailyPercent()\"></div>\n </div>\n <span class=\"mm-ai-quota__count\">\n {{ dailyConsumed() }} / {{ dailyCap() }} ({{ dailyPercent() }}%)\n </span>\n </div>\n\n @if (monthlyPercent() !== null) {\n <div class=\"mm-ai-quota__row\">\n <span class=\"mm-ai-quota__label\">Monthly</span>\n <div class=\"mm-ai-quota__bar mm-ai-quota__bar--{{ monthlyState() }}\">\n <div class=\"mm-ai-quota__fill\" [style.width.%]=\"monthlyPercent()\"></div>\n </div>\n <span class=\"mm-ai-quota__count\">\n {{ monthlyConsumed() }} / {{ monthlyCap() }} ({{ monthlyPercent() }}%)\n </span>\n </div>\n }\n</div>\n", styles: [":host{display:block;--mm-ai-quota-track-bg: #2a2a2a;--mm-ai-quota-ok: #2ea44f;--mm-ai-quota-warning: #d29922;--mm-ai-quota-critical: #d73a49}.mm-ai-quota{display:flex;flex-direction:column;gap:.5rem;font-size:.85rem}.mm-ai-quota__row{display:grid;grid-template-columns:4.5rem 1fr auto;align-items:center;gap:.5rem}.mm-ai-quota__label{font-weight:600;opacity:.7}.mm-ai-quota__bar{height:.5rem;border-radius:9999px;background:var(--mm-ai-quota-track-bg);overflow:hidden}.mm-ai-quota__bar--ok .mm-ai-quota__fill{background:var(--mm-ai-quota-ok)}.mm-ai-quota__bar--warning .mm-ai-quota__fill{background:var(--mm-ai-quota-warning)}.mm-ai-quota__bar--critical .mm-ai-quota__fill{background:var(--mm-ai-quota-critical)}.mm-ai-quota__fill{height:100%;transition:width .2s ease}.mm-ai-quota__count{font-variant-numeric:tabular-nums;opacity:.8}\n"] }]
|
|
566
|
+
}], propDecorators: { dailyConsumed: [{ type: i0.Input, args: [{ isSignal: true, alias: "dailyConsumed", required: true }] }], dailyCap: [{ type: i0.Input, args: [{ isSignal: true, alias: "dailyCap", required: true }] }], monthlyConsumed: [{ type: i0.Input, args: [{ isSignal: true, alias: "monthlyConsumed", required: false }] }], monthlyCap: [{ type: i0.Input, args: [{ isSignal: true, alias: "monthlyCap", required: false }] }] } });
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Self-service credential-ticket issuer (#4133, concept §10). Admin picks a
|
|
570
|
+
* scope + TTL, hits **Issue ticket**, copies the displayed code, hands it to
|
|
571
|
+
* a developer / operator out-of-band. The plaintext code is shown exactly
|
|
572
|
+
* once — refreshing the panel after issue clears it so a careless screenshot
|
|
573
|
+
* doesn't leak.
|
|
574
|
+
*
|
|
575
|
+
* Theme-neutral by design: visuals use CSS custom properties with neutral
|
|
576
|
+
* defaults so the host (Refinery Studio LCARS, demo-app, etc.) can override
|
|
577
|
+
* them via `--mm-ai-ticket-*` variables without touching the component.
|
|
578
|
+
*/
|
|
579
|
+
class AiCredentialTicketIssueComponent {
|
|
580
|
+
client = inject(AiAdapterClientService);
|
|
581
|
+
scope = signal('CredentialRegister', ...(ngDevMode ? [{ debugName: "scope" }] : /* istanbul ignore next */ []));
|
|
582
|
+
ttlMinutes = signal(5, ...(ngDevMode ? [{ debugName: "ttlMinutes" }] : /* istanbul ignore next */ []));
|
|
583
|
+
issuing = signal(false, ...(ngDevMode ? [{ debugName: "issuing" }] : /* istanbul ignore next */ []));
|
|
584
|
+
issued = signal(null, ...(ngDevMode ? [{ debugName: "issued" }] : /* istanbul ignore next */ []));
|
|
585
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
586
|
+
copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
587
|
+
/** Available scope choices — kept in sync with `AiCredentialTicketScope`. */
|
|
588
|
+
scopes = [
|
|
589
|
+
{
|
|
590
|
+
value: 'CredentialRegister',
|
|
591
|
+
label: 'Subscription token',
|
|
592
|
+
hint: 'For the bastion CLI to register an Anthropic OAuth pair on this tenant.',
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
value: 'DevSshKeyRegister',
|
|
596
|
+
label: 'Developer SSH key',
|
|
597
|
+
hint: 'For a developer to register their own SSH public key on this tenant.',
|
|
598
|
+
},
|
|
599
|
+
];
|
|
600
|
+
onIssue() {
|
|
601
|
+
if (this.issuing()) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const ttl = this.normalizedTtl();
|
|
605
|
+
this.issuing.set(true);
|
|
606
|
+
this.error.set(null);
|
|
607
|
+
this.copied.set(false);
|
|
608
|
+
this.issued.set(null);
|
|
609
|
+
this.client
|
|
610
|
+
.issueCredentialTicket({
|
|
611
|
+
scope: this.scope(),
|
|
612
|
+
ttlMinutes: ttl,
|
|
613
|
+
})
|
|
614
|
+
.subscribe({
|
|
615
|
+
next: (response) => {
|
|
616
|
+
this.issued.set(response);
|
|
617
|
+
this.issuing.set(false);
|
|
618
|
+
},
|
|
619
|
+
error: (err) => {
|
|
620
|
+
this.error.set(this.formatError(err));
|
|
621
|
+
this.issuing.set(false);
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
onCopy() {
|
|
626
|
+
const code = this.issued()?.code;
|
|
627
|
+
if (!code) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
// navigator.clipboard requires a secure context; if it's unavailable the
|
|
631
|
+
// operator can still select + copy by hand. Falling back silently keeps
|
|
632
|
+
// the panel usable on http://localhost dev origins.
|
|
633
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
634
|
+
navigator.clipboard.writeText(code).then(() => this.copied.set(true), () => this.copied.set(false));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
onClear() {
|
|
638
|
+
this.issued.set(null);
|
|
639
|
+
this.copied.set(false);
|
|
640
|
+
}
|
|
641
|
+
normalizedTtl() {
|
|
642
|
+
const v = Math.trunc(this.ttlMinutes());
|
|
643
|
+
// Server clamps to 60 anyway; clamp here too so the slider can't push a
|
|
644
|
+
// value the request would silently truncate.
|
|
645
|
+
if (Number.isNaN(v) || v < 1) {
|
|
646
|
+
return 1;
|
|
647
|
+
}
|
|
648
|
+
if (v > 60) {
|
|
649
|
+
return 60;
|
|
650
|
+
}
|
|
651
|
+
return v;
|
|
652
|
+
}
|
|
653
|
+
formatError(err) {
|
|
654
|
+
if (typeof err === 'object' &&
|
|
655
|
+
err !== null &&
|
|
656
|
+
'status' in err &&
|
|
657
|
+
typeof err.status === 'number') {
|
|
658
|
+
const status = err.status;
|
|
659
|
+
if (status === 401 || status === 403) {
|
|
660
|
+
return 'Not authorised — only tenant admins can issue tickets.';
|
|
661
|
+
}
|
|
662
|
+
if (status === 400) {
|
|
663
|
+
return 'Invalid request — check the scope value.';
|
|
664
|
+
}
|
|
665
|
+
return `Server returned HTTP ${status}.`;
|
|
666
|
+
}
|
|
667
|
+
return 'Failed to reach the AI Adapter — check the network.';
|
|
668
|
+
}
|
|
669
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiCredentialTicketIssueComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
670
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: AiCredentialTicketIssueComponent, isStandalone: true, selector: "mm-ai-credential-ticket-issue", ngImport: i0, template: "<section class=\"mm-ai-ticket\">\n <header class=\"mm-ai-ticket__header\">\n <div class=\"mm-ai-ticket__title\">Issue credential ticket</div>\n <div class=\"mm-ai-ticket__subtitle\">\n Mint a one-time code an operator redeems on their own machine.\n </div>\n </header>\n\n @if (!issued()) {\n <form\n class=\"mm-ai-ticket__form\"\n (ngSubmit)=\"onIssue()\"\n #issueForm=\"ngForm\"\n >\n <label class=\"mm-ai-ticket__field\">\n <span class=\"mm-ai-ticket__label\">Scope</span>\n <select\n class=\"mm-ai-ticket__select\"\n [ngModel]=\"scope()\"\n (ngModelChange)=\"scope.set($event)\"\n name=\"scope\"\n [disabled]=\"issuing()\"\n >\n @for (s of scopes; track s.value) {\n <option [value]=\"s.value\">{{ s.label }}</option>\n }\n </select>\n </label>\n <p class=\"mm-ai-ticket__hint\">\n @for (s of scopes; track s.value) {\n @if (s.value === scope()) {\n {{ s.hint }}\n }\n }\n </p>\n\n <label class=\"mm-ai-ticket__field\">\n <span class=\"mm-ai-ticket__label\">TTL (minutes)</span>\n <input\n class=\"mm-ai-ticket__input\"\n type=\"number\"\n min=\"1\"\n max=\"60\"\n step=\"1\"\n [ngModel]=\"ttlMinutes()\"\n (ngModelChange)=\"ttlMinutes.set(+$event)\"\n name=\"ttlMinutes\"\n [disabled]=\"issuing()\"\n />\n </label>\n <p class=\"mm-ai-ticket__hint\">\n Default 5 minutes. Server clamps to 60 \u2014 long TTLs leak.\n </p>\n\n <div class=\"mm-ai-ticket__actions\">\n <button\n class=\"mm-ai-ticket__button mm-ai-ticket__button--primary\"\n type=\"submit\"\n [disabled]=\"issuing()\"\n >\n @if (issuing()) {\n Issuing\u2026\n } @else {\n Issue ticket\n }\n </button>\n </div>\n\n @if (error()) {\n <p class=\"mm-ai-ticket__error\">{{ error() }}</p>\n }\n </form>\n } @else {\n <div class=\"mm-ai-ticket__result\">\n <p class=\"mm-ai-ticket__result-label\">\n One-time code (copy now \u2014 not shown again):\n </p>\n <div class=\"mm-ai-ticket__code-row\">\n <code class=\"mm-ai-ticket__code\">{{ issued()!.code }}</code>\n <button\n class=\"mm-ai-ticket__button mm-ai-ticket__button--secondary\"\n type=\"button\"\n (click)=\"onCopy()\"\n >\n @if (copied()) {\n Copied\n } @else {\n Copy\n }\n </button>\n </div>\n <dl class=\"mm-ai-ticket__meta\">\n <div>\n <dt>Scope</dt>\n <dd>{{ issued()!.scope }}</dd>\n </div>\n <div>\n <dt>Expires at</dt>\n <dd>{{ issued()!.expiresAt }}</dd>\n </div>\n <div>\n <dt>Ticket rtId</dt>\n <dd><code>{{ issued()!.rtId }}</code></dd>\n </div>\n </dl>\n <div class=\"mm-ai-ticket__actions\">\n <button\n class=\"mm-ai-ticket__button\"\n type=\"button\"\n (click)=\"onClear()\"\n >\n Issue another\n </button>\n </div>\n </div>\n }\n</section>\n", styles: [":host{display:block;--mm-ai-ticket-bg: #16191d;--mm-ai-ticket-surface: #1f2227;--mm-ai-ticket-border: #2f343b;--mm-ai-ticket-text: #e6e6e6;--mm-ai-ticket-muted: #9aa0a6;--mm-ai-ticket-accent: #64ceb9;--mm-ai-ticket-error: #ec658f;--mm-ai-ticket-code-bg: #0f1115}.mm-ai-ticket{background:var(--mm-ai-ticket-bg);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.5rem;padding:1rem 1.25rem;display:flex;flex-direction:column;gap:.85rem;font-family:inherit}.mm-ai-ticket__header{display:flex;flex-direction:column;gap:.15rem}.mm-ai-ticket__title{font-size:1rem;font-weight:600}.mm-ai-ticket__subtitle{font-size:.8rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__form,.mm-ai-ticket__result{display:flex;flex-direction:column;gap:.6rem}.mm-ai-ticket__field{display:flex;flex-direction:column;gap:.25rem}.mm-ai-ticket__label{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__select,.mm-ai-ticket__input{background:var(--mm-ai-ticket-surface);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.3rem;padding:.4rem .55rem;font-family:inherit;font-size:.9rem}.mm-ai-ticket__select:focus,.mm-ai-ticket__input:focus{outline:2px solid var(--mm-ai-ticket-accent);outline-offset:1px}.mm-ai-ticket__select:disabled,.mm-ai-ticket__input:disabled{opacity:.6;cursor:not-allowed}.mm-ai-ticket__hint{margin:0;font-size:.75rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__actions{display:flex;gap:.5rem;margin-top:.25rem}.mm-ai-ticket__button{background:var(--mm-ai-ticket-surface);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.3rem;padding:.4rem .85rem;font-family:inherit;font-size:.85rem;cursor:pointer}.mm-ai-ticket__button:hover:not(:disabled){border-color:var(--mm-ai-ticket-accent)}.mm-ai-ticket__button:disabled{opacity:.5;cursor:not-allowed}.mm-ai-ticket__button--primary{background:var(--mm-ai-ticket-accent);color:#0a0c0f;border-color:var(--mm-ai-ticket-accent)}.mm-ai-ticket__error{margin:0;font-size:.8rem;color:var(--mm-ai-ticket-error)}.mm-ai-ticket__result-label{margin:0;font-size:.85rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__code-row{display:flex;align-items:center;gap:.5rem}.mm-ai-ticket__code{flex:1;background:var(--mm-ai-ticket-code-bg);color:var(--mm-ai-ticket-accent);padding:.5rem .75rem;border-radius:.3rem;font-family:Roboto Mono,ui-monospace,monospace;font-size:1.05rem;letter-spacing:.1em;-webkit-user-select:all;user-select:all}.mm-ai-ticket__meta{display:grid;grid-template-columns:max-content 1fr;gap:.2rem .75rem;margin:0;font-size:.8rem}.mm-ai-ticket__meta>div{display:contents}.mm-ai-ticket__meta dt{color:var(--mm-ai-ticket-muted);text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.mm-ai-ticket__meta dd{margin:0;font-family:Roboto Mono,ui-monospace,monospace}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.MinValidator, selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]", inputs: ["min"] }, { kind: "directive", type: i1.MaxValidator, selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]", inputs: ["max"] }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
671
|
+
}
|
|
672
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AiCredentialTicketIssueComponent, decorators: [{
|
|
673
|
+
type: Component,
|
|
674
|
+
args: [{ selector: 'mm-ai-credential-ticket-issue', imports: [CommonModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<section class=\"mm-ai-ticket\">\n <header class=\"mm-ai-ticket__header\">\n <div class=\"mm-ai-ticket__title\">Issue credential ticket</div>\n <div class=\"mm-ai-ticket__subtitle\">\n Mint a one-time code an operator redeems on their own machine.\n </div>\n </header>\n\n @if (!issued()) {\n <form\n class=\"mm-ai-ticket__form\"\n (ngSubmit)=\"onIssue()\"\n #issueForm=\"ngForm\"\n >\n <label class=\"mm-ai-ticket__field\">\n <span class=\"mm-ai-ticket__label\">Scope</span>\n <select\n class=\"mm-ai-ticket__select\"\n [ngModel]=\"scope()\"\n (ngModelChange)=\"scope.set($event)\"\n name=\"scope\"\n [disabled]=\"issuing()\"\n >\n @for (s of scopes; track s.value) {\n <option [value]=\"s.value\">{{ s.label }}</option>\n }\n </select>\n </label>\n <p class=\"mm-ai-ticket__hint\">\n @for (s of scopes; track s.value) {\n @if (s.value === scope()) {\n {{ s.hint }}\n }\n }\n </p>\n\n <label class=\"mm-ai-ticket__field\">\n <span class=\"mm-ai-ticket__label\">TTL (minutes)</span>\n <input\n class=\"mm-ai-ticket__input\"\n type=\"number\"\n min=\"1\"\n max=\"60\"\n step=\"1\"\n [ngModel]=\"ttlMinutes()\"\n (ngModelChange)=\"ttlMinutes.set(+$event)\"\n name=\"ttlMinutes\"\n [disabled]=\"issuing()\"\n />\n </label>\n <p class=\"mm-ai-ticket__hint\">\n Default 5 minutes. Server clamps to 60 \u2014 long TTLs leak.\n </p>\n\n <div class=\"mm-ai-ticket__actions\">\n <button\n class=\"mm-ai-ticket__button mm-ai-ticket__button--primary\"\n type=\"submit\"\n [disabled]=\"issuing()\"\n >\n @if (issuing()) {\n Issuing\u2026\n } @else {\n Issue ticket\n }\n </button>\n </div>\n\n @if (error()) {\n <p class=\"mm-ai-ticket__error\">{{ error() }}</p>\n }\n </form>\n } @else {\n <div class=\"mm-ai-ticket__result\">\n <p class=\"mm-ai-ticket__result-label\">\n One-time code (copy now \u2014 not shown again):\n </p>\n <div class=\"mm-ai-ticket__code-row\">\n <code class=\"mm-ai-ticket__code\">{{ issued()!.code }}</code>\n <button\n class=\"mm-ai-ticket__button mm-ai-ticket__button--secondary\"\n type=\"button\"\n (click)=\"onCopy()\"\n >\n @if (copied()) {\n Copied\n } @else {\n Copy\n }\n </button>\n </div>\n <dl class=\"mm-ai-ticket__meta\">\n <div>\n <dt>Scope</dt>\n <dd>{{ issued()!.scope }}</dd>\n </div>\n <div>\n <dt>Expires at</dt>\n <dd>{{ issued()!.expiresAt }}</dd>\n </div>\n <div>\n <dt>Ticket rtId</dt>\n <dd><code>{{ issued()!.rtId }}</code></dd>\n </div>\n </dl>\n <div class=\"mm-ai-ticket__actions\">\n <button\n class=\"mm-ai-ticket__button\"\n type=\"button\"\n (click)=\"onClear()\"\n >\n Issue another\n </button>\n </div>\n </div>\n }\n</section>\n", styles: [":host{display:block;--mm-ai-ticket-bg: #16191d;--mm-ai-ticket-surface: #1f2227;--mm-ai-ticket-border: #2f343b;--mm-ai-ticket-text: #e6e6e6;--mm-ai-ticket-muted: #9aa0a6;--mm-ai-ticket-accent: #64ceb9;--mm-ai-ticket-error: #ec658f;--mm-ai-ticket-code-bg: #0f1115}.mm-ai-ticket{background:var(--mm-ai-ticket-bg);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.5rem;padding:1rem 1.25rem;display:flex;flex-direction:column;gap:.85rem;font-family:inherit}.mm-ai-ticket__header{display:flex;flex-direction:column;gap:.15rem}.mm-ai-ticket__title{font-size:1rem;font-weight:600}.mm-ai-ticket__subtitle{font-size:.8rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__form,.mm-ai-ticket__result{display:flex;flex-direction:column;gap:.6rem}.mm-ai-ticket__field{display:flex;flex-direction:column;gap:.25rem}.mm-ai-ticket__label{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__select,.mm-ai-ticket__input{background:var(--mm-ai-ticket-surface);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.3rem;padding:.4rem .55rem;font-family:inherit;font-size:.9rem}.mm-ai-ticket__select:focus,.mm-ai-ticket__input:focus{outline:2px solid var(--mm-ai-ticket-accent);outline-offset:1px}.mm-ai-ticket__select:disabled,.mm-ai-ticket__input:disabled{opacity:.6;cursor:not-allowed}.mm-ai-ticket__hint{margin:0;font-size:.75rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__actions{display:flex;gap:.5rem;margin-top:.25rem}.mm-ai-ticket__button{background:var(--mm-ai-ticket-surface);color:var(--mm-ai-ticket-text);border:1px solid var(--mm-ai-ticket-border);border-radius:.3rem;padding:.4rem .85rem;font-family:inherit;font-size:.85rem;cursor:pointer}.mm-ai-ticket__button:hover:not(:disabled){border-color:var(--mm-ai-ticket-accent)}.mm-ai-ticket__button:disabled{opacity:.5;cursor:not-allowed}.mm-ai-ticket__button--primary{background:var(--mm-ai-ticket-accent);color:#0a0c0f;border-color:var(--mm-ai-ticket-accent)}.mm-ai-ticket__error{margin:0;font-size:.8rem;color:var(--mm-ai-ticket-error)}.mm-ai-ticket__result-label{margin:0;font-size:.85rem;color:var(--mm-ai-ticket-muted)}.mm-ai-ticket__code-row{display:flex;align-items:center;gap:.5rem}.mm-ai-ticket__code{flex:1;background:var(--mm-ai-ticket-code-bg);color:var(--mm-ai-ticket-accent);padding:.5rem .75rem;border-radius:.3rem;font-family:Roboto Mono,ui-monospace,monospace;font-size:1.05rem;letter-spacing:.1em;-webkit-user-select:all;user-select:all}.mm-ai-ticket__meta{display:grid;grid-template-columns:max-content 1fr;gap:.2rem .75rem;margin:0;font-size:.8rem}.mm-ai-ticket__meta>div{display:contents}.mm-ai-ticket__meta dt{color:var(--mm-ai-ticket-muted);text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.mm-ai-ticket__meta dd{margin:0;font-family:Roboto Mono,ui-monospace,monospace}\n"] }]
|
|
675
|
+
}] });
|
|
676
|
+
|
|
677
|
+
/*
|
|
678
|
+
* Public API Surface of @meshmakers/octo-ai-console
|
|
679
|
+
*
|
|
680
|
+
* Reusable Angular building blocks for the OctoMesh AI Adapter session console
|
|
681
|
+
* (ADR-21, issue #4121). Host applications import individual standalone
|
|
682
|
+
* components or call `provideOctoAiConsole({...})` to wire the services.
|
|
683
|
+
*/
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Generated bundle index. Do not edit.
|
|
687
|
+
*/
|
|
688
|
+
|
|
689
|
+
export { AI_ADAPTER_OPTIONS, AI_SESSION_STREAM_CONNECTION_FACTORY, AiAdapterClientService, AiApprovalModalComponent, AiChatStreamComponent, AiCredentialTicketIssueComponent, AiJobStatusBadgeComponent, AiQuotaIndicatorComponent, AiSessionListComponent, AiSessionStreamService, AiToolCallComponent, provideOctoAiConsole };
|
|
690
|
+
//# sourceMappingURL=meshmakers-octo-ai-console.mjs.map
|