atypique-api / setup_final.mjs
RDS777's picture
Revert: suppression de Groq, retour au Qwen 7B local
2b3fd6a
Raw
History Blame Contribute Delete
15.5 kB
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');