Files
MuyueWorkspace/web/src/components/Tests.jsx
Muyue a7d4b31a0d
All checks were successful
PR Check / check (pull_request) Successful in 55s
feat(studio): force advanced reflection during browser-test sessions (v0.7.2)
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.
2026-04-27 12:02:04 +02:00

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'
}
}