✨ display word counts
This commit is contained in:
39
package-lock.json
generated
39
package-lock.json
generated
@@ -17,7 +17,10 @@
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"codemirror": "^6.0.1",
|
||||
"dompurify": "^3.3.1",
|
||||
"marked": "^17.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"zustand": "^4.5.0"
|
||||
@@ -1986,6 +1989,15 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dompurify": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||
"integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -2093,6 +2105,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/verror": {
|
||||
"version": "1.10.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
|
||||
@@ -3356,6 +3374,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz",
|
||||
@@ -4741,6 +4768,18 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.3.tgz",
|
||||
"integrity": "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
|
||||
@@ -31,7 +31,10 @@
|
||||
"@codemirror/view": "^6.35.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"codemirror": "^6.0.1",
|
||||
"dompurify": "^3.3.1",
|
||||
"marked": "^17.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"zustand": "^4.5.0"
|
||||
|
||||
@@ -85,6 +85,88 @@
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* Assistant messages use rendered Markdown — no pre-wrap needed */
|
||||
.chat-message-assistant .chat-message-content {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.chat-message-markdown {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.chat-message-markdown p {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
.chat-message-markdown p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chat-message-markdown h1,
|
||||
.chat-message-markdown h2,
|
||||
.chat-message-markdown h3 {
|
||||
color: var(--heading-color);
|
||||
font-family: var(--font-serif);
|
||||
font-weight: 600;
|
||||
margin: 12px 0 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.chat-message-markdown h1 { font-size: 15px; }
|
||||
.chat-message-markdown h2 { font-size: 14px; }
|
||||
.chat-message-markdown h3 { font-size: 13px; }
|
||||
|
||||
.chat-message-markdown ul,
|
||||
.chat-message-markdown ol {
|
||||
margin: 4px 0 8px;
|
||||
padding-left: 18px;
|
||||
}
|
||||
.chat-message-markdown li {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.chat-message-markdown code {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.chat-message-markdown pre {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 5px;
|
||||
padding: 10px 12px;
|
||||
overflow-x: auto;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.chat-message-markdown pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.chat-message-markdown blockquote {
|
||||
border-left: 3px solid var(--accent);
|
||||
margin: 6px 0;
|
||||
padding: 2px 10px;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chat-message-markdown strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-message-markdown em {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chat-message-markdown hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.chat-message-user .chat-message-content {
|
||||
background: var(--user-message-bg);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
import { useMemo } from 'react'
|
||||
import { marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
import type { ChatMessage } from '../../types/editor'
|
||||
|
||||
marked.setOptions({ breaks: true })
|
||||
|
||||
interface Props {
|
||||
message: ChatMessage
|
||||
}
|
||||
|
||||
export function ChatMessageItem({ message }: Props): JSX.Element {
|
||||
const html = useMemo(() => {
|
||||
if (message.role !== 'assistant' || !message.content) return null
|
||||
const raw = marked.parse(message.content) as string
|
||||
return DOMPurify.sanitize(raw)
|
||||
}, [message.role, message.content])
|
||||
|
||||
return (
|
||||
<div className={`chat-message chat-message-${message.role}`}>
|
||||
<div className="chat-message-label">
|
||||
{message.role === 'user' ? 'You' : 'Editor AI'}
|
||||
</div>
|
||||
<div className="chat-message-content">
|
||||
{message.content || <span className="chat-streaming-cursor">▍</span>}
|
||||
{html ? (
|
||||
<div
|
||||
className="chat-message-markdown"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
) : (
|
||||
message.content || <span className="chat-streaming-cursor">▍</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { EditorView, Decoration, type DecorationSet } from '@codemirror/view'
|
||||
import { EditorState, StateField, StateEffect, RangeSetBuilder } from '@codemirror/state'
|
||||
import { EditorState, StateField, StateEffect, RangeSetBuilder, Compartment } from '@codemirror/state'
|
||||
import { markdown } from '@codemirror/lang-markdown'
|
||||
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
|
||||
import { history, defaultKeymap, historyKeymap } from '@codemirror/commands'
|
||||
@@ -44,12 +44,14 @@ const annotationField = StateField.define<DecorationSet>({
|
||||
provide: (f) => EditorView.decorations.from(f)
|
||||
})
|
||||
|
||||
// Dark gothic theme for CodeMirror
|
||||
const hohoffTheme = EditorView.theme(
|
||||
const themeCompartment = new Compartment()
|
||||
|
||||
function buildTheme(fontSize: number): ReturnType<typeof EditorView.theme> {
|
||||
return EditorView.theme(
|
||||
{
|
||||
'&': {
|
||||
height: '100%',
|
||||
fontSize: '15px',
|
||||
fontSize: `${fontSize}px`,
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--text-primary)'
|
||||
},
|
||||
@@ -59,7 +61,7 @@ const hohoffTheme = EditorView.theme(
|
||||
overflow: 'auto'
|
||||
},
|
||||
'.cm-content': {
|
||||
padding: '24px 32px',
|
||||
padding: '16px 20px',
|
||||
maxWidth: '740px',
|
||||
margin: '0 auto',
|
||||
caretColor: 'var(--accent)'
|
||||
@@ -67,7 +69,7 @@ const hohoffTheme = EditorView.theme(
|
||||
'.cm-cursor': { borderLeftColor: 'var(--accent)' },
|
||||
'.cm-selectionBackground': { backgroundColor: 'rgba(167,139,95,0.2)' },
|
||||
'&.cm-focused .cm-selectionBackground': { backgroundColor: 'rgba(167,139,95,0.3)' },
|
||||
'.cm-line': { padding: '0' },
|
||||
'.cm-line': { padding: '0', marginBottom: '6px' },
|
||||
'.cm-gutters': { display: 'none' },
|
||||
'.cm-activeLine': { backgroundColor: 'transparent' },
|
||||
'.cm-activeLineGutter': { backgroundColor: 'transparent' },
|
||||
@@ -98,11 +100,12 @@ const hohoffTheme = EditorView.theme(
|
||||
},
|
||||
{ dark: true }
|
||||
)
|
||||
}
|
||||
|
||||
export function MarkdownEditor(): JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const viewRef = useRef<EditorView | null>(null)
|
||||
const { activeFilePath, activeFileContent, setContent, annotations } = useEditorStore()
|
||||
const { activeFilePath, activeFileContent, setContent, annotations, fontSize } = useEditorStore()
|
||||
|
||||
// Initialize CodeMirror once
|
||||
useEffect(() => {
|
||||
@@ -117,7 +120,7 @@ export function MarkdownEditor(): JSX.Element {
|
||||
markdown(),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
annotationField,
|
||||
hohoffTheme,
|
||||
themeCompartment.of(buildTheme(fontSize)),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
@@ -158,6 +161,13 @@ export function MarkdownEditor(): JSX.Element {
|
||||
view.dispatch({ effects: setAnnotationsEffect.of(annotations) })
|
||||
}, [annotations])
|
||||
|
||||
// Reconfigure theme when font size changes
|
||||
useEffect(() => {
|
||||
const view = viewRef.current
|
||||
if (!view) return
|
||||
view.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize)) })
|
||||
}, [fontSize])
|
||||
|
||||
return (
|
||||
<div className="editor-container">
|
||||
{!activeFilePath && (
|
||||
|
||||
@@ -31,7 +31,9 @@ export function AnalysisToolbar(): JSX.Element {
|
||||
setAIError,
|
||||
chatHistory,
|
||||
projectWordCount,
|
||||
setProjectWordCount
|
||||
setProjectWordCount,
|
||||
fontSize,
|
||||
setFontSize
|
||||
} = useEditorStore()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -154,6 +156,24 @@ export function AnalysisToolbar(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="toolbar-right">
|
||||
<div className="toolbar-fontsize">
|
||||
<button
|
||||
className="toolbar-fontsize-btn"
|
||||
onClick={() => setFontSize(fontSize - 1)}
|
||||
disabled={fontSize <= 11}
|
||||
title="Decrease font size"
|
||||
>
|
||||
A−
|
||||
</button>
|
||||
<button
|
||||
className="toolbar-fontsize-btn"
|
||||
onClick={() => setFontSize(fontSize + 1)}
|
||||
disabled={fontSize >= 24}
|
||||
title="Increase font size"
|
||||
>
|
||||
A+
|
||||
</button>
|
||||
</div>
|
||||
{activeFilePath && (
|
||||
<span
|
||||
className="toolbar-wordcount"
|
||||
|
||||
@@ -96,6 +96,37 @@
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
.toolbar-fontsize {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-fontsize-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-sans);
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.toolbar-fontsize-btn:hover:not(:disabled) {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.toolbar-fontsize-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.toolbar-wordcount {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
|
||||
@@ -38,6 +38,10 @@ interface EditorState {
|
||||
// Word counts
|
||||
projectWordCount: number
|
||||
setProjectWordCount: (count: number) => void
|
||||
|
||||
// Editor font size
|
||||
fontSize: number
|
||||
setFontSize: (size: number) => void
|
||||
}
|
||||
|
||||
export const useEditorStore = create<EditorState>((set, get) => ({
|
||||
@@ -122,5 +126,12 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
||||
setAnalysisMode: (analysisMode) => set({ analysisMode }),
|
||||
|
||||
projectWordCount: 0,
|
||||
setProjectWordCount: (projectWordCount) => set({ projectWordCount })
|
||||
setProjectWordCount: (projectWordCount) => set({ projectWordCount }),
|
||||
|
||||
fontSize: Number(localStorage.getItem('editorFontSize')) || 15,
|
||||
setFontSize: (size) => {
|
||||
const clamped = Math.max(11, Math.min(24, size))
|
||||
localStorage.setItem('editorFontSize', String(clamped))
|
||||
set({ fontSize: clamped })
|
||||
}
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user