Files
rfc-app/frontend/src/components/SelectionTooltip.jsx
T
Ben Stull ee6e3491e7 Drop "prototype/carryover" framing now that v1 is shipped
SPEC, DEV docs, and code comments still talked about the codebase as
a rewrite-in-progress against an external prototype. With v1 shipped
the framing reads oddly — it implies code is provisional when it's
the production thing. Recast §18 as "the technical stack," strip
"carryover from the prototype" comments across backend (api.py,
chat.py, providers.py) and frontend (DiffView, PromptBar,
SelectionTooltip, modelStyles), and rework SPEC §1 / §18 to introduce
OHM up front rather than as a follow-on to a prototype reference.

Also:
- RUNBOOK: bump Python prereq to 3.11+ to match the production VM
  (was 3.13).
- Remove IMPLEMENTATION-PROMPT.md — the original implementation brief
  is no longer load-bearing.
- Add deploy/DEPLOY-NEW-SESSION-PROMPT.md as the durable
  deploy-handoff prompt for new sessions.
2026-05-25 10:32:46 -07:00

122 lines
3.6 KiB
React

// SelectionTooltip.jsx — the §8.12 selection-anchored entry point.
//
// The contributor selects a passage in the editor; this floating
// panel appears anchored to that selection. Submitting creates a new
// range-anchored chat thread (or invokes the AI on the current branch
// chat with the selection as `quote`).
//
// Two affordances per §8.13: an "Ask" button that opens (or continues)
// a chat thread, and a "Flag" button that drops a flag thread anchored
// to the selection.
import { useState, useEffect, useRef } from 'react'
import ModelPicker from './ModelPicker.jsx'
export default function SelectionTooltip({
selection,
onAsk,
onFlag,
disabled,
models,
selectedModel,
onModelChange,
}) {
const [mode, setMode] = useState('ask') // 'ask' | 'flag'
const [prompt, setPrompt] = useState('')
const [flagText, setFlagText] = useState('')
const inputRef = useRef(null)
useEffect(() => {
if (selection) {
setPrompt('')
setFlagText('')
setMode('ask')
setTimeout(() => inputRef.current?.focus(), 50)
}
}, [selection?.text])
if (!selection) return null
const { coords } = selection
const TOOLTIP_HEIGHT = 110
const GAP = 10
const top = Math.max(8, coords.top - TOOLTIP_HEIGHT - GAP)
const left = Math.min(window.innerWidth - 360, Math.max(12, coords.left))
const handleSubmit = () => {
if (disabled) return
if (mode === 'ask') {
const text = prompt.trim()
if (!text) return
onAsk(text, selection.text)
setPrompt('')
} else {
const label = flagText.trim()
if (!label) return
onFlag(label, selection.text)
setFlagText('')
}
}
const onKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit() }
if (e.key === 'Escape') onAsk(null)
}
return (
<div
className="selection-tooltip"
style={{ top, left }}
onMouseDown={e => e.preventDefault()}
>
<div className="selection-tooltip-quote">
"{selection.text.length > 80 ? selection.text.slice(0, 80) + '…' : selection.text}"
</div>
<div className="selection-tooltip-tabs">
<button
className={`selection-tooltip-tab ${mode === 'ask' ? 'active' : ''}`}
onClick={() => setMode('ask')}
>Ask</button>
<button
className={`selection-tooltip-tab ${mode === 'flag' ? 'active' : ''}`}
onClick={() => setMode('flag')}
>Flag</button>
</div>
{mode === 'ask' && models?.length > 1 && (
<ModelPicker models={models} selected={selectedModel} onChange={onModelChange} />
)}
<div className="selection-tooltip-input-row">
{mode === 'ask' ? (
<input
ref={inputRef}
className="selection-tooltip-input"
value={prompt}
onChange={e => setPrompt(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Ask anything about this passage…"
disabled={disabled}
/>
) : (
<input
ref={inputRef}
className="selection-tooltip-input"
value={flagText}
onChange={e => setFlagText(e.target.value.slice(0, 200))}
onKeyDown={onKeyDown}
placeholder="What's wrong with this passage?"
disabled={disabled}
maxLength={200}
/>
)}
<button
className="selection-tooltip-btn"
onClick={handleSubmit}
disabled={disabled || (mode === 'ask' ? !prompt.trim() : !flagText.trim())}
>
{mode === 'ask' ? 'Ask' : 'Flag'}
</button>
</div>
</div>
)
}