chore: remove looptalk (#299)

* chore: remove looptalk

Remove looptalk in the current version. We will be rethinking looptalk in a fresh way.

* chore: formatting fix
This commit is contained in:
Abhishek 2026-05-16 17:45:12 +05:30 committed by GitHub
parent 0523dcb079
commit 45b00cd5d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 214 additions and 4634 deletions

View file

@ -1,21 +0,0 @@
import React, { ReactNode } from 'react'
import AppLayout from '@/components/layout/AppLayout'
interface LoopTalkLayoutProps {
children: ReactNode,
headerActions?: ReactNode,
backButton?: ReactNode,
}
const LoopTalkLayout: React.FC<LoopTalkLayoutProps> = ({ children, headerActions }) => {
// backButton is kept in interface for backward compatibility
// but not used with the new sidebar layout
return (
<AppLayout headerActions={headerActions}>
{children}
</AppLayout>
)
}
export default LoopTalkLayout

View file

@ -1,127 +0,0 @@
'use client';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet } from '@/client/sdk.gen';
import type { TestSessionResponse } from '@/client/types.gen';
import { ConversationsList } from '@/components/looptalk/ConversationsList';
import { LiveAudioPlayer } from '@/components/looptalk/LiveAudioPlayer';
import { TestSessionControls } from '@/components/looptalk/TestSessionControls';
import { TestSessionDetails } from '@/components/looptalk/TestSessionDetails';
import { Button } from '@/components/ui/button';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
import LoopTalkLayout from "../LoopTalkLayout";
function TestSessionLoading() {
return (
<div className="container mx-auto px-4 py-8">
<div className="space-y-4">
<div className="h-32 bg-muted rounded-lg animate-pulse"></div>
<div className="h-20 bg-muted rounded-lg animate-pulse"></div>
<div className="h-64 bg-muted rounded-lg animate-pulse"></div>
</div>
</div>
);
}
function TestSessionPageContent() {
const params = useParams();
const testSessionId = parseInt(params.id as string);
const { user, loading: authLoading } = useAuth();
const hasFetched = useRef(false);
const [testSession, setTestSession] = useState<TestSessionResponse | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (authLoading || !user || hasFetched.current) return;
hasFetched.current = true;
const fetchTestSession = async () => {
try {
const response = await getTestSessionApiV1LooptalkTestSessionsTestSessionIdGet({
path: {
test_session_id: testSessionId
},
});
if (!response.data) {
setError('Test session not found');
return;
}
setTestSession(response.data);
} catch (err) {
logger.error(`Error fetching test session: ${err}`);
setError('Failed to load test session');
}
};
fetchTestSession();
}, [authLoading, user, testSessionId]);
if (authLoading || (testSession === null && !error)) {
return <TestSessionLoading />;
}
if (error || !testSession) {
return (
<div className="container mx-auto px-4 py-8">
<div className="text-red-500 text-center py-8">
{error || 'Test session not found'}
</div>
</div>
);
}
const sessionForUI = {
id: testSession.id,
name: testSession.name,
description: '',
test_type: testSession.test_index !== null ? 'load_test' : 'single',
status: testSession.status,
actor_workflow_name: `Workflow ${testSession.actor_workflow_id}`,
adversary_workflow_name: `Workflow ${testSession.adversary_workflow_id}`,
created_at: testSession.created_at,
updated_at: testSession.created_at,
test_metadata: testSession.config
};
return (
<div className="container mx-auto px-4 py-8">
<TestSessionDetails session={sessionForUI} />
<TestSessionControls session={sessionForUI} />
<div className="mt-6">
<LiveAudioPlayer
testSessionId={testSessionId}
sessionStatus={testSession.status as 'pending' | 'running' | 'completed' | 'failed'}
autoStart={true}
/>
</div>
<div className="mt-8">
<h2 className="text-xl font-bold mb-4">Conversations</h2>
<ConversationsList testSessionId={testSessionId} />
</div>
</div>
);
}
export default function TestSessionPage() {
const backButton = (
<Link href="/looptalk">
<Button variant="ghost" size="sm">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Test Sessions
</Button>
</Link>
);
return (
<LoopTalkLayout backButton={backButton}>
<TestSessionPageContent />
</LoopTalkLayout>
);
}

View file

@ -1,40 +0,0 @@
"use client";
import { MessageSquare } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
export default function LoopTalkPage() {
return (
<div className="container mx-auto p-6 space-y-6">
<div>
<h1 className="text-3xl font-bold mb-2">LoopTalk</h1>
<p>Enable voice agents to talk to each other and create artificial datasets</p>
</div>
<Card>
<CardHeader>
<CardTitle>Coming Soon</CardTitle>
<CardDescription>
LoopTalk features are currently under development
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-12">
<MessageSquare className="w-16 h-16 mx-auto mb-6" />
<p className="text-lg mb-4">
We&apos;re building LoopTalk to enable voice agents to communicate with each other,
allowing you to generate artificial datasets for training and testing.
</p>
<p>
This powerful feature will help you create comprehensive test scenarios and improve your voice AI workflows.
</p>
<p className="mt-4">
Check back soon for updates!
</p>
</div>
</CardContent>
</Card>
</div>
);
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -872,34 +872,6 @@ export type CreateCredentialRequest = {
};
};
/**
* CreateLoadTestRequest
*/
export type CreateLoadTestRequest = {
/**
* Name Prefix
*/
name_prefix: string;
/**
* Actor Workflow Id
*/
actor_workflow_id: number;
/**
* Adversary Workflow Id
*/
adversary_workflow_id: number;
/**
* Test Count
*/
test_count: number;
/**
* Config
*/
config?: {
[key: string]: unknown;
};
};
/**
* CreateServiceKeyRequest
*/
@ -940,30 +912,6 @@ export type CreateServiceKeyResponse = {
expires_at?: string | null;
};
/**
* CreateTestSessionRequest
*/
export type CreateTestSessionRequest = {
/**
* Name
*/
name: string;
/**
* Actor Workflow Id
*/
actor_workflow_id: number;
/**
* Adversary Workflow Id
*/
adversary_workflow_id: number;
/**
* Config
*/
config?: {
[key: string]: unknown;
};
};
/**
* CreateToolRequest
*
@ -2111,38 +2059,6 @@ export type LastCampaignSettingsResponse = {
circuit_breaker?: CircuitBreakerConfigResponse | null;
};
/**
* LoadTestStatsResponse
*/
export type LoadTestStatsResponse = {
/**
* Total
*/
total: number;
/**
* Pending
*/
pending: number;
/**
* Running
*/
running: number;
/**
* Completed
*/
completed: number;
/**
* Failed
*/
failed: number;
/**
* Sessions
*/
sessions: Array<{
[key: string]: unknown;
}>;
};
/**
* LoginRequest
*/
@ -3498,68 +3414,6 @@ export type TelnyxConfigurationResponse = {
from_numbers: Array<string>;
};
/**
* TestSessionResponse
*/
export type TestSessionResponse = {
/**
* Id
*/
id: number;
/**
* Name
*/
name: string;
/**
* Status
*/
status: string;
/**
* Actor Workflow Id
*/
actor_workflow_id: number;
/**
* Adversary Workflow Id
*/
adversary_workflow_id: number;
/**
* Load Test Group Id
*/
load_test_group_id: string | null;
/**
* Test Index
*/
test_index: number | null;
/**
* Config
*/
config: {
[key: string]: unknown;
};
/**
* Results
*/
results: {
[key: string]: unknown;
} | null;
/**
* Error
*/
error: string | null;
/**
* Created At
*/
created_at: string;
/**
* Started At
*/
started_at: string | null;
/**
* Completed At
*/
completed_at: string | null;
};
/**
* TimeSlotRequest
*/
@ -9139,397 +8993,6 @@ export type ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutRes
200: unknown;
};
export type ListTestSessionsApiV1LooptalkTestSessionsGetData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path?: never;
query?: {
/**
* Status
*/
status?: string | null;
/**
* Load Test Group Id
*/
load_test_group_id?: string | null;
/**
* Limit
*/
limit?: number;
/**
* Offset
*/
offset?: number;
};
url: '/api/v1/looptalk/test-sessions';
};
export type ListTestSessionsApiV1LooptalkTestSessionsGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ListTestSessionsApiV1LooptalkTestSessionsGetError = ListTestSessionsApiV1LooptalkTestSessionsGetErrors[keyof ListTestSessionsApiV1LooptalkTestSessionsGetErrors];
export type ListTestSessionsApiV1LooptalkTestSessionsGetResponses = {
/**
* Response List Test Sessions Api V1 Looptalk Test Sessions Get
*
* Successful Response
*/
200: Array<TestSessionResponse>;
};
export type ListTestSessionsApiV1LooptalkTestSessionsGetResponse = ListTestSessionsApiV1LooptalkTestSessionsGetResponses[keyof ListTestSessionsApiV1LooptalkTestSessionsGetResponses];
export type CreateTestSessionApiV1LooptalkTestSessionsPostData = {
body: CreateTestSessionRequest;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path?: never;
query?: never;
url: '/api/v1/looptalk/test-sessions';
};
export type CreateTestSessionApiV1LooptalkTestSessionsPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateTestSessionApiV1LooptalkTestSessionsPostError = CreateTestSessionApiV1LooptalkTestSessionsPostErrors[keyof CreateTestSessionApiV1LooptalkTestSessionsPostErrors];
export type CreateTestSessionApiV1LooptalkTestSessionsPostResponses = {
/**
* Successful Response
*/
200: TestSessionResponse;
};
export type CreateTestSessionApiV1LooptalkTestSessionsPostResponse = CreateTestSessionApiV1LooptalkTestSessionsPostResponses[keyof CreateTestSessionApiV1LooptalkTestSessionsPostResponses];
export type GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path: {
/**
* Test Session Id
*/
test_session_id: number;
};
query?: never;
url: '/api/v1/looptalk/test-sessions/{test_session_id}';
};
export type GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError = GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetErrors[keyof GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetErrors];
export type GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponses = {
/**
* Successful Response
*/
200: TestSessionResponse;
};
export type GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse = GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponses[keyof GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponses];
export type StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path: {
/**
* Test Session Id
*/
test_session_id: number;
};
query?: never;
url: '/api/v1/looptalk/test-sessions/{test_session_id}/start';
};
export type StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError = StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostErrors[keyof StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostErrors];
export type StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path: {
/**
* Test Session Id
*/
test_session_id: number;
};
query?: never;
url: '/api/v1/looptalk/test-sessions/{test_session_id}/stop';
};
export type StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError = StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostErrors[keyof StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostErrors];
export type StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path: {
/**
* Test Session Id
*/
test_session_id: number;
};
query?: never;
url: '/api/v1/looptalk/test-sessions/{test_session_id}/conversation';
};
export type GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError = GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetErrors[keyof GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetErrors];
export type GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type CreateLoadTestApiV1LooptalkLoadTestsPostData = {
body: CreateLoadTestRequest;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path?: never;
query?: never;
url: '/api/v1/looptalk/load-tests';
};
export type CreateLoadTestApiV1LooptalkLoadTestsPostErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateLoadTestApiV1LooptalkLoadTestsPostError = CreateLoadTestApiV1LooptalkLoadTestsPostErrors[keyof CreateLoadTestApiV1LooptalkLoadTestsPostErrors];
export type CreateLoadTestApiV1LooptalkLoadTestsPostResponses = {
/**
* Response Create Load Test Api V1 Looptalk Load Tests Post
*
* Successful Response
*/
200: {
[key: string]: unknown;
};
};
export type CreateLoadTestApiV1LooptalkLoadTestsPostResponse = CreateLoadTestApiV1LooptalkLoadTestsPostResponses[keyof CreateLoadTestApiV1LooptalkLoadTestsPostResponses];
export type GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path: {
/**
* Load Test Group Id
*/
load_test_group_id: string;
};
query?: never;
url: '/api/v1/looptalk/load-tests/{load_test_group_id}/stats';
};
export type GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError = GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetErrors[keyof GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetErrors];
export type GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponses = {
/**
* Successful Response
*/
200: LoadTestStatsResponse;
};
export type GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse = GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponses[keyof GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponses];
export type GetActiveTestsApiV1LooptalkActiveTestsGetData = {
body?: never;
headers?: {
/**
* Authorization
*/
authorization?: string | null;
/**
* X-Api-Key
*/
'X-API-Key'?: string | null;
};
path?: never;
query?: never;
url: '/api/v1/looptalk/active-tests';
};
export type GetActiveTestsApiV1LooptalkActiveTestsGetErrors = {
/**
* Not found
*/
404: unknown;
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetActiveTestsApiV1LooptalkActiveTestsGetError = GetActiveTestsApiV1LooptalkActiveTestsGetErrors[keyof GetActiveTestsApiV1LooptalkActiveTestsGetErrors];
export type GetActiveTestsApiV1LooptalkActiveTestsGetResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData = {
body?: never;
headers?: {

View file

@ -1,126 +0,0 @@
'use client';
import { format } from 'date-fns';
import { useEffect, useState } from 'react';
import { getTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGet } from '@/client/sdk.gen';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
import { Conversation } from './types';
interface ConversationsListProps {
testSessionId: number;
}
export function ConversationsList({ testSessionId }: ConversationsListProps) {
const [conversations, setConversations] = useState<Conversation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { user } = useAuth();
useEffect(() => {
const fetchConversations = async () => {
if (!user) return;
try {
const response = await getTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGet({
path: {
test_session_id: testSessionId
},
});
// API returns { conversation: Conversation | null }
const responseData = response.data as { conversation: Conversation | null } | null;
if (responseData?.conversation) {
setConversations([responseData.conversation]);
} else {
setConversations([]);
}
} catch (err) {
logger.error('Error fetching conversations:', err);
setError('Failed to load conversations');
} finally {
setLoading(false);
}
};
fetchConversations();
// Poll for updates every 5 seconds
const interval = setInterval(fetchConversations, 5000);
return () => clearInterval(interval);
}, [testSessionId, user]);
if (loading && conversations.length === 0) {
return (
<div className="space-y-4">
{Array.from({ length: 3 }, (_, i) => (
<Card key={i} className="h-24 bg-gray-200 animate-pulse" />
))}
</div>
);
}
if (error) {
return (
<div className="text-red-500">
{error}
</div>
);
}
if (conversations.length === 0) {
return (
<Card>
<CardContent className="text-center py-8">
<div className="text-gray-500 mb-2">
No conversations started yet
</div>
<p className="text-sm text-gray-400">
Start the test session to begin agent conversations
</p>
</CardContent>
</Card>
);
}
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'active':
return 'default';
case 'completed':
return 'secondary';
case 'failed':
return 'destructive';
default:
return 'outline';
}
};
return (
<div className="space-y-4">
{conversations.map((conversation) => (
<Card key={conversation.id}>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg">
Conversation {conversation.conversation_pair_id || conversation.id}
</CardTitle>
<CardDescription>
Started: {format(new Date(conversation.created_at), 'h:mm:ss a')}
</CardDescription>
</div>
<Badge variant={getStatusBadgeVariant(conversation.status)}>
{conversation.status}
</Badge>
</div>
</CardHeader>
</Card>
))}
</div>
);
}

View file

@ -1,177 +0,0 @@
'use client';
import { Plus } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'sonner';
import { createTestSessionApiV1LooptalkTestSessionsPost } from '@/client/sdk.gen';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
export function CreateTestSessionButton() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const { user } = useAuth();
const [formData, setFormData] = useState({
name: '',
description: '',
test_type: 'single',
actor_workflow_id: '',
adversary_workflow_id: '',
concurrent_pairs: 1,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
if (!user) return;
const response = await createTestSessionApiV1LooptalkTestSessionsPost({
body: {
name: formData.name,
actor_workflow_id: parseInt(formData.actor_workflow_id),
adversary_workflow_id: parseInt(formData.adversary_workflow_id),
config: {
test_type: formData.test_type,
description: formData.description,
concurrent_pairs: formData.test_type === 'load_test' ? formData.concurrent_pairs : undefined
}
},
});
toast.success('Test session created successfully');
setOpen(false);
if (response.data?.id) {
router.push(`/looptalk/${response.data.id}`);
} else {
router.push('/looptalk');
}
} catch (error) {
logger.error('Error creating test session:', error);
toast.error('Failed to create test session');
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
New Test Session
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[525px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Create Test Session</DialogTitle>
<DialogDescription>
Set up a new LoopTalk test session to test conversations between agents.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Test Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="My Test Session"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder="Optional description of the test"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="test_type">Test Type</Label>
<Select
value={formData.test_type}
onValueChange={(value) => setFormData({ ...formData, test_type: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="single">Single Test</SelectItem>
{/* <SelectItem value="load_test">Load Test</SelectItem> */}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="actor_workflow">Actor Workflow ID</Label>
<Input
id="actor_workflow"
type="number"
value={formData.actor_workflow_id}
onChange={(e) => setFormData({ ...formData, actor_workflow_id: e.target.value })}
placeholder="Enter workflow ID"
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="adversary_workflow">Adversary Workflow ID</Label>
<Input
id="adversary_workflow"
type="number"
value={formData.adversary_workflow_id}
onChange={(e) => setFormData({ ...formData, adversary_workflow_id: e.target.value })}
placeholder="Enter workflow ID"
required
/>
</div>
{formData.test_type === 'load_test' && (
<div className="grid gap-2">
<Label htmlFor="concurrent_pairs">Concurrent Pairs</Label>
<Input
id="concurrent_pairs"
type="number"
min="1"
max="10"
value={formData.concurrent_pairs}
onChange={(e) => setFormData({ ...formData, concurrent_pairs: parseInt(e.target.value) || 1 })}
required
/>
</div>
)}
</div>
<DialogFooter>
<Button type="submit" disabled={loading}>
{loading ? 'Creating...' : 'Create Test Session'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}

View file

@ -1,370 +0,0 @@
'use client';
import { Pause, Play, Volume2, VolumeX } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
interface LiveAudioPlayerProps {
testSessionId: number;
sessionStatus: 'pending' | 'running' | 'completed' | 'failed';
autoStart?: boolean;
}
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
type AudioRole = 'mixed' | 'actor' | 'adversary';
export function LiveAudioPlayer({
testSessionId,
sessionStatus,
autoStart = false
}: LiveAudioPlayerProps) {
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
const [audioRole, setAudioRole] = useState<AudioRole>(() => {
// Load saved preference from localStorage
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('looptalk-audio-role');
return (saved as AudioRole) || 'mixed';
}
return 'mixed';
});
const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(0.8);
const [bufferedDuration, setBufferedDuration] = useState(0);
const [audioLevel, setAudioLevel] = useState(0);
const wsRef = useRef<WebSocket | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const gainNodeRef = useRef<GainNode | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const audioQueueRef = useRef<AudioBufferSourceNode[]>([]);
const nextStartTimeRef = useRef(0);
const animationFrameRef = useRef<number | undefined>(undefined);
const isConnectingRef = useRef(false);
const { user, getAccessToken } = useAuth();
// Auto-start streaming when session starts
useEffect(() => {
if (sessionStatus === 'running' && autoStart && !isPlaying) {
setIsPlaying(true);
}
}, [sessionStatus, autoStart, isPlaying]);
// Save audio role preference
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('looptalk-audio-role', audioRole);
}
}, [audioRole]);
// Audio level monitoring
const monitorAudioLevel = useCallback(() => {
if (!analyserRef.current) return;
const dataArray = new Uint8Array(analyserRef.current.frequencyBinCount);
analyserRef.current.getByteFrequencyData(dataArray);
// Calculate average level
const average = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
setAudioLevel(average / 255); // Normalize to 0-1
animationFrameRef.current = requestAnimationFrame(monitorAudioLevel);
}, []);
const connectWebSocket = useCallback(async () => {
// Check if already connected or connecting
if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) {
logger.debug('WebSocket already connected or connecting, skipping');
return;
}
// Prevent multiple concurrent connection attempts
if (isConnectingRef.current) {
logger.debug('Already attempting to connect, skipping');
return;
}
isConnectingRef.current = true;
try {
setConnectionStatus('connecting');
if (!user) return;
// Get auth token
const accessToken = await getAccessToken();
const httpBase = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
const baseUrl = httpBase.replace(/^http/, 'ws');
const wsUrl = `${baseUrl}/api/v1/looptalk/test-sessions/${testSessionId}/audio-stream?role=${audioRole}&token=${encodeURIComponent(accessToken || '')}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
// Create AudioContext with gain control and analyser
if (!audioContextRef.current) {
audioContextRef.current = new AudioContext();
gainNodeRef.current = audioContextRef.current.createGain();
analyserRef.current = audioContextRef.current.createAnalyser();
analyserRef.current.fftSize = 256;
// Connect gain -> analyser -> destination
gainNodeRef.current.connect(analyserRef.current);
analyserRef.current.connect(audioContextRef.current.destination);
// Set initial volume
gainNodeRef.current.gain.value = volume;
}
ws.onopen = () => {
setConnectionStatus('connected');
logger.info('Audio stream connected');
monitorAudioLevel();
};
ws.onmessage = async (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'audio' && data.audio) {
// Decode base64 audio data
const audioBytes = Uint8Array.from(atob(data.audio), c => c.charCodeAt(0));
// Create audio buffer from PCM data
const samplesPerChannel = audioBytes.length / (data.num_channels * 2);
const audioBuffer = audioContextRef.current!.createBuffer(
data.num_channels,
samplesPerChannel,
data.sample_rate
);
// Convert PCM to float samples
const dataView = new DataView(audioBytes.buffer);
for (let channel = 0; channel < data.num_channels; channel++) {
const channelData = audioBuffer.getChannelData(channel);
for (let i = 0; i < samplesPerChannel; i++) {
const sampleIndex = i * data.num_channels + channel;
const sample = dataView.getInt16(sampleIndex * 2, true) / 32768.0;
channelData[i] = sample;
}
}
// Schedule audio buffer playback
const source = audioContextRef.current!.createBufferSource();
source.buffer = audioBuffer;
source.connect(gainNodeRef.current!);
// Schedule seamless playback
const currentTime = audioContextRef.current!.currentTime;
if (nextStartTimeRef.current < currentTime) {
nextStartTimeRef.current = currentTime;
}
source.start(nextStartTimeRef.current);
nextStartTimeRef.current += audioBuffer.duration;
// Track scheduled sources
audioQueueRef.current.push(source);
source.onended = () => {
const index = audioQueueRef.current.indexOf(source);
if (index > -1) {
audioQueueRef.current.splice(index, 1);
}
};
setBufferedDuration(nextStartTimeRef.current - currentTime);
}
} catch (error) {
logger.error('Error processing audio data:', error);
}
};
ws.onerror = (error) => {
logger.error('WebSocket error:', error);
setConnectionStatus('error');
};
ws.onclose = (event) => {
setConnectionStatus('disconnected');
logger.info('Audio stream disconnected', { code: event.code, reason: event.reason });
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
} catch (error) {
logger.error('Error connecting to audio stream:', error);
setConnectionStatus('error');
} finally {
isConnectingRef.current = false;
}
}, [testSessionId, audioRole, user, getAccessToken, volume, monitorAudioLevel]); // Removed connectionStatus to avoid loops
const disconnect = useCallback(() => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
// Stop all scheduled audio
audioQueueRef.current.forEach(source => {
try {
source.stop();
} catch {
// Ignore if already stopped
}
});
audioQueueRef.current = [];
nextStartTimeRef.current = 0;
setBufferedDuration(0);
setAudioLevel(0);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
}, []);
// Handle play/pause
useEffect(() => {
if (isPlaying && sessionStatus === 'running') {
connectWebSocket();
} else {
disconnect();
}
return () => {
disconnect();
};
}, [isPlaying, sessionStatus, connectWebSocket, disconnect]); // Include stable callbacks
// Handle audio role changes
useEffect(() => {
// Use ref to check connection state to avoid dependency issues
if (isPlaying && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
logger.info('Audio role changed, reconnecting with new role:', audioRole);
// Reconnect with new role
disconnect();
// Set a flag to prevent double connections
const timer = setTimeout(() => {
if (isPlaying) {
connectWebSocket();
}
}, 500);
return () => clearTimeout(timer);
}
}, [audioRole, isPlaying, connectWebSocket, disconnect]); // Include all dependencies
// Update volume
useEffect(() => {
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = volume;
}
}, [volume]);
const getStatusColor = () => {
switch (connectionStatus) {
case 'connected': return 'bg-green-500';
case 'connecting': return 'bg-yellow-500';
case 'error': return 'bg-red-500';
default: return 'bg-gray-500';
}
};
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<Volume2 className="h-5 w-5" />
Live Audio Stream
</CardTitle>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${getStatusColor()}`} />
<Badge variant={connectionStatus === 'connected' ? 'default' : 'secondary'}>
{connectionStatus}
</Badge>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Play/Pause Controls */}
<div className="flex items-center gap-4">
<Button
onClick={() => setIsPlaying(!isPlaying)}
disabled={sessionStatus !== 'running'}
size="sm"
variant={isPlaying ? 'default' : 'outline'}
>
{isPlaying ? (
<>
<Pause className="h-4 w-4 mr-2" />
Pause
</>
) : (
<>
<Play className="h-4 w-4 mr-2" />
Play
</>
)}
</Button>
{/* Audio Role Selector */}
<div className="flex gap-1">
{(['mixed', 'actor', 'adversary'] as const).map((role) => (
<Button
key={role}
size="sm"
variant={audioRole === role ? 'default' : 'outline'}
onClick={() => setAudioRole(role)}
className="capitalize"
>
{role}
</Button>
))}
</div>
</div>
{/* Volume Control */}
<div className="flex items-center gap-4">
<VolumeX className="h-4 w-4 text-gray-500" />
<input
type="range"
value={volume}
onChange={(e) => setVolume(parseFloat(e.target.value))}
min="0"
max="1"
step="0.01"
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
/>
<Volume2 className="h-4 w-4 text-gray-500" />
</div>
{/* Audio Level Meter */}
<div className="space-y-2">
<div className="text-sm text-gray-500">Audio Level</div>
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-green-500 transition-all duration-100"
style={{ width: `${audioLevel * 100}%` }}
/>
</div>
</div>
{/* Status Info */}
<div className="text-sm text-gray-500">
{connectionStatus === 'connected' && (
<>Streaming... (buffered: {bufferedDuration.toFixed(1)}s)</>
)}
{connectionStatus === 'connecting' && 'Connecting to audio stream...'}
{connectionStatus === 'error' && 'Failed to connect to audio stream'}
{connectionStatus === 'disconnected' && sessionStatus === 'running' && 'Click play to start streaming'}
{sessionStatus === 'pending' && 'Waiting for session to start...'}
{sessionStatus === 'completed' && 'Session completed'}
{sessionStatus === 'failed' && 'Session failed'}
</div>
</CardContent>
</Card>
);
}

View file

@ -1,104 +0,0 @@
'use client';
import { useEffect, useState } from 'react';
import { listTestSessionsApiV1LooptalkTestSessionsGet } from '@/client/sdk.gen';
import type { TestSessionResponse } from '@/client/types.gen';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
import { TestSessionCard } from './TestSessionCard';
import { TestSession } from './types';
interface LoopTalkTestSessionsListProps {
status?: 'active' | 'completed' | 'failed';
}
export function LoopTalkTestSessionsList({ status }: LoopTalkTestSessionsListProps) {
const [sessions, setSessions] = useState<TestSession[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { user, getAccessToken } = useAuth();
useEffect(() => {
const fetchSessions = async () => {
if (!user) return;
try {
const accessToken = await getAccessToken();
const response = await listTestSessionsApiV1LooptalkTestSessionsGet({
query: status ? { status } : undefined,
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
// Transform API response to match UI types
const transformedSessions = (response.data || []).map((session: TestSessionResponse) => ({
id: session.id,
name: session.name,
description: '', // API doesn't return description
test_type: session.test_index !== null ? 'load_test' : 'single',
status: session.status,
actor_workflow_name: `Workflow ${session.actor_workflow_id}`,
adversary_workflow_name: `Workflow ${session.adversary_workflow_id}`,
created_at: session.created_at,
updated_at: session.created_at, // API doesn't have updated_at
test_metadata: session.config
}));
setSessions(transformedSessions);
} catch (err) {
logger.error('Error fetching test sessions:', err);
setError('Failed to load test sessions');
} finally {
setLoading(false);
}
};
fetchSessions();
}, [status, user, getAccessToken]);
if (loading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 3 }, (_, i) => (
<div key={i} className="bg-gray-200 rounded-lg h-40 animate-pulse"></div>
))}
</div>
);
}
if (error) {
return (
<div className="text-red-500">
{error}
</div>
);
}
if (sessions.length === 0) {
return (
<div className="text-center py-12 px-4">
<div className="text-gray-500 mb-2">
No {status ? `${status} ` : ''}test sessions found
</div>
{!status && (
<p className="text-sm text-gray-400">
Create a new test session to start testing agent conversations
</p>
)}
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sessions.map((session) => (
<TestSessionCard
key={session.id}
session={session}
/>
))}
</div>
);
}

View file

@ -1,188 +0,0 @@
'use client';
import { Volume2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
interface SimpleAudioPlayerProps {
testSessionId: number;
}
export function SimpleAudioPlayer({ testSessionId }: SimpleAudioPlayerProps) {
const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'error'>('connecting');
const [audioRole, setAudioRole] = useState<'mixed' | 'actor' | 'adversary'>('mixed');
const wsRef = useRef<WebSocket | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const [bufferedDuration, setBufferedDuration] = useState(0);
const { user, getAccessToken } = useAuth();
const audioQueueRef = useRef<AudioBufferSourceNode[]>([]);
const nextStartTimeRef = useRef(0);
useEffect(() => {
const connectWebSocket = async () => {
try {
if (!user) return;
// Get auth token
const accessToken = await getAccessToken();
// Create WebSocket connection - pass token as query param since WebSocket doesn't support headers
const httpBase = process.env.NEXT_PUBLIC_BACKEND_URL || window.location.origin;
const baseUrl = httpBase.replace(/^http/, 'ws');
const wsUrl = `${baseUrl}/api/v1/looptalk/test-sessions/${testSessionId}/audio-stream?role=${audioRole}&token=${encodeURIComponent(accessToken || '')}`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
// Create AudioContext
audioContextRef.current = new AudioContext();
ws.onopen = () => {
setConnectionStatus('connected');
};
ws.onmessage = async (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'audio' && data.audio) {
// Decode base64 audio data
const audioBytes = Uint8Array.from(atob(data.audio), c => c.charCodeAt(0));
// Create audio buffer from PCM data
const samplesPerChannel = audioBytes.length / (data.num_channels * 2); // 16-bit samples
const audioBuffer = audioContextRef.current!.createBuffer(
data.num_channels,
samplesPerChannel,
data.sample_rate
);
// Convert PCM to float samples for each channel
const dataView = new DataView(audioBytes.buffer);
for (let channel = 0; channel < data.num_channels; channel++) {
const channelData = audioBuffer.getChannelData(channel);
for (let i = 0; i < samplesPerChannel; i++) {
// Interleaved PCM data: L,R,L,R,... for stereo
const sampleIndex = i * data.num_channels + channel;
const sample = dataView.getInt16(sampleIndex * 2, true) / 32768.0;
channelData[i] = sample;
}
}
// Schedule audio buffer playback
const source = audioContextRef.current!.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContextRef.current!.destination);
// Schedule seamless playback
const currentTime = audioContextRef.current!.currentTime;
if (nextStartTimeRef.current < currentTime) {
nextStartTimeRef.current = currentTime;
}
source.start(nextStartTimeRef.current);
nextStartTimeRef.current += audioBuffer.duration;
// Keep track of scheduled sources for cleanup
audioQueueRef.current.push(source);
source.onended = () => {
const index = audioQueueRef.current.indexOf(source);
if (index > -1) {
audioQueueRef.current.splice(index, 1);
}
};
setBufferedDuration(prev => prev + audioBuffer.duration);
} else if (data.type === 'keepalive') {
// Connection is alive
}
} catch (error) {
logger.error('Error processing audio data:', error);
}
};
ws.onerror = (error) => {
logger.error('WebSocket error:', error);
setConnectionStatus('error');
};
ws.onclose = () => {
setConnectionStatus('error');
};
} catch (error) {
logger.error('Error connecting to audio stream:', error);
setConnectionStatus('error');
}
};
connectWebSocket();
// Cleanup
return () => {
if (wsRef.current) {
wsRef.current.close();
}
// Stop all scheduled audio
audioQueueRef.current.forEach(source => {
try {
source.stop();
} catch {
// Ignore if already stopped
}
});
audioQueueRef.current = [];
nextStartTimeRef.current = 0;
setBufferedDuration(0);
if (audioContextRef.current) {
audioContextRef.current.close();
}
};
}, [testSessionId, audioRole, user, getAccessToken]);
return (
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Volume2 className="h-5 w-5 text-gray-600" />
<span className="font-medium">Live Audio Stream</span>
</div>
<Badge variant={connectionStatus === 'connected' ? 'default' : connectionStatus === 'error' ? 'destructive' : 'secondary'}>
{connectionStatus}
</Badge>
</div>
<div className="flex gap-2 mb-4">
<button
className={`px-3 py-1 rounded text-sm ${audioRole === 'mixed' ? 'bg-primary text-white' : 'bg-gray-200'}`}
onClick={() => setAudioRole('mixed')}
>
Mixed
</button>
<button
className={`px-3 py-1 rounded text-sm ${audioRole === 'actor' ? 'bg-primary text-white' : 'bg-gray-200'}`}
onClick={() => setAudioRole('actor')}
>
Actor Only
</button>
<button
className={`px-3 py-1 rounded text-sm ${audioRole === 'adversary' ? 'bg-primary text-white' : 'bg-gray-200'}`}
onClick={() => setAudioRole('adversary')}
>
Adversary Only
</button>
</div>
<div className="text-sm text-gray-500">
{connectionStatus === 'connected' && (
<>Audio streaming... (buffered: {bufferedDuration.toFixed(1)}s)</>
)}
{connectionStatus === 'connecting' && 'Connecting to audio stream...'}
{connectionStatus === 'error' && 'Failed to connect to audio stream'}
</div>
</CardContent>
</Card>
);
}

View file

@ -1,103 +0,0 @@
'use client';
import { format } from 'date-fns';
import { Eye, Pause, Play, Users } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { TestSession } from './types';
interface TestSessionCardProps {
session: TestSession;
}
export function TestSessionCard({ session }: TestSessionCardProps) {
const router = useRouter();
const handleViewDetails = () => {
router.push(`/looptalk/${session.id}`);
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'active':
return 'default';
case 'completed':
return 'secondary';
case 'failed':
return 'destructive';
default:
return 'outline';
}
};
const getTestTypeIcon = (type: string) => {
switch (type) {
case 'load_test':
return <Users className="h-4 w-4" />;
default:
return <Play className="h-4 w-4" />;
}
};
return (
<Card className="hover:shadow-lg transition-shadow cursor-pointer" onClick={handleViewDetails}>
<CardHeader>
<div className="flex justify-between items-start">
<CardTitle className="text-lg">{session.name}</CardTitle>
<Badge variant={getStatusBadgeVariant(session.status)}>
{session.status}
</Badge>
</div>
{session.description && (
<CardDescription>{session.description}</CardDescription>
)}
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-gray-600">
{getTestTypeIcon(session.test_type)}
<span className="capitalize">{session.test_type.replace('_', ' ')}</span>
</div>
<div className="text-sm text-gray-500">
Created: {format(new Date(session.created_at), 'MMM d, yyyy h:mm a')}
</div>
{session.test_metadata?.concurrent_pairs && (
<div className="text-sm text-gray-600">
Concurrent pairs: {session.test_metadata.concurrent_pairs}
</div>
)}
</div>
<div className="mt-4 flex gap-2">
{session.status === 'active' && (
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
// TODO: Implement pause functionality
}}
>
<Pause className="h-4 w-4 mr-1" />
Pause
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={(e) => {
e.stopPropagation();
handleViewDetails();
}}
>
<Eye className="h-4 w-4 mr-1" />
View
</Button>
</div>
</CardContent>
</Card>
);
}

View file

@ -1,120 +0,0 @@
'use client';
import { Play, RotateCcw, Square } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'sonner';
import {
startTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPost,
stopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPost
} from '@/client/sdk.gen';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useAuth } from '@/lib/auth';
import logger from '@/lib/logger';
interface TestSessionControlsProps {
session: {
id: number;
status: string;
test_type: string;
};
}
export function TestSessionControls({ session }: TestSessionControlsProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const { user, getAccessToken } = useAuth();
const handleStart = async () => {
if (!user) return;
setLoading(true);
try {
const accessToken = await getAccessToken();
await startTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPost({
path: {
test_session_id: session.id
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
toast.success('Test session started');
router.refresh();
} catch (error) {
logger.error('Error starting test session:', error);
toast.error('Failed to start test session');
} finally {
setLoading(false);
}
};
const handleStop = async () => {
if (!user) return;
setLoading(true);
try {
const accessToken = await getAccessToken();
await stopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPost({
path: {
test_session_id: session.id
},
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
toast.success('Test session stopped');
router.refresh();
} catch (error) {
logger.error('Error stopping test session:', error);
toast.error('Failed to stop test session');
} finally {
setLoading(false);
}
};
return (
<Card className="mt-4">
<CardContent className="pt-6">
<div className="flex gap-2">
{session.status === 'pending' && (
<Button
onClick={handleStart}
disabled={loading}
className="flex items-center gap-2"
>
<Play className="h-4 w-4" />
Start Test
</Button>
)}
{session.status === 'active' && (
<>
<Button
variant="destructive"
onClick={handleStop}
disabled={loading}
className="flex items-center gap-2"
>
<Square className="h-4 w-4" />
Stop Test
</Button>
</>
)}
{session.status === 'completed' && (
<Button
variant="outline"
onClick={handleStart}
disabled={loading}
className="flex items-center gap-2"
>
<RotateCcw className="h-4 w-4" />
Restart Test
</Button>
)}
</div>
</CardContent>
</Card>
);
}

View file

@ -1,71 +0,0 @@
'use client';
import { format } from 'date-fns';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { TestSession } from './types';
interface TestSessionDetailsProps {
session: TestSession;
}
export function TestSessionDetails({ session }: TestSessionDetailsProps) {
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'active':
return 'default';
case 'completed':
return 'secondary';
case 'failed':
return 'destructive';
default:
return 'outline';
}
};
return (
<Card>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-2xl">{session.name}</CardTitle>
{session.description && (
<CardDescription className="mt-2">{session.description}</CardDescription>
)}
</div>
<Badge variant={getStatusBadgeVariant(session.status)} className="text-lg px-3 py-1">
{session.status}
</Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="font-semibold text-sm text-gray-600 mb-1">Test Type</h3>
<p className="capitalize">{session.test_type.replace('_', ' ')}</p>
</div>
<div>
<h3 className="font-semibold text-sm text-gray-600 mb-1">Created</h3>
<p>{format(new Date(session.created_at), 'MMM d, yyyy h:mm a')}</p>
</div>
<div>
<h3 className="font-semibold text-sm text-gray-600 mb-1">Actor Workflow</h3>
<p>{session.actor_workflow_name}</p>
</div>
<div>
<h3 className="font-semibold text-sm text-gray-600 mb-1">Adversary Workflow</h3>
<p>{session.adversary_workflow_name}</p>
</div>
{session.test_metadata?.concurrent_pairs && (
<div>
<h3 className="font-semibold text-sm text-gray-600 mb-1">Concurrent Pairs</h3>
<p>{session.test_metadata.concurrent_pairs}</p>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View file

@ -1,24 +0,0 @@
export interface TestSession {
id: number;
name: string;
description?: string;
test_type: string;
status: string;
actor_workflow_name: string;
adversary_workflow_name: string;
created_at: string;
updated_at: string;
test_metadata?: {
concurrent_pairs?: number;
[key: string]: unknown;
};
}
export interface Conversation {
id: number;
test_session_id: number;
conversation_pair_id?: string;
status: string;
created_at: string;
updated_at: string;
}