@open-mercato/ai-assistant 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +29 -12
  3. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +0 -30
  4. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
  5. package/dist/modules/ai_assistant/api/ai/chat/route.js +0 -7
  6. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  7. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +10 -10
  8. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
  9. package/dist/modules/ai_assistant/api/settings/route.js +13 -34
  10. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  11. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +0 -106
  12. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  13. package/dist/modules/ai_assistant/data/entities.js +0 -42
  14. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  15. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +1 -3
  16. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  17. package/dist/modules/ai_assistant/di.js +2 -6
  18. package/dist/modules/ai_assistant/di.js.map +2 -2
  19. package/dist/modules/ai_assistant/events.js +0 -6
  20. package/dist/modules/ai_assistant/events.js.map +2 -2
  21. package/dist/modules/ai_assistant/i18n/de.json +0 -42
  22. package/dist/modules/ai_assistant/i18n/en.json +0 -42
  23. package/dist/modules/ai_assistant/i18n/es.json +0 -42
  24. package/dist/modules/ai_assistant/i18n/pl.json +0 -42
  25. package/dist/modules/ai_assistant/lib/agent-runtime.js +5 -158
  26. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  27. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  28. package/dist/modules/ai_assistant/lib/auth.js +7 -6
  29. package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
  30. package/dist/modules/ai_assistant/lib/entity-graph.js +3 -3
  31. package/dist/modules/ai_assistant/lib/entity-graph.js.map +2 -2
  32. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +0 -3
  33. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
  34. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +1 -8
  35. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
  36. package/dist/modules/ai_assistant/lib/model-factory.js +1 -6
  37. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  38. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +0 -2
  39. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
  40. package/dist/modules/ai_assistant/lib/tool-search.js +12 -4
  41. package/dist/modules/ai_assistant/lib/tool-search.js.map +2 -2
  42. package/generated/entities/ai_agent_runtime_override/index.ts +0 -1
  43. package/generated/entities.ids.generated.ts +1 -2
  44. package/generated/entity-fields-registry.ts +1 -13
  45. package/package.json +12 -13
  46. package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +0 -48
  47. package/src/modules/ai_assistant/api/ai/chat/route.ts +0 -10
  48. package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +8 -58
  49. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +12 -10
  50. package/src/modules/ai_assistant/api/settings/route.ts +10 -33
  51. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +0 -146
  52. package/src/modules/ai_assistant/data/entities.ts +0 -56
  53. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +0 -4
  54. package/src/modules/ai_assistant/di.ts +1 -5
  55. package/src/modules/ai_assistant/events.ts +0 -21
  56. package/src/modules/ai_assistant/i18n/de.json +0 -42
  57. package/src/modules/ai_assistant/i18n/en.json +0 -42
  58. package/src/modules/ai_assistant/i18n/es.json +0 -42
  59. package/src/modules/ai_assistant/i18n/pl.json +0 -42
  60. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +5 -2
  61. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +0 -13
  62. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +0 -41
  63. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +0 -60
  64. package/src/modules/ai_assistant/lib/agent-runtime.ts +0 -259
  65. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +0 -10
  66. package/src/modules/ai_assistant/lib/auth.ts +15 -6
  67. package/src/modules/ai_assistant/lib/entity-graph.ts +3 -3
  68. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +0 -10
  69. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +1 -32
  70. package/src/modules/ai_assistant/lib/model-factory.ts +0 -33
  71. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +0 -2
  72. package/src/modules/ai_assistant/lib/tool-search.ts +17 -4
  73. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +16 -388
  74. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js +0 -80
  75. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js.map +0 -7
  76. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js +0 -124
  77. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js.map +0 -7
  78. package/dist/modules/ai_assistant/api/moderation-flags/route.js +0 -121
  79. package/dist/modules/ai_assistant/api/moderation-flags/route.js.map +0 -7
  80. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +0 -139
  81. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +0 -7
  82. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js +0 -10
  83. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js.map +0 -7
  84. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js +0 -26
  85. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js.map +0 -7
  86. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js +0 -44
  87. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js.map +0 -7
  88. package/dist/modules/ai_assistant/i18n/ko.json +0 -527
  89. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js +0 -43
  90. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js.map +0 -7
  91. package/dist/modules/ai_assistant/lib/moderation-policy.js +0 -24
  92. package/dist/modules/ai_assistant/lib/moderation-policy.js.map +0 -7
  93. package/dist/modules/ai_assistant/lib/moderation.js +0 -109
  94. package/dist/modules/ai_assistant/lib/moderation.js.map +0 -7
  95. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js +0 -19
  96. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js.map +0 -7
  97. package/generated/entities/ai_moderation_flag/index.ts +0 -9
  98. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts +0 -126
  99. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts +0 -161
  100. package/src/modules/ai_assistant/api/moderation-flags/__tests__/route.test.ts +0 -122
  101. package/src/modules/ai_assistant/api/moderation-flags/route.ts +0 -137
  102. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +0 -186
  103. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts +0 -24
  104. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx +0 -12
  105. package/src/modules/ai_assistant/data/repositories/AiModerationFlagRepository.ts +0 -79
  106. package/src/modules/ai_assistant/i18n/ko.json +0 -527
  107. package/src/modules/ai_assistant/lib/__tests__/input-moderation-gate.test.ts +0 -121
  108. package/src/modules/ai_assistant/lib/__tests__/moderation-flag-recorder.test.ts +0 -76
  109. package/src/modules/ai_assistant/lib/__tests__/moderation-policy.test.ts +0 -86
  110. package/src/modules/ai_assistant/lib/__tests__/moderation.test.ts +0 -114
  111. package/src/modules/ai_assistant/lib/moderation-flag-recorder.ts +0 -75
  112. package/src/modules/ai_assistant/lib/moderation-policy.ts +0 -77
  113. package/src/modules/ai_assistant/lib/moderation.ts +0 -179
  114. package/src/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.ts +0 -21
@@ -1,80 +0,0 @@
1
- import { test, expect, request as playwrightRequest } from "@playwright/test";
2
- import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
3
- import { getTokenScope, readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
4
- import {
5
- deleteAgentOverridesInDb,
6
- deleteModerationFlagsInDb,
7
- seedModerationFlagInDb
8
- } from "./helpers/aiAssistantFixtures.js";
9
- const SETTINGS = "/api/ai_assistant/settings";
10
- const FLAGS = "/api/ai_assistant/moderation-flags";
11
- test.describe("TC-AI-MODERATION-008: input moderation settings + audit", () => {
12
- test("PUT inputModeration persists per agent and round-trips on/off/inherit", async ({ request }) => {
13
- test.slow();
14
- const adminToken = await getAuthToken(request, "admin");
15
- const { tenantId } = getTokenScope(adminToken);
16
- const agentId = "it.moderation_settings";
17
- try {
18
- const on = await apiRequest(request, "PUT", SETTINGS, {
19
- token: adminToken,
20
- data: { agentId, inputModeration: true }
21
- });
22
- expect(on.status(), "PUT inputModeration=true returns 200").toBe(200);
23
- expect((await readJsonSafe(on))?.inputModeration).toBe(true);
24
- const off = await apiRequest(request, "PUT", SETTINGS, {
25
- token: adminToken,
26
- data: { agentId, inputModeration: false }
27
- });
28
- expect(off.status()).toBe(200);
29
- expect((await readJsonSafe(off))?.inputModeration).toBe(false);
30
- const inherit = await apiRequest(request, "PUT", SETTINGS, {
31
- token: adminToken,
32
- data: { agentId, inputModeration: null }
33
- });
34
- expect(inherit.status()).toBe(200);
35
- expect((await readJsonSafe(inherit))?.inputModeration).toBeNull();
36
- } finally {
37
- await deleteAgentOverridesInDb({ tenantId, agentId }).catch(() => void 0);
38
- }
39
- });
40
- test("GET /moderation-flags is tenant-scoped (cross-tenant rows are never returned)", async ({ request }) => {
41
- test.slow();
42
- const adminToken = await getAuthToken(request, "admin");
43
- const { tenantId } = getTokenScope(adminToken);
44
- const otherTenantId = "00000000-0000-4000-8000-0000000face7";
45
- let mineId = "";
46
- try {
47
- mineId = await seedModerationFlagInDb({ tenantId, agentId: "it.moderation_audit", userId: "it-user-mine" });
48
- await seedModerationFlagInDb({ tenantId: otherTenantId, agentId: "it.moderation_audit", userId: "it-user-other" });
49
- const res = await apiRequest(request, "GET", `${FLAGS}?pageSize=100`, { token: adminToken });
50
- expect(res.status()).toBe(200);
51
- const body = await readJsonSafe(res);
52
- expect(body?.items, "returns an items array").toBeTruthy();
53
- const ids = (body?.items ?? []).map((row) => row.id);
54
- expect(ids, "my tenant flag is visible").toContain(mineId);
55
- for (const row of body?.items ?? []) {
56
- expect(row.tenantId).toBe(tenantId);
57
- }
58
- } finally {
59
- await deleteModerationFlagsInDb(tenantId).catch(() => void 0);
60
- await deleteModerationFlagsInDb(otherTenantId).catch(() => void 0);
61
- }
62
- });
63
- test("GET /moderation-flags RBAC + validation gates", async ({ request, baseURL }) => {
64
- const adminToken = await getAuthToken(request, "admin");
65
- const tooLarge = await apiRequest(request, "GET", `${FLAGS}?pageSize=101`, { token: adminToken });
66
- expect(tooLarge.status()).toBe(400);
67
- expect((await readJsonSafe(tooLarge))?.code).toBe("validation_error");
68
- const employeeToken = await getAuthToken(request, "employee");
69
- const denied = await apiRequest(request, "GET", FLAGS, { token: employeeToken });
70
- expect(denied.status(), "employee lacks settings.manage -> 403").toBe(403);
71
- const anon = await playwrightRequest.newContext({ baseURL });
72
- try {
73
- const res = await anon.fetch(FLAGS, { method: "GET" });
74
- expect(res.status(), "unauthenticated GET is 401").toBe(401);
75
- } finally {
76
- await anon.dispose();
77
- }
78
- });
79
- });
80
- //# sourceMappingURL=TC-AI-MODERATION-008-settings-and-audit.spec.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts"],
4
- "sourcesContent": ["import { test, expect, request as playwrightRequest } from '@playwright/test';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport {\n deleteAgentOverridesInDb,\n deleteModerationFlagsInDb,\n seedModerationFlagInDb,\n} from './helpers/aiAssistantFixtures';\n\n/**\n * TC-AI-MODERATION-008 \u2014 Input-moderation settings + audit API.\n * Source: spec `.ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md`.\n *\n * Surfaces under test:\n * - PUT /api/ai_assistant/settings (persists `inputModeration` per agent)\n * - GET /api/ai_assistant/settings (reflects per-agent effective moderation policy)\n * - GET /api/ai_assistant/moderation-flags (tenant-scoped audit listing)\n *\n * Contract notes verified against the route handlers:\n * - PUT `{ agentId, inputModeration }` returns 200 echoing `inputModeration`.\n * - GET /settings exposes `agents[].moderation = { enforced, override, effective }`.\n * - GET /moderation-flags returns `{ items, total, page, pageSize }`, tenant-scoped,\n * gated by `ai_assistant.settings.manage`, `pageSize` capped at 100.\n *\n * Flags are seeded directly in the DB: a real flag is only ever born from the gate\n * during a flagged LLM turn (needs a live moderation call), so seeding keeps the\n * audit + isolation coverage deterministic and provider-free. This spec therefore\n * only runs under the coherent app+DB harness (`yarn test:integration` / `:ephemeral`).\n */\n\nconst SETTINGS = '/api/ai_assistant/settings';\nconst FLAGS = '/api/ai_assistant/moderation-flags';\n\ninterface FlagsResponse {\n items: Array<{ id: string; tenantId: string; agentId: string; userId: string; categories: Record<string, unknown> }>;\n total: number;\n page: number;\n pageSize: number;\n}\n\ntest.describe('TC-AI-MODERATION-008: input moderation settings + audit', () => {\n test('PUT inputModeration persists per agent and round-trips on/off/inherit', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId } = getTokenScope(adminToken);\n // A synthetic agent id keeps the test independent of the (empty-in-CI) live\n // agent registry; the PUT upserts an override row keyed by this id. The\n // per-agent `moderation` reflection on GET (registered agents only) is\n // covered by the route unit tests + the stubbed UI spec (TC-AI-MODERATION-009).\n const agentId = 'it.moderation_settings';\n\n try {\n const on = await apiRequest(request, 'PUT', SETTINGS, {\n token: adminToken,\n data: { agentId, inputModeration: true },\n });\n expect(on.status(), 'PUT inputModeration=true returns 200').toBe(200);\n expect((await readJsonSafe<{ inputModeration: boolean | null }>(on))?.inputModeration).toBe(true);\n\n const off = await apiRequest(request, 'PUT', SETTINGS, {\n token: adminToken,\n data: { agentId, inputModeration: false },\n });\n expect(off.status()).toBe(200);\n expect((await readJsonSafe<{ inputModeration: boolean | null }>(off))?.inputModeration).toBe(false);\n\n const inherit = await apiRequest(request, 'PUT', SETTINGS, {\n token: adminToken,\n data: { agentId, inputModeration: null },\n });\n expect(inherit.status()).toBe(200);\n expect((await readJsonSafe<{ inputModeration: boolean | null }>(inherit))?.inputModeration).toBeNull();\n } finally {\n await deleteAgentOverridesInDb({ tenantId, agentId }).catch(() => undefined);\n }\n });\n\n test('GET /moderation-flags is tenant-scoped (cross-tenant rows are never returned)', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId } = getTokenScope(adminToken);\n const otherTenantId = '00000000-0000-4000-8000-0000000face7';\n\n let mineId = '';\n try {\n mineId = await seedModerationFlagInDb({ tenantId, agentId: 'it.moderation_audit', userId: 'it-user-mine' });\n await seedModerationFlagInDb({ tenantId: otherTenantId, agentId: 'it.moderation_audit', userId: 'it-user-other' });\n\n const res = await apiRequest(request, 'GET', `${FLAGS}?pageSize=100`, { token: adminToken });\n expect(res.status()).toBe(200);\n const body = await readJsonSafe<FlagsResponse>(res);\n expect(body?.items, 'returns an items array').toBeTruthy();\n const ids = (body?.items ?? []).map((row) => row.id);\n expect(ids, 'my tenant flag is visible').toContain(mineId);\n // Every returned row is scoped to the caller's tenant \u2014 the other-tenant row never leaks.\n for (const row of body?.items ?? []) {\n expect(row.tenantId).toBe(tenantId);\n }\n } finally {\n await deleteModerationFlagsInDb(tenantId).catch(() => undefined);\n await deleteModerationFlagsInDb(otherTenantId).catch(() => undefined);\n }\n });\n\n test('GET /moderation-flags RBAC + validation gates', async ({ request, baseURL }) => {\n const adminToken = await getAuthToken(request, 'admin');\n\n // pageSize above the 100 cap is rejected by the zod query schema.\n const tooLarge = await apiRequest(request, 'GET', `${FLAGS}?pageSize=101`, { token: adminToken });\n expect(tooLarge.status()).toBe(400);\n expect((await readJsonSafe<{ code?: string }>(tooLarge))?.code).toBe('validation_error');\n\n // Employee carries ai_assistant.view but NOT ai_assistant.settings.manage.\n const employeeToken = await getAuthToken(request, 'employee');\n const denied = await apiRequest(request, 'GET', FLAGS, { token: employeeToken });\n expect(denied.status(), 'employee lacks settings.manage -> 403').toBe(403);\n\n const anon = await playwrightRequest.newContext({ baseURL });\n try {\n const res = await anon.fetch(FLAGS, { method: 'GET' });\n expect(res.status(), 'unauthenticated GET is 401').toBe(401);\n } finally {\n await anon.dispose();\n }\n });\n});\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,124 +0,0 @@
1
- import { test, expect } from "@playwright/test";
2
- import { login } from "@open-mercato/core/modules/core/__integration__/helpers/auth";
3
- const FLAGS_PAGE = "/backend/config/ai-assistant/moderation-flags";
4
- const AGENTS_PAGE = "/backend/config/ai-assistant/agents";
5
- test.describe("TC-AI-MODERATION-009: moderation UI surfaces", () => {
6
- test("audit page renders a flagged row with its category badge", async ({ page }) => {
7
- await login(page, "superadmin");
8
- await page.route("**/api/ai_assistant/moderation-flags**", async (route) => {
9
- await route.fulfill({
10
- status: 200,
11
- contentType: "application/json",
12
- body: JSON.stringify({
13
- items: [
14
- {
15
- id: "flag-it-1",
16
- tenantId: "t-1",
17
- organizationId: null,
18
- agentId: "support.portal_assistant",
19
- userId: "hashed-user-it",
20
- providerId: "openai",
21
- modelId: "gpt-5-mini",
22
- categories: { hate: { flagged: true, score: 0.97 }, violence: { flagged: false, score: 0.01 } },
23
- createdAt: "2026-06-10T12:00:00.000Z"
24
- }
25
- ],
26
- total: 1,
27
- page: 1,
28
- pageSize: 50
29
- })
30
- });
31
- });
32
- await page.goto(FLAGS_PAGE, { waitUntil: "domcontentloaded" });
33
- await expect(page.getByText("support.portal_assistant").first()).toBeVisible({ timeout: 2e4 });
34
- await expect(page.getByText("hashed-user-it").first()).toBeVisible();
35
- await expect(page.getByText("hate", { exact: true }).first()).toBeVisible();
36
- await expect(page.getByText("violence", { exact: true })).toHaveCount(0);
37
- });
38
- test("audit page renders the empty state when there are no flags", async ({ page }) => {
39
- await login(page, "superadmin");
40
- await page.route("**/api/ai_assistant/moderation-flags**", async (route) => {
41
- await route.fulfill({
42
- status: 200,
43
- contentType: "application/json",
44
- body: JSON.stringify({ items: [], total: 0, page: 1, pageSize: 50 })
45
- });
46
- });
47
- await page.goto(FLAGS_PAGE, { waitUntil: "domcontentloaded" });
48
- await expect(page.getByText(/No flagged messages/i).first()).toBeVisible({ timeout: 2e4 });
49
- });
50
- test("audit page redirects an unauthenticated visit to login", async ({ browser }) => {
51
- const context = await browser.newContext();
52
- const page = await context.newPage();
53
- try {
54
- await page.goto(FLAGS_PAGE, { waitUntil: "domcontentloaded" });
55
- await page.waitForURL(/\/login/, { timeout: 15e3 });
56
- expect(page.url()).toMatch(/\/login/);
57
- } finally {
58
- await context.close();
59
- }
60
- });
61
- test("agent settings renders the input-moderation section (enforced badge for untrusted agents)", async ({
62
- page
63
- }) => {
64
- await login(page, "superadmin");
65
- const agent = {
66
- id: "support.portal_assistant",
67
- moduleId: "support",
68
- label: "Portal assistant",
69
- description: "Customer-facing support assistant.",
70
- executionMode: "chat",
71
- readOnly: true,
72
- mutationPolicy: "read-only",
73
- untrustedInput: true,
74
- allowedTools: [],
75
- acceptedMediaTypes: [],
76
- tools: []
77
- };
78
- await page.route("**/api/ai_assistant/ai/agents", async (route) => {
79
- await route.fulfill({
80
- status: 200,
81
- contentType: "application/json",
82
- body: JSON.stringify({ agents: [agent], total: 1 })
83
- });
84
- });
85
- await page.route("**/api/ai_assistant/settings", async (route) => {
86
- await route.fulfill({
87
- status: 200,
88
- contentType: "application/json",
89
- body: JSON.stringify({
90
- availableProviders: [],
91
- agents: [
92
- {
93
- agentId: agent.id,
94
- moduleId: agent.moduleId,
95
- allowRuntimeModelOverride: true,
96
- codeDefaultProviderId: null,
97
- codeDefaultModelId: null,
98
- override: null,
99
- runtimeOverrideAllowlist: {
100
- env: null,
101
- tenant: null,
102
- effective: { providers: null, modelsByProvider: {}, hasRestrictions: false, tenantOverridesActive: false },
103
- envVarNames: { providers: "X", modelsByProvider: {} }
104
- },
105
- providerId: "openai",
106
- modelId: "gpt-5-mini",
107
- baseURL: null,
108
- source: "provider_default",
109
- moderation: { enforced: true, override: null, effective: "enforced" }
110
- }
111
- ]
112
- })
113
- });
114
- });
115
- await page.goto(AGENTS_PAGE, { waitUntil: "domcontentloaded" });
116
- const moderationSection = page.locator("[data-ai-agent-moderation]");
117
- const settingsContainer = page.locator("[data-ai-agent-settings]");
118
- await expect(moderationSection.or(settingsContainer).first()).toBeVisible({ timeout: 2e4 });
119
- if (await moderationSection.first().isVisible().catch(() => false)) {
120
- await expect(page.locator("[data-ai-agent-moderation-enforced]").first()).toBeVisible();
121
- }
122
- });
123
- });
124
- //# sourceMappingURL=TC-AI-MODERATION-009-ui.spec.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts"],
4
- "sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-MODERATION-009 \u2014 Moderation UI surfaces (Playwright).\n * Source: spec `.ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md`.\n *\n * Covers the two new backend UI surfaces deterministically by stubbing the\n * data routes (the page chrome + components render regardless of the live\n * registry / DB):\n * - `/backend/config/ai-assistant/moderation-flags` \u2014 audit DataTable.\n * - `/backend/config/ai-assistant/agents` \u2014 per-agent moderation section.\n *\n * The chat enforced\u2192rejection and chat-off\u2192passthrough flows are intentionally\n * NOT exercised here: the moderation gate runs SERVER-SIDE inside\n * `runAiAgentText`, so `page.route()` cannot stub it, and a live-provider e2e\n * would be non-deterministic (it needs a real OpenAI key + model behavior),\n * which `.ai/qa/AGENTS.md` forbids. Those branches are covered deterministically\n * by the gate unit tests (`input-moderation-gate.test.ts`), the moderation\n * service tests, and the `<AiChat>` `moderation_blocked` render test.\n */\n\nconst FLAGS_PAGE = '/backend/config/ai-assistant/moderation-flags';\nconst AGENTS_PAGE = '/backend/config/ai-assistant/agents';\n\ntest.describe('TC-AI-MODERATION-009: moderation UI surfaces', () => {\n test('audit page renders a flagged row with its category badge', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/moderation-flags**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n items: [\n {\n id: 'flag-it-1',\n tenantId: 't-1',\n organizationId: null,\n agentId: 'support.portal_assistant',\n userId: 'hashed-user-it',\n providerId: 'openai',\n modelId: 'gpt-5-mini',\n categories: { hate: { flagged: true, score: 0.97 }, violence: { flagged: false, score: 0.01 } },\n createdAt: '2026-06-10T12:00:00.000Z',\n },\n ],\n total: 1,\n page: 1,\n pageSize: 50,\n }),\n });\n });\n\n await page.goto(FLAGS_PAGE, { waitUntil: 'domcontentloaded' });\n\n await expect(page.getByText('support.portal_assistant').first()).toBeVisible({ timeout: 20_000 });\n await expect(page.getByText('hashed-user-it').first()).toBeVisible();\n // Only the flagged category renders as a badge; the non-flagged one is omitted.\n await expect(page.getByText('hate', { exact: true }).first()).toBeVisible();\n await expect(page.getByText('violence', { exact: true })).toHaveCount(0);\n });\n\n test('audit page renders the empty state when there are no flags', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/moderation-flags**', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ items: [], total: 0, page: 1, pageSize: 50 }),\n });\n });\n\n await page.goto(FLAGS_PAGE, { waitUntil: 'domcontentloaded' });\n await expect(page.getByText(/No flagged messages/i).first()).toBeVisible({ timeout: 20_000 });\n });\n\n test('audit page redirects an unauthenticated visit to login', async ({ browser }) => {\n const context = await browser.newContext();\n const page = await context.newPage();\n try {\n await page.goto(FLAGS_PAGE, { waitUntil: 'domcontentloaded' });\n await page.waitForURL(/\\/login/, { timeout: 15_000 });\n expect(page.url()).toMatch(/\\/login/);\n } finally {\n await context.close();\n }\n });\n\n test('agent settings renders the input-moderation section (enforced badge for untrusted agents)', async ({\n page,\n }) => {\n await login(page, 'superadmin');\n\n const agent = {\n id: 'support.portal_assistant',\n moduleId: 'support',\n label: 'Portal assistant',\n description: 'Customer-facing support assistant.',\n executionMode: 'chat',\n readOnly: true,\n mutationPolicy: 'read-only',\n untrustedInput: true,\n allowedTools: [],\n acceptedMediaTypes: [],\n tools: [],\n };\n\n await page.route('**/api/ai_assistant/ai/agents', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ agents: [agent], total: 1 }),\n });\n });\n await page.route('**/api/ai_assistant/settings', async (route) => {\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({\n availableProviders: [],\n agents: [\n {\n agentId: agent.id,\n moduleId: agent.moduleId,\n allowRuntimeModelOverride: true,\n codeDefaultProviderId: null,\n codeDefaultModelId: null,\n override: null,\n runtimeOverrideAllowlist: {\n env: null,\n tenant: null,\n effective: { providers: null, modelsByProvider: {}, hasRestrictions: false, tenantOverridesActive: false },\n envVarNames: { providers: 'X', modelsByProvider: {} },\n },\n providerId: 'openai',\n modelId: 'gpt-5-mini',\n baseURL: null,\n source: 'provider_default',\n moderation: { enforced: true, override: null, effective: 'enforced' },\n },\n ],\n }),\n });\n });\n\n await page.goto(AGENTS_PAGE, { waitUntil: 'domcontentloaded' });\n\n const moderationSection = page.locator('[data-ai-agent-moderation]');\n const settingsContainer = page.locator('[data-ai-agent-settings]');\n // Either the moderation section rendered (registry+detail loaded from the stub)\n // or at least the settings page chrome loaded \u2014 never a hard failure.\n await expect(moderationSection.or(settingsContainer).first()).toBeVisible({ timeout: 20_000 });\n\n if (await moderationSection.first().isVisible().catch(() => false)) {\n // The untrustedInput agent renders the non-editable Enforced badge.\n await expect(page.locator('[data-ai-agent-moderation-enforced]').first()).toBeVisible();\n }\n });\n});\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,121 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { z } from "zod";
3
- import { createLogger } from "@open-mercato/shared/lib/logger";
4
- import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
5
- import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
- import { AiModerationFlagRepository } from "../../data/repositories/AiModerationFlagRepository.js";
7
- import { hasRequiredFeatures } from "../../lib/auth.js";
8
- const logger = createLogger("ai_assistant");
9
- const REQUIRED_FEATURE = "ai_assistant.settings.manage";
10
- const MAX_PAGE_SIZE = 100;
11
- const querySchema = z.object({
12
- page: z.coerce.number().int().min(1).default(1),
13
- pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(50),
14
- agentId: z.string().min(1).max(256).optional(),
15
- userId: z.string().min(1).max(256).optional(),
16
- from: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "from must be a date in YYYY-MM-DD format").optional(),
17
- to: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "to must be a date in YYYY-MM-DD format").optional()
18
- });
19
- const openApi = {
20
- tag: "AI Assistant",
21
- summary: "Input moderation flags audit log",
22
- methods: {
23
- GET: {
24
- operationId: "aiAssistantModerationFlags",
25
- summary: "List input-moderation audit flags for the current tenant.",
26
- description: "Returns the append-only `ai_moderation_flags` audit rows (category flags + scores only \u2014 never prompt content) for the caller's tenant. Always tenant-scoped; cross-tenant access is impossible. Optionally filtered by `agentId`, `userId`, and a `from`/`to` date window. Paginated (`pageSize` \u2264 100). Requires `ai_assistant.settings.manage`.",
27
- query: querySchema,
28
- responses: [
29
- {
30
- status: 200,
31
- description: "Paginated moderation-flag rows: `{ items, total, page, pageSize }`.",
32
- mediaType: "application/json"
33
- }
34
- ],
35
- errors: [
36
- { status: 400, description: "Invalid query parameters." },
37
- { status: 401, description: "Unauthenticated caller." },
38
- { status: 403, description: "Caller lacks `ai_assistant.settings.manage`." },
39
- { status: 500, description: "Internal failure." }
40
- ]
41
- }
42
- }
43
- };
44
- const metadata = {
45
- path: "/ai_assistant/moderation-flags",
46
- GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
47
- };
48
- function jsonError(status, message, code, extra) {
49
- return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
50
- }
51
- async function GET(req) {
52
- const auth = await getAuthFromRequest(req);
53
- if (!auth) {
54
- return jsonError(401, "Unauthorized", "unauthenticated");
55
- }
56
- const { searchParams } = new URL(req.url);
57
- const queryResult = querySchema.safeParse({
58
- page: searchParams.get("page") ?? void 0,
59
- pageSize: searchParams.get("pageSize") ?? void 0,
60
- agentId: searchParams.get("agentId") ?? void 0,
61
- userId: searchParams.get("userId") ?? void 0,
62
- from: searchParams.get("from") ?? void 0,
63
- to: searchParams.get("to") ?? void 0
64
- });
65
- if (!queryResult.success) {
66
- return jsonError(400, "Invalid query parameters.", "validation_error", {
67
- issues: queryResult.error.issues
68
- });
69
- }
70
- const { page, pageSize, agentId, userId, from, to } = queryResult.data;
71
- try {
72
- const container = await createRequestContainer();
73
- const rbacService = container.resolve("rbacService");
74
- const acl = await rbacService.loadAcl(auth.sub, {
75
- tenantId: auth.tenantId,
76
- organizationId: auth.orgId
77
- });
78
- if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
79
- return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
80
- }
81
- if (!auth.tenantId) {
82
- return NextResponse.json({ items: [], total: 0, page, pageSize });
83
- }
84
- const em = container.resolve("em");
85
- const repo = new AiModerationFlagRepository(em);
86
- const { items, total } = await repo.list({
87
- tenantId: auth.tenantId,
88
- // Tenant-scoped abuse audit: do NOT narrow by organization_id. Tenant is
89
- // the hard isolation boundary; portal-surface flags often carry no org,
90
- // and a tenant admin reviewing abuse needs every flag in the tenant.
91
- agentId,
92
- userId,
93
- // `to` is an inclusive day; extend to end-of-day so same-day rows match.
94
- from: from ? /* @__PURE__ */ new Date(`${from}T00:00:00.000Z`) : void 0,
95
- to: to ? /* @__PURE__ */ new Date(`${to}T23:59:59.999Z`) : void 0,
96
- page,
97
- pageSize
98
- });
99
- const serialized = items.map((row) => ({
100
- id: row.id,
101
- tenantId: row.tenantId,
102
- organizationId: row.organizationId ?? null,
103
- agentId: row.agentId,
104
- userId: row.userId,
105
- providerId: row.providerId,
106
- modelId: row.modelId,
107
- categories: row.categories,
108
- createdAt: row.createdAt.toISOString()
109
- }));
110
- return NextResponse.json({ items: serialized, total, page, pageSize });
111
- } catch (error) {
112
- logger.error("Moderation flags GET failed", { err: error });
113
- return jsonError(500, "Failed to fetch moderation flags.", "internal_error");
114
- }
115
- }
116
- export {
117
- GET,
118
- metadata,
119
- openApi
120
- };
121
- //# sourceMappingURL=route.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../src/modules/ai_assistant/api/moderation-flags/route.ts"],
4
- "sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { AiModerationFlagRepository } from '../../data/repositories/AiModerationFlagRepository'\nimport { hasRequiredFeatures } from '../../lib/auth'\n\nconst logger = createLogger('ai_assistant')\n\nconst REQUIRED_FEATURE = 'ai_assistant.settings.manage'\nconst MAX_PAGE_SIZE = 100\n\nconst querySchema = z.object({\n page: z.coerce.number().int().min(1).default(1),\n pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(50),\n agentId: z.string().min(1).max(256).optional(),\n userId: z.string().min(1).max(256).optional(),\n from: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'from must be a date in YYYY-MM-DD format').optional(),\n to: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'to must be a date in YYYY-MM-DD format').optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Input moderation flags audit log',\n methods: {\n GET: {\n operationId: 'aiAssistantModerationFlags',\n summary: 'List input-moderation audit flags for the current tenant.',\n description:\n 'Returns the append-only `ai_moderation_flags` audit rows (category flags + scores only \u2014 never ' +\n 'prompt content) for the caller\\'s tenant. Always tenant-scoped; cross-tenant access is impossible. ' +\n 'Optionally filtered by `agentId`, `userId`, and a `from`/`to` date window. Paginated (`pageSize` \u2264 100). ' +\n 'Requires `ai_assistant.settings.manage`.',\n query: querySchema,\n responses: [\n {\n status: 200,\n description: 'Paginated moderation-flag rows: `{ items, total, page, pageSize }`.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.settings.manage`.' },\n { status: 500, description: 'Internal failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n path: '/ai_assistant/moderation-flags',\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(status: number, message: string, code: string, extra?: Record<string, unknown>): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function GET(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const { searchParams } = new URL(req.url)\n const queryResult = querySchema.safeParse({\n page: searchParams.get('page') ?? undefined,\n pageSize: searchParams.get('pageSize') ?? undefined,\n agentId: searchParams.get('agentId') ?? undefined,\n userId: searchParams.get('userId') ?? undefined,\n from: searchParams.get('from') ?? undefined,\n to: searchParams.get('to') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n const { page, pageSize, agentId, userId, from, to } = queryResult.data\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n\n if (!auth.tenantId) {\n return NextResponse.json({ items: [], total: 0, page, pageSize })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiModerationFlagRepository(em)\n const { items, total } = await repo.list({\n tenantId: auth.tenantId,\n // Tenant-scoped abuse audit: do NOT narrow by organization_id. Tenant is\n // the hard isolation boundary; portal-surface flags often carry no org,\n // and a tenant admin reviewing abuse needs every flag in the tenant.\n agentId,\n userId,\n // `to` is an inclusive day; extend to end-of-day so same-day rows match.\n from: from ? new Date(`${from}T00:00:00.000Z`) : undefined,\n to: to ? new Date(`${to}T23:59:59.999Z`) : undefined,\n page,\n pageSize,\n })\n\n const serialized = items.map((row) => ({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId ?? null,\n agentId: row.agentId,\n userId: row.userId,\n providerId: row.providerId,\n modelId: row.modelId,\n categories: row.categories,\n createdAt: row.createdAt.toISOString(),\n }))\n\n return NextResponse.json({ items: serialized, total, page, pageSize })\n } catch (error) {\n logger.error('Moderation flags GET failed', { err: error })\n return jsonError(500, 'Failed to fetch moderation flags.', 'internal_error')\n }\n}\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,139 +0,0 @@
1
- "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- import * as React from "react";
4
- import { useQuery } from "@tanstack/react-query";
5
- import { useT } from "@open-mercato/shared/lib/i18n/context";
6
- import { DataTable } from "@open-mercato/ui/backend/DataTable";
7
- import { Button } from "@open-mercato/ui/primitives/button";
8
- import { Input } from "@open-mercato/ui/primitives/input";
9
- import { Label } from "@open-mercato/ui/primitives/label";
10
- import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
11
- import { apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
12
- const PAGE_SIZE = 50;
13
- async function fetchModerationFlags(params) {
14
- const search = new URLSearchParams();
15
- search.set("page", String(params.page));
16
- search.set("pageSize", String(PAGE_SIZE));
17
- if (params.from) search.set("from", params.from);
18
- if (params.to) search.set("to", params.to);
19
- if (params.agentId) search.set("agentId", params.agentId);
20
- if (params.userId) search.set("userId", params.userId);
21
- const { result, status } = await apiCallOrThrow(
22
- `/api/ai_assistant/moderation-flags?${search.toString()}`,
23
- void 0,
24
- { errorMessage: "Failed to load moderation flags" }
25
- );
26
- if (!result) throw new Error(`Failed to load moderation flags (${status})`);
27
- return result;
28
- }
29
- function flaggedCategoryNames(categories) {
30
- return Object.entries(categories).filter(([, value]) => value.flagged).map(([name]) => name);
31
- }
32
- function AiModerationFlagsPageClient() {
33
- const t = useT();
34
- const [page, setPage] = React.useState(1);
35
- const [from, setFrom] = React.useState("");
36
- const [to, setTo] = React.useState("");
37
- const [agentId, setAgentId] = React.useState("");
38
- const [userId, setUserId] = React.useState("");
39
- const [applied, setApplied] = React.useState({ from: "", to: "", agentId: "", userId: "" });
40
- const query = useQuery({
41
- queryKey: ["ai_assistant", "moderation_flags", page, applied],
42
- queryFn: () => fetchModerationFlags({ page, ...applied }),
43
- retry: false
44
- });
45
- const applyFilters = React.useCallback(() => {
46
- setPage(1);
47
- setApplied({ from, to, agentId, userId });
48
- }, [from, to, agentId, userId]);
49
- const columns = React.useMemo(
50
- () => [
51
- {
52
- accessorKey: "agentId",
53
- header: t("ai_assistant.moderationFlags.columns.agent", "Agent")
54
- },
55
- {
56
- accessorKey: "userId",
57
- header: t("ai_assistant.moderationFlags.columns.user", "User"),
58
- meta: { truncate: true, maxWidth: 220 }
59
- },
60
- {
61
- accessorKey: "categories",
62
- header: t("ai_assistant.moderationFlags.columns.categories", "Categories"),
63
- cell: ({ row }) => {
64
- const names = flaggedCategoryNames(row.original.categories);
65
- if (names.length === 0) {
66
- return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
67
- }
68
- return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: names.map((name) => /* @__PURE__ */ jsx(StatusBadge, { variant: "error", dot: true, children: name }, name)) });
69
- }
70
- },
71
- {
72
- accessorKey: "createdAt",
73
- header: t("ai_assistant.moderationFlags.columns.createdAt", "Flagged at"),
74
- cell: ({ row }) => new Date(row.original.createdAt).toLocaleString()
75
- }
76
- ],
77
- [t]
78
- );
79
- const total = query.data?.total ?? 0;
80
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
81
- /* @__PURE__ */ jsxs("div", { children: [
82
- /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold", children: t("ai_assistant.moderationFlags.title", "Moderation flags") }),
83
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
84
- "ai_assistant.moderationFlags.subtitle",
85
- "Inputs blocked by the content safety filter. Category flags and scores only \u2014 no prompt content is stored."
86
- ) })
87
- ] }),
88
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-3", children: [
89
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
90
- /* @__PURE__ */ jsx(Label, { htmlFor: "moderation-from", children: t("ai_assistant.moderationFlags.filters.from", "From") }),
91
- /* @__PURE__ */ jsx(Input, { id: "moderation-from", type: "date", value: from, onChange: (e) => setFrom(e.target.value) })
92
- ] }),
93
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
94
- /* @__PURE__ */ jsx(Label, { htmlFor: "moderation-to", children: t("ai_assistant.moderationFlags.filters.to", "To") }),
95
- /* @__PURE__ */ jsx(Input, { id: "moderation-to", type: "date", value: to, onChange: (e) => setTo(e.target.value) })
96
- ] }),
97
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
98
- /* @__PURE__ */ jsx(Label, { htmlFor: "moderation-agent", children: t("ai_assistant.moderationFlags.columns.agent", "Agent") }),
99
- /* @__PURE__ */ jsx(
100
- Input,
101
- {
102
- id: "moderation-agent",
103
- value: agentId,
104
- onChange: (e) => setAgentId(e.target.value),
105
- placeholder: t("ai_assistant.moderationFlags.filters.agentPlaceholder", "module.agent")
106
- }
107
- )
108
- ] }),
109
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
110
- /* @__PURE__ */ jsx(Label, { htmlFor: "moderation-user", children: t("ai_assistant.moderationFlags.columns.user", "User") }),
111
- /* @__PURE__ */ jsx(Input, { id: "moderation-user", value: userId, onChange: (e) => setUserId(e.target.value) })
112
- ] }),
113
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: applyFilters, children: t("ai_assistant.moderationFlags.filters.apply", "Apply") })
114
- ] }),
115
- /* @__PURE__ */ jsx(
116
- DataTable,
117
- {
118
- columns,
119
- data: query.data?.items ?? [],
120
- isLoading: query.isLoading,
121
- error: query.error ? query.error.message : void 0,
122
- emptyState: t("ai_assistant.moderationFlags.empty", "No flagged messages."),
123
- pagination: {
124
- page,
125
- pageSize: PAGE_SIZE,
126
- total,
127
- totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),
128
- onPageChange: setPage
129
- }
130
- }
131
- )
132
- ] });
133
- }
134
- var AiModerationFlagsPageClient_default = AiModerationFlagsPageClient;
135
- export {
136
- AiModerationFlagsPageClient,
137
- AiModerationFlagsPageClient_default as default
138
- };
139
- //# sourceMappingURL=AiModerationFlagsPageClient.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../../../src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { ColumnDef } from '@tanstack/react-table'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst PAGE_SIZE = 50\n\ntype ModerationCategory = { flagged: boolean; score: number }\n\ntype ModerationFlagRow = {\n id: string\n tenantId: string\n organizationId: string | null\n agentId: string\n userId: string\n providerId: string\n modelId: string\n categories: Record<string, ModerationCategory>\n createdAt: string\n}\n\ntype ModerationFlagsResponse = {\n items: ModerationFlagRow[]\n total: number\n page: number\n pageSize: number\n}\n\nasync function fetchModerationFlags(params: {\n page: number\n from: string\n to: string\n agentId: string\n userId: string\n}): Promise<ModerationFlagsResponse> {\n const search = new URLSearchParams()\n search.set('page', String(params.page))\n search.set('pageSize', String(PAGE_SIZE))\n if (params.from) search.set('from', params.from)\n if (params.to) search.set('to', params.to)\n if (params.agentId) search.set('agentId', params.agentId)\n if (params.userId) search.set('userId', params.userId)\n const { result, status } = await apiCallOrThrow<ModerationFlagsResponse>(\n `/api/ai_assistant/moderation-flags?${search.toString()}`,\n undefined,\n { errorMessage: 'Failed to load moderation flags' },\n )\n if (!result) throw new Error(`Failed to load moderation flags (${status})`)\n return result\n}\n\nfunction flaggedCategoryNames(categories: Record<string, ModerationCategory>): string[] {\n return Object.entries(categories)\n .filter(([, value]) => value.flagged)\n .map(([name]) => name)\n}\n\nexport function AiModerationFlagsPageClient() {\n const t = useT()\n const [page, setPage] = React.useState(1)\n const [from, setFrom] = React.useState('')\n const [to, setTo] = React.useState('')\n const [agentId, setAgentId] = React.useState('')\n const [userId, setUserId] = React.useState('')\n const [applied, setApplied] = React.useState({ from: '', to: '', agentId: '', userId: '' })\n\n const query = useQuery<ModerationFlagsResponse>({\n queryKey: ['ai_assistant', 'moderation_flags', page, applied],\n queryFn: () => fetchModerationFlags({ page, ...applied }),\n retry: false,\n })\n\n const applyFilters = React.useCallback(() => {\n setPage(1)\n setApplied({ from, to, agentId, userId })\n }, [from, to, agentId, userId])\n\n const columns = React.useMemo<ColumnDef<ModerationFlagRow, unknown>[]>(\n () => [\n {\n accessorKey: 'agentId',\n header: t('ai_assistant.moderationFlags.columns.agent', 'Agent'),\n },\n {\n accessorKey: 'userId',\n header: t('ai_assistant.moderationFlags.columns.user', 'User'),\n meta: { truncate: true, maxWidth: 220 },\n },\n {\n accessorKey: 'categories',\n header: t('ai_assistant.moderationFlags.columns.categories', 'Categories'),\n cell: ({ row }) => {\n const names = flaggedCategoryNames(row.original.categories)\n if (names.length === 0) {\n return <span className=\"text-muted-foreground\">\u2014</span>\n }\n return (\n <div className=\"flex flex-wrap gap-1\">\n {names.map((name) => (\n <StatusBadge key={name} variant=\"error\" dot>\n {name}\n </StatusBadge>\n ))}\n </div>\n )\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('ai_assistant.moderationFlags.columns.createdAt', 'Flagged at'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ],\n [t],\n )\n\n const total = query.data?.total ?? 0\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div>\n <h2 className=\"text-base font-semibold\">\n {t('ai_assistant.moderationFlags.title', 'Moderation flags')}\n </h2>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ai_assistant.moderationFlags.subtitle',\n 'Inputs blocked by the content safety filter. Category flags and scores only \u2014 no prompt content is stored.',\n )}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-from\">{t('ai_assistant.moderationFlags.filters.from', 'From')}</Label>\n <Input id=\"moderation-from\" type=\"date\" value={from} onChange={(e) => setFrom(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-to\">{t('ai_assistant.moderationFlags.filters.to', 'To')}</Label>\n <Input id=\"moderation-to\" type=\"date\" value={to} onChange={(e) => setTo(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-agent\">{t('ai_assistant.moderationFlags.columns.agent', 'Agent')}</Label>\n <Input\n id=\"moderation-agent\"\n value={agentId}\n onChange={(e) => setAgentId(e.target.value)}\n placeholder={t('ai_assistant.moderationFlags.filters.agentPlaceholder', 'module.agent')}\n />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-user\">{t('ai_assistant.moderationFlags.columns.user', 'User')}</Label>\n <Input id=\"moderation-user\" value={userId} onChange={(e) => setUserId(e.target.value)} />\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={applyFilters}>\n {t('ai_assistant.moderationFlags.filters.apply', 'Apply')}\n </Button>\n </div>\n\n <DataTable<ModerationFlagRow>\n columns={columns}\n data={query.data?.items ?? []}\n isLoading={query.isLoading}\n error={query.error ? (query.error as Error).message : undefined}\n emptyState={t('ai_assistant.moderationFlags.empty', 'No flagged messages.')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),\n onPageChange: setPage,\n }}\n />\n </div>\n )\n}\n\nexport default AiModerationFlagsPageClient\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,10 +0,0 @@
1
- import { jsx } from "react/jsx-runtime";
2
- import { Page, PageBody } from "@open-mercato/ui/backend/Page";
3
- import { AiModerationFlagsPageClient } from "./AiModerationFlagsPageClient.js";
4
- async function AiModerationFlagsPage() {
5
- return /* @__PURE__ */ jsx(Page, { children: /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(AiModerationFlagsPageClient, {}) }) });
6
- }
7
- export {
8
- AiModerationFlagsPage as default
9
- };
10
- //# sourceMappingURL=page.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../../../src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx"],
4
- "sourcesContent": ["import { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { AiModerationFlagsPageClient } from './AiModerationFlagsPageClient'\n\nexport default async function AiModerationFlagsPage() {\n return (\n <Page>\n <PageBody>\n <AiModerationFlagsPageClient />\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "AAOQ;AAPR,SAAS,MAAM,gBAAgB;AAC/B,SAAS,mCAAmC;AAE5C,eAAO,wBAA+C;AACpD,SACE,oBAAC,QACC,8BAAC,YACC,8BAAC,+BAA4B,GAC/B,GACF;AAEJ;",
6
- "names": []
7
- }
@@ -1,26 +0,0 @@
1
- import React from "react";
2
- const moderationIcon = React.createElement(
3
- "svg",
4
- { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
5
- React.createElement("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }),
6
- React.createElement("path", { d: "M12 8v4" }),
7
- React.createElement("path", { d: "M12 16h.01" })
8
- );
9
- const metadata = {
10
- requireAuth: true,
11
- requireFeatures: ["ai_assistant.settings.manage"],
12
- pageTitle: "Moderation flags",
13
- pageTitleKey: "ai_assistant.moderationFlags.navTitle",
14
- pageGroup: "Module Configs",
15
- pageGroupKey: "settings.sections.moduleConfigs",
16
- pageOrder: 432,
17
- icon: moderationIcon,
18
- pageContext: "settings",
19
- breadcrumb: [
20
- { label: "Moderation flags", labelKey: "ai_assistant.moderationFlags.navTitle" }
21
- ]
22
- };
23
- export {
24
- metadata
25
- };
26
- //# sourceMappingURL=page.meta.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../../../../../src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts"],
4
- "sourcesContent": ["import React from 'react'\n\nconst moderationIcon = React.createElement(\n 'svg',\n { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },\n React.createElement('path', { d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }),\n React.createElement('path', { d: 'M12 8v4' }),\n React.createElement('path', { d: 'M12 16h.01' }),\n)\n\nexport const metadata = {\n requireAuth: true,\n requireFeatures: ['ai_assistant.settings.manage'],\n pageTitle: 'Moderation flags',\n pageTitleKey: 'ai_assistant.moderationFlags.navTitle',\n pageGroup: 'Module Configs',\n pageGroupKey: 'settings.sections.moduleConfigs',\n pageOrder: 432,\n icon: moderationIcon,\n pageContext: 'settings' as const,\n breadcrumb: [\n { label: 'Moderation flags', labelKey: 'ai_assistant.moderationFlags.navTitle' },\n ],\n} as const\n"],
5
- "mappings": "AAAA,OAAO,WAAW;AAElB,MAAM,iBAAiB,MAAM;AAAA,EAC3B;AAAA,EACA,EAAE,OAAO,IAAI,QAAQ,IAAI,SAAS,aAAa,MAAM,QAAQ,QAAQ,gBAAgB,aAAa,GAAG,eAAe,SAAS,gBAAgB,QAAQ;AAAA,EACrJ,MAAM,cAAc,QAAQ,EAAE,GAAG,8CAA8C,CAAC;AAAA,EAChF,MAAM,cAAc,QAAQ,EAAE,GAAG,UAAU,CAAC;AAAA,EAC5C,MAAM,cAAc,QAAQ,EAAE,GAAG,aAAa,CAAC;AACjD;AAEO,MAAM,WAAW;AAAA,EACtB,aAAa;AAAA,EACb,iBAAiB,CAAC,8BAA8B;AAAA,EAChD,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,cAAc;AAAA,EACd,WAAW;AAAA,EACX,MAAM;AAAA,EACN,aAAa;AAAA,EACb,YAAY;AAAA,IACV,EAAE,OAAO,oBAAoB,UAAU,wCAAwC;AAAA,EACjF;AACF;",
6
- "names": []
7
- }