Contribute rewrite Phase 5: inline word-diff in manual pending card
The §8.11 manual-edit card was a stub — "{N} paragraphs edited
directly" plus a countdown and a Save-now button. The spec says the
card should grow one inline word-diff per touched paragraph as the
contributor types, in the same green/red register the Phase 3 accepted
overlay uses. Phase 5 lands that.
In RFCView.jsx, the 800ms-debounced handleEditorUpdate now computes a
per-paragraph token diff alongside the existing paragraph count and
stores both on manualPending. Baseline is the same Phase 4 gutter
source — originalSourceLinesRef.current, the last server-confirmed
body split on blank lines — so the card resets to empty the same
moment the gutter clears (accept/decline/manualFlush/branch-switch).
In ChangePanel.jsx, diffWords is exported (it was already the AI
card's inline-diff engine — token-level LCS over a whitespace-
preserving /(\\s+)/ split, ~30 lines, no runtime dep). The manual
card consumes the same tokens. Wholly-inserted paragraphs render as
all-insert blocks; wholly-deleted paragraphs as all-delete blocks.
Visual register is intentionally shared with Phase 3's preview
overlay: same #dcfce7/#166534 inserts, same #fee2e2/#991b1b
strike-through deletes. Selectors are scoped under .change-manual-diff
rather than reusing .markdown-preview .tracked-* since the card lives
outside the preview surface.
Pre-fancy stance, matching Phase 4's gutter: the diff is index-aligned
against the baseline, so adding a paragraph in the middle lights up
the rest of the doc. Tolerated as the "you've touched stuff below this
point" cue. An LCS-anchored future pass can fix it.
Verification gap matching Phases 2/3/4: backend was not running this
session, so the live RFCView → real-branch flow wasn't exercised.
Drove the Vite preview sandbox instead — mounted ChangePanel with a
hand-built diffs payload, confirmed three blocks render (mixed edit,
wholly-inserted, wholly-deleted), inserts/deletes carry the expected
computed colors, no console errors. Backend integration suite still
green (125 passed).
This commit is contained in:
@@ -879,6 +879,33 @@
|
|||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* §8.11 Phase 5 — per-paragraph word-diff inside the pending manual
|
||||||
|
card. Shares Phase 3's green/red register with .markdown-preview
|
||||||
|
.tracked-insert / .tracked-delete so the contributor sees one
|
||||||
|
"change layer" vocabulary across the preview pane (accepted) and the
|
||||||
|
change card (their own pending typing). Scoped under its own class
|
||||||
|
rather than reusing the preview selectors since the card is not
|
||||||
|
inside .markdown-preview. */
|
||||||
|
.change-manual-diff {
|
||||||
|
margin: 6px 0;
|
||||||
|
display: flex; flex-direction: column; gap: 6px;
|
||||||
|
}
|
||||||
|
.change-manual-diff-block {
|
||||||
|
font-size: 12px; line-height: 1.6;
|
||||||
|
background: #f9fafb; border-radius: 4px;
|
||||||
|
padding: 6px 8px; border: 1px solid #e5e5e5;
|
||||||
|
white-space: pre-wrap; word-break: break-word;
|
||||||
|
}
|
||||||
|
.change-manual-diff .manual-diff-insert {
|
||||||
|
background: #dcfce7; color: #166534;
|
||||||
|
border-radius: 2px; padding: 1px 2px;
|
||||||
|
}
|
||||||
|
.change-manual-diff .manual-diff-delete {
|
||||||
|
background: #fee2e2; color: #991b1b;
|
||||||
|
text-decoration: line-through;
|
||||||
|
border-radius: 2px; padding: 1px 2px;
|
||||||
|
}
|
||||||
.btn-save-now {
|
.btn-save-now {
|
||||||
background: none; border: 1px solid #d4d4d4;
|
background: none; border: 1px solid #d4d4d4;
|
||||||
border-radius: 4px; padding: 2px 8px;
|
border-radius: 4px; padding: 2px 8px;
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { useState, useEffect, useRef } from 'react'
|
|||||||
|
|
||||||
const PREVIEW_LENGTH = 220
|
const PREVIEW_LENGTH = 220
|
||||||
|
|
||||||
function diffWords(original, proposed) {
|
// Word-level LCS over whitespace-preserving tokens. Used by both the
|
||||||
|
// AI change-card InlineDiff (original→proposed) and Phase 5's
|
||||||
|
// manual-pending-card per-paragraph diff (baseline→current).
|
||||||
|
export function diffWords(original, proposed) {
|
||||||
const a = (original || '').split(/(\s+)/)
|
const a = (original || '').split(/(\s+)/)
|
||||||
const b = (proposed || '').split(/(\s+)/)
|
const b = (proposed || '').split(/(\s+)/)
|
||||||
const m = a.length, n = b.length
|
const m = a.length, n = b.length
|
||||||
@@ -85,6 +88,7 @@ export default function ChangePanel({
|
|||||||
{manualPendingStatus && (
|
{manualPendingStatus && (
|
||||||
<ManualPendingCard
|
<ManualPendingCard
|
||||||
paragraphCount={manualPendingStatus.paragraphCount}
|
paragraphCount={manualPendingStatus.paragraphCount}
|
||||||
|
diffs={manualPendingStatus.diffs}
|
||||||
savingIn={manualPendingStatus.savingIn}
|
savingIn={manualPendingStatus.savingIn}
|
||||||
onSaveNow={manualPendingStatus.onSaveNow}
|
onSaveNow={manualPendingStatus.onSaveNow}
|
||||||
/>
|
/>
|
||||||
@@ -114,7 +118,7 @@ export default function ChangePanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ManualPendingCard({ paragraphCount, savingIn, onSaveNow }) {
|
function ManualPendingCard({ paragraphCount, diffs, savingIn, onSaveNow }) {
|
||||||
return (
|
return (
|
||||||
<div className="change-item type-manual state-pending">
|
<div className="change-item type-manual state-pending">
|
||||||
<div className="change-meta">
|
<div className="change-meta">
|
||||||
@@ -124,6 +128,19 @@ function ManualPendingCard({ paragraphCount, savingIn, onSaveNow }) {
|
|||||||
<div className="change-label">
|
<div className="change-label">
|
||||||
{paragraphCount} paragraph{paragraphCount === 1 ? '' : 's'} edited directly
|
{paragraphCount} paragraph{paragraphCount === 1 ? '' : 's'} edited directly
|
||||||
</div>
|
</div>
|
||||||
|
{diffs && diffs.length > 0 && (
|
||||||
|
<div className="change-manual-diff">
|
||||||
|
{diffs.map(d => (
|
||||||
|
<div key={d.baselineIndex} className="change-manual-diff-block">
|
||||||
|
{d.tokens.map((t, idx) =>
|
||||||
|
t.type === 'same' ? <span key={idx}>{t.text}</span> :
|
||||||
|
t.type === 'add' ? <span key={idx} className="manual-diff-insert">{t.text}</span> :
|
||||||
|
<span key={idx} className="manual-diff-delete">{t.text}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="change-manual-status">
|
<div className="change-manual-status">
|
||||||
unsaved · auto-save in {savingIn}
|
unsaved · auto-save in {savingIn}
|
||||||
<button type="button" className="btn-save-now" onClick={onSaveNow}>Save now</button>
|
<button type="button" className="btn-save-now" onClick={onSaveNow}>Save now</button>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import SelectionTooltip from './SelectionTooltip.jsx'
|
|||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import PromptBar from './PromptBar.jsx'
|
import PromptBar from './PromptBar.jsx'
|
||||||
import ChatPanel from './ChatPanel.jsx'
|
import ChatPanel from './ChatPanel.jsx'
|
||||||
import ChangePanel from './ChangePanel.jsx'
|
import ChangePanel, { diffWords } from './ChangePanel.jsx'
|
||||||
import DiffView from './DiffView.jsx'
|
import DiffView from './DiffView.jsx'
|
||||||
import PRModal from './PRModal.jsx'
|
import PRModal from './PRModal.jsx'
|
||||||
import GraduateDialog from './GraduateDialog.jsx'
|
import GraduateDialog from './GraduateDialog.jsx'
|
||||||
@@ -214,12 +214,25 @@ export default function RFCView({ viewer }) {
|
|||||||
const current = splitSourceParagraphs(doc)
|
const current = splitSourceParagraphs(doc)
|
||||||
const baseline = originalSourceLinesRef.current || []
|
const baseline = originalSourceLinesRef.current || []
|
||||||
const len = Math.max(current.length, baseline.length)
|
const len = Math.max(current.length, baseline.length)
|
||||||
let changed = 0
|
// §8.11 Phase 5: per-paragraph word-diff for the pending manual card.
|
||||||
|
// Naive index-aligned compare against the same baseline Phase 4's
|
||||||
|
// gutter uses; tolerates the insert-in-middle cascade as the "you've
|
||||||
|
// touched stuff below this point" cue. An LCS-anchored pass could
|
||||||
|
// refine this later.
|
||||||
|
const diffs = []
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
if ((current[i] ?? '') !== (baseline[i] ?? '')) changed++
|
const a = baseline[i] ?? ''
|
||||||
|
const b = current[i] ?? ''
|
||||||
|
if (a === b) continue
|
||||||
|
diffs.push({
|
||||||
|
baselineIndex: i,
|
||||||
|
baselineText: a,
|
||||||
|
currentText: b,
|
||||||
|
tokens: diffWords(a, b),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (changed > 0) {
|
if (diffs.length > 0) {
|
||||||
setManualPending({ paragraphCount: changed })
|
setManualPending({ paragraphCount: diffs.length, diffs })
|
||||||
setManualCountdown({ deadline: Date.now() + MANUAL_IDLE_MS })
|
setManualCountdown({ deadline: Date.now() + MANUAL_IDLE_MS })
|
||||||
} else {
|
} else {
|
||||||
setManualPending(null)
|
setManualPending(null)
|
||||||
@@ -754,6 +767,7 @@ export default function RFCView({ viewer }) {
|
|||||||
focusedChangeId={focusedChangeId}
|
focusedChangeId={focusedChangeId}
|
||||||
manualPendingStatus={manualPending ? {
|
manualPendingStatus={manualPending ? {
|
||||||
paragraphCount: manualPending.paragraphCount,
|
paragraphCount: manualPending.paragraphCount,
|
||||||
|
diffs: manualPending.diffs,
|
||||||
savingIn: manualCountdownLabel(manualCountdown),
|
savingIn: manualCountdownLabel(manualCountdown),
|
||||||
onSaveNow: handleSaveNow,
|
onSaveNow: handleSaveNow,
|
||||||
} : null}
|
} : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user