custom feedback

This commit is contained in:
2026-02-27 21:48:16 +10:00
parent 00a44191df
commit 1116134e84
7 changed files with 69 additions and 17 deletions

View File

@@ -93,12 +93,23 @@ Name the single most important thing to fix in a revision of this chapter.`
if (payload.attachments && payload.attachments.length > 0) {
const names = payload.attachments.map((a) => a.name).join(', ')
prompt += `\n\nThe user has attached the following reference file(s): ${names}.
When suggesting edits based on the attached material, identify exact passages in the chapter and use this format for each suggestion:
ISSUE: [category of improvement]
PASSAGE: "[exact quoted text from the chapter]"
PROBLEM: [brief explanation of why this needs changing]
SUGGESTION: "[revised text]"`
prompt += `\n\n---
ATTACHED REFERENCE MATERIAL: ${names}
The user has provided reference file(s) above. Based on these references and the user's instruction, identify specific passages in the chapter that should be changed.
You MUST output every suggested edit in this exact structured format — no other format will create highlights in the editor:
ISSUE: [short category label, e.g. "Tone", "Vocabulary", "Style Match", "Pacing"]
PASSAGE: "[copy the exact verbatim text from the chapter — at least 10 characters, unique enough to be found]"
PROBLEM: [one sentence explaining why this passage needs changing in light of the reference material]
SUGGESTION: "[the revised replacement text]"
Rules:
- PASSAGE must be a verbatim copy of text that exists in the chapter above. Do not paraphrase.
- Include as many ISSUE/PASSAGE/PROBLEM/SUGGESTION blocks as are genuinely warranted.
- After all structured blocks, you may add a brief overall summary.
---`
}
return prompt

View File

@@ -68,11 +68,14 @@ export function ChatPanel(): JSX.Element {
}
)
// After streaming, parse AI response for annotations
// After streaming, parse AI response for annotations.
// Attachment-driven messages are tagged 'custom' so they appear with a
// distinct visual treatment in the Feedback panel.
const currentHistory = useEditorStore.getState().chatHistory
const lastMsg = currentHistory[currentHistory.length - 1]
if (lastMsg?.role === 'assistant' && lastMsg.content.length > 0) {
const parsed = parseAnnotationsFromAIResponse(lastMsg.content, activeFileContent)
const overrideType = attachments.length > 0 ? 'custom' as const : undefined
const parsed = parseAnnotationsFromAIResponse(lastMsg.content, activeFileContent, overrideType)
if (parsed.length > 0) {
setAnnotations(parsed)
}

View File

@@ -123,6 +123,14 @@
color: var(--text-primary);
}
/* Wrapper that holds the type badge + optional source tag side by side */
.fb-card-header-badges {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
/* Coloured type badge */
.fb-card-badge {
font-size: 10px;
@@ -137,6 +145,20 @@
align-self: flex-start;
}
/* Small label shown on attachment-driven (custom) annotations */
.fb-card-source-tag {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(30, 200, 150, 0.75);
border: 1px solid rgba(30, 200, 150, 0.3);
border-radius: 3px;
padding: 1px 5px;
align-self: flex-start;
white-space: nowrap;
}
/* Passage excerpt — wraps so the full text is always visible */
.fb-card-excerpt {
font-size: 12px;

View File

@@ -23,6 +23,7 @@ function badgeColor(type: TextAnnotation['type']): string {
case 'consistency': return 'rgba(220, 80, 80, 0.75)'
case 'style': return 'rgba(80, 160, 255, 0.75)'
case 'critique': return 'rgba(160, 80, 220, 0.75)'
case 'custom': return 'rgba(30, 200, 150, 0.8)'
}
}
@@ -41,6 +42,11 @@ function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.E
const [state, setState] = useState<AnalysisState>(() => {
const cached = tooltipAnalysisCache.get(ann.id)
if (cached) return { status: 'done', text: cached.text, suggestion: cached.suggestion }
// Custom (attachment-driven) annotations already carry their analysis — show
// the problem description and suggestion immediately without an extra AI call.
if (ann.type === 'custom') {
return { status: 'done', text: ann.message, suggestion: ann.suggestion ?? null }
}
return { status: 'idle' }
})
@@ -85,7 +91,12 @@ function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.E
{/* Header — click to jump to passage in editor */}
<div className="fb-card-header" onClick={() => scrollToAnnotation(ann)} title="Jump to passage">
<div className="fb-card-header-top">
<span className="fb-card-badge">{typeName}</span>
<div className="fb-card-header-badges">
<span className="fb-card-badge">{typeName}</span>
{ann.type === 'custom' && (
<span className="fb-card-source-tag">from attachment</span>
)}
</div>
<button
className="fb-card-dismiss"
onClick={(e) => { e.stopPropagation(); onDismiss() }}
@@ -135,7 +146,7 @@ function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.E
}
export function FeedbackPanel(): JSX.Element {
const { annotations, setAnnotations, removeAnnotation } = useEditorStore()
const { annotations, clearAnnotations, removeAnnotation } = useEditorStore()
const [analyseAll, setAnalyseAll] = useState(false)
// Reset "Analyse all" whenever the annotation set changes (new critique run),
@@ -149,7 +160,7 @@ export function FeedbackPanel(): JSX.Element {
}, [annotations])
function handleClearAll(): void {
setAnnotations([])
clearAnnotations()
tooltipAnalysisCache.clear()
}

View File

@@ -2,6 +2,7 @@ import { useEffect } from 'react'
import { useEditorStore } from '../../store/editorStore'
import { detectPassiveVoice } from '../../utils/passiveVoice'
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
import { tooltipAnalysisCache } from '../Editor/MarkdownEditor'
import './Toolbar.css'
function countWords(text: string): number {
@@ -167,7 +168,7 @@ export function AnalysisToolbar(): JSX.Element {
{annotations.length > 0 && (
<button
className="toolbar-btn toolbar-btn-clear"
onClick={clearAnnotations}
onClick={() => { clearAnnotations(); tooltipAnalysisCache.clear() }}
title="Remove all highlights"
>
Clear

View File

@@ -21,7 +21,7 @@ export interface ChatMessage {
attachments?: AttachmentMeta[] // metadata only — stored in history for display
}
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'critique'
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'critique' | 'custom'
export interface TextAnnotation {
id: string

View File

@@ -1,9 +1,12 @@
import type { TextAnnotation, AnnotationType } from '../types/editor'
// Attempt to locate AI-quoted text in the document and create highlight annotations
// Attempt to locate AI-quoted text in the document and create highlight annotations.
// Pass `overrideType` to force every resulting annotation to use that type (e.g. 'custom'
// for attachment-driven feedback) instead of inferring it from surrounding context.
export function parseAnnotationsFromAIResponse(
aiResponse: string,
documentContent: string
documentContent: string,
overrideType?: AnnotationType
): TextAnnotation[] {
const annotations: TextAnnotation[] = []
let id = 0
@@ -33,10 +36,11 @@ export function parseAnnotationsFromAIResponse(
)
if (alreadyAnnotated) continue
// Determine annotation type from context around the quote in the AI response
// Determine annotation type — use override when provided (e.g. 'custom' for
// attachment-driven feedback), otherwise infer from context around the quote.
const contextStart = Math.max(0, match.index - 200)
const contextBefore = aiResponse.slice(contextStart, match.index).toLowerCase()
const type = classifyType(contextBefore)
const type = overrideType ?? classifyType(contextBefore)
// Try to extract a suggestion from text after the quote
const afterQuote = aiResponse.slice(match.index + match[0].length, match.index + match[0].length + 400)