grammar checker

This commit is contained in:
TC
2026-06-20 22:18:12 +10:00
parent 833385c8d8
commit 199e909778
8 changed files with 145 additions and 5 deletions

View File

@@ -563,6 +563,11 @@ function buildTheme(fontSize: number, dark: boolean, focusMode = false): ReturnT
backgroundColor: 'rgba(240, 100, 180, 0.15)',
borderBottom: '2px solid rgba(240, 100, 180, 0.65)',
borderRadius: '2px'
},
'.annotation-grammar': {
backgroundColor: 'rgba(220, 60, 60, 0.12)',
borderBottom: '2px solid rgba(220, 60, 60, 0.75)',
borderRadius: '2px'
}
},
{ dark }

View File

@@ -29,6 +29,7 @@ function badgeColor(type: TextAnnotation['type']): string {
case 'user_comment': return 'rgba(240, 100, 180, 0.85)'
case 'document_note': return 'rgba(80, 180, 240, 0.85)'
case 'polish': return 'rgba(55, 138, 221, 0.75)'
case 'grammar': return 'rgba(220, 60, 60, 0.8)'
}
}
@@ -295,13 +296,14 @@ function PolishMeter({ score }: { score: PolishScore }): JSX.Element {
const [activeKey, setActiveKey] = useState<string | null>(null)
const [tooltip, setTooltip] = useState<TooltipState | null>(null)
const { setAnnotations, clearAnnotations } = useEditorStore()
const widgetRef = useRef<HTMLDivElement>(null)
const dims = Object.entries(score.dimensions) as [string, PolishDimension][]
function handleDimClick(key: string, dim: PolishDimension): void {
if (activeKey === key) {
setActiveKey(null)
clearAnnotations()
setAnnotations([])
return
}
if (dim.matches.length === 0) return
@@ -326,8 +328,21 @@ function PolishMeter({ score }: { score: PolishScore }): JSX.Element {
}
}, [score.overall])
// Hide highlights when clicking outside the polish meter
useEffect(() => {
if (!activeKey) return
function handleOutsideClick(e: MouseEvent): void {
if (widgetRef.current && !widgetRef.current.contains(e.target as Node)) {
setActiveKey(null)
setAnnotations([])
}
}
document.addEventListener('mousedown', handleOutsideClick)
return () => document.removeEventListener('mousedown', handleOutsideClick)
}, [activeKey, setAnnotations])
return (
<div className="pm-widget">
<div ref={widgetRef} className="pm-widget">
<div className="pm-header">
<span className="pm-title">Polish</span>
<span className="pm-overall" style={{ color: scoreColor(score.overall) }}>{score.overall}</span>

View File

@@ -363,6 +363,34 @@ export function AnalysisToolbar(): JSX.Element {
}
}
const runGrammar = async (): Promise<void> => {
if (!hasFile || isAILoading) return
setAILoading(true)
setAIError(null)
setAnalysisMode('none')
try {
const matches = await window.api.checkGrammar(activeFileContent)
const newAnnotations = matches.map((m) => {
const matched = activeFileContent.slice(m.offset, m.offset + m.length)
return {
id: `grammar-${m.offset}-${m.ruleId}`,
type: 'grammar' as const,
from: m.offset,
to: m.offset + m.length,
matchedText: matched,
message: m.message,
suggestion: m.replacement ?? undefined,
}
})
const existing = useEditorStore.getState().annotations.filter((a) => a.type !== 'grammar')
setAnnotations([...existing, ...newAnnotations])
} catch (err) {
setAIError(err instanceof Error ? err.message : 'Grammar check failed')
} finally {
setAILoading(false)
}
}
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
@@ -371,7 +399,8 @@ export function AnalysisToolbar(): JSX.Element {
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 + pastProgressiveCount + weakVerbsCount + clichesCount + consistencyCount + styleCount + showTellCount + critiqueCount
const grammarCount = annotations.filter((a) => a.type === 'grammar').length
const totalCount = passiveCount + pastProgressiveCount + weakVerbsCount + clichesCount + consistencyCount + styleCount + showTellCount + critiqueCount + grammarCount
const anyActive = Boolean(analysisMode)
const sentenceStats = activeFileContent ? computeSentenceStats(activeFileContent) : null
const paragraphRhythm = activeFileContent ? computeParagraphRhythm(activeFileContent) : []
@@ -506,6 +535,14 @@ export function AnalysisToolbar(): JSX.Element {
<span>Critique</span>
{critiqueCount > 0 && <span className="toolbar-analyze-count">{critiqueCount}</span>}
</button>
<div className="context-menu-separator" />
<button
className="context-menu-item"
onClick={() => { setAnalyzeOpen(false); void runGrammar() }}
>
<span>Grammar</span>
{grammarCount > 0 && <span className="toolbar-analyze-count">{grammarCount}</span>}
</button>
</div>
)
})(),

View File

@@ -41,7 +41,7 @@ export interface ChatSession {
messages: ChatMessage[]
}
export type AnnotationType = 'passive_voice' | 'past_progressive' | 'weak_verbs' | 'cliches' | 'consistency' | 'style' | 'show_tell' | 'critique' | 'custom' | 'user_comment' | 'document_note' | 'polish'
export type AnnotationType = 'passive_voice' | 'past_progressive' | 'weak_verbs' | 'cliches' | 'consistency' | 'style' | 'show_tell' | 'critique' | 'custom' | 'user_comment' | 'document_note' | 'polish' | 'grammar'
export interface TextAnnotation {
id: string