import { useState, useRef } from "react";
const C = {
bg: "#080810", surface: "#0f0f1a", surface2: "#141425",
border: "#1e1e32", border2: "#2a2a45", text: "#c8c8e0",
dim: "#55556a", accent: "#fb204a", teal: "#00d4aa",
gold: "#ffb347", blue: "#61d4ff", white: "#eeeef8",
};
const SYMPTOMS = [
{ id:"muddy", icon:"🌫️", name:"Suena apagado / embarrado", hint:"Todo suena junto, sin claridad. Los graves dominan." },
{ id:"thin", icon:"💨", name:"Suena delgado / sin cuerpo", hint:"La mezcla se escucha flaca, sin peso ni presencia." },
{ id:"harsh", icon:"⚡", name:"Suena duro / agresivo", hint:"Duele escuchar. Cansa los oídos. Demasiado agudo." },
{ id:"buried", icon:"🪦", name:"La voz se pierde", hint:"El vocal suena bien solo pero desaparece en la mezcla." },
{ id:"pumping", icon:"🫀", name:"La mezcla respira / bombea raro", hint:"El volumen sube y baja de forma extraña." },
{ id:"nowidth", icon:"📡", name:"Todo suena al centro / mono", hint:"No hay amplitud ni sensación de espacio estéreo." },
{ id:"clash", icon:"⚔️", name:"Bombo y bajo chocan", hint:"Cuando entran juntos, se tapan o se oye raro." },
{ id:"flat", icon:"📉", name:"Suena plano / sin energía", hint:"Falta emoción, empuje, dimensión." },
{ id:"loud", icon:"🔊", name:"No llega al volumen de referencias", hint:"Comparada con canciones comerciales, suena más baja." },
{ id:"phase", icon:"🌀", name:"Algo desaparece en mono", hint:"En el teléfono o un parlante, algo se pierde." },
{ id:"reverb", icon:"🏛️", name:"Demasiado reverb / suena lejano", hint:"Los elementos suenan en una caverna." },
{ id:"nogroove",icon:"🥁", name:"La batería no tiene punch", hint:"El bombo o la caja no golpean. Suenan blandos." },
];
const GENRES = ["Trap / Hip-Hop","Reggaetón / Urbano","Pop","R&B / Soul","Rock","Electrónica / EDM","Cumbia / Salsa","Balada","Otro"];
const QUICK_QS = [
"¿Puedes explicar más el fix rápido?",
"¿Qué plugin me recomiendas?",
"¿Cómo aplico esto en mi DAW?",
"Dame un ejemplo con un instrumento específico",
"¿Cómo suena diferente en monitores vs auriculares?",
];
// ── MARKDOWN FORMATTER ──
function formatMd(text) {
const lines = text.split("\n");
const out = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
if (!line.trim()) { i++; continue; }
if (/^#{1,3}\s/.test(line)) {
out.push(
→
{inlineFormat(lines[i].replace(/^[-•]\s/,""))}
);
i++;
}
out.push(
);
}
// ── MAIN COMPONENT ──
export default function MixDoctor() {
const [stage, setStage] = useState("symptoms"); // symptoms | loading | result
const [selected, setSelected] = useState(new Set());
const [genre, setGenre] = useState("");
const [userNote, setUserNote] = useState("");
const [diagnosis, setDiagnosis] = useState(null);
const [thread, setThread] = useState([]);
const [followupText, setFollowupText] = useState("");
const [isTyping, setIsTyping] = useState(false);
const [showQuick, setShowQuick] = useState(true);
const history = useRef([]);
const threadRef = useRef(null);
function toggleSymptom(id) {
setSelected(prev => {
const n = new Set(prev);
n.has(id) ? n.delete(id) : n.add(id);
return n;
});
}
async function runDiagnosis() {
const selectedList = SYMPTOMS.filter(s => selected.has(s.id)).map(s => s.name);
const prompt = `Eres Mix Doctor, un ingeniero de mezcla experto que ayuda a productores principiantes e intermedios a diagnosticar y resolver problemas en sus mezclas. Respondes en español, con un tono directo, profesional pero accesible — como un mentor experimentado.
El productor describe los siguientes problemas en su mezcla:
${selectedList.map(s=>`• ${s}`).join("\n")}
${userNote ? `\nDescripción adicional: "${userNote}"` : ""}
${genre ? `\nGénero: ${genre}` : ""}
Proporciona un diagnóstico completo. Estructura tu respuesta así:
**🔍 Diagnóstico Principal**
Identifica el problema raíz más probable. Explica por qué ocurre.
**🎯 Causas Más Probables**
Lista las 3-4 causas más comunes para estos síntomas, ordenadas de más a menos probable.
**🛠️ Soluciones Paso a Paso**
Instrucciones concretas y específicas. Incluye frecuencias, valores de parámetros, técnicas reales.
**⚡ Fix Rápido (5 minutos)**
Una sola cosa que puede intentar AHORA MISMO.
**📐 Cómo Prevenir Este Problema**
Un consejo de flujo de trabajo para futuras mezclas.
Sé específico y técnico. Si el género es relevante, adapta los consejos.`;
history.current = [{ role:"user", content: prompt }];
setStage("loading");
try {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method:"POST",
headers:{"Content-Type":"application/json"},
body: JSON.stringify({
model:"claude-sonnet-4-20250514",
max_tokens:1000,
messages: history.current
})
});
const data = await res.json();
const text = data.content?.map(b=>b.text||"").join("") || "No se recibió respuesta.";
history.current.push({ role:"assistant", content: text });
setDiagnosis({ text, selectedList, genre });
setStage("result");
} catch(e) {
setDiagnosis({ text:"Error de conexión. Intenta de nuevo.", selectedList, genre });
setStage("result");
}
}
async function sendMessage(msg) {
if (!msg.trim()) return;
setShowQuick(false);
setIsTyping(true);
setThread(prev => [...prev, { role:"user", content: msg }]);
setFollowupText("");
history.current.push({ role:"user", content: msg });
setTimeout(()=> threadRef.current?.scrollIntoView({ behavior:"smooth", block:"end" }), 100);
try {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method:"POST",
headers:{"Content-Type":"application/json"},
body: JSON.stringify({
model:"claude-sonnet-4-20250514",
max_tokens:1000,
messages: history.current
})
});
const data = await res.json();
const text = data.content?.map(b=>b.text||"").join("") || "No se recibió respuesta.";
history.current.push({ role:"assistant", content: text });
setThread(prev => [...prev, { role:"assistant", content: text }]);
} catch(e) {
setThread(prev => [...prev, { role:"assistant", content:"Error de conexión. Intenta de nuevo." }]);
}
setIsTyping(false);
setTimeout(()=> threadRef.current?.scrollIntoView({ behavior:"smooth", block:"end" }), 100);
}
function reset() {
setStage("symptoms"); setSelected(new Set()); setGenre("");
setUserNote(""); setDiagnosis(null); setThread([]);
setFollowupText(""); setIsTyping(false); setShowQuick(true);
history.current = [];
}
// ── STYLES ──
const s = {
wrap: { background:C.bg, minHeight:"100vh", fontFamily:"'Open Sans',sans-serif", color:C.text },
header: { padding:"24px 28px 0", borderBottom:`1px solid ${C.border}`, background:"linear-gradient(180deg,#0c0c18,transparent)", display:"flex", justifyContent:"space-between", alignItems:"center", flexWrap:"wrap", gap:10 },
h1: { fontFamily:"Montserrat,sans-serif", fontWeight:900, fontSize:28, color:C.white, letterSpacing:-1, lineHeight:1 },
main: { maxWidth:860, margin:"0 auto", padding:"32px 28px 60px" },
stageLabel: { fontSize:10, letterSpacing:5, color:C.accent, textTransform:"uppercase", marginBottom:10, display:"flex", alignItems:"center", gap:10 },
stageTitle: { fontFamily:"Montserrat,sans-serif", fontWeight:800, fontSize:22, color:C.white, marginBottom:8, letterSpacing:-0.5 },
stageDesc: { fontSize:13, color:C.dim, marginBottom:24, lineHeight:1.7 },
symptomGrid: { display:"grid", gridTemplateColumns:"repeat(auto-fill,minmax(220px,1fr))", gap:8, marginBottom:24 },
symptomBtn: (sel) => ({
background: sel ? `rgba(251,32,74,0.1)` : C.surface,
border: `1px solid ${sel ? C.accent : C.border}`,
borderRadius:4, padding:"14px 16px", cursor:"pointer",
textAlign:"left", transition:"all 0.15s", display:"flex",
alignItems:"flex-start", gap:10, width:"100%"
}),
genreRow: { display:"flex", flexWrap:"wrap", gap:8, marginBottom:24 },
genrePill: (sel) => ({
padding:"6px 14px", borderRadius:20,
border:`1px solid ${sel ? C.teal : C.border2}`,
background: sel ? "rgba(0,212,170,0.1)" : "transparent",
color: sel ? C.teal : C.dim,
fontSize:12, fontWeight:600, cursor:"pointer",
transition:"all 0.15s", fontFamily:"'Open Sans',sans-serif"
}),
textarea: { width:"100%", background:C.surface, border:`1px solid ${C.border}`, borderRadius:4, padding:"12px 14px", color:C.white, fontFamily:"'Open Sans',sans-serif", fontSize:13, lineHeight:1.7, resize:"vertical", minHeight:88, outline:"none" },
diagBtn: (dis) => ({
background: dis ? C.border2 : C.accent,
color: dis ? C.dim : "#fff",
border:"none", borderRadius:4, padding:"14px 32px",
fontFamily:"Montserrat,sans-serif", fontWeight:800,
fontSize:13, letterSpacing:1, textTransform:"uppercase",
cursor: dis ? "not-allowed" : "pointer",
display:"flex", alignItems:"center", gap:8,
transition:"all 0.2s"
}),
card: { background:C.surface, border:`1px solid ${C.border}`, borderLeft:`3px solid ${C.accent}`, borderRadius:4, overflow:"hidden", marginBottom:12 },
cardHead: { padding:"12px 20px", background:C.surface2, borderBottom:`1px solid ${C.border}`, fontSize:9, letterSpacing:4, color:C.dim, textTransform:"uppercase", display:"flex", alignItems:"center", gap:8 },
cardBody: { padding:"18px 20px" },
threadMsg: (role) => ({
padding:"14px 18px", borderRadius:4, marginBottom:10,
background: C.surface,
border:`1px solid ${C.border}`,
borderLeft:`3px solid ${role==="user" ? C.gold : C.accent}`,
fontSize:13, lineHeight:1.8, color:C.text
}),
followInput: { flex:1, background:C.surface2, border:`1px solid ${C.border}`, borderRadius:4, padding:"11px 14px", color:C.white, fontFamily:"'Open Sans',sans-serif", fontSize:13, outline:"none" },
followBtn: { background:C.surface2, border:`1px solid ${C.border2}`, borderRadius:4, padding:"11px 18px", color:C.text, fontFamily:"Montserrat,sans-serif", fontWeight:700, fontSize:12, letterSpacing:1, cursor:"pointer" },
resetBtn: { background:"none", border:`1px solid ${C.border2}`, borderRadius:4, padding:"9px 18px", color:C.dim, fontSize:12, fontWeight:600, cursor:"pointer", marginTop:20, fontFamily:"'Open Sans',sans-serif" },
quickBtn: { background:"none", border:`1px solid ${C.border}`, borderRadius:20, padding:"6px 13px", color:C.dim, fontSize:11, cursor:"pointer", fontFamily:"'Open Sans',sans-serif", transition:"all 0.15s" },
};
return (
{inlineFormat(line.replace(/^#{1,3}\s/,""))}
); } else if (/^[-•]\s/.test(line)) { const items = []; while (i < lines.length && /^[-•]\s/.test(lines[i])) { items.push(- {items}
{inlineFormat(line)}
); } i++; } return out; } function inlineFormat(text) { const parts = []; const re = /(\*\*(.+?)\*\*|\*(.+?)\*|`(.+?)`)/g; let last = 0, m; while ((m = re.exec(text)) !== null) { if (m.index > last) parts.push(text.slice(last, m.index)); if (m[2]) parts.push({m[2]}); else if (m[3]) parts.push({m[3]}); else if (m[4]) parts.push({m[4]});
last = m.index + m[0].length;
}
if (last < text.length) parts.push(text.slice(last));
return parts.length === 1 && typeof parts[0] === "string" ? parts[0] : parts;
}
// ── LOADING BARS ──
function LoadingBars() {
const bars = [
{h:20,c:C.accent},{h:32,c:C.gold},{h:44,c:C.accent},{h:28,c:C.teal},
{h:40,c:C.accent},{h:22,c:C.blue},{h:36,c:C.accent},{h:26,c:C.gold},{h:42,c:C.teal}
];
return (
{bars.map((b,i)=>(
))}
Analizando tu mezcla...
Consultando con el ingeniero
{/* HEADER */}
Herramienta de Diagnóstico
Mix Doctor
Juan Manuel Alcantara
Ingeniero de Mezcla
{/* ── STAGE: SYMPTOMS ── */}
{stage === "symptoms" && (
}
{/* ── STAGE: RESULT ── */}
{stage === "result" && diagnosis && (
);
}
Paso 1
)}
{/* ── STAGE: LOADING ── */}
{stage === "loading" && ¿Qué está mal en tu mezcla?
Selecciona uno o más síntomas que escuchas. Puedes elegir varios.
{SYMPTOMS.map(sym => (
))}
Describe el problema (opcional)
Género
{GENRES.map(g => (
))}
{/* Diagnosis header */}
{diagnosis.genre && (
{diagnosis.genre}
)}
{/* Diagnosis content */}
{/* Follow-up section */}
))}
{/* Typing indicator */}
{isTyping && (
)}
{/* FOOTER */}
Diagnóstico completado
{diagnosis.selectedList.slice(0,2).join(" · ")}{diagnosis.selectedList.length>2?` · +${diagnosis.selectedList.length-2} más`:""}
Diagnóstico del Mix Doctor
{formatMd(diagnosis.text)}
Sigue preguntando
{/* Thread */}
{thread.map((msg,i) => (
{msg.role==="user"?"Tú":"Mix Doctor"}
{msg.role==="assistant" ? formatMd(msg.content) : msg.content}
{[0,0.2,0.4].map((d,i)=>(
))}
)}
{/* Quick questions */}
{showQuick && !isTyping && (
{QUICK_QS.map(q=>(
))}
)}
{/* Input row */}
setFollowupText(e.target.value)}
onKeyDown={e=>e.key==="Enter"&&sendMessage(followupText)}
placeholder="Pregunta algo más sobre tu mezcla..." />
Juan Manuel Alcantara
Mix Doctor · Guía Interactiva de Mezcla
Join Our Free Trial
Get started today before this once in a lifetime opportunity expires.