create-safest-tools 0.2.0
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/LICENSE +201 -0
- package/README.md +71 -0
- package/bin/create-safest-tools.mjs +12 -0
- package/package.json +34 -0
- package/src/cli.mjs +128 -0
- package/src/config.mjs +151 -0
- package/src/scaffold.mjs +30 -0
- package/template/LICENSE +201 -0
- package/template/README.md +35 -0
- package/template/console/README.md +28 -0
- package/template/console/analytics/AnalyticsWorkspace.tsx +55 -0
- package/template/console/analytics/controller.ts +133 -0
- package/template/console/analytics/events.ts +33 -0
- package/template/console/analytics/store.ts +33 -0
- package/template/console/analytics/types.ts +38 -0
- package/template/console/appeals/AppealWorkspace.tsx +73 -0
- package/template/console/appeals/controller.ts +125 -0
- package/template/console/appeals/events.ts +33 -0
- package/template/console/appeals/store.ts +35 -0
- package/template/console/appeals/types.ts +37 -0
- package/template/console/assistant/AssistantWorkspace.tsx +133 -0
- package/template/console/assistant/types.ts +34 -0
- package/template/console/auth/AccountJourney.tsx +69 -0
- package/template/console/auth/InvitationAcceptance.tsx +145 -0
- package/template/console/auth/OAuthButtons.tsx +78 -0
- package/template/console/auth/OperatorLogin.tsx +110 -0
- package/template/console/auth/PasswordRecovery.tsx +132 -0
- package/template/console/auth/account-route.ts +15 -0
- package/template/console/auth/browser-navigation.ts +7 -0
- package/template/console/auth/events.ts +15 -0
- package/template/console/command/CommandCentre.tsx +119 -0
- package/template/console/command/controller.ts +267 -0
- package/template/console/command/events.ts +19 -0
- package/template/console/command/store.ts +36 -0
- package/template/console/command/types.ts +61 -0
- package/template/console/components/AnalystIdentity.tsx +25 -0
- package/template/console/components/PageHeader.tsx +27 -0
- package/template/console/configuration/ConfigurationDialog.tsx +200 -0
- package/template/console/configuration/ConfigurationWorkspace.tsx +66 -0
- package/template/console/configuration/FormBuilder.tsx +143 -0
- package/template/console/configuration/api.ts +40 -0
- package/template/console/configuration/events.ts +14 -0
- package/template/console/configuration/types.ts +79 -0
- package/template/console/lib/format.ts +49 -0
- package/template/console/lib/http.ts +96 -0
- package/template/console/main.tsx +225 -0
- package/template/console/operations/OperationsWorkspace.tsx +126 -0
- package/template/console/operations/controller.ts +237 -0
- package/template/console/operations/events.ts +37 -0
- package/template/console/operations/store.ts +32 -0
- package/template/console/operations/types.ts +107 -0
- package/template/console/people/PeopleWorkspace.tsx +81 -0
- package/template/console/people/types.ts +25 -0
- package/template/console/profile/ProfileWorkspace.tsx +136 -0
- package/template/console/profile/events.ts +9 -0
- package/template/console/profile/types.ts +6 -0
- package/template/console/quality/QualityWorkspace.tsx +87 -0
- package/template/console/quality/controller.ts +154 -0
- package/template/console/quality/events.ts +34 -0
- package/template/console/quality/store.ts +36 -0
- package/template/console/quality/types.ts +70 -0
- package/template/console/queues/QueueEditor.tsx +173 -0
- package/template/console/queues/QueueWorkspace.tsx +109 -0
- package/template/console/queues/controller.ts +194 -0
- package/template/console/queues/events.ts +34 -0
- package/template/console/queues/store.ts +37 -0
- package/template/console/queues/types.ts +102 -0
- package/template/console/registry/RegistryDialog.tsx +198 -0
- package/template/console/registry/RegistryWorkspace.tsx +108 -0
- package/template/console/registry/events.ts +14 -0
- package/template/console/registry/types.ts +52 -0
- package/template/console/reports/ReportDrawer.tsx +159 -0
- package/template/console/reports/ReportWorkspace.tsx +90 -0
- package/template/console/reports/controller.ts +517 -0
- package/template/console/reports/events.ts +42 -0
- package/template/console/reports/store.ts +39 -0
- package/template/console/reports/types.ts +230 -0
- package/template/console/settings/BrandEditor.tsx +126 -0
- package/template/console/settings/ChannelDialog.tsx +241 -0
- package/template/console/settings/SettingsWorkspace.tsx +155 -0
- package/template/console/settings/events.ts +19 -0
- package/template/console/settings/handoff.ts +22 -0
- package/template/console/settings/types.ts +93 -0
- package/template/console/shell/WorkspaceShell.tsx +160 -0
- package/template/console/shell/controller.ts +182 -0
- package/template/console/shell/events.ts +25 -0
- package/template/console/shell/navigation.ts +61 -0
- package/template/console/shell/store.ts +56 -0
- package/template/console/shell/types.ts +86 -0
- package/template/console/workflows/CreateWorkflowDialog.tsx +87 -0
- package/template/console/workflows/WorkflowCanvas.tsx +42 -0
- package/template/console/workflows/WorkflowDialogs.tsx +6 -0
- package/template/console/workflows/WorkflowStudio.tsx +331 -0
- package/template/console/workflows/WorkflowWorkspace.tsx +82 -0
- package/template/console/workflows/events.ts +17 -0
- package/template/console/workflows/graph.ts +156 -0
- package/template/console/workflows/templates.ts +70 -0
- package/template/console/workflows/types.ts +201 -0
- package/template/gitignore.template +18 -0
- package/template/migrations/0001_reports_foundation.sql +444 -0
- package/template/migrations/0002_human_report_loop.sql +65 -0
- package/template/migrations/0003_delivery_reliability.sql +18 -0
- package/template/migrations/0004_public_intake.sql +14 -0
- package/template/migrations/0005_operations_visibility.sql +24 -0
- package/template/migrations/0006_ai_governance.sql +198 -0
- package/template/migrations/0007_ai_release_gates.sql +6 -0
- package/template/migrations/0008_retention_analytics_exports.sql +52 -0
- package/template/migrations/0009_retention_derived_copies.sql +14 -0
- package/template/migrations/0010_ai_quality_controls.sql +26 -0
- package/template/migrations/0011_analyst_presence.sql +28 -0
- package/template/migrations/0012_queue_policies.sql +87 -0
- package/template/migrations/0013_routing_agents.sql +63 -0
- package/template/migrations/0014_webhook_enrichments.sql +122 -0
- package/template/migrations/0015_queue_owned_ai.sql +55 -0
- package/template/migrations/0016_operator_accounts.sql +67 -0
- package/template/migrations/0017_operator_profiles_and_recovery.sql +33 -0
- package/template/migrations/0018_platform_configuration.sql +382 -0
- package/template/migrations/0019_workflow_authoring_runtime.sql +372 -0
- package/template/migrations/0020_tasks_findings_assistant_budgets.sql +427 -0
- package/template/migrations/0021_abuse_evidence_operations.sql +324 -0
- package/template/migrations/0022_workflow_dispatch_operations.sql +42 -0
- package/template/migrations/0023_component_connection_execution.sql +75 -0
- package/template/migrations/0024_access_runtime_integrity.sql +72 -0
- package/template/migrations/0025_installation_timezone.sql +11 -0
- package/template/migrations/0026_ai_and_egress_execution_controls.sql +49 -0
- package/template/migrations/0027_prompt_and_ai_registry.sql +37 -0
- package/template/migrations/0028_step_attempt_ai_provenance.sql +13 -0
- package/template/migrations/0029_evidence_fetch_transport.sql +5 -0
- package/template/migrations/0030_evidence_dlq_incidents.sql +45 -0
- package/template/migrations/0031_shadow_quality_integrity.sql +7 -0
- package/template/migrations/0032_action_delivery_outbox.sql +55 -0
- package/template/migrations/0033_configuration_and_assistant_drafts.sql +43 -0
- package/template/migrations/0034_installation_integrations.sql +31 -0
- package/template/migrations/0035_workspace_governance.sql +21 -0
- package/template/migrations/0036_published_routing_baseline.sql +15 -0
- package/template/migrations/0037_builtin_phishing_specialist.sql +71 -0
- package/template/migrations/0038_remove_deprecated_enrichment_runtime.sql +228 -0
- package/template/migrations/0039_secure_reporting_channels.sql +45 -0
- package/template/migrations/0040_notification_only_reporting.sql +24 -0
- package/template/migrations/0041_better_auth_credentials.sql +17 -0
- package/template/package.json +47 -0
- package/template/public/_headers +27 -0
- package/template/public/app-icon-192.png +0 -0
- package/template/public/app-icon-512.png +0 -0
- package/template/public/brand-icon.svg +7 -0
- package/template/public/brand-tokens.css +80 -0
- package/template/public/console/auth-shell.js +19340 -0
- package/template/public/customer-brand.js +58 -0
- package/template/public/embed/embed.css +139 -0
- package/template/public/embed/embed.js +408 -0
- package/template/public/embed/index.html +91 -0
- package/template/public/favicon.svg +7 -0
- package/template/public/fonts/Manrope-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Italic-Variable.ttf +0 -0
- package/template/public/fonts/Newsreader-Variable.ttf +0 -0
- package/template/public/index.html +123 -0
- package/template/public/logo-primary.svg +7 -0
- package/template/public/logo-reversed.svg +7 -0
- package/template/public/manifest.webmanifest +21 -0
- package/template/public/public-report.js +184 -0
- package/template/public/report/index.html +39 -0
- package/template/public/report/public-report.css +20 -0
- package/template/public/social-card.png +0 -0
- package/template/public/styles.css +1521 -0
- package/template/public/widget.css +80 -0
- package/template/public/widget.js +220 -0
- package/template/reports.config.example.json +38 -0
- package/template/reports.schema.json +89 -0
- package/template/scripts/reports-auth-onboarding.mjs +180 -0
- package/template/scripts/reports-backup.mjs +276 -0
- package/template/scripts/reports-cloudflare-preflight.mjs +152 -0
- package/template/scripts/reports-deploy.mjs +173 -0
- package/template/scripts/reports-plan.mjs +226 -0
- package/template/scripts/reports-restore.mjs +180 -0
- package/template/scripts/reports-secrets.mjs +73 -0
- package/template/scripts/reports-uninstall-plan.mjs +32 -0
- package/template/src/agent-executor.ts +330 -0
- package/template/src/ai-observability.ts +163 -0
- package/template/src/ai-registry-validation.ts +244 -0
- package/template/src/ai-registry.ts +292 -0
- package/template/src/api-cursor.ts +84 -0
- package/template/src/assistant.ts +554 -0
- package/template/src/audit.ts +39 -0
- package/template/src/backup-service.ts +269 -0
- package/template/src/budget-control.ts +127 -0
- package/template/src/component-executor.ts +845 -0
- package/template/src/configuration-registry.ts +550 -0
- package/template/src/connection-egress.ts +430 -0
- package/template/src/connection-oauth.ts +368 -0
- package/template/src/evidence-service.ts +335 -0
- package/template/src/human-tasks.ts +316 -0
- package/template/src/index.ts +4114 -0
- package/template/src/installation-admin.ts +301 -0
- package/template/src/intake-abuse.ts +156 -0
- package/template/src/platform-registry-validation.ts +367 -0
- package/template/src/platform-registry.ts +753 -0
- package/template/src/report-admin.ts +342 -0
- package/template/src/report-ai-quality.ts +390 -0
- package/template/src/report-ai-validation.ts +80 -0
- package/template/src/report-ai.ts +857 -0
- package/template/src/report-auth.ts +292 -0
- package/template/src/report-better-auth.ts +583 -0
- package/template/src/report-context-schema.ts +132 -0
- package/template/src/report-context.ts +126 -0
- package/template/src/report-crypto.ts +142 -0
- package/template/src/report-delivery.ts +579 -0
- package/template/src/report-form-validation.ts +51 -0
- package/template/src/report-governance.ts +510 -0
- package/template/src/report-http.ts +90 -0
- package/template/src/report-operations.ts +685 -0
- package/template/src/report-operator-accounts.ts +810 -0
- package/template/src/report-presence.ts +398 -0
- package/template/src/report-queue-validation.ts +309 -0
- package/template/src/report-queues.ts +478 -0
- package/template/src/report-repository.ts +972 -0
- package/template/src/report-router-agent.ts +328 -0
- package/template/src/report-router-validation.ts +81 -0
- package/template/src/report-routing.ts +178 -0
- package/template/src/report-turnstile.ts +142 -0
- package/template/src/report-types.ts +224 -0
- package/template/src/report-validation.ts +279 -0
- package/template/src/report-workflow-validation.ts +119 -0
- package/template/src/report-workflow.ts +886 -0
- package/template/src/shadow-quality.ts +278 -0
- package/template/src/workflow-actions.ts +782 -0
- package/template/src/workflow-compiler.ts +230 -0
- package/template/src/workflow-dynamic-runtime.ts +613 -0
- package/template/src/workflow-effects.ts +316 -0
- package/template/src/workflow-expressions.ts +191 -0
- package/template/src/workflow-platform-types.ts +121 -0
- package/template/src/workflow-platform-validation.ts +540 -0
- package/template/src/workflow-repository.ts +916 -0
- package/template/src/workflow-runs.ts +686 -0
- package/template/src/workflow-simulator.ts +211 -0
- package/template/src/workspace-branding.ts +128 -0
- package/template/src/workspace-governance.ts +279 -0
- package/template/tsconfig.console.json +23 -0
- package/template/tsconfig.json +22 -0
- package/template/vite.console.config.ts +20 -0
- package/template/worker-configuration.d.ts +65 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { QualityReviewInput, QualityWorkspaceSnapshot } from "./types";
|
|
2
|
+
|
|
3
|
+
export const qualityEvents = {
|
|
4
|
+
state: "safest:quality:state",
|
|
5
|
+
refresh: "safest:quality:refresh",
|
|
6
|
+
open: "safest:quality:open",
|
|
7
|
+
close: "safest:quality:close",
|
|
8
|
+
claim: "safest:quality:claim",
|
|
9
|
+
review: "safest:quality:review",
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
function dispatch<TDetail>(type: string, detail?: TDetail): void {
|
|
13
|
+
window.dispatchEvent(detail === undefined ? new Event(type) : new CustomEvent(type, { detail }));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function dispatchQualityRefresh(): void { dispatch(qualityEvents.refresh); }
|
|
17
|
+
export function dispatchOpenQualitySample(sampleId: string): void { dispatch(qualityEvents.open, { sampleId }); }
|
|
18
|
+
export function dispatchCloseQualitySample(): void { dispatch(qualityEvents.close); }
|
|
19
|
+
export function dispatchClaimQualitySample(): void { dispatch(qualityEvents.claim); }
|
|
20
|
+
export function dispatchQualityReview(input: QualityReviewInput): void { dispatch(qualityEvents.review, input); }
|
|
21
|
+
|
|
22
|
+
export function isQualityWorkspaceSnapshot(value: unknown): value is QualityWorkspaceSnapshot {
|
|
23
|
+
if (!value || typeof value !== "object") return false;
|
|
24
|
+
const candidate = value as Partial<QualityWorkspaceSnapshot>;
|
|
25
|
+
return typeof candidate.loading === "boolean"
|
|
26
|
+
&& typeof candidate.refreshing === "boolean"
|
|
27
|
+
&& typeof candidate.error === "string"
|
|
28
|
+
&& typeof candidate.actorId === "string"
|
|
29
|
+
&& typeof candidate.generatedAt === "string"
|
|
30
|
+
&& typeof candidate.canRead === "boolean"
|
|
31
|
+
&& typeof candidate.canDecide === "boolean"
|
|
32
|
+
&& Array.isArray(candidate.samples)
|
|
33
|
+
&& typeof candidate.detail === "object";
|
|
34
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isQualityWorkspaceSnapshot, qualityEvents } from "./events";
|
|
2
|
+
import type { QualityWorkspaceSnapshot } from "./types";
|
|
3
|
+
|
|
4
|
+
const subscribers = new Set<() => void>();
|
|
5
|
+
|
|
6
|
+
let snapshot: QualityWorkspaceSnapshot = {
|
|
7
|
+
loading: true,
|
|
8
|
+
refreshing: false,
|
|
9
|
+
error: "",
|
|
10
|
+
actorId: "",
|
|
11
|
+
generatedAt: new Date().toISOString(),
|
|
12
|
+
canRead: false,
|
|
13
|
+
canDecide: false,
|
|
14
|
+
samples: [],
|
|
15
|
+
detail: { sampleId: null, loading: false, error: "", pendingAction: null, record: null },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
window.addEventListener(qualityEvents.state, (event) => {
|
|
19
|
+
if (!(event instanceof CustomEvent) || !isQualityWorkspaceSnapshot(event.detail)) return;
|
|
20
|
+
snapshot = event.detail;
|
|
21
|
+
for (const subscriber of subscribers) subscriber();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export function getQualityWorkspaceSnapshot(): QualityWorkspaceSnapshot {
|
|
25
|
+
return snapshot;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setQualityWorkspaceSnapshot(next: QualityWorkspaceSnapshot | ((current: QualityWorkspaceSnapshot) => QualityWorkspaceSnapshot)): void {
|
|
29
|
+
snapshot = typeof next === "function" ? next(snapshot) : next;
|
|
30
|
+
for (const subscriber of subscribers) subscriber();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function subscribeToQualityWorkspace(callback: () => void): () => void {
|
|
34
|
+
subscribers.add(callback);
|
|
35
|
+
return () => subscribers.delete(callback);
|
|
36
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type QualityIssueCode = "" | "incorrect_outcome" | "incorrect_policy_code" | "unsupported_evidence" | "unsafe_or_inaccurate_message" | "should_have_escalated" | "other";
|
|
2
|
+
export type QualityPendingAction = "claim" | "review" | null;
|
|
3
|
+
|
|
4
|
+
export interface QualitySampleSummary {
|
|
5
|
+
id: string;
|
|
6
|
+
reportReference: string;
|
|
7
|
+
sampleReason: string;
|
|
8
|
+
aiOutcome?: string;
|
|
9
|
+
aiRunStatus: string;
|
|
10
|
+
selectedAt: string;
|
|
11
|
+
state: string;
|
|
12
|
+
agrees: boolean | null;
|
|
13
|
+
claimedBy?: string | null;
|
|
14
|
+
claimExpiresAt?: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface QualitySampleDetail extends QualitySampleSummary {
|
|
18
|
+
configVersionId: string;
|
|
19
|
+
model: string;
|
|
20
|
+
promptVersion: string;
|
|
21
|
+
policyVersionId: string;
|
|
22
|
+
aiPolicyCode?: string | null;
|
|
23
|
+
messageTemplateId?: string | null;
|
|
24
|
+
aiEvidenceReferences?: string[];
|
|
25
|
+
rawOutput: unknown;
|
|
26
|
+
qualityReviewerId?: string | null;
|
|
27
|
+
issueCodes?: string[];
|
|
28
|
+
correctedOutcomeCode?: string | null;
|
|
29
|
+
correctedPolicyCode?: string | null;
|
|
30
|
+
qualityNotes?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface QualityReportDetail {
|
|
34
|
+
reasonCode: string;
|
|
35
|
+
targetType: string;
|
|
36
|
+
targetReference: string;
|
|
37
|
+
answers: Array<{ fieldKey: string; value: unknown }>;
|
|
38
|
+
decisions: Array<{ makerType: string; decisionCode: string; policyCode?: string }>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface QualityDetailRecord {
|
|
42
|
+
sample: QualitySampleDetail;
|
|
43
|
+
report: QualityReportDetail;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface QualityWorkspaceSnapshot {
|
|
47
|
+
loading: boolean;
|
|
48
|
+
refreshing: boolean;
|
|
49
|
+
error: string;
|
|
50
|
+
actorId: string;
|
|
51
|
+
generatedAt: string;
|
|
52
|
+
canRead: boolean;
|
|
53
|
+
canDecide: boolean;
|
|
54
|
+
samples: QualitySampleSummary[];
|
|
55
|
+
detail: {
|
|
56
|
+
sampleId: string | null;
|
|
57
|
+
loading: boolean;
|
|
58
|
+
error: string;
|
|
59
|
+
pendingAction: QualityPendingAction;
|
|
60
|
+
record: QualityDetailRecord | null;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface QualityReviewInput {
|
|
65
|
+
agrees: boolean;
|
|
66
|
+
issueCode: QualityIssueCode;
|
|
67
|
+
correctedOutcomeCode: string;
|
|
68
|
+
correctedPolicyCode: string;
|
|
69
|
+
notes: string;
|
|
70
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useSyncExternalStore, type FormEvent } from "react";
|
|
2
|
+
import { dispatchCloseQueueEditor, dispatchPublishQueue } from "./events";
|
|
3
|
+
import { getQueueWorkspaceSnapshot, subscribeToQueueWorkspace } from "./store";
|
|
4
|
+
import type { QueueManagementMode, QueuePolicyInput, QueueRecord, QueueRolloutMode } from "./types";
|
|
5
|
+
|
|
6
|
+
const AI_FIELDS = [
|
|
7
|
+
["target.type", "Target type"],
|
|
8
|
+
["answers.details", "Report details"],
|
|
9
|
+
["evidence.references", "Evidence references"],
|
|
10
|
+
["enrichments", "Completed enrichments"],
|
|
11
|
+
] as const;
|
|
12
|
+
const AI_TEMPLATES = [
|
|
13
|
+
["ai-more-context-v1", "Ask for more context"],
|
|
14
|
+
["ai-no-violation-v1", "No violation"],
|
|
15
|
+
["ai-out-of-scope-v1", "Out of scope"],
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
interface QueueDraft {
|
|
19
|
+
name: string;
|
|
20
|
+
key: string;
|
|
21
|
+
description: string;
|
|
22
|
+
managementMode: QueueManagementMode;
|
|
23
|
+
rolloutMode: QueueRolloutMode;
|
|
24
|
+
fallbackQueueId: string;
|
|
25
|
+
priority: string;
|
|
26
|
+
responseSlaMinutes: string;
|
|
27
|
+
resolutionSlaMinutes: string;
|
|
28
|
+
claimTtlMinutes: string;
|
|
29
|
+
reviewerInstructions: string;
|
|
30
|
+
policyVersionId: string;
|
|
31
|
+
aiInstructions: string;
|
|
32
|
+
model: string;
|
|
33
|
+
confidenceThreshold: string;
|
|
34
|
+
maxFollowUpTurns: string;
|
|
35
|
+
allowedOutcomes: string;
|
|
36
|
+
allowedFields: string[];
|
|
37
|
+
allowedTemplateIds: string[];
|
|
38
|
+
dailyReportBudget: string;
|
|
39
|
+
dailyCostBudgetMicrousd: string;
|
|
40
|
+
inputCostMicrousdPerMillion: string;
|
|
41
|
+
outputCostMicrousdPerMillion: string;
|
|
42
|
+
qualitySampleRate: string;
|
|
43
|
+
maxWaitHours: string;
|
|
44
|
+
minimumShadowRuns: string;
|
|
45
|
+
minimumQualityReviews: string;
|
|
46
|
+
maximumFailureRate: string;
|
|
47
|
+
maximumDisagreementRate: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function selectedValues(select: HTMLSelectElement): string[] {
|
|
51
|
+
return [...select.selectedOptions].map((option) => option.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function commaList(value: string): string[] {
|
|
55
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function queueDraft(queue: QueueRecord | null): QueueDraft {
|
|
59
|
+
const policy = queue?.activePolicy;
|
|
60
|
+
const bounds = policy?.aiBounds;
|
|
61
|
+
return {
|
|
62
|
+
name: queue?.name ?? "",
|
|
63
|
+
key: queue?.key ?? "",
|
|
64
|
+
description: queue?.description ?? "",
|
|
65
|
+
managementMode: policy?.managementMode ?? "human",
|
|
66
|
+
rolloutMode: policy?.rolloutMode ?? "off",
|
|
67
|
+
fallbackQueueId: policy?.fallbackQueueId ?? "",
|
|
68
|
+
priority: String(policy?.priority ?? 50),
|
|
69
|
+
responseSlaMinutes: policy?.responseSlaMinutes == null ? "" : String(policy.responseSlaMinutes),
|
|
70
|
+
resolutionSlaMinutes: policy?.resolutionSlaMinutes == null ? "" : String(policy.resolutionSlaMinutes),
|
|
71
|
+
claimTtlMinutes: String(policy?.claimTtlMinutes ?? 15),
|
|
72
|
+
reviewerInstructions: policy?.reviewerInstructions ?? "Review the allegation and referenced evidence against the published customer policy. Record the evidence considered and rationale.",
|
|
73
|
+
policyVersionId: bounds?.policyVersionId ?? "general-policy-v1",
|
|
74
|
+
aiInstructions: policy?.aiInstructions ?? "",
|
|
75
|
+
model: bounds?.model ?? "@cf/meta/llama-3.1-8b-instruct-fast",
|
|
76
|
+
confidenceThreshold: String(bounds?.confidenceThreshold ?? 0.85),
|
|
77
|
+
maxFollowUpTurns: String(bounds?.maxFollowUpTurns ?? 0),
|
|
78
|
+
allowedOutcomes: (bounds?.allowedOutcomes ?? ["no_violation_found", "out_of_scope", "insufficient_information", "escalate_to_human"]).join(", "),
|
|
79
|
+
allowedFields: bounds?.allowedFields ?? AI_FIELDS.map(([value]) => value),
|
|
80
|
+
allowedTemplateIds: bounds?.allowedTemplateIds ?? AI_TEMPLATES.map(([value]) => value),
|
|
81
|
+
dailyReportBudget: String(bounds?.dailyReportBudget ?? 100),
|
|
82
|
+
dailyCostBudgetMicrousd: String(bounds?.dailyCostBudgetMicrousd ?? 1_000_000),
|
|
83
|
+
inputCostMicrousdPerMillion: String(bounds?.inputCostMicrousdPerMillion ?? 0),
|
|
84
|
+
outputCostMicrousdPerMillion: String(bounds?.outputCostMicrousdPerMillion ?? 0),
|
|
85
|
+
qualitySampleRate: String(bounds?.qualitySampleRate ?? 0.1),
|
|
86
|
+
maxWaitHours: String(bounds?.maxWaitHours ?? 168),
|
|
87
|
+
minimumShadowRuns: String(bounds?.minimumShadowRuns ?? 25),
|
|
88
|
+
minimumQualityReviews: String(bounds?.minimumQualityReviews ?? 10),
|
|
89
|
+
maximumFailureRate: String(bounds?.maximumFailureRate ?? 0.1),
|
|
90
|
+
maximumDisagreementRate: String(bounds?.maximumDisagreementRate ?? 0.1),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function QueueEditor() {
|
|
95
|
+
const snapshot = useSyncExternalStore(subscribeToQueueWorkspace, getQueueWorkspaceSnapshot, getQueueWorkspaceSnapshot);
|
|
96
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
97
|
+
const nameRef = useRef<HTMLInputElement>(null);
|
|
98
|
+
const queue = snapshot.queues.find((candidate) => candidate.id === snapshot.editor.queueId) ?? null;
|
|
99
|
+
const [draft, setDraft] = useState<QueueDraft>(() => queueDraft(queue));
|
|
100
|
+
const humanQueues = snapshot.queues.filter((candidate) => candidate.id !== queue?.id && candidate.status === "active" && candidate.activePolicy?.managementMode === "human");
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const dialog = dialogRef.current;
|
|
104
|
+
if (!dialog) return;
|
|
105
|
+
if (snapshot.editor.open && !dialog.open) {
|
|
106
|
+
dialog.showModal();
|
|
107
|
+
nameRef.current?.focus();
|
|
108
|
+
}
|
|
109
|
+
if (!snapshot.editor.open && dialog.open) dialog.close();
|
|
110
|
+
}, [snapshot.editor.open]);
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (snapshot.editor.open) setDraft(queueDraft(queue));
|
|
113
|
+
}, [snapshot.editor.open, snapshot.editor.queueId]);
|
|
114
|
+
|
|
115
|
+
const update = <TKey extends keyof QueueDraft>(key: TKey, value: QueueDraft[TKey]): void => setDraft((current) => ({ ...current, [key]: value }));
|
|
116
|
+
const changeMode = (mode: QueueManagementMode): void => setDraft((current) => ({ ...current, managementMode: mode, rolloutMode: mode === "human" ? "off" : current.rolloutMode }));
|
|
117
|
+
const updateName = (name: string): void => setDraft((current) => ({
|
|
118
|
+
...current,
|
|
119
|
+
name,
|
|
120
|
+
key: queue ? current.key : name.toLowerCase().trim().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "").slice(0, 80),
|
|
121
|
+
}));
|
|
122
|
+
const submit = (event: FormEvent<HTMLFormElement>): void => {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
const human = draft.managementMode === "human";
|
|
125
|
+
const policy: QueuePolicyInput = {
|
|
126
|
+
managementMode: draft.managementMode,
|
|
127
|
+
rolloutMode: human ? "off" : draft.rolloutMode,
|
|
128
|
+
priority: Number(draft.priority),
|
|
129
|
+
responseSlaMinutes: draft.responseSlaMinutes ? Number(draft.responseSlaMinutes) : null,
|
|
130
|
+
resolutionSlaMinutes: draft.resolutionSlaMinutes ? Number(draft.resolutionSlaMinutes) : null,
|
|
131
|
+
claimTtlMinutes: Number(draft.claimTtlMinutes),
|
|
132
|
+
fallbackQueueId: draft.fallbackQueueId || null,
|
|
133
|
+
reviewerInstructions: draft.reviewerInstructions,
|
|
134
|
+
aiInstructions: human ? null : draft.aiInstructions,
|
|
135
|
+
aiBounds: {
|
|
136
|
+
allowedOutcomes: commaList(draft.allowedOutcomes),
|
|
137
|
+
confidenceThreshold: Number(draft.confidenceThreshold),
|
|
138
|
+
maxFollowUpTurns: Number(draft.maxFollowUpTurns),
|
|
139
|
+
model: draft.model,
|
|
140
|
+
policyVersionId: draft.policyVersionId,
|
|
141
|
+
allowedFields: draft.allowedFields,
|
|
142
|
+
allowedTemplateIds: draft.allowedTemplateIds,
|
|
143
|
+
dailyReportBudget: Number(draft.dailyReportBudget),
|
|
144
|
+
dailyCostBudgetMicrousd: Number(draft.dailyCostBudgetMicrousd),
|
|
145
|
+
inputCostMicrousdPerMillion: Number(draft.inputCostMicrousdPerMillion),
|
|
146
|
+
outputCostMicrousdPerMillion: Number(draft.outputCostMicrousdPerMillion),
|
|
147
|
+
qualitySampleRate: Number(draft.qualitySampleRate),
|
|
148
|
+
maxWaitHours: Number(draft.maxWaitHours),
|
|
149
|
+
minimumShadowRuns: Number(draft.minimumShadowRuns),
|
|
150
|
+
minimumQualityReviews: Number(draft.minimumQualityReviews),
|
|
151
|
+
maximumFailureRate: Number(draft.maximumFailureRate),
|
|
152
|
+
maximumDisagreementRate: Number(draft.maximumDisagreementRate),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
dispatchPublishQueue({ queueId: queue?.id ?? null, key: draft.key, name: draft.name, description: draft.description, policy });
|
|
156
|
+
};
|
|
157
|
+
const disabled = snapshot.queuePending;
|
|
158
|
+
const human = draft.managementMode === "human";
|
|
159
|
+
const version = queue ? `Publishing policy version ${(queue.activePolicy?.version ?? 0) + 1}; prior versions remain immutable.` : "New queue and policy version 1";
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<dialog ref={dialogRef} className="detail-dialog queue-dialog" aria-labelledby="queue-editor-title" onCancel={(event) => { event.preventDefault(); dispatchCloseQueueEditor(); }} data-react-slice="queue-editor">
|
|
163
|
+
<div className="detail-head"><div><h2 id="queue-editor-title">{queue?.name ?? "Create queue"}</h2><p>{version}</p></div><button className="icon-button" type="button" aria-label="Close queue editor" onClick={dispatchCloseQueueEditor}>×</button></div>
|
|
164
|
+
<form className="queue-form" onSubmit={submit} aria-busy={disabled}>
|
|
165
|
+
<section className="queue-form-section"><div><h3>Queue details</h3></div><label>Name<input ref={nameRef} maxLength={100} required disabled={disabled} value={draft.name} onChange={(event) => updateName(event.currentTarget.value)} /></label><label>Key<input maxLength={80} pattern="[a-z0-9]+(?:-[a-z0-9]+)*" required disabled={disabled || Boolean(queue)} value={draft.key} onChange={(event) => update("key", event.currentTarget.value)} /></label><label className="wide-field">Description<textarea maxLength={1000} disabled={disabled} value={draft.description} onChange={(event) => update("description", event.currentTarget.value)} /></label></section>
|
|
166
|
+
<section className="queue-form-section"><div><h3>Mode and rollout</h3></div><label>Managed by<select disabled={disabled} value={draft.managementMode} onChange={(event) => changeMode(event.currentTarget.value as QueueManagementMode)}><option value="human">Human</option><option value="ai">AI</option><option value="hybrid">Hybrid</option></select></label><label>AI rollout<select disabled={disabled || human} value={human ? "off" : draft.rolloutMode} onChange={(event) => update("rolloutMode", event.currentTarget.value as QueueRolloutMode)}><option value="off">Off</option><option value="shadow">Shadow</option><option value="assist">Assist</option><option value="autonomous">Autonomous</option></select></label><label>Human fallback<select disabled={disabled} value={draft.fallbackQueueId} onChange={(event) => update("fallbackQueueId", event.currentTarget.value)}><option value="">None</option>{humanQueues.map((candidate) => <option value={candidate.id} key={candidate.id}>{candidate.name}</option>)}</select></label><label>Priority<input type="number" min="0" max="100" required disabled={disabled} value={draft.priority} onChange={(event) => update("priority", event.currentTarget.value)} /></label><label>Response SLA (minutes)<input type="number" min="1" max="525600" disabled={disabled} value={draft.responseSlaMinutes} onChange={(event) => update("responseSlaMinutes", event.currentTarget.value)} /></label><label>Resolution SLA (minutes)<input type="number" min="1" max="525600" disabled={disabled} value={draft.resolutionSlaMinutes} onChange={(event) => update("resolutionSlaMinutes", event.currentTarget.value)} /></label><label>Claim TTL (minutes)<input type="number" min="1" max="1440" required disabled={disabled} value={draft.claimTtlMinutes} onChange={(event) => update("claimTtlMinutes", event.currentTarget.value)} /></label></section>
|
|
167
|
+
<section className="queue-form-section policy-copy-section"><div><h3>Reviewer instructions</h3></div><label>Adjudication policy version<input required disabled={disabled} value={draft.policyVersionId} onChange={(event) => update("policyVersionId", event.currentTarget.value)} /></label><label className="wide-field">Instructions<textarea maxLength={10000} required disabled={disabled} value={draft.reviewerInstructions} onChange={(event) => update("reviewerInstructions", event.currentTarget.value)} /></label></section>
|
|
168
|
+
<section className="queue-form-section policy-copy-section" hidden={human}><div><h3>AI criteria and instructions</h3></div><label className="wide-field">AI instructions<textarea maxLength={10000} required={!human} disabled={disabled} value={draft.aiInstructions} onChange={(event) => update("aiInstructions", event.currentTarget.value)} /></label><label>Workers AI model<input required disabled={disabled} value={draft.model} onChange={(event) => update("model", event.currentTarget.value)} /></label><label>Confidence threshold<input type="number" min="0" max="1" step="0.01" disabled={disabled} value={draft.confidenceThreshold} onChange={(event) => update("confidenceThreshold", event.currentTarget.value)} /></label><label>Maximum follow-up turns<input type="number" min="0" max="10" disabled={disabled} value={draft.maxFollowUpTurns} onChange={(event) => update("maxFollowUpTurns", event.currentTarget.value)} /></label><label className="wide-field">Allowed outcome codes <small>Comma-separated bounded result types.</small><input disabled={disabled} value={draft.allowedOutcomes} onChange={(event) => update("allowedOutcomes", event.currentTarget.value)} /></label><label>Fields shared with case AI<select multiple size={4} disabled={disabled} value={draft.allowedFields} onChange={(event) => update("allowedFields", selectedValues(event.currentTarget))}>{AI_FIELDS.map(([value, label]) => <option value={value} key={value}>{label}</option>)}</select></label><label>Approved reporter templates<select multiple size={3} disabled={disabled} value={draft.allowedTemplateIds} onChange={(event) => update("allowedTemplateIds", selectedValues(event.currentTarget))}>{AI_TEMPLATES.map(([value, label]) => <option value={value} key={value}>{label}</option>)}</select></label><label>Daily report budget<input type="number" min="0" max="1000000" disabled={disabled} value={draft.dailyReportBudget} onChange={(event) => update("dailyReportBudget", event.currentTarget.value)} /></label><label>Daily cost budget (µUSD)<input type="number" min="0" max="1000000000" disabled={disabled} value={draft.dailyCostBudgetMicrousd} onChange={(event) => update("dailyCostBudgetMicrousd", event.currentTarget.value)} /></label><label>Input price (µUSD / 1M units)<input type="number" min="0" max="1000000000" disabled={disabled} value={draft.inputCostMicrousdPerMillion} onChange={(event) => update("inputCostMicrousdPerMillion", event.currentTarget.value)} /></label><label>Output price (µUSD / 1M units)<input type="number" min="0" max="1000000000" disabled={disabled} value={draft.outputCostMicrousdPerMillion} onChange={(event) => update("outputCostMicrousdPerMillion", event.currentTarget.value)} /></label><label>Human quality sample<input type="number" min="0" max="1" step="0.01" disabled={disabled} value={draft.qualitySampleRate} onChange={(event) => update("qualitySampleRate", event.currentTarget.value)} /></label><label>Maximum wait (hours)<input type="number" min="1" max="720" disabled={disabled} value={draft.maxWaitHours} onChange={(event) => update("maxWaitHours", event.currentTarget.value)} /></label><label>Required shadow runs<input type="number" min="1" max="100000" disabled={disabled} value={draft.minimumShadowRuns} onChange={(event) => update("minimumShadowRuns", event.currentTarget.value)} /></label><label>Required quality reviews<input type="number" min="0" max="100000" disabled={disabled} value={draft.minimumQualityReviews} onChange={(event) => update("minimumQualityReviews", event.currentTarget.value)} /></label><label>Maximum failure rate<input type="number" min="0" max="1" step="0.01" disabled={disabled} value={draft.maximumFailureRate} onChange={(event) => update("maximumFailureRate", event.currentTarget.value)} /></label><label>Maximum disagreement rate<input type="number" min="0" max="1" step="0.01" disabled={disabled} value={draft.maximumDisagreementRate} onChange={(event) => update("maximumDisagreementRate", event.currentTarget.value)} /></label><p className="queue-safety-copy wide-field">Customer application actions remain human-approved in the first release. Protected, uncertain, failed, contested, and out-of-bound reports move to a human.</p></section>
|
|
169
|
+
<div className="queue-form-footer"><p className="error" role="alert">{snapshot.editor.error}</p><div><button className="secondary" type="button" disabled={disabled} onClick={dispatchCloseQueueEditor}>Cancel</button><button className="primary" type="submit" disabled={disabled}>{disabled ? "Publishing…" : queue ? "Publish policy version" : "Create and publish"}</button></div></div>
|
|
170
|
+
</form>
|
|
171
|
+
</dialog>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useEffect, useState, useSyncExternalStore, type FormEvent } from "react";
|
|
2
|
+
import { readable } from "../lib/format";
|
|
3
|
+
import { PageHeader } from "../components/PageHeader";
|
|
4
|
+
import { dispatchCreateQueue, dispatchEditQueue, dispatchPublishRouting } from "./events";
|
|
5
|
+
import { getQueueWorkspaceSnapshot, subscribeToQueueWorkspace } from "./store";
|
|
6
|
+
import type { QueueRecord, RoutingAgentConfiguration, RoutingAgentInput, RoutingRolloutMode } from "./types";
|
|
7
|
+
|
|
8
|
+
const ROUTING_FIELDS = [
|
|
9
|
+
["reason_code", "Reason code"],
|
|
10
|
+
["source", "Source"],
|
|
11
|
+
["target_type", "Target type"],
|
|
12
|
+
["answers.details", "Report details"],
|
|
13
|
+
["trusted_facts", "Signed trusted facts"],
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ROUTING_INSTRUCTIONS = "Choose only among the assigned eligible queues. Route uncertainty or protected cases to a human. Do not decide the report.";
|
|
17
|
+
|
|
18
|
+
function selectedValues(select: HTMLSelectElement): string[] {
|
|
19
|
+
return [...select.selectedOptions].map((option) => option.value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function routingDraft(configuration: RoutingAgentConfiguration | null, queues: QueueRecord[]): RoutingAgentInput {
|
|
23
|
+
const active = queues.filter((queue) => queue.status === "active");
|
|
24
|
+
const human = active.filter((queue) => queue.activePolicy?.managementMode === "human");
|
|
25
|
+
return {
|
|
26
|
+
rolloutMode: configuration?.rolloutMode ?? "off",
|
|
27
|
+
model: configuration?.model ?? "@cf/meta/llama-3.1-8b-instruct-fast",
|
|
28
|
+
confidenceThreshold: configuration?.confidenceThreshold ?? 0.85,
|
|
29
|
+
safeguardQueueId: configuration?.safeguardQueueId ?? human[0]?.id ?? "",
|
|
30
|
+
defaultQueueId: configuration?.defaultQueueId ?? human[0]?.id ?? "",
|
|
31
|
+
instructions: configuration?.instructions ?? DEFAULT_ROUTING_INSTRUCTIONS,
|
|
32
|
+
candidateQueueIds: configuration?.candidateQueueIds ?? active.map((queue) => queue.id),
|
|
33
|
+
allowedFields: configuration?.allowedFields ?? ROUTING_FIELDS.map(([value]) => value),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function RoutingAgentForm({ configuration, queues, loading, error, pending, canManage }: {
|
|
38
|
+
configuration: RoutingAgentConfiguration | null;
|
|
39
|
+
queues: QueueRecord[];
|
|
40
|
+
loading: boolean;
|
|
41
|
+
error: string;
|
|
42
|
+
pending: boolean;
|
|
43
|
+
canManage: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
const [draft, setDraft] = useState<RoutingAgentInput>(() => routingDraft(configuration, queues));
|
|
46
|
+
const activeQueues = queues.filter((queue) => queue.status === "active");
|
|
47
|
+
const humanQueues = activeQueues.filter((queue) => queue.activePolicy?.managementMode === "human");
|
|
48
|
+
|
|
49
|
+
useEffect(() => setDraft(routingDraft(configuration, queues)), [configuration?.version, queues]);
|
|
50
|
+
const update = <TKey extends keyof RoutingAgentInput>(key: TKey, value: RoutingAgentInput[TKey]): void => {
|
|
51
|
+
setDraft((current) => ({ ...current, [key]: value }));
|
|
52
|
+
};
|
|
53
|
+
const submit = (event: FormEvent<HTMLFormElement>): void => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
dispatchPublishRouting(draft);
|
|
56
|
+
};
|
|
57
|
+
const disabled = !canManage || loading || pending;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<form className="routing-agent-form" onSubmit={submit} aria-busy={loading || pending}>
|
|
61
|
+
<div className="routing-agent-head">
|
|
62
|
+
<div><h3>Routing agent</h3><p>Routes only to approved queues. Safeguards and human fallback always take priority.</p></div>
|
|
63
|
+
<span className="badge">{loading ? "Loading" : configuration ? `Version ${configuration.version} · ${readable(configuration.rolloutMode)}` : "Not configured"}</span>
|
|
64
|
+
</div>
|
|
65
|
+
<label>Rollout<select value={draft.rolloutMode} disabled={disabled} onChange={(event) => update("rolloutMode", event.currentTarget.value as RoutingRolloutMode)}><option value="off">Off</option><option value="shadow">Shadow</option><option value="active">Active</option></select></label>
|
|
66
|
+
<label>Workers AI model<input value={draft.model} required disabled={disabled} onChange={(event) => update("model", event.currentTarget.value)} /></label>
|
|
67
|
+
<label>Confidence threshold<input type="number" min="0" max="1" step="0.01" value={draft.confidenceThreshold} required disabled={disabled} onChange={(event) => update("confidenceThreshold", Number(event.currentTarget.value))} /></label>
|
|
68
|
+
<label>Safeguard queue<select value={draft.safeguardQueueId} required disabled={disabled} onChange={(event) => update("safeguardQueueId", event.currentTarget.value)}><option value="" disabled>Select a human queue</option>{humanQueues.map((queue) => <option value={queue.id} key={queue.id}>{queue.name}</option>)}</select></label>
|
|
69
|
+
<label>Default human queue<select value={draft.defaultQueueId} required disabled={disabled} onChange={(event) => update("defaultQueueId", event.currentTarget.value)}><option value="" disabled>Select a human queue</option>{humanQueues.map((queue) => <option value={queue.id} key={queue.id}>{queue.name}</option>)}</select></label>
|
|
70
|
+
<label>Candidate queues<select multiple size={4} value={draft.candidateQueueIds} required disabled={disabled} onChange={(event) => update("candidateQueueIds", selectedValues(event.currentTarget))}>{activeQueues.map((queue) => <option value={queue.id} key={queue.id}>{queue.name} · {readable(queue.activePolicy?.managementMode || "unconfigured")}</option>)}</select></label>
|
|
71
|
+
<label className="routing-agent-instructions">Routing instructions<textarea maxLength={10000} required value={draft.instructions} disabled={disabled} onChange={(event) => update("instructions", event.currentTarget.value)} /></label>
|
|
72
|
+
<label>Fields shared with routing AI<select multiple size={5} value={draft.allowedFields} disabled={disabled} onChange={(event) => update("allowedFields", selectedValues(event.currentTarget))}>{ROUTING_FIELDS.map(([value, label]) => <option value={value} key={value}>{label}</option>)}</select></label>
|
|
73
|
+
<div className="routing-agent-footer"><p className="error" role="alert">{error}</p>{canManage ? <button className="primary" type="submit" disabled={disabled}>{pending ? "Publishing…" : "Publish routing version"}</button> : null}</div>
|
|
74
|
+
</form>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function QueueCard({ queue, canManage }: { queue: QueueRecord; canManage: boolean }) {
|
|
79
|
+
const policy = queue.activePolicy;
|
|
80
|
+
const mode = policy ? `${readable(policy.managementMode)} · ${readable(policy.rolloutMode)}` : "No published policy";
|
|
81
|
+
return (
|
|
82
|
+
<article className={`queue-card ${policy?.managementMode || "unconfigured"}`}>
|
|
83
|
+
<div><div><strong>{queue.name}</strong><small>{queue.key} · policy v{policy?.version ?? "—"}</small></div><div><span className={`badge${policy?.rolloutMode === "autonomous" ? " urgent" : ""}`}>{mode}</span><span className={`badge${queue.status === "active" ? "" : " urgent"}`}>{readable(queue.status)}</span></div></div>
|
|
84
|
+
<p>{queue.description || "No queue description."}</p>
|
|
85
|
+
<dl><dt>Open</dt><dd>{queue.counts.openReports}</dd><dt>Priority</dt><dd>{policy?.priority ?? "—"}</dd><dt>Response SLA</dt><dd>{policy?.responseSlaMinutes ? `${policy.responseSlaMinutes}m` : "—"}</dd></dl>
|
|
86
|
+
<p className="queue-instructions">{policy?.reviewerInstructions || "No reviewer instructions have been published."}</p>
|
|
87
|
+
{canManage ? <button className="secondary" type="button" onClick={() => dispatchEditQueue(queue.id)}>Publish new policy</button> : null}
|
|
88
|
+
</article>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function QueueWorkspace() {
|
|
93
|
+
const snapshot = useSyncExternalStore(subscribeToQueueWorkspace, getQueueWorkspaceSnapshot, getQueueWorkspaceSnapshot);
|
|
94
|
+
return (
|
|
95
|
+
<div className="page-stack" data-react-slice="queue-workspace">
|
|
96
|
+
<PageHeader titleId="queues-title" title="Queues and policies" description="Set ownership, response targets, and safeguards.">
|
|
97
|
+
{snapshot.canManage ? <div className="surface-buttons"><button className="primary" type="button" onClick={dispatchCreateQueue}>Create queue</button></div> : null}
|
|
98
|
+
</PageHeader>
|
|
99
|
+
<section className="surface queues-surface" aria-labelledby="queues-title">
|
|
100
|
+
<div className="queue-policy-note"><strong>Ownership and rollout are separate.</strong><span>Choose who owns the work, then how far AI may proceed. Publishing locks a new policy version.</span></div>
|
|
101
|
+
<RoutingAgentForm configuration={snapshot.routingAgent} queues={snapshot.queues} loading={snapshot.routingLoading} error={snapshot.routingError} pending={snapshot.routingPending} canManage={snapshot.canManage} />
|
|
102
|
+
<div className="queue-list" aria-busy={snapshot.loading}>
|
|
103
|
+
{snapshot.loading ? <><div className="skeleton-card" /><div className="skeleton-card" /></> : snapshot.queues.length ? snapshot.queues.map((queue) => <QueueCard queue={queue} canManage={snapshot.canManage} key={queue.id} />) : <div className="empty compact"><h3>No report queues configured</h3><p>Create a queue to define ownership and response policy.</p></div>}
|
|
104
|
+
</div>
|
|
105
|
+
<p className="error surface-error" role="alert">{snapshot.error}</p>
|
|
106
|
+
</section>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { errorMessage, mutationHeaders, requestJson } from "../lib/http";
|
|
2
|
+
import { dispatchReportRefresh } from "../reports/events";
|
|
3
|
+
import { getShellSnapshot, subscribeToShell } from "../shell/store";
|
|
4
|
+
import { queueEvents } from "./events";
|
|
5
|
+
import { getQueueWorkspaceSnapshot, setQueueWorkspaceSnapshot } from "./store";
|
|
6
|
+
import type { QueuePublishInput, QueueRecord, QueueWorkspaceSnapshot, RoutingAgentConfiguration, RoutingAgentInput } from "./types";
|
|
7
|
+
|
|
8
|
+
interface QueueListResponse { queues?: QueueRecord[] }
|
|
9
|
+
interface RoutingAgentResponse { configurations?: RoutingAgentConfiguration[] }
|
|
10
|
+
|
|
11
|
+
const initialSnapshot = (): QueueWorkspaceSnapshot => ({
|
|
12
|
+
loading: true,
|
|
13
|
+
error: "",
|
|
14
|
+
routingLoading: true,
|
|
15
|
+
routingError: "",
|
|
16
|
+
routingPending: false,
|
|
17
|
+
queuePending: false,
|
|
18
|
+
queues: [],
|
|
19
|
+
routingAgent: null,
|
|
20
|
+
canManage: false,
|
|
21
|
+
editor: { open: false, queueId: null, error: "" },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let actorId = "";
|
|
25
|
+
let started = false;
|
|
26
|
+
|
|
27
|
+
function permissions(snapshot: QueueWorkspaceSnapshot): QueueWorkspaceSnapshot {
|
|
28
|
+
return { ...snapshot, canManage: getShellSnapshot().session?.permissions.manageConfiguration === true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function loadQueues(): Promise<void> {
|
|
32
|
+
const session = getShellSnapshot().session;
|
|
33
|
+
if (!session) return;
|
|
34
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions({ ...snapshot, error: "" }));
|
|
35
|
+
try {
|
|
36
|
+
const suffix = session.permissions.manageConfiguration ? "?include_archived=true" : "";
|
|
37
|
+
const response = await requestJson<QueueListResponse>(`/v1/admin/queues${suffix}`);
|
|
38
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions({ ...snapshot, queues: response.queues || [] }));
|
|
39
|
+
} catch (cause) {
|
|
40
|
+
setQueueWorkspaceSnapshot((snapshot) => ({ ...snapshot, error: errorMessage(cause, "Queues could not be loaded.") }));
|
|
41
|
+
} finally {
|
|
42
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions({ ...snapshot, loading: false }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function loadRoutingAgent(): Promise<void> {
|
|
47
|
+
if (!getShellSnapshot().session) return;
|
|
48
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions({ ...snapshot, routingError: "" }));
|
|
49
|
+
try {
|
|
50
|
+
const response = await requestJson<RoutingAgentResponse>("/v1/admin/routing-agent");
|
|
51
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions({ ...snapshot, routingAgent: response.configurations?.[0] || null }));
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
setQueueWorkspaceSnapshot((snapshot) => ({ ...snapshot, routingError: errorMessage(cause, "The routing agent configuration could not be loaded.") }));
|
|
54
|
+
} finally {
|
|
55
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions({ ...snapshot, routingLoading: false }));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function publishRoutingAgent(input: RoutingAgentInput): Promise<void> {
|
|
60
|
+
const snapshot = getQueueWorkspaceSnapshot();
|
|
61
|
+
if (!snapshot.canManage || snapshot.routingPending) return;
|
|
62
|
+
setQueueWorkspaceSnapshot({ ...snapshot, routingError: "", routingPending: true });
|
|
63
|
+
try {
|
|
64
|
+
await requestJson("/v1/admin/routing-agent", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: mutationHeaders(),
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
enabled: input.rolloutMode !== "off",
|
|
69
|
+
rollout_mode: input.rolloutMode,
|
|
70
|
+
model: input.model,
|
|
71
|
+
instructions: input.instructions,
|
|
72
|
+
candidate_queue_ids: input.candidateQueueIds,
|
|
73
|
+
safeguard_queue_id: input.safeguardQueueId,
|
|
74
|
+
default_queue_id: input.defaultQueueId,
|
|
75
|
+
confidence_threshold: input.confidenceThreshold,
|
|
76
|
+
allowed_fields: input.allowedFields,
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
await loadRoutingAgent();
|
|
80
|
+
} catch (cause) {
|
|
81
|
+
setQueueWorkspaceSnapshot((current) => ({ ...current, routingError: errorMessage(cause, "The routing agent configuration could not be published.") }));
|
|
82
|
+
} finally {
|
|
83
|
+
setQueueWorkspaceSnapshot((current) => permissions({ ...current, routingPending: false }));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function openEditor(queueId: string | null): void {
|
|
88
|
+
const snapshot = getQueueWorkspaceSnapshot();
|
|
89
|
+
if (!snapshot.canManage) return;
|
|
90
|
+
setQueueWorkspaceSnapshot({ ...snapshot, editor: { open: true, queueId, error: "" } });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function closeEditor(): void {
|
|
94
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions({ ...snapshot, editor: { open: false, queueId: null, error: "" } }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function saveQueue(input: QueuePublishInput): Promise<void> {
|
|
98
|
+
const snapshot = getQueueWorkspaceSnapshot();
|
|
99
|
+
if (!snapshot.canManage || snapshot.queuePending || snapshot.editor.queueId !== input.queueId) return;
|
|
100
|
+
setQueueWorkspaceSnapshot({ ...snapshot, queuePending: true, editor: { ...snapshot.editor, error: "" } });
|
|
101
|
+
try {
|
|
102
|
+
const bounds = input.policy.aiBounds;
|
|
103
|
+
const policy = {
|
|
104
|
+
management_mode: input.policy.managementMode,
|
|
105
|
+
rollout_mode: input.policy.managementMode === "human" ? "off" : input.policy.rolloutMode,
|
|
106
|
+
priority: input.policy.priority,
|
|
107
|
+
response_sla_minutes: input.policy.responseSlaMinutes,
|
|
108
|
+
resolution_sla_minutes: input.policy.resolutionSlaMinutes,
|
|
109
|
+
claim_ttl_minutes: input.policy.claimTtlMinutes,
|
|
110
|
+
fallback_queue_id: input.policy.fallbackQueueId,
|
|
111
|
+
reviewer_instructions: input.policy.reviewerInstructions,
|
|
112
|
+
ai_instructions: input.policy.managementMode === "human" ? null : input.policy.aiInstructions,
|
|
113
|
+
ai_bounds: {
|
|
114
|
+
allowed_outcomes: bounds.allowedOutcomes,
|
|
115
|
+
confidence_threshold: bounds.confidenceThreshold,
|
|
116
|
+
max_follow_up_turns: bounds.maxFollowUpTurns,
|
|
117
|
+
autonomous_actions: false,
|
|
118
|
+
model: bounds.model,
|
|
119
|
+
policy_version_id: bounds.policyVersionId,
|
|
120
|
+
allowed_fields: bounds.allowedFields,
|
|
121
|
+
allowed_template_ids: bounds.allowedTemplateIds,
|
|
122
|
+
daily_report_budget: bounds.dailyReportBudget,
|
|
123
|
+
daily_cost_budget_microusd: bounds.dailyCostBudgetMicrousd,
|
|
124
|
+
input_cost_microusd_per_million: bounds.inputCostMicrousdPerMillion,
|
|
125
|
+
output_cost_microusd_per_million: bounds.outputCostMicrousdPerMillion,
|
|
126
|
+
quality_sample_rate: bounds.qualitySampleRate,
|
|
127
|
+
max_wait_hours: bounds.maxWaitHours,
|
|
128
|
+
minimum_shadow_runs: bounds.minimumShadowRuns,
|
|
129
|
+
minimum_quality_reviews: bounds.minimumQualityReviews,
|
|
130
|
+
maximum_failure_rate: bounds.maximumFailureRate,
|
|
131
|
+
maximum_disagreement_rate: bounds.maximumDisagreementRate,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const editingQueue = input.queueId ? snapshot.queues.find((queue) => queue.id === input.queueId) : null;
|
|
135
|
+
if (editingQueue) {
|
|
136
|
+
await requestJson(`/v1/admin/queues/${encodeURIComponent(editingQueue.id)}`, {
|
|
137
|
+
method: "PATCH",
|
|
138
|
+
headers: mutationHeaders(),
|
|
139
|
+
body: JSON.stringify({ name: input.name, description: input.description }),
|
|
140
|
+
});
|
|
141
|
+
await requestJson(`/v1/admin/queues/${encodeURIComponent(editingQueue.id)}/policy-versions`, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: mutationHeaders(),
|
|
144
|
+
body: JSON.stringify(policy),
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
const position = snapshot.queues.reduce((maximum, queue) => Math.max(maximum, Number(queue.position || 0)), 0) + 10;
|
|
148
|
+
await requestJson("/v1/admin/queues", {
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: mutationHeaders(),
|
|
151
|
+
body: JSON.stringify({ key: input.key, name: input.name, description: input.description, position, policy }),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
closeEditor();
|
|
155
|
+
await Promise.all([loadQueues(), loadRoutingAgent()]);
|
|
156
|
+
dispatchReportRefresh();
|
|
157
|
+
} catch (cause) {
|
|
158
|
+
setQueueWorkspaceSnapshot((current) => ({
|
|
159
|
+
...current,
|
|
160
|
+
editor: { ...current.editor, error: errorMessage(cause, "The queue policy could not be published.") },
|
|
161
|
+
}));
|
|
162
|
+
} finally {
|
|
163
|
+
setQueueWorkspaceSnapshot((current) => permissions({ ...current, queuePending: false }));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function startQueueController(): void {
|
|
168
|
+
if (started) return;
|
|
169
|
+
started = true;
|
|
170
|
+
subscribeToShell(() => {
|
|
171
|
+
const session = getShellSnapshot().session;
|
|
172
|
+
const nextActorId = session?.actor.id || "";
|
|
173
|
+
if (nextActorId === actorId) {
|
|
174
|
+
setQueueWorkspaceSnapshot((snapshot) => permissions(snapshot));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
actorId = nextActorId;
|
|
178
|
+
setQueueWorkspaceSnapshot(initialSnapshot());
|
|
179
|
+
if (session) void Promise.all([loadQueues(), loadRoutingAgent()]);
|
|
180
|
+
});
|
|
181
|
+
window.addEventListener(queueEvents.create, () => openEditor(null));
|
|
182
|
+
window.addEventListener(queueEvents.edit, (event) => {
|
|
183
|
+
if (event instanceof CustomEvent && typeof event.detail?.queueId === "string") openEditor(event.detail.queueId);
|
|
184
|
+
});
|
|
185
|
+
window.addEventListener(queueEvents.closeEditor, closeEditor);
|
|
186
|
+
window.addEventListener(queueEvents.publishRouting, (event) => {
|
|
187
|
+
if (!(event instanceof CustomEvent) || !event.detail || !Array.isArray(event.detail.candidateQueueIds) || !Array.isArray(event.detail.allowedFields)) return;
|
|
188
|
+
void publishRoutingAgent(event.detail as RoutingAgentInput);
|
|
189
|
+
});
|
|
190
|
+
window.addEventListener(queueEvents.publishQueue, (event) => {
|
|
191
|
+
if (!(event instanceof CustomEvent) || !event.detail?.policy?.aiBounds) return;
|
|
192
|
+
void saveQueue(event.detail as QueuePublishInput);
|
|
193
|
+
});
|
|
194
|
+
}
|