Redesign writing assistant UI: right-side panel, diff view, proofread, floating inline assist
- sectionParser.ts: add parseFallbackSections() — splits heading-less notes by paragraph boundaries; single-line ≤120 char paragraphs become pseudo-headings so Q&A-style notes get individual selectable sections in the assist panel - useAssist.ts: add DiffLine type + computeDiff() (LCS line diff), isProofreading ref, diff computed, proofread() method (full-document one-click), reset isProofreading in accept() and reject() - NoteEditorView + TaskEditorView: complete layout redesign - Assist panel moves from bottom 33% to right-side 320px column (flex-row) - ✨ Assist toggle button in toolbar, state persisted to localStorage - Floating ✨ pill button (teleported to <body>) above text selections; click opens panel with selection pre-loaded and instruction textarea focused - Proofread button in panel header: sends entire document, labels streaming as "Proofreading document..." and review as "Document proofread" - Review state shows LCS line-level diff (red removed, green added); "Show full text" toggle switches to rendered proposal - Mobile (≤768px): panel drops to bottom 45% height - theme.css: add global .inline-assist-btn styles for teleported floating button - Site-wide max-width increase: list/chat/settings views 960px→1200px; viewer layout 1200px→1400px (content area 960px→1100px); editor pages already at 1400px Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,73 @@ export interface MarkdownSection {
|
||||
|
||||
const HEADING_RE = /^(#{1,6})\s+(.*)$/gm;
|
||||
|
||||
function parseFallbackSections(body: string): MarkdownSection[] {
|
||||
if (!body.trim())
|
||||
return [];
|
||||
|
||||
const isPseudoHeading = (text: string): boolean => {
|
||||
const t = text.trim();
|
||||
return (
|
||||
t.length > 0 && t.length <= 120 &&
|
||||
!t.includes('\n') &&
|
||||
!/^[-*+>]/.test(t) &&
|
||||
!/^\d+\./.test(t) &&
|
||||
!/^`{3,}/.test(t)
|
||||
);
|
||||
};
|
||||
|
||||
// Build paragraph list with exact start/end offsets
|
||||
const separatorRe = /\n{2,}/g;
|
||||
const paragraphs: Array<{ text: string; start: number; end: number }> = [];
|
||||
let lastEnd = 0;
|
||||
let sep: RegExpExecArray | null;
|
||||
while ((sep = separatorRe.exec(body)) !== null) {
|
||||
if (sep.index > lastEnd)
|
||||
paragraphs.push({ text: body.slice(lastEnd, sep.index), start: lastEnd, end: sep.index });
|
||||
lastEnd = sep.index + sep[0].length;
|
||||
}
|
||||
if (lastEnd < body.length)
|
||||
paragraphs.push({ text: body.slice(lastEnd), start: lastEnd, end: body.length });
|
||||
|
||||
if (paragraphs.length === 0)
|
||||
return [{ heading: '', level: 0, content: body, startOffset: 0, endOffset: body.length }];
|
||||
|
||||
const headingIndices = new Set(
|
||||
paragraphs.map((p, i) => isPseudoHeading(p.text) ? i : -1).filter(i => i >= 0)
|
||||
);
|
||||
if (headingIndices.size === 0)
|
||||
return [{ heading: '', level: 0, content: body, startOffset: 0, endOffset: body.length }];
|
||||
|
||||
const sections: MarkdownSection[] = [];
|
||||
const headingIdxArray = [...headingIndices].sort((a, b) => a - b);
|
||||
const firstHi = headingIdxArray[0];
|
||||
|
||||
// Preamble (content before first pseudo-heading)
|
||||
if (firstHi > 0) {
|
||||
sections.push({
|
||||
heading: '', level: 0,
|
||||
content: body.slice(paragraphs[0].start, paragraphs[firstHi].start),
|
||||
startOffset: paragraphs[0].start,
|
||||
endOffset: paragraphs[firstHi].start,
|
||||
});
|
||||
}
|
||||
|
||||
// One section per pseudo-heading
|
||||
for (let h = 0; h < headingIdxArray.length; h++) {
|
||||
const hi = headingIdxArray[h];
|
||||
const nextHi = h + 1 < headingIdxArray.length ? headingIdxArray[h + 1] : null;
|
||||
const sectionEnd = nextHi !== null ? paragraphs[nextHi].start : body.length;
|
||||
sections.push({
|
||||
heading: paragraphs[hi].text.trim(),
|
||||
level: 0,
|
||||
content: body.slice(paragraphs[hi].start, sectionEnd),
|
||||
startOffset: paragraphs[hi].start,
|
||||
endOffset: sectionEnd,
|
||||
});
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
export function parseMarkdownSections(body: string): MarkdownSection[] {
|
||||
const sections: MarkdownSection[] = [];
|
||||
const matches: { index: number; level: number; heading: string }[] = [];
|
||||
@@ -23,17 +90,7 @@ export function parseMarkdownSections(body: string): MarkdownSection[] {
|
||||
|
||||
// Preamble: text before the first heading
|
||||
if (matches.length === 0) {
|
||||
// Entire body is a single preamble section
|
||||
if (body.length > 0) {
|
||||
sections.push({
|
||||
heading: "",
|
||||
level: 0,
|
||||
content: body,
|
||||
startOffset: 0,
|
||||
endOffset: body.length,
|
||||
});
|
||||
}
|
||||
return sections;
|
||||
return parseFallbackSections(body);
|
||||
}
|
||||
|
||||
if (matches[0].index > 0) {
|
||||
|
||||
Reference in New Issue
Block a user