add https server functionality - befor auth functionality implementation

This commit is contained in:
2025-10-23 11:41:03 +06:00
parent 3e1cb1e078
commit e1bd79db8c
27 changed files with 2164 additions and 97 deletions

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/validation"
)
// SaveResultRequest представляет запрос на сохранение результата
@@ -62,6 +63,20 @@ func handleSaveResult(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины полей
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidateCommand(req.Command); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidateExplanation(req.Explanation); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Создаем папку результатов если не существует
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
apiJsonResponse(w, SaveResultResponse{
@@ -124,6 +139,28 @@ func handleAddToHistory(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины полей
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidateCommand(req.Command); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidateCommand(req.Response); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidateExplanation(req.Explanation); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidateSystemPrompt(req.System); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Проверяем, есть ли уже такой запрос в истории
entries, err := Read(config.AppConfig.ResultHistory)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/gpt"
"github.com/direct-dev-ru/linux-command-gpt/validation"
)
// ExecuteRequest представляет запрос на выполнение
@@ -58,9 +59,20 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины пользовательского сообщения
if err := validation.ValidateUserMessage(req.Prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Определяем системный промпт
systemPrompt := ""
if req.SystemText != "" {
// Валидация длины пользовательского системного промпта
if err := validation.ValidateSystemPrompt(req.SystemText); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemPrompt = req.SystemText
} else if req.SystemID > 0 && req.SystemID <= 5 {
// Получаем системный промпт по ID
@@ -70,9 +82,19 @@ func handleExecute(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
return
}
// Валидация длины системного промпта из базы
if err := validation.ValidateSystemPrompt(prompt.Content); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemPrompt = prompt.Content
} else {
// Используем промпт по умолчанию
// Валидация длины системного промпта по умолчанию
if err := validation.ValidateSystemPrompt(config.AppConfig.Prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemPrompt = config.AppConfig.Prompt
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/gpt"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
"github.com/direct-dev-ru/linux-command-gpt/validation"
"github.com/russross/blackfriday/v2"
)
@@ -22,6 +23,8 @@ type ExecutePageData struct {
ResultSection template.HTML
VerboseButtons template.HTML
ActionButtons template.HTML
// Поля конфигурации для валидации
MaxUserMessageLength int
}
// SystemPromptOption представляет опцию системного промпта
@@ -74,13 +77,14 @@ func showExecuteForm(w http.ResponseWriter) {
}
data := ExecutePageData{
Title: "Выполнение запроса",
Header: "Выполнение запроса",
CurrentPrompt: "",
SystemOptions: systemOptions,
ResultSection: template.HTML(""),
VerboseButtons: template.HTML(""),
ActionButtons: template.HTML(""),
Title: "Выполнение запроса",
Header: "Выполнение запроса",
CurrentPrompt: "",
SystemOptions: systemOptions,
ResultSection: template.HTML(""),
VerboseButtons: template.HTML(""),
ActionButtons: template.HTML(""),
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -102,6 +106,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины пользовательского сообщения
if err := validation.ValidateUserMessage(prompt); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
systemID := 1
if systemIDStr != "" {
if id, err := strconv.Atoi(systemIDStr); err == nil && id >= 1 && id <= 5 {
@@ -116,6 +126,12 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины системного промпта
if err := validation.ValidateSystemPrompt(systemPrompt.Content); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Создаем GPT клиент
gpt3 := gpt.NewGpt3(
config.AppConfig.ProviderType,
@@ -179,13 +195,14 @@ func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
}
data := ExecutePageData{
Title: "Результат выполнения",
Header: "Результат выполнения",
CurrentPrompt: prompt,
SystemOptions: systemOptions,
ResultSection: template.HTML(formatResultSection(result)),
VerboseButtons: template.HTML(formatVerboseButtons(result)),
ActionButtons: template.HTML(formatActionButtons(result)),
Title: "Результат выполнения",
Header: "Результат выполнения",
CurrentPrompt: prompt,
SystemOptions: systemOptions,
ResultSection: template.HTML(formatResultSection(result)),
VerboseButtons: template.HTML(formatVerboseButtons(result)),
ActionButtons: template.HTML(formatActionButtons(result)),
MaxUserMessageLength: config.AppConfig.Validation.MaxUserMessageLength,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")

View File

@@ -151,18 +151,29 @@ func handleHistoryView(w http.ResponseWriter, r *http.Request) {
</div>`, string(explanationHTML))
}
// Создаем HTML страницу
htmlPage := fmt.Sprintf(templates.HistoryViewTemplate,
index, // title
index, // header
targetEntry.Timestamp.Format("02.01.2006 15:04:05"), // timestamp
index, // meta index
targetEntry.Command, // command
targetEntry.Response, // response
explanationSection, // explanation (if exists)
index, // delete button index
)
// Создаем данные для шаблона
data := struct {
Index int
Timestamp string
Command string
Response string
ExplanationHTML template.HTML
}{
Index: index,
Timestamp: targetEntry.Timestamp.Format("02.01.2006 15:04:05"),
Command: targetEntry.Command,
Response: targetEntry.Response,
ExplanationHTML: template.HTML(explanationSection),
}
// Парсим и выполняем шаблон
tmpl := templates.HistoryViewTemplate
t, err := template.New("history_view").Parse(tmpl)
if err != nil {
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(htmlPage))
t.Execute(w, data)
}

View File

@@ -9,8 +9,10 @@ import (
"strconv"
"strings"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/gpt"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
"github.com/direct-dev-ru/linux-command-gpt/validation"
)
// VerbosePrompt структура для промптов подробности
@@ -82,13 +84,19 @@ func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
verbosePrompts := getVerbosePromptsFromFile(pm.Prompts, lang)
data := struct {
Prompts []PromptWithDefault
VerbosePrompts []VerbosePrompt
Lang string
Prompts []PromptWithDefault
VerbosePrompts []VerbosePrompt
Lang string
MaxSystemPromptLength int
MaxPromptNameLength int
MaxPromptDescLength int
}{
Prompts: promptsWithDefault,
VerbosePrompts: verbosePrompts,
Lang: lang,
Prompts: promptsWithDefault,
VerbosePrompts: verbosePrompts,
Lang: lang,
MaxSystemPromptLength: config.AppConfig.Validation.MaxSystemPromptLength,
MaxPromptNameLength: config.AppConfig.Validation.MaxPromptNameLength,
MaxPromptDescLength: config.AppConfig.Validation.MaxPromptDescLength,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -124,6 +132,20 @@ func handleAddPrompt(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины полей
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidatePromptName(promptData.Name); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Добавляем промпт
if err := pm.AddPrompt(promptData.Name, promptData.Description, promptData.Content); err != nil {
http.Error(w, fmt.Sprintf("Ошибка добавления промпта: %v", err), http.StatusInternalServerError)
@@ -171,6 +193,20 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
return
}
// Валидация длины полей
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidatePromptName(promptData.Name); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Обновляем промпт
if err := pm.UpdatePrompt(id, promptData.Name, promptData.Description, promptData.Content); err != nil {
http.Error(w, fmt.Sprintf("Ошибка обновления промпта: %v", err), http.StatusInternalServerError)
@@ -181,6 +217,76 @@ func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Промпт успешно обновлен"))
}
// handleEditVerbosePrompt обрабатывает редактирование промпта подробности
func handleEditVerbosePrompt(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Получаем режим из URL
mode := strings.TrimPrefix(r.URL.Path, "/prompts/edit-verbose/")
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов
pm := gpt.NewPromptManager(homeDir)
// Парсим JSON данные
var promptData struct {
Name string `json:"name"`
Description string `json:"description"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&promptData); err != nil {
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
return
}
// Валидация длины полей
if err := validation.ValidateSystemPrompt(promptData.Content); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidatePromptName(promptData.Name); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validation.ValidatePromptDescription(promptData.Description); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Определяем ID по режиму
var id int
switch mode {
case "v":
id = 6
case "vv":
id = 7
case "vvv":
id = 8
default:
http.Error(w, "Неверный режим промпта", http.StatusBadRequest)
return
}
// Обновляем промпт
if err := pm.UpdatePrompt(id, promptData.Name, promptData.Description, promptData.Content); err != nil {
http.Error(w, fmt.Sprintf("Ошибка обновления промпта: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Промпт подробности успешно обновлен"))
}
// handleDeletePrompt обрабатывает удаление промпта
func handleDeletePrompt(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {

View File

@@ -191,12 +191,26 @@ func handleFileView(w http.ResponseWriter, r *http.Request) {
// Конвертируем Markdown в HTML
htmlContent := blackfriday.Run(content)
// Создаем HTML страницу с красивым отображением
htmlPage := fmt.Sprintf(templates.FileViewTemplate, filename, filename, string(htmlContent))
// Создаем данные для шаблона
data := struct {
Filename string
Content template.HTML
}{
Filename: filename,
Content: template.HTML(htmlContent),
}
// Парсим и выполняем шаблон
tmpl := templates.FileViewTemplate
t, err := template.New("file_view").Parse(tmpl)
if err != nil {
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
return
}
// Устанавливаем заголовки для отображения HTML
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(htmlPage))
t.Execute(w, data)
}
// handleDeleteFile обрабатывает удаление файла

View File

@@ -1,29 +1,149 @@
package serve
import (
"crypto/tls"
"fmt"
"net/http"
"os"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/ssl"
)
// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов
// StartResultServer запускает HTTP/HTTPS сервер для просмотра сохраненных результатов
func StartResultServer(host, port string) error {
// Регистрируем все маршруты
registerRoutes()
addr := fmt.Sprintf("%s:%s", host, port)
fmt.Printf("Сервер запущен на http://%s\n", addr)
fmt.Println("Нажмите Ctrl+C для остановки")
// Тестовое логирование для проверки debug флага
if config.AppConfig.MainFlags.Debug {
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
// Проверяем, нужно ли использовать HTTPS
useHTTPS := ssl.ShouldUseHTTPS(host)
if useHTTPS {
// Регистрируем HTTPS маршруты (включая редирект)
registerHTTPSRoutes()
// Создаем директорию для SSL сертификатов
sslDir := fmt.Sprintf("%s/server/ssl", config.AppConfig.Server.ConfigFolder)
if err := os.MkdirAll(sslDir, 0755); err != nil {
return fmt.Errorf("failed to create SSL directory: %v", err)
}
// Загружаем или генерируем SSL сертификат
cert, err := ssl.LoadOrGenerateCert(host)
if err != nil {
return fmt.Errorf("failed to load/generate SSL certificate: %v", err)
}
// Настраиваем TLS
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*cert},
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
// Отключаем проверку клиентских сертификатов
ClientAuth: tls.NoClientCert,
// Добавляем логирование для отладки
GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if config.AppConfig.MainFlags.Debug {
fmt.Printf("🔍 TLS запрос от %s (SNI: %s)\n", clientHello.Conn.RemoteAddr(), clientHello.ServerName)
}
return cert, nil
},
}
// Создаем HTTPS сервер
server := &http.Server{
Addr: addr,
TLSConfig: tlsConfig,
}
fmt.Printf("🔒 Сервер запущен на https://%s (SSL включен)\n", addr)
fmt.Println("Нажмите Ctrl+C для остановки")
// Тестовое логирование для проверки debug флага
if config.AppConfig.MainFlags.Debug {
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
} else {
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
}
return server.ListenAndServeTLS("", "")
} else {
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
// Регистрируем обычные маршруты для HTTP
registerRoutes()
fmt.Printf("🌐 Сервер запущен на http://%s (HTTP режим)\n", addr)
fmt.Println("Нажмите Ctrl+C для остановки")
// Тестовое логирование для проверки debug флага
if config.AppConfig.MainFlags.Debug {
fmt.Printf("🔍 DEBUG РЕЖИМ ВКЛЮЧЕН - веб-операции будут логироваться\n")
} else {
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
}
return http.ListenAndServe(addr, nil)
}
}
// handleHTTPSRedirect обрабатывает редирект с HTTP на HTTPS
func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) {
// Определяем протокол и хост
host := r.Host
if host == "" {
host = r.Header.Get("Host")
}
return http.ListenAndServe(addr, nil)
// Редиректим на HTTPS
httpsURL := fmt.Sprintf("https://%s%s", host, r.RequestURI)
http.Redirect(w, r, httpsURL, http.StatusMovedPermanently)
}
// registerHTTPSRoutes регистрирует маршруты для HTTPS сервера
func registerHTTPSRoutes() {
// Регистрируем все маршруты кроме главной страницы
registerRoutesExceptHome()
// Регистрируем главную страницу с проверкой HTTPS
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Проверяем, пришел ли запрос по HTTP (не HTTPS)
if r.TLS == nil {
handleHTTPSRedirect(w, r)
return
}
// Если уже HTTPS, обрабатываем как обычно
handleResultsPage(w, r)
})
}
// registerRoutesExceptHome регистрирует все маршруты кроме главной страницы
func registerRoutesExceptHome() {
// Файлы
http.HandleFunc("/file/", handleFileView)
http.HandleFunc("/delete/", handleDeleteFile)
// История запросов
http.HandleFunc("/history", handleHistoryPage)
http.HandleFunc("/history/view/", handleHistoryView)
http.HandleFunc("/history/delete/", handleDeleteHistoryEntry)
http.HandleFunc("/history/clear", handleClearHistory)
// Управление промптами
http.HandleFunc("/prompts", handlePromptsPage)
http.HandleFunc("/prompts/add", handleAddPrompt)
http.HandleFunc("/prompts/edit/", handleEditPrompt)
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)
http.HandleFunc("/prompts/save-lang", handleSaveLang)
// Веб-страница для выполнения запросов
http.HandleFunc("/run", handleExecutePage)
// API для выполнения запросов
http.HandleFunc("/api/execute", handleExecute)
// API для сохранения результатов и истории
http.HandleFunc("/api/save-result", handleSaveResult)
http.HandleFunc("/api/add-to-history", handleAddToHistory)
}
// registerRoutes регистрирует все маршруты сервера
@@ -43,6 +163,7 @@ func registerRoutes() {
http.HandleFunc("/prompts", handlePromptsPage)
http.HandleFunc("/prompts/add", handleAddPrompt)
http.HandleFunc("/prompts/edit/", handleEditPrompt)
http.HandleFunc("/prompts/edit-verbose/", handleEditVerbosePrompt)
http.HandleFunc("/prompts/delete/", handleDeletePrompt)
http.HandleFunc("/prompts/restore/", handleRestorePrompt)
http.HandleFunc("/prompts/restore-verbose/", handleRestoreVerbosePrompt)

View File

@@ -11,6 +11,15 @@ var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").P
e.preventDefault();
return false;
}
// Валидация длины полей
const prompt = document.getElementById('prompt').value;
const maxUserMessageLength = {{.MaxUserMessageLength}};
if (prompt.length > maxUserMessageLength) {
alert('Пользовательское сообщение слишком длинное: максимум ' + maxUserMessageLength + ' символов');
e.preventDefault();
return false;
}
this.dataset.submitting = 'true';
const submitBtn = document.getElementById('submitBtn');
const loading = document.getElementById('loading');

View File

@@ -7,13 +7,13 @@ const FileViewTemplate = `
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s - LCG Results</title>
<title>{{.Filename}} - LCG Results</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
min-height: 100vh;
}
.container {
@@ -25,7 +25,7 @@ const FileViewTemplate = `
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 20px 30px;
display: flex;
@@ -125,11 +125,11 @@ const FileViewTemplate = `
<body>
<div class="container">
<div class="header">
<h1>📄 %s</h1>
<h1>📄 {{.Filename}}</h1>
<a href="/" class="back-btn">← Назад к списку</a>
</div>
<div class="content">
%s
{{.Content}}
</div>
</div>
</body>

View File

@@ -143,6 +143,7 @@ const HistoryPageTemplate = `
.history-header { flex-direction: column; align-items: flex-start; gap: 8px; }
.history-item { padding: 15px; }
.history-response { font-size: 0.85em; }
.search-container input { font-size: 16px; width: 96% !important; }
}
@media (max-width: 480px) {

View File

@@ -7,13 +7,13 @@ const HistoryViewTemplate = `
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Запись #%d - LCG History</title>
<title>Запись #{{.Index}} - LCG History</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #56ab2f 0%%, #a8e6cf 100%%);
background: linear-gradient(135deg, #56ab2f 0%, #a8e6cf 100%);
min-height: 100vh;
}
.container {
@@ -25,7 +25,7 @@ const HistoryViewTemplate = `
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 20px 30px;
display: flex;
@@ -212,7 +212,7 @@ const HistoryViewTemplate = `
.back-btn { padding: 6px 12px; font-size: 0.9em; }
.content { padding: 20px; }
.actions { flex-direction: column; }
.action-btn { width: 100%; text-align: center; }
.action-btn { text-align: center; }
.history-response-content { font-size: 0.9em; }
}
@media (max-width: 480px) {
@@ -223,34 +223,34 @@ const HistoryViewTemplate = `
<body>
<div class="container">
<div class="header">
<h1>📝 Запись #%d</h1>
<h1>📝 Запись #{{.Index}}</h1>
<a href="/history" class="back-btn">← Назад к истории</a>
</div>
<div class="content">
<div class="history-meta">
<div class="history-meta-item">
<span class="history-meta-label">📅 Время:</span> %s
<span class="history-meta-label">📅 Время:</span> {{.Timestamp}}
</div>
<div class="history-meta-item">
<span class="history-meta-label">🔢 Индекс:</span> #%d
<span class="history-meta-label">🔢 Индекс:</span> #{{.Index}}
</div>
</div>
<div class="history-command">
<h3>💬 Запрос пользователя:</h3>
<div class="history-command-text">%s</div>
<div class="history-command-text">{{.Command}}</div>
</div>
<div class="history-response">
<h3>🤖 Ответ Модели:</h3>
<div class="history-response-content">%s</div>
<div class="history-response-content">{{.Response}}</div>
</div>
%s
{{.ExplanationHTML}}
<div class="actions">
<a href="/history" class="action-btn">📝 К истории</a>
<button class="action-btn delete-btn" onclick="deleteHistoryEntry(%d)">🗑️ Удалить запись</button>
<button class="action-btn delete-btn" onclick="deleteHistoryEntry({{.Index}})">🗑️ Удалить запись</button>
</div>
</div>
</div>

View File

@@ -407,7 +407,12 @@ const PromptsPageTemplate = `
function editVerbosePrompt(mode, content) {
// Редактирование промпта подробности
alert('Редактирование промптов подробности будет реализовано');
document.getElementById('formTitle').textContent = 'Редактировать промпт подробности (' + mode + ')';
document.getElementById('promptId').value = mode;
document.getElementById('promptName').value = mode;
document.getElementById('promptDescription').value = 'Промпт для режима ' + mode;
document.getElementById('promptContent').value = content;
document.getElementById('promptForm').style.display = 'block';
}
function deletePrompt(id) {
@@ -432,10 +437,42 @@ const PromptsPageTemplate = `
document.getElementById('promptFormData').addEventListener('submit', function(e) {
e.preventDefault();
// Валидация длины полей
const name = document.getElementById('promptName').value;
const description = document.getElementById('promptDescription').value;
const content = document.getElementById('promptContent').value;
const maxContentLength = {{.MaxSystemPromptLength}};
const maxNameLength = {{.MaxPromptNameLength}};
const maxDescLength = {{.MaxPromptDescLength}};
if (content.length > maxContentLength) {
alert('Содержимое промпта слишком длинное: максимум ' + maxContentLength + ' символов');
return;
}
if (name.length > maxNameLength) {
alert('Название промпта слишком длинное: максимум ' + maxNameLength + ' символов');
return;
}
if (description.length > maxDescLength) {
alert('Описание промпта слишком длинное: максимум ' + maxDescLength + ' символов');
return;
}
const formData = new FormData(this);
const id = formData.get('id');
const url = id ? '/prompts/edit/' + id : '/prompts/add';
const method = id ? 'PUT' : 'POST';
// Определяем, это системный промпт или промпт подробности
const isVerbosePrompt = ['v', 'vv', 'vvv'].includes(id);
let url, method;
if (isVerbosePrompt) {
url = '/prompts/edit-verbose/' + id;
method = 'PUT';
} else {
url = id ? '/prompts/edit/' + id : '/prompts/add';
method = id ? 'PUT' : 'POST';
}
fetch(url, {
method: method,

View File

@@ -171,7 +171,7 @@ const ResultsPageTemplate = `
.stats { grid-template-columns: 1fr 1fr; }
.nav-buttons { flex-direction: column; gap: 8px; }
.nav-btn, .nav-button { text-align: center; padding: 12px 16px; font-size: 14px; }
.search-container input { font-size: 16px; }
.search-container input { font-size: 16px; width: 96% !important; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1.8em; }