- {details.agent.description && (
-
diff --git a/frontend/src/pages/evaluators/evaluators/index.ts b/frontend/src/pages/evaluators/evaluators/index.ts
new file mode 100644
index 00000000..26f11e34
--- /dev/null
+++ b/frontend/src/pages/evaluators/evaluators/index.ts
@@ -0,0 +1,2 @@
+export { default as EvaluateTestAgents } from './EvaluateTestAgents'
+export { default as EvaluatorDetail } from './EvaluatorDetail'
diff --git a/frontend/src/pages/evaluators/index.ts b/frontend/src/pages/evaluators/index.ts
new file mode 100644
index 00000000..36d0ac10
--- /dev/null
+++ b/frontend/src/pages/evaluators/index.ts
@@ -0,0 +1,2 @@
+export * from './evaluators'
+export * from './results'
diff --git a/frontend/src/pages/EvaluationDetail.tsx b/frontend/src/pages/evaluators/results/EvaluationDetail.tsx
similarity index 96%
rename from frontend/src/pages/EvaluationDetail.tsx
rename to frontend/src/pages/evaluators/results/EvaluationDetail.tsx
index 4433e11e..c45a97cb 100644
--- a/frontend/src/pages/EvaluationDetail.tsx
+++ b/frontend/src/pages/evaluators/results/EvaluationDetail.tsx
@@ -1,7 +1,7 @@
import { useParams, Link } from 'react-router-dom'
import { useQuery } from '@tanstack/react-query'
-import { apiClient } from '../lib/api'
-import { EvaluationStatus } from '../types/api'
+import { apiClient } from '../../../lib/api'
+import { EvaluationStatus } from '../../../types/api'
import {
ArrowLeft,
Loader,
diff --git a/frontend/src/pages/EvaluatorResultDetail.tsx b/frontend/src/pages/evaluators/results/EvaluatorResultDetail.tsx
similarity index 99%
rename from frontend/src/pages/EvaluatorResultDetail.tsx
rename to frontend/src/pages/evaluators/results/EvaluatorResultDetail.tsx
index 801afa95..5a1c1183 100644
--- a/frontend/src/pages/EvaluatorResultDetail.tsx
+++ b/frontend/src/pages/evaluators/results/EvaluatorResultDetail.tsx
@@ -1,14 +1,14 @@
import React, { useState, useRef, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useParams, useNavigate } from 'react-router-dom'
-import { apiClient } from '../lib/api'
+import { apiClient } from '../../../lib/api'
import { ArrowLeft, Clock, CheckCircle, XCircle, Loader, BarChart3, Phone, Brain, HelpCircle, Sparkles, AudioWaveform, MessageSquare, Download, RotateCcw, PhoneIncoming, PhoneOutgoing, Tag, ExternalLink } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
-import Button from '../components/Button'
-import RetellCallDetails from '../components/call-recordings/RetellCallDetails'
-import VapiCallDetails from '../components/call-recordings/VapiCallDetails'
-import ElevenLabsCallDetails from '../components/call-recordings/ElevenLabsCallDetails'
-import { useToast } from '../hooks/useToast'
+import Button from '../../../components/Button'
+import RetellCallDetails from '../../../components/call-recordings/RetellCallDetails'
+import VapiCallDetails from '../../../components/call-recordings/VapiCallDetails'
+import ElevenLabsCallDetails from '../../../components/call-recordings/ElevenLabsCallDetails'
+import { useToast } from '../../../hooks/useToast'
// Comprehensive metric information with descriptions and ideal values
const METRIC_INFO: Record
Professionalism
-
- Clarity
- |
-
- Empathy
- |
Problem Resolution
|
@@ -256,24 +248,6 @@ function ConversationEvaluationSection() {
-
)}
-
- {metrics.clarity !== undefined ? (
-
- {(metrics.clarity * 100).toFixed(0)}%
-
- ) : (
- -
- )}
- |
-
- {metrics.empathy !== undefined ? (
-
- {(metrics.empathy * 100).toFixed(0)}%
-
- ) : (
- -
- )}
- |
{metrics.problem_resolution !== undefined ? (
diff --git a/frontend/src/pages/MetricsManagement.tsx b/frontend/src/pages/metrics/MetricsManagement.tsx
similarity index 63%
rename from frontend/src/pages/MetricsManagement.tsx
rename to frontend/src/pages/metrics/MetricsManagement.tsx
index 6c13b4aa..43fb82c0 100644
--- a/frontend/src/pages/MetricsManagement.tsx
+++ b/frontend/src/pages/metrics/MetricsManagement.tsx
@@ -1,9 +1,9 @@
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useMemo } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
-import { apiClient } from '../lib/api'
-import Button from '../components/Button'
-import { useToast } from '../hooks/useToast'
-import { Plus, Edit, Trash2, X, ToggleLeft, ToggleRight, Brain, RefreshCw, AudioWaveform, Sparkles } from 'lucide-react'
+import { apiClient } from '../../lib/api'
+import Button from '../../components/Button'
+import { useToast } from '../../hooks/useToast'
+import { Edit, Trash2, X, ToggleLeft, ToggleRight, Brain, RefreshCw, AudioWaveform, Sparkles, Plus } from 'lucide-react'
interface Metric {
id: string
@@ -51,7 +51,11 @@ export default function MetricsManagement() {
const queryClient = useQueryClient()
const { showToast, ToastContainer } = useToast()
const [showCreateModal, setShowCreateModal] = useState(false)
+ const [showEnableModal, setShowEnableModal] = useState(false)
const [editingMetric, setEditingMetric] = useState (null)
+ const [sortField, setSortField] = useState<'type' | 'method'>('type')
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc')
+ const [selectedDisabledMetricIds, setSelectedDisabledMetricIds] = useState>(new Set())
const [formData, setFormData] = useState({
name: '',
description: '',
@@ -121,6 +125,21 @@ export default function MetricsManagement() {
},
})
+ const enableMetricsMutation = useMutation({
+ mutationFn: async (metricIds: string[]) =>
+ Promise.all(metricIds.map((metricId) => apiClient.updateMetric(metricId, { enabled: true }))),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['metrics'] })
+ const count = selectedDisabledMetricIds.size
+ setSelectedDisabledMetricIds(new Set())
+ setShowEnableModal(false)
+ showToast(`Enabled ${count} metric${count > 1 ? 's' : ''}`, 'success')
+ },
+ onError: () => {
+ showToast('Failed to enable selected metrics', 'error')
+ },
+ })
+
const resetForm = () => {
setFormData({
name: '',
@@ -183,6 +202,57 @@ export default function MetricsManagement() {
resetForm()
}
+ const handleSort = (field: 'type' | 'method') => {
+ if (sortField === field) {
+ setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'))
+ return
+ }
+ setSortField(field)
+ setSortDirection('asc')
+ }
+
+ const disabledMetrics = useMemo(
+ () => metrics.filter((metric: Metric) => !metric.enabled),
+ [metrics]
+ )
+
+ const toggleDisabledMetricSelection = (metricId: string) => {
+ setSelectedDisabledMetricIds((prev) => {
+ const next = new Set(prev)
+ if (next.has(metricId)) {
+ next.delete(metricId)
+ } else {
+ next.add(metricId)
+ }
+ return next
+ })
+ }
+
+ const handleEnableSelectedMetrics = () => {
+ if (selectedDisabledMetricIds.size === 0) return
+ enableMetricsMutation.mutate(Array.from(selectedDisabledMetricIds))
+ }
+
+ const sortedEnabledMetrics = useMemo(() => {
+ const getTypeLabel = (metric: Metric) => (isQuantitativeMetric(metric.name) ? 'quantitative' : 'qualitative')
+ const getMethodLabel = (metric: Metric) => (isAIVoiceMetric(metric.name) ? 'ai voice' : isAudioMetric(metric.name) ? 'acoustic' : 'llm')
+
+ return metrics
+ .filter((metric: Metric) => metric.enabled)
+ .sort((a: Metric, b: Metric) => {
+ const aValue = sortField === 'type' ? getTypeLabel(a) : getMethodLabel(a)
+ const bValue = sortField === 'type' ? getTypeLabel(b) : getMethodLabel(b)
+
+ const baseCompare = aValue.localeCompare(bValue)
+ if (baseCompare !== 0) {
+ return sortDirection === 'asc' ? baseCompare : -baseCompare
+ }
+
+ // Stable secondary sort for predictable ordering within groups.
+ return a.name.localeCompare(b.name)
+ })
+ }, [metrics, sortField, sortDirection])
+
return (
@@ -192,8 +262,22 @@ export default function MetricsManagement() {
Manage evaluation metrics for your conversations
+
+ Acoustic defaults: only Pitch Variance is enabled; Jitter, Shimmer, and HNR start disabled.
+
+
-
@@ -223,9 +296,12 @@ export default function MetricsManagement() {
{isLoading ? (
Loading...
- ) : metrics.length === 0 ? (
+ ) : sortedEnabledMetrics.length === 0 ? (
- No metrics yet. Default metrics will be created automatically.
+ No enabled metrics.
+
+ Use Add Metric above to enable one from the disabled list.
+
) : (
@@ -239,13 +315,31 @@ export default function MetricsManagement() {
Description
- Type
+
|
Data Type
|
- Method
+
|
Enabled
@@ -256,7 +350,7 @@ export default function MetricsManagement() {
|
- {metrics.map((metric: Metric) => {
+ {sortedEnabledMetrics.map((metric: Metric) => {
const isAudio = isAudioMetric(metric.name)
const isQuantitative = isQuantitativeMetric(metric.name)
return (
@@ -462,6 +556,112 @@ export default function MetricsManagement() {
)}
+
+ {/* Enable Metrics Modal */}
+ {showEnableModal && (
+
+
+ {
+ setShowEnableModal(false)
+ setSelectedDisabledMetricIds(new Set())
+ }}
+ />
+
+
+ Add Metrics
+
+
+
+ Choose from disabled metrics below to enable them. Each metric includes its purpose and evaluation method.
+
+
+ {disabledMetrics.length === 0 ? (
+
+ All metrics are currently enabled.
+
+ ) : (
+
+ {disabledMetrics.map((metric: Metric) => {
+ const isAudio = isAudioMetric(metric.name)
+ const isQuantitative = isQuantitativeMetric(metric.name)
+ const isSelected = selectedDisabledMetricIds.has(metric.id)
+
+ return (
+
+ )
+ })}
+
+ )}
+
+
+
+
+
+
+
+
+ )}
)
}
diff --git a/frontend/src/pages/metrics/index.ts b/frontend/src/pages/metrics/index.ts
new file mode 100644
index 00000000..2495edfb
--- /dev/null
+++ b/frontend/src/pages/metrics/index.ts
@@ -0,0 +1,2 @@
+export { default as Metrics } from './Metrics'
+export { default as MetricsManagement } from './MetricsManagement'
diff --git a/frontend/src/pages/Observability.tsx b/frontend/src/pages/observability/Observability.tsx
similarity index 97%
rename from frontend/src/pages/Observability.tsx
rename to frontend/src/pages/observability/Observability.tsx
index 31b13cc8..c7350d16 100644
--- a/frontend/src/pages/Observability.tsx
+++ b/frontend/src/pages/observability/Observability.tsx
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
-import { apiClient } from '../lib/api'
+import { apiClient } from '../../lib/api'
import { useState, useMemo } from 'react'
import { motion } from 'framer-motion'
import {
diff --git a/frontend/src/pages/ObservabilityCallDetail.tsx b/frontend/src/pages/observability/ObservabilityCallDetail.tsx
similarity index 96%
rename from frontend/src/pages/ObservabilityCallDetail.tsx
rename to frontend/src/pages/observability/ObservabilityCallDetail.tsx
index 00bca84a..7dbff76a 100644
--- a/frontend/src/pages/ObservabilityCallDetail.tsx
+++ b/frontend/src/pages/observability/ObservabilityCallDetail.tsx
@@ -8,11 +8,11 @@ import {
} from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
-import Button from '../components/Button'
-import ConfirmModal from '../components/ConfirmModal'
-import { apiClient } from '../lib/api'
-import RetellCallDetails from '../components/call-recordings/RetellCallDetails'
-import VapiCallDetails from '../components/call-recordings/VapiCallDetails'
+import Button from '../../components/Button'
+import ConfirmModal from '../../components/ConfirmModal'
+import { apiClient } from '../../lib/api'
+import RetellCallDetails from '../../components/call-recordings/RetellCallDetails'
+import VapiCallDetails from '../../components/call-recordings/VapiCallDetails'
export default function ObservabilityCallDetail() {
const navigate = useNavigate()
diff --git a/frontend/src/pages/ObservabilityCalls.tsx b/frontend/src/pages/observability/ObservabilityCalls.tsx
similarity index 96%
rename from frontend/src/pages/ObservabilityCalls.tsx
rename to frontend/src/pages/observability/ObservabilityCalls.tsx
index 510d6063..232d208a 100644
--- a/frontend/src/pages/ObservabilityCalls.tsx
+++ b/frontend/src/pages/observability/ObservabilityCalls.tsx
@@ -7,9 +7,9 @@ import {
} from 'lucide-react'
import { motion } from 'framer-motion'
-import Button from '../components/Button'
-import ConfirmModal from '../components/ConfirmModal'
-import { apiClient } from '../lib/api'
+import Button from '../../components/Button'
+import ConfirmModal from '../../components/ConfirmModal'
+import { apiClient } from '../../lib/api'
export default function ObservabilityCalls() {
const navigate = useNavigate()
diff --git a/frontend/src/pages/observability/index.ts b/frontend/src/pages/observability/index.ts
new file mode 100644
index 00000000..34e117dd
--- /dev/null
+++ b/frontend/src/pages/observability/index.ts
@@ -0,0 +1,3 @@
+export { default as Observability } from './Observability'
+export { default as ObservabilityCalls } from './ObservabilityCalls'
+export { default as ObservabilityCallDetail } from './ObservabilityCallDetail'
diff --git a/frontend/src/pages/Personas.tsx b/frontend/src/pages/personas/Personas.tsx
similarity index 66%
rename from frontend/src/pages/Personas.tsx
rename to frontend/src/pages/personas/Personas.tsx
index 1d3adf58..9666dd13 100644
--- a/frontend/src/pages/Personas.tsx
+++ b/frontend/src/pages/personas/Personas.tsx
@@ -1,5 +1,6 @@
-import { useState, useMemo } from 'react'
+import { useState, useMemo, type ReactNode } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { createPortal } from 'react-dom'
import {
Users,
Plus,
@@ -8,7 +9,6 @@ import {
Copy,
X,
Loader2,
- Sparkles,
UserPlus,
Languages,
Globe,
@@ -23,9 +23,9 @@ import {
} from 'lucide-react'
// Use emoji flags with better styling - most reliable solution
// Emoji flags work everywhere and don't require additional dependencies
-import { apiClient } from '../lib/api'
-import { useToast } from '../hooks/useToast'
-import Button from '../components/Button'
+import { apiClient } from '../../lib/api'
+import { useToast } from '../../hooks/useToast'
+import Button from '../../components/Button'
interface Persona {
id: string
@@ -39,74 +39,6 @@ interface Persona {
created_by?: string | null
}
-// Known default persona names from seed data
-const DEFAULT_PERSONA_NAMES = [
- 'Grumpy Old Man',
- 'Confused Senior',
- 'Busy Professional',
- 'Friendly Customer',
- 'Angry Caller',
-]
-
-// Default persona templates with images and descriptions
-interface DefaultPersonaTemplate {
- name: string
- image: string
- description: string
- language: string
- accent: string
- gender: string
- background_noise: string
-}
-
-const DEFAULT_PERSONA_TEMPLATES: DefaultPersonaTemplate[] = [
- {
- name: 'Angry Caller',
- image: '/angry_customer_male.png',
- description: 'A frustrated male customer with an American accent, often calling from noisy street environments',
- language: 'en',
- accent: 'american',
- gender: 'male',
- background_noise: 'street',
- },
- {
- name: 'Confused Senior',
- image: '/confused_senior_american.png',
- description: 'An elderly person who may need extra patience and clear explanations, typically calling from home',
- language: 'en',
- accent: 'american',
- gender: 'female',
- background_noise: 'home',
- },
- {
- name: 'Friendly Customer',
- image: '/friendly_customer_female.png',
- description: 'A cheerful and cooperative female customer with a pleasant American accent',
- language: 'en',
- accent: 'american',
- gender: 'female',
- background_noise: 'none',
- },
- {
- name: 'Busy Professional',
- image: '/busy_professional_american.png',
- description: 'A time-conscious professional calling from a busy office environment',
- language: 'en',
- accent: 'american',
- gender: 'neutral',
- background_noise: 'office',
- },
- {
- name: 'Grumpy Old Man',
- image: '/grumpy_old_man.png',
- description: 'An older male customer who may be less patient and prefers straightforward communication',
- language: 'en',
- accent: 'american',
- gender: 'male',
- background_noise: 'none',
- },
-]
-
const genderIcons: Record = {
male: '👨',
female: '👩',
@@ -199,16 +131,12 @@ const noiseConfig: Record(null)
const [showEditModal, setShowEditModal] = useState(false)
const [showCloneModal, setShowCloneModal] = useState(false)
- const [showDetailsModal, setShowDetailsModal] = useState(false)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [selectedPersona, setSelectedPersona] = useState(null)
const [deleteDependencies, setDeleteDependencies] = useState | null>(null)
@@ -220,6 +148,11 @@ export default function Personas() {
background_noise: 'none',
})
+ const renderModal = (content: ReactNode) => {
+ if (typeof document === 'undefined') return null
+ return createPortal(content, document.body)
+ }
+
// Fetch personas
const { data: personas = [], isLoading, error, isError } = useQuery({
@@ -228,35 +161,7 @@ export default function Personas() {
retry: 1,
})
- // Separate personas into default and user-created
- const { defaultPersonas, userPersonas } = useMemo(() => {
- const defaults: Persona[] = []
- const userCreated: Persona[] = []
-
- // Track which default persona IDs we've seen to avoid duplicates
- const seenDefaultIds = new Set()
-
- personas.forEach((persona: Persona) => {
- // Check if it's a default persona:
- // 1. Name exactly matches a default persona name
- // 2. created_by is null/empty (seeded personas)
- // 3. Name doesn't contain "(Copy)" or "(Clone)" (cloned personas)
- // 4. We haven't seen this ID before (to handle edge cases)
- const isExactDefaultName = DEFAULT_PERSONA_NAMES.includes(persona.name)
- const isCloned = persona.name.includes('(Copy)') || persona.name.includes('(Clone)')
- const isDefault = isExactDefaultName && !isCloned && !persona.created_by && !seenDefaultIds.has(persona.id)
-
- if (isDefault) {
- seenDefaultIds.add(persona.id)
- defaults.push(persona)
- } else {
- // All cloned personas, custom personas, and modified defaults go to user-created
- userCreated.push(persona)
- }
- })
-
- return { defaultPersonas: defaults, userPersonas: userCreated }
- }, [personas])
+ const userPersonas = useMemo(() => personas as Persona[], [personas])
// Create persona mutation
const createMutation = useMutation({
@@ -328,18 +233,6 @@ export default function Personas() {
},
})
- // Seed demo data mutation
- const seedMutation = useMutation({
- mutationFn: () => apiClient.seedDemoData(),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['personas'] })
- showToast('Demo personas loaded successfully!', 'success')
- },
- onError: (error: any) => {
- showToast(`Failed to load demo data: ${error.response?.data?.detail || error.message}`, 'error')
- },
- })
-
const resetForm = () => {
setFormData({
name: '',
@@ -358,39 +251,10 @@ export default function Personas() {
const handleCloseMainModal = () => {
setShowMainModal(false)
- setCreateMode(null)
resetForm()
setSelectedPersona(null)
}
- const handleSelectDefaultPersona = (persona: Persona) => {
- setSelectedPersona(persona)
- // Pre-populate form with default persona data
- setFormData({
- name: `${persona.name} (Copy)`,
- language: persona.language,
- accent: persona.accent,
- gender: persona.gender,
- background_noise: persona.background_noise,
- })
- // Switch to custom mode to show the full form
- setCreateMode('custom')
- }
-
- const handleSelectDefaultPersonaTemplate = (template: DefaultPersonaTemplate) => {
- // Pre-populate form with template data, adding "(Copy)" to avoid being classified as default
- setFormData({
- name: `${template.name} (Copy)`,
- language: template.language,
- accent: template.accent,
- gender: template.gender,
- background_noise: template.background_noise,
- })
- // Open modal in custom mode to show the full form
- setCreateMode('custom')
- setShowMainModal(true)
- }
-
const openEditModal = (persona: Persona) => {
setSelectedPersona(persona)
setFormData({
@@ -483,15 +347,6 @@ export default function Personas() {
Create and manage personas for testing voice AI agents
- {personas.length === 0 && (
-
- )}
- {/* Default Personas Carousel */}
-
-
- Default Personas
- Click on any persona to create a copy with these settings
-
-
-
-
- {DEFAULT_PERSONA_TEMPLATES.map((template) => (
- handleSelectDefaultPersonaTemplate(template)}
- className="flex-shrink-0 w-64 cursor-pointer group"
- >
-
- {/* Image */}
-
- 
- {/* Overlay gradient for text readability */}
-
-
- {/* Description overlay */}
-
- {template.name}
-
- {template.description}
-
-
-
-
- {/* Info badges */}
-
-
- Gender:
- {template.gender}
-
-
- Accent:
- {template.accent}
-
-
- Noise:
-
- {template.background_noise === 'none' ? 'None' : template.background_noise}
-
-
-
-
- {/* Hover effect indicator */}
-
-
-
- ))}
-
-
-
-
-
{personas.length === 0 ? (
No personas yet
- Create your first persona or load demo personas to get started
-
-
- or
-
-
+ Create your first custom persona to get started
+
) : (
@@ -611,7 +386,7 @@ export default function Personas() {
No custom personas yet
- Create your first persona or clone a default persona to get started
+ Create your first custom persona to get started
|