✨ past progressive, weak verb, and cliche analysis
This commit is contained in:
@@ -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** (2–3 sentences on what the chapter achieves and its most significant weakness)
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') ||
|
||||
|
||||
76
src/renderer/utils/pastProgressive.ts
Normal file
76
src/renderer/utils/pastProgressive.ts
Normal 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
Reference in New Issue
Block a user