mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
0ae40e73a4
The v0.27.5 comment-only ruling removed every label affordance from HTML and live-app annotate surfaces, leaving no one-click positive feedback: the only path was opening the composer and typing prose. Restore exactly ONE affordance, the hardcoded 'Looks good' thumbs-up, on both input routes: - selection toolbar: commentOnly + a provided onQuickLabel now renders only the thumbs-up (no Delete, no Zap picker, Alt+digit suppressed); HtmlViewer passes a handler that filters by label id as defense in depth - pinpoint: the composer gains an optional one-click 'Looks good' footer action (disabled once anything is typed, so it can never discard a draft), emitting the same isQuickLabel comment shape with the draft's multi-select targets The trust-boundary clamp is untouched: redline/quickLabel modes stay collapsed to selection, so a hostile page still cannot force a DELETION or an arbitrary label. THUMBS_UP_LABEL moves to utils/quickLabels as the canonical definition.
91 lines
5.2 KiB
TypeScript
91 lines
5.2 KiB
TypeScript
/**
|
|
* Quick Labels — preset annotation labels for one-click feedback
|
|
*
|
|
* Labels are stored in cookies (same pattern as other settings)
|
|
* so they persist across different port-based sessions.
|
|
*/
|
|
|
|
import { storage } from './storage';
|
|
|
|
const STORAGE_KEY = 'plannotator-quick-labels';
|
|
|
|
export interface QuickLabel {
|
|
id: string; // kebab-case identifier e.g. "needs-tests"
|
|
emoji: string; // single emoji e.g. "🧪"
|
|
text: string; // display text e.g. "Needs tests"
|
|
color: string; // key into LABEL_COLOR_MAP
|
|
tip?: string; // optional instruction injected into feedback for the agent
|
|
}
|
|
|
|
/** Inline styles for label colors (avoids Tailwind dynamic class purging) */
|
|
export const LABEL_COLOR_MAP: Record<string, { bg: string; text: string; darkText: string }> = {
|
|
blue: { bg: 'rgba(59,130,246,0.15)', text: '#2563eb', darkText: '#60a5fa' },
|
|
red: { bg: 'rgba(239,68,68,0.15)', text: '#dc2626', darkText: '#f87171' },
|
|
orange: { bg: 'rgba(249,115,22,0.15)', text: '#ea580c', darkText: '#fb923c' },
|
|
yellow: { bg: 'rgba(234,179,8,0.15)', text: '#ca8a04', darkText: '#facc15' },
|
|
purple: { bg: 'rgba(147,51,234,0.15)', text: '#9333ea', darkText: '#a78bfa' },
|
|
teal: { bg: 'rgba(20,184,166,0.15)', text: '#0d9488', darkText: '#2dd4bf' },
|
|
pink: { bg: 'rgba(236,72,153,0.15)', text: '#db2777', darkText: '#f472b6' },
|
|
green: { bg: 'rgba(34,197,94,0.15)', text: '#16a34a', darkText: '#4ade80' },
|
|
cyan: { bg: 'rgba(8,145,178,0.15)', text: '#0891b2', darkText: '#22d3ee' },
|
|
amber: { bg: 'rgba(180,83,9,0.15)', text: '#b45309', darkText: '#fbbf24' },
|
|
};
|
|
|
|
/**
|
|
* The hardcoded one-click positive label behind the toolbar's 👍 button and
|
|
* the composer's "Looks good" action. Deliberately NOT part of the
|
|
* configurable set: it is the ONLY label comment-only surfaces (HTML /
|
|
* live-app) may emit — their restricted handlers filter on this id.
|
|
*/
|
|
export const THUMBS_UP_LABEL: QuickLabel = {
|
|
id: 'thumbs-up',
|
|
emoji: '👍',
|
|
text: 'Looks good',
|
|
color: 'green',
|
|
};
|
|
|
|
export const DEFAULT_QUICK_LABELS: QuickLabel[] = [
|
|
{ id: 'clarify-this', emoji: '❓', text: 'Clarify this', color: 'yellow' },
|
|
{ id: 'missing-overview', emoji: '🗺️', text: 'Missing overview', color: 'purple', tip: 'Provide a narrative overview of what is being built, why it is being built, and how it will be built. Add this before the implementation details.' },
|
|
{ id: 'verify-this', emoji: '🔍', text: 'Verify this', color: 'orange', tip: 'This seems like an assumption. Verify by reading the actual code before proceeding.' },
|
|
{ id: 'give-me-an-example', emoji: '🔬', text: 'Give me an example', color: 'cyan', tip: 'This is too abstract. Show a before/after, a sample input/output, or a specific scenario so I can see how this actually works.' },
|
|
{ id: 'match-existing-patterns', emoji: '🧬', text: 'Match existing patterns', color: 'teal', tip: 'Search the codebase for existing patterns, components, or utilities that already solve this. Reuse what exists rather than introducing a new approach.' },
|
|
{ id: 'consider-alternatives', emoji: '🔄', text: 'Consider alternatives', color: 'pink', tip: 'Propose 2-3 alternative approaches with trade-offs based on the actual codebase. Also check the Plannotator plans directory (PLANNOTATOR_DATA_DIR or ~/.plannotator/plans/) for prior plan versions that may have already explored or rejected similar approaches.' },
|
|
{ id: 'ensure-no-regression', emoji: '📉', text: 'Ensure no regression', color: 'amber', tip: 'Verify that this change will not break existing behavior. Identify what could regress and how to protect against it.' },
|
|
{ id: 'out-of-scope', emoji: '🚫', text: 'Out of scope', color: 'red', tip: 'This is not part of the current task. Remove it and stay focused on what was actually requested.' },
|
|
{ id: 'needs-tests', emoji: '🧪', text: 'Needs tests', color: 'blue' },
|
|
{ id: 'nice-approach', emoji: '👍', text: 'Nice approach', color: 'green' },
|
|
];
|
|
|
|
export function getQuickLabels(): QuickLabel[] {
|
|
const raw = storage.getItem(STORAGE_KEY);
|
|
if (!raw) return DEFAULT_QUICK_LABELS;
|
|
try {
|
|
const parsed = JSON.parse(raw) as QuickLabel[];
|
|
return parsed.length > 0 ? parsed : DEFAULT_QUICK_LABELS;
|
|
} catch {
|
|
return DEFAULT_QUICK_LABELS;
|
|
}
|
|
}
|
|
|
|
export function saveQuickLabels(labels: QuickLabel[]): void {
|
|
storage.setItem(STORAGE_KEY, JSON.stringify(labels));
|
|
}
|
|
|
|
export function resetQuickLabels(): void {
|
|
storage.removeItem(STORAGE_KEY);
|
|
}
|
|
|
|
/** Find a configured label whose "emoji text" matches an annotation's text field */
|
|
export function findLabelByText(annotationText: string): QuickLabel | undefined {
|
|
return getQuickLabels().find(l => `${l.emoji} ${l.text}` === annotationText);
|
|
}
|
|
|
|
/** Get color styles for a label, respecting dark mode */
|
|
export function getLabelColors(color: string): { bg: string; text: string } {
|
|
const colors = LABEL_COLOR_MAP[color];
|
|
if (!colors) return { bg: 'rgba(128,128,128,0.15)', text: '#666' };
|
|
const isDark = document.documentElement.classList.contains('dark');
|
|
return { bg: colors.bg, text: isDark ? colors.darkText : colors.text };
|
|
}
|