diff --git a/.github/workflows/lint-go.yml b/.github/workflows/lint-go.yml index dcf06d0..f36bd44 100644 --- a/.github/workflows/lint-go.yml +++ b/.github/workflows/lint-go.yml @@ -18,6 +18,6 @@ jobs: check-latest: true - name: Run golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: working-directory: ./backend \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend index 86707c2..18b6c5b 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -21,8 +21,9 @@ RUN cmake -B /app/algorithm/build -S /app/algorithm && \ #2. Build the Go binary (enable CGO) WORKDIR /app/backend RUN apt-get update && \ - apt-get install -y ca-certificates wget tar && \ - wget -qO- https://github.com/daulet/tokenizers/releases/download/v1.27.0/libtokenizers.linux-amd64.tar.gz | tar xvz -C /usr/lib/ + apt-get install -y ca-certificates wget tar + +RUN wget -O- https://github.com/daulet/tokenizers/releases/download/v1.27.0/libtokenizers.linux-amd64.tar.gz | tar xvz -C /usr/lib/ RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o /app/bin/server ./cmd/server/main.go diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 1241064..f1ada00 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -74,7 +74,8 @@ func main() { cardRepo := repository.NewPgCardRepository(queries, &logger) algoEngine := algorithm.NewSM2Engine() - embeder, err := tokenizer.NewOnnxEmbedder(cfg.TokenizerPath, cfg.ONNXModelPath, cfg.ONNXLibraryPath, &logger) + embeder, err := tokenizer.NewOnnxEmbedder(cfg.Embeder.TokenizerPath, cfg.Embeder.ONNXModelPath, + cfg.Embeder.ONNXLibraryPath, &logger) if err != nil { logger.Fatal().Err(err).Msg("Failed to create embedded tokenizer") } @@ -85,7 +86,7 @@ func main() { srv := &nhttp.Server{ Addr: ":" + cfg.HTTP.Port, - Handler: cardHandler.InitRoutes(cfg.GinMode), + Handler: cardHandler.InitRoutes(cfg.HTTP.GinMode), ReadTimeout: cfg.HTTP.Timeout, } diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 52916a2..ec86e7e 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -7,33 +7,33 @@ import ( "github.com/ilyakaznacheev/cleanenv" ) -type HTTP struct { +type HTTPConfig struct { Port string `yaml:"port" env:"HTTP_PORT" env-default:"8080"` Timeout time.Duration `yaml:"timeout" env:"API_TIMEOUT" env-default:"5s"` GinMode string `yaml:"gin_mode" env:"GIN_MODE" env-default:"debug"` } -type Log struct { +type LogConfig struct { Level string `env-default:"info" yaml:"log_level" env:"LOG_LEVEL"` } -type DB struct { +type DBConfig struct { URL string `env:"DATABASE_URL" env-required:"true"` MaxConns int32 `yaml:"max_conns" env:"DB_MAX_CONNS" env-default:"10"` MinConns int32 `yaml:"min_conns" env:"DB_MIN_CONNS" env-default:"2"` } -type Embeder struct { +type EmbederConfig struct { TokenizerPath string `yaml:"tokenizer_path" env:"TOKENIZER_PATH" env-default:"/app/models/tokenizer.json"` ONNXModelPath string `yaml:"onnx_model_path" env:"ONNX_MODEL_PATH" env-default:"/app/models/model.onnx"` ONNXLibraryPath string `yaml:"onnx_library_path" env:"ONNX_LIBRARY_PATH" env-default:"/usr/local/lib/libonnxruntime.so"` } type Config struct { - HTTP `yaml:"http"` - Log `yaml:"logger"` - DB `yaml:"postgres"` - Embeder `yaml:"embeder"` + HTTP HTTPConfig `yaml:"http"` + Log LogConfig `yaml:"logger"` + DB DBConfig `yaml:"postgres"` + Embeder EmbederConfig `yaml:"embeder"` } func NewConfig() (*Config, error) { diff --git a/backend/internal/http/router.go b/backend/internal/http/router.go index d2075b2..3f95a6e 100644 --- a/backend/internal/http/router.go +++ b/backend/internal/http/router.go @@ -32,6 +32,8 @@ func (h *CardHandler) InitRoutes(ginMode string) *gin.Engine { router.Use(gin.Recovery()) router.StaticFile("/", "./frontend/index.html") + router.Static("/css", "./frontend/css") + router.Static("/js", "./frontend/js") card := router.Group("/api/v1/cards") card.GET("/review", h.GetReviewBatch) diff --git a/frontend/css/style.css b/frontend/css/style.css index 97d06f6..274fbbc 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -1,3 +1,9 @@ +:root { + --success: #22c55e; + --warning: #f59e0b; + --error: #ef4444; +} + * { box-sizing: border-box; } @@ -211,287 +217,65 @@ button { .trainer-actions { margin-top: 16px; } -/* Дополнительные стили для главной страницы */ -.container { - max-width: 1000px; - margin: 0 auto; -} - -.chat-container { - background: white; - border-radius: 16px; - box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08); - overflow: hidden; -} - -.chat-messages { - height: 400px; - overflow-y: auto; - padding: 20px; - display: flex; - flex-direction: column; - gap: 16px; - background: #f9fafb; -} - -.message { - display: flex; - gap: 12px; - max-width: 80%; -} - -.message.user { - align-self: flex-end; - flex-direction: row-reverse; -} - -.message.bot { - align-self: flex-start; -} -.message-avatar { - width: 36px; - height: 36px; - border-radius: 50%; - background: #e5e7eb; - display: flex; - align-items: center; - justify-content: center; - font-size: 20px; +.similarity-bar { + width: 100%; + height: 12px; + background-color: #e5e7eb; + border-radius: 999px; + overflow: hidden; + margin-top: 10px; } -.message-content { - background: white; - padding: 12px 16px; - border-radius: 12px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +.similarity-fill { + height: 100%; + background-color: var(--success); + transition: width 0.4s ease, background-color 0.4s ease; } -.message.user .message-content { - background: #2563eb; - color: white; +.similarity-label { + font-weight: 600; + margin-right: 8px; } -.message-content p { - margin: 0; - line-height: 1.5; +.similarity-value { + font-weight: 700; + color: #374151; } -.message-meta { - margin-top: 8px; - font-size: 12px; - color: #6b7280; +.chat-container { + display: flex; + flex-direction: column; + gap: 16px; } -.chat-input-form { - padding: 16px; - background: white; - border-top: 1px solid #e5e7eb; +.chat-messages { + min-height: 250px; + max-height: 400px; + overflow-y: auto; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; } .input-group { - display: flex; - gap: 12px; + display: flex; + gap: 12px; } .input-group textarea { - flex: 1; - padding: 12px; - border: 1px solid #e5e7eb; - border-radius: 8px; - font-family: inherit; - font-size: 14px; - resize: vertical; -} - -.input-group textarea:focus { - outline: none; - border-color: #2563eb; + flex-grow: 1; + border: 1px solid #d1d5db; + border-radius: 10px; + padding: 12px; + font-family: inherit; } .input-group button { - padding: 0 24px; - background: #2563eb; - color: white; - border: none; - border-radius: 8px; - cursor: pointer; - font-weight: 500; -} - -.input-group button:hover { - background: #1d4ed8; -} - -.trainer-container { - background: white; - border-radius: 16px; - padding: 24px; - box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08); -} - -.question-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; -} - -.question-badge { - background: #e5e7eb; - padding: 4px 12px; - border-radius: 20px; - font-size: 12px; - font-weight: 500; -} - -.card-number { - font-size: 12px; - color: #6b7280; -} - -.answer-form { - margin: 24px 0; -} - -.form-label { - display: block; - margin-bottom: 8px; - font-weight: 500; -} - -.answer-form textarea { - width: 100%; - padding: 12px; - border: 1px solid #e5e7eb; - border-radius: 8px; - font-family: inherit; - font-size: 14px; - resize: vertical; -} - -.answer-form textarea:focus { - outline: none; - border-color: #2563eb; -} - -.btn-primary { - margin-top: 12px; - padding: 10px 20px; - background: #2563eb; - color: white; - border: none; - border-radius: 8px; - cursor: pointer; - font-weight: 500; -} - -.btn-primary:hover:not(:disabled) { - background: #1d4ed8; -} - -.btn-primary:disabled { - background: #9ca3af; - cursor: not-allowed; -} - -.btn-secondary { - padding: 10px 20px; - background: #e5e7eb; - color: #1f2937; - border: none; - border-radius: 8px; - cursor: pointer; - font-weight: 500; -} - -.btn-secondary:hover { - background: #d1d5db; -} - -.result-header { - margin-bottom: 16px; -} - -.result-header h3 { - margin: 0; - font-size: 18px; -} - -.result-similarity { - margin-bottom: 16px; -} - -.similarity-label { - font-size: 14px; - font-weight: 500; -} - -.similarity-value { - font-size: 24px; - font-weight: 700; - margin-left: 8px; -} - -.similarity-bar { - margin-top: 8px; - height: 8px; - background: #e5e7eb; - border-radius: 4px; - overflow: hidden; -} - -.similarity-fill { - height: 100%; - background: #2563eb; - transition: width 0.3s ease; -} - -.explanation-label { - font-weight: 500; - margin-bottom: 8px; -} - -.explanation-text { - line-height: 1.6; - color: #4b5563; -} - -.loading-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.spinner { - width: 40px; - height: 40px; - border: 4px solid rgba(255, 255, 255, 0.3); - border-top-color: white; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -.loading-overlay p { - margin-top: 16px; - color: white; -} - -/* Переменные цветов */ -:root { - --success: #10b981; - --warning: #f59e0b; - --error: #ef4444; + white-space: nowrap; + padding: 0 24px; } \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 48a1cb4..48629ba 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -33,7 +33,6 @@