UNPKG

@withpica/mcp-server

Version:

MCP Server for withPICA — enables AI assistants to interact with your withPICA music catalog

280 lines (258 loc) • 8.52 kB
// Copyright (c) 2025-2026 Withpica Ltd. All rights reserved. /** * pica_upload MCP App card — generic drop zone. * * Renders a drop zone + file input, instantiates createUploadManager() from * shared.ts, and reports uploaded file metadata back to the agent via * app.updateModelContext() once all files have settled. * * DOM API only — never innerHTML. */ import { buildAppHtml } from "./shared.js"; const UPLOAD_BODY = ` <div class="stack gap-6"> <header> <h1 class="heading heading-lg" id="up-title">upload files</h1> <p class="text-secondary" id="up-subtitle">drop files here or click to browse</p> </header> <section> <div id="up-dropzone" class="up-dropzone"> <p class="text-secondary" id="up-dropzone-label">drop files here or click to browse</p> <input type="file" id="up-input" multiple style="display: none;" /> </div> <div id="up-thumbnails" class="up-thumbnails"></div> <p class="text-tertiary" id="up-max-msg" style="margin-top: 8px; display: none;"></p> <p class="text-tertiary" id="up-status" style="margin-top: 12px; display: none;"></p> </section> </div> <style> .up-dropzone { background: rgba(255,255,255,0.03); border: 1px dashed rgba(255,255,255,0.15); border-radius: 12px; padding: 64px 24px; text-align: center; cursor: pointer; transition: background 0.15s, border-color 0.15s; } .up-dropzone.dragover { background: rgba(184,115,51,0.05); border-color: var(--accent); } .up-thumbnails { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px; } .up-thumb { position: relative; min-width: 120px; max-width: 180px; padding: 12px; border-radius: 8px; border: 1px solid var(--card-border); background: rgba(255,255,255,0.03); font-size: 12px; } .up-thumb-filename { color: var(--text-primary); word-break: break-all; margin-bottom: 4px; } .up-thumb-state { color: var(--text-tertiary); font-size: 11px; } .up-thumb-state-failed { border-color: rgba(255,80,80,0.5); } .up-thumb-state-network_error { border-color: rgba(255,165,0,0.4); } .up-thumb-state-uploaded { border-color: rgba(135,169,107,0.5); } .up-thumb-remove { position: absolute; top: 4px; right: 4px; width: 18px; height: 18px; border-radius: 50%; background: rgba(0,0,0,0.6); color: white; font-size: 11px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; } @media (max-width: 600px) { .up-dropzone { padding: 40px 16px; } } </style> `; const UPLOAD_JS = ` var upState = { config: { accept: "any", purpose: null, max_files: null }, manager: null, reported: false, }; function upAcceptToAttr(accept) { switch (accept) { case "image": return "image/*"; case "audio": return "audio/*"; case "video": return "video/*"; case "document": return ".pdf,.doc,.docx,.txt,.csv,.xls,.xlsx,.rtf,.md"; default: return ""; } } function upRender(files) { var container = document.getElementById("up-thumbnails"); if (!container) return; container.textContent = ""; files.forEach(function (f) { var thumb = document.createElement("div"); thumb.className = "up-thumb up-thumb-state-" + f.state; var name = document.createElement("div"); name.className = "up-thumb-filename"; name.textContent = f.filename; thumb.appendChild(name); var stateEl = document.createElement("div"); stateEl.className = "up-thumb-state"; if (f.state === "uploading") stateEl.textContent = "uploading..."; else if (f.state === "uploaded") stateEl.textContent = "uploaded"; else if (f.state === "failed") stateEl.textContent = "failed — click to retry"; else if (f.state === "network_error") stateEl.textContent = "network error — check connection or use web dashboard"; else stateEl.textContent = f.state; thumb.appendChild(stateEl); if (f.state === "failed") { thumb.style.cursor = "pointer"; thumb.onclick = function () { upState.manager.retryFile(f.id); }; } var removeBtn = document.createElement("button"); removeBtn.className = "up-thumb-remove"; removeBtn.textContent = "\\u00D7"; removeBtn.onclick = function (e) { e.stopPropagation(); upState.manager.removeFile(f.id); }; thumb.appendChild(removeBtn); container.appendChild(thumb); }); upMaybeReport(files); } function upMaybeReport(files) { if (upState.reported) return; if (files.length === 0) return; var settled = files.every(function (f) { return f.state === "uploaded" || f.state === "failed" || f.state === "network_error"; }); if (!settled) return; var uploaded = files .filter(function (f) { return f.state === "uploaded"; }) .map(function (f) { return { id: f.upload_id || null, url: f.url, mime_type: f.mime_type, size: f.size, filename: f.filename, uploaded_at: f.uploaded_at, }; }); upState.reported = true; var status = document.getElementById("up-status"); if (status) { status.style.display = "block"; status.textContent = uploaded.length + " file(s) uploaded — returning to agent"; } if (app && typeof app.updateModelContext === "function") { try { app.updateModelContext({ context: [{ type: "text", text: JSON.stringify({ uploaded: uploaded }), }], }); } catch (err) { console.warn("[pica_upload] updateModelContext failed:", err); } } } function upEnforceMaxFiles(incoming) { var maxMsgEl = document.getElementById("up-max-msg"); if (maxMsgEl) { maxMsgEl.style.display = "none"; maxMsgEl.textContent = ""; } var max = upState.config.max_files; if (!max || !upState.manager) return incoming; var currentCount = upState.manager.getFiles().length; var remaining = max - currentCount; if (remaining <= 0) { if (maxMsgEl) { maxMsgEl.textContent = "max " + max + " files — extras ignored"; maxMsgEl.style.display = "block"; } return []; } if (incoming.length > remaining) { if (maxMsgEl) { maxMsgEl.textContent = "max " + max + " files — extras ignored"; maxMsgEl.style.display = "block"; } return Array.prototype.slice.call(incoming, 0, remaining); } return incoming; } function upSetup() { var dz = document.getElementById("up-dropzone"); var input = document.getElementById("up-input"); var title = document.getElementById("up-title"); var subtitle = document.getElementById("up-subtitle"); var dzLabel = document.getElementById("up-dropzone-label"); if (upState.config.purpose && title) { title.textContent = "upload: " + upState.config.purpose; } if (upState.config.accept && upState.config.accept !== "any" && subtitle) { subtitle.textContent = "drop " + upState.config.accept + " files here or click to browse"; if (dzLabel) dzLabel.textContent = "drop " + upState.config.accept + " files here"; } if (input) { input.accept = upAcceptToAttr(upState.config.accept); } if (!dz || !input) return; dz.addEventListener("click", function () { input.click(); }); input.addEventListener("change", function (e) { if (e.target.files && e.target.files.length) { var allowed = upEnforceMaxFiles(e.target.files); if (allowed.length) { upState.reported = false; upState.manager.addFiles(allowed); } } input.value = ""; }); dz.addEventListener("dragover", function (e) { e.preventDefault(); dz.classList.add("dragover"); }); dz.addEventListener("dragleave", function () { dz.classList.remove("dragover"); }); dz.addEventListener("drop", function (e) { e.preventDefault(); dz.classList.remove("dragover"); if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) { var allowed = upEnforceMaxFiles(e.dataTransfer.files); if (allowed.length) { upState.reported = false; upState.manager.addFiles(allowed); } } }); } window.onPicaToolResult = function (data) { if (data && data.config) { upState.config = { accept: data.config.accept || "any", purpose: data.config.purpose || null, max_files: data.config.max_files || null, }; } upState.manager = createUploadManager({ accept: upState.config.accept, onStateChange: upRender, app: app, }); upSetup(); }; `; export const UPLOAD_HTML = buildAppHtml("pica \u2014 upload", UPLOAD_BODY, UPLOAD_JS); //# sourceMappingURL=upload.js.map