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:
@@ -14,6 +14,33 @@ export interface AssistTarget {
|
||||
endOffset: number;
|
||||
}
|
||||
|
||||
export interface DiffLine {
|
||||
type: 'equal' | 'delete' | 'insert';
|
||||
text: string;
|
||||
}
|
||||
|
||||
function computeDiff(a: string, b: string): DiffLine[] {
|
||||
const aLines = a.split('\n');
|
||||
const bLines = b.split('\n');
|
||||
const m = aLines.length, n = bLines.length;
|
||||
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
||||
for (let i = m - 1; i >= 0; i--)
|
||||
for (let j = n - 1; j >= 0; j--)
|
||||
dp[i][j] = aLines[i] === bLines[j]
|
||||
? dp[i+1][j+1] + 1
|
||||
: Math.max(dp[i+1][j], dp[i][j+1]);
|
||||
const result: DiffLine[] = [];
|
||||
let i = 0, j = 0;
|
||||
while (i < m && j < n) {
|
||||
if (aLines[i] === bLines[j]) { result.push({ type: 'equal', text: aLines[i++] }); j++; }
|
||||
else if (dp[i+1][j] >= dp[i][j+1]) result.push({ type: 'delete', text: aLines[i++] });
|
||||
else result.push({ type: 'insert', text: bLines[j++] });
|
||||
}
|
||||
while (i < m) result.push({ type: 'delete', text: aLines[i++] });
|
||||
while (j < n) result.push({ type: 'insert', text: bLines[j++] });
|
||||
return result;
|
||||
}
|
||||
|
||||
export function useAssist(body: Ref<string>) {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
@@ -25,6 +52,7 @@ export function useAssist(body: Ref<string>) {
|
||||
const streamingText = ref("");
|
||||
const proposedText = ref("");
|
||||
const error = ref("");
|
||||
const isProofreading = ref(false);
|
||||
|
||||
// Snapshot of body at the time a section/selection was chosen
|
||||
let bodySnapshot = "";
|
||||
@@ -56,6 +84,11 @@ export function useAssist(body: Ref<string>) {
|
||||
state.value !== "streaming"
|
||||
);
|
||||
|
||||
const diff = computed<DiffLine[]>(() => {
|
||||
if (state.value !== 'review' || !target.value || !proposedText.value) return [];
|
||||
return computeDiff(target.value.text, proposedText.value);
|
||||
});
|
||||
|
||||
function refreshSections() {
|
||||
sections.value = parseMarkdownSections(body.value);
|
||||
}
|
||||
@@ -145,6 +178,20 @@ export function useAssist(body: Ref<string>) {
|
||||
}
|
||||
}
|
||||
|
||||
async function proofread() {
|
||||
if (state.value === 'streaming') return;
|
||||
isProofreading.value = true;
|
||||
selectedSection.value = null;
|
||||
customSelection.value = { start: 0, end: body.value.length, text: body.value };
|
||||
bodySnapshot = body.value;
|
||||
error.value = '';
|
||||
instruction.value =
|
||||
"Proofread for clarity, grammar, spelling, and flow. " +
|
||||
"Preserve the author's voice and all factual content. " +
|
||||
"Return only the improved text without explanation.";
|
||||
await submit();
|
||||
}
|
||||
|
||||
function accept(): string {
|
||||
if (!target.value || !proposedText.value) return body.value;
|
||||
|
||||
@@ -177,6 +224,7 @@ export function useAssist(body: Ref<string>) {
|
||||
proposedText.value = "";
|
||||
error.value = "";
|
||||
state.value = "idle";
|
||||
isProofreading.value = false;
|
||||
|
||||
return newBody;
|
||||
}
|
||||
@@ -186,6 +234,7 @@ export function useAssist(body: Ref<string>) {
|
||||
streamingText.value = "";
|
||||
error.value = "";
|
||||
state.value = "idle";
|
||||
isProofreading.value = false;
|
||||
// Keep selection + instruction for retry
|
||||
}
|
||||
|
||||
@@ -205,11 +254,14 @@ export function useAssist(body: Ref<string>) {
|
||||
proposedText,
|
||||
error,
|
||||
canSubmit,
|
||||
diff,
|
||||
isProofreading,
|
||||
refreshSections,
|
||||
selectSection,
|
||||
selectTextRange,
|
||||
clearSelection,
|
||||
submit,
|
||||
proofread,
|
||||
accept,
|
||||
reject,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user