mirror of
http://88.130.71.182:3000/BlitTech/contexta_mb.git
synced 2026-06-12 23:23:22 +00:00
Initial commit
This commit is contained in:
237
src/screens/chatbots/tabs/TestingTab.tsx
Normal file
237
src/screens/chatbots/tabs/TestingTab.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { COLORS, FONT_SIZE, SPACING, RADIUS, TEXT, SHADOWS } from '../../../theme';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { chatAPI } from '../../../services/api';
|
||||
|
||||
interface TestResult {
|
||||
question: string;
|
||||
response: string;
|
||||
confidence_score: number;
|
||||
sources: { document_name: string; chunk_text: string; score: number }[];
|
||||
model_used: string;
|
||||
}
|
||||
|
||||
export function TestingTab({ chatbotId }: { chatbotId: string }) {
|
||||
const { theme } = useTheme();
|
||||
const [questions, setQuestions] = useState<string[]>(['']);
|
||||
const [results, setResults] = useState<TestResult[]>([]);
|
||||
const [expandedIdx, setExpandedIdx] = useState<number | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const addQuestion = () => {
|
||||
if (questions.length < 10) setQuestions(prev => [...prev, '']);
|
||||
};
|
||||
|
||||
const updateQuestion = (idx: number, val: string) => {
|
||||
setQuestions(prev => prev.map((q, i) => (i === idx ? val : q)));
|
||||
};
|
||||
|
||||
const removeQuestion = (idx: number) => {
|
||||
setQuestions(prev => prev.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
const runTests = async () => {
|
||||
const valid = questions.map(q => q.trim()).filter(Boolean);
|
||||
if (!valid.length) return;
|
||||
setRunning(true);
|
||||
setError('');
|
||||
setResults([]);
|
||||
setExpandedIdx(null);
|
||||
try {
|
||||
const data = await chatAPI.test(chatbotId, valid);
|
||||
setResults(data);
|
||||
setExpandedIdx(0);
|
||||
} catch (e: any) {
|
||||
setError(e?.response?.data?.detail ?? 'Test failed. Please try again.');
|
||||
} finally {
|
||||
setRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confidenceColor = (score: number) => {
|
||||
if (score >= 0.7) return COLORS.success;
|
||||
if (score >= 0.4) return COLORS.warning;
|
||||
return COLORS.error;
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.scroll}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
showsVerticalScrollIndicator={false}>
|
||||
|
||||
<View style={[styles.card, { backgroundColor: theme.surface, borderColor: theme.border }, SHADOWS.sm]}>
|
||||
<Text style={[styles.cardTitle, { color: theme.text }]}>Test Questions</Text>
|
||||
<Text style={[styles.cardDesc, { color: theme.textSecondary }]}>
|
||||
Enter up to 10 questions to test how your chatbot responds.
|
||||
</Text>
|
||||
|
||||
{questions.map((q, idx) => (
|
||||
<View key={idx} style={styles.questionRow}>
|
||||
<Text style={[styles.questionNum, { color: theme.textMuted }]}>{idx + 1}.</Text>
|
||||
<TextInput
|
||||
style={[styles.questionInput, { backgroundColor: theme.inputBg, borderColor: theme.border, color: theme.text }]}
|
||||
value={q}
|
||||
onChangeText={val => updateQuestion(idx, val)}
|
||||
placeholder="Ask a question..."
|
||||
placeholderTextColor={theme.placeholder}
|
||||
/>
|
||||
{questions.length > 1 && (
|
||||
<TouchableOpacity onPress={() => removeQuestion(idx)} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||
<Text style={styles.removeBtn}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
|
||||
<View style={styles.actions}>
|
||||
{questions.length < 10 && (
|
||||
<TouchableOpacity onPress={addQuestion}>
|
||||
<Text style={[styles.addBtn, { color: COLORS.primary }]}>+ Add question</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.runBtn,
|
||||
{ backgroundColor: COLORS.primary },
|
||||
(running || !questions.some(q => q.trim())) && styles.runBtnDisabled,
|
||||
]}
|
||||
onPress={runTests}
|
||||
disabled={running || !questions.some(q => q.trim())}>
|
||||
{running
|
||||
? <ActivityIndicator size="small" color="#fff" />
|
||||
: <Text style={styles.runBtnText}>▶ Run Tests</Text>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{error ? (
|
||||
<Text style={[styles.error, { color: COLORS.error }]}>{error}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{results.length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.resultsLabel, { color: theme.textMuted }]}>
|
||||
{results.length} RESULT{results.length !== 1 ? 'S' : ''}
|
||||
</Text>
|
||||
{results.map((r, idx) => (
|
||||
<View key={idx} style={[styles.resultCard, { backgroundColor: theme.surface, borderColor: theme.border }, SHADOWS.sm]}>
|
||||
<TouchableOpacity
|
||||
style={styles.resultHeader}
|
||||
onPress={() => setExpandedIdx(expandedIdx === idx ? null : idx)}>
|
||||
<View style={[styles.confBadge, { backgroundColor: confidenceColor(r.confidence_score) + '22' }]}>
|
||||
<Text style={[styles.confText, { color: confidenceColor(r.confidence_score) }]}>
|
||||
{Math.round(r.confidence_score * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[styles.resultQuestion, { color: theme.text }]} numberOfLines={1}>{r.question}</Text>
|
||||
<Text style={[styles.chevron, { color: theme.textMuted }]}>{expandedIdx === idx ? '▲' : '▼'}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{expandedIdx === idx && (
|
||||
<View style={[styles.resultBody, { borderTopColor: theme.border }]}>
|
||||
<Text style={[styles.responseText, { color: theme.text }]}>{r.response}</Text>
|
||||
|
||||
{r.sources.length > 0 && (
|
||||
<>
|
||||
<Text style={[styles.sourcesLabel, { color: theme.textMuted }]}>SOURCES</Text>
|
||||
{r.sources.map((src, si) => (
|
||||
<View key={si} style={[styles.sourceChip, { backgroundColor: theme.bgSecondary, borderColor: theme.border }]}>
|
||||
<Text style={[styles.sourceName, { color: theme.textSecondary }]}>
|
||||
{src.document_name}
|
||||
<Text style={{ color: theme.textMuted }}> · {Math.round(src.score * 100)}%</Text>
|
||||
</Text>
|
||||
<Text style={[styles.sourceChunk, { color: theme.textMuted }]} numberOfLines={2}>{src.chunk_text}</Text>
|
||||
</View>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text style={[styles.modelLabel, { color: theme.textMuted }]}>Model: {r.model_used}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scroll: { flex: 1 },
|
||||
scrollContent: { padding: SPACING.lg, gap: SPACING.lg, paddingBottom: SPACING.xxxl },
|
||||
|
||||
card: {
|
||||
borderRadius: RADIUS.xl,
|
||||
borderWidth: 1,
|
||||
padding: SPACING.lg,
|
||||
gap: SPACING.md,
|
||||
},
|
||||
cardTitle: { ...TEXT.h4 },
|
||||
cardDesc: { ...TEXT.small },
|
||||
|
||||
questionRow: { flexDirection: 'row', alignItems: 'center', gap: SPACING.sm },
|
||||
questionNum: { width: 18, fontSize: FONT_SIZE.sm, textAlign: 'right' },
|
||||
questionInput: {
|
||||
flex: 1,
|
||||
borderWidth: 1.5,
|
||||
borderRadius: RADIUS.md,
|
||||
paddingHorizontal: SPACING.md,
|
||||
paddingVertical: SPACING.sm,
|
||||
fontSize: FONT_SIZE.md,
|
||||
},
|
||||
removeBtn: { color: '#ccc', fontSize: FONT_SIZE.md, paddingHorizontal: SPACING.xs },
|
||||
|
||||
actions: { flexDirection: 'row', alignItems: 'center', marginTop: SPACING.xs },
|
||||
addBtn: { fontSize: FONT_SIZE.sm, fontWeight: '600' },
|
||||
runBtn: {
|
||||
marginLeft: 'auto',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: SPACING.xs,
|
||||
borderRadius: RADIUS.md,
|
||||
paddingVertical: SPACING.sm,
|
||||
paddingHorizontal: SPACING.lg,
|
||||
minWidth: 100,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
runBtnDisabled: { opacity: 0.5 },
|
||||
runBtnText: { color: COLORS.white, fontSize: FONT_SIZE.sm, fontWeight: '700' },
|
||||
error: { fontSize: FONT_SIZE.sm },
|
||||
|
||||
resultsLabel: { fontSize: FONT_SIZE.xs, fontWeight: '700', letterSpacing: 0.8 },
|
||||
|
||||
resultCard: { borderRadius: RADIUS.xl, borderWidth: 1, overflow: 'hidden' },
|
||||
resultHeader: { flexDirection: 'row', alignItems: 'center', padding: SPACING.md, gap: SPACING.sm },
|
||||
confBadge: { borderRadius: RADIUS.sm, paddingVertical: 3, paddingHorizontal: SPACING.sm },
|
||||
confText: { fontSize: FONT_SIZE.xs, fontWeight: '800' },
|
||||
resultQuestion: { flex: 1, ...TEXT.smallM },
|
||||
chevron: { fontSize: FONT_SIZE.sm },
|
||||
|
||||
resultBody: { borderTopWidth: 1, padding: SPACING.md, gap: SPACING.sm },
|
||||
responseText: { ...TEXT.body, lineHeight: 22 },
|
||||
|
||||
sourcesLabel: { fontSize: FONT_SIZE.xs, fontWeight: '700', letterSpacing: 0.8, marginTop: SPACING.xs },
|
||||
sourceChip: {
|
||||
borderRadius: RADIUS.md,
|
||||
borderWidth: 1,
|
||||
padding: SPACING.sm,
|
||||
gap: 3,
|
||||
},
|
||||
sourceName: { fontSize: FONT_SIZE.xs, fontWeight: '600' },
|
||||
sourceChunk: { fontSize: FONT_SIZE.xs, lineHeight: 16 },
|
||||
modelLabel: { fontSize: FONT_SIZE.xs, marginTop: SPACING.xs },
|
||||
});
|
||||
Reference in New Issue
Block a user