Merge branch 'main' into feat/speaches-integration

This commit is contained in:
Abhishek Kumar 2026-03-28 13:52:28 +05:30
commit 2eaaabd936
20 changed files with 357 additions and 45 deletions

View file

@ -1,6 +1,7 @@
"use client";
import { ArrowLeft, Check, Clock, Download, Pause, Pencil, Play, RefreshCw, X } from 'lucide-react';
import { format } from 'date-fns';
import { ArrowLeft, CalendarIcon, Check, Clock, Download, Pause, Pencil, Play, RefreshCw, X } from 'lucide-react';
import { useParams, useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
@ -16,7 +17,11 @@ import {
import type { CampaignResponse } from '@/client/types.gen';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Separator } from '@/components/ui/separator';
import { CampaignRuns } from '@/components/workflow-runs';
import { useAuth } from '@/lib/auth';
@ -43,6 +48,13 @@ export default function CampaignDetailPage() {
const [isExecutingAction, setIsExecutingAction] = useState(false);
const [isDownloadingReport, setIsDownloadingReport] = useState(false);
// Report date range state
const [reportStartDate, setReportStartDate] = useState<Date | undefined>(undefined);
const [reportStartTime, setReportStartTime] = useState('00:00');
const [reportEndDate, setReportEndDate] = useState<Date | undefined>(undefined);
const [reportEndTime, setReportEndTime] = useState('23:59');
const [isReportPopoverOpen, setIsReportPopoverOpen] = useState(false);
// Fetch campaign details
const fetchCampaign = useCallback(async () => {
if (!user) return;
@ -113,16 +125,33 @@ export default function CampaignDetailPage() {
}
};
// Build ISO datetime string from date + time
const buildDateTime = (date: Date | undefined, time: string): string | undefined => {
if (!date) return undefined;
const [hours, minutes] = time.split(':').map(Number);
const combined = new Date(date);
combined.setHours(hours, minutes, 0, 0);
return combined.toISOString();
};
// Handle download report
const handleDownloadReport = async () => {
if (!user) return;
setIsDownloadingReport(true);
setIsReportPopoverOpen(false);
try {
const accessToken = await getAccessToken();
const startDate = buildDateTime(reportStartDate, reportStartTime);
const endDate = buildDateTime(reportEndDate, reportEndTime);
const response = await downloadCampaignReportApiV1CampaignCampaignIdReportGet({
path: {
campaign_id: campaignId,
},
query: {
start_date: startDate,
end_date: endDate,
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
@ -150,6 +179,13 @@ export default function CampaignDetailPage() {
}
};
const handleClearDateRange = () => {
setReportStartDate(undefined);
setReportStartTime('00:00');
setReportEndDate(undefined);
setReportEndTime('23:59');
};
// Handle start campaign
const handleStart = async () => {
if (!user) return;
@ -368,10 +404,85 @@ export default function CampaignDetailPage() {
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleDownloadReport} disabled={isDownloadingReport}>
<Download className="h-4 w-4 mr-2" />
Download Report
</Button>
<Popover open={isReportPopoverOpen} onOpenChange={setIsReportPopoverOpen}>
<PopoverTrigger asChild>
<Button variant="outline" disabled={isDownloadingReport}>
<Download className="h-4 w-4 mr-2" />
Download Report
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-4" align="end">
<div className="space-y-4">
<div className="text-sm font-medium">Filter by date range</div>
<div className="grid gap-3">
<div className="space-y-1.5">
<Label className="text-xs">From</Label>
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="w-[140px] justify-start text-left font-normal">
<CalendarIcon className="mr-2 h-3.5 w-3.5" />
{reportStartDate ? format(reportStartDate, 'MMM dd, yyyy') : 'Start date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={reportStartDate}
onSelect={setReportStartDate}
disabled={(date) => reportEndDate ? date > reportEndDate : false}
/>
</PopoverContent>
</Popover>
<Input
type="time"
value={reportStartTime}
onChange={(e) => setReportStartTime(e.target.value)}
className="w-[100px] h-8 text-xs"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs">To</Label>
<div className="flex gap-2">
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="w-[140px] justify-start text-left font-normal">
<CalendarIcon className="mr-2 h-3.5 w-3.5" />
{reportEndDate ? format(reportEndDate, 'MMM dd, yyyy') : 'End date'}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={reportEndDate}
onSelect={setReportEndDate}
disabled={(date) => reportStartDate ? date < reportStartDate : false}
/>
</PopoverContent>
</Popover>
<Input
type="time"
value={reportEndTime}
onChange={(e) => setReportEndTime(e.target.value)}
className="w-[100px] h-8 text-xs"
/>
</div>
</div>
</div>
<Separator />
<div className="flex justify-between">
<Button variant="ghost" size="sm" onClick={handleClearDateRange}>
Clear
</Button>
<Button size="sm" onClick={handleDownloadReport} disabled={isDownloadingReport}>
<Download className="h-3.5 w-3.5 mr-1.5" />
{reportStartDate || reportEndDate ? 'Download Filtered' : 'Download All'}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
{renderActionButton()}
</div>
</div>

View file

@ -43,6 +43,7 @@ const BrowserCall = ({ workflowId, workflowRunId, initialContextVariables }: {
apiKeyModalOpen,
setApiKeyModalOpen,
apiKeyError,
apiKeyErrorCode,
workflowConfigError,
workflowConfigModalOpen,
setWorkflowConfigModalOpen,
@ -91,10 +92,14 @@ const BrowserCall = ({ workflowId, workflowRunId, initialContextVariables }: {
};
}, [isCompleted, auth.isAuthenticated, workflowId, workflowRunId]);
const navigateToApiKeys = () => {
const navigateToCredits = () => {
router.push('/api-keys');
};
const navigateToModelConfig = () => {
router.push('/model-configurations');
};
const navigateToWorkflow = () => {
router.push(`/workflow/${workflowId}`)
}
@ -161,7 +166,9 @@ const BrowserCall = ({ workflowId, workflowRunId, initialContextVariables }: {
open={apiKeyModalOpen}
onOpenChange={setApiKeyModalOpen}
error={apiKeyError}
onNavigateToApiKeys={navigateToApiKeys}
errorCode={apiKeyErrorCode}
onNavigateToCredits={navigateToCredits}
onNavigateToModelConfig={navigateToModelConfig}
/>
<WorkflowConfigErrorDialog

View file

@ -7,23 +7,25 @@ interface ApiKeyErrorDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
error: string | null;
onNavigateToApiKeys: () => void;
errorCode: string | null;
onNavigateToCredits: () => void;
onNavigateToModelConfig: () => void;
}
export const ApiKeyErrorDialog = ({
open,
onOpenChange,
error,
onNavigateToApiKeys
errorCode,
onNavigateToCredits,
onNavigateToModelConfig,
}: ApiKeyErrorDialogProps) => {
// Check if this is a quota error based on the error message
const isQuotaError = error?.toLowerCase().includes('insufficient') ||
error?.toLowerCase().includes('credits') ||
error?.toLowerCase().includes('quota');
const isQuotaError = errorCode === 'quota_exceeded';
const title = isQuotaError ? "Insufficient Credits" : "API Configuration Error";
const icon = isQuotaError ? <CreditCard className="h-5 w-5 text-orange-500" /> : <Key className="h-5 w-5 text-red-500" />;
const buttonText = isQuotaError ? "Add Credits" : "Go to API Keys Settings";
const buttonText = isQuotaError ? "Add Credits" : "Go to Model Configurations";
const onNavigate = isQuotaError ? onNavigateToCredits : onNavigateToModelConfig;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@ -51,7 +53,7 @@ export const ApiKeyErrorDialog = ({
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={onNavigateToApiKeys}>
<Button onClick={onNavigate}>
{buttonText}
</Button>
</DialogFooter>

View file

@ -42,6 +42,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
const [isCompleted, setIsCompleted] = useState(false);
const [apiKeyModalOpen, setApiKeyModalOpen] = useState(false);
const [apiKeyError, setApiKeyError] = useState<string | null>(null);
const [apiKeyErrorCode, setApiKeyErrorCode] = useState<string | null>(null);
const [workflowConfigModalOpen, setWorkflowConfigModalOpen] = useState(false);
const [workflowConfigError, setWorkflowConfigError] = useState<string | null>(null);
const [isStarting, setIsStarting] = useState(false);
@ -264,12 +265,15 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
break;
case 'error':
// Check if this is a quota exceeded error
if (message.payload?.error_type === 'quota_exceeded') {
// Check if this is a quota/service key error
if (message.payload?.error_type === 'quota_exceeded' ||
message.payload?.error_type === 'invalid_service_key' ||
message.payload?.error_type === 'quota_check_failed') {
// Log as info since it's a handled business logic case
logger.info('Quota exceeded, showing user dialog:', message.payload.message);
logger.info('Quota/service key error, showing user dialog:', message.payload.message);
// Set error state for display
setApiKeyErrorCode(message.payload.error_type);
setApiKeyError(message.payload.message || 'Service quota exceeded');
setApiKeyModalOpen(true);
@ -545,6 +549,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
if (response.error) {
setApiKeyModalOpen(true);
setApiKeyErrorCode('invalid_api_key');
let msg = 'API Key Error';
const detail = (response.error as unknown as { detail?: { errors: { model: string; message: string }[] } }).detail;
if (Array.isArray(detail)) {
@ -685,6 +690,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
apiKeyModalOpen,
setApiKeyModalOpen,
apiKeyError,
apiKeyErrorCode,
workflowConfigError,
workflowConfigModalOpen,
setWorkflowConfigModalOpen,

View file

@ -3274,7 +3274,16 @@ export type DownloadCampaignReportApiV1CampaignCampaignIdReportGetData = {
path: {
campaign_id: number;
};
query?: never;
query?: {
/**
* Filter runs created on or after this datetime (ISO 8601)
*/
start_date?: string | null;
/**
* Filter runs created on or before this datetime (ISO 8601)
*/
end_date?: string | null;
};
url: '/api/v1/campaign/{campaign_id}/report';
};

View file

@ -36,10 +36,26 @@ const EdgeDetailsDialog = ({ open, onOpenChange, data, onSave }: EdgeDetailsDial
}
}, [data, open]);
const handleSave = () => {
const handleSave = useCallback(() => {
onSave({ condition: condition, label: label, transition_speech: transitionSpeech || undefined });
onOpenChange(false);
};
}, [condition, label, transitionSpeech, onSave, onOpenChange]);
// Handle Cmd+S / Ctrl+S keyboard shortcut to save
useEffect(() => {
if (!open) return;
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault();
e.stopImmediatePropagation();
handleSave();
}
};
window.addEventListener('keydown', handleKeyDown, true);
return () => window.removeEventListener('keydown', handleKeyDown, true);
}, [open, handleSave]);
return (
<Dialog open={open} onOpenChange={onOpenChange}>