💄 cleaner ui for feedback
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useEditorStore } from '../../store/editorStore'
|
||||
import { detectPassiveVoice } from '../../utils/passiveVoice'
|
||||
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
|
||||
import { tooltipAnalysisCache } from '../Editor/MarkdownEditor'
|
||||
import '../FileTree/ContextMenu.css'
|
||||
import './Toolbar.css'
|
||||
|
||||
const BIBLE_PROMPTS = {
|
||||
@@ -65,9 +67,15 @@ export function AnalysisToolbar(): JSX.Element {
|
||||
setFontSize,
|
||||
setRightPanelTab,
|
||||
outlineOpen,
|
||||
toggleOutline
|
||||
toggleOutline,
|
||||
revisionPanelOpen,
|
||||
toggleRevisionPanel
|
||||
} = useEditorStore()
|
||||
|
||||
const [analyzeOpen, setAnalyzeOpen] = useState(false)
|
||||
const analyzeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const analyzeMenuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
window.api.getProjectWordCount().then(setProjectWordCount).catch(() => {})
|
||||
}, [])
|
||||
@@ -79,6 +87,32 @@ export function AnalysisToolbar(): JSX.Element {
|
||||
}
|
||||
}, [isDirty])
|
||||
|
||||
// Close dropdown on file change
|
||||
useEffect(() => { setAnalyzeOpen(false) }, [activeFilePath])
|
||||
|
||||
// Click-outside closes dropdown
|
||||
useEffect(() => {
|
||||
if (!analyzeOpen) return
|
||||
const handler = (e: MouseEvent): void => {
|
||||
if (
|
||||
!analyzeButtonRef.current?.contains(e.target as Node) &&
|
||||
!analyzeMenuRef.current?.contains(e.target as Node)
|
||||
) {
|
||||
setAnalyzeOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handler)
|
||||
return () => document.removeEventListener('mousedown', handler)
|
||||
}, [analyzeOpen])
|
||||
|
||||
// Escape closes dropdown
|
||||
useEffect(() => {
|
||||
if (!analyzeOpen) return
|
||||
const handler = (e: KeyboardEvent): void => { if (e.key === 'Escape') setAnalyzeOpen(false) }
|
||||
document.addEventListener('keydown', handler)
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [analyzeOpen])
|
||||
|
||||
const hasFile = Boolean(activeFilePath)
|
||||
const isStoryBible = activeFilePath?.endsWith('Story Bible.md') ?? false
|
||||
|
||||
@@ -182,6 +216,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 + consistencyCount + styleCount + showTellCount + critiqueCount
|
||||
const anyActive = Boolean(analysisMode)
|
||||
const docWordCount = countWords(activeFileContent)
|
||||
|
||||
return (
|
||||
@@ -226,62 +262,15 @@ export function AnalysisToolbar(): JSX.Element {
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`toolbar-btn${analysisMode === 'passive_voice' ? ' active' : ''}`}
|
||||
onClick={runPassiveVoice}
|
||||
disabled={!hasFile}
|
||||
title="Highlight passive voice sentences instantly (no AI required)"
|
||||
>
|
||||
Passive Voice
|
||||
{passiveCount > 0 && (
|
||||
<span className="toolbar-badge">{passiveCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`toolbar-btn${analysisMode === 'consistency' ? ' active' : ''}`}
|
||||
onClick={() => runAIAnalysis('consistency')}
|
||||
ref={analyzeButtonRef}
|
||||
className={`toolbar-btn toolbar-analyze-btn${anyActive ? ' active' : ''}`}
|
||||
onClick={() => setAnalyzeOpen((v) => !v)}
|
||||
disabled={!hasFile || isAILoading}
|
||||
title="Check character names, timeline, and repeated phrases via AI"
|
||||
title="Run analysis on this chapter"
|
||||
>
|
||||
{isAILoading && analysisMode === 'consistency' ? 'Checking…' : 'Consistency'}
|
||||
{consistencyCount > 0 && (
|
||||
<span className="toolbar-badge">{consistencyCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`toolbar-btn${analysisMode === 'style' ? ' active' : ''}`}
|
||||
onClick={() => runAIAnalysis('style')}
|
||||
disabled={!hasFile || isAILoading}
|
||||
title="Pacing, sentence variety, show-don't-tell feedback via AI"
|
||||
>
|
||||
{isAILoading && analysisMode === 'style' ? 'Analyzing…' : 'Style'}
|
||||
{styleCount > 0 && (
|
||||
<span className="toolbar-badge">{styleCount}</span>
|
||||
)}
|
||||
</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
|
||||
className={`toolbar-btn${analysisMode === 'critique' ? ' active' : ''}`}
|
||||
onClick={() => runAIAnalysis('critique')}
|
||||
disabled={!hasFile || isAILoading}
|
||||
title="Honest overall critique of this chapter via AI"
|
||||
>
|
||||
{isAILoading && analysisMode === 'critique' ? 'Reading…' : 'Critique'}
|
||||
{critiqueCount > 0 && (
|
||||
<span className="toolbar-badge">{critiqueCount}</span>
|
||||
{isAILoading ? 'Analyzing…' : `Analyze ${analyzeOpen ? '▴' : '▾'}`}
|
||||
{totalCount > 0 && !isAILoading && (
|
||||
<span className="toolbar-analyze-badge">{totalCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -294,11 +283,68 @@ export function AnalysisToolbar(): JSX.Element {
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
|
||||
{analyzeOpen && analyzeButtonRef.current && createPortal(
|
||||
(() => {
|
||||
const rect = analyzeButtonRef.current!.getBoundingClientRect()
|
||||
return (
|
||||
<div
|
||||
ref={analyzeMenuRef}
|
||||
className="context-menu toolbar-analyze-menu"
|
||||
style={{ top: rect.bottom + 4, left: rect.left }}
|
||||
>
|
||||
<button
|
||||
className={`context-menu-item${analysisMode === 'passive_voice' ? ' active' : ''}`}
|
||||
onClick={() => { setAnalyzeOpen(false); runPassiveVoice() }}
|
||||
>
|
||||
<span>Passive Voice</span>
|
||||
{passiveCount > 0 && <span className="toolbar-analyze-count">{passiveCount}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={`context-menu-item${analysisMode === 'consistency' ? ' active' : ''}`}
|
||||
onClick={() => { setAnalyzeOpen(false); void runAIAnalysis('consistency') }}
|
||||
>
|
||||
<span>Consistency</span>
|
||||
{consistencyCount > 0 && <span className="toolbar-analyze-count">{consistencyCount}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={`context-menu-item${analysisMode === 'style' ? ' active' : ''}`}
|
||||
onClick={() => { setAnalyzeOpen(false); void runAIAnalysis('style') }}
|
||||
>
|
||||
<span>Style</span>
|
||||
{styleCount > 0 && <span className="toolbar-analyze-count">{styleCount}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={`context-menu-item${analysisMode === 'show_tell' ? ' active' : ''}`}
|
||||
onClick={() => { setAnalyzeOpen(false); void runAIAnalysis('show_tell') }}
|
||||
>
|
||||
<span>Show vs Tell</span>
|
||||
{showTellCount > 0 && <span className="toolbar-analyze-count">{showTellCount}</span>}
|
||||
</button>
|
||||
<button
|
||||
className={`context-menu-item${analysisMode === 'critique' ? ' active' : ''}`}
|
||||
onClick={() => { setAnalyzeOpen(false); void runAIAnalysis('critique') }}
|
||||
>
|
||||
<span>Critique</span>
|
||||
{critiqueCount > 0 && <span className="toolbar-analyze-count">{critiqueCount}</span>}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})(),
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="toolbar-right">
|
||||
<button
|
||||
className={`toolbar-btn toolbar-btn-outline${revisionPanelOpen ? ' active' : ''}`}
|
||||
onClick={toggleRevisionPanel}
|
||||
title="Revision history"
|
||||
>
|
||||
⟳
|
||||
</button>
|
||||
<button
|
||||
className={`toolbar-btn toolbar-btn-outline${outlineOpen ? ' active' : ''}`}
|
||||
onClick={toggleOutline}
|
||||
|
||||
Reference in New Issue
Block a user