@open-mercato/ai-assistant 0.6.3-develop.3901.1.ddad60693a → 0.6.3
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js +87 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js +119 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js.map +7 -0
- package/dist/modules/ai_assistant/acl.js +1 -0
- package/dist/modules/ai_assistant/acl.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +3 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js +128 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js +271 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +9 -1
- package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/conversations/route.js +4 -1
- package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +5 -1
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/components/ConversationShareButton.js +5 -0
- package/dist/modules/ai_assistant/components/ConversationShareButton.js.map +7 -0
- package/dist/modules/ai_assistant/components/ConversationShareDialog.js +5 -0
- package/dist/modules/ai_assistant/components/ConversationShareDialog.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +3 -0
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +235 -5
- package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +2 -2
- package/dist/modules/ai_assistant/events.js +14 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +17 -0
- package/dist/modules/ai_assistant/i18n/en.json +17 -0
- package/dist/modules/ai_assistant/i18n/es.json +17 -0
- package/dist/modules/ai_assistant/i18n/pl.json +17 -0
- package/dist/modules/ai_assistant/lib/conversation-storage.js +12 -3
- package/dist/modules/ai_assistant/lib/conversation-storage.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js +15 -0
- package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js.map +7 -0
- package/dist/modules/ai_assistant/notifications.client.js +30 -0
- package/dist/modules/ai_assistant/notifications.client.js.map +7 -0
- package/dist/modules/ai_assistant/notifications.js +27 -0
- package/dist/modules/ai_assistant/notifications.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +2 -1
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +59 -0
- package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +7 -0
- package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js +123 -0
- package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js.map +7 -0
- package/generated/entities/ai_chat_conversation_participant/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -8
- package/src/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.ts +117 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.ts +159 -0
- package/src/modules/ai_assistant/__tests__/integration/ai-chat-sharing.test.ts +406 -0
- package/src/modules/ai_assistant/acl.ts +1 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +3 -0
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.ts +149 -0
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.ts +314 -0
- package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +9 -1
- package/src/modules/ai_assistant/api/ai/conversations/route.ts +4 -1
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +4 -0
- package/src/modules/ai_assistant/components/ConversationShareButton.tsx +1 -0
- package/src/modules/ai_assistant/components/ConversationShareDialog.tsx +1 -0
- package/src/modules/ai_assistant/data/entities.ts +4 -0
- package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +270 -7
- package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +297 -3
- package/src/modules/ai_assistant/events.ts +31 -0
- package/src/modules/ai_assistant/i18n/__tests__/conversation-share-translations.test.ts +59 -0
- package/src/modules/ai_assistant/i18n/de.json +17 -0
- package/src/modules/ai_assistant/i18n/en.json +17 -0
- package/src/modules/ai_assistant/i18n/es.json +17 -0
- package/src/modules/ai_assistant/i18n/pl.json +17 -0
- package/src/modules/ai_assistant/lib/conversation-storage.ts +22 -1
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +25 -0
- package/src/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.ts +15 -0
- package/src/modules/ai_assistant/notifications.client.ts +29 -0
- package/src/modules/ai_assistant/notifications.ts +25 -0
- package/src/modules/ai_assistant/setup.ts +2 -1
- package/src/modules/ai_assistant/subscribers/__tests__/conversation-shared-notify.test.ts +116 -0
- package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +78 -0
- package/src/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.tsx +121 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/participants/%5BuserId%5D/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\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 { hasRequiredFeatures } from '../../../../../../lib/auth'\nimport {\n AiChatConversationAccessError,\n AiChatParticipantNotFoundError,\n createConversationStorage,\n} from '../../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst participantParamsSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n userId: z.string().uuid('userId must be a valid UUID'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Revoke a conversation participant',\n methods: {\n DELETE: {\n operationId: 'aiAssistantRevokeConversationParticipant',\n summary: 'Revoke a participant from a conversation (soft-delete).',\n description:\n 'Soft-deletes the participant row. If no active non-owner participants remain, ' +\n 'the conversation visibility is reset to \"private\". ' +\n 'Only the conversation owner or a manager may revoke participants.',\n responses: [\n {\n status: 204,\n description: 'Participant revoked.',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string; userId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return jsonError(401, 'Unauthorized', 'unauthenticated')\n const rawParams = await context.params\n const parseResult = participantParamsSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid path parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n if (!auth.tenantId) return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n const canShare = hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n try {\n const repo = createConversationStorage(container)\n await repo.revokeParticipant(\n parseResult.data.conversationId,\n parseResult.data.userId,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.unshared',\n {\n conversationId: parseResult.data.conversationId,\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n ownerUserId: auth.sub,\n participantUserId: parseResult.data.userId,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return new NextResponse(null, { status: 204 })\n } catch (err) {\n if (err instanceof AiChatParticipantNotFoundError) {\n return jsonError(404, err.message || 'Participant not found or already revoked.', 'participant_not_found')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, err.message || 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
4
|
+
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
6
|
+
import { User } from "@open-mercato/core/modules/auth/data/entities";
|
|
7
|
+
import { hasRequiredFeatures } from "../../../../../lib/auth.js";
|
|
8
|
+
import {
|
|
9
|
+
createConversationStorage,
|
|
10
|
+
AiChatConversationAccessError,
|
|
11
|
+
AiChatConversationDuplicateParticipantError
|
|
12
|
+
} from "../../../../../lib/conversation-storage.js";
|
|
13
|
+
import { emitAiAssistantEvent } from "../../../../../events.js";
|
|
14
|
+
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
15
|
+
const MANAGE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.manage";
|
|
16
|
+
const SHARE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.share";
|
|
17
|
+
const conversationIdParamSchema = z.object({
|
|
18
|
+
conversationId: z.string().trim().min(1, "conversationId must be a non-empty string").max(128, "conversationId exceeds the maximum length of 128 characters")
|
|
19
|
+
});
|
|
20
|
+
const addParticipantBodySchema = z.object({
|
|
21
|
+
userId: z.string().uuid("userId must be a valid UUID"),
|
|
22
|
+
role: z.enum(["viewer"]).default("viewer")
|
|
23
|
+
});
|
|
24
|
+
const openApi = {
|
|
25
|
+
tag: "AI Assistant",
|
|
26
|
+
summary: "Manage conversation participants",
|
|
27
|
+
methods: {
|
|
28
|
+
GET: {
|
|
29
|
+
operationId: "aiAssistantListConversationParticipants",
|
|
30
|
+
summary: "List active participants of a conversation.",
|
|
31
|
+
description: "Returns the list of active (non-revoked) participants for the conversation. Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.",
|
|
32
|
+
responses: [
|
|
33
|
+
{
|
|
34
|
+
status: 200,
|
|
35
|
+
description: "List of active participants.",
|
|
36
|
+
mediaType: "application/json"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
errors: [
|
|
40
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
41
|
+
{ status: 403, description: "Caller lacks required features." },
|
|
42
|
+
{ status: 404, description: "Conversation not found or not accessible." }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
POST: {
|
|
46
|
+
operationId: "aiAssistantAddConversationParticipant",
|
|
47
|
+
summary: "Add a participant to a conversation.",
|
|
48
|
+
description: "Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.",
|
|
49
|
+
responses: [
|
|
50
|
+
{
|
|
51
|
+
status: 201,
|
|
52
|
+
description: 'Participant added; conversation visibility updated to "shared".',
|
|
53
|
+
mediaType: "application/json"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
errors: [
|
|
57
|
+
{ status: 400, description: "Invalid request body." },
|
|
58
|
+
{ status: 401, description: "Unauthenticated caller." },
|
|
59
|
+
{ status: 403, description: "Caller lacks required feature or is not the owner." },
|
|
60
|
+
{ status: 404, description: "Conversation not found." }
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const metadata = {
|
|
66
|
+
GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },
|
|
67
|
+
POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
|
|
68
|
+
};
|
|
69
|
+
function jsonError(status, message, code, extra) {
|
|
70
|
+
return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
|
|
71
|
+
}
|
|
72
|
+
async function resolveCallerContext(req, context) {
|
|
73
|
+
const auth = await getAuthFromRequest(req);
|
|
74
|
+
if (!auth) return { kind: "unauthorized" };
|
|
75
|
+
const rawParams = await context.params;
|
|
76
|
+
const parseResult = conversationIdParamSchema.safeParse(rawParams);
|
|
77
|
+
if (!parseResult.success) {
|
|
78
|
+
return { kind: "invalid-id", issues: parseResult.error.issues };
|
|
79
|
+
}
|
|
80
|
+
const container = await createRequestContainer();
|
|
81
|
+
const rbacService = container.resolve("rbacService");
|
|
82
|
+
const acl = await rbacService.loadAcl(auth.sub, {
|
|
83
|
+
tenantId: auth.tenantId,
|
|
84
|
+
organizationId: auth.orgId
|
|
85
|
+
});
|
|
86
|
+
if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
|
|
87
|
+
return { kind: "forbidden" };
|
|
88
|
+
}
|
|
89
|
+
if (!auth.tenantId) return { kind: "missing-tenant" };
|
|
90
|
+
return {
|
|
91
|
+
kind: "ok",
|
|
92
|
+
tenantId: auth.tenantId,
|
|
93
|
+
organizationId: auth.orgId ?? null,
|
|
94
|
+
userId: auth.sub,
|
|
95
|
+
conversationId: parseResult.data.conversationId,
|
|
96
|
+
canManageConversations: hasRequiredFeatures(
|
|
97
|
+
[MANAGE_CONVERSATIONS_FEATURE],
|
|
98
|
+
acl.features,
|
|
99
|
+
acl.isSuperAdmin,
|
|
100
|
+
rbacService
|
|
101
|
+
),
|
|
102
|
+
canShare: hasRequiredFeatures(
|
|
103
|
+
[SHARE_CONVERSATIONS_FEATURE],
|
|
104
|
+
acl.features,
|
|
105
|
+
acl.isSuperAdmin,
|
|
106
|
+
rbacService
|
|
107
|
+
)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function GET(req, context) {
|
|
111
|
+
const callerCtx = await resolveCallerContext(req, context);
|
|
112
|
+
if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
|
|
113
|
+
if (callerCtx.kind === "invalid-id") {
|
|
114
|
+
return jsonError(400, "Invalid conversation id.", "validation_error", {
|
|
115
|
+
issues: callerCtx.issues
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (callerCtx.kind === "forbidden") {
|
|
119
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
120
|
+
}
|
|
121
|
+
if (callerCtx.kind === "missing-tenant") {
|
|
122
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const container = await createRequestContainer();
|
|
126
|
+
const repo = createConversationStorage(container);
|
|
127
|
+
const repoCtx = {
|
|
128
|
+
tenantId: callerCtx.tenantId,
|
|
129
|
+
organizationId: callerCtx.organizationId,
|
|
130
|
+
userId: callerCtx.userId,
|
|
131
|
+
canManageConversations: callerCtx.canManageConversations
|
|
132
|
+
};
|
|
133
|
+
const conversation = await repo.getById(callerCtx.conversationId, repoCtx);
|
|
134
|
+
if (!conversation) {
|
|
135
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
136
|
+
}
|
|
137
|
+
const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx);
|
|
138
|
+
return NextResponse.json({
|
|
139
|
+
ownerUserId: conversation.ownerUserId,
|
|
140
|
+
participants: participants.map((p) => ({
|
|
141
|
+
userId: p.userId,
|
|
142
|
+
role: p.role,
|
|
143
|
+
lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,
|
|
144
|
+
addedAt: p.createdAt.toISOString()
|
|
145
|
+
}))
|
|
146
|
+
});
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err instanceof AiChatConversationAccessError) {
|
|
149
|
+
return jsonError(403, "Access denied.", "forbidden");
|
|
150
|
+
}
|
|
151
|
+
return jsonError(500, "Internal server error.", "internal_error");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function POST(req, context) {
|
|
155
|
+
const callerCtx = await resolveCallerContext(req, context);
|
|
156
|
+
if (callerCtx.kind === "unauthorized") return jsonError(401, "Unauthorized", "unauthenticated");
|
|
157
|
+
if (callerCtx.kind === "invalid-id") {
|
|
158
|
+
return jsonError(400, "Invalid conversation id.", "validation_error", {
|
|
159
|
+
issues: callerCtx.issues
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (callerCtx.kind === "forbidden") {
|
|
163
|
+
return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
|
|
164
|
+
}
|
|
165
|
+
if (callerCtx.kind === "missing-tenant") {
|
|
166
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
167
|
+
}
|
|
168
|
+
if (!callerCtx.canShare) {
|
|
169
|
+
return jsonError(
|
|
170
|
+
403,
|
|
171
|
+
`Caller lacks required feature "${SHARE_CONVERSATIONS_FEATURE}".`,
|
|
172
|
+
"forbidden"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
let body;
|
|
176
|
+
try {
|
|
177
|
+
body = await req.json();
|
|
178
|
+
} catch {
|
|
179
|
+
return jsonError(400, "Invalid JSON body.", "invalid_body");
|
|
180
|
+
}
|
|
181
|
+
const parseResult = addParticipantBodySchema.safeParse(body);
|
|
182
|
+
if (!parseResult.success) {
|
|
183
|
+
return jsonError(400, "Invalid request body.", "validation_error", {
|
|
184
|
+
issues: parseResult.error.issues
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const targetUserId = parseResult.data.userId;
|
|
188
|
+
if (targetUserId === callerCtx.userId) {
|
|
189
|
+
return jsonError(400, "Cannot share a conversation with yourself.", "self_share_not_allowed");
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const container = await createRequestContainer();
|
|
193
|
+
const em = container.resolve("em");
|
|
194
|
+
const targetUserFilter = {
|
|
195
|
+
id: targetUserId,
|
|
196
|
+
tenantId: callerCtx.tenantId,
|
|
197
|
+
deletedAt: null,
|
|
198
|
+
...callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}
|
|
199
|
+
};
|
|
200
|
+
const targetUser = await findOneWithDecryption(
|
|
201
|
+
em,
|
|
202
|
+
User,
|
|
203
|
+
targetUserFilter,
|
|
204
|
+
{},
|
|
205
|
+
{ tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId }
|
|
206
|
+
);
|
|
207
|
+
if (!targetUser) {
|
|
208
|
+
return jsonError(
|
|
209
|
+
400,
|
|
210
|
+
"Target user must be a staff user in the same tenant and organization.",
|
|
211
|
+
"user_not_found"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const repo = createConversationStorage(container);
|
|
215
|
+
const participant = await repo.addParticipant(
|
|
216
|
+
callerCtx.conversationId,
|
|
217
|
+
targetUserId,
|
|
218
|
+
parseResult.data.role,
|
|
219
|
+
{
|
|
220
|
+
tenantId: callerCtx.tenantId,
|
|
221
|
+
organizationId: callerCtx.organizationId,
|
|
222
|
+
userId: callerCtx.userId,
|
|
223
|
+
canManageConversations: callerCtx.canManageConversations
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
try {
|
|
227
|
+
await emitAiAssistantEvent(
|
|
228
|
+
"ai_assistant.conversation.shared",
|
|
229
|
+
{
|
|
230
|
+
conversationId: callerCtx.conversationId,
|
|
231
|
+
tenantId: callerCtx.tenantId,
|
|
232
|
+
organizationId: callerCtx.organizationId,
|
|
233
|
+
ownerUserId: callerCtx.userId,
|
|
234
|
+
participantUserId: participant.userId,
|
|
235
|
+
role: participant.role
|
|
236
|
+
},
|
|
237
|
+
{ persistent: false }
|
|
238
|
+
);
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
return NextResponse.json(
|
|
242
|
+
{
|
|
243
|
+
participant: {
|
|
244
|
+
userId: participant.userId,
|
|
245
|
+
role: participant.role,
|
|
246
|
+
lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,
|
|
247
|
+
addedAt: participant.createdAt.toISOString()
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{ status: 201 }
|
|
251
|
+
);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof AiChatConversationDuplicateParticipantError) {
|
|
254
|
+
return jsonError(409, err.message, "duplicate_participant");
|
|
255
|
+
}
|
|
256
|
+
if (err instanceof AiChatConversationAccessError) {
|
|
257
|
+
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
258
|
+
}
|
|
259
|
+
if (err instanceof Error && err.message.toLowerCase().includes("owner")) {
|
|
260
|
+
return jsonError(403, err.message, "forbidden");
|
|
261
|
+
}
|
|
262
|
+
return jsonError(500, "Internal server error.", "internal_error");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export {
|
|
266
|
+
GET,
|
|
267
|
+
POST,
|
|
268
|
+
metadata,
|
|
269
|
+
openApi
|
|
270
|
+
};
|
|
271
|
+
//# sourceMappingURL=route.js.map
|
package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/participants/route.ts"],
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { FilterQuery } from '@mikro-orm/core'\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 { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport {\n createConversationStorage,\n AiChatConversationAccessError,\n AiChatConversationDuplicateParticipantError,\n} from '../../../../../lib/conversation-storage'\nimport { emitAiAssistantEvent } from '../../../../../events'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\nconst SHARE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.share'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nconst addParticipantBodySchema = z.object({\n userId: z.string().uuid('userId must be a valid UUID'),\n role: z.enum(['viewer']).default('viewer'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Manage conversation participants',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversationParticipants',\n summary: 'List active participants of a conversation.',\n description:\n 'Returns the list of active (non-revoked) participants for the conversation. ' +\n 'Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.',\n responses: [\n {\n status: 200,\n description: 'List of active participants.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required features.' },\n { status: 404, description: 'Conversation not found or not accessible.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantAddConversationParticipant',\n summary: 'Add a participant to a conversation.',\n description:\n 'Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. ' +\n 'Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.',\n responses: [\n {\n status: 201,\n description: 'Participant added; conversation visibility updated to \"shared\".',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks required feature or is not the owner.' },\n { status: 404, description: 'Conversation not found.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n canShare: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations: hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n canShare: hasRequiredFeatures(\n [SHARE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n ),\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const repoCtx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n }\n const conversation = await repo.getById(callerCtx.conversationId, repoCtx)\n if (!conversation) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participants = await repo.listParticipants(callerCtx.conversationId, repoCtx)\n return NextResponse.json({\n ownerUserId: conversation.ownerUserId,\n participants: participants.map((p) => ({\n userId: p.userId,\n role: p.role,\n lastReadAt: p.lastReadAt ? p.lastReadAt.toISOString() : null,\n addedAt: p.createdAt.toISOString(),\n })),\n })\n } catch (err) {\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(403, 'Access denied.', 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (!callerCtx.canShare) {\n return jsonError(\n 403,\n `Caller lacks required feature \"${SHARE_CONVERSATIONS_FEATURE}\".`,\n 'forbidden',\n )\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return jsonError(400, 'Invalid JSON body.', 'invalid_body')\n }\n const parseResult = addParticipantBodySchema.safeParse(body)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n const targetUserId = parseResult.data.userId\n if (targetUserId === callerCtx.userId) {\n return jsonError(400, 'Cannot share a conversation with yourself.', 'self_share_not_allowed')\n }\n\n try {\n const container = await createRequestContainer()\n const em = container.resolve<EntityManager>('em')\n const targetUserFilter: FilterQuery<User> = {\n id: targetUserId,\n tenantId: callerCtx.tenantId,\n deletedAt: null,\n ...(callerCtx.organizationId ? { organizationId: callerCtx.organizationId } : {}),\n }\n const targetUser = await findOneWithDecryption<User>(\n em,\n User,\n targetUserFilter,\n {},\n { tenantId: callerCtx.tenantId, organizationId: callerCtx.organizationId },\n )\n if (!targetUser) {\n return jsonError(\n 400,\n 'Target user must be a staff user in the same tenant and organization.',\n 'user_not_found',\n )\n }\n\n const repo = createConversationStorage(container)\n const participant = await repo.addParticipant(\n callerCtx.conversationId,\n targetUserId,\n parseResult.data.role,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n try {\n await emitAiAssistantEvent(\n 'ai_assistant.conversation.shared',\n {\n conversationId: callerCtx.conversationId,\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n ownerUserId: callerCtx.userId,\n participantUserId: participant.userId,\n role: participant.role,\n },\n { persistent: false },\n )\n } catch {\n // non-fatal\n }\n return NextResponse.json(\n {\n participant: {\n userId: participant.userId,\n role: participant.role,\n lastReadAt: participant.lastReadAt ? participant.lastReadAt.toISOString() : null,\n addedAt: participant.createdAt.toISOString(),\n },\n },\n { status: 201 },\n )\n } catch (err) {\n if (err instanceof AiChatConversationDuplicateParticipantError) {\n return jsonError(409, err.message, 'duplicate_participant')\n }\n if (err instanceof AiChatConversationAccessError) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n if (err instanceof Error && err.message.toLowerCase().includes('owner')) {\n return jsonError(403, err.message, 'forbidden')\n }\n return jsonError(500, 'Internal server error.', 'internal_error')\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -160,8 +160,16 @@ async function GET(req, context) {
|
|
|
160
160
|
if (!transcript) {
|
|
161
161
|
return jsonError(404, "Conversation not found.", "conversation_not_found");
|
|
162
162
|
}
|
|
163
|
+
const participantCount = await repo.getParticipantCount(
|
|
164
|
+
callerCtx.tenantId,
|
|
165
|
+
callerCtx.organizationId,
|
|
166
|
+
callerCtx.conversationId
|
|
167
|
+
);
|
|
163
168
|
return NextResponse.json({
|
|
164
|
-
conversation: serializeAiChatConversation(transcript.conversation
|
|
169
|
+
conversation: serializeAiChatConversation(transcript.conversation, {
|
|
170
|
+
callerUserId: callerCtx.userId,
|
|
171
|
+
participantCount
|
|
172
|
+
}),
|
|
165
173
|
messages: transcript.messages.map(serializeAiChatMessage),
|
|
166
174
|
nextCursor: transcript.nextCursor
|
|
167
175
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/ai_assistant/api/ai/conversations/%5BconversationId%5D/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\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 {\n aiChatConversationTranscriptQuerySchema,\n aiChatConversationUpdateSchema,\n} from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n serializeAiChatMessage,\n} from '../../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-conversation AI chat operations',\n methods: {\n GET: {\n operationId: 'aiAssistantGetConversation',\n summary: 'Fetch a conversation summary and recent transcript.',\n description:\n 'Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. ' +\n 'View-only callers can load only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can load conversations across users in the same ' +\n 'tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor ' +\n 'returns the next older page when paging back through long transcripts.',\n responses: [\n {\n status: 200,\n description: 'Conversation transcript page for the authenticated owner.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path or query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n PATCH: {\n operationId: 'aiAssistantUpdateConversation',\n summary: 'Update an existing conversation.',\n description:\n 'Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting ' +\n '`status` to `closed` archives the conversation while keeping its transcript intact. ' +\n 'View-only callers can update only their own conversations; conversation managers can ' +\n 'update conversations in the same tenant/organization.',\n responses: [\n {\n status: 200,\n description: 'Updated conversation summary.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n DELETE: {\n operationId: 'aiAssistantDeleteConversation',\n summary: 'Soft-delete a conversation and its messages.',\n description:\n 'View-only callers can delete only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. ' +\n 'Marks the conversation row and every undeleted message row with a `deleted_at` timestamp ' +\n 'in one transaction. The transcript remains in the database for audit/restore until a future ' +\n 'retention worker hard-deletes it.',\n responses: [\n {\n status: 200,\n description: 'Soft-delete acknowledgment.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n PATCH: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n const url = new URL(req.url)\n const queryResult = aiChatConversationTranscriptQuerySchema.safeParse({\n limit: url.searchParams.get('limit') ?? undefined,\n before: url.searchParams.get('before') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const transcript = await repo.getTranscript(\n callerCtx.conversationId,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n limit: queryResult.data.limit,\n before: queryResult.data.before ?? null,\n },\n )\n if (!transcript) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n return NextResponse.json({\n conversation: serializeAiChatConversation(transcript.conversation),\n messages: transcript.messages.map(serializeAiChatMessage),\n nextCursor: transcript.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversation GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function PATCH(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n const parseResult = aiChatConversationUpdateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation patch.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const row = await repo.update(\n callerCtx.conversationId,\n parseResult.data,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n return NextResponse.json(serializeAiChatConversation(row))\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation PATCH] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to update conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n await repo.softDelete(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation DELETE] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to delete conversation.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\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 {\n aiChatConversationTranscriptQuerySchema,\n aiChatConversationUpdateSchema,\n} from '../../../../data/validators'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n serializeAiChatMessage,\n} from '../../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nconst conversationIdParamSchema = z.object({\n conversationId: z\n .string()\n .trim()\n .min(1, 'conversationId must be a non-empty string')\n .max(128, 'conversationId exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Per-conversation AI chat operations',\n methods: {\n GET: {\n operationId: 'aiAssistantGetConversation',\n summary: 'Fetch a conversation summary and recent transcript.',\n description:\n 'Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. ' +\n 'View-only callers can load only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can load conversations across users in the same ' +\n 'tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor ' +\n 'returns the next older page when paging back through long transcripts.',\n responses: [\n {\n status: 200,\n description: 'Conversation transcript page for the authenticated owner.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid path or query parameters.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n PATCH: {\n operationId: 'aiAssistantUpdateConversation',\n summary: 'Update an existing conversation.',\n description:\n 'Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting ' +\n '`status` to `closed` archives the conversation while keeping its transcript intact. ' +\n 'View-only callers can update only their own conversations; conversation managers can ' +\n 'update conversations in the same tenant/organization.',\n responses: [\n {\n status: 200,\n description: 'Updated conversation summary.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n DELETE: {\n operationId: 'aiAssistantDeleteConversation',\n summary: 'Soft-delete a conversation and its messages.',\n description:\n 'View-only callers can delete only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. ' +\n 'Marks the conversation row and every undeleted message row with a `deleted_at` timestamp ' +\n 'in one transaction. The transcript remains in the database for audit/restore until a future ' +\n 'retention worker hard-deletes it.',\n responses: [\n {\n status: 200,\n description: 'Soft-delete acknowledgment.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No conversation accessible to the caller.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n PATCH: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ conversationId: string }>\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function resolveCallerContext(req: NextRequest, context: RouteContext): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | { kind: 'invalid-id'; issues: unknown }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n conversationId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\n const rawParams = await context.params\n const parseResult = conversationIdParamSchema.safeParse(rawParams)\n if (!parseResult.success) {\n return { kind: 'invalid-id', issues: parseResult.error.issues }\n }\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) return { kind: 'missing-tenant' }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: parseResult.data.conversationId,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n const url = new URL(req.url)\n const queryResult = aiChatConversationTranscriptQuerySchema.safeParse({\n limit: url.searchParams.get('limit') ?? undefined,\n before: url.searchParams.get('before') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const transcript = await repo.getTranscript(\n callerCtx.conversationId,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n limit: queryResult.data.limit,\n before: queryResult.data.before ?? null,\n },\n )\n if (!transcript) {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n const participantCount = await repo.getParticipantCount(\n callerCtx.tenantId,\n callerCtx.organizationId,\n callerCtx.conversationId,\n )\n return NextResponse.json({\n conversation: serializeAiChatConversation(transcript.conversation, {\n callerUserId: callerCtx.userId,\n participantCount,\n }),\n messages: transcript.messages.map(serializeAiChatMessage),\n nextCursor: transcript.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversation GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function PATCH(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n const parseResult = aiChatConversationUpdateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation patch.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const row = await repo.update(\n callerCtx.conversationId,\n parseResult.data,\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n )\n return NextResponse.json(serializeAiChatConversation(row))\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation PATCH] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to update conversation.',\n 'internal_error',\n )\n }\n}\n\nexport async function DELETE(req: NextRequest, context: RouteContext): Promise<Response> {\n const callerCtx = await resolveCallerContext(req, context)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'invalid-id') {\n return jsonError(400, 'Invalid conversation id.', 'validation_error', {\n issues: callerCtx.issues,\n })\n }\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n await repo.softDelete(callerCtx.conversationId, {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n })\n return NextResponse.json({ ok: true })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, 'Conversation not found.', 'conversation_not_found')\n }\n console.error('[AI Conversation DELETE] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to delete conversation.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,mBAAmB;AACzB,MAAM,+BAA+B;AAErC,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,gBAAgB,EACb,OAAO,EACP,KAAK,EACL,IAAI,GAAG,2CAA2C,EAClD,IAAI,KAAK,6DAA6D;AAC3E,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAKF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,oCAAoC;AAAA,QAChE,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,QAC5E,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,MAC1E;AAAA,IACF;AAAA,IACA,OAAO;AAAA,MACL,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAIF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,wBAAwB;AAAA,QACpD,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,QAC5E,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,MAC1E;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAKF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,WAAW;AAAA,QACb;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,0BAA0B;AAAA,QACtD,EAAE,QAAQ,KAAK,aAAa,gDAAgD;AAAA,QAC5E,EAAE,QAAQ,KAAK,aAAa,4CAA4C;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAAA,EAC9D,OAAO,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AAAA,EAChE,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,gBAAgB,EAAE;AACnE;AAMA,SAAS,UACP,QACA,SACA,MACA,OACc;AACd,SAAO,aAAa,KAAK,EAAE,OAAO,SAAS,MAAM,GAAI,SAAS,CAAC,EAAG,GAAG,EAAE,OAAO,CAAC;AACjF;AAEA,eAAe,qBAAqB,KAAkB,SAapD;AACA,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,KAAM,QAAO,EAAE,MAAM,eAAe;AACzC,QAAM,YAAY,MAAM,QAAQ;AAChC,QAAM,cAAc,0BAA0B,UAAU,SAAS;AACjE,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,EAAE,MAAM,cAAc,QAAQ,YAAY,MAAM,OAAO;AAAA,EAChE;AACA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,QAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,IAC9C,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK;AAAA,EACvB,CAAC;AACD,MAAI,CAAC,oBAAoB,CAAC,gBAAgB,GAAG,IAAI,UAAU,IAAI,cAAc,WAAW,GAAG;AACzF,WAAO,EAAE,MAAM,YAAY;AAAA,EAC7B;AACA,QAAM,yBAAyB;AAAA,IAC7B,CAAC,4BAA4B;AAAA,IAC7B,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ;AAAA,EACF;AACA,MAAI,CAAC,KAAK,SAAU,QAAO,EAAE,MAAM,iBAAiB;AACpD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU,KAAK;AAAA,IACf,gBAAgB,KAAK,SAAS;AAAA,IAC9B,QAAQ,KAAK;AAAA,IACb,gBAAgB,YAAY,KAAK;AAAA,IACjC;AAAA,EACF;AACF;AAEA,eAAsB,IAAI,KAAkB,SAA0C;AACpF,QAAM,YAAY,MAAM,qBAAqB,KAAK,OAAO;AACzD,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,cAAc;AACnC,WAAO,UAAU,KAAK,4BAA4B,oBAAoB;AAAA,MACpE,QAAQ,UAAU;AAAA,IACpB,CAAC;AAAA,EACH;AACA,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,EAC3E;AAEA,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,cAAc,wCAAwC,UAAU;AAAA,IACpE,OAAO,IAAI,aAAa,IAAI,OAAO,KAAK;AAAA,IACxC,QAAQ,IAAI,aAAa,IAAI,QAAQ,KAAK;AAAA,EAC5C,CAAC;AACD,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,UAAU,KAAK,6BAA6B,oBAAoB;AAAA,MACrE,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,aAAa,MAAM,KAAK;AAAA,MAC5B,UAAU;AAAA,MACV;AAAA,QACE,UAAU,UAAU;AAAA,QACpB,gBAAgB,UAAU;AAAA,QAC1B,QAAQ,UAAU;AAAA,QAClB,wBAAwB,UAAU;AAAA,MACpC;AAAA,MACA;AAAA,QACE,OAAO,YAAY,KAAK;AAAA,QACxB,QAAQ,YAAY,KAAK,UAAU;AAAA,MACrC;AAAA,IACF;AACA,QAAI,CAAC,YAAY;AACf,aAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,IAC3E;AACA,UAAM,mBAAmB,MAAM,KAAK;AAAA,MAClC,UAAU;AAAA,MACV,UAAU;AAAA,MACV,UAAU;AAAA,IACZ;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,cAAc,4BAA4B,WAAW,cAAc;AAAA,QACjE,cAAc,UAAU;AAAA,QACxB;AAAA,MACF,CAAC;AAAA,MACD,UAAU,WAAW,SAAS,IAAI,sBAAsB;AAAA,MACxD,YAAY,WAAW;AAAA,IACzB,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,kCAAkC,KAAK;AACrD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,MAAM,KAAkB,SAA0C;AACtF,QAAM,YAAY,MAAM,qBAAqB,KAAK,OAAO;AACzD,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,cAAc;AACnC,WAAO,UAAU,KAAK,4BAA4B,oBAAoB;AAAA,MACpE,QAAQ,UAAU;AAAA,IACpB,CAAC;AAAA,EACH;AACA,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,EAC3E;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,IAAI,KAAK;AAAA,EAC3B,QAAQ;AACN,WAAO,UAAU,KAAK,oCAAoC,kBAAkB;AAAA,EAC9E;AACA,QAAM,cAAc,+BAA+B,UAAU,OAAO;AACpE,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,UAAU,KAAK,+BAA+B,oBAAoB;AAAA,MACvE,QAAQ,YAAY,MAAM;AAAA,IAC5B,CAAC;AAAA,EACH;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,UAAU;AAAA,MACV,YAAY;AAAA,MACZ;AAAA,QACE,UAAU,UAAU;AAAA,QACpB,gBAAgB,UAAU;AAAA,QAC1B,QAAQ,UAAU;AAAA,QAClB,wBAAwB,UAAU;AAAA,MACpC;AAAA,IACF;AACA,WAAO,aAAa,KAAK,4BAA4B,GAAG,CAAC;AAAA,EAC3D,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,SAAS,iCAAiC;AAC5E,aAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,IAC3E;AACA,YAAQ,MAAM,oCAAoC,KAAK;AACvD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;AAEA,eAAsB,OAAO,KAAkB,SAA0C;AACvF,QAAM,YAAY,MAAM,qBAAqB,KAAK,OAAO;AACzD,MAAI,UAAU,SAAS,eAAgB,QAAO,UAAU,KAAK,gBAAgB,iBAAiB;AAC9F,MAAI,UAAU,SAAS,cAAc;AACnC,WAAO,UAAU,KAAK,4BAA4B,oBAAoB;AAAA,MACpE,QAAQ,UAAU;AAAA,IACpB,CAAC;AAAA,EACH;AACA,MAAI,UAAU,SAAS,aAAa;AAClC,WAAO,UAAU,KAAK,kCAAkC,gBAAgB,MAAM,WAAW;AAAA,EAC3F;AACA,MAAI,UAAU,SAAS,kBAAkB;AACvC,WAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,EAC3E;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,OAAO,0BAA0B,SAAS;AAChD,UAAM,KAAK,WAAW,UAAU,gBAAgB;AAAA,MAC9C,UAAU,UAAU;AAAA,MACpB,gBAAgB,UAAU;AAAA,MAC1B,QAAQ,UAAU;AAAA,MAClB,wBAAwB,UAAU;AAAA,IACpC,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACvC,SAAS,OAAO;AACd,QAAI,iBAAiB,SAAS,MAAM,SAAS,iCAAiC;AAC5E,aAAO,UAAU,KAAK,2BAA2B,wBAAwB;AAAA,IAC3E;AACA,YAAQ,MAAM,qCAAqC,KAAK;AACxD,WAAO;AAAA,MACL;AAAA,MACA,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACzC;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -132,7 +132,7 @@ async function GET(req) {
|
|
|
132
132
|
}
|
|
133
133
|
);
|
|
134
134
|
return NextResponse.json({
|
|
135
|
-
items: result.items.map(serializeAiChatConversation),
|
|
135
|
+
items: result.items.map((row) => serializeAiChatConversation(row)),
|
|
136
136
|
nextCursor: result.nextCursor
|
|
137
137
|
});
|
|
138
138
|
} catch (error) {
|
|
@@ -187,6 +187,9 @@ async function POST(req) {
|
|
|
187
187
|
const status = beforeRow ? 200 : 201;
|
|
188
188
|
return NextResponse.json(serializeAiChatConversation(row), { status });
|
|
189
189
|
} catch (error) {
|
|
190
|
+
if (error instanceof Error && error.name === "AiChatConversationOrgNotFoundError") {
|
|
191
|
+
return jsonError(400, error.message, "organization_not_found");
|
|
192
|
+
}
|
|
190
193
|
if (error instanceof Error && error.name === "AiChatConversationAccessError") {
|
|
191
194
|
return jsonError(404, error.message, "conversation_not_found");
|
|
192
195
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../src/modules/ai_assistant/api/ai/conversations/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\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 {\n aiChatConversationCreateSchema,\n aiChatConversationListQuerySchema,\n} from '../../../data/validators'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n} from '../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Server-side AI chat conversations',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversations',\n summary: 'List AI chat conversations visible to the caller.',\n description:\n 'Returns `{ items, nextCursor }` for the authenticated caller, ordered by `lastMessageAt` ' +\n 'descending. View-only callers receive only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` may list conversations across users in the same ' +\n 'tenant/organization. The ' +\n '`agent` and `status` filters are optional; `cursor` is the ISO timestamp returned by a ' +\n 'previous response.',\n responses: [\n {\n status: 200,\n description: 'Caller-owned conversation summaries.',\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 the `ai_assistant.view` feature.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantCreateConversation',\n summary: 'Idempotently create a new AI chat conversation.',\n description:\n 'If a non-deleted conversation already exists with the supplied `conversationId` for the ' +\n 'authenticated caller in this tenant/org, returns the existing summary. Otherwise creates a ' +\n 'fresh row and writes the owner-participant row in the same transaction.',\n responses: [\n {\n status: 200,\n description: 'Existing conversation (idempotent path).',\n mediaType: 'application/json',\n },\n {\n status: 201,\n description: 'Newly created conversation.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function loadCallerContext(req: NextRequest): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) {\n return { kind: 'missing-tenant' }\n }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return NextResponse.json({ items: [], nextCursor: null })\n }\n\n const url = new URL(req.url)\n const parseResult = aiChatConversationListQuerySchema.safeParse({\n agent: url.searchParams.get('agent') ?? undefined,\n status: url.searchParams.get('status') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n cursor: url.searchParams.get('cursor') ?? undefined,\n })\n if (!parseResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const result = await repo.list(\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n agentId: parseResult.data.agent ?? null,\n status: parseResult.data.status ?? null,\n limit: parseResult.data.limit,\n cursor: parseResult.data.cursor ?? null,\n },\n )\n return NextResponse.json({\n items: result.items.map(serializeAiChatConversation),\n nextCursor: result.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversations GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to list conversations.',\n 'internal_error',\n )\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(400, 'Caller is not bound to a tenant.', 'tenant_required')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const parseResult = aiChatConversationCreateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation payload.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const ctx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: false,\n }\n const beforeRow = parseResult.data.conversationId\n ? await repo.getById(parseResult.data.conversationId, ctx)\n : null\n const row = await repo.createOrGet(\n {\n conversationId: parseResult.data.conversationId,\n agentId: parseResult.data.agentId,\n title: parseResult.data.title ?? null,\n pageContext: parseResult.data.pageContext ?? null,\n },\n ctx,\n )\n const status = beforeRow ? 200 : 201\n return NextResponse.json(serializeAiChatConversation(row), { status })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, error.message, 'conversation_not_found')\n }\n console.error('[AI Conversations POST] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to create conversation.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse, type NextRequest } from 'next/server'\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 {\n aiChatConversationCreateSchema,\n aiChatConversationListQuerySchema,\n} from '../../../data/validators'\nimport { hasRequiredFeatures } from '../../../lib/auth'\nimport {\n createConversationStorage,\n serializeAiChatConversation,\n} from '../../../lib/conversation-storage'\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\nconst MANAGE_CONVERSATIONS_FEATURE = 'ai_assistant.conversations.manage'\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Server-side AI chat conversations',\n methods: {\n GET: {\n operationId: 'aiAssistantListConversations',\n summary: 'List AI chat conversations visible to the caller.',\n description:\n 'Returns `{ items, nextCursor }` for the authenticated caller, ordered by `lastMessageAt` ' +\n 'descending. View-only callers receive only their own conversations. Callers with ' +\n '`ai_assistant.conversations.manage` may list conversations across users in the same ' +\n 'tenant/organization. The ' +\n '`agent` and `status` filters are optional; `cursor` is the ISO timestamp returned by a ' +\n 'previous response.',\n responses: [\n {\n status: 200,\n description: 'Caller-owned conversation summaries.',\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 the `ai_assistant.view` feature.' },\n ],\n },\n POST: {\n operationId: 'aiAssistantCreateConversation',\n summary: 'Idempotently create a new AI chat conversation.',\n description:\n 'If a non-deleted conversation already exists with the supplied `conversationId` for the ' +\n 'authenticated caller in this tenant/org, returns the existing summary. Otherwise creates a ' +\n 'fresh row and writes the owner-participant row in the same transaction.',\n responses: [\n {\n status: 200,\n description: 'Existing conversation (idempotent path).',\n mediaType: 'application/json',\n },\n {\n status: 201,\n description: 'Newly created conversation.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid request body.' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nasync function loadCallerContext(req: NextRequest): Promise<\n | { kind: 'unauthorized' }\n | { kind: 'forbidden' }\n | { kind: 'missing-tenant' }\n | {\n kind: 'ok'\n tenantId: string\n organizationId: string | null\n userId: string\n canManageConversations: boolean\n }\n> {\n const auth = await getAuthFromRequest(req)\n if (!auth) return { kind: 'unauthorized' }\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 if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return { kind: 'forbidden' }\n }\n const canManageConversations = hasRequiredFeatures(\n [MANAGE_CONVERSATIONS_FEATURE],\n acl.features,\n acl.isSuperAdmin,\n rbacService,\n )\n if (!auth.tenantId) {\n return { kind: 'missing-tenant' }\n }\n return {\n kind: 'ok',\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n canManageConversations,\n }\n}\n\nexport async function GET(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return NextResponse.json({ items: [], nextCursor: null })\n }\n\n const url = new URL(req.url)\n const parseResult = aiChatConversationListQuerySchema.safeParse({\n agent: url.searchParams.get('agent') ?? undefined,\n status: url.searchParams.get('status') ?? undefined,\n limit: url.searchParams.get('limit') ?? undefined,\n cursor: url.searchParams.get('cursor') ?? undefined,\n })\n if (!parseResult.success) {\n return jsonError(400, 'Invalid query parameters.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const result = await repo.list(\n {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: callerCtx.canManageConversations,\n },\n {\n agentId: parseResult.data.agent ?? null,\n status: parseResult.data.status ?? null,\n limit: parseResult.data.limit,\n cursor: parseResult.data.cursor ?? null,\n },\n )\n return NextResponse.json({\n items: result.items.map((row) => serializeAiChatConversation(row)),\n nextCursor: result.nextCursor,\n })\n } catch (error) {\n console.error('[AI Conversations GET] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to list conversations.',\n 'internal_error',\n )\n }\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const callerCtx = await loadCallerContext(req)\n if (callerCtx.kind === 'unauthorized') return jsonError(401, 'Unauthorized', 'unauthenticated')\n if (callerCtx.kind === 'forbidden') {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n if (callerCtx.kind === 'missing-tenant') {\n return jsonError(400, 'Caller is not bound to a tenant.', 'tenant_required')\n }\n\n let rawBody: unknown\n try {\n rawBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const parseResult = aiChatConversationCreateSchema.safeParse(rawBody)\n if (!parseResult.success) {\n return jsonError(400, 'Invalid conversation payload.', 'validation_error', {\n issues: parseResult.error.issues,\n })\n }\n\n try {\n const container = await createRequestContainer()\n const repo = createConversationStorage(container)\n const ctx = {\n tenantId: callerCtx.tenantId,\n organizationId: callerCtx.organizationId,\n userId: callerCtx.userId,\n canManageConversations: false,\n }\n const beforeRow = parseResult.data.conversationId\n ? await repo.getById(parseResult.data.conversationId, ctx)\n : null\n const row = await repo.createOrGet(\n {\n conversationId: parseResult.data.conversationId,\n agentId: parseResult.data.agentId,\n title: parseResult.data.title ?? null,\n pageContext: parseResult.data.pageContext ?? null,\n },\n ctx,\n )\n const status = beforeRow ? 200 : 201\n return NextResponse.json(serializeAiChatConversation(row), { status })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationOrgNotFoundError') {\n return jsonError(400, error.message, 'organization_not_found')\n }\n if (error instanceof Error && error.name === 'AiChatConversationAccessError') {\n return jsonError(404, error.message, 'conversation_not_found')\n }\n console.error('[AI Conversations POST] Failure:', error)\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to create conversation.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Textarea } from "@open-mercato/ui/primitives/textarea";
|
|
|
14
14
|
import { EmptyState } from "@open-mercato/ui/backend/EmptyState";
|
|
15
15
|
import { apiCall, apiCallOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
16
16
|
import { AiChat, createAiUiPartRegistry, LoopDisabledBanner, useAiShortcuts } from "@open-mercato/ui/ai";
|
|
17
|
+
import { ConversationShareButton } from "../../../../components/ConversationShareButton.js";
|
|
17
18
|
async function fetchAgents() {
|
|
18
19
|
const { result, status } = await apiCallOrThrow(
|
|
19
20
|
"/api/ai_assistant/ai/agents",
|
|
@@ -196,6 +197,7 @@ function ChatLane({ agent, debug }) {
|
|
|
196
197
|
[agent]
|
|
197
198
|
);
|
|
198
199
|
const [uiParts, setUiParts] = React.useState([]);
|
|
200
|
+
const [conversationId, setConversationId] = React.useState(null);
|
|
199
201
|
React.useEffect(() => {
|
|
200
202
|
const seeds = readPlaygroundUiPartSeeds();
|
|
201
203
|
if (seeds.length > 0) setUiParts(seeds);
|
|
@@ -222,7 +224,9 @@ function ChatLane({ agent, debug }) {
|
|
|
222
224
|
className: "min-h-96",
|
|
223
225
|
debugTools,
|
|
224
226
|
debugPromptSections,
|
|
225
|
-
uiParts
|
|
227
|
+
uiParts,
|
|
228
|
+
onConversationIdChange: setConversationId,
|
|
229
|
+
headerActions: conversationId ? /* @__PURE__ */ jsx(ConversationShareButton, { conversationId }) : null
|
|
226
230
|
},
|
|
227
231
|
agent.id
|
|
228
232
|
) });
|