💄 cleaner ui for feedback
This commit is contained in:
@@ -121,13 +121,6 @@ export default function App(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="app-titlebar-icon-group">
|
<div className="app-titlebar-icon-group">
|
||||||
<button
|
|
||||||
className={`app-titlebar-theme-btn app-titlebar-revision-btn${revisionPanelOpen ? ' active' : ''}`}
|
|
||||||
onClick={toggleRevisionPanel}
|
|
||||||
title="Revision history"
|
|
||||||
>
|
|
||||||
⟳
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="app-titlebar-theme-btn"
|
className="app-titlebar-theme-btn"
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
|
|||||||
@@ -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 { useEditorStore } from '../../store/editorStore'
|
||||||
import { detectPassiveVoice } from '../../utils/passiveVoice'
|
import { detectPassiveVoice } from '../../utils/passiveVoice'
|
||||||
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
|
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
|
||||||
import { tooltipAnalysisCache } from '../Editor/MarkdownEditor'
|
import { tooltipAnalysisCache } from '../Editor/MarkdownEditor'
|
||||||
|
import '../FileTree/ContextMenu.css'
|
||||||
import './Toolbar.css'
|
import './Toolbar.css'
|
||||||
|
|
||||||
const BIBLE_PROMPTS = {
|
const BIBLE_PROMPTS = {
|
||||||
@@ -65,9 +67,15 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
setFontSize,
|
setFontSize,
|
||||||
setRightPanelTab,
|
setRightPanelTab,
|
||||||
outlineOpen,
|
outlineOpen,
|
||||||
toggleOutline
|
toggleOutline,
|
||||||
|
revisionPanelOpen,
|
||||||
|
toggleRevisionPanel
|
||||||
} = useEditorStore()
|
} = useEditorStore()
|
||||||
|
|
||||||
|
const [analyzeOpen, setAnalyzeOpen] = useState(false)
|
||||||
|
const analyzeButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
const analyzeMenuRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.api.getProjectWordCount().then(setProjectWordCount).catch(() => {})
|
window.api.getProjectWordCount().then(setProjectWordCount).catch(() => {})
|
||||||
}, [])
|
}, [])
|
||||||
@@ -79,6 +87,32 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [isDirty])
|
}, [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 hasFile = Boolean(activeFilePath)
|
||||||
const isStoryBible = activeFilePath?.endsWith('Story Bible.md') ?? false
|
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 styleCount = annotations.filter((a) => a.type === 'style').length
|
||||||
const showTellCount = annotations.filter((a) => a.type === 'show_tell').length
|
const showTellCount = annotations.filter((a) => a.type === 'show_tell').length
|
||||||
const critiqueCount = annotations.filter((a) => a.type === 'critique').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)
|
const docWordCount = countWords(activeFileContent)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -226,62 +262,15 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={`toolbar-btn${analysisMode === 'passive_voice' ? ' active' : ''}`}
|
ref={analyzeButtonRef}
|
||||||
onClick={runPassiveVoice}
|
className={`toolbar-btn toolbar-analyze-btn${anyActive ? ' active' : ''}`}
|
||||||
disabled={!hasFile}
|
onClick={() => setAnalyzeOpen((v) => !v)}
|
||||||
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')}
|
|
||||||
disabled={!hasFile || isAILoading}
|
disabled={!hasFile || isAILoading}
|
||||||
title="Check character names, timeline, and repeated phrases via AI"
|
title="Run analysis on this chapter"
|
||||||
>
|
>
|
||||||
{isAILoading && analysisMode === 'consistency' ? 'Checking…' : 'Consistency'}
|
{isAILoading ? 'Analyzing…' : `Analyze ${analyzeOpen ? '▴' : '▾'}`}
|
||||||
{consistencyCount > 0 && (
|
{totalCount > 0 && !isAILoading && (
|
||||||
<span className="toolbar-badge">{consistencyCount}</span>
|
<span className="toolbar-analyze-badge">{totalCount}</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>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -294,11 +283,68 @@ export function AnalysisToolbar(): JSX.Element {
|
|||||||
Clear
|
Clear
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div className="toolbar-right">
|
<div className="toolbar-right">
|
||||||
|
<button
|
||||||
|
className={`toolbar-btn toolbar-btn-outline${revisionPanelOpen ? ' active' : ''}`}
|
||||||
|
onClick={toggleRevisionPanel}
|
||||||
|
title="Revision history"
|
||||||
|
>
|
||||||
|
⟳
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`toolbar-btn toolbar-btn-outline${outlineOpen ? ' active' : ''}`}
|
className={`toolbar-btn toolbar-btn-outline${outlineOpen ? ' active' : ''}`}
|
||||||
onClick={toggleOutline}
|
onClick={toggleOutline}
|
||||||
|
|||||||
@@ -150,6 +150,45 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Analyze dropdown button ─────────────────────────────────────── */
|
||||||
|
.toolbar-analyze-btn {
|
||||||
|
min-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-analyze-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent);
|
||||||
|
color: #1a1208;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 5px;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown menu scoped overrides */
|
||||||
|
.toolbar-analyze-menu .context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-analyze-menu .context-menu-item.active {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-analyze-count {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Story Bible toolbar ─────────────────────────────────────────── */
|
/* ── Story Bible toolbar ─────────────────────────────────────────── */
|
||||||
.toolbar-bible-label {
|
.toolbar-bible-label {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
|
|||||||
@@ -113,10 +113,6 @@
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-titlebar-revision-btn {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-titlebar-icon-group {
|
.app-titlebar-icon-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user