diff --git a/apps/rowboat/app/lib/components/reason-badge.tsx b/apps/rowboat/app/lib/components/reason-badge.tsx new file mode 100644 index 00000000..c204f855 --- /dev/null +++ b/apps/rowboat/app/lib/components/reason-badge.tsx @@ -0,0 +1,50 @@ +import Link from "next/link"; +import { Turn } from "@/src/entities/models/turn"; +import { z } from "zod"; + +export function ReasonBadge({ + reason, + projectId +}: { + reason: z.infer['reason']; + projectId?: string; +}) { + const getReasonDisplay = () => { + switch (reason.type) { + case 'chat': + return { label: 'CHAT', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' }; + case 'api': + return { label: 'API', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' }; + case 'job': + return { + label: `JOB: ${reason.jobId}`, + color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300', + isJob: true, + jobId: reason.jobId + }; + default: + return { label: 'UNKNOWN', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300' }; + } + }; + + const { label, color, isJob, jobId } = getReasonDisplay(); + + // Job reasons should ALWAYS be linked when we have a projectId + if (isJob && jobId && projectId) { + return ( + + {label} + + ); + } + + // Otherwise render as a regular badge + return ( + + {label} + + ); +} diff --git a/apps/rowboat/app/projects/[projectId]/conversations/components/conversation-view.tsx b/apps/rowboat/app/projects/[projectId]/conversations/components/conversation-view.tsx index b0da2c9c..0312a587 100644 --- a/apps/rowboat/app/projects/[projectId]/conversations/components/conversation-view.tsx +++ b/apps/rowboat/app/projects/[projectId]/conversations/components/conversation-view.tsx @@ -9,68 +9,7 @@ import { Turn } from "@/src/entities/models/turn"; import { z } from "zod"; import Link from "next/link"; import { MessageDisplay } from "../../../../lib/components/message-display"; - -function TurnReason({ reason }: { reason: z.infer['reason'] }) { - const getReasonDisplay = () => { - switch (reason.type) { - case 'chat': - return { label: 'CHAT', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' }; - case 'api': - return { label: 'API', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' }; - case 'job': - return { label: `JOB: ${reason.jobId}`, color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300' }; - default: - return { label: 'UNKNOWN', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300' }; - } - }; - - const { label, color } = getReasonDisplay(); - - return ( - - {label} - - ); -} - -function TurnReasonWithLink({ reason, projectId }: { reason: z.infer['reason']; projectId: string }) { - const getReasonDisplay = () => { - switch (reason.type) { - case 'chat': - return { label: 'CHAT', color: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' }; - case 'api': - return { label: 'API', color: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' }; - case 'job': - return { - label: `JOB: ${reason.jobId}`, - color: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300', - isJob: true, - jobId: reason.jobId - }; - default: - return { label: 'UNKNOWN', color: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300' }; - } - }; - - const { label, color, isJob, jobId } = getReasonDisplay(); - - if (isJob && jobId) { - return ( - - {label} - - ); - } - - return ( - - {label} - - ); -} +import { ReasonBadge } from "../../../../lib/components/reason-badge"; function TurnContainer({ turn, index, projectId }: { turn: z.infer; index: number; projectId: string }) { return ( @@ -82,7 +21,7 @@ function TurnContainer({ turn, index, projectId }: { turn: z.infer; TURN #{index + 1} - +
{new Date(turn.createdAt).toLocaleTimeString()} @@ -199,6 +138,12 @@ export function ConversationView({ projectId, conversationId }: { projectId: str {conversation.isLiveWorkflow ? 'Yes' : 'No'}
+
+ Reason: + + + +
diff --git a/apps/rowboat/app/projects/[projectId]/conversations/components/conversations-list.tsx b/apps/rowboat/app/projects/[projectId]/conversations/components/conversations-list.tsx index 5e89dcbf..02b68ed2 100644 --- a/apps/rowboat/app/projects/[projectId]/conversations/components/conversations-list.tsx +++ b/apps/rowboat/app/projects/[projectId]/conversations/components/conversations-list.tsx @@ -8,6 +8,7 @@ import { listConversations } from "@/app/actions/conversation_actions"; import { z } from "zod"; import { ListedConversationItem } from "@/src/application/repositories/conversations.repository.interface"; import { isToday, isThisWeek, isThisMonth } from "@/lib/utils/date"; +import { ReasonBadge } from "@/app/lib/components/reason-badge"; type ListedItem = z.infer; @@ -101,26 +102,30 @@ export function ConversationsList({ projectId }: { projectId: string }) { Conversation - Created + Reason + Created {group.map((c) => ( - - - - {c.id} - - - - {new Date(c.createdAt).toLocaleString()} - - + + + + {c.id} + + + + + + + {new Date(c.createdAt).toLocaleString()} + + ))} diff --git a/apps/rowboat/src/application/repositories/conversations.repository.interface.ts b/apps/rowboat/src/application/repositories/conversations.repository.interface.ts index 02ba52a4..bb73a2b9 100644 --- a/apps/rowboat/src/application/repositories/conversations.repository.interface.ts +++ b/apps/rowboat/src/application/repositories/conversations.repository.interface.ts @@ -6,6 +6,7 @@ import { PaginatedList } from "@/src/entities/common/paginated-list"; export const CreateConversationData = Conversation.pick({ projectId: true, workflow: true, + reason: true, isLiveWorkflow: true, }); @@ -17,6 +18,7 @@ export const AddTurnData = Turn.omit({ export const ListedConversationItem = Conversation.pick({ id: true, + reason: true, projectId: true, createdAt: true, updatedAt: true, diff --git a/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts index 7ff60313..30318551 100644 --- a/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts @@ -6,12 +6,14 @@ import { Conversation } from "@/src/entities/models/conversation"; import { Workflow } from "@/app/lib/types/workflow_types"; import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface'; import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy'; +import { Reason } from '@/src/entities/models/turn'; const inputSchema = z.object({ caller: z.enum(["user", "api", "job_worker"]), userId: z.string().optional(), apiKey: z.string().optional(), projectId: z.string(), + reason: Reason, workflow: Workflow.optional(), isLiveWorkflow: z.boolean().optional(), }); @@ -40,7 +42,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase { } async execute(data: z.infer): Promise> { - const { caller, userId, apiKey, projectId } = data; + const { caller, userId, apiKey, projectId, reason } = data; let isLiveWorkflow = Boolean(data.isLiveWorkflow); let workflow = data.workflow; @@ -75,6 +77,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase { // create conversation return await this.conversationsRepository.create({ projectId, + reason, workflow, isLiveWorkflow, }); diff --git a/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts index fa433d5e..218e6345 100644 --- a/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/run-conversation-turn.use-case.ts @@ -1,4 +1,4 @@ -import { Turn, TurnEvent } from "@/src/entities/models/turn"; +import { Reason, Turn, TurnEvent } from "@/src/entities/models/turn"; import { USE_BILLING } from "@/app/lib/feature_flags"; import { authorize, getCustomerIdForProject, logUsage } from "@/app/lib/billing"; import { NotFoundError } from '@/src/entities/errors/common'; @@ -14,7 +14,7 @@ const inputSchema = z.object({ userId: z.string().optional(), apiKey: z.string().optional(), conversationId: z.string(), - reason: Turn.shape.reason, + reason: Reason, input: Turn.shape.input, }); diff --git a/apps/rowboat/src/application/workers/jobs.worker.ts b/apps/rowboat/src/application/workers/jobs.worker.ts index 8f8807a9..663dc02b 100644 --- a/apps/rowboat/src/application/workers/jobs.worker.ts +++ b/apps/rowboat/src/application/workers/jobs.worker.ts @@ -57,6 +57,10 @@ export class JobsWorker implements IJobsWorker { const conversation = await this.createConversationUseCase.execute({ caller: "job_worker", projectId, + reason: { + type: "job", + jobId: job.id, + }, workflow: job.input.workflow, isLiveWorkflow: true, }); diff --git a/apps/rowboat/src/entities/models/conversation.ts b/apps/rowboat/src/entities/models/conversation.ts index 9049c1e6..c33263ab 100644 --- a/apps/rowboat/src/entities/models/conversation.ts +++ b/apps/rowboat/src/entities/models/conversation.ts @@ -1,11 +1,12 @@ import { z } from "zod"; -import { Turn } from "./turn"; +import { Reason, Turn } from "./turn"; import { Workflow } from "@/app/lib/types/workflow_types"; export const Conversation = z.object({ id: z.string(), projectId: z.string(), workflow: Workflow, + reason: Reason, isLiveWorkflow: z.boolean(), turns: z.array(Turn).optional(), createdAt: z.string().datetime(), diff --git a/apps/rowboat/src/entities/models/turn.ts b/apps/rowboat/src/entities/models/turn.ts index 80d225fa..0f7cdbf0 100644 --- a/apps/rowboat/src/entities/models/turn.ts +++ b/apps/rowboat/src/entities/models/turn.ts @@ -14,7 +14,7 @@ const jobReason = z.object({ jobId: z.string(), }); -const reason = z.discriminatedUnion("type", [ +export const Reason = z.discriminatedUnion("type", [ chatReason, apiReason, jobReason, @@ -22,7 +22,7 @@ const reason = z.discriminatedUnion("type", [ export const Turn = z.object({ id: z.string(), - reason, + reason: Reason, input: z.object({ messages: z.array(Message), mockTools: z.record(z.string(), z.string()).nullable().optional(), diff --git a/apps/rowboat/src/infrastructure/repositories/mongodb.conversations.repository.ts b/apps/rowboat/src/infrastructure/repositories/mongodb.conversations.repository.ts index a988839d..12cf9b9c 100644 --- a/apps/rowboat/src/infrastructure/repositories/mongodb.conversations.repository.ts +++ b/apps/rowboat/src/infrastructure/repositories/mongodb.conversations.repository.ts @@ -91,6 +91,7 @@ export class MongoDBConversationsRepository implements IConversationsRepository projectId: 1, createdAt: 1, updatedAt: 1, + reason: 1, }) .toArray(); diff --git a/apps/rowboat/src/interface-adapters/controllers/conversations/create-playground-conversation.controller.ts b/apps/rowboat/src/interface-adapters/controllers/conversations/create-playground-conversation.controller.ts index ad7aedfd..9fa2ca7c 100644 --- a/apps/rowboat/src/interface-adapters/controllers/conversations/create-playground-conversation.controller.ts +++ b/apps/rowboat/src/interface-adapters/controllers/conversations/create-playground-conversation.controller.ts @@ -38,6 +38,9 @@ export class CreatePlaygroundConversationController implements ICreatePlayground return await this.createConversationUseCase.execute({ caller: "user", userId, + reason: { + type: "chat", + }, projectId, workflow, isLiveWorkflow, diff --git a/apps/rowboat/src/interface-adapters/controllers/conversations/run-turn.controller.ts b/apps/rowboat/src/interface-adapters/controllers/conversations/run-turn.controller.ts index 71ab496d..a60fd319 100644 --- a/apps/rowboat/src/interface-adapters/controllers/conversations/run-turn.controller.ts +++ b/apps/rowboat/src/interface-adapters/controllers/conversations/run-turn.controller.ts @@ -49,6 +49,7 @@ export class RunTurnController implements IRunTurnController { } const { caller, userId, apiKey, projectId, input } = result.data; let conversationId = result.data.conversationId; + const reason = caller === "user" ? { type: "chat" as const } : { type: "api" as const }; // if conversationId is not provided, create conversation if (!conversationId) { @@ -57,6 +58,7 @@ export class RunTurnController implements IRunTurnController { userId, apiKey, projectId, + reason, }); conversationId = conversation.id; } @@ -67,7 +69,7 @@ export class RunTurnController implements IRunTurnController { userId, apiKey, conversationId, - reason: caller === "user" ? { type: "chat" } : { type: "api" }, + reason, input, });