refactor: implement new UI/UX architecture with redesigned desktop and mobile layouts, CSS-in-CSS modularization, and updated components.
This commit is contained in:
+129
-4
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
aiActions "git.pengzhan.dev/haopengzhan/rsspipe/internal/actions/ai"
|
||||
"git.pengzhan.dev/haopengzhan/rsspipe/internal/ai"
|
||||
"git.pengzhan.dev/haopengzhan/rsspipe/internal/auth"
|
||||
"git.pengzhan.dev/haopengzhan/rsspipe/internal/crawler"
|
||||
@@ -222,11 +223,12 @@ func (h *Handler) CreateFeed(w http.ResponseWriter, r *http.Request) {
|
||||
if req.Title != nil && strings.TrimSpace(*req.Title) != "" {
|
||||
title = strings.TrimSpace(*req.Title)
|
||||
}
|
||||
if title == "" {
|
||||
title = req.FeedURL
|
||||
iconURL := ""
|
||||
if req.IconURL != nil && strings.TrimSpace(*req.IconURL) != "" {
|
||||
iconURL = strings.TrimSpace(*req.IconURL)
|
||||
}
|
||||
|
||||
feed, err := h.db.CreateFeed(title, req.FeedURL, res.SiteURL, req.CategoryID)
|
||||
feed, err := h.db.CreateFeed(title, req.FeedURL, res.SiteURL, iconURL, req.CategoryID)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -426,7 +428,7 @@ func (h *Handler) ImportOPML(w http.ResponseWriter, r *http.Request) {
|
||||
title = item.FeedURL
|
||||
}
|
||||
|
||||
_, err := h.db.CreateFeed(title, item.FeedURL, item.SiteURL, catID)
|
||||
_, err := h.db.CreateFeed(title, item.FeedURL, item.SiteURL, "", catID)
|
||||
if err == nil {
|
||||
createdCount++
|
||||
}
|
||||
@@ -636,6 +638,70 @@ func (h *Handler) ScrapeEntry(w http.ResponseWriter, r *http.Request) {
|
||||
h.writeJSON(w, http.StatusOK, updatedEntry)
|
||||
}
|
||||
|
||||
// ToggleShareEntry toggles public sharing for an entry and returns the updated state
|
||||
func (h *Handler) ToggleShareEntry(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
shareUUID, err := h.db.ToggleShareEntry(id)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
entry, _ := h.db.GetEntry(id)
|
||||
h.writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"shared": shareUUID != nil,
|
||||
"share_uuid": shareUUID,
|
||||
"entry": entry,
|
||||
})
|
||||
}
|
||||
|
||||
// GenerateAISummary on-demand generates an AI summary for a single article
|
||||
func (h *Handler) GenerateAISummary(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
entry, err := h.db.GetEntry(id)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusNotFound, "entry not found")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := aiActions.LoadConfig(h.db)
|
||||
if !cfg.Enabled {
|
||||
h.writeError(w, http.StatusBadRequest, "AI is not enabled in settings")
|
||||
return
|
||||
}
|
||||
if cfg.APIKey == "" && cfg.Provider != "ollama" {
|
||||
h.writeError(w, http.StatusBadRequest, "AI API key is not configured in settings")
|
||||
return
|
||||
}
|
||||
|
||||
prompt := "Summarize the key points of this article clearly with 3 concise bullet points and a brief takeaway."
|
||||
resultText, err := h.aiClient.GenerateEnrichment(r.Context(), cfg, prompt, *entry)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "AI generation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
variant := models.EntryVariant{
|
||||
EntryID: entry.ID,
|
||||
VariantKey: "ai_summary",
|
||||
ContentType: "text",
|
||||
TextValue: resultText,
|
||||
Metadata: map[string]interface{}{
|
||||
"model": cfg.Model,
|
||||
},
|
||||
}
|
||||
if err := h.db.SetVariant(variant); err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "failed to save AI summary: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
updated, err := h.db.GetEntry(id)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
h.writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
// Rules API Handlers
|
||||
func (h *Handler) ListRules(w http.ResponseWriter, r *http.Request) {
|
||||
rules, err := h.db.ListRules()
|
||||
@@ -967,3 +1033,62 @@ func (h *Handler) TestAIConnection(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
h.writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
|
||||
// Search History Handlers
|
||||
func (h *Handler) GetSearchHistory(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query().Get("q")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 10
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
history, err := h.db.GetSearchHistory(q, limit)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "failed to get search history: "+err.Error())
|
||||
return
|
||||
}
|
||||
if history == nil {
|
||||
history = []models.SearchHistoryItem{}
|
||||
}
|
||||
h.writeJSON(w, http.StatusOK, history)
|
||||
}
|
||||
|
||||
func (h *Handler) RecordSearch(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Keyword) == "" {
|
||||
h.writeJSON(w, http.StatusOK, map[string]string{"status": "ignored"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.RecordSearch(req.Keyword); err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "failed to record search: "+err.Error())
|
||||
return
|
||||
}
|
||||
h.writeJSON(w, http.StatusOK, map[string]string{"status": "recorded"})
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteSearchHistory(w http.ResponseWriter, r *http.Request) {
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
if keyword != "" {
|
||||
if err := h.db.DeleteSearchHistory(keyword); err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "failed to delete search history: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := h.db.ClearSearchHistory(); err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, "failed to clear search history: "+err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
h.writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http
|
||||
r.Get("/entries/{id}", h.GetEntry)
|
||||
r.Put("/entries/{id}/read", h.UpdateEntryRead)
|
||||
r.Put("/entries/{id}/star", h.UpdateEntryStar)
|
||||
r.Post("/entries/{id}/share", h.ToggleShareEntry)
|
||||
r.Post("/entries/{id}/ai-summary", h.GenerateAISummary)
|
||||
r.Post("/entries/mark-all-read", h.MarkAllRead)
|
||||
r.Post("/entries/{id}/scrape", h.ScrapeEntry)
|
||||
r.Get("/entries/{id}/variants", h.ListEntryVariants)
|
||||
@@ -82,6 +84,11 @@ func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http
|
||||
r.Put("/settings/ai", h.UpdateAISettings)
|
||||
r.Post("/settings/ai/test", h.TestAIConnection)
|
||||
|
||||
// Search History
|
||||
r.Get("/search/history", h.GetSearchHistory)
|
||||
r.Post("/search/history", h.RecordSearch)
|
||||
r.Delete("/search/history", h.DeleteSearchHistory)
|
||||
|
||||
// OPML
|
||||
r.Post("/opml/import", h.ImportOPML)
|
||||
r.Get("/opml/export", h.ExportOPML)
|
||||
|
||||
Reference in New Issue
Block a user