ee6e3491e7
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.
122 lines
3.6 KiB
React
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>
|
|
)
|
|
}
|