Suggestions: right-margin comment rail + Mandarin explanations
Surface every outstanding suggestion as a card in the right-hand
whitespace, vertically aligned to the text it flags — so the writer sees
the whole queue at once instead of hovering each highlight. Cards stack
with collision avoidance, link both ways with their highlight (hover/click
↔ soft text wash, driven through the decoration plugin so it survives
edit repaints), and carry the same Accept / Dismiss / Ask Petal actions.
The rail is a progressive enhancement: it mounts only when there's room
beside the editor, otherwise the existing inline hover card is unchanged.
Stacked cards that reach the bottom-right corner tuck behind the
companion mascot (z-order).
When a card is expanded, the Ask Petal bubble now opens with the
Simplified-Chinese translation of the explanation (the English stays in
the card body) instead of repeating the same text twice — a new
POST /api/suggestions/{id}/translate one-shot LLM endpoint, loaded
lazily on open with an English fallback.
Verified live against the local LLM via the uitest harness: rail
stacking, hover↔text wash, expand/Ask Petal, accept-from-rail, narrow
fallback, and the Mandarin bubble.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { streamSuggestionChat, type ChatMessage } from '../../api/client'
|
||||
import { api, streamSuggestionChat, type ChatMessage } from '../../api/client'
|
||||
|
||||
interface Props {
|
||||
suggestionId: string
|
||||
// Pre-populates Petal's first bubble so the conversation opens with context.
|
||||
// The English explanation (shown in the card body). Petal's opening bubble is
|
||||
// its Simplified-Chinese translation, fetched on open — so the panel doesn't
|
||||
// just repeat the same English text twice. Falls back to this on failure.
|
||||
explanation: string
|
||||
}
|
||||
|
||||
@@ -17,9 +19,10 @@ const CHAT_FONT = "'Nunito', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC
|
||||
// the card (unmounting) clears it. Each send streams Petal's reply token-by-
|
||||
// token into the latest assistant bubble.
|
||||
export function AskPetal({ suggestionId, explanation }: Props) {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{ role: 'assistant', content: explanation },
|
||||
])
|
||||
// Opening bubble starts empty (caret-only) and fills with the Mandarin
|
||||
// translation once it lands; `seeding` drives that loading caret.
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([{ role: 'assistant', content: '' }])
|
||||
const [seeding, setSeeding] = useState(true)
|
||||
const [input, setInput] = useState('')
|
||||
const [streaming, setStreaming] = useState(false)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
@@ -36,6 +39,31 @@ export function AskPetal({ suggestionId, explanation }: Props) {
|
||||
inputRef.current?.focus()
|
||||
}, [])
|
||||
|
||||
// Fetch the Chinese translation of the explanation to seed the first bubble.
|
||||
// Only replaces the seed bubble if the user hasn't started chatting yet (the
|
||||
// conversation always opens with this one assistant turn). Falls back to the
|
||||
// English explanation if the translation can't be fetched.
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
api
|
||||
.translateSuggestion(suggestionId)
|
||||
.then((res) => {
|
||||
if (cancelled) return
|
||||
const text = res.translation.trim() || explanation
|
||||
setMessages((prev) => (prev.length === 1 ? [{ role: 'assistant', content: text }] : prev))
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setMessages((prev) => (prev.length === 1 ? [{ role: 'assistant', content: explanation }] : prev))
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setSeeding(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [suggestionId, explanation])
|
||||
|
||||
async function send() {
|
||||
const text = input.trim()
|
||||
if (!text || streaming) return
|
||||
@@ -86,7 +114,12 @@ export function AskPetal({ suggestionId, explanation }: Props) {
|
||||
style={{ maxHeight: 220 }}
|
||||
>
|
||||
{messages.map((m, i) => (
|
||||
<Bubble key={i} role={m.role} content={m.content} streaming={streaming && i === messages.length - 1} />
|
||||
<Bubble
|
||||
key={i}
|
||||
role={m.role}
|
||||
content={m.content}
|
||||
streaming={(streaming && i === messages.length - 1) || (seeding && i === 0)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user