✨ show vs tell
This commit is contained in:
@@ -58,16 +58,29 @@ SUGGESTION: [how to fix it]`,
|
|||||||
style: `Analyze the writing style and provide specific improvement suggestions:
|
style: `Analyze the writing style and provide specific improvement suggestions:
|
||||||
- Pacing: identify slow passages or rushed moments
|
- Pacing: identify slow passages or rushed moments
|
||||||
- Sentence variety: flag runs of similar length or structure
|
- Sentence variety: flag runs of similar length or structure
|
||||||
- Show don't tell: identify passages that tell emotion/state rather than showing it
|
|
||||||
- Gothic atmosphere: passages where the atmospheric tone is inconsistent
|
- Gothic atmosphere: passages where the atmospheric tone is inconsistent
|
||||||
- Dialogue: any dialogue that feels stilted or unnatural
|
- Dialogue: any dialogue that feels stilted or unnatural
|
||||||
|
|
||||||
For each suggestion:
|
For each suggestion:
|
||||||
ISSUE: [type: Pacing / Sentence Variety / Show-Don't-Tell / Atmosphere / Dialogue]
|
ISSUE: [type: Pacing / Sentence Variety / Atmosphere / Dialogue]
|
||||||
PASSAGE: "[exact quoted text]"
|
PASSAGE: "[exact quoted text]"
|
||||||
PROBLEM: [specific explanation]
|
PROBLEM: [specific explanation]
|
||||||
SUGGESTION: [concrete rewrite or approach]`,
|
SUGGESTION: [concrete rewrite or approach]`,
|
||||||
|
|
||||||
|
show_tell: `Identify every passage in this chapter where the author tells the reader something that should instead be shown through action, dialogue, sensation, or concrete detail.
|
||||||
|
|
||||||
|
Focus on:
|
||||||
|
- Emotions or inner states stated directly ("she felt afraid", "he was relieved") instead of expressed through physical reaction or behaviour
|
||||||
|
- Character motivations or intentions explained by the narrator rather than revealed through what characters do or say
|
||||||
|
- Abstract or evaluative description ("the room was oppressive", "it was a beautiful day") instead of specific sensory detail that lets the reader feel it
|
||||||
|
- Dialogue tags or beats that tell the reader how something was said rather than letting word choice and action carry it
|
||||||
|
|
||||||
|
For each instance:
|
||||||
|
ISSUE: Show Don't Tell
|
||||||
|
PASSAGE: "[exact quoted text from the chapter]"
|
||||||
|
PROBLEM: [one sentence explaining what is being told that should be shown]
|
||||||
|
SUGGESTION: "[a concrete rewrite that shows the same moment through action, sensation, or dialogue]"`,
|
||||||
|
|
||||||
critique: `Give an honest, detailed critique of this chapter as a whole. Structure your response as follows:
|
critique: `Give an honest, detailed critique of this chapter as a whole. Structure your response as follows:
|
||||||
|
|
||||||
**Overall impression** (2–3 sentences on what the chapter achieves and its most significant weakness)
|
**Overall impression** (2–3 sentences on what the chapter achieves and its most significant weakness)
|
||||||
|
|||||||
@@ -98,8 +98,10 @@ export function analyseAnnotation(
|
|||||||
ann: TextAnnotation,
|
ann: TextAnnotation,
|
||||||
onUpdate: (text: string, streaming: boolean, suggestion: string | null) => void
|
onUpdate: (text: string, streaming: boolean, suggestion: string | null) => void
|
||||||
): () => void {
|
): () => void {
|
||||||
const cached = tooltipAnalysisCache.get(ann.id)
|
const cached = tooltipAnalysisCache.get(ann.id) ?? ann.analysisCache ?? null
|
||||||
if (cached) {
|
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)
|
onUpdate(cached.text, false, cached.suggestion)
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
@@ -131,6 +133,7 @@ export function analyseAnnotation(
|
|||||||
const text = accumulated || ann.message
|
const text = accumulated || ann.message
|
||||||
const suggestion = extractBlockquote(text) ?? ann.suggestion ?? null
|
const suggestion = extractBlockquote(text) ?? ann.suggestion ?? null
|
||||||
tooltipAnalysisCache.set(ann.id, { text, suggestion })
|
tooltipAnalysisCache.set(ann.id, { text, suggestion })
|
||||||
|
useEditorStore.getState().setAnnotationAnalysis(ann.id, { text, suggestion })
|
||||||
onUpdate(text, false, suggestion)
|
onUpdate(text, false, suggestion)
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
if (!cancelled) onUpdate(ann.message, false, ann.suggestion ?? null)
|
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)',
|
borderBottom: '2px solid rgba(80, 160, 255, 0.7)',
|
||||||
borderRadius: '2px'
|
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': {
|
'.annotation-critique': {
|
||||||
backgroundColor: 'rgba(160, 80, 220, 0.18)',
|
backgroundColor: 'rgba(160, 80, 220, 0.18)',
|
||||||
borderBottom: '2px solid rgba(160, 80, 220, 0.7)',
|
borderBottom: '2px solid rgba(160, 80, 220, 0.7)',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ function badgeColor(type: TextAnnotation['type']): string {
|
|||||||
case 'passive_voice': return 'rgba(255, 200, 0, 0.75)'
|
case 'passive_voice': return 'rgba(255, 200, 0, 0.75)'
|
||||||
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 'show_tell': return 'rgba(255, 140, 30, 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)'
|
case 'custom': return 'rgba(30, 200, 150, 0.8)'
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,7 @@ interface FeedbackCardProps {
|
|||||||
|
|
||||||
function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.Element {
|
function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.Element {
|
||||||
const [state, setState] = useState<AnalysisState>(() => {
|
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 }
|
if (cached) return { status: 'done', text: cached.text, suggestion: cached.suggestion }
|
||||||
// Custom (attachment-driven) annotations already carry their analysis — show
|
// Custom (attachment-driven) annotations already carry their analysis — show
|
||||||
// the problem description and suggestion immediately without an extra AI call.
|
// the problem description and suggestion immediately without an extra AI call.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
setAnalysisMode('passive_voice')
|
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
|
if (!activeFilePath || isAILoading) return
|
||||||
|
|
||||||
setAnalysisMode(mode)
|
setAnalysisMode(mode)
|
||||||
@@ -70,6 +70,8 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
? 'Please check this chapter for consistency issues (character names, timeline, repeated phrases).'
|
? 'Please check this chapter for consistency issues (character names, timeline, repeated phrases).'
|
||||||
: mode === 'style'
|
: mode === 'style'
|
||||||
? 'Please analyze the style and pacing of this chapter and suggest improvements.'
|
? 'Please analyze the style and pacing of this chapter and suggest improvements.'
|
||||||
|
: 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.'
|
: 'Please give me an honest critique of this chapter.'
|
||||||
|
|
||||||
addUserMessage(prompt)
|
addUserMessage(prompt)
|
||||||
@@ -96,7 +98,10 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
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 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) {
|
if (newAnnotations.length > 0) {
|
||||||
const existing = useEditorStore.getState().annotations.filter((a) => a.type !== mode)
|
const existing = useEditorStore.getState().annotations.filter((a) => a.type !== mode)
|
||||||
setAnnotations([...existing, ...newAnnotations])
|
setAnnotations([...existing, ...newAnnotations])
|
||||||
@@ -113,6 +118,7 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
const passiveCount = annotations.filter((a) => a.type === 'passive_voice').length
|
const passiveCount = annotations.filter((a) => a.type === 'passive_voice').length
|
||||||
const consistencyCount = annotations.filter((a) => a.type === 'consistency').length
|
const consistencyCount = annotations.filter((a) => a.type === 'consistency').length
|
||||||
const styleCount = annotations.filter((a) => a.type === 'style').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 critiqueCount = annotations.filter((a) => a.type === 'critique').length
|
||||||
const docWordCount = countWords(activeFileContent)
|
const docWordCount = countWords(activeFileContent)
|
||||||
|
|
||||||
@@ -155,6 +161,18 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</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
|
<button
|
||||||
className={`toolbar-btn${analysisMode === 'critique' ? ' active' : ''}`}
|
className={`toolbar-btn${analysisMode === 'critique' ? ' active' : ''}`}
|
||||||
onClick={() => runAIAnalysis('critique')}
|
onClick={() => runAIAnalysis('critique')}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ interface EditorState {
|
|||||||
linkAnnotationsToMessage: (messageId: string, annotationIds: string[]) => void
|
linkAnnotationsToMessage: (messageId: string, annotationIds: string[]) => void
|
||||||
clearArchivedAnnotations: () => void
|
clearArchivedAnnotations: () => void
|
||||||
removeArchivedAnnotation: (id: string) => void
|
removeArchivedAnnotation: (id: string) => void
|
||||||
|
setAnnotationAnalysis: (id: string, result: { text: string; suggestion: string | null }) => void
|
||||||
|
|
||||||
// Analysis mode
|
// Analysis mode
|
||||||
analysisMode: AnalysisMode
|
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',
|
analysisMode: 'none',
|
||||||
setAnalysisMode: (analysisMode) => {
|
setAnalysisMode: (analysisMode) => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface ChatSession {
|
|||||||
messages: ChatMessage[]
|
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 {
|
export interface TextAnnotation {
|
||||||
id: string
|
id: string
|
||||||
@@ -41,11 +41,12 @@ export interface TextAnnotation {
|
|||||||
applied?: boolean // true when the suggestion has been applied to the document
|
applied?: boolean // true when the suggestion has been applied to the document
|
||||||
dismissed?: boolean // true when the user dismissed this annotation (archived)
|
dismissed?: boolean // true when the user dismissed this annotation (archived)
|
||||||
autoAnalyse?: boolean // true when created via context menu — FeedbackCard starts AI analysis immediately
|
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 {
|
export interface AIPayload {
|
||||||
mode: AIMode
|
mode: AIMode
|
||||||
|
|||||||
@@ -68,13 +68,22 @@ function classifyType(contextBefore: string): AnnotationType {
|
|||||||
if (contextBefore.includes('passive')) return 'passive_voice'
|
if (contextBefore.includes('passive')) return 'passive_voice'
|
||||||
if (
|
if (
|
||||||
contextBefore.includes('consistency') ||
|
contextBefore.includes('consistency') ||
|
||||||
contextBefore.includes('character') ||
|
|
||||||
contextBefore.includes('timeline') ||
|
contextBefore.includes('timeline') ||
|
||||||
contextBefore.includes('repeated') ||
|
contextBefore.includes('repeated') ||
|
||||||
contextBefore.includes('contradiction')
|
contextBefore.includes('contradiction')
|
||||||
) {
|
) {
|
||||||
return 'consistency'
|
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'
|
return 'style'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user