mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-07 07:55:16 +02:00
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:
parent
0523dcb079
commit
45b00cd5d0
34 changed files with 214 additions and 4634 deletions
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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'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
|
|
@ -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?: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue