feat: add rtf in logs

This commit is contained in:
Abhishek Kumar 2026-01-14 18:35:18 +05:30
parent a172db8022
commit d25f898a8f
10 changed files with 284 additions and 23 deletions

View file

@ -0,0 +1,166 @@
'use client';
import { CheckCircle, MessageSquare, MicOff,Wrench } from 'lucide-react';
import { cn } from '@/lib/utils';
interface RealtimeFeedbackEvent {
type: string;
payload: {
text?: string;
final?: boolean;
user_id?: string;
timestamp?: string;
function_name?: string;
tool_call_id?: string;
result?: string;
};
timestamp: string;
turn: number;
}
export interface WorkflowRunLogs {
realtime_feedback_events?: RealtimeFeedbackEvent[];
}
interface RealtimeFeedbackLogsProps {
logs: WorkflowRunLogs | null;
}
const EventItem = ({ event }: { event: RealtimeFeedbackEvent }) => {
// Function call message - centered
if (event.type === 'rtf-function-call-start' || event.type === 'rtf-function-call-end') {
return (
<div className="flex justify-center">
<div className="px-3 py-1.5 rounded-full text-xs bg-amber-500/10 border border-amber-500/20 inline-flex items-center gap-2">
{event.type === 'rtf-function-call-start' ? (
<Wrench className="h-3 w-3 text-amber-500" />
) : (
<CheckCircle className="h-3 w-3 text-green-500" />
)}
<span className="font-mono text-amber-700 dark:text-amber-400">
{event.payload.function_name}()
</span>
{event.type === 'rtf-function-call-end' && (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
);
}
const isUser = event.type === 'rtf-user-transcription';
// User messages on right, bot messages on left
return (
<div className={cn(
"flex",
isUser ? "justify-end" : "justify-start"
)}>
<div
className={cn(
"max-w-[85%] px-3 py-2 rounded-2xl text-sm",
isUser
? "bg-primary text-primary-foreground rounded-br-md"
: "bg-muted rounded-bl-md"
)}
>
<div className="whitespace-pre-wrap leading-relaxed">{event.payload.text}</div>
</div>
</div>
);
};
function processEvents(events: RealtimeFeedbackEvent[]): RealtimeFeedbackEvent[] {
// Filter out interim transcriptions
const filteredEvents = events.filter(event => {
if (event.type === 'rtf-user-transcription' && !event.payload.final) {
return false;
}
return true;
});
// Combine consecutive rtf-bot-text events by turn
const processed: RealtimeFeedbackEvent[] = [];
let currentBotText: RealtimeFeedbackEvent | null = null;
for (const event of filteredEvents) {
if (event.type === 'rtf-bot-text') {
if (currentBotText && currentBotText.turn === event.turn) {
// Same turn, combine the text
currentBotText.payload.text = (currentBotText.payload.text || '') + ' ' + (event.payload.text || '');
} else {
// Different turn or first bot text
if (currentBotText) {
processed.push(currentBotText);
}
// Deep copy to avoid mutating original event
currentBotText = {
...event,
payload: { ...event.payload }
};
}
} else {
// Not a bot text event
if (currentBotText) {
processed.push(currentBotText);
currentBotText = null;
}
processed.push(event);
}
}
// Don't forget the last bot text if there is one
if (currentBotText) {
processed.push(currentBotText);
}
return processed;
}
export function RealtimeFeedbackLogs({ logs }: RealtimeFeedbackLogsProps) {
const rawEvents = logs?.realtime_feedback_events;
const events = rawEvents ? processEvents(rawEvents) : undefined;
return (
<div className="w-full h-full flex flex-col bg-background border-l border-border">
{/* Header */}
<div className="px-4 py-3 border-b border-border shrink-0">
<div className="flex items-center justify-center gap-2">
<MessageSquare className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="font-medium text-sm whitespace-nowrap">Call Transcript</span>
<div className="flex items-center gap-1 text-xs px-2 py-0.5 rounded-full shrink-0 bg-muted text-muted-foreground">
<MicOff className="h-3 w-3" />
<span>Ended</span>
</div>
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto">
{!events || events.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm">
<MessageSquare className="h-10 w-10 mb-4 opacity-30" />
<p className="font-medium">No conversation recorded</p>
<p className="text-xs mt-1 text-center px-4">
Real-time feedback events were not captured for this call
</p>
</div>
) : (
<div className="space-y-3 p-4">
{events.map((event, index) => (
<EventItem key={index} event={event} />
))}
</div>
)}
</div>
{/* Footer with message count */}
{events && events.length > 0 && (
<div className="px-4 py-2 border-t border-border text-xs text-muted-foreground shrink-0">
{events.filter(e => e.type === 'rtf-user-transcription' || e.type === 'rtf-bot-text').length} messages
</div>
)}
</div>
);
}

View file

@ -2,5 +2,6 @@ export * from './ApiKeyErrorDialog';
export * from './AudioControls';
export * from './ConnectionStatus';
export * from './ContextDisplay';
export * from './RealtimeFeedbackLogs';
export * from './RealtimeFeedbackPanel';
export * from './WorkflowConfigErrorDialog'
export * from './WorkflowConfigErrorDialog';

View file

@ -6,6 +6,7 @@ import { useParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import BrowserCall from '@/app/workflow/[workflowId]/run/[runId]/BrowserCall';
import { RealtimeFeedbackLogs, WorkflowRunLogs } from '@/app/workflow/[workflowId]/run/[runId]/components/RealtimeFeedbackLogs';
import WorkflowLayout from '@/app/workflow/WorkflowLayout';
import { getWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGet } from '@/client/sdk.gen';
import { MediaPreviewButtons, MediaPreviewDialog } from '@/components/MediaPreviewDialog';
@ -23,6 +24,7 @@ interface WorkflowRunResponse {
recording_url: string | null;
initial_context: Record<string, string | number | boolean | object> | null;
gathered_context: Record<string, string | number | boolean | object> | null;
logs: WorkflowRunLogs | null;
}
function ContextDisplay({ title, context }: { title: string; context: Record<string, string | number | boolean | object> | null }) {
@ -116,6 +118,7 @@ export default function WorkflowRunPage() {
recording_url: response.data?.recording_url ?? null,
initial_context: response.data?.initial_context as Record<string, string> | null ?? null,
gathered_context: response.data?.gathered_context as Record<string, string> | null ?? null,
logs: response.data?.logs as WorkflowRunLogs | null ?? null,
});
};
fetchWorkflowRun();
@ -147,8 +150,10 @@ export default function WorkflowRunPage() {
}
else if (workflowRun?.is_completed) {
returnValue = (
<div className="h-full flex items-center justify-center p-6">
<div className="w-full max-w-4xl space-y-6">
<div className="flex h-full w-full">
{/* Main content - 2/3 width */}
<div className="w-2/3 h-full flex items-center justify-center overflow-y-auto">
<div className="w-full max-w-4xl space-y-6 p-6">
<Card className="border-border">
<CardHeader className="flex flex-row items-center justify-between">
<div className="flex items-center gap-4">
@ -235,17 +240,23 @@ export default function WorkflowRunPage() {
</CardContent>
</Card>
<div className="grid gap-6 md:grid-cols-2">
<ContextDisplay
title="Initial Context"
context={workflowRun?.initial_context}
/>
<ContextDisplay
title="Gathered Context"
context={workflowRun?.gathered_context}
/>
<div className="grid gap-6 md:grid-cols-2">
<ContextDisplay
title="Initial Context"
context={workflowRun?.initial_context}
/>
<ContextDisplay
title="Gathered Context"
context={workflowRun?.gathered_context}
/>
</div>
</div>
</div>
{/* Transcript panel - 1/3 width */}
<div className="w-1/3 h-full shrink-0">
<RealtimeFeedbackLogs logs={workflowRun?.logs} />
</div>
</div>
);
}

View file

@ -964,6 +964,9 @@ export type WorkflowRunResponseSchema = {
[key: string]: unknown;
} | null;
call_type: CallType;
logs?: {
[key: string]: unknown;
} | null;
};
export type WorkflowRunUsageResponse = {