✨ feedback archives
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user