@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
JavaScript
// 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;
}
(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