Files
petal/internal/suggestions/translate.go
prosolis 82e2bcc777 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
2026-06-26 12:23:02 -07:00

58 lines
1.5 KiB
Go

package suggestions
import (
"database/sql"
"errors"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"gitea.parodia.dev/drwily/petal/internal/db"
"gitea.parodia.dev/drwily/petal/internal/llm"
)
type translateResponse struct {
Translation string `json:"translation"`
}
// translate renders a suggestion's English explanation into Simplified Chinese
// for the Ask Petal bubble, so the ESL reader sees the "why" in her first
// language instead of a second copy of the same English text. The explanation is
// loaded server-side from the suggestion id (scoped to the local user) and never
// trusted from the client, mirroring chat (spec Note #10).
func (h *Handler) translate(w http.ResponseWriter, r *http.Request) {
sugID := chi.URLParam(r, "id")
var explanation string
err := h.DB.QueryRow(
`SELECT s.explanation
FROM suggestions s
JOIN documents d ON d.id = s.doc_id
WHERE s.id = ? AND d.user_id = ?`,
sugID, db.LocalUserID,
).Scan(&explanation)
if errors.Is(err, sql.ErrNoRows) {
errorJSON(w, http.StatusNotFound, "suggestion not found")
return
}
if err != nil {
serverError(w, err)
return
}
explanation = strings.TrimSpace(explanation)
if explanation == "" {
writeJSON(w, http.StatusOK, translateResponse{Translation: ""})
return
}
out, err := llm.RunTranslate(r.Context(), h.Client, explanation)
if err != nil {
errorJSON(w, http.StatusBadGateway, "translate failed: "+err.Error())
return
}
writeJSON(w, http.StatusOK, translateResponse{Translation: out})
}