All checks were successful
PR Check / check (pull_request) Successful in 55s
When at least one browser_test session is connected, every chat message in Studio now auto-enables advanced reflection regardless of the user toggle. The intent: during AI-driven UI testing, having a second model produce a preliminary [RAPPORT PRÉALABLE] materially improves which clicks the active model decides to perform and the quality of the final ✓/✗ report. - handlers_chat: derive wantReflection from body.AdvancedReflection OR (browserTestStore has any active session). The user toggle still works for normal conversations; tests just override it. - Silent fallback when no inactive provider is configured (no error, no behaviour change for single-provider setups). - Tests.jsx: add a hint explaining the auto-on behaviour so the user understands why the Studio toggle appears bypassed. - Version 0.7.1 → 0.7.2 + CHANGELOG entry.
242 lines
11 KiB
JavaScript
242 lines
11 KiB
JavaScript
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
import { TestTube2, Copy, RefreshCw, CheckCircle2, AlertTriangle, Globe, Terminal as TerminalIcon } from 'lucide-react'
|
|
|
|
export default function Tests({ api }) {
|
|
const [snippet, setSnippet] = useState(null)
|
|
const [snippetError, setSnippetError] = useState('')
|
|
const [sessions, setSessions] = useState([])
|
|
const [console_, setConsole_] = useState([])
|
|
const [activeSessionId, setActiveSessionId] = useState('')
|
|
const [copied, setCopied] = useState(false)
|
|
const pollRef = useRef(null)
|
|
|
|
const refreshSnippet = useCallback(async () => {
|
|
try {
|
|
const data = await api.getTestSnippet()
|
|
setSnippet(data)
|
|
setSnippetError('')
|
|
} catch (err) {
|
|
setSnippetError(err.message || 'Failed to load snippet')
|
|
}
|
|
}, [api])
|
|
|
|
const refreshSessions = useCallback(async () => {
|
|
try {
|
|
const data = await api.getTestSessions()
|
|
const next = data.sessions || []
|
|
setSessions(next)
|
|
if (!activeSessionId && next.length > 0) {
|
|
setActiveSessionId(next[0].id)
|
|
} else if (activeSessionId && !next.find(s => s.id === activeSessionId)) {
|
|
setActiveSessionId(next.length > 0 ? next[0].id : '')
|
|
}
|
|
} catch {}
|
|
}, [api, activeSessionId])
|
|
|
|
const refreshConsole = useCallback(async () => {
|
|
if (!activeSessionId) {
|
|
setConsole_([])
|
|
return
|
|
}
|
|
try {
|
|
const data = await api.getTestConsole(activeSessionId)
|
|
setConsole_(data.console || [])
|
|
} catch {
|
|
setConsole_([])
|
|
}
|
|
}, [api, activeSessionId])
|
|
|
|
useEffect(() => {
|
|
refreshSnippet()
|
|
}, [refreshSnippet])
|
|
|
|
useEffect(() => {
|
|
refreshSessions()
|
|
refreshConsole()
|
|
pollRef.current = setInterval(() => {
|
|
refreshSessions()
|
|
refreshConsole()
|
|
}, 2000)
|
|
return () => clearInterval(pollRef.current)
|
|
}, [refreshSessions, refreshConsole])
|
|
|
|
const copySnippet = useCallback(async () => {
|
|
if (!snippet) return
|
|
try {
|
|
await navigator.clipboard.writeText(snippet.snippet)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 1500)
|
|
} catch {}
|
|
}, [snippet])
|
|
|
|
const activeSession = sessions.find(s => s.id === activeSessionId) || null
|
|
|
|
return (
|
|
<div className="tests-layout" style={{ padding: '20px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px', height: '100%', overflow: 'auto' }}>
|
|
<section className="tests-pane">
|
|
<header style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
|
<TestTube2 size={18} />
|
|
<h2 style={{ margin: 0, fontSize: '1.1em' }}>Tests pilotés par l'IA</h2>
|
|
</header>
|
|
|
|
<p style={{ marginTop: 0, opacity: 0.85, lineHeight: 1.5 }}>
|
|
Donnez à l'IA Studio le contrôle d'un onglet de votre navigateur pour tester chaque bouton et détecter les erreurs console.
|
|
</p>
|
|
|
|
<div style={{ borderTop: '1px solid var(--border, rgba(128,128,128,0.3))', paddingTop: 12, marginTop: 12 }}>
|
|
<h3 style={{ fontSize: '0.95em', margin: '0 0 8px' }}>1. Connexion</h3>
|
|
<ol style={{ paddingLeft: 18, lineHeight: 1.6 }}>
|
|
<li>Ouvrez la page à tester dans n'importe quel navigateur (Chrome, Firefox, Edge…).</li>
|
|
<li>Ouvrez la console développeur (<kbd>F12</kbd>).</li>
|
|
<li>Collez ce snippet et appuyez sur <kbd>Entrée</kbd> :</li>
|
|
</ol>
|
|
|
|
{snippetError && (
|
|
<div style={{ background: 'rgba(220,80,80,0.1)', border: '1px solid rgba(220,80,80,0.3)', padding: 8, borderRadius: 4, marginBottom: 8 }}>
|
|
{snippetError}
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ position: 'relative', marginBottom: 12 }}>
|
|
<pre style={{
|
|
background: 'var(--bg-secondary, rgba(0,0,0,0.3))',
|
|
padding: '10px 12px',
|
|
borderRadius: 4,
|
|
fontSize: '0.75em',
|
|
maxHeight: 180,
|
|
overflow: 'auto',
|
|
border: '1px solid var(--border, rgba(128,128,128,0.3))',
|
|
margin: 0,
|
|
}}>
|
|
{snippet?.snippet || 'Chargement…'}
|
|
</pre>
|
|
<button
|
|
onClick={copySnippet}
|
|
disabled={!snippet}
|
|
title="Copier"
|
|
style={{
|
|
position: 'absolute', top: 6, right: 6,
|
|
background: 'var(--bg-tertiary, rgba(255,255,255,0.08))',
|
|
border: '1px solid var(--border, rgba(128,128,128,0.3))',
|
|
color: 'inherit', padding: '4px 8px', borderRadius: 3,
|
|
cursor: 'pointer', fontSize: '0.75em',
|
|
display: 'flex', alignItems: 'center', gap: 4,
|
|
}}
|
|
>
|
|
<Copy size={11} /> {copied ? 'Copié !' : 'Copier'}
|
|
</button>
|
|
</div>
|
|
|
|
<button onClick={refreshSnippet} style={{ background: 'transparent', border: '1px solid var(--border, rgba(128,128,128,0.3))', color: 'inherit', padding: '4px 10px', borderRadius: 3, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: '0.85em' }}>
|
|
<RefreshCw size={12} /> Régénérer le token
|
|
</button>
|
|
<small style={{ display: 'block', opacity: 0.6, marginTop: 4 }}>
|
|
Le token expire après {snippet?.expires_in ? Math.round(snippet.expires_in / 60) : 5} minutes ou dès la première connexion.
|
|
</small>
|
|
</div>
|
|
|
|
<div style={{ borderTop: '1px solid var(--border, rgba(128,128,128,0.3))', paddingTop: 12, marginTop: 16 }}>
|
|
<h3 style={{ fontSize: '0.95em', margin: '0 0 8px' }}>2. Pilotage par l'IA</h3>
|
|
<p style={{ margin: '0 0 8px', lineHeight: 1.5 }}>
|
|
Une fois la session connectée, allez dans l'onglet <strong>Studio</strong> et demandez par exemple :
|
|
</p>
|
|
<pre style={{ background: 'var(--bg-secondary, rgba(0,0,0,0.3))', padding: 8, borderRadius: 4, fontSize: '0.85em', margin: 0 }}>
|
|
{`Teste tous les boutons de cette page,
|
|
clique sur chacun, et dis-moi
|
|
lesquels déclenchent une erreur console.`}
|
|
</pre>
|
|
<p style={{ margin: '8px 0 0', opacity: 0.75, fontSize: '0.85em' }}>
|
|
L'IA dispose de l'outil <code>browser_test</code> avec les actions <code>list_clickables</code>, <code>click</code>, <code>console</code>, <code>eval</code>, <code>type</code>, <code>current_url</code>, <code>wait</code>, <code>summary</code>.
|
|
</p>
|
|
<p style={{ margin: '8px 0 0', padding: 8, fontSize: '0.85em', background: 'var(--accent-bg, rgba(108,92,231,0.1))', border: '1px solid var(--accent, #6c5ce7)', borderRadius: 4 }}>
|
|
<strong>Réflexion avancée auto :</strong> tant qu'au moins une session de test est connectée, chaque message dans Studio utilise automatiquement la réflexion avancée — un second modèle (s'il est configuré) produit un rapport d'analyse préalable injecté dans le prompt actif. Le toggle Studio est ignoré pendant la session.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="tests-pane">
|
|
<header style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12, justifyContent: 'space-between' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<Globe size={16} />
|
|
<h2 style={{ margin: 0, fontSize: '1.1em' }}>Sessions connectées</h2>
|
|
</div>
|
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: '0.85em' }}>
|
|
{sessions.length > 0 ? <CheckCircle2 size={14} color="#3aaa61" /> : <span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: '50%', background: '#888' }} />}
|
|
{sessions.length} session{sessions.length > 1 ? 's' : ''}
|
|
</span>
|
|
</header>
|
|
|
|
{sessions.length === 0 ? (
|
|
<div style={{ padding: 16, textAlign: 'center', opacity: 0.7, border: '1px dashed var(--border, rgba(128,128,128,0.3))', borderRadius: 4 }}>
|
|
<AlertTriangle size={20} style={{ opacity: 0.4 }} />
|
|
<div style={{ marginTop: 6 }}>Aucune session active.</div>
|
|
<small>Collez le snippet dans une page pour démarrer.</small>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
|
|
{sessions.map(s => (
|
|
<button
|
|
key={s.id}
|
|
onClick={() => setActiveSessionId(s.id)}
|
|
style={{
|
|
textAlign: 'left',
|
|
background: s.id === activeSessionId ? 'var(--accent-bg, rgba(108,92,231,0.15))' : 'transparent',
|
|
border: '1px solid ' + (s.id === activeSessionId ? 'var(--accent, #6c5ce7)' : 'var(--border, rgba(128,128,128,0.3))'),
|
|
color: 'inherit',
|
|
padding: 8, borderRadius: 4, cursor: 'pointer',
|
|
}}
|
|
>
|
|
<div style={{ fontWeight: 500, fontSize: '0.9em', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{s.title || s.url || s.id}
|
|
</div>
|
|
<div style={{ fontSize: '0.75em', opacity: 0.65, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{s.url} · session {s.id.slice(0, 8)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{activeSession && (
|
|
<div style={{ borderTop: '1px solid var(--border, rgba(128,128,128,0.3))', paddingTop: 12 }}>
|
|
<header style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 8 }}>
|
|
<TerminalIcon size={14} />
|
|
<h3 style={{ margin: 0, fontSize: '0.95em' }}>Console (live, dernières {console_.length})</h3>
|
|
</header>
|
|
<div style={{
|
|
background: 'var(--bg-secondary, rgba(0,0,0,0.3))',
|
|
padding: 8,
|
|
borderRadius: 4,
|
|
maxHeight: 380,
|
|
overflow: 'auto',
|
|
fontSize: '0.8em',
|
|
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
border: '1px solid var(--border, rgba(128,128,128,0.3))',
|
|
}}>
|
|
{console_.length === 0 ? (
|
|
<div style={{ opacity: 0.5 }}>(aucun message console)</div>
|
|
) : (
|
|
console_.map((c, i) => (
|
|
<div key={i} style={{ color: levelColor(c.level), padding: '2px 0', borderBottom: '1px dashed rgba(128,128,128,0.15)' }}>
|
|
<span style={{ opacity: 0.55, fontSize: '0.85em' }}>[{c.time?.slice(11, 19)} {c.level}]</span> {c.message}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</section>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function levelColor(lvl) {
|
|
switch (lvl) {
|
|
case 'error': return '#ff6b6b'
|
|
case 'warn': return '#f5a623'
|
|
case 'info': return '#4dabf7'
|
|
case 'debug': return '#888'
|
|
default: return 'inherit'
|
|
}
|
|
}
|