routeur_ia_api / static /voice_daily.html
Cyril Dupland
With daily
a7400dd
raw
history blame
8.3 kB
<!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>