Contribute rewrite Phase 1: CM6 markdown source editor
Swap the Contribute-mode editing surface from Tiptap WYSIWYG to a CodeMirror 6 markdown source editor. Discuss mode and any read-only viewing continues to render through Tiptap. The §8.11 manual-edit debounce now reads the raw markdown source via CodeMirror's doc, eliminating the lossy `editor.getText()` round-trip that RFCView.jsx flagged as a §19.2 candidate; what the contributor typed is exactly what gets POSTed to manual flush. The §8.10 paragraph-margin gutter accent on Tiptap is dropped — it was dead in read-only Discuss anyway, and Phase 4 of the Contribute rewrite adds a change-anchored gutter against the CM6 raw pane. The Tiptap-side accept-time injection of <span class="tracked-*"> markup also drops out — CM6 can't render those spans, and Phase 3's preview pane is the proper home for tracked changes. The reviewMode / DiffView toggle still works against marked-rendered HTML in the interim until Phase 7 retires it. Notes: - Bundle gzipped grows ~50KB for CM6 modules (state/view/commands/ language/lang-markdown). Mermaid in Phase 2 will be the larger lift and should lazy-load. - Verified the CM6 editor mounts cleanly via Vite dev preview: ref handle (`view/getDoc/setDoc`) is wired, `onUpdate` fires on doc changes, gutter / line-numbers / active-line all render, and the source round-trips verbatim. Could not drive the full RFCView Contribute flow end-to-end without a running backend; the manual countdown + save-now + accept/decline pathways are verified by inspection only. - 125 backend integration tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-12
@@ -1,4 +1,4 @@
|
||||
/* Adapted from the prototype's App.css per §18, narrowed to slice-1 surfaces. */
|
||||
/* App.css — narrowed to slice-1 surfaces. */
|
||||
|
||||
.boot {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
@@ -441,13 +441,6 @@
|
||||
.editor-content .tiptap ul, .editor-content .tiptap ol { padding-left: 24px; }
|
||||
.editor-content .tiptap code { background: #f0f0ee; padding: 1px 5px; border-radius: 3px; font-size: 13px; }
|
||||
|
||||
.editor-content .tiptap .paragraph-changed {
|
||||
border-left: 3px solid #f59e0b;
|
||||
padding-left: 10px;
|
||||
margin-left: -13px;
|
||||
background: linear-gradient(to right, #fffbeb 0%, transparent 60%);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
.editor-content .tiptap .selection-highlight {
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border-radius: 2px;
|
||||
@@ -463,6 +456,27 @@
|
||||
border-radius: 2px; padding: 1px 2px; cursor: pointer;
|
||||
}
|
||||
|
||||
/* CodeMirror 6 source editor (Contribute mode). */
|
||||
.cm-source-editor {
|
||||
flex: 1; min-height: 0;
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cm-source-editor .cm-editor {
|
||||
flex: 1; min-height: 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 13px; line-height: 1.6;
|
||||
background: #fff;
|
||||
}
|
||||
.cm-source-editor .cm-editor.cm-focused { outline: none; }
|
||||
.cm-source-editor .cm-scroller { padding: 24px 0; }
|
||||
.cm-source-editor .cm-content { padding: 0 32px; max-width: 880px; }
|
||||
.cm-source-editor .cm-gutters {
|
||||
background: #fafafa; border-right: 1px solid #f0f0ee; color: #b0b0b0;
|
||||
}
|
||||
.cm-source-editor .cm-activeLineGutter { background: #f3f4f6; color: #555; }
|
||||
.cm-source-editor .cm-activeLine { background: #fafbfc; }
|
||||
|
||||
.readonly-bar {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
padding: 10px 16px; text-align: center;
|
||||
@@ -1236,8 +1250,8 @@
|
||||
color: #fbbf24; /* admin link sits a notch warmer to signal authority */
|
||||
}
|
||||
|
||||
/* /philosophy — the §14.2 read surface. The body inherits the
|
||||
prototype's markdown styling; the header is a thin chrome strip. */
|
||||
/* /philosophy — the §14.2 read surface. The body uses the shared
|
||||
markdown styling; the header is a thin chrome strip. */
|
||||
.chrome-pane {
|
||||
flex: 1; min-width: 0; overflow: auto;
|
||||
padding: 0; background: #fff;
|
||||
@@ -1298,8 +1312,8 @@
|
||||
}
|
||||
|
||||
/* Richer landing page (§14.1) — adds the three-item deck under the
|
||||
pitch. The .landing container's flex centering stays from the
|
||||
prototype; the new content lives inside .landing-inner. */
|
||||
pitch. The .landing container handles flex centering; the new
|
||||
content lives inside .landing-inner. */
|
||||
.landing-inner {
|
||||
max-width: 620px;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
// Editor.jsx — the §8 center-column editor.
|
||||
// Editor.jsx — the Discuss-mode read-only renderer (and main/no-contribute
|
||||
// fallback). Tiptap on ProseMirror per §18; the Contribute-mode editing
|
||||
// surface moved to MarkdownSourceEditor (CodeMirror 6) in the Phase 1
|
||||
// rewrite of Contribute, leaving this surface for read-only rendering
|
||||
// plus the §8.12 selection tooltip's coords.
|
||||
//
|
||||
// Tiptap on ProseMirror per §18. Two ProseMirror plugins live alongside
|
||||
// StarterKit:
|
||||
//
|
||||
// • paragraphDiff — the §8.10 paragraph-margin gutter accent. Compares
|
||||
// each paragraph against an open-session baseline (the
|
||||
// `originalParagraphsRef` ref the parent owns and refreshes when the
|
||||
// baseline shifts — e.g. on branch switch or a server-side flush).
|
||||
// One ProseMirror plugin still lives alongside StarterKit:
|
||||
//
|
||||
// • selectionHighlight — keeps a selected passage highlighted while
|
||||
// focus moves to the §8.12 selection tooltip. Driven by meta
|
||||
// transactions dispatched from the parent.
|
||||
//
|
||||
// The inline tracked-delete / tracked-insert markup from §8.10 is
|
||||
// session-local HTML the parent injects via `editor.commands.setContent`
|
||||
// when a change is accepted; the editor itself doesn't own that state.
|
||||
// On reload the markup clears and DiffView (toolbar toggle) is the
|
||||
// durable read of accepted changes.
|
||||
// The paragraph-margin gutter accent that used to live here is dropped;
|
||||
// Phase 4 of the Contribute rewrite adds a proper change-anchored gutter
|
||||
// against the CodeMirror raw pane.
|
||||
|
||||
import { useEditor, EditorContent, Extension } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
@@ -25,47 +21,6 @@ import { marked } from 'marked'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
|
||||
// ── Paragraph diff plugin ────────────────────────────────────────────────
|
||||
|
||||
const diffKey = new PluginKey('paragraphDiff')
|
||||
|
||||
function makeDiffPlugin(originalParagraphsRef) {
|
||||
return new Plugin({
|
||||
key: diffKey,
|
||||
props: {
|
||||
decorations(state) {
|
||||
const originals = originalParagraphsRef?.current
|
||||
if (!originals || originals.length === 0) return DecorationSet.empty
|
||||
|
||||
const decorations = []
|
||||
let idx = 0
|
||||
state.doc.descendants((node, pos) => {
|
||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
||||
const current = node.textContent.trim()
|
||||
const original = (originals[idx] ?? '').trim()
|
||||
if (current !== original) {
|
||||
decorations.push(
|
||||
Decoration.node(pos, pos + node.nodeSize, { class: 'paragraph-changed' })
|
||||
)
|
||||
}
|
||||
idx++
|
||||
}
|
||||
})
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function DiffExtension(originalParagraphsRef) {
|
||||
return Extension.create({
|
||||
name: 'paragraphDiff',
|
||||
addProseMirrorPlugins() {
|
||||
return [makeDiffPlugin(originalParagraphsRef)]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Selection highlight plugin ────────────────────────────────────────────
|
||||
|
||||
export const selectionHighlightKey = new PluginKey('selectionHighlight')
|
||||
@@ -110,7 +65,6 @@ function SelectionHighlightExtension() {
|
||||
export default function Editor({
|
||||
content,
|
||||
editorRef,
|
||||
originalParagraphsRef,
|
||||
onSelectionChange,
|
||||
onUpdate,
|
||||
editable = true,
|
||||
@@ -132,7 +86,6 @@ export default function Editor({
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
DiffExtension(originalParagraphsRef),
|
||||
SelectionHighlightExtension(),
|
||||
],
|
||||
content: '<p></p>',
|
||||
@@ -167,20 +120,10 @@ export default function Editor({
|
||||
}
|
||||
}, [editor, onSelectionChange, reportSelection])
|
||||
|
||||
// Reload content + snapshot baseline paragraphs.
|
||||
useEffect(() => {
|
||||
if (!editor || content == null) return
|
||||
const html = marked.parse(content)
|
||||
editor.commands.setContent(html, false)
|
||||
if (originalParagraphsRef) {
|
||||
const paragraphs = []
|
||||
editor.state.doc.descendants(node => {
|
||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
||||
paragraphs.push(node.textContent.trim())
|
||||
}
|
||||
})
|
||||
originalParagraphsRef.current = paragraphs
|
||||
}
|
||||
}, [content, editor])
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// MarkdownSourceEditor.jsx — the Contribute-mode raw markdown source editor.
|
||||
//
|
||||
// CodeMirror 6 per §18 (Phase 1 of the Contribute rewrite). The editing
|
||||
// surface is plain markdown; the rendered preview lives in a sibling pane
|
||||
// (added in Phase 2). No HTML→markdown round-trip — `getDoc()` returns
|
||||
// the source verbatim, which is what we POST in §8.11's manual flush.
|
||||
//
|
||||
// The parent receives an imperative handle on `editorRef`:
|
||||
// { view, getDoc(), setDoc(text) }
|
||||
// modeled lightly after the Tiptap surface so RFCView can branch on
|
||||
// whichever editor is mounted without an adapter layer.
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view'
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
|
||||
import { markdown } from '@codemirror/lang-markdown'
|
||||
import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching } from '@codemirror/language'
|
||||
|
||||
export default function MarkdownSourceEditor({
|
||||
initialDoc = '',
|
||||
editorRef,
|
||||
onUpdate,
|
||||
}) {
|
||||
const hostRef = useRef(null)
|
||||
const viewRef = useRef(null)
|
||||
const onUpdateRef = useRef(onUpdate)
|
||||
|
||||
useEffect(() => { onUpdateRef.current = onUpdate }, [onUpdate])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hostRef.current) return
|
||||
const state = EditorState.create({
|
||||
doc: initialDoc,
|
||||
extensions: [
|
||||
history(),
|
||||
lineNumbers(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||
markdown(),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.updateListener.of(u => {
|
||||
if (u.docChanged) {
|
||||
onUpdateRef.current?.(u.state.doc.toString())
|
||||
}
|
||||
}),
|
||||
],
|
||||
})
|
||||
const view = new EditorView({ state, parent: hostRef.current })
|
||||
viewRef.current = view
|
||||
if (editorRef) {
|
||||
editorRef.current = {
|
||||
view,
|
||||
getDoc: () => view.state.doc.toString(),
|
||||
setDoc: (text) => {
|
||||
const cur = view.state.doc.toString()
|
||||
if (cur === text) return
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: text },
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
view.destroy()
|
||||
viewRef.current = null
|
||||
if (editorRef && editorRef.current?.view === view) editorRef.current = null
|
||||
}
|
||||
// Construct once. Doc changes flow through the setDoc effect below.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Reflect external doc changes (branch reload, server-side flush, etc).
|
||||
useEffect(() => {
|
||||
const v = viewRef.current
|
||||
if (!v) return
|
||||
if (v.state.doc.toString() === initialDoc) return
|
||||
v.dispatch({
|
||||
changes: { from: 0, to: v.state.doc.length, insert: initialDoc ?? '' },
|
||||
})
|
||||
}, [initialDoc])
|
||||
|
||||
return <div ref={hostRef} className="cm-source-editor" />
|
||||
}
|
||||
@@ -34,7 +34,9 @@ import {
|
||||
streamChatTurn,
|
||||
} from '../api'
|
||||
import Editor, { selectionHighlightKey } from './Editor.jsx'
|
||||
import MarkdownSourceEditor from './MarkdownSourceEditor.jsx'
|
||||
import SelectionTooltip from './SelectionTooltip.jsx'
|
||||
import { marked } from 'marked'
|
||||
import PromptBar from './PromptBar.jsx'
|
||||
import ChatPanel from './ChatPanel.jsx'
|
||||
import ChangePanel from './ChangePanel.jsx'
|
||||
@@ -51,6 +53,15 @@ function debounce(fn, ms) {
|
||||
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms) }
|
||||
}
|
||||
|
||||
// Split markdown source into paragraph-ish blocks on blank lines for the
|
||||
// §8.11 manual-edit count. Headings and lists each count as one block.
|
||||
function splitSourceParagraphs(md) {
|
||||
return (md || '')
|
||||
.split(/\n\s*\n/)
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0)
|
||||
}
|
||||
|
||||
export default function RFCView({ viewer }) {
|
||||
const { slug } = useParams()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
@@ -66,8 +77,11 @@ export default function RFCView({ viewer }) {
|
||||
const [selectedModel, setSelectedModel] = useState('')
|
||||
|
||||
// Editor state — owned here so accept/decline can mutate it.
|
||||
// editorRef holds either a Tiptap instance (Discuss / read-only) or a
|
||||
// MarkdownSourceEditor handle ({view, getDoc, setDoc}) (Contribute);
|
||||
// call sites branch on shape.
|
||||
const editorRef = useRef(null)
|
||||
const originalParagraphsRef = useRef([])
|
||||
const originalSourceLinesRef = useRef([])
|
||||
const [editorContent, setEditorContent] = useState('')
|
||||
|
||||
// Selection + tooltip + selection highlight per §8.12.
|
||||
@@ -132,6 +146,7 @@ export default function RFCView({ viewer }) {
|
||||
.then(view => {
|
||||
setBranchView(view)
|
||||
setEditorContent(view.body || '')
|
||||
originalSourceLinesRef.current = splitSourceParagraphs(view.body || '')
|
||||
setChanges(view.changes || [])
|
||||
})
|
||||
.catch(err => setError(err.message))
|
||||
@@ -152,21 +167,23 @@ export default function RFCView({ viewer }) {
|
||||
|
||||
useEffect(() => {
|
||||
const editor = editorRef.current
|
||||
if (!editor?.view) return
|
||||
// Only Tiptap exposes `state.tr` — when CM6 is mounted, the selection
|
||||
// highlight has no surface to render against, so skip the dispatch.
|
||||
if (!editor?.view || !editor?.state?.tr) return
|
||||
editor.view.dispatch(editor.state.tr.setMeta(selectionHighlightKey, highlightRange))
|
||||
}, [highlightRange])
|
||||
|
||||
// Manual-edit debounced upsert per §8.11 — produces a pending manual
|
||||
// card with a live countdown and an explicit Save now.
|
||||
// card with a live countdown and an explicit Save now. With the CM6
|
||||
// markdown source editor (Phase 1), the doc IS the markdown — no more
|
||||
// HTML→getText round-trip, so §19.2 lossiness is gone.
|
||||
const flushManualBuffer = useCallback(async () => {
|
||||
const editor = editorRef.current
|
||||
if (!editor || !branchView || mode !== 'contribute') return
|
||||
const text = editor.getText()
|
||||
// Convert to a rough markdown by stripping HTML — for v1 we round-trip
|
||||
// through the editor's getText; this matches the prototype's behavior.
|
||||
// A faithful HTML→markdown round-trip is a §19.2 candidate.
|
||||
const newContent = text.trim() + '\n'
|
||||
if (!newContent || newContent.trim() === (branchView.body || '').trim()) {
|
||||
if (typeof editor.getDoc !== 'function') return
|
||||
const raw = editor.getDoc()
|
||||
const newContent = raw.endsWith('\n') ? raw : raw + '\n'
|
||||
if (newContent.trim() === (branchView.body || '').trim()) {
|
||||
setManualPending(null)
|
||||
return
|
||||
}
|
||||
@@ -179,6 +196,7 @@ export default function RFCView({ viewer }) {
|
||||
const fresh = await getBranch(slug, branchParam)
|
||||
setBranchView(fresh)
|
||||
setChanges(fresh.changes || [])
|
||||
originalSourceLinesRef.current = splitSourceParagraphs(fresh.body || '')
|
||||
setManualPending(null)
|
||||
loadAllMessages(slug, branchParam, fresh.threads).then(setMessages)
|
||||
}
|
||||
@@ -187,22 +205,15 @@ export default function RFCView({ viewer }) {
|
||||
}
|
||||
}, [slug, branchParam, branchView, mode, manualPending?.paragraphCount])
|
||||
|
||||
const handleEditorUpdate = useMemo(() => debounce((plainText) => {
|
||||
const handleEditorUpdate = useMemo(() => debounce((doc) => {
|
||||
if (mode !== 'contribute' || !branchView) return
|
||||
const editor = editorRef.current
|
||||
if (!editor) return
|
||||
const currentParagraphs = []
|
||||
editor.state.doc.descendants(node => {
|
||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
||||
currentParagraphs.push(node.textContent.trim())
|
||||
}
|
||||
})
|
||||
const baseline = originalParagraphsRef.current || []
|
||||
const current = splitSourceParagraphs(doc)
|
||||
const baseline = originalSourceLinesRef.current || []
|
||||
const len = Math.max(current.length, baseline.length)
|
||||
let changed = 0
|
||||
currentParagraphs.forEach((t, i) => {
|
||||
const orig = (baseline[i] ?? '').trim()
|
||||
if (t !== orig) changed++
|
||||
})
|
||||
for (let i = 0; i < len; i++) {
|
||||
if ((current[i] ?? '') !== (baseline[i] ?? '')) changed++
|
||||
}
|
||||
if (changed > 0) {
|
||||
setManualPending({ paragraphCount: changed })
|
||||
setManualCountdown({ deadline: Date.now() + MANUAL_IDLE_MS })
|
||||
@@ -347,24 +358,16 @@ export default function RFCView({ viewer }) {
|
||||
// ── Accept / decline / reask ──────────────────────────────────────────
|
||||
const handleAccept = useCallback(async ({ change, proposed, wasEdited }) => {
|
||||
try {
|
||||
const { commit_sha } = await apiAccept(slug, branchParam, change.id, { proposed, wasEdited })
|
||||
// Inject tracked-change markup into the editor so it renders inline.
|
||||
const editor = editorRef.current
|
||||
if (editor && change.original) {
|
||||
const html = editor.getHTML()
|
||||
const tracked =
|
||||
`<span class="tracked-delete" data-change-id="${change.id}">${change.original}</span>` +
|
||||
`<span class="tracked-insert" data-change-id="${change.id}">${proposed}</span>`
|
||||
const next = html.replace(change.original, tracked)
|
||||
if (next !== html) editor.commands.setContent(next, false)
|
||||
}
|
||||
// Pull the authoritative branch state — body, sha, changes.
|
||||
await apiAccept(slug, branchParam, change.id, { proposed, wasEdited })
|
||||
// Pull authoritative branch state and reset the editor to the new
|
||||
// body. The proper tracked-changes overlay lives in Phase 3's
|
||||
// preview pane; with CM6 as the source editor there is no HTML
|
||||
// surface to inject `<span class="tracked-*">` into.
|
||||
const fresh = await getBranch(slug, branchParam)
|
||||
setBranchView(fresh)
|
||||
setChanges(fresh.changes || [])
|
||||
// We do not reset editorContent here — the editor is showing the
|
||||
// tracked markup overlay; resetting would clear the visual diff
|
||||
// until DiffView is toggled.
|
||||
setEditorContent(fresh.body || '')
|
||||
originalSourceLinesRef.current = splitSourceParagraphs(fresh.body || '')
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
@@ -419,10 +422,21 @@ export default function RFCView({ viewer }) {
|
||||
|
||||
const toggleReviewMode = useCallback(() => {
|
||||
setReviewMode(prev => {
|
||||
if (!prev) setReviewHTML(editorRef.current?.getHTML() || '')
|
||||
if (!prev) {
|
||||
// CM6 is the contribute-mode editor in Phase 1, so source HTML
|
||||
// comes from rendering the current markdown via marked. The
|
||||
// per-session inline tracked-change spans that Tiptap accumulated
|
||||
// are gone for now; Phase 3's preview pane is the proper home
|
||||
// for tracked changes anyway.
|
||||
const editor = editorRef.current
|
||||
const md = typeof editor?.getDoc === 'function'
|
||||
? editor.getDoc()
|
||||
: (branchView?.body || '')
|
||||
setReviewHTML(marked.parse(md))
|
||||
}
|
||||
return !prev
|
||||
})
|
||||
}, [])
|
||||
}, [branchView])
|
||||
|
||||
// ── Branch dropdown navigation ─────────────────────────────────────────
|
||||
const onPickBranch = useCallback((name) => {
|
||||
@@ -621,23 +635,31 @@ export default function RFCView({ viewer }) {
|
||||
<DiffView html={reviewHTML} changes={changes} messages={messages} />
|
||||
) : (
|
||||
<>
|
||||
<Editor
|
||||
content={editorContent}
|
||||
editorRef={editorRef}
|
||||
originalParagraphsRef={originalParagraphsRef}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onUpdate={editorEditable ? handleEditorUpdate : undefined}
|
||||
editable={editorEditable}
|
||||
/>
|
||||
<SelectionTooltip
|
||||
selection={selection}
|
||||
onAsk={handleTooltipAsk}
|
||||
onFlag={handleTooltipFlag}
|
||||
disabled={isStreaming || !viewer}
|
||||
models={models}
|
||||
selectedModel={selectedModel}
|
||||
onModelChange={setSelectedModel}
|
||||
/>
|
||||
{editorEditable ? (
|
||||
<MarkdownSourceEditor
|
||||
initialDoc={editorContent}
|
||||
editorRef={editorRef}
|
||||
onUpdate={handleEditorUpdate}
|
||||
/>
|
||||
) : (
|
||||
<Editor
|
||||
content={editorContent}
|
||||
editorRef={editorRef}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
editable={false}
|
||||
/>
|
||||
)}
|
||||
{inDiscuss && (
|
||||
<SelectionTooltip
|
||||
selection={selection}
|
||||
onAsk={handleTooltipAsk}
|
||||
onFlag={handleTooltipFlag}
|
||||
disabled={isStreaming || !viewer}
|
||||
models={models}
|
||||
selectedModel={selectedModel}
|
||||
onModelChange={setSelectedModel}
|
||||
/>
|
||||
)}
|
||||
{showPromptBar ? (
|
||||
<PromptBar
|
||||
selection={selection?.text || null}
|
||||
|
||||
Reference in New Issue
Block a user