UI.html•21.5 kB
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Sketch Context MCP</title>
    <style>
      body {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          Helvetica, Arial, sans-serif;
        margin: 0;
        padding: 20px;
        color: #333333;
        background-color: #f5f5f5;
      }
      .container {
        display: flex;
        flex-direction: column;
        height: 100%;
      }
      h1 {
        font-size: 16px;
        font-weight: 600;
        margin-bottom: 10px;
        color: #191919;
      }
      h2 {
        font-size: 14px;
        font-weight: 600;
        margin-top: 20px;
        margin-bottom: 8px;
        color: #191919;
      }
      button {
        background-color: #F8A01D;
        border: none;
        color: white;
        padding: 8px 12px;
        border-radius: 6px;
        margin-top: 8px;
        margin-bottom: 8px;
        cursor: pointer;
        font-size: 14px;
        transition: background-color 0.2s;
      }
      button:hover {
        background-color: #E59018;
      }
      button.secondary {
        background-color: #e0e0e0;
        color: #333333;
      }
      button.secondary:hover {
        background-color: #d0d0d0;
      }
      button:disabled {
        background-color: #cccccc;
        color: #666666;
        cursor: not-allowed;
      }
      input {
        border: 1px solid #cccccc;
        border-radius: 4px;
        padding: 8px;
        margin-bottom: 12px;
        font-size: 14px;
        width: 100%;
        box-sizing: border-box;
        background-color: #ffffff;
        color: #333333;
      }
      label {
        display: block;
        margin-bottom: 4px;
        font-size: 12px;
        font-weight: 500;
        color: #666666;
      }
      .status {
        margin-top: 16px;
        padding: 12px;
        border-radius: 6px;
        font-size: 14px;
      }
      .status.connected {
        background-color: #e6f7ee;
        color: #0f7b45;
      }
      .status.disconnected {
        background-color: #fbe9e7;
        color: #c62828;
      }
      .status.info {
        background-color: #e3f2fd;
        color: #1565c0;
      }
      .section {
        margin-bottom: 24px;
      }
      .hidden {
        display: none;
      }
      .logo {
        width: 50px;
        height: 50px;
      }
      .header {
        display: flex;
        align-items: center;
        margin-bottom: 16px;
      }
      .header-text {
        margin-left: 12px;
      }
      .header-text h1 {
        margin: 0;
        font-size: 16px;
      }
      .header-text p {
        margin: 4px 0 0 0;
        font-size: 12px;
        color: #666666;
      }
      .tabs {
        display: flex;
        border-bottom: 1px solid #e0e0e0;
        margin-bottom: 16px;
      }
      .tab {
        padding: 8px 16px;
        cursor: pointer;
        font-size: 14px;
        font-weight: 500;
        color: #666666;
      }
      .tab.active {
        border-bottom: 2px solid #F8A01D;
        color: #F8A01D;
      }
      .tab-content {
        display: none;
      }
      .tab-content.active {
        display: block;
      }
      .link {
        color: #F8A01D;
        text-decoration: none;
        cursor: pointer;
      }
      .link:hover {
        text-decoration: underline;
      }
      .header-logo {
        padding: 16px;
        border-radius: 16px;
        background-color: #ffffff;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .header-logo-image {
        width: 24px;
        height: 24px;
        object-fit: contain;
      }
      .selection-item {
        padding: 8px;
        margin-bottom: 8px;
        border: 1px solid #e0e0e0;
        border-radius: 4px;
        background-color: #ffffff;
      }
      .selection-item-header {
        display: flex;
        justify-content: space-between;
        font-weight: 500;
        margin-bottom: 4px;
      }
      .selection-item-type {
        color: #666666;
        font-size: 12px;
      }
      .selection-item-details {
        color: #666666;
        font-size: 12px;
      }
      #selection-list {
        max-height: 200px;
        overflow-y: auto;
        margin-bottom: 12px;
      }
      pre {
        background-color: #f0f0f0;
        padding: 8px;
        border-radius: 4px;
        overflow-x: auto;
        font-size: 12px;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="header">
        <div class="header-logo">
          <img
            class="header-logo-image"
            src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAOISURBVHgBrZdLbExRGMf/d++dmU5nKFOdYqpT08aziCdJI7qwaES6IKTEo0EsJCSCHQtiYVMSlrZWXhuJZ4gFjW66QOzaeDS0wqTT6cx0eu/1P/fctDO9r+n8kl/OPfec+/3Od75z7nfIQ36z7pnmZPj+kJHSCZRoCGTBf47ql8AKPPyj8tqekRGaTHR06o0IwVFOaV2AQfnw/2KKdmrGCt7UXu9KGKx0l0bYUUbJKVXjKQxR5Q36GDk9qAh06eKFbgLOoWHiWMKaQCCDdOxMNPUFbUO3mAw+jEo3A0xJQYoynGAiUn+0vfnm+FhW8Ly93krGSDXSQwzPVJ9sqXYPDBss0rFspmcaaKMccINijw3clBXMWIHDNEsFbvJ8z3hG0WfSFqVbBrGe4G/5QXyuaYZe5nFYx0PtUV/u7hLrEzjcXikcb3VQK7jMnZjQ+xIdMpwgPNwbsGvF1i3SQK21G4WTTolYJoxM6kjYITa9W7MXFBpnVGJEJh9dO4gN26axpn4Gpi3PfBhSP+Bi0234d/EjpwecPz8Ptcf2IvT1B1ovv0BgYw3WrF3liNpgUTgxDyP1REtaWdRoGHfTHHiK87Fxx1JU5E9Fv+8uel+1YoQzVOTP9h5A9bHd6Lv7Fs+PXcCsukrU1u13RLn35/Wq1ZXXWo9N6KMc+6KK6vI8lJdY9+jqB+/7fvgbWtDZ/QsDzVdRVFboGlz14B7D2Nir/r7vMHSZ6QpVFhXYdXHJdOt3XsHUvBxsi3NTDTO5Xu17OoPfsZeIlsZyc5iqiOBa6+oxXFmB2sZNKCtfiL4bn3D78lNrKI6fQdmSIvT0+nGj+RneXHkVq+NJcIx6qWz1orzdGxR4KRX3FV5hbK25Fo01NfDMXABPvgetN55i+NG3h3UDnnKjzHKTmzC/fsLX0ABfzyByPGVYXL3YvsD5pYVYvHwBPnd04dbrDvjDURf4pAiVIWLOY+lmTgbEVFm9fBEK87Kta7P32n2E+oectC8QJ9mRQ2pxzW+VyvEKG7eYihLdCcOhXV9ahfb4/KaZhGPFdSnmMIkdUYvHJdvOSQXRdLydNJNMTw76Vs4eG5Ll4qlIK4lD1C6XkjGSOlFLx0uFbLV0vBR/K0fKfn5cOnb71OFT+w+Kcq0gkMWyI4KPKN0UaUwc6OyJ4bvw0iMbJgjBHyh2BKP40FnNAAAAAElFTkSuQmCC"
          />
        </div>
        <div class="header-text">
          <h1>Sketch Context MCP</h1>
          <p>Connect Sketch to Cursor AI using MCP</p>
        </div>
      </div>
      <div class="tabs">
        <div id="tab-connection" class="tab active">Connection</div>
        <div id="tab-selection" class="tab">Selection</div>
        <div id="tab-about" class="tab">About</div>
      </div>
      <div id="content-connection" class="tab-content active">
        <div class="section">
          <label for="port">WebSocket Server Port</label>
          <div style="display: flex; gap: 8px">
            <input
              type="number"
              id="port"
              placeholder="3333"
              value="3333"
              min="1024"
              max="65535"
            />
            <button id="btn-connect" class="primary">Connect</button>
          </div>
        </div>
        <div id="connection-status" class="status disconnected">
          Not connected to Cursor MCP server
        </div>
        <div class="section">
          <button id="btn-disconnect" class="secondary" disabled>
            Disconnect
          </button>
        </div>
        <div class="section">
          <h2>Cursor Connection Guide</h2>
          <p>To connect this plugin to Cursor:</p>
          <ol>
            <li>Start the WebSocket server by clicking Connect above</li>
            <li>Open Cursor and go to Settings > Features > Context</li>
            <li>Add a new MCP server with URL: <code id="server-url">http://localhost:3333/sse</code></li>
            <li>Click "Connect" in Cursor</li>
          </ol>
        </div>
      </div>
      <div id="content-selection" class="tab-content">
        <div class="section">
          <h2>Selected Layers</h2>
          <div id="selection-list"></div>
          <button id="btn-copy-ids" class="primary">Copy Selection IDs</button>
          <button id="btn-refresh-selection" class="secondary">Refresh Selection</button>
        </div>
      </div>
      <div id="content-about" class="tab-content">
        <div class="section">
          <h2>About Sketch Context MCP</h2>
          <p>
            This plugin allows Cursor AI to communicate with Sketch, enabling
            AI-assisted design operations.
          </p>
          <p>Version: 1.0.0</p>
          <h2>How to Use</h2>
          <ol>
            <li>Connect to the WebSocket server using the Connection tab</li>
            <li>Select elements in Sketch and use the Selection tab to copy their IDs</li>
            <li>Use these IDs in Cursor to reference specific elements</li>
          </ol>
          <h2>Documentation</h2>
          <p>
            For more information, visit the
            <a class="link" onclick="openURL('https://github.com/yourusername/sketch-context-mcp')">
              GitHub repository
            </a>
          </p>
        </div>
      </div>
    </div>
    <script>
      // WebSocket connection state
      const state = {
        connected: false,
        socket: null,
        serverPort: 3333,
        pendingRequests: new Map(),
        channel: null,
      };
      // UI Elements
      const portInput = document.getElementById("port");
      const connectButton = document.getElementById("btn-connect");
      const disconnectButton = document.getElementById("btn-disconnect");
      const connectionStatus = document.getElementById("connection-status");
      const serverUrl = document.getElementById("server-url");
      const selectionList = document.getElementById("selection-list");
      const copyIdsButton = document.getElementById("btn-copy-ids");
      const refreshSelectionButton = document.getElementById("btn-refresh-selection");
      // Tabs
      const tabs = document.querySelectorAll(".tab");
      const tabContents = document.querySelectorAll(".tab-content");
      // Initialize UI
      function updateConnectionStatus(isConnected, message) {
        state.connected = isConnected;
        connectionStatus.innerHTML =
          message ||
          (isConnected
            ? "Connected to Cursor MCP server"
            : "Not connected to Cursor MCP server");
        connectionStatus.className = `status ${
          isConnected ? "connected" : "disconnected"
        }`;
        connectButton.disabled = isConnected;
        disconnectButton.disabled = !isConnected;
        portInput.disabled = isConnected;
      }
      // Connect to WebSocket server
      async function connectToServer(port) {
        try {
          if (state.connected && state.socket) {
            updateConnectionStatus(true, "Already connected to server");
            return;
          }
          state.serverPort = port;
          state.socket = new WebSocket(`ws://localhost:${port}`);
          serverUrl.textContent = `http://localhost:${port}/sse`;
          state.socket.onopen = () => {
            // Generate random channel name
            const channelName = generateChannelName();
            console.log("Joining channel:", channelName);
            state.channel = channelName;
            // Join the channel
            state.socket.send(
              JSON.stringify({
                type: "join",
                channel: channelName.trim(),
              })
            );
            
            updateConnectionStatus(false, "Connected to server, joining channel...");
            connectionStatus.className = "status info";
          };
          state.socket.onmessage = (event) => {
            try {
              const data = JSON.parse(event.data);
              console.log("Received message:", data);
              if (data.type === "system") {
                // Successfully joined channel
                if (data.message && data.message.result) {
                  state.connected = true;
                  const channelName = data.channel;
                  updateConnectionStatus(
                    true,
                    `Connected to server on port ${port} in channel: <strong>${channelName}</strong>`
                  );
                  // Notify the plugin code
                  window.postMessage("notify", {
                    message: `Connected to Cursor MCP server on port ${port} in channel: ${channelName}`,
                  });
                }
              } else if (data.type === "error") {
                console.error("Error:", data.message);
                updateConnectionStatus(false, `Error: ${data.message}`);
                state.socket.close();
              }
              handleSocketMessage(data);
            } catch (error) {
              console.error("Error parsing message:", error);
            }
          };
          state.socket.onclose = () => {
            state.connected = false;
            state.socket = null;
            updateConnectionStatus(false, "Disconnected from server");
          };
          state.socket.onerror = (error) => {
            console.error("WebSocket error:", error);
            updateConnectionStatus(false, "Connection error");
            state.connected = false;
            state.socket = null;
          };
        } catch (error) {
          console.error("Connection error:", error);
          updateConnectionStatus(
            false,
            `Connection error: ${error.message || "Unknown error"}`
          );
        }
      }
      // Disconnect from websocket server
      function disconnectFromServer() {
        if (state.socket) {
          state.socket.close();
          state.socket = null;
          state.connected = false;
          updateConnectionStatus(false, "Disconnected from server");
        }
      }
      // Handle messages from the WebSocket
      async function handleSocketMessage(payload) {
        const data = payload.message;
        if (!data) return;
        
        console.log("handleSocketMessage", data);
        // If it's a response to a previous request
        if (data.id && state.pendingRequests.has(data.id)) {
          const { resolve, reject } = state.pendingRequests.get(data.id);
          state.pendingRequests.delete(data.id);
          if (data.error) {
            reject(new Error(data.error));
          } else {
            resolve(data.result);
          }
          return;
        }
        // If it's a new command
        if (data.command) {
          try {
            // Send the command to the plugin code
            window.postMessage("execute-command", {
              id: data.id,
              command: data.command,
              params: data.params,
            });
          } catch (error) {
            // Send error back to WebSocket
            sendErrorResponse(
              data.id,
              error.message || "Error executing command"
            );
          }
        }
      }
      // Send a command to the WebSocket server
      async function sendCommand(command, params) {
        return new Promise((resolve, reject) => {
          if (!state.connected || !state.socket) {
            reject(new Error("Not connected to server"));
            return;
          }
          const id = generateId();
          state.pendingRequests.set(id, { resolve, reject });
          state.socket.send(
            JSON.stringify({
              id,
              type: "message",
              channel: state.channel,
              message: {
                id,
                command,
                params,
              },
            })
          );
          // Set timeout to reject the promise after 30 seconds
          setTimeout(() => {
            if (state.pendingRequests.has(id)) {
              state.pendingRequests.delete(id);
              reject(new Error("Request timed out"));
            }
          }, 30000);
        });
      }
      // Send success response back to WebSocket
      function sendSuccessResponse(id, result) {
        if (!state.connected || !state.socket) {
          console.error("Cannot send response: socket not connected");
          return;
        }
        state.socket.send(
          JSON.stringify({
            id,
            type: "message",
            channel: state.channel,
            message: {
              id,
              result,
            },
          })
        );
      }
      // Send error response back to WebSocket
      function sendErrorResponse(id, errorMessage) {
        if (!state.connected || !state.socket) {
          console.error("Cannot send error response: socket not connected");
          return;
        }
        state.socket.send(
          JSON.stringify({
            id,
            type: "message",
            channel: state.channel,
            message: {
              id,
              error: errorMessage,
            },
          })
        );
      }
      // Helper to generate unique IDs
      function generateId() {
        return (
          Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
        );
      }
      // Helper to generate channel names
      function generateChannelName() {
        const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
        let result = "";
        for (let i = 0; i < 8; i++) {
          result += characters.charAt(
            Math.floor(Math.random() * characters.length)
          );
        }
        return result;
      }
      // Function to open URLs
      function openURL(url) {
        window.postMessage("open-url", { url });
      }
      // Function to update selection list
      function updateSelectionList(selection) {
        selectionList.innerHTML = "";
        
        if (!selection || !selection.length) {
          selectionList.innerHTML = "<p>No layers selected</p>";
          return;
        }
        
        selection.forEach(item => {
          const itemElement = document.createElement("div");
          itemElement.className = "selection-item";
          
          const header = document.createElement("div");
          header.className = "selection-item-header";
          
          const name = document.createElement("span");
          name.textContent = item.name;
          
          const type = document.createElement("span");
          type.className = "selection-item-type";
          type.textContent = item.type;
          
          header.appendChild(name);
          header.appendChild(type);
          
          const details = document.createElement("div");
          details.className = "selection-item-details";
          details.textContent = `ID: ${item.id}`;
          
          itemElement.appendChild(header);
          itemElement.appendChild(details);
          selectionList.appendChild(itemElement);
        });
      }
      // Function to copy selection IDs
      function copySelectionIDs() {
        window.postMessage("copy-selection", {});
      }
      // Function to refresh selection
      function refreshSelection() {
        window.postMessage("refresh-selection", {});
      }
      // Tab switching
      tabs.forEach((tab) => {
        tab.addEventListener("click", () => {
          tabs.forEach((t) => t.classList.remove("active"));
          tabContents.forEach((c) => c.classList.remove("active"));
          tab.classList.add("active");
          const contentId = "content-" + tab.id.split("-")[1];
          document.getElementById(contentId).classList.add("active");
          
          // If selection tab is opened, refresh the selection
          if (tab.id === "tab-selection") {
            refreshSelection();
          }
        });
      });
      // Connect to server
      connectButton.addEventListener("click", () => {
        const port = parseInt(portInput.value, 10) || 3333;
        updateConnectionStatus(false, "Connecting...");
        connectionStatus.className = "status info";
        connectToServer(port);
      });
      // Disconnect from server
      disconnectButton.addEventListener("click", () => {
        updateConnectionStatus(false, "Disconnecting...");
        connectionStatus.className = "status info";
        disconnectFromServer();
      });
      // Copy selection IDs
      copyIdsButton.addEventListener("click", copySelectionIDs);
      
      // Refresh selection
      refreshSelectionButton.addEventListener("click", refreshSelection);
      // Communication with the plugin code
      window.addEventListener("message", (event) => {
        if (event.data && event.data.pluginMessage) {
          const message = event.data.pluginMessage;
          
          switch (message.type) {
            case "selection-updated":
              updateSelectionList(message.selection);
              break;
            case "connection-status":
              updateConnectionStatus(message.connected, message.message);
              break;
            case "command-result":
              // Forward the result from plugin code back to WebSocket
              sendSuccessResponse(message.id, message.result);
              break;
            case "command-error":
              // Forward the error from plugin code back to WebSocket
              sendErrorResponse(message.id, message.error);
              break;
          }
        }
      });
      // Helper function to send messages to the plugin code
      window.postMessage = function(type, data) {
        window.webkit.messageHandlers.sketchPlugin.postMessage({
          type: type,
          ...data
        });
      };
    </script>
  </body>
</html>