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
58 lines
1.5 KiB
Go
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})
|
|
}
|