"Sometimes it's hard to describe what you want in a text box. Ask for a throwaway editor for the exact thing you're working on — and always end with an export button that turns whatever you did in the UI back into something you can paste into the agent or commit."
Core constraints (non-negotiable)
Every page produced by the 09-editor paradigm must have a "copy result" button. Let all of the user's operations in the UI → a pasteable block of text → go back to the agent.
An "editor" with no export outlet = a toy, not a tool.
When to use
| User says | Which paradigm | Template |
|---|---|---|
| "help me prioritize / triage 20 tickets" | Drag-and-drop triage board | Self-built |
| "toggle a set of feature flags / configure some parameters" | Config editor | Self-built |
| "tune a prompt, I want to see a few samples" | Prompt tuner | ../../templates/prompt.html |
| "I want to edit in the UI and then copy it back" | Any | — |
Paradigm 9.1 · Drag-and-drop triage board
Spatial shape: N columns (typically 4: Now / Next / Later / Cut), each column a <ul>, cards can be dragged between columns. "Copy as markdown" export at the top.
HTML5 drag-and-drop key JS:
let dragged = null;
document.querySelectorAll('.card').forEach(c => {
c.draggable = true;
c.addEventListener('dragstart', () => { dragged = c; c.classList.add('dragging'); });
c.addEventListener('dragend', () => c.classList.remove('dragging'));
});
document.querySelectorAll('.column ul').forEach(col => {
col.addEventListener('dragover', e => {
e.preventDefault();
const after = [...col.querySelectorAll('.card:not(.dragging)')]
.find(c => e.clientY < c.getBoundingClientRect().top + c.offsetHeight / 2);
if (after) col.insertBefore(dragged, after);
else col.appendChild(dragged);
});
});
// 导出
document.querySelector('#copy-md').onclick = async () => {
const md = [...document.querySelectorAll('.column')].map(col => {
const title = col.querySelector('h2').textContent;
const items = [...col.querySelectorAll('.card')]
.map(c => `- ${c.querySelector('.card-title').textContent.trim()}`)
.join('\n');
return `## ${title}\n${items}`;
}).join('\n\n');
await navigator.clipboard.writeText(md);
document.querySelector('#copy-md').textContent = 'Copied ✓';
};
Key CSS(columns and drag states):
.board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.column { background: var(--gray-100); border-radius: 12px; padding: 16px;
min-height: 400px; }
.column h2 { font-size: 14px; text-transform: uppercase;
letter-spacing: 0.06em; color: var(--gray-700); margin: 0 0 12px; }
.column ul { list-style: none; padding: 0; margin: 0;
display: flex; flex-direction: column; gap: 8px; min-height: 100px; }
.card { background: var(--white); border: 1px solid var(--gray-300);
border-radius: 8px; padding: 12px; cursor: grab; }
.card.dragging { opacity: 0.4; cursor: grabbing; }
.card:hover { border-color: var(--clay); }
No standalone template (the data varies per person, generate as needed).
Paradigm 9.2 · Config editor (feature flag editor)
Spatial shape: grouped fieldset, each flag is a toggle.change event checks dependencies (if A is on, B must be on too). "Copy diff" outputsonly the changed keys。
Key JS(diff output):
function readState() {
const state = {};
document.querySelectorAll('input[type="checkbox"][data-flag]').forEach(el => {
state[el.dataset.flag] = el.checked;
});
return state;
}
const initial = readState();
// 依赖检查
document.addEventListener('change', () => {
document.querySelectorAll('[data-requires]').forEach(el => {
const reqs = el.dataset.requires.split(',');
const unmet = reqs.find(r => !readState()[r]);
el.parentElement.querySelector('.warning').hidden = !unmet || !el.checked;
if (unmet && el.checked) {
el.parentElement.querySelector('.warning').textContent =
`Requires ${unmet} to be enabled`;
}
});
});
document.querySelector('#copy-diff').onclick = async () => {
const now = readState();
const diff = Object.fromEntries(
Object.entries(now).filter(([k, v]) => initial[k] !== v)
);
if (Object.keys(diff).length === 0) {
document.querySelector('#copy-diff').textContent = 'No changes';
return;
}
await navigator.clipboard.writeText(JSON.stringify(diff, null, 2));
document.querySelector('#copy-diff').textContent = 'Copied ✓';
};
No standalone template (the flag set varies per project).
Paradigm 9.3 · Prompt tuner
Spatial shape: left <textarea>(template, including {{slot}}) + N sample input cards on the top right + N live-filled results on the bottom right. Typing re-renders immediately. "Copy prompt" outputs the current template.
Key JS:
const tpl = document.querySelector('#tpl');
const samples = JSON.parse(document.querySelector('#samples').textContent);
function fill(template, data) {
return template.replace(/\{\{(\w+)\}\}/g, (_, k) =>
data[k] != null ? data[k] : `{{${k}}}`
);
}
function render() {
document.querySelectorAll('.preview').forEach((p, i) => {
p.textContent = fill(tpl.value, samples[i]);
});
document.querySelector('#tpl-meta').textContent =
tpl.value.length + ' chars · ~' + Math.ceil(tpl.value.length / 4) + ' tokens';
}
tpl.addEventListener('input', render);
document.querySelector('#copy-tpl').onclick = async () => {
await navigator.clipboard.writeText(tpl.value);
};
render();
Sample data is embedded in the page (no external API calls):
<script type="application/json" id="samples">
[
{ "label": "Frustrated", "customer_name": "Mira", "tone": "calm", "issue": "..." },
{ "label": "Confused", "customer_name": "Devon", "tone": "warm", "issue": "..." }
]
</script>
Ready-to-use template:../../templates/prompt.html — includes 3 real samples and a live preview.
Generic export button template (standard implementation)
async function copyResult(text, btn) {
try {
await navigator.clipboard.writeText(text);
const old = btn.textContent;
btn.textContent = 'Copied ✓';
setTimeout(() => { btn.textContent = old; }, 1500);
} catch (e) {
btn.textContent = 'Press Ctrl+C';
// fallback: select hidden textarea + execCommand('copy')
}
}
Common principles
- Always have an export button— this is the hallmark of 09-editor; without it, it is not this paradigm
- Export format serves the agent— markdown / JSON diff / plain text, depending on how the user will use it afterward
- Real data mock— not "Sample 1, Sample 2", but "Frustrated · long-time user / Confused · new user"
- Dragging must have hover visual feedback——
.card.dragging { opacity: 0.4 }is the bottom line - The config editor's dependency warning must appear immediately— use the
changeevent, not submit - The prompt tuner's token count estimate uses chars/4—No real tokenizer needed