mirror of
http://88.130.71.182:3000/BlitTech/contexta_mb.git
synced 2026-06-12 23:23:22 +00:00
238 lines
8.8 KiB
TypeScript
238 lines
8.8 KiB
TypeScript
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 },
|
|
});
|