web-ui.html•10.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FM8 MCP Controller</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
padding: 40px;
max-width: 800px;
width: 100%;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
h1 {
color: #667eea;
margin-bottom: 10px;
font-size: 32px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.chat-container {
height: 400px;
overflow-y: auto;
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
background: #fafafa;
}
.message {
margin-bottom: 15px;
padding: 12px 16px;
border-radius: 12px;
max-width: 80%;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user {
background: #667eea;
color: white;
margin-left: auto;
}
.assistant {
background: #e9ecef;
color: #333;
}
.tool-call {
background: #fff3cd;
color: #856404;
font-size: 13px;
margin: 5px 0;
padding: 8px 12px;
}
.input-container {
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 14px 18px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
}
button {
padding: 14px 30px;
background: #667eea;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
font-weight: 600;
}
button:hover {
background: #5568d3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.status {
display: flex;
gap: 10px;
margin-bottom: 20px;
font-size: 13px;
}
.status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-online { background: #28a745; }
.status-offline { background: #dc3545; }
.examples {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.examples h3 {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.example-btn {
display: inline-block;
padding: 6px 12px;
margin: 4px;
background: white;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.example-btn:hover {
background: #667eea;
color: white;
border-color: #667eea;
}
</style>
</head>
<body>
<div class="container">
<h1>🎹 FM8 Controller</h1>
<p class="subtitle">Chat with AI to control your FM8 synthesizer via MIDI</p>
<div class="status">
<div>
<span class="status-dot" id="mcp-status"></span>
<span id="mcp-text">MCP Server: Checking...</span>
</div>
<div>
<span class="status-dot" id="ollama-status"></span>
<span id="ollama-text">Ollama: Checking...</span>
</div>
</div>
<div class="chat-container" id="chat"></div>
<div class="input-container">
<input
type="text"
id="input"
placeholder="Ask me to control FM8..."
onkeypress="if(event.key==='Enter') sendMessage()"
/>
<button onclick="sendMessage()" id="send-btn">Send</button>
</div>
<div class="examples">
<h3>Try these examples:</h3>
<span class="example-btn" onclick="setInput('Show me all available mappings')">List mappings</span>
<span class="example-btn" onclick="setInput('Send CC 10 with value 64')">Send CC</span>
<span class="example-btn" onclick="setInput('Set LFO1 to modulate filter cutoff at 80')">LFO modulation</span>
<span class="example-btn" onclick="setInput('What is the server status?')">Check status</span>
<span class="example-btn" onclick="setInput('Emergency panic!')">Panic</span>
</div>
</div>
<script>
const MCP_URL = 'http://localhost:3333';
const OLLAMA_URL = 'http://localhost:11434';
const MODEL = 'llama3.2';
let messages = [];
let isProcessing = false;
const tools = [
{
type: "function",
function: {
name: "send_by_cc",
description: "Send a raw MIDI CC (0-127) to FM8.",
parameters: {
type: "object",
properties: {
cc: { type: "number", description: "MIDI CC number (0-127)" },
value: { type: "number", description: "MIDI CC value (0-127)" }
},
required: ["cc", "value"]
}
}
},
{
type: "function",
function: {
name: "send_by_route",
description: "Send FM8 matrix route.",
parameters: {
type: "object",
properties: {
source: { type: "string", description: "Source (e.g., 'LFO1')" },
dest: { type: "string", description: "Destination (e.g., 'CUTOFF')" },
value: { type: "number", description: "Amount (0-127)" }
},
required: ["source", "dest", "value"]
}
}
},
{
type: "function",
function: {
name: "list_mappings",
description: "List all FM8 mappings.",
parameters: { type: "object", properties: {} }
}
},
{
type: "function",
function: {
name: "status",
description: "Get server status.",
parameters: { type: "object", properties: {} }
}
},
{
type: "function",
function: {
name: "panic",
description: "Emergency stop.",
parameters: { type: "object", properties: {} }
}
}
];
// Check server status
async function checkStatus() {
try {
const mcpRes = await fetch(`${MCP_URL}/health`);
document.getElementById('mcp-status').className = 'status-dot status-online';
document.getElementById('mcp-text').textContent = 'MCP Server: Online';
} catch {
document.getElementById('mcp-status').className = 'status-dot status-offline';
document.getElementById('mcp-text').textContent = 'MCP Server: Offline';
}
try {
const ollamaRes = await fetch(`${OLLAMA_URL}/api/tags`);
document.getElementById('ollama-status').className = 'status-dot status-online';
document.getElementById('ollama-text').textContent = 'Ollama: Online';
} catch {
document.getElementById('ollama-status').className = 'status-dot status-offline';
document.getElementById('ollama-text').textContent = 'Ollama: Offline';
}
}
async function callMCPTool(name, args) {
const response = await fetch(`${MCP_URL}/mcp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: { name, arguments: args }
})
});
const result = await response.json();
return result.result;
}
function addMessage(role, content, isToolCall = false) {
const chat = document.getElementById('chat');
const msg = document.createElement('div');
msg.className = `message ${isToolCall ? 'tool-call' : role}`;
msg.textContent = content;
chat.appendChild(msg);
chat.scrollTop = chat.scrollHeight;
}
async function sendMessage() {
if (isProcessing) return;
const input = document.getElementById('input');
const text = input.value.trim();
if (!text) return;
input.value = '';
isProcessing = true;
document.getElementById('send-btn').disabled = true;
addMessage('user', text);
messages.push({ role: 'user', content: text });
try {
const response = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: MODEL,
messages: messages,
tools: tools,
stream: false
})
});
const data = await response.json();
if (data.message.tool_calls) {
messages.push(data.message);
for (const toolCall of data.message.tool_calls) {
addMessage('assistant', `🔧 ${toolCall.function.name}(${JSON.stringify(toolCall.function.arguments)})`, true);
const result = await callMCPTool(toolCall.function.name, toolCall.function.arguments);
addMessage('assistant', `✅ ${JSON.stringify(result)}`, true);
messages.push({ role: 'tool', content: JSON.stringify(result) });
}
const finalResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: MODEL,
messages: messages,
stream: false
})
});
const finalData = await finalResponse.json();
messages.push(finalData.message);
addMessage('assistant', finalData.message.content);
} else {
messages.push(data.message);
addMessage('assistant', data.message.content);
}
} catch (error) {
addMessage('assistant', `❌ Error: ${error.message}`);
}
isProcessing = false;
document.getElementById('send-btn').disabled = false;
input.focus();
}
function setInput(text) {
document.getElementById('input').value = text;
document.getElementById('input').focus();
}
// Initial status check
checkStatus();
setInterval(checkStatus, 5000);
</script>
</body>
</html>