✨ custom feedback
This commit is contained in:
@@ -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) {
|
if (payload.attachments && payload.attachments.length > 0) {
|
||||||
const names = payload.attachments.map((a) => a.name).join(', ')
|
const names = payload.attachments.map((a) => a.name).join(', ')
|
||||||
prompt += `\n\nThe user has attached the following reference file(s): ${names}.
|
prompt += `\n\n---
|
||||||
When suggesting edits based on the attached material, identify exact passages in the chapter and use this format for each suggestion:
|
ATTACHED REFERENCE MATERIAL: ${names}
|
||||||
ISSUE: [category of improvement]
|
|
||||||
PASSAGE: "[exact quoted text from the chapter]"
|
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.
|
||||||
PROBLEM: [brief explanation of why this needs changing]
|
|
||||||
SUGGESTION: "[revised text]"`
|
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
|
return prompt
|
||||||
|
|||||||
@@ -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 currentHistory = useEditorStore.getState().chatHistory
|
||||||
const lastMsg = currentHistory[currentHistory.length - 1]
|
const lastMsg = currentHistory[currentHistory.length - 1]
|
||||||
if (lastMsg?.role === 'assistant' && lastMsg.content.length > 0) {
|
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) {
|
if (parsed.length > 0) {
|
||||||
setAnnotations(parsed)
|
setAnnotations(parsed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,14 @@
|
|||||||
color: var(--text-primary);
|
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 */
|
/* Coloured type badge */
|
||||||
.fb-card-badge {
|
.fb-card-badge {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -137,6 +145,20 @@
|
|||||||
align-self: flex-start;
|
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 */
|
/* Passage excerpt — wraps so the full text is always visible */
|
||||||
.fb-card-excerpt {
|
.fb-card-excerpt {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function badgeColor(type: TextAnnotation['type']): string {
|
|||||||
case 'consistency': return 'rgba(220, 80, 80, 0.75)'
|
case 'consistency': return 'rgba(220, 80, 80, 0.75)'
|
||||||
case 'style': return 'rgba(80, 160, 255, 0.75)'
|
case 'style': return 'rgba(80, 160, 255, 0.75)'
|
||||||
case 'critique': return 'rgba(160, 80, 220, 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 [state, setState] = useState<AnalysisState>(() => {
|
||||||
const cached = tooltipAnalysisCache.get(ann.id)
|
const cached = tooltipAnalysisCache.get(ann.id)
|
||||||
if (cached) return { status: 'done', text: cached.text, suggestion: cached.suggestion }
|
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' }
|
return { status: 'idle' }
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -85,7 +91,12 @@ function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.E
|
|||||||
{/* Header — click to jump to passage in editor */}
|
{/* 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" onClick={() => scrollToAnnotation(ann)} title="Jump to passage">
|
||||||
<div className="fb-card-header-top">
|
<div className="fb-card-header-top">
|
||||||
|
<div className="fb-card-header-badges">
|
||||||
<span className="fb-card-badge">{typeName}</span>
|
<span className="fb-card-badge">{typeName}</span>
|
||||||
|
{ann.type === 'custom' && (
|
||||||
|
<span className="fb-card-source-tag">from attachment</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
className="fb-card-dismiss"
|
className="fb-card-dismiss"
|
||||||
onClick={(e) => { e.stopPropagation(); onDismiss() }}
|
onClick={(e) => { e.stopPropagation(); onDismiss() }}
|
||||||
@@ -135,7 +146,7 @@ function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.E
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FeedbackPanel(): JSX.Element {
|
export function FeedbackPanel(): JSX.Element {
|
||||||
const { annotations, setAnnotations, removeAnnotation } = useEditorStore()
|
const { annotations, clearAnnotations, removeAnnotation } = useEditorStore()
|
||||||
const [analyseAll, setAnalyseAll] = useState(false)
|
const [analyseAll, setAnalyseAll] = useState(false)
|
||||||
|
|
||||||
// Reset "Analyse all" whenever the annotation set changes (new critique run),
|
// Reset "Analyse all" whenever the annotation set changes (new critique run),
|
||||||
@@ -149,7 +160,7 @@ export function FeedbackPanel(): JSX.Element {
|
|||||||
}, [annotations])
|
}, [annotations])
|
||||||
|
|
||||||
function handleClearAll(): void {
|
function handleClearAll(): void {
|
||||||
setAnnotations([])
|
clearAnnotations()
|
||||||
tooltipAnalysisCache.clear()
|
tooltipAnalysisCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
|
|||||||
import { useEditorStore } from '../../store/editorStore'
|
import { useEditorStore } from '../../store/editorStore'
|
||||||
import { detectPassiveVoice } from '../../utils/passiveVoice'
|
import { detectPassiveVoice } from '../../utils/passiveVoice'
|
||||||
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
|
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
|
||||||
|
import { tooltipAnalysisCache } from '../Editor/MarkdownEditor'
|
||||||
import './Toolbar.css'
|
import './Toolbar.css'
|
||||||
|
|
||||||
function countWords(text: string): number {
|
function countWords(text: string): number {
|
||||||
@@ -167,7 +168,7 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
{annotations.length > 0 && (
|
{annotations.length > 0 && (
|
||||||
<button
|
<button
|
||||||
className="toolbar-btn toolbar-btn-clear"
|
className="toolbar-btn toolbar-btn-clear"
|
||||||
onClick={clearAnnotations}
|
onClick={() => { clearAnnotations(); tooltipAnalysisCache.clear() }}
|
||||||
title="Remove all highlights"
|
title="Remove all highlights"
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export interface ChatMessage {
|
|||||||
attachments?: AttachmentMeta[] // metadata only — stored in history for display
|
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 {
|
export interface TextAnnotation {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { TextAnnotation, AnnotationType } from '../types/editor'
|
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(
|
export function parseAnnotationsFromAIResponse(
|
||||||
aiResponse: string,
|
aiResponse: string,
|
||||||
documentContent: string
|
documentContent: string,
|
||||||
|
overrideType?: AnnotationType
|
||||||
): TextAnnotation[] {
|
): TextAnnotation[] {
|
||||||
const annotations: TextAnnotation[] = []
|
const annotations: TextAnnotation[] = []
|
||||||
let id = 0
|
let id = 0
|
||||||
@@ -33,10 +36,11 @@ export function parseAnnotationsFromAIResponse(
|
|||||||
)
|
)
|
||||||
if (alreadyAnnotated) continue
|
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 contextStart = Math.max(0, match.index - 200)
|
||||||
const contextBefore = aiResponse.slice(contextStart, match.index).toLowerCase()
|
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
|
// 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)
|
const afterQuote = aiResponse.slice(match.index + match[0].length, match.index + match[0].length + 400)
|
||||||
|
|||||||
Reference in New Issue
Block a user