diff --git a/src/renderer/components/Feedback/FeedbackPanel.css b/src/renderer/components/Feedback/FeedbackPanel.css index 81e3750..bb8e9df 100644 --- a/src/renderer/components/Feedback/FeedbackPanel.css +++ b/src/renderer/components/Feedback/FeedbackPanel.css @@ -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; +} diff --git a/src/renderer/components/Feedback/FeedbackPanel.tsx b/src/renderer/components/Feedback/FeedbackPanel.tsx index 723328c..4664d87 100644 --- a/src/renderer/components/Feedback/FeedbackPanel.tsx +++ b/src/renderer/components/Feedback/FeedbackPanel.tsx @@ -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 ( +
+
+
+ {typeName} + {status} +
+ {onRemove && ( + + )} +
+ "{ann.matchedText}" + {ann.suggestion && ( + → {ann.suggestion} + )} +
+ ) +} + 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 (
@@ -177,34 +226,67 @@ export function FeedbackPanel(): JSX.Element { return (
-
- - -
+ {hasActive && ( + <> +
+ + +
-
- {annotations.map(ann => ( - { cancelPendingDismiss(ann.id); removeAnnotation(ann.id); tooltipAnalysisCache.delete(ann.id) }} - /> - ))} -
+
+ {annotations.map(ann => ( + { cancelPendingDismiss(ann.id); removeAnnotation(ann.id); tooltipAnalysisCache.delete(ann.id) }} + /> + ))} +
+ + )} + + {hasArchive && ( +
+ + + Archive + {archivedAnnotations.length} + + {dismissedCount > 0 && ( + + )} + +
+ {archivedAnnotations.map(ann => ( + removeArchivedAnnotation(ann.id) : undefined} + /> + ))} +
+
+ )}
) } diff --git a/src/renderer/store/editorStore.ts b/src/renderer/store/editorStore.ts index f7c2541..a2b8d18 100644 --- a/src/renderer/store/editorStore.ts +++ b/src/renderer/store/editorStore.ts @@ -41,6 +41,8 @@ interface EditorState { clearAnnotations: () => void markAnnotationApplied: (id: string) => void linkAnnotationsToMessage: (messageId: string, annotationIds: string[]) => void + clearArchivedAnnotations: () => void + removeArchivedAnnotation: (id: string) => void // Analysis mode analysisMode: AnalysisMode @@ -140,7 +142,7 @@ export const useEditorStore = create((set, get) => ({ activeFileContent: content, isDirty: false, chatHistory: existing, - annotations: savedAnnotationState?.annotations.filter(a => !a.applied) ?? [], + annotations: savedAnnotationState?.annotations.filter(a => !a.applied && !a.dismissed) ?? [], analysisMode: savedAnnotationState?.mode ?? 'none' }) scheduleSave(() => { @@ -282,11 +284,11 @@ export const useEditorStore = create((set, get) => ({ annotationsByFile: {}, setAnnotations: (annotations) => { set((s) => { - // Preserve previously applied annotations so chat history links remain valid - const existingApplied = s.activeFilePath - ? (s.annotationsByFile[s.activeFilePath]?.annotations ?? []).filter(a => a.applied) + // Preserve applied and dismissed annotations so chat history links and archive remain intact + const existingArchived = s.activeFilePath + ? (s.annotationsByFile[s.activeFilePath]?.annotations ?? []).filter(a => a.applied || a.dismissed) : [] - const merged = [...existingApplied, ...annotations] + const merged = [...existingArchived, ...annotations] const annotationsByFile = s.activeFilePath ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations: merged } } : s.annotationsByFile @@ -306,8 +308,12 @@ export const useEditorStore = create((set, get) => ({ removeAnnotation: (id) => { set((s) => { const annotations = s.annotations.filter((a) => a.id !== id) + const fileAnnotations = s.activeFilePath + ? (s.annotationsByFile[s.activeFilePath]?.annotations ?? []) + : [] + const updatedAll = fileAnnotations.map(a => a.id === id ? { ...a, dismissed: true } : a) const annotationsByFile = s.activeFilePath - ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations } } + ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations: updatedAll } } : s.annotationsByFile return { annotations, annotationsByFile } }) @@ -324,9 +330,16 @@ export const useEditorStore = create((set, get) => ({ }, clearAnnotations: () => { set((s) => { - const annotationsByFile = s.activeFilePath - ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: 'none' as AnalysisMode, annotations: [] } } - : s.annotationsByFile + if (!s.activeFilePath) return { annotations: [], analysisMode: 'none' as AnalysisMode } + const fileState = s.annotationsByFile[s.activeFilePath] + // Archive active annotations as dismissed; leave already-archived items untouched + const updatedAll = (fileState?.annotations ?? []).map(a => + !a.applied && !a.dismissed ? { ...a, dismissed: true } : a + ) + const annotationsByFile = { + ...s.annotationsByFile, + [s.activeFilePath]: { mode: 'none' as AnalysisMode, annotations: updatedAll } + } return { annotations: [], analysisMode: 'none', annotationsByFile } }) scheduleSave(() => { @@ -392,6 +405,54 @@ export const useEditorStore = create((set, get) => ({ }) }, + clearArchivedAnnotations: () => { + set((s) => { + if (!s.activeFilePath) return {} + const fileState = s.annotationsByFile[s.activeFilePath] + if (!fileState) return {} + return { + annotationsByFile: { + ...s.annotationsByFile, + [s.activeFilePath]: { ...fileState, annotations: fileState.annotations.filter(a => !a.dismissed) } + } + } + }) + scheduleSave(() => { + const st = get() + return { + activeFilePath: st.activeFilePath, + scrollPositions: st.scrollPositions, + chatSessionsByFile: st.chatSessionsByFile, + activeSessionIdByFile: st.activeSessionIdByFile, + annotationsByFile: st.annotationsByFile + } + }) + }, + + removeArchivedAnnotation: (id) => { + set((s) => { + if (!s.activeFilePath) return {} + const fileState = s.annotationsByFile[s.activeFilePath] + if (!fileState) return {} + return { + annotationsByFile: { + ...s.annotationsByFile, + [s.activeFilePath]: { ...fileState, annotations: fileState.annotations.filter(a => a.id !== id) } + } + } + }) + scheduleSave(() => { + const st = get() + return { + activeFilePath: st.activeFilePath, + scrollPositions: st.scrollPositions, + chatSessionsByFile: st.chatSessionsByFile, + activeSessionIdByFile: st.activeSessionIdByFile, + annotationsByFile: st.annotationsByFile + } + }) + }, + analysisMode: 'none', setAnalysisMode: (analysisMode) => { set((s) => { diff --git a/src/renderer/types/editor.ts b/src/renderer/types/editor.ts index 4a4e575..be8d689 100644 --- a/src/renderer/types/editor.ts +++ b/src/renderer/types/editor.ts @@ -39,6 +39,7 @@ export interface TextAnnotation { message: string suggestion?: string applied?: boolean // true when the suggestion has been applied to the document + dismissed?: boolean // true when the user dismissed this annotation (archived) } export type AnalysisMode = 'none' | 'passive_voice' | 'consistency' | 'style' | 'critique'