✨ past progressive, weak verb, and cliche analysis
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user