chat_ui.jsβ’17.4 kB
// Chat UI JavaScript for Fusion 360 MCP
let ws = null;
let currentConversationId = null;
let messageHistory = [];
let isWaitingForResponse = false;
let sidebarOpen = false;
// Initialize WebSocket connection
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
updateStatus('Connected', true);
console.log('WebSocket connected');
};
ws.onclose = () => {
updateStatus('Disconnected', false);
console.log('WebSocket disconnected');
// Attempt to reconnect after 3 seconds
setTimeout(initWebSocket, 3000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
updateStatus('Error', false);
};
ws.onmessage = (event) => {
handleWebSocketMessage(JSON.parse(event.data));
};
}
// Handle incoming WebSocket messages
function handleWebSocketMessage(data) {
switch(data.type) {
case 'response':
addAssistantMessage(data.content, data.code, data.executionResult);
isWaitingForResponse = false;
removeTypingIndicator();
enableInput();
break;
case 'stream':
updateStreamingMessage(data.content);
break;
case 'execution_result':
updateExecutionResult(data.messageId, data.result, data.success);
break;
case 'models':
updateModelList(data.models);
break;
case 'error':
showError(data.message);
isWaitingForResponse = false;
removeTypingIndicator();
enableInput();
break;
}
}
// Update status indicator
function updateStatus(text, connected) {
const statusText = document.getElementById('statusText');
const statusDot = document.getElementById('statusDot');
statusText.textContent = text;
if (connected) {
statusDot.classList.remove('error');
} else {
statusDot.classList.add('error');
}
}
// Send message to server
function sendMessage() {
const input = document.getElementById('messageInput');
const message = input.value.trim();
if (!message || isWaitingForResponse) return;
// Hide welcome screen if visible
const welcomeScreen = document.getElementById('welcomeScreen');
if (welcomeScreen) {
welcomeScreen.style.display = 'none';
}
// Add user message to UI
addUserMessage(message);
// Clear input
input.value = '';
input.style.height = 'auto';
// Show typing indicator
showTypingIndicator();
// Send via WebSocket
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
content: message,
conversationId: currentConversationId
}));
isWaitingForResponse = true;
disableInput();
} else {
showError('Not connected to server. Please wait...');
removeTypingIndicator();
}
}
// Add user message to UI
function addUserMessage(content) {
const container = document.getElementById('messagesContainer');
const messageDiv = document.createElement('div');
messageDiv.className = 'message user';
messageDiv.innerHTML = `
<div class="message-avatar">U</div>
<div class="message-content">
<div class="message-text">${escapeHtml(content)}</div>
</div>
`;
container.appendChild(messageDiv);
scrollToBottom();
messageHistory.push({ role: 'user', content });
}
// Add assistant message to UI
function addAssistantMessage(content, code = null, executionResult = null) {
const container = document.getElementById('messagesContainer');
const messageDiv = document.createElement('div');
messageDiv.className = 'message assistant';
let messageContent = `
<div class="message-avatar">AI</div>
<div class="message-content">
<div class="message-text">${formatMessage(content)}</div>
`;
if (code) {
const codeId = 'code-' + Date.now();
messageContent += `
<div class="code-block">
<div class="code-header">
<span class="code-lang">Python</span>
<div class="code-actions">
<button class="code-btn toggle" onclick="toggleCode('${codeId}')">
<span id="${codeId}-toggle-text">Show Code</span>
<span id="${codeId}-toggle-icon">βΌ</span>
</button>
<button class="code-btn" onclick="copyCode('${codeId}')">Copy</button>
<button class="code-btn execute" onclick="executeCode('${codeId}', this)">Execute</button>
</div>
</div>
<div class="code-content" id="${codeId}-content">
<pre id="${codeId}">${escapeHtml(code)}</pre>
</div>
<div id="${codeId}-result"></div>
</div>
`;
}
if (executionResult) {
const resultClass = executionResult.success ? 'success' : 'error';
messageContent += `
<div class="execution-result ${resultClass}">
${executionResult.success ? 'β' : 'β'} ${escapeHtml(executionResult.message)}
</div>
`;
}
messageContent += '</div>';
messageDiv.innerHTML = messageContent;
container.appendChild(messageDiv);
scrollToBottom();
messageHistory.push({ role: 'assistant', content, code, executionResult });
}
// Show typing indicator
function showTypingIndicator() {
const container = document.getElementById('messagesContainer');
const typingDiv = document.createElement('div');
typingDiv.className = 'message assistant typing-indicator-msg';
typingDiv.id = 'typingIndicator';
typingDiv.innerHTML = `
<div class="message-avatar">AI</div>
<div class="message-content">
<div class="typing-indicator">
<div class="typing-dot"></div>
<div class="typing-dot"></div>
<div class="typing-dot"></div>
</div>
</div>
`;
container.appendChild(typingDiv);
scrollToBottom();
}
// Remove typing indicator
function removeTypingIndicator() {
const indicator = document.getElementById('typingIndicator');
if (indicator) {
indicator.remove();
}
}
// Execute code
function executeCode(codeId, button) {
const codeElement = document.getElementById(codeId);
const code = codeElement.textContent;
const resultDiv = document.getElementById(codeId + '-result');
// Disable button
button.disabled = true;
button.textContent = 'Executing...';
// Send execution request
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'execute',
code: code,
messageId: codeId
}));
}
// Show loading state
resultDiv.innerHTML = '<div class="execution-result">Executing...</div>';
}
// Update execution result
function updateExecutionResult(messageId, result, success) {
const resultDiv = document.getElementById(messageId + '-result');
const button = document.querySelector(`button[onclick*="${messageId}"]`);
if (resultDiv) {
const resultClass = success ? 'success' : 'error';
resultDiv.innerHTML = `
<div class="execution-result ${resultClass}">
${success ? 'β Execution successful' : 'β Execution failed'}: ${escapeHtml(result)}
</div>
`;
}
if (button) {
button.disabled = false;
button.textContent = 'Execute';
}
}
// Copy code to clipboard
function copyCode(codeId) {
const codeElement = document.getElementById(codeId);
const code = codeElement.textContent;
navigator.clipboard.writeText(code).then(() => {
// Show temporary success message
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
});
}
// Format message (basic markdown support)
function formatMessage(text) {
// Code blocks
text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
// Inline code
text = text.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
// Italic
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// Line breaks
text = text.replace(/\n/g, '<br>');
return text;
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Scroll to bottom
function scrollToBottom() {
const container = document.getElementById('messagesContainer');
container.scrollTop = container.scrollHeight;
}
// Handle Enter key in textarea
function handleKeyPress(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
}
// Auto-resize textarea
document.addEventListener('DOMContentLoaded', () => {
const textarea = document.getElementById('messageInput');
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
});
// Initialize WebSocket
initWebSocket();
// Load models for current backend
loadModels();
});
// Backend selection
function updateBackend() {
const backend = document.getElementById('backendSelect').value;
const apiKeyGroup = document.getElementById('apiKeyGroup');
const modelSelect = document.getElementById('modelSelect');
// Show/hide API key field
if (backend === 'ollama') {
apiKeyGroup.style.display = 'none';
} else {
apiKeyGroup.style.display = 'block';
}
// Show loading state
modelSelect.innerHTML = '<option value="">Loading models...</option>';
// Load models for selected backend
loadModels();
}
// Load available models
function loadModels() {
const backend = document.getElementById('backendSelect').value;
const apiKey = document.getElementById('apiKeyInput').value;
console.log('Loading models for backend:', backend);
// Wait for WebSocket to be ready
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'get_models',
backend: backend,
apiKey: apiKey || null
}));
} else {
console.error('WebSocket not ready');
setTimeout(loadModels, 1000); // Retry after 1 second
}
}
// Update model list
function updateModelList(models) {
const select = document.getElementById('modelSelect');
const currentModelSpan = document.getElementById('currentModel');
console.log('Updating model list:', models);
select.innerHTML = '';
if (models && models.length > 0) {
models.forEach(model => {
const option = document.createElement('option');
option.value = model;
option.textContent = model;
select.appendChild(option);
});
const firstModel = models[0];
select.value = firstModel;
currentModelSpan.textContent = firstModel;
// Send model selection to server
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'set_model',
model: firstModel,
backend: document.getElementById('backendSelect').value,
apiKey: document.getElementById('apiKeyInput').value || null
}));
}
console.log('Model set to:', firstModel);
} else {
select.innerHTML = '<option value="">No models available</option>';
currentModelSpan.textContent = 'None';
console.warn('No models available for this backend');
}
}
// Model selection change
document.addEventListener('DOMContentLoaded', () => {
const modelSelect = document.getElementById('modelSelect');
modelSelect.addEventListener('change', () => {
const model = modelSelect.value;
document.getElementById('currentModel').textContent = model;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'set_model',
model: model,
backend: document.getElementById('backendSelect').value,
apiKey: document.getElementById('apiKeyInput').value
}));
}
});
});
// New chat
function newChat() {
currentConversationId = 'conv-' + Date.now();
messageHistory = [];
const container = document.getElementById('messagesContainer');
container.innerHTML = '';
const welcomeScreen = document.createElement('div');
welcomeScreen.className = 'welcome-screen';
welcomeScreen.id = 'welcomeScreen';
welcomeScreen.innerHTML = `
<h2>Welcome to Fusion 360 MCP</h2>
<p>AI-powered design automation for Fusion 360. Try one of these examples to get started:</p>
<div class="example-prompts">
<div class="example-prompt" onclick="sendExample('Create a 10mm cube at the origin')">
<div class="example-prompt-title">Basic Shape</div>
<div class="example-prompt-text">Create a 10mm cube at the origin</div>
</div>
<div class="example-prompt" onclick="sendExample('Create a cylinder with 20mm diameter and 50mm height')">
<div class="example-prompt-title">Cylinder</div>
<div class="example-prompt-text">Create a cylinder with 20mm diameter and 50mm height</div>
</div>
<div class="example-prompt" onclick="sendExample('Create a rectangular pattern of 5x3 holes, each 3mm diameter, spaced 10mm apart')">
<div class="example-prompt-title">Pattern</div>
<div class="example-prompt-text">Create a rectangular pattern of holes</div>
</div>
<div class="example-prompt" onclick="sendExample('Create a fillet of 2mm radius on all edges of the selected body')">
<div class="example-prompt-title">Fillet</div>
<div class="example-prompt-text">Create a fillet on all edges</div>
</div>
</div>
`;
container.appendChild(welcomeScreen);
}
// Send example prompt
function sendExample(text) {
document.getElementById('messageInput').value = text;
sendMessage();
}
// Show error message
function showError(message) {
const container = document.getElementById('messagesContainer');
const errorDiv = document.createElement('div');
errorDiv.className = 'message assistant';
errorDiv.innerHTML = `
<div class="message-avatar">β οΈ</div>
<div class="message-content">
<div class="execution-result error">
Error: ${escapeHtml(message)}
</div>
</div>
`;
container.appendChild(errorDiv);
scrollToBottom();
}
// Disable input while waiting
function disableInput() {
document.getElementById('messageInput').disabled = true;
document.getElementById('sendBtn').disabled = true;
}
// Enable input
function enableInput() {
document.getElementById('messageInput').disabled = false;
document.getElementById('sendBtn').disabled = false;
document.getElementById('messageInput').focus();
}
// Toggle code block visibility
function toggleCode(codeId) {
const codeContent = document.getElementById(`${codeId}-content`);
const toggleText = document.getElementById(`${codeId}-toggle-text`);
const toggleIcon = document.getElementById(`${codeId}-toggle-icon`);
if (codeContent.classList.contains('expanded')) {
codeContent.classList.remove('expanded');
toggleText.textContent = 'Show Code';
toggleIcon.textContent = 'βΌ';
} else {
codeContent.classList.add('expanded');
toggleText.textContent = 'Hide Code';
toggleIcon.textContent = 'β²';
}
}
// Toggle sidebar on mobile
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
const hamburger = document.getElementById('hamburgerBtn');
sidebarOpen = !sidebarOpen;
if (sidebarOpen) {
sidebar.classList.add('open');
overlay.classList.add('show');
hamburger.classList.add('active');
} else {
sidebar.classList.remove('open');
overlay.classList.remove('show');
hamburger.classList.remove('active');
}
}
// Close sidebar
function closeSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
const hamburger = document.getElementById('hamburgerBtn');
sidebarOpen = false;
sidebar.classList.remove('open');
overlay.classList.remove('show');
hamburger.classList.remove('active');
}