feat: add EditorGridSizePicker and EditorSelectionFrame components; implement FreeCreateView for article management; introduce ArticleSelectionOptimizeService for optimizing article selections
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -17,6 +20,7 @@ import (
|
||||
|
||||
type ArticleHandler struct {
|
||||
svc *app.ArticleService
|
||||
selectionOptimize *app.ArticleSelectionOptimizeService
|
||||
promptGenerateSvc *app.PromptRuleGenerationService
|
||||
streams *stream.GenerationHub
|
||||
streamEnabled bool
|
||||
@@ -39,6 +43,11 @@ func NewArticleHandler(a *bootstrap.App) *ArticleHandler {
|
||||
|
||||
return &ArticleHandler{
|
||||
svc: app.NewArticleService(a.DB, a.AuditLogs, a.ObjectStorage),
|
||||
selectionOptimize: app.NewArticleSelectionOptimizeService(
|
||||
a.DB,
|
||||
a.LLM,
|
||||
a.Config.LLM.MaxOutputTokens,
|
||||
),
|
||||
promptGenerateSvc: app.NewPromptRuleGenerationService(
|
||||
a.DB,
|
||||
a.LLM,
|
||||
@@ -124,6 +133,22 @@ func (h *ArticleHandler) GenerateFromRule(c *gin.Context) {
|
||||
response.SuccessWithStatus(c, http.StatusCreated, data)
|
||||
}
|
||||
|
||||
func (h *ArticleHandler) Create(c *gin.Context) {
|
||||
var req app.CreateArticleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
// Allow empty body
|
||||
req = app.CreateArticleRequest{}
|
||||
}
|
||||
|
||||
data, err := h.svc.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.SuccessWithStatus(c, http.StatusCreated, data)
|
||||
}
|
||||
|
||||
func (h *ArticleHandler) Detail(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
@@ -210,6 +235,111 @@ func (h *ArticleHandler) UploadImage(c *gin.Context) {
|
||||
response.SuccessWithStatus(c, http.StatusCreated, data)
|
||||
}
|
||||
|
||||
type articleSelectionOptimizeStreamEvent struct {
|
||||
ArticleID int64 `json:"article_id"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Delta string `json:"delta,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
func (h *ArticleHandler) OptimizeSelectionStream(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "article id must be a number"))
|
||||
return
|
||||
}
|
||||
|
||||
var req app.ArticleSelectionOptimizeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40013, "invalid_payload", err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.selectionOptimize.ValidateRequest(c.Request.Context(), id, req); err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
response.Error(c, response.ErrInternal(50011, "stream_unsupported", "response writer does not support streaming"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := writeSSE(c, "start", articleSelectionOptimizeStreamEvent{
|
||||
ArticleID: id,
|
||||
Status: "generating",
|
||||
Done: false,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
|
||||
streamCtx, cancel := context.WithCancel(c.Request.Context())
|
||||
defer cancel()
|
||||
|
||||
var streamed strings.Builder
|
||||
var streamWriteErr error
|
||||
|
||||
result, err := h.selectionOptimize.OptimizeSelection(streamCtx, req, func(delta string) {
|
||||
if delta == "" || streamWriteErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
streamed.WriteString(delta)
|
||||
if writeErr := writeSSE(c, "delta", articleSelectionOptimizeStreamEvent{
|
||||
ArticleID: id,
|
||||
Status: "generating",
|
||||
Delta: delta,
|
||||
Content: streamed.String(),
|
||||
Done: false,
|
||||
}); writeErr != nil {
|
||||
streamWriteErr = writeErr
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
})
|
||||
if streamWriteErr != nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(streamCtx.Err(), context.Canceled) {
|
||||
return
|
||||
}
|
||||
|
||||
appErr := response.Normalize(err)
|
||||
_ = writeSSE(c, "error", articleSelectionOptimizeStreamEvent{
|
||||
ArticleID: id,
|
||||
Status: "failed",
|
||||
Content: strings.TrimSpace(streamed.String()),
|
||||
Error: resolveOptimizeStreamErrorMessage(appErr),
|
||||
Done: true,
|
||||
})
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
if err := writeSSE(c, "completed", articleSelectionOptimizeStreamEvent{
|
||||
ArticleID: id,
|
||||
Status: "completed",
|
||||
Content: result.Content,
|
||||
Model: result.Model,
|
||||
Done: true,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
func (h *ArticleHandler) Stream(c *gin.Context) {
|
||||
if !h.streamEnabled {
|
||||
response.Error(c, response.ErrNotFound(40412, "generation_stream_disabled", "generation stream is disabled"))
|
||||
@@ -324,3 +454,16 @@ func writeSSE(c *gin.Context, event string, payload interface{}) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveOptimizeStreamErrorMessage(appErr *response.AppError) string {
|
||||
if appErr == nil {
|
||||
return "optimize selection failed"
|
||||
}
|
||||
if detail := strings.TrimSpace(appErr.Detail); detail != "" {
|
||||
return detail
|
||||
}
|
||||
if message := strings.TrimSpace(appErr.Message); message != "" {
|
||||
return message
|
||||
}
|
||||
return "optimize selection failed"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user