feedback archives

This commit is contained in:
2026-03-01 15:43:00 +10:00
parent 70f8c00e5e
commit 0e584c5e03
4 changed files with 338 additions and 38 deletions

View File

@@ -252,3 +252,159 @@
.fb-card-apply:hover {
opacity: 0.85;
}
/* ── Archive section ─────────────────────────────────── */
.fb-archive {
flex-shrink: 0;
border-top: 1px solid var(--border);
}
.fb-archive-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 12px;
cursor: pointer;
list-style: none;
user-select: none;
color: var(--text-muted);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
transition: color 0.15s;
}
.fb-archive-header::-webkit-details-marker { display: none; }
.fb-archive-header:hover {
color: var(--text-secondary);
}
.fb-archive-title {
display: flex;
align-items: center;
gap: 6px;
}
.fb-archive-count {
background: var(--border);
color: var(--text-muted);
border-radius: 10px;
font-size: 10px;
font-weight: 700;
padding: 1px 6px;
letter-spacing: 0;
}
.fb-archive-clear-btn {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-muted);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
padding: 2px 7px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.fb-archive-clear-btn:hover {
color: #e07070;
border-color: rgba(200, 60, 60, 0.5);
}
.fb-archive-list {
display: flex;
flex-direction: column;
gap: 6px;
padding: 6px 12px 10px;
max-height: 260px;
overflow-y: auto;
}
/* ── Archive card ────────────────────────────────────── */
.fb-archive-card {
flex-shrink: 0;
border: 1px solid var(--border);
border-radius: 5px;
padding: 7px 10px;
background: var(--message-bg);
opacity: 0.7;
display: flex;
flex-direction: column;
gap: 4px;
}
.fb-archive-card:hover {
opacity: 1;
}
.fb-archive-card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.fb-archive-card-badges {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
/* "applied" / "dismissed" status tag */
.fb-archive-status {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
border-radius: 3px;
padding: 1px 5px;
white-space: nowrap;
align-self: flex-start;
}
.fb-archive-status--applied {
color: rgba(80, 200, 120, 0.85);
border: 1px solid rgba(80, 200, 120, 0.3);
}
.fb-archive-status--dismissed {
color: rgba(160, 160, 160, 0.75);
border: 1px solid rgba(160, 160, 160, 0.25);
}
.fb-archive-card-dismiss {
background: none;
border: none;
color: var(--text-muted);
font-size: 15px;
line-height: 1;
padding: 0 2px;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
flex-shrink: 0;
}
.fb-archive-card:hover .fb-archive-card-dismiss {
opacity: 1;
}
.fb-archive-card-dismiss:hover {
color: var(--text-primary);
}
.fb-archive-card-excerpt {
font-size: 11px;
color: var(--text-muted);
font-style: italic;
line-height: 1.5;
}
.fb-archive-card-suggestion {
font-size: 11px;
color: var(--text-secondary);
line-height: 1.5;
}

View File

@@ -145,10 +145,56 @@ function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.E
)
}
interface ArchiveCardProps {
ann: TextAnnotation
onRemove?: () => void
}
function ArchiveCard({ ann, onRemove }: ArchiveCardProps): JSX.Element {
const typeName = ann.type.replace(/_/g, ' ')
const status = ann.applied ? 'applied' : 'dismissed'
return (
<div
className={`fb-archive-card fb-archive-card--${status}`}
style={{ '--badge-color': badgeColor(ann.type) } as React.CSSProperties}
>
<div className="fb-archive-card-header">
<div className="fb-archive-card-badges">
<span className="fb-card-badge">{typeName}</span>
<span className={`fb-archive-status fb-archive-status--${status}`}>{status}</span>
</div>
{onRemove && (
<button className="fb-archive-card-dismiss" onClick={onRemove} title="Remove from archive">
×
</button>
)}
</div>
<span className="fb-archive-card-excerpt">"{ann.matchedText}"</span>
{ann.suggestion && (
<span className="fb-archive-card-suggestion"> {ann.suggestion}</span>
)}
</div>
)
}
export function FeedbackPanel(): JSX.Element {
const { annotations, clearAnnotations, removeAnnotation } = useEditorStore()
const {
annotations,
annotationsByFile,
activeFilePath,
clearAnnotations,
removeAnnotation,
clearArchivedAnnotations,
removeArchivedAnnotation,
} = useEditorStore()
const [analyseAll, setAnalyseAll] = useState(false)
const archivedAnnotations = activeFilePath
? (annotationsByFile[activeFilePath]?.annotations ?? []).filter(a => a.applied || a.dismissed)
: []
const dismissedCount = archivedAnnotations.filter(a => a.dismissed).length
// Reset "Analyse all" whenever the annotation set changes (new critique run),
// so auto-analysis doesn't carry over to fresh results unexpectedly.
const prevAnnotationsRef = useRef(annotations)
@@ -164,7 +210,10 @@ export function FeedbackPanel(): JSX.Element {
tooltipAnalysisCache.clear()
}
if (annotations.length === 0) {
const hasActive = annotations.length > 0
const hasArchive = archivedAnnotations.length > 0
if (!hasActive && !hasArchive) {
return (
<div className="fb-panel">
<div className="fb-empty">
@@ -177,34 +226,67 @@ export function FeedbackPanel(): JSX.Element {
return (
<div className="fb-panel">
<div className="fb-toolbar">
<button
className="fb-toolbar-btn"
onClick={() => setAnalyseAll(true)}
disabled={analyseAll}
title="Run AI analysis on all highlighted passages"
>
Analyse all
</button>
<button
className="fb-toolbar-btn fb-toolbar-btn--clear"
onClick={handleClearAll}
title="Remove all highlights"
>
Clear all
</button>
</div>
{hasActive && (
<>
<div className="fb-toolbar">
<button
className="fb-toolbar-btn"
onClick={() => setAnalyseAll(true)}
disabled={analyseAll}
title="Run AI analysis on all highlighted passages"
>
Analyse all
</button>
<button
className="fb-toolbar-btn fb-toolbar-btn--clear"
onClick={handleClearAll}
title="Archive all highlights"
>
Clear all
</button>
</div>
<div className="fb-list">
{annotations.map(ann => (
<FeedbackCard
key={ann.id}
ann={ann}
autoAnalyse={analyseAll}
onDismiss={() => { cancelPendingDismiss(ann.id); removeAnnotation(ann.id); tooltipAnalysisCache.delete(ann.id) }}
/>
))}
</div>
<div className="fb-list">
{annotations.map(ann => (
<FeedbackCard
key={ann.id}
ann={ann}
autoAnalyse={analyseAll}
onDismiss={() => { cancelPendingDismiss(ann.id); removeAnnotation(ann.id); tooltipAnalysisCache.delete(ann.id) }}
/>
))}
</div>
</>
)}
{hasArchive && (
<details className="fb-archive">
<summary className="fb-archive-header">
<span className="fb-archive-title">
Archive
<span className="fb-archive-count">{archivedAnnotations.length}</span>
</span>
{dismissedCount > 0 && (
<button
className="fb-archive-clear-btn"
onClick={(e) => { e.preventDefault(); clearArchivedAnnotations() }}
title="Permanently remove all dismissed items"
>
Clear dismissed
</button>
)}
</summary>
<div className="fb-archive-list">
{archivedAnnotations.map(ann => (
<ArchiveCard
key={ann.id}
ann={ann}
onRemove={ann.dismissed ? () => removeArchivedAnnotation(ann.id) : undefined}
/>
))}
</div>
</details>
)}
</div>
)
}