@nauticana/sail 1.1.33 → 1.1.34
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 +17 -11
- package/package.json +1 -1
- package/src/agent/component/approval_inbox.html +63 -0
- package/src/agent/component/approval_inbox.ts +91 -0
- package/src/agent/component/decision_timeline.html +4 -4
- package/src/agent/component/decision_timeline.ts +2 -0
- package/src/agent/component/prompt_section_editor.html +4 -3
- package/src/agent/component/prompt_section_editor.ts +12 -5
- package/src/agent/component/release_sections.html +1 -1
- package/src/agent/component/release_sections.ts +2 -1
- package/src/agent/component/studio_page.html +269 -0
- package/src/agent/component/studio_page.ts +346 -0
- package/src/agent/component/studio_tab.ts +14 -0
- package/src/agent/component/turn_event_list.html +9 -0
- package/src/agent/index.ts +6 -0
- package/src/agent/model/agent_studio.ts +122 -9
- package/src/agent/model/approval.ts +42 -0
- package/src/agent/model/conversation.ts +9 -1
- package/src/agent/model/decision.ts +2 -1
- package/src/agent/model/turn_event.ts +10 -0
- package/src/agent/service/agent_studio.service.ts +36 -1
- package/src/agent/service/approval.service.ts +18 -0
- package/src/agent/util/release_diff.ts +33 -0
- package/src/agent/util/studio_errors.ts +11 -0
- package/src/agent/util/turn_timeline.ts +7 -1
- package/src/component/login/account_deletion.html +17 -4
- package/src/component/login/account_deletion.ts +35 -28
- package/src/component/login/social_login.ts +6 -5
- package/src/index.ts +1 -0
- package/src/model/auth.ts +17 -2
- package/src/service/auth.service.ts +10 -4
- package/src/service/caption_pipe.ts +26 -0
- package/src/service/label.service.ts +5 -4
- package/src/service/rest_url.ts +1 -0
package/README.md
CHANGED
|
@@ -264,7 +264,7 @@ Default page size is 100, capped at 1000 server-side. Sail maps `limit`, `offset
|
|
|
264
264
|
|
|
265
265
|
## Backend-driven constant captions
|
|
266
266
|
|
|
267
|
-
`LabelService` loads one
|
|
267
|
+
`LabelService` loads one caption map from the backend-provided `ApplicationData`: a keel `constant_value` domain from `ConstantCache` by constant id, or a catalog table registered in `foreign_key_lookup` from `TableCache` by table name. `sailCaption` is the pipe form: `{{ code | sailCaption:'constant_id' }}`. The domain is the backend `constant_id` (for example `ride_type`), not a locale: keel's constant catalogue does not contain a language column. It does not call generic `constant_value` CRUD, which is normally restricted to administrative roles.
|
|
268
268
|
|
|
269
269
|
```typescript
|
|
270
270
|
this.labels.loadLabels('ride_type').subscribe(() => {
|
|
@@ -684,7 +684,7 @@ A complete screen is the pieces above assembled over `BillingService` — no met
|
|
|
684
684
|
|
|
685
685
|
## Agent UI (v1.1.20)
|
|
686
686
|
|
|
687
|
-
Agent Studio and agent-conversation UI live in a secondary entry point, `@nauticana/sail/agent`. Nothing in it is exported from `@nauticana/sail`, so an app that never imports it bundles none of it. It needs no additional peer dependency.
|
|
687
|
+
Agent Studio and agent-conversation UI live in a secondary entry point, `@nauticana/sail/agent`. Nothing in it is exported from `@nauticana/sail`, so an app that never imports it bundles none of it. It needs no additional peer dependency. The wire types match Scout v1.2.8.
|
|
688
688
|
|
|
689
689
|
```ts
|
|
690
690
|
import { AgentStudioService, PromptLanguageEditorComponent, TurnStream } from '@nauticana/sail/agent';
|
|
@@ -692,7 +692,10 @@ import { AgentStudioService, PromptLanguageEditorComponent, TurnStream } from '@
|
|
|
692
692
|
|
|
693
693
|
| Export | Purpose |
|
|
694
694
|
|---|---|
|
|
695
|
-
| `AgentStudioService` | Client for
|
|
695
|
+
| `AgentStudioService` | Client for Scout's core Studio routes under `/api/agent-studio/` (fixed, unversioned paths): the `features` manifest, list, draft, settings, create, rename, archive, enable, test, publish, restore, reset, default, history, audit, release sections, models. Models mirror the backend's JSON names. |
|
|
696
|
+
| `sail-studio-page` | Embeddable Studio: an agent rail grouped by `[groupBy]` (agent type by default; pass a function for workflow or position), and Overview, Prompts, Settings (scoped bindings and what they resolve to), Test, and History (diff, restore, audit) tabs. Product tabs are `<ng-template sailStudioTab="id" label="Label" let-agent>`. A control shows only when the `agents` feature lists its action and the caller holds the `AGENT_STUDIO` grant at `agent:<name>` (`agent_type:<type>` for create); hiding is never the authorization. `hasUnsavedChanges()` serves a `canDeactivate` guard. |
|
|
697
|
+
| `sail-approval-inbox` / `ApprovalService` | The signed-in person's pending approvals (`GET /api/approval/pending`) and a decision view: approve, reject, or edit a tool call's arguments (`POST /api/approval/resolve`). A `409` reloads the list; `(decided)` emits the result. |
|
|
698
|
+
| `studioErrorMessage(err)` | One message for a failed Studio call: the conflict cause, the field errors, or the detail. |
|
|
696
699
|
| `studioConflictCause(err)` | `'revision'` (reload the draft), `'sealed'` (a layer sits under a sealed one) or `'other'` for a `409`. |
|
|
697
700
|
| `studioFieldErrors(err)` / `retryAfterSeconds(err)` | Field errors of a `400`; `Retry-After` of a `429`/`503`. |
|
|
698
701
|
| `sail-prompt-language-editor` | One language's prompt sections. Emits the whole language on every edit — a save must send every layer back, because an editable layer missing from the request ends its binding. Emits `resetRequested` with the scope, section, and language to reset. |
|
|
@@ -705,7 +708,7 @@ import { AgentStudioService, PromptLanguageEditorComponent, TurnStream } from '@
|
|
|
705
708
|
| `ConversationService` | Transport for Scout's conversation bridge: `submit`, `stream(requestId, turnStream)`, `cancel`. Mount point injected through `CONVERSATION_BASE_PATH`. |
|
|
706
709
|
| `SseParser` | Incremental `text/event-stream` parser: `push(chunk)` returns whole events out of chunks that split anywhere, including between the CR and LF of a CRLF; `finish()` flushes what the closing body left pending. |
|
|
707
710
|
|
|
708
|
-
Map your audit rows onto `DecisionRecord` to feed the timeline. Style the `.prompt-*`, `.release-*`, `.turn-*`, and `.
|
|
711
|
+
Map your audit rows onto `DecisionRecord` (outcome `A`, `D`, or `P`, captioned from `audit_decision_outcome`) to feed the timeline. Style the `.prompt-*`, `.release-*`, `.turn-*`, `.decision-*`, `.studio__*`, and `.approval-inbox__*` class hooks. Scout's short codes render through `sailCaption`, a pipe over the backend's constant captions.
|
|
709
712
|
|
|
710
713
|
### Conversation transport (v1.1.22)
|
|
711
714
|
|
|
@@ -719,7 +722,7 @@ providers: [{ provide: CONVERSATION_BASE_PATH, useValue: '/api/wingmate/' }]
|
|
|
719
722
|
|
|
720
723
|
```ts
|
|
721
724
|
const stream = new TurnStream(requestId);
|
|
722
|
-
this.conversation.submit({ request_id: requestId, conversation_id: conversationId,
|
|
725
|
+
this.conversation.submit({ request_id: requestId, conversation_id: conversationId, agent_type: 'wingmate', input })
|
|
723
726
|
.pipe(switchMap(() => this.conversation.stream(requestId, stream)), takeUntilDestroyed())
|
|
724
727
|
.subscribe({ error: (err) => this.onStreamError(err) });
|
|
725
728
|
```
|
|
@@ -1016,7 +1019,7 @@ Config required in `SAIL_GUI_CONFIG`:
|
|
|
1016
1019
|
}
|
|
1017
1020
|
```
|
|
1018
1021
|
|
|
1019
|
-
Under the hood, the component calls `BaseAuthService.loginSocial(provider, idToken, consent)` → `POST /public/login/social` and emits `loginSuccess: LoginResponseSocial`. The session is completed automatically (token stored, app data loaded, routes initialized).
|
|
1022
|
+
Under the hood, the component calls `BaseAuthService.loginSocial(provider, idToken, consent)` → `POST /public/login/social` and emits `loginSuccess: LoginResponseSocial`. Apple sends the user's name only on the first authorization and never in the ID token, so that sign-in carries it as `socialName: {firstName?, lastName?}` for the app to pre-fill its profile. The session is completed automatically (token stored, app data loaded, routes initialized).
|
|
1020
1023
|
|
|
1021
1024
|
For backward compatibility, the older OAuth-code flow `BaseAuthService.loginWithGoogle(code)` (hits `/public/login/google`) is still supported; prefer `loginSocial` for new code.
|
|
1022
1025
|
|
|
@@ -1383,14 +1386,16 @@ These are App Store / Play Store compliance primitives from keel. All are opt-in
|
|
|
1383
1386
|
|
|
1384
1387
|
### Re-authentication gate
|
|
1385
1388
|
|
|
1386
|
-
The four sensitive endpoints below require **recent re-authentication** before they will run — pass `password`
|
|
1389
|
+
The four sensitive endpoints below require **recent re-authentication** before they will run — pass `password`, `twoFactorCode` or `reauthCode` to prove the user is still present at the keyboard. The shipped components already capture this; only callers using the service methods directly need to do this.
|
|
1387
1390
|
|
|
1388
1391
|
| Method | Required re-auth |
|
|
1389
1392
|
|--------|-----------------|
|
|
1390
1393
|
| `setup2FA(reauth)` | `password` (or `twoFactorCode` when re-rotating) |
|
|
1391
1394
|
| `disable2FA(password, code)` | **both** `password` and current TOTP `code` |
|
|
1392
|
-
| `deleteAccount(reauth, reason?)` | `password`
|
|
1393
|
-
| `logoutEverywhere(reauth)` | `password`
|
|
1395
|
+
| `deleteAccount(reauth, reason?)` | `password`, `twoFactorCode` or `reauthCode` |
|
|
1396
|
+
| `logoutEverywhere(reauth)` | `password`, `twoFactorCode` or `reauthCode` |
|
|
1397
|
+
|
|
1398
|
+
An account without a password re-authenticates with `reauthCode`, a one-time code sent by `sendReauthCode(channel?)` (`POST /api/user/reauth/send`; no channel prefers email).
|
|
1394
1399
|
|
|
1395
1400
|
The `ReauthCredentials` type is exported as a shared shape:
|
|
1396
1401
|
```typescript
|
|
@@ -1402,10 +1407,10 @@ auth.deleteAccount(reauth, 'Closing my account.').subscribe();
|
|
|
1402
1407
|
### Account deletion
|
|
1403
1408
|
|
|
1404
1409
|
```html
|
|
1405
|
-
<sail-account-deletion [confirmationText]="'DELETE'"></sail-account-deletion>
|
|
1410
|
+
<sail-account-deletion [confirmationText]="'DELETE'" reauthMethod="code"></sail-account-deletion>
|
|
1406
1411
|
```
|
|
1407
1412
|
|
|
1408
|
-
Typed-confirmation UX: the destructive button is disabled until the user types the exact `confirmationText` (default `DELETE`) **and**
|
|
1413
|
+
Typed-confirmation UX: the destructive button is disabled until the user types the exact `confirmationText` (default `DELETE`) **and** re-authenticates. `reauthMethod` (`password` default, or `code`) picks the initial method; the user can switch, and the code path sends, resends and submits a `reauthCode`. Optional reason textarea is forwarded to the backend. On success, local session is cleared and the router navigates to `config.accountDeletedRoute` (default `/login/local`).
|
|
1409
1414
|
|
|
1410
1415
|
### Logout everywhere
|
|
1411
1416
|
|
|
@@ -1433,6 +1438,7 @@ Platform codes: `'I'` iOS, `'A'` Android, `'W'` Web. The token acquisition (Capa
|
|
|
1433
1438
|
|----------|---------------|
|
|
1434
1439
|
| `DELETE /api/user/account` | `deleteAccount(reauth, reason?)` |
|
|
1435
1440
|
| `POST /api/user/logout-everywhere` | `logoutEverywhere(reauth)` |
|
|
1441
|
+
| `POST /api/user/reauth/send` | `sendReauthCode(channel?)` |
|
|
1436
1442
|
| `POST /api/push/register` | `registerPushToken(platform, token, appVersion?, deviceModel?)` |
|
|
1437
1443
|
| `POST /api/push/revoke` | `revokePushToken(token)` |
|
|
1438
1444
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.34",
|
|
4
4
|
"description": "Metadata-driven Angular component library for CRUD admin frontends backed by the keel Go backend. Tables, forms, navigation, OTP/2FA/social auth, billing, payout onboarding.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<div class="approval-inbox">
|
|
2
|
+
<section class="approval-inbox__list" aria-label="Pending approvals">
|
|
3
|
+
@if (requests().length === 0 && !loading()) {
|
|
4
|
+
<p class="approval-inbox__empty">Nothing is waiting for your decision.</p>
|
|
5
|
+
}
|
|
6
|
+
<ul>
|
|
7
|
+
@for (request of requests(); track key(request)) {
|
|
8
|
+
<li>
|
|
9
|
+
<button type="button" class="approval-inbox__item" [class.selected]="key(request) === selectedKey()"
|
|
10
|
+
[attr.aria-current]="key(request) === selectedKey()" (click)="select(request)">
|
|
11
|
+
<span class="approval-inbox__summary">{{ request.summary || request.action }}</span>
|
|
12
|
+
@if (request.deadline_at) {
|
|
13
|
+
<span class="approval-inbox__deadline">Due {{ request.deadline_at | date: 'medium' }}</span>
|
|
14
|
+
}
|
|
15
|
+
</button>
|
|
16
|
+
</li>
|
|
17
|
+
}
|
|
18
|
+
</ul>
|
|
19
|
+
<button matButton type="button" [disabled]="loading()" (click)="load()">Refresh</button>
|
|
20
|
+
</section>
|
|
21
|
+
|
|
22
|
+
@if (selected(); as request) {
|
|
23
|
+
<section class="approval-inbox__decision" aria-label="Decision">
|
|
24
|
+
<h3>{{ request.summary || request.action }}</h3>
|
|
25
|
+
<dl>
|
|
26
|
+
<dt>Action</dt><dd>{{ request.action }}</dd>
|
|
27
|
+
<dt>Resource</dt><dd>{{ request.resource }}</dd>
|
|
28
|
+
<dt>Effect</dt><dd>{{ request.output_class | sailCaption:'agent_output_class' }}</dd>
|
|
29
|
+
<dt>Risk tier</dt><dd>{{ request.risk_tier }}</dd>
|
|
30
|
+
<dt>Required by</dt><dd>{{ request.rule_source | sailCaption:'approval_rule_source' }} · {{ request.rule_id }}</dd>
|
|
31
|
+
@if (request.approver_reference; as approver) {
|
|
32
|
+
<dt>Assigned to</dt><dd>{{ approver.kind | sailCaption:'approval_approver_kind' }} {{ approver.ref }} (step {{ request.route_step }})</dd>
|
|
33
|
+
}
|
|
34
|
+
@if (request.deadline_at) {
|
|
35
|
+
<dt>Deadline</dt><dd>{{ request.deadline_at | date: 'medium' }}</dd>
|
|
36
|
+
}
|
|
37
|
+
@if (request.revises_step_id) {
|
|
38
|
+
<dt>Revises</dt><dd>step {{ request.revises_step_id }}</dd>
|
|
39
|
+
}
|
|
40
|
+
<dt>Requested</dt><dd>{{ request.created_at | date: 'medium' }}</dd>
|
|
41
|
+
</dl>
|
|
42
|
+
|
|
43
|
+
<mat-form-field>
|
|
44
|
+
<mat-label>Reason (optional)</mat-label>
|
|
45
|
+
<textarea matInput rows="2" [value]="reason()" (input)="reason.set($any($event.target).value)"></textarea>
|
|
46
|
+
</mat-form-field>
|
|
47
|
+
<mat-form-field>
|
|
48
|
+
<mat-label>Revised tool arguments (JSON object, for Edit)</mat-label>
|
|
49
|
+
<textarea matInput rows="4" [value]="revisedArguments()" (input)="revisedArguments.set($any($event.target).value)"></textarea>
|
|
50
|
+
</mat-form-field>
|
|
51
|
+
|
|
52
|
+
<div class="approval-inbox__actions">
|
|
53
|
+
<button matButton="elevated" type="button" [disabled]="loading()" (click)="decide('A')">Approve</button>
|
|
54
|
+
<button matButton type="button" class="warn" [disabled]="loading()" (click)="decide('X')">Reject</button>
|
|
55
|
+
<button matButton type="button" [disabled]="loading() || !revisedArguments().trim()" (click)="decide('E')">Edit and run</button>
|
|
56
|
+
</div>
|
|
57
|
+
</section>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@if (errorMessage()) {
|
|
61
|
+
<p class="approval-inbox__error" role="alert">{{ errorMessage() }}</p>
|
|
62
|
+
}
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DatePipe } from '@angular/common';
|
|
2
|
+
import { ChangeDetectionStrategy, Component, OnInit, computed, inject, output, signal } from '@angular/core';
|
|
3
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
4
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
5
|
+
import { MatInputModule } from '@angular/material/input';
|
|
6
|
+
import { BaseAsync } from '../../component/abstract/base_async';
|
|
7
|
+
import { CaptionPipe } from '../../service/caption_pipe';
|
|
8
|
+
import { ApprovalRequest, ApprovalVerdict } from '../model/approval';
|
|
9
|
+
import { ApprovalService } from '../service/approval.service';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The signed-in person's pending approvals and a decision view: approve, reject, or edit a
|
|
13
|
+
* tool call's arguments. A 409 (reassigned or decided elsewhere) reloads the list.
|
|
14
|
+
* Selector: <sail-approval-inbox>.
|
|
15
|
+
*/
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'sail-approval-inbox',
|
|
18
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
19
|
+
imports: [DatePipe, MatButtonModule, MatFormFieldModule, MatInputModule, CaptionPipe],
|
|
20
|
+
templateUrl: './approval_inbox.html',
|
|
21
|
+
})
|
|
22
|
+
export class ApprovalInboxComponent extends BaseAsync implements OnInit {
|
|
23
|
+
private readonly approvals = inject(ApprovalService);
|
|
24
|
+
|
|
25
|
+
readonly decided = output<ApprovalRequest>();
|
|
26
|
+
|
|
27
|
+
readonly requests = signal<ApprovalRequest[]>([]);
|
|
28
|
+
readonly selectedKey = signal('');
|
|
29
|
+
readonly reason = signal('');
|
|
30
|
+
readonly revisedArguments = signal('');
|
|
31
|
+
readonly selected = computed(() => this.requests().find((r) => this.key(r) === this.selectedKey()));
|
|
32
|
+
|
|
33
|
+
ngOnInit(): void {
|
|
34
|
+
this.load();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
load(): void {
|
|
38
|
+
this.run(this.approvals.pending(), (rows) => {
|
|
39
|
+
this.requests.set(rows);
|
|
40
|
+
if (!this.selected()) this.select(rows[0]);
|
|
41
|
+
}, 'Failed to load approvals.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
select(request: ApprovalRequest | undefined): void {
|
|
45
|
+
this.selectedKey.set(request ? this.key(request) : '');
|
|
46
|
+
this.reason.set('');
|
|
47
|
+
this.revisedArguments.set('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
decide(verdict: ApprovalVerdict): void {
|
|
51
|
+
const request = this.selected();
|
|
52
|
+
if (!request) return;
|
|
53
|
+
let revised: Record<string, unknown> | undefined;
|
|
54
|
+
if (verdict === 'E') {
|
|
55
|
+
revised = parseObject(this.revisedArguments());
|
|
56
|
+
if (!revised) {
|
|
57
|
+
this.errorMessage.set('Revised arguments must be a JSON object.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.run(this.approvals.resolve({
|
|
62
|
+
request_id: request.request_id,
|
|
63
|
+
execution_step_id: request.execution_step_id,
|
|
64
|
+
verdict,
|
|
65
|
+
proposed_digest: request.proposed_digest,
|
|
66
|
+
reason: this.reason().trim() || undefined,
|
|
67
|
+
revised_arguments: revised,
|
|
68
|
+
}), (result) => {
|
|
69
|
+
this.requests.update((rows) => rows.filter((r) => this.key(r) !== this.key(request)));
|
|
70
|
+
this.select(this.requests()[0]);
|
|
71
|
+
this.decided.emit(result);
|
|
72
|
+
}, 'Failed to record the decision.', (err) => {
|
|
73
|
+
if ((err as { status?: number })?.status !== 409) return;
|
|
74
|
+
this.load();
|
|
75
|
+
this.errorMessage.set('This request was reassigned or decided elsewhere; the list was refreshed.');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
key(request: ApprovalRequest): string {
|
|
80
|
+
return `${request.request_id}#${request.execution_step_id}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseObject(text: string): Record<string, unknown> | undefined {
|
|
85
|
+
try {
|
|
86
|
+
const value: unknown = JSON.parse(text);
|
|
87
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
@for (record of group.records; track record.id) {
|
|
6
6
|
<li
|
|
7
7
|
class="decision-record"
|
|
8
|
-
[class.decision-record--allow]="record.outcome === '
|
|
9
|
-
[class.decision-record--deny]="record.outcome === '
|
|
10
|
-
[class.decision-record--pending]="record.outcome === '
|
|
8
|
+
[class.decision-record--allow]="record.outcome === 'A'"
|
|
9
|
+
[class.decision-record--deny]="record.outcome === 'D'"
|
|
10
|
+
[class.decision-record--pending]="record.outcome === 'P'"
|
|
11
11
|
>
|
|
12
12
|
<time class="decision-record__time" [attr.datetime]="record.occurred_at">{{ record.occurred_at }}</time>
|
|
13
13
|
<span class="decision-record__action">{{ record.action }}</span>
|
|
14
14
|
<span class="decision-record__resource">{{ record.resource }}</span>
|
|
15
|
-
<span class="decision-record__outcome">{{ record.outcome }}</span>
|
|
15
|
+
<span class="decision-record__outcome">{{ record.outcome | sailCaption:'audit_decision_outcome' }}</span>
|
|
16
16
|
<span class="decision-record__principal">{{ record.principal.kind }} {{ record.principal.id }}</span>
|
|
17
17
|
@if (record.reason) {
|
|
18
18
|
<p class="decision-record__reason">{{ record.reason }}</p>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
|
+
import { CaptionPipe } from '../../service/caption_pipe';
|
|
2
3
|
import { DecisionCategoryGroup, DecisionRecord } from '../model/decision';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -8,6 +9,7 @@ import { DecisionCategoryGroup, DecisionRecord } from '../model/decision';
|
|
|
8
9
|
@Component({
|
|
9
10
|
selector: 'sail-decision-timeline',
|
|
10
11
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
12
|
+
imports: [CaptionPipe],
|
|
11
13
|
templateUrl: './decision_timeline.html',
|
|
12
14
|
})
|
|
13
15
|
export class DecisionTimelineComponent {
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
<mat-form-field>
|
|
38
38
|
<mat-label>Merge mode</mat-label>
|
|
39
39
|
<mat-select formControlName="merge_mode">
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
@for (mode of mergeModes(); track mode.code) {
|
|
41
|
+
<mat-option [value]="mode.code">{{ mode.caption }}</mat-option>
|
|
42
|
+
}
|
|
42
43
|
</mat-select>
|
|
43
44
|
</mat-form-field>
|
|
44
45
|
<mat-form-field>
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
} @else {
|
|
65
66
|
<dl class="prompt-layer__readonly">
|
|
66
67
|
<dt>Merge mode</dt>
|
|
67
|
-
<dd>{{ row.layer.merge_mode }}</dd>
|
|
68
|
+
<dd>{{ row.layer.merge_mode | sailCaption:'config_merge_mode' }}</dd>
|
|
68
69
|
<dt>Instruction</dt>
|
|
69
70
|
<dd class="prompt-layer__text">{{ row.layer.instruction }}</dd>
|
|
70
71
|
<dt>Output</dt>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, effect, input, output, signal, untracked } from '@angular/core';
|
|
2
|
-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
1
|
+
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output, signal, untracked } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
3
3
|
import { FormArray, FormControl, FormGroup, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
|
|
4
4
|
import { MatButtonModule } from '@angular/material/button';
|
|
5
5
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
@@ -7,6 +7,9 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|
|
7
7
|
import { MatIconModule } from '@angular/material/icon';
|
|
8
8
|
import { MatInputModule } from '@angular/material/input';
|
|
9
9
|
import { MatSelectModule } from '@angular/material/select';
|
|
10
|
+
import { map } from 'rxjs';
|
|
11
|
+
import { CaptionPipe } from '../../service/caption_pipe';
|
|
12
|
+
import { LabelOption, LabelService } from '../../service/label.service';
|
|
10
13
|
import { AgentMergeMode, AgentPromptLayer, AgentPromptSection } from '../model/agent_studio';
|
|
11
14
|
|
|
12
15
|
type LayerForm = FormGroup<{
|
|
@@ -25,7 +28,7 @@ export type EditableLayerSlot = 'type' | 'agent';
|
|
|
25
28
|
|
|
26
29
|
function layerContentRequired(group: LayerForm): ValidationErrors | null {
|
|
27
30
|
const { instruction, output, merge_mode } = group.getRawValue();
|
|
28
|
-
if (merge_mode === '
|
|
31
|
+
if (merge_mode === 'R') return instruction.trim() ? null : { instructionRequired: true };
|
|
29
32
|
return instruction.trim() || output.trim() ? null : { contentRequired: true };
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -37,7 +40,7 @@ function layerContentRequired(group: LayerForm): ValidationErrors | null {
|
|
|
37
40
|
@Component({
|
|
38
41
|
selector: 'sail-prompt-section-editor',
|
|
39
42
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
40
|
-
imports: [ReactiveFormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule],
|
|
43
|
+
imports: [ReactiveFormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatIconModule, MatInputModule, MatSelectModule, CaptionPipe],
|
|
41
44
|
templateUrl: './prompt_section_editor.html',
|
|
42
45
|
})
|
|
43
46
|
export class PromptSectionEditorComponent {
|
|
@@ -49,6 +52,10 @@ export class PromptSectionEditorComponent {
|
|
|
49
52
|
readonly sectionChange = output<AgentPromptSection>();
|
|
50
53
|
readonly validChange = output<boolean>();
|
|
51
54
|
|
|
55
|
+
private readonly labels = inject(LabelService);
|
|
56
|
+
/** A prompt layer appends or replaces; intersect applies to other settings only. */
|
|
57
|
+
protected readonly mergeModes = toSignal(this.labels.loadLabels('config_merge_mode').pipe(
|
|
58
|
+
map(() => this.labels.options('config_merge_mode').filter((o) => o.code !== 'I'))), { initialValue: [] as LabelOption[] });
|
|
52
59
|
protected readonly forms = new FormArray<LayerForm>([]);
|
|
53
60
|
protected readonly rows = signal<LayerRow[]>([]);
|
|
54
61
|
private emitted?: AgentPromptSection;
|
|
@@ -95,7 +102,7 @@ export class PromptSectionEditorComponent {
|
|
|
95
102
|
const layer: AgentPromptLayer = {
|
|
96
103
|
scope_id: slot === 'type' ? this.typeScopeId() : this.agentScopeId(),
|
|
97
104
|
scope_kind: '',
|
|
98
|
-
merge_mode: '
|
|
105
|
+
merge_mode: 'A',
|
|
99
106
|
sealed: false,
|
|
100
107
|
editable: true,
|
|
101
108
|
instruction: '',
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<h4 class="release-section__caption">{{ section.caption }}</h4>
|
|
7
7
|
<p class="release-section__source">
|
|
8
8
|
Decided by {{ section.source.scope_kind }} scope {{ section.source.scope_id }}
|
|
9
|
-
({{ section.source.merge_mode }})
|
|
9
|
+
({{ section.source.merge_mode | sailCaption:'config_merge_mode' }})
|
|
10
10
|
@if (section.source.sealed) {
|
|
11
11
|
<span class="release-section__sealed">
|
|
12
12
|
<mat-icon aria-hidden="true">lock</mat-icon>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
|
2
2
|
import { MatIconModule } from '@angular/material/icon';
|
|
3
|
+
import { CaptionPipe } from '../../service/caption_pipe';
|
|
3
4
|
import { AgentReleaseSection } from '../model/agent_studio';
|
|
4
5
|
|
|
5
6
|
interface ReleaseLanguageGroup {
|
|
@@ -14,7 +15,7 @@ interface ReleaseLanguageGroup {
|
|
|
14
15
|
@Component({
|
|
15
16
|
selector: 'sail-release-sections',
|
|
16
17
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
17
|
-
imports: [MatIconModule],
|
|
18
|
+
imports: [MatIconModule, CaptionPipe],
|
|
18
19
|
templateUrl: './release_sections.html',
|
|
19
20
|
})
|
|
20
21
|
export class ReleaseSectionsComponent {
|