mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-19 08:28:10 +02:00
chore: fix tracing for text chat mode
This commit is contained in:
parent
e23cce444f
commit
08a2435ba5
31 changed files with 1753 additions and 597 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { ChevronLeft, ChevronRight, Download, Globe } from 'lucide-react';
|
||||
import { ArrowDownLeft, ArrowUpRight, ChevronLeft, ChevronRight, Download, Globe, MessageSquare, Phone } from 'lucide-react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useCallback, useEffect, useId, useState } from 'react';
|
||||
import TimezoneSelect, { type ITimezoneOption } from 'react-timezone-select';
|
||||
|
|
@ -23,6 +23,7 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useUserConfig } from '@/context/UserConfigContext';
|
||||
import { useAuth } from '@/lib/auth';
|
||||
import { usageFilterAttributes } from '@/lib/filterAttributes';
|
||||
|
|
@ -32,6 +33,53 @@ import { ActiveFilter, DateRangeValue } from '@/types/filters';
|
|||
// Get local timezone
|
||||
const getLocalTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Collapse a run's `mode` (from WorkflowRunMode in api/enums.py) into a coarse
|
||||
// channel. Telephony providers (twilio, plivo, telnyx, vonage, vobiz, cloudonix,
|
||||
// ari, ...) are phone calls; webrtc/smallwebrtc are browser web calls; textchat
|
||||
// is a text conversation. Anything unknown falls back to "phone".
|
||||
const WEB_CALL_MODES = new Set(['webrtc', 'smallwebrtc']);
|
||||
const TEXT_CHAT_MODES = new Set(['textchat']);
|
||||
|
||||
const getCallChannel = (mode?: string | null): 'phone' | 'web' | 'chat' => {
|
||||
if (mode && TEXT_CHAT_MODES.has(mode)) return 'chat';
|
||||
if (mode && WEB_CALL_MODES.has(mode)) return 'web';
|
||||
return 'phone';
|
||||
};
|
||||
|
||||
// Render the call's channel (mode) and direction (call_type) as two compact
|
||||
// icons in a single cell, with a tooltip spelling out the full label. The
|
||||
// channel icon shows medium/how (phone / web / chat); the colored arrow shows
|
||||
// direction (inbound = incoming/emerald, outbound = outgoing/blue).
|
||||
const CallTypeCell = ({ mode, callType }: { mode?: string | null; callType?: string | null }) => {
|
||||
if (!mode && !callType) {
|
||||
return <span className="text-sm text-muted-foreground">-</span>;
|
||||
}
|
||||
|
||||
const channel = getCallChannel(mode);
|
||||
const ChannelIcon = channel === 'chat' ? MessageSquare : channel === 'web' ? Globe : Phone;
|
||||
const channelLabel = channel === 'chat' ? 'Text chat' : channel === 'web' ? 'Web call' : 'Phone call';
|
||||
|
||||
const isInbound = callType === 'inbound';
|
||||
const DirectionIcon = isInbound ? ArrowDownLeft : ArrowUpRight;
|
||||
const directionLabel = isInbound ? 'Inbound' : 'Outbound';
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<ChannelIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<DirectionIcon
|
||||
className={`h-3.5 w-3.5 ${isInbound ? 'text-emerald-600' : 'text-blue-600'}`}
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={4}>
|
||||
{directionLabel} · {channelLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default function UsagePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -534,13 +582,7 @@ export default function UsagePage() {
|
|||
</TableCell>
|
||||
<TableCell>{run.workflow_name || 'Unknown'}</TableCell>
|
||||
<TableCell>
|
||||
{run.call_type ? (
|
||||
<Badge variant={run.call_type === 'inbound' ? "secondary" : "default"}>
|
||||
{run.call_type === 'inbound' ? 'Inbound' : 'Outbound'}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">-</span>
|
||||
)}
|
||||
<CallTypeCell mode={run.mode} callType={run.call_type} />
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{(run.call_type === 'inbound'
|
||||
|
|
|
|||
|
|
@ -385,45 +385,6 @@ export const WorkflowEditorHeader = ({
|
|||
</Popover>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 bg-transparent border-[#3a3a3a] hover:bg-[#2a2a2a] text-white"
|
||||
onClick={onTestAgentClick}
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
Test Agent
|
||||
</Button>
|
||||
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 bg-transparent border-[#3a3a3a] hover:bg-[#2a2a2a] text-white"
|
||||
disabled={isCallDisabled}
|
||||
onClick={onPhoneCallClick}
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
Phone Call
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Save button (only shown when editing the draft) */}
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!isDirty || savingWorkflow}
|
||||
className="bg-teal-600 hover:bg-teal-700 text-white px-4"
|
||||
>
|
||||
{savingWorkflow ? (
|
||||
<>
|
||||
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
"Save"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Publish button (only when on draft with no unsaved changes) */}
|
||||
{!isViewingHistoricalVersion && hasDraft && (
|
||||
<Button
|
||||
|
|
@ -446,6 +407,45 @@ export const WorkflowEditorHeader = ({
|
|||
</Button>
|
||||
)}
|
||||
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 bg-transparent border-[#3a3a3a] hover:bg-[#2a2a2a] text-white"
|
||||
disabled={isCallDisabled}
|
||||
onClick={onPhoneCallClick}
|
||||
>
|
||||
<Phone className="w-4 h-4" />
|
||||
Phone Call
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center gap-2 bg-transparent border-[#3a3a3a] hover:bg-[#2a2a2a] text-white"
|
||||
onClick={onTestAgentClick}
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
Test Agent
|
||||
</Button>
|
||||
|
||||
{/* Save button (only shown when editing the draft) */}
|
||||
{!isViewingHistoricalVersion && (
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!isDirty || savingWorkflow}
|
||||
className="bg-teal-600 hover:bg-teal-700 text-white px-4"
|
||||
>
|
||||
{savingWorkflow ? (
|
||||
<>
|
||||
<LoaderCircle className="w-4 h-4 mr-2 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
"Save"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* More options dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -74,6 +74,12 @@ type TextChatSession = Omit<WorkflowRunTextSessionResponse, "session_data" | "ch
|
|||
checkpoint: TextChatCheckpoint;
|
||||
};
|
||||
|
||||
interface TextChatToolEvent {
|
||||
kind: "start" | "result";
|
||||
functionName: string;
|
||||
resultText?: string;
|
||||
}
|
||||
|
||||
function toTextChatSession(response: WorkflowRunTextSessionResponse): TextChatSession {
|
||||
return {
|
||||
...response,
|
||||
|
|
@ -182,6 +188,66 @@ function TypingBubble() {
|
|||
);
|
||||
}
|
||||
|
||||
function stringifyToolResult(result: unknown) {
|
||||
if (result == null) return "No result";
|
||||
if (typeof result === "string") return result;
|
||||
try {
|
||||
return JSON.stringify(result);
|
||||
} catch {
|
||||
return String(result);
|
||||
}
|
||||
}
|
||||
|
||||
function extractToolEvents(events: Array<Record<string, unknown>>): TextChatToolEvent[] {
|
||||
return events.reduce<TextChatToolEvent[]>((acc, event) => {
|
||||
const eventType = event.type;
|
||||
const payload = event.payload;
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return acc;
|
||||
}
|
||||
const typedPayload = payload as Record<string, unknown>;
|
||||
|
||||
const functionName = typeof typedPayload.function_name === "string"
|
||||
? typedPayload.function_name
|
||||
: "tool";
|
||||
|
||||
if (eventType === "tool_call_started") {
|
||||
acc.push({ kind: "start", functionName });
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (eventType === "tool_call_result") {
|
||||
acc.push({
|
||||
kind: "result",
|
||||
functionName,
|
||||
resultText: stringifyToolResult(typedPayload.result),
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function ToolEventBubble({ event }: { event: TextChatToolEvent }) {
|
||||
return (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[85%] rounded-2xl rounded-bl-md border border-border/70 bg-background px-3.5 py-2 text-sm leading-6 text-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="h-5 px-1.5 text-[10px] uppercase tracking-[0.14em]">
|
||||
{event.kind === "start" ? "Tool" : "Result"}
|
||||
</Badge>
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
{event.kind === "start"
|
||||
? `${event.functionName}()`
|
||||
: `${event.functionName} -> ${event.resultText ?? "No result"}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmbeddedVoiceTester({
|
||||
workflowId,
|
||||
workflowRunId,
|
||||
|
|
@ -357,14 +423,17 @@ function ManualTextChat({
|
|||
initialContextVariables,
|
||||
disabled,
|
||||
disabledReason,
|
||||
onActiveChange,
|
||||
}: {
|
||||
workflowId: number;
|
||||
ready: boolean;
|
||||
initialContextVariables?: Record<string, string>;
|
||||
disabled: boolean;
|
||||
disabledReason: string | null;
|
||||
onActiveChange?: (active: boolean) => void;
|
||||
}) {
|
||||
const [session, setSession] = useState<TextChatSession | null>(null);
|
||||
const [started, setStarted] = useState(false);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [creatingSession, setCreatingSession] = useState(false);
|
||||
const [sendingMessage, setSendingMessage] = useState(false);
|
||||
|
|
@ -403,11 +472,15 @@ function ManualTextChat({
|
|||
}, [disabled, initialContextVariables, workflowId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (creatingSession || session || !ready || disabled) {
|
||||
if (!started || creatingSession || session || !ready || disabled) {
|
||||
return;
|
||||
}
|
||||
void createSession();
|
||||
}, [createSession, creatingSession, disabled, ready, session]);
|
||||
}, [createSession, creatingSession, disabled, ready, session, started]);
|
||||
|
||||
useEffect(() => {
|
||||
onActiveChange?.(started);
|
||||
}, [onActiveChange, started]);
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
if (!session || !draft.trim() || disabled) return;
|
||||
|
|
@ -460,6 +533,25 @@ function ManualTextChat({
|
|||
|
||||
const inputDisabled = disabled || !session;
|
||||
|
||||
if (!started && !session) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
{disabledReason ? <DisabledNotice reason={disabledReason} /> : null}
|
||||
<EmptyState
|
||||
icon={<MessageSquareText className="h-7 w-7" />}
|
||||
title="Chat with this agent"
|
||||
description="Test the agent over a text conversation. Send messages and see how it responds, with tool calls and rewind support."
|
||||
action={
|
||||
<Button onClick={() => setStarted(true)} disabled={disabled || !ready}>
|
||||
<MessageSquareText className="h-4 w-4" />
|
||||
Start Test
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{disabledReason ? (
|
||||
|
|
@ -484,11 +576,19 @@ function ManualTextChat({
|
|||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 py-1">
|
||||
{turns.map((turn) => (
|
||||
{turns.map((turn) => {
|
||||
const toolEvents = extractToolEvents(turn.events);
|
||||
return (
|
||||
<div key={turn.id} className="group space-y-1.5">
|
||||
{turn.user_message ? (
|
||||
<MessageBubble role="user" text={turn.user_message.text} />
|
||||
) : null}
|
||||
{toolEvents.map((event, index) => (
|
||||
<ToolEventBubble
|
||||
key={`${turn.id}-${event.kind}-${event.functionName}-${index}`}
|
||||
event={event}
|
||||
/>
|
||||
))}
|
||||
{turn.assistant_message ? (
|
||||
<MessageBubble role="agent" text={turn.assistant_message.text} />
|
||||
) : turn.status === "failed" ? (
|
||||
|
|
@ -510,7 +610,8 @@ function ManualTextChat({
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{sendingMessage ? <TypingBubble /> : null}
|
||||
<div ref={scrollEndRef} />
|
||||
</div>
|
||||
|
|
@ -634,6 +735,7 @@ export function WorkflowTesterPanel({
|
|||
const [activeMode, setActiveMode] = useState<"audio" | "text">("audio");
|
||||
const [chatMode, setChatMode] = useState<"manual" | "simulated">("manual");
|
||||
const [chatSessionKey, setChatSessionKey] = useState(0);
|
||||
const [chatActive, setChatActive] = useState(false);
|
||||
const [voiceRunId, setVoiceRunId] = useState<number | null>(null);
|
||||
const [creatingVoiceRun, setCreatingVoiceRun] = useState(false);
|
||||
const [tokenReady, setTokenReady] = useState(false);
|
||||
|
|
@ -788,7 +890,7 @@ export function WorkflowTesterPanel({
|
|||
<div className="flex h-full min-h-0 flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<ChatModeToggle value={chatMode} onChange={setChatMode} />
|
||||
{chatMode === "manual" ? (
|
||||
{chatMode === "manual" && chatActive ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
|
@ -810,6 +912,7 @@ export function WorkflowTesterPanel({
|
|||
initialContextVariables={initialContextVariables}
|
||||
disabled={testerBlocked}
|
||||
disabledReason={effectiveDisabledReason}
|
||||
onActiveChange={setChatActive}
|
||||
/>
|
||||
) : (
|
||||
<AiSimulatorPlaceholder disabledReason={effectiveDisabledReason} />
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { downloadFile } from '@/lib/files';
|
|||
import { getRandomId } from '@/lib/utils';
|
||||
|
||||
interface WorkflowRunResponse {
|
||||
mode: string;
|
||||
is_completed: boolean;
|
||||
transcript_url: string | null;
|
||||
recording_url: string | null;
|
||||
|
|
@ -183,6 +184,7 @@ export default function WorkflowRunPage() {
|
|||
});
|
||||
setIsLoading(false);
|
||||
const runData = {
|
||||
mode: response.data?.mode ?? '',
|
||||
is_completed: response.data?.is_completed ?? false,
|
||||
transcript_url: response.data?.transcript_url ?? null,
|
||||
recording_url: response.data?.recording_url ?? null,
|
||||
|
|
@ -223,6 +225,8 @@ export default function WorkflowRunPage() {
|
|||
};
|
||||
|
||||
let returnValue = null;
|
||||
const isTextChatRun = workflowRun?.mode === WORKFLOW_RUN_MODES.TEXTCHAT;
|
||||
const showHistoricalRunView = Boolean(workflowRun?.is_completed || isTextChatRun);
|
||||
|
||||
if (isLoading) {
|
||||
returnValue = (
|
||||
|
|
@ -246,7 +250,7 @@ export default function WorkflowRunPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
else if (workflowRun?.is_completed) {
|
||||
else if (showHistoricalRunView) {
|
||||
returnValue = (
|
||||
<div className={`flex ${RUN_SHELL_HEIGHT_CLASS} min-h-0 w-full overflow-hidden bg-background`}>
|
||||
<div className="min-w-0 flex-1 overflow-y-auto">
|
||||
|
|
@ -254,27 +258,35 @@ export default function WorkflowRunPage() {
|
|||
<Card className="border-border">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<CardTitle className="text-2xl">Agent Run Completed</CardTitle>
|
||||
<div className="h-8 w-8 bg-emerald-500/20 rounded-full flex items-center justify-center">
|
||||
<svg className="h-5 w-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<CardTitle className="text-2xl">
|
||||
{isTextChatRun ? 'Text Chat Session' : 'Agent Run Completed'}
|
||||
</CardTitle>
|
||||
<div className={`h-8 w-8 rounded-full flex items-center justify-center ${isTextChatRun ? 'bg-sky-500/15' : 'bg-emerald-500/20'}`}>
|
||||
{isTextChatRun ? (
|
||||
<FileText className="h-5 w-5 text-sky-500" />
|
||||
) : (
|
||||
<svg className="h-5 w-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleTestAgain}
|
||||
disabled={startingCall}
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
>
|
||||
{startingCall ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Phone className="h-4 w-4" />
|
||||
)}
|
||||
{startingCall ? 'Starting...' : 'Test Again'}
|
||||
</Button>
|
||||
{!isTextChatRun && (
|
||||
<Button
|
||||
onClick={handleTestAgain}
|
||||
disabled={startingCall}
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
>
|
||||
{startingCall ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Phone className="h-4 w-4" />
|
||||
)}
|
||||
{startingCall ? 'Starting...' : 'Test Again'}
|
||||
</Button>
|
||||
)}
|
||||
<Link href={`/workflow/${params.workflowId}`}>
|
||||
<Button
|
||||
ref={customizeButtonRef}
|
||||
|
|
@ -294,41 +306,49 @@ export default function WorkflowRunPage() {
|
|||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-8">Your voice agent run has been completed successfully. You can preview or download the transcript and recording.</p>
|
||||
<p className="text-muted-foreground mb-8">
|
||||
{isTextChatRun
|
||||
? 'Review the conversation history, metrics, and context captured for this text session.'
|
||||
: 'Your voice agent run has been completed successfully. You can preview or download the transcript and recording.'}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Preview:</span>
|
||||
<MediaPreviewButton
|
||||
recordingUrl={workflowRun?.recording_url}
|
||||
transcriptUrl={workflowRun?.transcript_url}
|
||||
runId={Number(params.runId)}
|
||||
onOpenPreview={openPreview}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-l border-border pl-4">
|
||||
<span className="text-sm text-muted-foreground">Download:</span>
|
||||
<Button
|
||||
onClick={() => downloadFile(workflowRun?.transcript_url)}
|
||||
disabled={!workflowRun?.transcript_url || !auth.isAuthenticated}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Transcript
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => downloadFile(workflowRun?.recording_url)}
|
||||
disabled={!workflowRun?.recording_url || !auth.isAuthenticated}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Video className="h-4 w-4" />
|
||||
Recording
|
||||
</Button>
|
||||
</div>
|
||||
{!isTextChatRun && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Preview:</span>
|
||||
<MediaPreviewButton
|
||||
recordingUrl={workflowRun?.recording_url}
|
||||
transcriptUrl={workflowRun?.transcript_url}
|
||||
runId={Number(params.runId)}
|
||||
onOpenPreview={openPreview}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 border-l border-border pl-4">
|
||||
<span className="text-sm text-muted-foreground">Download:</span>
|
||||
<Button
|
||||
onClick={() => downloadFile(workflowRun?.transcript_url ?? null)}
|
||||
disabled={!workflowRun?.transcript_url || !auth.isAuthenticated}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Transcript
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => downloadFile(workflowRun?.recording_url ?? null)}
|
||||
disabled={!workflowRun?.recording_url || !auth.isAuthenticated}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
<Video className="h-4 w-4" />
|
||||
Recording
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{workflowRun?.gathered_context?.trace_url && (
|
||||
<div className="flex items-center gap-2 border-l border-border pl-4">
|
||||
<div className={`flex items-center gap-2 ${isTextChatRun ? '' : 'border-l border-border pl-4'}`}>
|
||||
<span className="text-sm text-muted-foreground">Trace:</span>
|
||||
<Button
|
||||
asChild
|
||||
|
|
@ -352,19 +372,19 @@ export default function WorkflowRunPage() {
|
|||
</Card>
|
||||
|
||||
<RunMetricsSection
|
||||
costInfo={workflowRun?.cost_info}
|
||||
logs={workflowRun?.logs}
|
||||
gatheredContext={workflowRun?.gathered_context}
|
||||
costInfo={workflowRun?.cost_info ?? null}
|
||||
logs={workflowRun?.logs ?? null}
|
||||
gatheredContext={workflowRun?.gathered_context ?? null}
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<ContextDisplay
|
||||
title="Initial Context"
|
||||
context={workflowRun?.initial_context}
|
||||
context={workflowRun?.initial_context ?? null}
|
||||
/>
|
||||
<ContextDisplay
|
||||
title="Gathered Context"
|
||||
context={workflowRun?.gathered_context}
|
||||
context={workflowRun?.gathered_context ?? null}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -379,7 +399,7 @@ export default function WorkflowRunPage() {
|
|||
|
||||
<div className="h-full min-h-0 w-[420px] shrink-0 border-l border-border bg-background p-5">
|
||||
<TranscriptRailFrame className="h-full">
|
||||
<RealtimeFeedback mode="historical" logs={workflowRun?.logs} />
|
||||
<RealtimeFeedback mode="historical" logs={workflowRun?.logs ?? null} />
|
||||
</TranscriptRailFrame>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -411,7 +431,7 @@ export default function WorkflowRunPage() {
|
|||
{dialog}
|
||||
|
||||
{/* Onboarding Tooltip for Customize Workflow */}
|
||||
{workflowRun?.is_completed && (
|
||||
{showHistoricalRunView && (
|
||||
<OnboardingTooltip
|
||||
title='Customize Your Workflow'
|
||||
targetRef={customizeButtonRef}
|
||||
|
|
|
|||
|
|
@ -4675,6 +4675,10 @@ export type WorkflowRunUsageResponse = {
|
|||
* Call Type
|
||||
*/
|
||||
call_type?: string | null;
|
||||
/**
|
||||
* Mode
|
||||
*/
|
||||
mode?: string | null;
|
||||
/**
|
||||
* Disposition
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue