Backend: - Extract shared internal/httputil (WriteJSON/ErrorJSON/BadRequest/ ServerError); drop the triple-duplicated helpers in docs, suggestions, vocab. ServerError now logs the real error and returns a generic 500 so raw DB/internal errors never reach the client. - vocab capture: validate doc_id ownership (blank -> none, unknown -> 400 instead of a leaked FK 500); rune-safe clamp word/gloss/definition/ phonetic/example. - vocab review(): wrap the read-modify-write in a transaction (TOCTOU). - /api request-size cap via MaxBytesReader middleware (2 MiB), exempting /api/images (own 10 MiB limit). Frontend: - StatusBar: drive the checking/voicing/collocating indicators from one array; llmDown uses !anyBusy. - Slide-overs: new useFocusTrap hook (focus-in, Tab trap, focus-restore) on GardenPanel + HistoryPanel, both role=dialog/aria-modal/aria-label. - speech.ts: export stopSpeech(); GardenPanel cancels audio on unmount. Tests: add doc_id-validation and field-clamp coverage; full suite green. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
59 lines
1.6 KiB
Go
59 lines
1.6 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/httputil"
|
|
"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) {
|
|
httputil.ErrorJSON(w, http.StatusNotFound, "suggestion not found")
|
|
return
|
|
}
|
|
if err != nil {
|
|
httputil.ServerError(w, err)
|
|
return
|
|
}
|
|
|
|
explanation = strings.TrimSpace(explanation)
|
|
if explanation == "" {
|
|
httputil.WriteJSON(w, http.StatusOK, translateResponse{Translation: ""})
|
|
return
|
|
}
|
|
|
|
out, err := llm.RunTranslate(r.Context(), h.Client, explanation)
|
|
if err != nil {
|
|
httputil.ErrorJSON(w, http.StatusBadGateway, "translate failed: "+err.Error())
|
|
return
|
|
}
|
|
|
|
httputil.WriteJSON(w, http.StatusOK, translateResponse{Translation: out})
|
|
}
|