🎉 initial commit

This commit is contained in:
2026-02-20 14:17:34 +10:00
commit e21d1c7b58
33 changed files with 8207 additions and 0 deletions

View 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>
)
}

View 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;
}