show vs tell

This commit is contained in:
2026-03-02 18:29:00 +10:00
parent ad2c243849
commit d2fe2daa9b
7 changed files with 93 additions and 11 deletions

View File

@@ -98,8 +98,10 @@ export function analyseAnnotation(
ann: TextAnnotation,
onUpdate: (text: string, streaming: boolean, suggestion: string | null) => void
): () => void {
const cached = tooltipAnalysisCache.get(ann.id)
const cached = tooltipAnalysisCache.get(ann.id) ?? ann.analysisCache ?? null
if (cached) {
// Warm the in-memory cache so subsequent calls this session are instant
if (!tooltipAnalysisCache.has(ann.id)) tooltipAnalysisCache.set(ann.id, cached)
onUpdate(cached.text, false, cached.suggestion)
return () => {}
}
@@ -131,6 +133,7 @@ export function analyseAnnotation(
const text = accumulated || ann.message
const suggestion = extractBlockquote(text) ?? ann.suggestion ?? null
tooltipAnalysisCache.set(ann.id, { text, suggestion })
useEditorStore.getState().setAnnotationAnalysis(ann.id, { text, suggestion })
onUpdate(text, false, suggestion)
}).catch(() => {
if (!cancelled) onUpdate(ann.message, false, ann.suggestion ?? null)
@@ -363,6 +366,11 @@ function buildTheme(fontSize: number, dark: boolean): ReturnType<typeof EditorVi
borderBottom: '2px solid rgba(80, 160, 255, 0.7)',
borderRadius: '2px'
},
'.annotation-show_tell': {
backgroundColor: 'rgba(255, 140, 30, 0.18)',
borderBottom: '2px solid rgba(255, 140, 30, 0.7)',
borderRadius: '2px'
},
'.annotation-critique': {
backgroundColor: 'rgba(160, 80, 220, 0.18)',
borderBottom: '2px solid rgba(160, 80, 220, 0.7)',

View File

@@ -22,6 +22,7 @@ function badgeColor(type: TextAnnotation['type']): string {
case 'passive_voice': return 'rgba(255, 200, 0, 0.75)'
case 'consistency': return 'rgba(220, 80, 80, 0.75)'
case 'style': return 'rgba(80, 160, 255, 0.75)'
case 'show_tell': return 'rgba(255, 140, 30, 0.75)'
case 'critique': return 'rgba(160, 80, 220, 0.75)'
case 'custom': return 'rgba(30, 200, 150, 0.8)'
}
@@ -40,7 +41,7 @@ interface FeedbackCardProps {
function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.Element {
const [state, setState] = useState<AnalysisState>(() => {
const cached = tooltipAnalysisCache.get(ann.id)
const cached = tooltipAnalysisCache.get(ann.id) ?? ann.analysisCache ?? null
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.

View File

@@ -59,7 +59,7 @@ export function AnalysisToolbar(): JSX.Element {
setAnalysisMode('passive_voice')
}
const runAIAnalysis = async (mode: 'consistency' | 'style' | 'critique'): Promise<void> => {
const runAIAnalysis = async (mode: 'consistency' | 'style' | 'show_tell' | 'critique'): Promise<void> => {
if (!activeFilePath || isAILoading) return
setAnalysisMode(mode)
@@ -70,7 +70,9 @@ export function AnalysisToolbar(): JSX.Element {
? 'Please check this chapter for consistency issues (character names, timeline, repeated phrases).'
: mode === 'style'
? 'Please analyze the style and pacing of this chapter and suggest improvements.'
: 'Please give me an honest critique of this chapter.'
: mode === 'show_tell'
? 'Please identify every passage in this chapter where I am telling rather than showing.'
: 'Please give me an honest critique of this chapter.'
addUserMessage(prompt)
startAssistantMessage()
@@ -96,7 +98,10 @@ export function AnalysisToolbar(): JSX.Element {
const currentHistory = useEditorStore.getState().chatHistory
const lastMsg = currentHistory[currentHistory.length - 1]
if (lastMsg?.role === 'assistant' && lastMsg.content.length > 0) {
const newAnnotations = parseAnnotationsFromAIResponse(lastMsg.content, activeFileContent)
// For show_tell mode, force all annotations to the show_tell type so the
// classifier doesn't accidentally mis-label them as 'style' or 'consistency'.
const overrideType = mode === 'show_tell' ? 'show_tell' : undefined
const newAnnotations = parseAnnotationsFromAIResponse(lastMsg.content, activeFileContent, overrideType)
if (newAnnotations.length > 0) {
const existing = useEditorStore.getState().annotations.filter((a) => a.type !== mode)
setAnnotations([...existing, ...newAnnotations])
@@ -113,6 +118,7 @@ export function AnalysisToolbar(): JSX.Element {
const passiveCount = annotations.filter((a) => a.type === 'passive_voice').length
const consistencyCount = annotations.filter((a) => a.type === 'consistency').length
const styleCount = annotations.filter((a) => a.type === 'style').length
const showTellCount = annotations.filter((a) => a.type === 'show_tell').length
const critiqueCount = annotations.filter((a) => a.type === 'critique').length
const docWordCount = countWords(activeFileContent)
@@ -155,6 +161,18 @@ export function AnalysisToolbar(): JSX.Element {
)}
</button>
<button
className={`toolbar-btn${analysisMode === 'show_tell' ? ' active' : ''}`}
onClick={() => runAIAnalysis('show_tell')}
disabled={!hasFile || isAILoading}
title="Find passages that tell rather than show via AI"
>
{isAILoading && analysisMode === 'show_tell' ? 'Reading…' : 'Show vs Tell'}
{showTellCount > 0 && (
<span className="toolbar-badge">{showTellCount}</span>
)}
</button>
<button
className={`toolbar-btn${analysisMode === 'critique' ? ' active' : ''}`}
onClick={() => runAIAnalysis('critique')}

View File

@@ -43,6 +43,7 @@ interface EditorState {
linkAnnotationsToMessage: (messageId: string, annotationIds: string[]) => void
clearArchivedAnnotations: () => void
removeArchivedAnnotation: (id: string) => void
setAnnotationAnalysis: (id: string, result: { text: string; suggestion: string | null }) => void
// Analysis mode
analysisMode: AnalysisMode
@@ -457,6 +458,37 @@ export const useEditorStore = create<EditorState>((set, get) => ({
})
},
setAnnotationAnalysis: (id, result) => {
set((s) => {
if (!s.activeFilePath) return {}
const fileState = s.annotationsByFile[s.activeFilePath]
if (!fileState) return {}
const updatedAll = fileState.annotations.map(a =>
a.id === id ? { ...a, analysisCache: result } : a
)
const annotations = s.annotations.map(a =>
a.id === id ? { ...a, analysisCache: result } : a
)
return {
annotations,
annotationsByFile: {
...s.annotationsByFile,
[s.activeFilePath]: { ...fileState, annotations: updatedAll }
}
}
})
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) => {

View File

@@ -28,7 +28,7 @@ export interface ChatSession {
messages: ChatMessage[]
}
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'critique' | 'custom'
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'show_tell' | 'critique' | 'custom'
export interface TextAnnotation {
id: string
@@ -41,11 +41,12 @@ export interface TextAnnotation {
applied?: boolean // true when the suggestion has been applied to the document
dismissed?: boolean // true when the user dismissed this annotation (archived)
autoAnalyse?: boolean // true when created via context menu — FeedbackCard starts AI analysis immediately
analysisCache?: { text: string; suggestion: string | null } // persisted AI analysis result
}
export type AnalysisMode = 'none' | 'passive_voice' | 'consistency' | 'style' | 'critique'
export type AnalysisMode = 'none' | 'passive_voice' | 'consistency' | 'style' | 'show_tell' | 'critique'
export type AIMode = 'chat' | 'passive_voice' | 'consistency' | 'style' | 'critique'
export type AIMode = 'chat' | 'passive_voice' | 'consistency' | 'style' | 'show_tell' | 'critique'
export interface AIPayload {
mode: AIMode

View File

@@ -68,13 +68,22 @@ function classifyType(contextBefore: string): AnnotationType {
if (contextBefore.includes('passive')) return 'passive_voice'
if (
contextBefore.includes('consistency') ||
contextBefore.includes('character') ||
contextBefore.includes('timeline') ||
contextBefore.includes('repeated') ||
contextBefore.includes('contradiction')
) {
return 'consistency'
}
if (
contextBefore.includes('show don') ||
contextBefore.includes('show-don') ||
contextBefore.includes('show vs tell') ||
contextBefore.includes('show/tell') ||
contextBefore.includes('telling') ||
contextBefore.includes('told') && contextBefore.includes('show')
) {
return 'show_tell'
}
return 'style'
}