Spaces:
Sleeping
Sleeping
| import { writeFileSync, mkdirSync } from 'fs'; | |
| const files = { | |
| // ====== FICHIERS DE BASE ====== | |
| "src/app/globals.css": `@tailwind base; | |
| @tailwind components; | |
| @tailwind utilities; | |
| :root { | |
| --vortex-400: #7be0f7; | |
| --vortex-500: #3b82f6; | |
| --singularity-glow: #f59e0b; | |
| } | |
| body { | |
| background: #0a0a0a; | |
| color: #e5e5e5; | |
| } | |
| .gradient-text { | |
| background: linear-gradient(135deg, var(--vortex-400), var(--singularity-glow)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| }`, | |
| "tailwind.config.js": `/** @type {import('tailwindcss').Config} */ | |
| module.exports = { | |
| content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], | |
| theme: { | |
| extend: { | |
| colors: { | |
| vortex: { 400: '#7be0f7', 500: '#3b82f6', 600: '#2563eb' }, | |
| singularity: { glow: '#f59e0b' }, | |
| }, | |
| }, | |
| }, | |
| plugins: [], | |
| };`, | |
| "tsconfig.json": JSON.stringify({ | |
| compilerOptions: { | |
| target: "ES2017", | |
| lib: ["dom", "dom.iterable", "esnext"], | |
| allowJs: true, | |
| skipLibCheck: true, | |
| strict: false, | |
| noEmit: true, | |
| esModuleInterop: true, | |
| module: "esnext", | |
| moduleResolution: "bundler", | |
| resolveJsonModule: true, | |
| isolatedModules: true, | |
| jsx: "preserve", | |
| incremental: true, | |
| paths: { "@/*": ["./src/*"] } | |
| }, | |
| include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], | |
| exclude: ["node_modules"] | |
| }, null, 2), | |
| // ====== LAYOUT & PAGE ====== | |
| "src/app/layout.tsx": `import type { Metadata } from "next"; | |
| import "./globals.css"; | |
| import { VortexProvider } from "@/context/VortexContext"; | |
| export const metadata: Metadata = { | |
| title: "ATYPIQUE v3.0", | |
| description: "Binôme cognitif", | |
| }; | |
| export default function RootLayout({ children }: { children: React.ReactNode }) { | |
| return ( | |
| <html lang="fr"> | |
| <body> | |
| <VortexProvider>{children}</VortexProvider> | |
| </body> | |
| </html> | |
| ); | |
| }`, | |
| "src/app/page.tsx": `"use client"; | |
| import { Header } from "@/components/Header"; | |
| import { Sidebar } from "@/components/Sidebar"; | |
| import { Chat } from "@/components/Chat"; | |
| export default function Home() { | |
| return ( | |
| <div className="h-screen flex flex-col bg-gray-950 overflow-hidden"> | |
| <Header /> | |
| <div className="flex-1 flex overflow-hidden"> | |
| <Sidebar /> | |
| <main className="flex-1"> | |
| <Chat /> | |
| </main> | |
| </div> | |
| </div> | |
| ); | |
| }`, | |
| // ====== CONTEXT ====== | |
| "src/context/VortexContext.tsx": `"use client"; | |
| import React, { createContext, useContext, useState, useCallback } from "react"; | |
| const API = process.env.NEXT_PUBLIC_API_URL || "https://rds777-atypique-api.hf.space"; | |
| type VortexState = { | |
| progress: number; | |
| activeTab: string; | |
| setActiveTab: (t: string) => void; | |
| sendMessage: (msg: string) => Promise<string>; | |
| }; | |
| const Ctx = createContext<VortexState>({} as VortexState); | |
| export const useVortex = () => useContext(Ctx); | |
| export function VortexProvider({ children }: { children: React.ReactNode }) { | |
| const [progress] = useState(0); | |
| const [activeTab, setActiveTab] = useState("chat"); | |
| const sendMessage = useCallback(async (msg: string) => { | |
| try { | |
| const r = await fetch(API + "/api/chat", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ message: msg }), | |
| }); | |
| const d = await r.json(); | |
| return d.response; | |
| } catch { | |
| return "Erreur de connexion"; | |
| } | |
| }, []); | |
| return ( | |
| <Ctx.Provider value={{ progress, activeTab, setActiveTab, sendMessage }}> | |
| {children} | |
| </Ctx.Provider> | |
| ); | |
| }`, | |
| // ====== COMPOSANTS ====== | |
| "src/components/Header.tsx": `"use client"; | |
| import { motion } from "framer-motion"; | |
| import { Zap } from "lucide-react"; | |
| export function Header() { | |
| return ( | |
| <header className="flex items-center justify-between px-6 py-3 border-b border-gray-800 bg-gray-900/50"> | |
| <motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="flex items-center gap-3"> | |
| <Zap className="w-6 h-6 text-yellow-500" /> | |
| <h1 className="text-xl font-bold gradient-text">ATYPIQUE</h1> | |
| </motion.div> | |
| </header> | |
| ); | |
| }`, | |
| "src/components/Sidebar.tsx": `"use client"; | |
| import { motion } from "framer-motion"; | |
| import { MessageCircle } from "lucide-react"; | |
| import { useVortex } from "@/context/VortexContext"; | |
| export function Sidebar() { | |
| const { activeTab, setActiveTab } = useVortex(); | |
| return ( | |
| <nav className="w-16 flex flex-col items-center py-4 gap-2 border-r border-gray-800"> | |
| <motion.button | |
| whileHover={{ scale: 1.1 }} | |
| whileTap={{ scale: 0.9 }} | |
| onClick={() => setActiveTab("chat")} | |
| className={"p-2 rounded-lg " + (activeTab === "chat" ? "bg-vortex-500/20 text-vortex-400" : "text-gray-500")} | |
| > | |
| <MessageCircle className="w-5 h-5" /> | |
| </motion.button> | |
| </nav> | |
| ); | |
| }`, | |
| // ====== MIC BUTTON CORRIGÉ (détection client + Web Speech) ====== | |
| "src/components/MicButton.tsx": `"use client"; | |
| import { useState, useEffect, useRef } from "react"; | |
| import { Mic, MicOff, Loader } from "lucide-react"; | |
| interface MicButtonProps { | |
| onTranscription: (text: string) => void; | |
| disabled?: boolean; | |
| } | |
| export function MicButton({ onTranscription, disabled }: MicButtonProps) { | |
| const [isListening, setIsListening] = useState(false); | |
| const [isSupported, setIsSupported] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const recognitionRef = useRef<any>(null); | |
| // Détection uniquement côté client | |
| useEffect(() => { | |
| const SpeechRecognition = | |
| (window as any).SpeechRecognition || | |
| (window as any).webkitSpeechRecognition; | |
| if (SpeechRecognition) { | |
| setIsSupported(true); | |
| const recognition = new SpeechRecognition(); | |
| recognition.lang = "fr-FR"; | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| recognition.maxAlternatives = 1; | |
| recognition.onresult = (event: any) => { | |
| const transcript = event.results[0][0].transcript; | |
| console.log("✅ Transcription:", transcript); | |
| onTranscription(transcript); | |
| setIsListening(false); | |
| }; | |
| recognition.onerror = (event: any) => { | |
| console.error("❌ Erreur micro:", event.error); | |
| setError(event.error); | |
| setIsListening(false); | |
| }; | |
| recognition.onend = () => { | |
| setIsListening(false); | |
| }; | |
| recognitionRef.current = recognition; | |
| } else { | |
| setIsSupported(false); | |
| } | |
| }, []); | |
| const toggleListening = () => { | |
| if (!recognitionRef.current) return; | |
| setError(null); | |
| if (isListening) { | |
| recognitionRef.current.stop(); | |
| setIsListening(false); | |
| } else { | |
| try { | |
| recognitionRef.current.start(); | |
| setIsListening(true); | |
| console.log("🎤 Écoute démarrée..."); | |
| } catch (e) { | |
| console.error("Erreur start:", e); | |
| } | |
| } | |
| }; | |
| if (!isSupported) { | |
| return ( | |
| <span title="Micro non supporté sur ce navigateur (utilise Chrome/Edge)"> | |
| 🚫 | |
| </span> | |
| ); | |
| } | |
| return ( | |
| <button | |
| type="button" | |
| onClick={toggleListening} | |
| disabled={disabled || isListening} | |
| className={\`p-2 rounded-lg transition-colors \${isListening ? "bg-red-500/20 text-red-400 animate-pulse" : "text-gray-400 hover:text-vortex-400"}\`} | |
| aria-label="Enregistrement vocal" | |
| > | |
| {isListening ? <MicOff className="w-5 h-5" /> : <Mic className="w-5 h-5" />} | |
| </button> | |
| ); | |
| }`, | |
| // ====== CHAT (avec le mic intégré) ====== | |
| "src/components/Chat.tsx": `"use client"; | |
| import { useState } from "react"; | |
| import { motion } from "framer-motion"; | |
| import { Send } from "lucide-react"; | |
| import { useVortex } from "@/context/VortexContext"; | |
| import { MicButton } from "./MicButton"; | |
| export function Chat() { | |
| const [messages, setMessages] = useState([ | |
| { role: "assistant", content: "Bonjour ! Tapez @help pour voir les commandes." }, | |
| ]); | |
| const [input, setInput] = useState(""); | |
| const { sendMessage } = useVortex(); | |
| const handleSend = async () => { | |
| if (!input.trim()) return; | |
| setMessages((prev) => [...prev, { role: "user", content: input }]); | |
| setInput(""); | |
| const response = await sendMessage(input); | |
| setMessages((prev) => [...prev, { role: "assistant", content: response }]); | |
| }; | |
| return ( | |
| <div className="flex flex-col h-full"> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4"> | |
| {messages.map((msg, i) => ( | |
| <motion.div | |
| key={i} | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className={"flex " + (msg.role === "user" ? "justify-end" : "justify-start")} | |
| > | |
| <div | |
| className={ | |
| "max-w-[80%] px-4 py-2 rounded-xl " + | |
| (msg.role === "user" ? "bg-vortex-600 text-white" : "bg-gray-800 text-gray-200") | |
| } | |
| > | |
| <p className="whitespace-pre-wrap text-sm">{msg.content}</p> | |
| </div> | |
| </motion.div> | |
| ))} | |
| </div> | |
| <div className="p-4 border-t border-gray-800"> | |
| <div className="flex gap-2"> | |
| <MicButton onTranscription={(text) => setInput(text)} /> | |
| <input | |
| value={input} | |
| onChange={(e) => setInput(e.target.value)} | |
| onKeyDown={(e) => e.key === "Enter" && handleSend()} | |
| placeholder="Votre message..." | |
| className="flex-1 bg-gray-800 text-gray-200 px-4 py-2 rounded-xl outline-none text-sm" | |
| /> | |
| <motion.button | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| onClick={handleSend} | |
| className="p-2 bg-vortex-600 text-white rounded-xl" | |
| > | |
| <Send className="w-5 h-5" /> | |
| </motion.button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }`, | |
| // ====== PAGE DE TEST MIC (indépendante) ====== | |
| "public/micro.html": `<!DOCTYPE html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Micro ATYPIQUE</title> | |
| <style> | |
| body { font-family: Arial; background: #0a0a0a; color: #eee; text-align: center; padding: 2rem; } | |
| button { padding: 1rem 2rem; font-size: 1.2rem; border-radius: 2rem; border: none; cursor: pointer; background: #f59e0b; color: #000; } | |
| button.recording { background: #c00; color: #fff; } | |
| #status { margin-top: 1rem; color: #aaa; white-space: pre-wrap; } | |
| #response { margin-top: 1rem; color: #7be0f7; white-space: pre-wrap; text-align: left; max-width: 600px; margin: 0 auto; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>🎤 Micro ATYPIQUE</h1> | |
| <p>Enregistrez votre voix et obtenez une transcription + réponse</p> | |
| <button id="micBtn">Démarrer l'enregistrement</button> | |
| <div id="status"></div> | |
| <div id="response"></div> | |
| <script> | |
| const API_URL = 'https://rds777-atypique-api.hf.space'; | |
| let recorder, stream, chunks = []; | |
| document.getElementById('micBtn').addEventListener('click', async () => { | |
| const btn = document.getElementById('micBtn'); | |
| if (recorder && recorder.state === 'recording') { | |
| recorder.stop(); | |
| btn.textContent = 'Démarrer l\\'enregistrement'; | |
| btn.classList.remove('recording'); | |
| document.getElementById('status').textContent = 'Envoi au backend...'; | |
| } else { | |
| try { | |
| stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| recorder = new MediaRecorder(stream); | |
| chunks = []; | |
| recorder.ondataavailable = e => chunks.push(e.data); | |
| recorder.onstop = async () => { | |
| const blob = new Blob(chunks, { type: 'audio/webm' }); | |
| const audioContext = new AudioContext(); | |
| const arrayBuffer = await blob.arrayBuffer(); | |
| const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); | |
| const wavBlob = encodeWAV(audioBuffer); | |
| const formData = new FormData(); | |
| formData.append('audio', wavBlob, 'recording.wav'); | |
| document.getElementById('status').textContent = 'Transcription...'; | |
| try { | |
| const res = await fetch(API_URL + '/api/stt-vosk', { method: 'POST', body: formData }); | |
| const data = await res.json(); | |
| if (data.text) { | |
| document.getElementById('status').textContent = '✅ ' + data.text; | |
| const chatRes = await fetch(API_URL + '/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ message: data.text }) | |
| }); | |
| const chatData = await chatRes.json(); | |
| document.getElementById('response').textContent = '🤖 Réponse :\\n' + chatData.response; | |
| } else { | |
| document.getElementById('status').textContent = 'Erreur: ' + JSON.stringify(data); | |
| } | |
| } catch (e) { | |
| document.getElementById('status').textContent = 'Erreur réseau: ' + e.message; | |
| } | |
| stream.getTracks().forEach(t => t.stop()); | |
| }; | |
| recorder.start(); | |
| btn.textContent = '⏹️ Arrêter'; | |
| btn.classList.add('recording'); | |
| document.getElementById('status').textContent = '🔴 Parlez...'; | |
| } catch (err) { | |
| document.getElementById('status').textContent = 'Erreur micro: ' + err.message; | |
| } | |
| } | |
| }); | |
| function encodeWAV(audioBuffer) { | |
| const numChannels = audioBuffer.numberOfChannels; | |
| const sampleRate = audioBuffer.sampleRate; | |
| const bitsPerSample = 16; | |
| const data = audioBuffer.getChannelData(0); | |
| const length = data.length; | |
| const buffer = new ArrayBuffer(44 + length * 2); | |
| const view = new DataView(buffer); | |
| const writeString = (offset, string) => { for (let i = 0; i < string.length; i++) view.setUint8(offset + i, string.charCodeAt(i)); }; | |
| writeString(0, 'RIFF'); | |
| view.setUint32(4, 36 + length * 2, true); | |
| writeString(8, 'WAVE'); | |
| writeString(12, 'fmt '); | |
| view.setUint32(16, 16, true); | |
| view.setUint16(20, 1, true); | |
| view.setUint16(22, numChannels, true); | |
| view.setUint32(24, sampleRate, true); | |
| view.setUint32(28, sampleRate * numChannels * bitsPerSample / 8, true); | |
| view.setUint16(32, numChannels * bitsPerSample / 8, true); | |
| view.setUint16(34, bitsPerSample, true); | |
| writeString(36, 'data'); | |
| view.setUint32(40, length * 2, true); | |
| for (let i = 0; i < length; i++) { | |
| const sample = Math.max(-1, Math.min(1, data[i])); | |
| view.setInt16(44 + i * 2, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true); | |
| } | |
| return new Blob([buffer], { type: 'audio/wav' }); | |
| } | |
| </script> | |
| </body> | |
| </html>` | |
| }; | |
| // Écriture des fichiers | |
| for (const [path, content] of Object.entries(files)) { | |
| const dir = path.substring(0, path.lastIndexOf('/')); | |
| if (dir) mkdirSync(dir, { recursive: true }); | |
| writeFileSync(path, content, 'utf8'); | |
| console.log('✅ ' + path); | |
| } | |
| console.log('\n🎉 Tous les fichiers ont été créés (UI riche + micro corrigé).'); | |
| console.log('Lance maintenant :'); | |
| console.log(' npm install @tailwindcss/postcss tailwindcss framer-motion lucide-react'); | |
| console.log(' npm run build'); | |
| console.log(' npx vercel --prod --yes'); |