Initial commit

This commit is contained in:
belviskhoremk
2026-05-08 13:01:47 +00:00
parent 864bbd389e
commit 9e663bdc8b
64 changed files with 20910 additions and 74 deletions

View 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 },
});