import { useState, useEffect, useRef } from 'react' import { Sparkles, ArrowRight, ArrowLeft, Loader } from 'lucide-react' import { useI18n, LANGUAGES } from '../i18n' import { getLayoutList } from '../i18n/keyboards' const STEPS = [ { key: 'welcome', title: 'welcome' }, { key: 'name', title: 'name' }, { key: 'language', title: 'language' }, { key: 'keyboard', title: 'keyboard' }, { key: 'apikey', title: 'apikey' }, { key: 'editor', title: 'editor' }, { key: 'done', title: 'done' }, ] const BASE_EDITORS = ['vim', 'nvim', 'vscode', 'emacs', 'nano', 'helix'] export default function OnboardingWizard({ api, onComplete }) { const { t, language, keyboard, setLanguage, setKeyboard } = useI18n() const [step, setStep] = useState(0) const [answers, setAnswers] = useState({ name: '', language: 'fr', keyboard: 'azerty', apikey: '', editor: '', }) const [editorList, setEditorList] = useState(BASE_EDITORS) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [requiredError, setRequiredError] = useState(false) const [validating, setValidating] = useState(false) const [keyValid, setKeyValid] = useState(false) const [scanning, setScanning] = useState(false) const [scanMessage, setScanMessage] = useState('') const scanAbortRef = useRef(null) const current = STEPS[step] const layouts = getLayoutList() const goNext = () => { if (step < STEPS.length - 1) { if (!canProceed) { setRequiredError(true); return } setRequiredError(false) setStep(step + 1) } } const canProceed = (() => { switch (current.key) { case 'welcome': return true case 'name': return answers.name.trim().length > 0 case 'language': return !!answers.language case 'keyboard': return !!answers.keyboard case 'apikey': return keyValid && !scanning case 'editor': return true case 'done': return true default: return true } })() const goPrev = () => { if (step > 0) setStep(step - 1) } const cycleOption = (key, list, dir) => { const idx = list.findIndex(item => item.id === answers[key]) const next = (idx + dir + list.length) % list.length setAnswers(a => ({ ...a, [key]: list[next].id })) } const cycleOptionEditor = (dir) => { const idx = editorList.findIndex(ed => ed === answers.editor) const next = (idx + dir + editorList.length) % editorList.length setAnswers(a => ({ ...a, editor: editorList[next] })) } const handleScanViaChat = async (apikey) => { setScanning(true) setScanMessage('Recherche des éditeurs sur votre système...') setError(null) try { const detected = [] const fallback = async () => { setScanMessage('Utilisation du scan local...') const data = await api.getEditors() return (data.editors || []).map(e => e.name) } const prompt = 'Liste tous les éditeurs de texte et IDE installés sur ce système. Exécute les commandes nécessaires pour les détecter (which, command -v, etc.). Réponds UNIQUEMENT avec les noms séparés par des virgules, sans aucune autre explication. Exemples: vim, nvim, code, emacs, nano, helix, subl, zed' const ctrl = new AbortController() scanAbortRef.current = ctrl const full = await api.sendChat(prompt, true, (text, data) => { if (data.tool_call) setScanMessage('Exécution: ' + (data.tool_call.name || '...')) else if (data.tool_result) setScanMessage('Analyse des résultats...') else if (data.content) setScanMessage('Réception: ' + text.slice(0, 60) + (text.length > 60 ? '...' : '')) }, ctrl.signal) const names = full.split(/[,\n]/).map(s => s.replace(/[^a-zA-Z0-9._-]/g, '')).filter(Boolean) if (names.length > 0) { detected.push(...names) } else { detected.push(...(await fallback())) } setEditorList([...new Set(detected.map(n => n.toLowerCase()))]) setScanMessage('') } catch (err) { try { setScanMessage('Fallback: scan local...') const data = await api.getEditors() const detected = (data.editors || []).map(e => e.name) setEditorList([...new Set(detected)]) } catch {} setScanMessage('') } setScanning(false) } useEffect(() => { const handler = (e) => { if (e.key === 'Escape') { goPrev(); return } if (current.key === 'language') { if (e.key === 'Tab' || e.key === 'ArrowRight') { e.preventDefault(); cycleOption('language', LANGUAGES, 1); return } if (e.key === 'ArrowLeft') { e.preventDefault(); cycleOption('language', LANGUAGES, -1); return } } if (current.key === 'keyboard') { if (e.key === 'Tab' || e.key === 'ArrowRight') { e.preventDefault(); cycleOption('keyboard', layouts, 1); return } if (e.key === 'ArrowLeft') { e.preventDefault(); cycleOption('keyboard', layouts, -1); return } } if (current.key === 'editor') { if (e.key === 'Tab' || e.key === 'ArrowRight') { e.preventDefault(); cycleOptionEditor(1); return } if (e.key === 'ArrowLeft') { e.preventDefault(); cycleOptionEditor(-1); return } } if (e.key === 'Tab') { e.preventDefault(); const input = document.querySelector('.onboarding-input'); if (input) input.focus(); return } if (e.key === 'Enter' && current.key !== 'done' && current.key !== 'editor') { e.preventDefault(); goNext() } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, [step, current, answers, editorList]) useEffect(() => { return () => { if (scanAbortRef.current) scanAbortRef.current.abort() } }, []) useEffect(() => { if (current.key === 'done' && !saving) { handleSave() } }, [step]) const handleValidateKey = async () => { if (!answers.apikey.trim()) return setValidating(true) setError(null) try { await api.validateProvider({ name: 'minimax', api_key: answers.apikey, model: 'MiniMax-M2.7', base_url: 'https://api.minimax.io/v1', }) setKeyValid(true) await api.saveProvider({ name: 'minimax', api_key: answers.apikey, model: 'MiniMax-M2.7', base_url: 'https://api.minimax.io/v1', active: true, }) handleScanViaChat(answers.apikey) } catch (err) { setError(err.message || 'Clé invalide') setKeyValid(false) } setValidating(false) } const handleSave = async () => { setSaving(true) setError(null) try { const profile = { name: answers.name, pseudo: answers.name.split(' ')[0] || 'user', editor: answers.editor, } if (answers.apikey.trim()) { profile.apikey = answers.apikey } await api.saveProfile(profile) await api.savePreferences({ language: answers.language, keyboard_layout: answers.keyboard, }) if (answers.apikey.trim()) { await api.saveProvider({ name: 'minimax', api_key: answers.apikey, model: 'MiniMax-M2.7', base_url: 'https://api.minimax.io/v1', active: true, }) } onComplete() } catch (err) { setError(err.message || 'Erreur lors de la sauvegarde') setSaving(false) } } return (