past progressive, weak verb, and cliche analysis

This commit is contained in:
TC
2026-06-12 11:20:59 +10:00
parent e309845289
commit 161a1093e8
6 changed files with 160 additions and 10 deletions

View File

@@ -160,6 +160,34 @@ 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]"`,
past_progressive: `Identify every instance of past progressive tense (was/were + verb-ing) in this chapter that would read more strongly in simple past tense.
Do not flag past progressive that is grammatically necessary (e.g. interrupted actions: "She was reading when he entered"). Only flag cases where simple past would be tighter.
For each instance:
ISSUE: Past Progressive
PASSAGE: "[exact quoted sentence from the chapter]"
PROBLEM: [one sentence on why simple past would be stronger]
SUGGESTION: "[rewritten in simple past]"`,
weak_verbs: `Identify sentences in this chapter where the main verb is weak — relying on "to be" (was, were, is, are), "to have" (had, has), "to get" (got), "to seem" (seemed, appeared), "to look" (looked), or "to feel" (felt) as the primary predicate when a stronger, more specific verb would make the prose more vivid.
Do not flag auxiliary uses (passive voice, perfect aspect) — only flag cases where these are the main, load-bearing verb in the sentence.
For each instance:
ISSUE: Weak Verb
PASSAGE: "[exact quoted sentence from the chapter]"
PROBLEM: [explain what the weak verb is and why a stronger verb would serve better]
SUGGESTION: "[rewritten with a more specific, active verb]"`,
cliches: `Identify every cliché, hackneyed phrase, or overused expression in this chapter. This includes worn-out metaphors, stock phrases, predictable comparisons, and any language that has become so common it has lost its impact.
For each instance:
ISSUE: Cliché
PASSAGE: "[exact quoted phrase or sentence from the chapter]"
PROBLEM: [name the cliché and briefly explain why it's stale]
SUGGESTION: "[a fresher, more original alternative]"`,
critique: `Give an honest, detailed critique of this chapter as a whole. Structure your response as follows:
**Overall impression** (23 sentences on what the chapter achieves and its most significant weakness)

View File

@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useEditorStore } from '../../store/editorStore'
import { detectPassiveVoice } from '../../utils/passiveVoice'
import { detectPastProgressive } from '../../utils/pastProgressive'
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
import { tooltipAnalysisCache } from '../Editor/MarkdownEditor'
import '../FileTree/ContextMenu.css'
@@ -256,7 +257,15 @@ export function AnalysisToolbar(): JSX.Element {
setAnalysisMode('passive_voice')
}
const runAIAnalysis = async (mode: 'consistency' | 'style' | 'show_tell' | 'critique'): Promise<void> => {
const runPastProgressive = (): void => {
if (!hasFile) return
const found = detectPastProgressive(activeFileContent)
const existing = useEditorStore.getState().annotations.filter((a) => a.type !== 'past_progressive')
setAnnotations([...existing, ...found])
setAnalysisMode('past_progressive')
}
const runAIAnalysis = async (mode: 'consistency' | 'style' | 'show_tell' | 'critique' | 'weak_verbs' | 'cliches' | 'past_progressive'): Promise<void> => {
if (!activeFilePath || isAILoading) return
setAnalysisMode(mode)
@@ -269,7 +278,13 @@ export function AnalysisToolbar(): JSX.Element {
? '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.'
: mode === 'weak_verbs'
? 'Please identify sentences in this chapter that use weak verbs (was, were, had, got, seemed, appeared, looked, felt) as the main predicate.'
: mode === 'cliches'
? 'Please identify every cliché and overused phrase in this chapter.'
: mode === 'past_progressive'
? 'Please identify every past progressive construction (was/were + verb-ing) in this chapter that would be stronger in simple past.'
: 'Please give me an honest critique of this chapter.'
addUserMessage(prompt)
startAssistantMessage()
@@ -295,9 +310,13 @@ export function AnalysisToolbar(): JSX.Element {
const currentHistory = useEditorStore.getState().chatHistory
const lastMsg = currentHistory[currentHistory.length - 1]
if (lastMsg?.role === 'assistant' && lastMsg.content.length > 0) {
// 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
// Force annotation type for modes where the classifier might mis-label.
const overrideType =
mode === 'show_tell' ? 'show_tell' :
mode === 'weak_verbs' ? 'weak_verbs' :
mode === 'cliches' ? 'cliches' :
mode === 'past_progressive' ? 'past_progressive' :
undefined
const { annotations: newAnnotations } = parseAnnotationsFromAIResponse(lastMsg.content, activeFileContent, overrideType)
if (newAnnotations.length > 0) {
const existing = useEditorStore.getState().annotations.filter((a) => a.type !== mode)
@@ -313,11 +332,14 @@ export function AnalysisToolbar(): JSX.Element {
}
const passiveCount = annotations.filter((a) => a.type === 'passive_voice').length
const pastProgressiveCount = annotations.filter((a) => a.type === 'past_progressive').length
const weakVerbsCount = annotations.filter((a) => a.type === 'weak_verbs').length
const clichesCount = annotations.filter((a) => a.type === 'cliches').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 totalCount = passiveCount + consistencyCount + styleCount + showTellCount + critiqueCount
const totalCount = passiveCount + pastProgressiveCount + weakVerbsCount + clichesCount + consistencyCount + styleCount + showTellCount + critiqueCount
const anyActive = Boolean(analysisMode)
const docWordCount = countWords(activeFileContent)
const sentenceStats = activeFileContent ? computeSentenceStats(activeFileContent) : null
@@ -405,6 +427,27 @@ export function AnalysisToolbar(): JSX.Element {
<span>Passive Voice</span>
{passiveCount > 0 && <span className="toolbar-analyze-count">{passiveCount}</span>}
</button>
<button
className={`context-menu-item${analysisMode === 'past_progressive' ? ' active' : ''}`}
onClick={() => { setAnalyzeOpen(false); runPastProgressive() }}
>
<span>Past Progressive</span>
{pastProgressiveCount > 0 && <span className="toolbar-analyze-count">{pastProgressiveCount}</span>}
</button>
<button
className={`context-menu-item${analysisMode === 'weak_verbs' ? ' active' : ''}`}
onClick={() => { setAnalyzeOpen(false); void runAIAnalysis('weak_verbs') }}
>
<span>Weak Verbs</span>
{weakVerbsCount > 0 && <span className="toolbar-analyze-count">{weakVerbsCount}</span>}
</button>
<button
className={`context-menu-item${analysisMode === 'cliches' ? ' active' : ''}`}
onClick={() => { setAnalyzeOpen(false); void runAIAnalysis('cliches') }}
>
<span>Clichés</span>
{clichesCount > 0 && <span className="toolbar-analyze-count">{clichesCount}</span>}
</button>
<button
className={`context-menu-item${analysisMode === 'consistency' ? ' active' : ''}`}
onClick={() => { setAnalyzeOpen(false); void runAIAnalysis('consistency') }}

View File

@@ -41,7 +41,7 @@ export interface ChatSession {
messages: ChatMessage[]
}
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'show_tell' | 'critique' | 'custom' | 'user_comment' | 'document_note'
export type AnnotationType = 'passive_voice' | 'past_progressive' | 'weak_verbs' | 'cliches' | 'consistency' | 'style' | 'show_tell' | 'critique' | 'custom' | 'user_comment' | 'document_note'
export interface TextAnnotation {
id: string
@@ -58,9 +58,9 @@ export interface TextAnnotation {
comment?: string // user-written note text (only set for user_comment and document_note types)
}
export type AnalysisMode = 'none' | 'passive_voice' | 'consistency' | 'style' | 'show_tell' | 'critique'
export type AnalysisMode = 'none' | 'passive_voice' | 'past_progressive' | 'weak_verbs' | 'cliches' | 'consistency' | 'style' | 'show_tell' | 'critique'
export type AIMode = 'chat' | 'passive_voice' | 'consistency' | 'style' | 'show_tell' | 'critique'
export type AIMode = 'chat' | 'passive_voice' | 'past_progressive' | 'weak_verbs' | 'cliches' | 'consistency' | 'style' | 'show_tell' | 'critique'
export interface AIPayload {
mode: AIMode

View File

@@ -83,6 +83,9 @@ function deduplicateOverlapping(annotations: TextAnnotation[]): TextAnnotation[]
function classifyType(contextBefore: string): AnnotationType {
if (contextBefore.includes('passive')) return 'passive_voice'
if (contextBefore.includes('past progressive') || contextBefore.includes('past_progressive')) return 'past_progressive'
if (contextBefore.includes('weak verb') || contextBefore.includes('weak_verb')) return 'weak_verbs'
if (contextBefore.includes('clich')) return 'cliches'
if (
contextBefore.includes('consistency') ||
contextBefore.includes('timeline') ||

View File

@@ -0,0 +1,76 @@
import type { TextAnnotation } from '../types/editor'
// Matches "was/were + optional adverb + verb-ing"
const PAST_PROGRESSIVE_PATTERN = /\b(was|were)\b(\s+\w+ly)?\s+(\w+ing)\b/gi
function findSentenceStart(text: string, pos: number): number {
let i = pos - 1
while (i > 0) {
if (/[.!?]/.test(text[i]) && i + 1 < text.length && /\s/.test(text[i + 1])) {
return i + 2
}
if (text[i] === '\n' && i > 0 && text[i - 1] === '\n') {
return i + 1
}
i--
}
return 0
}
function findSentenceEnd(text: string, pos: number): number {
let i = pos
while (i < text.length) {
if (/[.!?]/.test(text[i])) return i + 1
if (text[i] === '\n') return i
i++
}
return text.length
}
// Gerunds that are commonly nouns/adjectives rather than past progressive verbs
const NOUN_GERUNDS = new Set([
'morning', 'evening', 'ceiling', 'feeling', 'something', 'nothing', 'anything',
'everything', 'meeting', 'building', 'opening', 'beginning', 'ending', 'following',
'interesting', 'amazing', 'surprising', 'concerning', 'leading', 'according',
'existing', 'remaining', 'overwhelming', 'encouraging', 'promising', 'confusing',
'missing', 'boring', 'exciting', 'shocking', 'outstanding', 'underlying'
])
export function detectPastProgressive(text: string): TextAnnotation[] {
const annotations: TextAnnotation[] = []
const seenRanges = new Set<string>()
PAST_PROGRESSIVE_PATTERN.lastIndex = 0
let match: RegExpExecArray | null
while ((match = PAST_PROGRESSIVE_PATTERN.exec(text)) !== null) {
const verb = match[3].toLowerCase()
if (NOUN_GERUNDS.has(verb)) continue
const matchStart = match.index
const matchEnd = match.index + match[0].length
const lineStart = text.lastIndexOf('\n', matchStart) + 1
const lineText = text.slice(lineStart, matchEnd)
if (lineText.trimStart().startsWith('#')) continue
const from = findSentenceStart(text, matchStart)
const to = findSentenceEnd(text, matchEnd)
const key = `${from}-${to}`
if (seenRanges.has(key)) continue
seenRanges.add(key)
const sentence = text.slice(from, to).trim()
annotations.push({
id: `pp-${matchStart}`,
type: 'past_progressive',
from,
to,
matchedText: sentence,
message: `Past progressive: "${match[0].trim()}" — consider simple past`,
suggestion: undefined
})
}
return annotations
}

File diff suppressed because one or more lines are too long