Ask Petal answers in both languages, with room to read

The tutor prompt said "never mix languages in a single response" and
mirrored the language of the question, so asking in English — which she
does, because she is practising — returned the one explanation surface
that gives nothing in her own language. It now answers in both, pair
language first, halves separated by a blank line.

Which half is the safety net and which is the lesson depends on who is
writing: the pair is (English + X) and Petal is used from both ends, so
the prompt asks for both and says it doesn't know which way round.

The split is a rendering nicety, never a parse the reply depends on: a
half-streamed reply is all one half, a model that ignores the
instruction renders as one block, and nothing is ever dropped.

For the height, the first attempt clamped the box to the room left below
the anchored card so it could never overhang — measured, that gave 176px
against a 442px answer, worse than the 220px it replaced. The card's own
chrome spends ~290px of an 810px window, so "fits below the word" and
"room to read" are not both available. The ceiling is now a flat 50vh and
the overhang is made navigable instead, per item 4: the card reports its
reach like the rail already does, the column grows, and the page can
scroll to the actions below it.
This commit is contained in:
prosolis
2026-07-28 07:04:16 -07:00
parent f082a930cb
commit 978cb80642
13 changed files with 470 additions and 24 deletions
+16
View File
@@ -164,6 +164,22 @@ describe('the zh pack', () => {
if (p.code === 'pt-PT') expect(p.locale).toBe('pt-PT') // never pt-BR
})
// The chat-failure line is the only message the Ask Petal panel writes without
// the model, and it renders through the same bilingual bubble as a real reply
// (splitBilingual, blank line between the halves). A pack that writes it as
// one language gets a bubble with a muted empty half — and, worse, tells the
// half of the pair that can't read that language nothing at all.
it.each(PACKS)('says the chat-failure line in both halves of the pair ($code)', async (p) => {
const { splitBilingual } = await import('../components/Editor/bilingualReply')
const { native, en } = splitBilingual(p.editor.chatFailed)
expect(native, `${p.code} chatFailed has no pair-language half`).not.toBe('')
expect(en, `${p.code} chatFailed has no English half`).not.toBe('')
// The English half is the one every reader of every pack shares, so it is
// the one worth pinning: a pack that translated it has lost the point.
expect(en).toMatch(/ask me again/i)
expect(native).not.toBe(en)
})
it.each(PACKS)('labels every companion, tone and style ($code)', async (p) => {
const { COMPANIONS } = await import('../components/Companion/companions')
for (const c of COMPANIONS) {
+1
View File
@@ -307,6 +307,7 @@ export const fr: Pack = {
editor: {
askPlaceholder: 'Ask why… / Demande pourquoi…',
chatFailed: 'Je nai pas réussi à répondre — repose-moi la question, sil te plaît. 🌸\n\nI had trouble answering just now — please ask me again. 🌸',
findPlaceholder: 'Rechercher · Find',
findNone: 'Rien · 0',
matchCase: 'Match case · Respecter la casse',
+1
View File
@@ -285,6 +285,7 @@ export const ptPT: Pack = {
editor: {
askPlaceholder: 'Ask why… / Pergunta porquê…',
chatFailed: 'Não consegui responder agora — pergunta-me outra vez, se faz favor. 🌸\n\nI had trouble answering just now — please ask me again. 🌸',
findPlaceholder: 'Localizar · Find',
findNone: 'Nada · 0',
matchCase: 'Match case · Maiúsculas/minúsculas',
+1
View File
@@ -185,6 +185,7 @@ export const zh: Pack = {
editor: {
askPlaceholder: 'Ask why… / 问为什么…',
chatFailed: '我这会儿没答上来,再问我一次好吗?🌸\n\nI had trouble answering just now — please ask me again. 🌸',
findPlaceholder: '查找 · Find',
findNone: '无 · 0',
matchCase: 'Match case · 区分大小写',
+4
View File
@@ -155,6 +155,10 @@ export interface Pack {
editor: {
askPlaceholder: string
// Shown in Petal's own chat bubble when the reply never arrives. It is
// the one line in that panel Petal writes without the model, so the pack
// owns it — and it is bilingual like every answer beside it.
chatFailed: string
findPlaceholder: string
findNone: string
matchCase: string