🎉 initial commit
This commit is contained in:
146
src/renderer/components/Toolbar/AnalysisToolbar.tsx
Normal file
146
src/renderer/components/Toolbar/AnalysisToolbar.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { useEditorStore } from '../../store/editorStore'
|
||||
import { detectPassiveVoice } from '../../utils/passiveVoice'
|
||||
import { parseAnnotationsFromAIResponse } from '../../utils/annotationParser'
|
||||
import './Toolbar.css'
|
||||
|
||||
export function AnalysisToolbar(): JSX.Element {
|
||||
const {
|
||||
activeFilePath,
|
||||
activeFileContent,
|
||||
isDirty,
|
||||
analysisMode,
|
||||
annotations,
|
||||
isAILoading,
|
||||
setAnnotations,
|
||||
clearAnnotations,
|
||||
setAnalysisMode,
|
||||
addUserMessage,
|
||||
startAssistantMessage,
|
||||
appendToLastAssistantMessage,
|
||||
setAILoading,
|
||||
setAIError,
|
||||
chatHistory
|
||||
} = useEditorStore()
|
||||
|
||||
const hasFile = Boolean(activeFilePath)
|
||||
|
||||
const runPassiveVoice = (): void => {
|
||||
if (!hasFile) return
|
||||
const found = detectPassiveVoice(activeFileContent)
|
||||
setAnnotations(found)
|
||||
setAnalysisMode('passive_voice')
|
||||
}
|
||||
|
||||
const runAIAnalysis = async (mode: 'consistency' | 'style'): Promise<void> => {
|
||||
if (!activeFilePath || isAILoading) return
|
||||
|
||||
setAnalysisMode(mode)
|
||||
setAIError(null)
|
||||
|
||||
const prompt =
|
||||
mode === 'consistency'
|
||||
? 'Please check this chapter for consistency issues (character names, timeline, repeated phrases).'
|
||||
: 'Please analyze the style and pacing of this chapter and suggest improvements.'
|
||||
|
||||
addUserMessage(prompt)
|
||||
startAssistantMessage()
|
||||
setAILoading(true)
|
||||
|
||||
try {
|
||||
await window.api.streamAIMessage(
|
||||
{
|
||||
mode,
|
||||
documentContent: activeFileContent,
|
||||
documentPath: activeFilePath,
|
||||
conversationHistory: chatHistory
|
||||
.slice(-10)
|
||||
.map((m) => ({ role: m.role, content: m.content })),
|
||||
userMessage: prompt
|
||||
},
|
||||
(chunk: string) => {
|
||||
appendToLastAssistantMessage(chunk)
|
||||
}
|
||||
)
|
||||
|
||||
// Parse annotations from response
|
||||
const currentHistory = useEditorStore.getState().chatHistory
|
||||
const lastMsg = currentHistory[currentHistory.length - 1]
|
||||
if (lastMsg?.role === 'assistant' && lastMsg.content.length > 0) {
|
||||
const newAnnotations = parseAnnotationsFromAIResponse(lastMsg.content, activeFileContent)
|
||||
if (newAnnotations.length > 0) setAnnotations(newAnnotations)
|
||||
}
|
||||
} catch (err) {
|
||||
setAIError(err instanceof Error ? err.message : 'Analysis failed')
|
||||
} finally {
|
||||
setAILoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const passiveCount = annotations.filter((a) => a.type === 'passive_voice').length
|
||||
const otherCount = annotations.filter((a) => a.type !== 'passive_voice').length
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<div className="toolbar-left">
|
||||
<button
|
||||
className={`toolbar-btn${analysisMode === 'passive_voice' ? ' active' : ''}`}
|
||||
onClick={runPassiveVoice}
|
||||
disabled={!hasFile}
|
||||
title="Highlight passive voice sentences instantly (no AI required)"
|
||||
>
|
||||
Passive Voice
|
||||
{analysisMode === 'passive_voice' && passiveCount > 0 && (
|
||||
<span className="toolbar-badge">{passiveCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`toolbar-btn${analysisMode === 'consistency' ? ' active' : ''}`}
|
||||
onClick={() => runAIAnalysis('consistency')}
|
||||
disabled={!hasFile || isAILoading}
|
||||
title="Check character names, timeline, and repeated phrases via AI"
|
||||
>
|
||||
{isAILoading && analysisMode === 'consistency' ? 'Checking…' : 'Consistency'}
|
||||
{analysisMode === 'consistency' && otherCount > 0 && (
|
||||
<span className="toolbar-badge">{otherCount}</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'}
|
||||
{analysisMode === 'style' && otherCount > 0 && (
|
||||
<span className="toolbar-badge">{otherCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{annotations.length > 0 && (
|
||||
<button
|
||||
className="toolbar-btn toolbar-btn-clear"
|
||||
onClick={clearAnnotations}
|
||||
title="Remove all highlights"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="toolbar-right">
|
||||
{isDirty && (
|
||||
<span className="toolbar-dirty" title="Unsaved changes — press Cmd+S to save">
|
||||
●
|
||||
</span>
|
||||
)}
|
||||
{activeFilePath && (
|
||||
<span className="toolbar-filename">
|
||||
{activeFilePath.split('/').pop()?.replace(/\.md$/, '')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
97
src/renderer/components/Toolbar/Toolbar.css
Normal file
97
src/renderer/components/Toolbar/Toolbar.css
Normal file
@@ -0,0 +1,97 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 12px;
|
||||
background: var(--toolbar-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
position: relative;
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-family: var(--font-sans);
|
||||
padding: 4px 11px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover:not(:disabled) {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.toolbar-btn.active {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: rgba(167, 139, 95, 0.1);
|
||||
}
|
||||
|
||||
.toolbar-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.toolbar-btn-clear {
|
||||
border-color: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.toolbar-btn-clear:hover:not(:disabled) {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.toolbar-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;
|
||||
}
|
||||
|
||||
.toolbar-dirty {
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-filename {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-serif);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 260px;
|
||||
}
|
||||
Reference in New Issue
Block a user