| <!DOCTYPE html> |
| <html lang="fr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>CAPL - Agent Vocal (Daily)</title> |
| <script src="https://unpkg.com/@daily-co/daily-js"></script> |
| <style> |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| body { |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
| background: #f5f7fa; |
| color: #1a1a2e; |
| min-height: 100vh; |
| padding: 24px; |
| } |
| .card { |
| background: #fff; |
| border-radius: 16px; |
| box-shadow: 0 4px 24px rgba(0,0,0,.08); |
| padding: 40px 36px; |
| max-width: 520px; |
| margin: 0 auto 24px; |
| } |
| h1 { font-size: 1.5rem; font-weight: 700; margin-bottom: 4px; } |
| .subtitle { font-size: .875rem; color: #6b7280; margin-bottom: 24px; } |
| label { display: block; font-size: .8125rem; font-weight: 600; color: #374151; margin-bottom: 4px; } |
| input, select { |
| width: 100%; |
| padding: 10px 12px; |
| border: 1px solid #d1d5db; |
| border-radius: 8px; |
| font-size: .875rem; |
| margin-bottom: 16px; |
| } |
| input:focus, select:focus { |
| outline: none; |
| border-color: #2563eb; |
| box-shadow: 0 0 0 3px rgba(37,99,235,.12); |
| } |
| .row { display: flex; gap: 12px; } |
| .row > div { flex: 1; } |
| .status-bar { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| margin-bottom: 20px; |
| padding: 10px 14px; |
| border-radius: 8px; |
| font-size: .8125rem; |
| font-weight: 500; |
| } |
| .status-bar.disconnected { background: #fef2f2; color: #991b1b; } |
| .status-bar.connecting { background: #fffbeb; color: #92400e; } |
| .status-bar.connected { background: #f0fdf4; color: #166534; } |
| .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } |
| .disconnected .dot { background: #ef4444; } |
| .connecting .dot { background: #f59e0b; animation: pulse 1s infinite; } |
| .connected .dot { background: #22c55e; } |
| @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } } |
| button { |
| width: 100%; |
| padding: 12px; |
| border: none; |
| border-radius: 10px; |
| font-size: .9375rem; |
| font-weight: 600; |
| cursor: pointer; |
| transition: background .15s; |
| } |
| .btn-connect { background: #2563eb; color: #fff; } |
| .btn-connect:hover { background: #1d4ed8; } |
| .btn-disconnect { background: #ef4444; color: #fff; } |
| .btn-disconnect:hover { background: #dc2626; } |
| .btn-connect:disabled { background: #93c5fd; cursor: not-allowed; } |
| #daily-container { |
| max-width: 520px; |
| margin: 0 auto; |
| min-height: 400px; |
| border-radius: 12px; |
| overflow: hidden; |
| background: #1a1a2e; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="card"> |
| <h1>Agent Vocal CAPL (Daily)</h1> |
| <p class="subtitle">Conversation via Daily.co — compatible Hugging Face Spaces</p> |
|
|
| <label for="token">JWT Token</label> |
| <input id="token" type="text" placeholder="Collez votre token JWT ici"> |
|
|
| <div class="row"> |
| <div> |
| <label for="model">Modele</label> |
| <select id="model"> |
| <option value="">Par defaut</option> |
| <option value="mistral-large-latest">Mistral Large</option> |
| <option value="magistral-medium-latest">Magistral Medium</option> |
| </select> |
| </div> |
| <div> |
| <label for="project-id">Project ID</label> |
| <input id="project-id" type="text" placeholder="(optionnel)"> |
| </div> |
| </div> |
|
|
| <div id="status-bar" class="status-bar disconnected"> |
| <span class="dot"></span> |
| <span id="status-text">Deconnecte</span> |
| </div> |
|
|
| <button id="action-btn" class="btn-connect">Se connecter</button> |
| </div> |
|
|
| <div id="daily-container"></div> |
|
|
| <script> |
| const tokenEl = document.getElementById("token"); |
| const modelEl = document.getElementById("model"); |
| const projectEl = document.getElementById("project-id"); |
| const statusBar = document.getElementById("status-bar"); |
| const statusText = document.getElementById("status-text"); |
| const actionBtn = document.getElementById("action-btn"); |
| const container = document.getElementById("daily-container"); |
| |
| let callFrame = null; |
| let connected = false; |
| |
| function setStatus(state, text) { |
| statusBar.className = "status-bar " + state; |
| statusText.textContent = text; |
| connected = (state === "connected" || state === "connecting"); |
| actionBtn.className = connected ? "btn-disconnect" : "btn-connect"; |
| actionBtn.textContent = connected ? "Se deconnecter" : "Se connecter"; |
| } |
| |
| async function connect() { |
| const token = tokenEl.value.trim(); |
| if (!token) { |
| alert("Veuillez saisir un token JWT."); |
| return; |
| } |
| |
| setStatus("connecting", "Creation de la room Daily..."); |
| |
| try { |
| const params = new URLSearchParams(); |
| const model = modelEl.value; |
| const projectId = projectEl.value.trim(); |
| if (model) params.set("model", model); |
| if (projectId) params.set("project_id", projectId); |
| const qs = params.toString(); |
| |
| const res = await fetch("/voice/daily-start" + (qs ? "?" + qs : ""), { |
| method: "POST", |
| headers: { |
| "Content-Type": "application/json", |
| "Authorization": "Bearer " + token |
| }, |
| body: JSON.stringify({}) |
| }); |
| |
| if (!res.ok) { |
| const err = await res.text(); |
| throw new Error("Erreur: " + res.status + " — " + err); |
| } |
| |
| const data = await res.json(); |
| const roomUrl = data.room_url; |
| const roomToken = data.token; |
| |
| if (!roomUrl) { |
| throw new Error("Reponse invalide: room_url manquant"); |
| } |
| |
| setStatus("connecting", "Connexion a la room..."); |
| |
| callFrame = window.Daily.createFrame(container, { |
| showLeaveButton: true, |
| iframeStyle: { width: "100%", height: "400px", border: "0" } |
| }); |
| |
| callFrame.on("joined-meeting", () => { |
| setStatus("connected", "Connecte — Parlez !"); |
| }); |
| |
| callFrame.on("left-meeting", () => { |
| setStatus("disconnected", "Deconnecte"); |
| callFrame = null; |
| }); |
| |
| callFrame.on("error", (e) => { |
| console.error("Daily error", e); |
| setStatus("disconnected", "Erreur: " + (e.errorMsg || e.type)); |
| }); |
| |
| const joinConfig = { url: roomUrl }; |
| if (roomToken) joinConfig.token = roomToken; |
| await callFrame.join(joinConfig); |
| } catch (err) { |
| console.error(err); |
| setStatus("disconnected", "Erreur: " + err.message); |
| callFrame = null; |
| } |
| } |
| |
| function disconnect() { |
| if (callFrame) { |
| callFrame.leave(); |
| callFrame.destroy(); |
| callFrame = null; |
| } |
| setStatus("disconnected", "Deconnecte"); |
| } |
| |
| actionBtn.addEventListener("click", () => { |
| if (!connected) connect(); |
| else disconnect(); |
| }); |
| </script> |
| </body> |
| </html> |
|
|