343 lines
9.6 KiB
TypeScript
343 lines
9.6 KiB
TypeScript
/**
|
|
* MI Triage Panel — floating preview UI for content triage proposals.
|
|
*
|
|
* Shows the AI summary, proposed shapes as cards with remove buttons,
|
|
* connections, and Create All / Cancel actions.
|
|
* Uses existing --rs-* CSS variables for theming.
|
|
*/
|
|
|
|
import type { TriageManager } from "./mi-content-triage";
|
|
|
|
/** Icon lookup by tagName — matches TOOL_HINTS from mi-tool-schema.ts */
|
|
const SHAPE_ICONS: Record<string, { icon: string; label: string }> = {
|
|
"folk-markdown": { icon: "📝", label: "Note" },
|
|
"folk-embed": { icon: "🔗", label: "Embed" },
|
|
"folk-calendar": { icon: "📅", label: "Calendar" },
|
|
"folk-map": { icon: "🗺️", label: "Map" },
|
|
"folk-workflow-block": { icon: "⚙️", label: "Workflow" },
|
|
"folk-social-post": { icon: "📣", label: "Social Post" },
|
|
"folk-choice-vote": { icon: "🗳️", label: "Vote" },
|
|
"folk-prompt": { icon: "🤖", label: "AI Chat" },
|
|
"folk-image-gen": { icon: "🎨", label: "AI Image" },
|
|
"folk-slide": { icon: "🖼️", label: "Slide" },
|
|
};
|
|
|
|
export class MiTriagePanel {
|
|
private el: HTMLDivElement;
|
|
private manager: TriageManager;
|
|
|
|
constructor(manager: TriageManager) {
|
|
this.manager = manager;
|
|
this.el = document.createElement("div");
|
|
this.el.className = "mi-triage-panel";
|
|
this.el.innerHTML = this.renderLoading();
|
|
this.injectStyles();
|
|
document.body.appendChild(this.el);
|
|
|
|
// Re-render when manager state changes
|
|
this.manager = manager;
|
|
const originalOnChange = (manager as any).onChange;
|
|
(manager as any).onChange = () => {
|
|
originalOnChange?.();
|
|
this.render();
|
|
};
|
|
}
|
|
|
|
private render() {
|
|
switch (this.manager.status) {
|
|
case "analyzing":
|
|
this.el.innerHTML = this.renderLoading();
|
|
break;
|
|
case "ready":
|
|
this.el.innerHTML = this.renderProposal();
|
|
this.bindEvents();
|
|
break;
|
|
case "error":
|
|
this.el.innerHTML = this.renderError();
|
|
this.bindCloseEvent();
|
|
break;
|
|
default:
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
private renderLoading(): string {
|
|
return `
|
|
<div class="mi-triage-header">
|
|
<span class="mi-triage-title">MI Content Triage</span>
|
|
</div>
|
|
<div class="mi-triage-body mi-triage-loading">
|
|
<div class="mi-triage-spinner"></div>
|
|
<p>Analyzing content...</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderError(): string {
|
|
return `
|
|
<div class="mi-triage-header">
|
|
<span class="mi-triage-title">MI Content Triage</span>
|
|
<button class="mi-triage-close" data-action="close">×</button>
|
|
</div>
|
|
<div class="mi-triage-body mi-triage-error">
|
|
<p>${this.manager.error || "Something went wrong"}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderProposal(): string {
|
|
const p = this.manager.proposal!;
|
|
const shapeCards = p.shapes
|
|
.map((s, i) => {
|
|
const info = SHAPE_ICONS[s.tagName] || { icon: "📦", label: s.tagName };
|
|
return `
|
|
<div class="mi-triage-card" data-index="${i}">
|
|
<div class="mi-triage-card-header">
|
|
<span class="mi-triage-card-icon">${info.icon}</span>
|
|
<span class="mi-triage-card-label">${escapeHtml(s.label)}</span>
|
|
<span class="mi-triage-card-badge">${info.label}</span>
|
|
<button class="mi-triage-card-remove" data-action="remove" data-index="${i}">×</button>
|
|
</div>
|
|
<div class="mi-triage-card-snippet">${escapeHtml(s.snippet || "")}</div>
|
|
</div>`;
|
|
})
|
|
.join("");
|
|
|
|
const connList =
|
|
p.connections.length > 0
|
|
? `<div class="mi-triage-connections">
|
|
<div class="mi-triage-conn-title">Connections</div>
|
|
${p.connections
|
|
.map((c) => {
|
|
const from = p.shapes[c.fromIndex]?.label || `#${c.fromIndex}`;
|
|
const to = p.shapes[c.toIndex]?.label || `#${c.toIndex}`;
|
|
return `<div class="mi-triage-conn">${escapeHtml(from)} → ${escapeHtml(to)}<span class="mi-triage-conn-reason">${escapeHtml(c.reason || "")}</span></div>`;
|
|
})
|
|
.join("")}
|
|
</div>`
|
|
: "";
|
|
|
|
return `
|
|
<div class="mi-triage-header">
|
|
<span class="mi-triage-title">MI Content Triage</span>
|
|
<button class="mi-triage-close" data-action="close">×</button>
|
|
</div>
|
|
<div class="mi-triage-summary">${escapeHtml(p.summary)}</div>
|
|
<div class="mi-triage-body">
|
|
${shapeCards}
|
|
${connList}
|
|
</div>
|
|
<div class="mi-triage-footer">
|
|
<button class="mi-triage-btn mi-triage-btn-cancel" data-action="close">Cancel</button>
|
|
<button class="mi-triage-btn mi-triage-btn-commit" data-action="commit">Create All (${p.shapes.length} shape${p.shapes.length !== 1 ? "s" : ""})</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private bindEvents() {
|
|
this.el.querySelectorAll("[data-action='remove']").forEach((btn) => {
|
|
btn.addEventListener("click", (e) => {
|
|
const idx = parseInt((e.currentTarget as HTMLElement).dataset.index || "0");
|
|
this.manager.removeShape(idx);
|
|
});
|
|
});
|
|
this.el.querySelector("[data-action='commit']")?.addEventListener("click", () => {
|
|
this.manager.commitAll();
|
|
this.close();
|
|
});
|
|
this.bindCloseEvent();
|
|
}
|
|
|
|
private bindCloseEvent() {
|
|
this.el.querySelectorAll("[data-action='close']").forEach((btn) => {
|
|
btn.addEventListener("click", () => this.close());
|
|
});
|
|
}
|
|
|
|
close() {
|
|
this.el.remove();
|
|
}
|
|
|
|
private injectStyles() {
|
|
if (document.getElementById("mi-triage-styles")) return;
|
|
const style = document.createElement("style");
|
|
style.id = "mi-triage-styles";
|
|
style.textContent = `
|
|
.mi-triage-panel {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 420px;
|
|
max-width: 90vw;
|
|
max-height: 80vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--rs-surface, #1e1e2e);
|
|
color: var(--rs-text, #cdd6f4);
|
|
border: 1px solid var(--rs-border, #45475a);
|
|
border-radius: 16px;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
|
z-index: 10000;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
font-size: 13px;
|
|
animation: mi-triage-in 0.2s ease-out;
|
|
}
|
|
@keyframes mi-triage-in {
|
|
from { opacity: 0; transform: translate(-50%, -48%); }
|
|
to { opacity: 1; transform: translate(-50%, -50%); }
|
|
}
|
|
.mi-triage-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 16px 10px;
|
|
border-bottom: 1px solid var(--rs-border, #45475a);
|
|
}
|
|
.mi-triage-title {
|
|
font-weight: 600;
|
|
font-size: 14px;
|
|
}
|
|
.mi-triage-close {
|
|
background: none;
|
|
border: none;
|
|
color: var(--rs-text-muted, #a6adc8);
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
padding: 0 4px;
|
|
line-height: 1;
|
|
}
|
|
.mi-triage-close:hover { color: var(--rs-text, #cdd6f4); }
|
|
.mi-triage-summary {
|
|
padding: 10px 16px;
|
|
font-size: 12px;
|
|
color: var(--rs-text-muted, #a6adc8);
|
|
border-bottom: 1px solid var(--rs-border, #45475a);
|
|
}
|
|
.mi-triage-body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.mi-triage-loading {
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 40px 16px;
|
|
}
|
|
.mi-triage-spinner {
|
|
width: 28px;
|
|
height: 28px;
|
|
border: 3px solid var(--rs-border, #45475a);
|
|
border-top-color: #14b8a6;
|
|
border-radius: 50%;
|
|
animation: mi-spin 0.8s linear infinite;
|
|
margin-bottom: 12px;
|
|
}
|
|
@keyframes mi-spin { to { transform: rotate(360deg); } }
|
|
.mi-triage-error {
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 32px 16px;
|
|
color: #f38ba8;
|
|
}
|
|
.mi-triage-card {
|
|
background: var(--rs-card-bg, #313244);
|
|
border: 1px solid var(--rs-border, #45475a);
|
|
border-radius: 10px;
|
|
padding: 10px 12px;
|
|
}
|
|
.mi-triage-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.mi-triage-card-icon { font-size: 16px; }
|
|
.mi-triage-card-label {
|
|
flex: 1;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.mi-triage-card-badge {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
border-radius: 6px;
|
|
background: var(--rs-badge-bg, #45475a);
|
|
color: var(--rs-text-muted, #a6adc8);
|
|
white-space: nowrap;
|
|
}
|
|
.mi-triage-card-remove {
|
|
background: none;
|
|
border: none;
|
|
color: var(--rs-text-muted, #a6adc8);
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
padding: 0 2px;
|
|
line-height: 1;
|
|
}
|
|
.mi-triage-card-remove:hover { color: #f38ba8; }
|
|
.mi-triage-card-snippet {
|
|
font-size: 11px;
|
|
color: var(--rs-text-muted, #a6adc8);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.mi-triage-connections {
|
|
padding: 8px 0 0;
|
|
border-top: 1px solid var(--rs-border, #45475a);
|
|
}
|
|
.mi-triage-conn-title {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--rs-text-muted, #a6adc8);
|
|
margin-bottom: 4px;
|
|
}
|
|
.mi-triage-conn {
|
|
font-size: 11px;
|
|
padding: 2px 0;
|
|
color: var(--rs-text, #cdd6f4);
|
|
}
|
|
.mi-triage-conn-reason {
|
|
margin-left: 6px;
|
|
color: var(--rs-text-muted, #a6adc8);
|
|
}
|
|
.mi-triage-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
padding: 12px 16px;
|
|
border-top: 1px solid var(--rs-border, #45475a);
|
|
}
|
|
.mi-triage-btn {
|
|
padding: 7px 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
}
|
|
.mi-triage-btn-cancel {
|
|
background: var(--rs-card-bg, #313244);
|
|
color: var(--rs-text, #cdd6f4);
|
|
}
|
|
.mi-triage-btn-cancel:hover { background: var(--rs-border, #45475a); }
|
|
.mi-triage-btn-commit {
|
|
background: #14b8a6;
|
|
color: white;
|
|
}
|
|
.mi-triage-btn-commit:hover { background: #0d9488; }
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|