Both sections of the advice arrived in the language she is not learning
Reported from the phone: "I have my main language set as Portuguese and I say
I'm learning English but yet Petal presents the Ask Petal advice in both
sections as Portuguese."
Nothing was wrong with targetFor again. A Portuguese document by a Portuguese
writer is explained in Portuguese, which is the whole point of Phase 28. The
card was right. What was wrong was the tap underneath it: /suggestions/{id}
/translate answered "" for exactly that case, on the reasoning that an
unasked-for English rendering of an explanation she can already read is not a
seed but noise. That reasoning had the writer facing the wrong way. She is
learning English. The half she is *practising* is the half worth a tap, and the
bubble sits directly beneath the explanation inside the same card, so answering
"" left her with Portuguese, the same Portuguese again, and no English anywhere
on the card. targetFor's own comment promised the other language stays one tap
away in both directions; only one direction had ever been built.
So the endpoint keeps the one rule it always claimed: render into whichever
half the explanation is not already in. English explanation into her language,
as before; her language into the English she is learning, which is new. Both
ends of that are now parameters — TranslateMessages took the source language
for granted as English because until Phase 28 it always was. The zh prompt is
unchanged byte for byte, which its test still pins.
The client fallback was the same symptom from a different cause and would have
survived the server fix: an empty answer, or an unreachable model, seeded the
bubble with the explanation itself — a verbatim repeat of the line two above
it, which reads as Petal replying in the language the tap was pressed to
escape. With the endpoint always having somewhere to go, empty now means only
that the model didn't answer, so the panel opens with no bubble at all and the
input where she can ask. AskPetal no longer takes the explanation as a prop; it
never needed anything but the id.
The test that pinned the refusal now pins the rendering, and carries the report.
Claude-Session: https://claude.ai/code/session_01KGACAtTPjvZ2PipDZ5qD99
This commit is contained in:
@@ -5,12 +5,11 @@ import { splitBilingual } from './bilingualReply'
|
||||
import { fromIME } from '../../lib/ime'
|
||||
|
||||
interface Props {
|
||||
// The card's explanation is deliberately NOT passed in. Petal's opening
|
||||
// bubble is that explanation rendered into the other half of the pair,
|
||||
// fetched on open from this id — and when that can't be had, the panel has
|
||||
// nothing to say rather than a second copy of the card.
|
||||
suggestionId: string
|
||||
// The English explanation (shown in the card body). Petal's opening bubble is
|
||||
// its translation into the pair language, fetched on open — so the panel
|
||||
// doesn't just repeat the same English text twice. Falls back to this on
|
||||
// failure.
|
||||
explanation: string
|
||||
}
|
||||
|
||||
// CJK fallback stack — Nunito has no Chinese glyphs, and on the zh pair both
|
||||
@@ -44,10 +43,11 @@ const CHAT_MIN_PX = 160
|
||||
// conversation lives in this component's state — nothing is persisted; closing
|
||||
// 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) {
|
||||
export function AskPetal({ suggestionId }: Props) {
|
||||
const t = usePack()
|
||||
// Opening bubble starts empty (caret-only) and fills with the pair-language
|
||||
// translation once it lands; `seeding` drives that loading caret.
|
||||
// Opening bubble starts empty (caret-only) and fills with the other half of
|
||||
// the pair once it lands; `seeding` drives that loading caret, and the bubble
|
||||
// goes away entirely if there turns out to be nothing to put in it.
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([{ role: 'assistant', content: '' }])
|
||||
const [seeding, setSeeding] = useState(true)
|
||||
const [input, setInput] = useState('')
|
||||
@@ -82,23 +82,31 @@ export function AskPetal({ suggestionId, explanation }: Props) {
|
||||
inputRef.current?.focus({ preventScroll: true })
|
||||
}, [])
|
||||
|
||||
// Fetch the pair-language translation of the explanation to seed the first
|
||||
// bubble.
|
||||
// Fetch the explanation rendered into the other half of her pair to seed the
|
||||
// first bubble — English under a Portuguese card, her language under an
|
||||
// English one; the server decides which way round (suggestions/translate.go).
|
||||
// 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.
|
||||
// conversation opens with this one assistant turn and nothing else).
|
||||
//
|
||||
// When it can't be fetched the panel opens empty rather than falling back to
|
||||
// the explanation, which sits two lines higher in the card: a bubble that
|
||||
// repeats the card verbatim reads as Petal answering in the language the tap
|
||||
// was pressed to escape. Nothing to add, nothing shown.
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const seed = (text: string) =>
|
||||
setMessages((prev) =>
|
||||
prev.length !== 1 ? prev : text === '' ? [] : [{ role: 'assistant', content: text }],
|
||||
)
|
||||
api
|
||||
.translateSuggestion(suggestionId)
|
||||
.then((res) => {
|
||||
if (cancelled) return
|
||||
const text = res.translation.trim() || explanation
|
||||
setMessages((prev) => (prev.length === 1 ? [{ role: 'assistant', content: text }] : prev))
|
||||
seed(res.translation.trim())
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled) return
|
||||
setMessages((prev) => (prev.length === 1 ? [{ role: 'assistant', content: explanation }] : prev))
|
||||
seed('')
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setSeeding(false)
|
||||
@@ -106,7 +114,7 @@ export function AskPetal({ suggestionId, explanation }: Props) {
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [suggestionId, explanation])
|
||||
}, [suggestionId])
|
||||
|
||||
async function send() {
|
||||
const text = input.trim()
|
||||
|
||||
@@ -197,7 +197,7 @@ export function SuggestionCard({
|
||||
{asking ? 'Hide Petal' : 'Ask Petal ✨'}
|
||||
</button>
|
||||
|
||||
{asking && <AskPetal suggestionId={suggestion.id} explanation={suggestion.explanation} />}
|
||||
{asking && <AskPetal suggestionId={suggestion.id} />}
|
||||
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
{hasReplacement && (
|
||||
|
||||
@@ -217,7 +217,7 @@ const RailCard = forwardRef<HTMLDivElement, CardProps>(function RailCard(
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{expanded && <AskPetal suggestionId={suggestion.id} explanation={suggestion.explanation} />}
|
||||
{expanded && <AskPetal suggestionId={suggestion.id} />}
|
||||
|
||||
<div className="mt-2.5 flex items-center gap-2">
|
||||
{hasReplacement && (
|
||||
|
||||
Reference in New Issue
Block a user