Compare commits

...

3 Commits

Author SHA1 Message Date
3e1cb1e078 release v2.0.1 2025-10-22 22:49:45 +06:00
e37599d3ef mobile version styled -ready for new version 2.0.1 2025-10-22 18:37:08 +06:00
344f763bb4 before refactor serve.go 2025-10-22 08:56:39 +06:00
38 changed files with 5342 additions and 2294 deletions

View File

@@ -1,33 +1,53 @@
archives:
- format: tar.gz
builds:
- binary: lcg
env:
- CGO_ENABLED=0
goarch:
- amd64
- arm64
- arm
goos:
- linux
- darwin
changelog:
filters:
exclude:
- '^docs:'
- '^test:'
sort: asc
checksum:
name_template: 'checksums.txt'
release:
draft: true
snapshot:
name_template: "{{ incpatch .Version }}-next"
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
release:
footer: >-
---
Released by [GoReleaser](https://github.com/goreleaser/goreleaser).

33
.goreleaser.yaml.old Normal file
View File

@@ -0,0 +1,33 @@
archives:
- format: tar.gz
builds:
- binary: lcg
env:
- CGO_ENABLED=0
goarch:
- amd64
- arm64
- arm
goos:
- linux
- darwin
changelog:
filters:
exclude:
- '^docs:'
- '^test:'
sort: asc
checksum:
name_template: 'checksums.txt'
release:
draft: true
snapshot:
name_template: "{{ incpatch .Version }}-next"
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

View File

@@ -181,3 +181,7 @@ curl -sS -X POST \
- Добавление новых полей в ответах, не используемых клиентом, допустимо при сохранении существующих.
- Переименование или удаление полей `response` (proxy) и `message.content` (ollama) нарушит совместимость.
---
Дополнительно: для HTTP API веб‑сервера (эндпоинт `POST /execute`, только `curl`) см. `API_GUIDE.md` с примерами и подробной схемой запроса/ответа.

259
API_GUIDE.md Normal file
View File

@@ -0,0 +1,259 @@
# API Guide - Linux Command GPT
## Обзор
API позволяет выполнять запросы к Linux Command GPT через HTTP POST запросы с помощью curl. API принимает только запросы от curl (проверка User-Agent).
## Endpoint
``` curl
POST /execute
```
## Запуск сервера
```bash
# Запуск сервера
lcg serve
# Запуск на другом порту
lcg serve --port 9000
# Запуск с автоматическим открытием браузера
lcg serve --browser
```
## Структура запроса
### JSON Payload
```json
{
"prompt": "создать директорию test",
"system_id": 1,
"system": "альтернативный системный промпт",
"verbose": "vv",
"timeout": 120
}
```
### Поля запроса
| Поле | Тип | Обязательное | Описание |
|------|-----|--------------|----------|
| `prompt` | string | ✅ | Пользовательский запрос |
| `system_id` | int | ❌ | ID системного промпта (1-5) |
| `system` | string | ❌ | Текст системного промпта (альтернатива system_id) |
| `verbose` | string | ❌ | Степень подробности: "v", "vv", "vvv" |
| `timeout` | int | ❌ | Таймаут в секундах (по умолчанию: 120) |
### Структура ответа
```json
{
"success": true,
"command": "mkdir test",
"explanation": "Команда mkdir создает новую директорию...",
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
"elapsed": 2.34
}
```
## Примеры использования
### 1. Базовый запрос
```bash
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{
"prompt": "создать директорию test"
}'
```
**Ответ:**
```json
{
"success": true,
"command": "mkdir test",
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
"elapsed": 1.23
}
```
### 2. Запрос с системным промптом по ID
```bash
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{
"prompt": "найти все файлы .txt",
"system_id": 2
}'
```
### 3. Запрос с кастомным системным промптом
```bash
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{
"prompt": "показать использование памяти",
"system": "Ты эксперт по Linux. Отвечай только командами без объяснений."
}'
```
### 4. Запрос с подробным объяснением
```bash
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{
"prompt": "архивировать папку documents",
"verbose": "vv",
"timeout": 180
}'
```
**Ответ:**
```json
{
"success": true,
"command": "tar -czf documents.tar.gz documents/",
"explanation": "Команда tar создает архив в формате gzip...",
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
"elapsed": 3.45
}
```
### 5. Запрос с максимальной подробностью
```bash
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{
"prompt": "настроить SSH сервер",
"system_id": 3,
"verbose": "vvv",
"timeout": 300
}'
```
## Системные промпты
| ID | Название | Описание |
|----|----------|----------|
| 1 | basic | Базовые команды Linux |
| 2 | advanced | Продвинутые команды |
| 3 | system | Системное администрирование |
| 4 | network | Сетевые команды |
| 5 | security | Безопасность |
## Степени подробности
| Уровень | Описание |
|---------|----------|
| `v` | Краткое объяснение |
| `vv` | Подробное объяснение с альтернативами |
| `vvv` | Максимально подробное объяснение с примерами |
## Обработка ошибок
### Ошибка валидации
```json
{
"success": false,
"error": "Prompt is required"
}
```
### Ошибка AI
```json
{
"success": false,
"error": "Failed to get response from AI"
}
```
### Ошибка доступа
``` text
HTTP 403 Forbidden
Only curl requests are allowed
```
## Переменные окружения
Убедитесь, что настроены необходимые переменные:
```bash
# Основные настройки
export LCG_PROVIDER="ollama"
export LCG_HOST="http://localhost:11434"
export LCG_MODEL="hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"
# Для proxy провайдера
export LCG_PROVIDER="proxy"
export LCG_HOST="https://your-proxy-server.com"
export LCG_JWT_TOKEN="your-jwt-token"
```
## Безопасность
- ✅ **Только curl**: API принимает только запросы от curl
- ✅ **POST только**: Только POST запросы к `/execute`
- ✅ **JSON валидация**: Строгая проверка входных данных
- ✅ **Таймауты**: Ограничение времени выполнения запросов
## Примеры скриптов
### Bash скрипт для автоматизации
```bash
#!/bin/bash
API_URL="http://localhost:8080/execute"
# Функция для выполнения запроса
execute_command() {
local prompt="$1"
local verbose="${2:-}"
curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"prompt\": \"$prompt\", \"verbose\": \"$verbose\"}" | \
jq -r '.command'
}
# Использование
echo "Команда: $(execute_command "создать директорию backup")"
```
### Python скрипт
```python
import requests
import json
def execute_command(prompt, system_id=None, verbose=None):
url = "http://localhost:8080/execute"
payload = {"prompt": prompt}
if system_id:
payload["system_id"] = system_id
if verbose:
payload["verbose"] = verbose
response = requests.post(url, json=payload)
return response.json()
# Использование
result = execute_command("показать использование диска", verbose="vv")
print(f"Команда: {result['command']}")
if 'explanation' in result:
print(f"Объяснение: {result['explanation']}")
```

152
CHANGELOG.txt Normal file
View File

@@ -0,0 +1,152 @@
CHANGELOG
=========
Версия 2.0.1 (2025-10-22)
=========================
## ✨ ИЗМЕНЕНИЯ И НОВОЕ
- 📱 Веб‑интерфейс: улучшена мобильная адаптация (кнопки, шрифты, отступы, верстка)
- 🧭 Учет `prefers-reduced-motion` для снижения анимаций при необходимости
- 🔌 Добавлен публичный REST эндпоинт `POST /execute` для программного доступа
- ⚠️ Ограничение безопасности: принимаются только запросы от `curl` (User-Agent)
- 📖 Подробности и примеры: см. `API_GUIDE.md`
## 🐛 ИСПРАВЛЕНИЯ И УЛУЧШЕНИЯ
- 🔧 Рефакторинг HTTPсервера: вынесен в пакет `serve/*` для лучшей поддерживаемости
- 🗑️ Удалены устаревшие файлы: `cmd/serve.go`, `_main.go`
- 🛠️ Скрипты релиза: небольшие правки в `shell-code/pre-release.sh` и `shell-code/release.py`
## 📚 ДОКУМЕНТАЦИЯ
- Добавлен `API_GUIDE.md` (описание `/execute`, примеры `curl`/Python)
- 📝 Обновлены `README.md` и `USAGE_GUIDE.md` (актуализация про мобильную версию и API)
- 🔗 В `API_CONTRACT.md` добавлена ссылка на `API_GUIDE.md` (HTTP API веб‑сервера)
## ♻️ СОВМЕСТИМОСТЬ
- ✅ Обратная совместимость сохранена; CLI и провайдеры (proxy/ollama) без изменений
- 🚫 Миграция не требуется
Версия 2.0.0 (2025-01-19)
=========================
КРУПНОЕ ОБНОВЛЕНИЕ: Полная реструктуризация архитектуры и добавление веб-интерфейса
## 🚀 НОВЫЕ ВОЗМОЖНОСТИ
### Веб-интерфейс
- ✨ Добавлен полноценный веб-сервер для управления результатами
- 🌐 Веб-интерфейс доступен по адресу http://localhost:8080 (по умолчанию)
- 📊 Страница просмотра результатов с форматированием и подсветкой синтаксиса
- 📝 Страница истории запросов с возможностью удаления
- ⚙️ Страница управления промптами с полным CRUD функционалом
### Управление промптами
- 🔧 Полная система управления системными промптами через веб-интерфейс
- 🌍 Поддержка многоязычности (английский/русский) с переключением языка
- 📝 Редактирование, добавление и удаление промптов через веб-интерфейс
- 🔄 Кнопки восстановления промптов к значениям по умолчанию
- 📋 Двухвкладочный интерфейс: системные промпты (1-5) и промпты подробности (v/vv/vvv)
### Улучшенная система промптов
- 📁 Централизованное хранение промптов в файле sys_prompts
- 🏗️ Встроенные промпты загружаются из YAML файла (builtin_prompts.yaml)
- 🔍 Автоматическое определение встроенных промптов
- 🌐 Многоязычные промпты с поддержкой английского и русского языков
- 🔄 Автоматическая инициализация sys_prompts при первом запуске CLI
### Новые CLI команды
- 📜 `lcg history` - просмотр истории запросов
- 📜 `lcg history clear` - очистка истории
- 📜 `lcg history delete <id>` - удаление записи из истории
- 📝 `lcg prompts list` - список всех промптов
- 📝 `lcg prompts list --full` - полный вывод содержимого промптов
- 📝 `lcg prompts add` - добавление нового промпта
- 📝 `lcg prompts edit <id>` - редактирование промпта
- 📝 `lcg prompts delete <id>` - удаление промпта
### Расширенная функциональность
- 🔍 Флаг --debug для отображения параметров запросов и промптов
- 📊 Улучшенное отображение результатов с подсветкой синтаксиса
- 🎨 Обновленный дизайн веб-интерфейса с современным UI
- 📱 Адаптивный дизайн для различных размеров экрана
## 🏗️ АРХИТЕКТУРНЫЕ ИЗМЕНЕНИЯ
### Новая структура проекта
- 📁 cmd/ - модули команд (explain.go, history.go, serve.go)
- 📁 config/ - централизованная конфигурация
- 📁 gpt/ - логика работы с промптами и LLM
- 📁 reader/ - модуль чтения файлов
- 📁 shell-code/ - скрипты сборки и развертывания
### Централизованная конфигурация
- ⚙️ config/config.go - единая точка конфигурации
- 🔧 Поддержка переменных окружения для всех настроек
- 📝 Автоматическое создание конфигурационных файлов
### Встроенные промпты
- 📄 builtin_prompts.yaml - YAML файл с встроенными промптами
- 🔧 builtin_prompts.go - логика работы с встроенными промптами
- 🌐 Поддержка многоязычности на уровне YAML
## 🐛 ИСПРАВЛЕНИЯ
- 🔧 Исправлена проблема с регистронезависимым поиском в истории
- 🎨 Улучшена цветовая схема веб-интерфейса (менее яркие цвета)
- 🔘 Стандартизированы размеры и цвета кнопок навигации
- 🌍 Исправлена логика определения языка в веб-интерфейсе
- 🔍 Исправлена логика определения встроенных промптов при смене языка
## 📋 ОБРАТНАЯ СОВМЕСТИМОСТЬ
- ✅ Все существующие CLI команды сохранены
- ✅ Поддержка всех переменных окружения из v1.x.x
- ✅ Автоматическая миграция существующих конфигураций
- ✅ Сохранена совместимость с существующими API
## 🔧 ТЕХНИЧЕСКИЕ УЛУЧШЕНИЯ
### Производительность
- ⚡ Оптимизирована загрузка промптов
- 🚀 Улучшена скорость работы веб-интерфейса
- 💾 Эффективное кэширование промптов
### Безопасность
- 🔒 Валидация входных данных в веб-интерфейсе
- 🛡️ Защита от XSS атак
- 🔐 Безопасная обработка файлов
### Код
- 🧹 Рефакторинг архитектуры для лучшей поддерживаемости
- 📚 Улучшенная документация кода
- 🧪 Добавлены тесты для критических компонентов
## 📦 ЗАВИСИМОСТИ
### Новые зависимости
- github.com/urfave/cli/v2 - CLI фреймворк
- gopkg.in/yaml.v3 - работа с YAML файлами
- html/template - шаблонизация веб-страниц
### Обновленные зависимости
- Обновлены все существующие зависимости до последних версий
## 🚀 МИГРАЦИЯ С v1.x.x
1. **Автоматическая миграция**: При первом запуске v2.0.0 автоматически создастся файл sys_prompts с встроенными промптами
2. **Сохранение настроек**: Все переменные окружения и настройки сохраняются
3. **Новые возможности**: Доступ к веб-интерфейсу через `lcg serve-result`
4. **Управление промптами**: Используйте `lcg prompts list` для просмотра всех промптов
## 📖 ДОКУМЕНТАЦИЯ
- 📚 Обновлен USAGE_GUIDE.md с описанием новых команд
- 📋 Добавлен API_CONTRACT.md для веб-интерфейса
- 🔧 Обновлен README.md с инструкциями по использованию
---
**Примечание**: Версия 2.0.0 представляет собой кардинальное обновление с полной реструктуризацией архитектуры. Рекомендуется ознакомиться с новыми возможностями веб-интерфейса и системой управления промптами.

View File

@@ -9,7 +9,7 @@ Generate Linux commands from natural language. Supports Ollama and Proxy backend
Build from source:
```bash
git clone --depth 1 https://github.com/Direct-Dev-Ru/go-lcg.git ~/.linux-command-gpt
git clone --depth 1 https://github.com/Direct-Dev-Ru/linux-command-gpt.git ~/.linux-command-gpt
cd ~/.linux-command-gpt
go build -o lcg
@@ -35,12 +35,29 @@ Explanations:
Clipboard support requires `xclip` or `xsel`.
## What's new in 2.0.1
- Mobile UI improvements: better responsiveness (buttons, fonts, spacing) and reduced motion support
- Public REST endpoint: `POST /execute` (curl-only) for programmatic access — see `API_GUIDE.md`
## Environment
- `LCG_PROVIDER` (ollama|proxy), `LCG_HOST`, `LCG_MODEL`, `LCG_PROMPT`
- `LCG_TIMEOUT` (default 120), `LCG_RESULT_FOLDER` (default ./gpt_results)
- `LCG_RESULT_HISTORY` (default $(LCG_RESULT_FOLDER)/lcg_history.json)
- `LCG_JWT_TOKEN` (for proxy)
- `LCG_PROVIDER` (default `ollama`) — provider type: `ollama` or `proxy`
- `LCG_HOST` (default `http://192.168.87.108:11434/`) — base API URL
- `LCG_MODEL` (default `hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M`)
- `LCG_PROMPT` — default system prompt content
- `LCG_PROXY_URL` (default `/api/v1/protected/sberchat/chat`) — proxy chat endpoint
- `LCG_COMPLETIONS_PATH` (default `api/chat`) — Ollama chat endpoint (relative)
- `LCG_TIMEOUT` (default `300`) — request timeout in seconds
- `LCG_RESULT_FOLDER` (default `~/.config/lcg/gpt_results`) — folder for saved results
- `LCG_RESULT_HISTORY` (default `$(LCG_RESULT_FOLDER)/lcg_history.json`) — JSON history path
- `LCG_PROMPT_FOLDER` (default `~/.config/lcg/gpt_sys_prompts`) — folder for system prompts
- `LCG_PROMPT_ID` (default `1`) — default system prompt ID
- `LCG_BROWSER_PATH` — custom browser executable path for `--browser` flag
- `LCG_JWT_TOKEN` — JWT token for proxy provider
- `LCG_NO_HISTORY` — if `1`/`true`, disables history writes for the process
- `LCG_ALLOW_EXECUTION` — if `1`/`true`, enables command execution via `(e)` action menu
- `LCG_SERVER_PORT` (default `8080`), `LCG_SERVER_HOST` (default `localhost`) — HTTP server settings
## Flags
@@ -48,6 +65,8 @@ Clipboard support requires `xclip` or `xsel`.
- `--sys, -s` system prompt content or ID
- `--prompt-id, --pid` choose built-in prompt (15)
- `--timeout, -t` request timeout (sec)
- `--no-history, --nh` disable writing/updating JSON history for this run
- `--debug, -d` show debug information (request parameters and prompts)
- `--version, -v` print version; `--help, -h` help
## Commands
@@ -60,10 +79,13 @@ Clipboard support requires `xclip` or `xsel`.
- `history list` — list history from JSON
- `history view <index>` — view by index
- `history delete <index>` — delete by index (re-numbering)
- `serve` — start HTTP server to browse saved results (`--port`, `--host`, `--browser`)
- `/run` — web interface for executing requests
- `/execute` — API endpoint for programmatic access via curl
## Saving results
Files are saved to `LCG_RESULT_FOLDER`.
Files are saved to `LCG_RESULT_FOLDER` (default `~/.config/lcg/gpt_results`).
- Command result: `gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md`
- `# <title>` — H1 with original request (trimmed to 120 chars: first 116 + `...`)
@@ -82,4 +104,64 @@ Files are saved to `LCG_RESULT_FOLDER`.
- On new request, if the same command exists, you will be prompted to view or overwrite.
- Showing from history does not call the API; the standard action menu is shown.
## Browser Integration
The `serve` command supports automatic browser opening:
```bash
# Start server and open browser automatically
lcg serve --browser
# Use custom browser
export LCG_BROWSER_PATH="/usr/bin/firefox"
lcg serve --browser
# Start on custom host/port with browser
lcg serve --host 0.0.0.0 --port 9000 --browser
```
Supported browsers (in priority order):
- Yandex Browser (`yandex-browser`, `yandex-browser-stable`)
- Mozilla Firefox (`firefox`, `firefox-esr`)
- Google Chrome (`google-chrome`, `google-chrome-stable`)
- Chromium (`chromium`, `chromium-browser`)
## API Access
The `serve` command provides both a web interface and REST API:
**Web Interface:**
- Browse results at `http://localhost:8080/`
- Execute requests at `http://localhost:8080/run`
- Manage prompts at `http://localhost:8080/prompts`
- View history at `http://localhost:8080/history`
**REST API:**
```bash
# Start server
lcg serve
# Make API request
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-d '{"prompt": "create directory test", "verbose": "vv"}'
```
**Response:**
```json
{
"success": true,
"command": "mkdir test",
"explanation": "The mkdir command creates a new directory...",
"model": "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M",
"elapsed": 1.23
}
```
For complete API documentation, see `API_GUIDE.md`.
For full guide in Russian, see `USAGE_GUIDE.md`.

View File

@@ -21,7 +21,8 @@ sudo apt-get install xsel
Сборка из исходников:
```bash
git clone --depth 1 https://github.com/asrul10/linux-command-gpt.git ~/.linux-command-gpt
git clone --depth 1 https://github.com/Direct-Dev-Ru/go-lcg.git ~/.linux-command-gpt
cd ~/.linux-command-gpt
go build -o lcg
@@ -59,6 +60,12 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего:
```
### Что нового в 2.0.1
- Улучшена мобильная версия веб‑интерфейса: корректные размеры кнопок, шрифтов и отступов; адаптивная верстка
- Учитывается `prefers-reduced-motion` для снижения анимаций, если это задано в системе
- Добавлен REST эндпоинт `POST /execute` (только через curl) — см. подробности и примеры в `API_GUIDE.md`
## Переменные окружения
Можно настроить поведение без изменения командной строки.
@@ -75,10 +82,12 @@ lcg --file /path/to/context.txt "хочу вывести список дирек
| `LCG_PROVIDER` | `ollama` | Тип провайдера: `ollama` или `proxy`. |
| `LCG_JWT_TOKEN` | пусто | JWT токен для `proxy` провайдера (альтернатива — файл `~/.proxy_jwt_token`). |
| `LCG_PROMPT_ID` | `1` | ID системного промпта по умолчанию. |
| `LCG_BROWSER_PATH` | пусто | Путь к браузеру для автооткрытия (`--browser`). |
| `LCG_TIMEOUT` | `300` | Таймаут запроса в секундах. |
| `LCG_RESULT_HISTORY` | `$(LCG_RESULT_FOLDER)/lcg_history.json` | Путь к JSONистории запросов. |
| `LCG_PROMPT_FOLDER` | `~/.config/lcg/gpt_sys_prompts` | Папка для хранения системных промптов. |
| `LCG_NO_HISTORY` | пусто | Если `1`/`true` — полностью отключает запись/обновление истории. |
| `LCG_ALLOW_EXECUTION` | пусто | Если `1`/`true` — включает возможность выполнения команд через опцию `(e)` в меню действий. |
| `LCG_SERVER_PORT` | `8080` | Порт для HTTP сервера просмотра результатов. |
| `LCG_SERVER_HOST` | `localhost` | Хост для HTTP сервера просмотра результатов. |
@@ -108,7 +117,7 @@ lcg [глобальные опции] <описание команды>
- `--file, -f string` — прочитать часть запроса из файла и добавить к описанию.
- `--sys, -s string` — системный промпт (содержимое или ID как строка). Если не задан, используется `--prompt-id` или `LCG_PROMPT`.
- `--prompt-id, --pid int` — ID системного промпта (15 для стандартных, либо ваш кастомный ID).
- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 300).
- `--timeout, -t int` — таймаут запроса в секундах (по умолчанию 120; через `LCG_TIMEOUT` 300).
- `--no-history, --nh` — отключить запись/обновление истории для текущего запуска.
- `--debug, -d` — показать отладочную информацию (параметры запроса и промпты).
- `--version, -v` — вывести версию.
@@ -132,10 +141,11 @@ lcg [глобальные опции] <описание команды>
- `lcg prompts list --full` (`-f`) — полный вывод содержимого без обрезки длинных строк.
- `lcg prompts add` (`-a`) — добавить пользовательский промпт (по шагам в интерактиве).
- `lcg prompts delete <id>` (`-d`) — удалить пользовательский промпт по ID (>5).
- `lcg test-prompt <prompt-id> <описание>` (`-tp`): показать детали выбранного системного промпта и протестировать его на заданном описании.
- `lcg serve-result` (`serve`): запустить HTTP сервер для просмотра сохраненных результатов:
- `lcg test-prompt <prompt-id> <описание>` (алиас: `tp`): показать детали выбранного системного промпта и протестировать его на заданном описании.
- `lcg serve`: запустить HTTP сервер для просмотра сохраненных результатов:
- `--port, -p` — порт сервера (по умолчанию из `LCG_SERVER_PORT`)
- `--host, -H` — хост сервера (по умолчанию из `LCG_SERVER_HOST`)
- `--browser, -b` — открыть браузер автоматически после старта
### Подробные объяснения (v/vv/vvv)
@@ -218,7 +228,7 @@ lcg [глобальные опции] <описание команды>
### Веб-интерфейс управления
Через HTTP сервер (`lcg serve-result`) доступно полное управление промптами:
Через HTTP сервер (`lcg serve`) доступно полное управление промптами:
- **Просмотр всех промптов** (встроенных и пользовательских)
- **Редактирование любых промптов** (включая встроенные)
@@ -236,22 +246,25 @@ gpt_request_<MODEL>_YYYY-MM-DD_HH-MM-SS.md
## HTTP сервер для просмотра результатов
Команда `lcg serve-result` запускает веб-сервер для удобного просмотра всех сохраненных результатов:
Команда `lcg serve` запускает веб-сервер для удобного просмотра всех сохраненных результатов:
```bash
# Запуск с настройками по умолчанию
lcg serve-result
lcg serve
# Запуск на другом порту
lcg serve-result --port 9090
lcg serve --port 9090
# Запуск на другом хосте
lcg serve-result --host 0.0.0.0 --port 8080
lcg serve --host 0.0.0.0 --port 8080
# Автооткрытие браузера (опционально)
lcg serve --browser
# Использование переменных окружения
export LCG_SERVER_PORT=3000
export LCG_SERVER_HOST=0.0.0.0
lcg serve-result
lcg serve
```
### Возможности веб-интерфейса
@@ -276,6 +289,11 @@ lcg serve-result
- `## Command` — первая сгенерированная команда.
- `## Explanation and Alternatives (model: <MODEL>)` — подробное объяснение и альтернативы.
### Браузер
- По умолчанию UI не открывается автоматически. Для автооткрытия используйте `--browser`.
- Путь к конкретному браузеру можно задать переменной `LCG_BROWSER_PATH`.
## Выполнение сгенерированной команды
Действие `e` запустит команду через `bash -c`. Перед запуском потребуется подтверждение `y/yes`. Всегда проверяйте команду вручную, особенно при операциях с файлами и сетью.
@@ -333,18 +351,18 @@ lcg models
```bash
# Запуск сервера
lcg serve-result
lcg serve
# Запуск на другом порту
lcg serve-result --port 9090
lcg serve --port 9090
# Запуск на всех интерфейсах
lcg serve-result --host 0.0.0.0 --port 8080
lcg serve --host 0.0.0.0 --port 8080
```
## История
`lcg history` выводит историю текущего процесса (не сохраняется между запусками, максимум 100 записей):
`lcg history` выводит историю из JSONфайла (`LCG_RESULT_HISTORY`), сохраняется между запусками:
```bash
lcg history list
@@ -387,3 +405,20 @@ lcg history list
## Лицензия и исходники
См. README и репозиторий проекта. Предложения и баг‑репорты приветствуются в Issues.
## Доступ к локальному API
Эндпоинт: `POST /execute` (только через curl).
```bash
# Запустить сервер
lcg serve
# Выполнить запрос
curl -X POST http://localhost:8080/execute \
-H "Content-Type: application/json" \
-A curl \
-d '{"prompt": "create directory test", "verbose": "vv"}'
```
Подробности и примеры: `API_GUIDE.md`.

View File

@@ -1 +1 @@
v2.0.0
v2.0.1

224
_main.go
View File

@@ -1,224 +0,0 @@
// package main
// import (
// _ "embed"
// "fmt"
// "math"
// "os"
// "os/user"
// "path"
// "strings"
// "time"
// "github.com/atotto/clipboard"
// "github.com/direct-dev-ru/linux-command-gpt/gpt"
// "github.com/direct-dev-ru/linux-command-gpt/reader"
// )
// //go:embed VERSION.txt
// var Version string
// var cwd, _ = os.Getwd()
// var (
// HOST = getEnv("LCG_HOST", "http://192.168.87.108:11434/")
// COMPLETIONS = getEnv("LCG_COMPLETIONS_PATH", "api/chat") // relative part of endpoint
// MODEL = getEnv("LCG_MODEL", "codegeex4")
// PROMPT = getEnv("LCG_PROMPT", "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols.")
// API_KEY_FILE = getEnv("LCG_API_KEY_FILE", ".openai_api_key")
// RESULT_FOLDER = getEnv("LCG_RESULT_FOLDER", path.Join(cwd, "gpt_results"))
// // HOST = "https://api.openai.com/v1/"
// // COMPLETIONS = "chat/completions"
// // MODEL = "gpt-4o-mini"
// // MODEL = "codellama:13b"
// // This file is created in the user's home directory
// // Example: /home/username/.openai_api_key
// // API_KEY_FILE = ".openai_api_key"
// HELP = `
// Usage: lcg [options]
// --help -h output usage information
// --version -v output the version number
// --file -f read part of command from file or bash feature $(...)
// --update-key -u update the API key
// --delete-key -d delete the API key
// Example Usage: lcg I want to extract linux-command-gpt.tar.gz file
// Example Usage: lcg --file /path/to/file.json I want to print object questions with jq
// Env Vars:
// LCG_HOST - defaults to "http://192.168.87.108:11434/" - endpoint for Ollama or other LLM API
// LCG_COMPLETIONS_PATH -defaults to "api/chat" - relative part of endpoint
// LCG_MODEL - defaults to "codegeex4"
// LCG_PROMPT - defaults to Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks.
// LCG_API_KEY_FILE - defaults to ${HOME}/.openai_api_key - file with API key
// LCG_RESULT_FOLDER - defaults to $(pwd)/gpt_results - folder to save results
// `
// VERSION = Version
// CMD_HELP = 100
// CMD_VERSION = 101
// CMD_UPDATE = 102
// CMD_DELETE = 103
// CMD_COMPLETION = 110
// )
// // getEnv retrieves the value of the environment variable `key` or returns `defaultValue` if not set.
// func getEnv(key, defaultValue string) string {
// if value, exists := os.LookupEnv(key); exists {
// return value
// }
// return defaultValue
// }
// func handleCommand(cmd string) int {
// if cmd == "" || cmd == "--help" || cmd == "-h" {
// return CMD_HELP
// }
// if cmd == "--version" || cmd == "-v" {
// return CMD_VERSION
// }
// if cmd == "--update-key" || cmd == "-u" {
// return CMD_UPDATE
// }
// if cmd == "--delete-key" || cmd == "-d" {
// return CMD_DELETE
// }
// return CMD_COMPLETION
// }
// func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
// gpt3.InitKey()
// s := time.Now()
// done := make(chan bool)
// go func() {
// loadingChars := []rune{'-', '\\', '|', '/'}
// i := 0
// for {
// select {
// case <-done:
// fmt.Printf("\r")
// return
// default:
// fmt.Printf("\rLoading %c", loadingChars[i])
// i = (i + 1) % len(loadingChars)
// time.Sleep(30 * time.Millisecond)
// }
// }
// }()
// r := gpt3.Completions(cmd)
// done <- true
// elapsed := time.Since(s).Seconds()
// elapsed = math.Round(elapsed*100) / 100
// if r == "" {
// return "", elapsed
// }
// return r, elapsed
// }
// func main() {
// currentUser, err := user.Current()
// if err != nil {
// panic(err)
// }
// args := os.Args
// cmd := ""
// file := ""
// if len(args) > 1 {
// start := 1
// if args[1] == "--file" || args[1] == "-f" {
// file = args[2]
// start = 3
// }
// cmd = strings.Join(args[start:], " ")
// }
// if file != "" {
// err := reader.FileToPrompt(&cmd, file)
// if err != nil {
// fmt.Println(err)
// return
// }
// }
// if _, err := os.Stat(RESULT_FOLDER); os.IsNotExist(err) {
// os.MkdirAll(RESULT_FOLDER, 0755)
// }
// h := handleCommand(cmd)
// if h == CMD_HELP {
// fmt.Println(HELP)
// return
// }
// if h == CMD_VERSION {
// fmt.Println(VERSION)
// return
// }
// gpt3 := gpt.Gpt3{
// CompletionUrl: HOST + COMPLETIONS,
// Model: MODEL,
// Prompt: PROMPT,
// HomeDir: currentUser.HomeDir,
// ApiKeyFile: API_KEY_FILE,
// Temperature: 0.01,
// }
// if h == CMD_UPDATE {
// gpt3.UpdateKey()
// return
// }
// if h == CMD_DELETE {
// gpt3.DeleteKey()
// return
// }
// c := "R"
// r := ""
// elapsed := 0.0
// for c == "R" || c == "r" {
// r, elapsed = getCommand(gpt3, cmd)
// c = "N"
// fmt.Printf("Completed in %v seconds\n\n", elapsed)
// fmt.Println(r)
// fmt.Print("\nDo you want to (c)opy, (s)ave to file, (r)egenerate, or take (N)o action on the command? (c/r/N): ")
// fmt.Scanln(&c)
// // no action
// if c == "N" || c == "n" {
// return
// }
// }
// if r == "" {
// return
// }
// // Copy to clipboard
// if c == "C" || c == "c" {
// clipboard.WriteAll(r)
// fmt.Println("\033[33mCopied to clipboard")
// return
// }
// if c == "S" || c == "s" {
// timestamp := time.Now().Format("2006-01-02_15-04-05") // Format: YYYY-MM-DD_HH-MM-SS
// filename := fmt.Sprintf("gpt_request_%s(%s).md", timestamp, gpt3.Model)
// filePath := path.Join(RESULT_FOLDER, filename)
// resultString := fmt.Sprintf("## Prompt:\n\n%s\n\n------------------\n\n## Response:\n\n%s\n\n", cmd+". "+gpt3.Prompt, r)
// os.WriteFile(filePath, []byte(resultString), 0644)
// fmt.Println("\033[33mSaved to file")
// return
// }
// }

View File

@@ -54,6 +54,7 @@ func ShowHistory(historyPath string, printColored func(string, string), colorYel
for _, h := range items {
ts := h.Timestamp.Format("2006-01-02 15:04:05")
fmt.Printf("%d. [%s] %s → %s\n", h.Index, ts, h.Command, h.Response)
fmt.Printf("%s\n", "========================================================================================")
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,23 +7,24 @@ import (
)
type Config struct {
Cwd string
Host string
ProxyUrl string
Completions string
Model string
Prompt string
ApiKeyFile string
ResultFolder string
PromptFolder string
ProviderType string
JwtToken string
PromptID string
Timeout string
ResultHistory string
NoHistoryEnv string
MainFlags MainFlags
Server ServerConfig
Cwd string
Host string
ProxyUrl string
Completions string
Model string
Prompt string
ApiKeyFile string
ResultFolder string
PromptFolder string
ProviderType string
JwtToken string
PromptID string
Timeout string
ResultHistory string
NoHistoryEnv string
AllowExecution bool
MainFlags MainFlags
Server ServerConfig
}
type MainFlags struct {
@@ -61,21 +62,22 @@ func Load() Config {
promptFolder := getEnv("LCG_PROMPT_FOLDER", path.Join(homedir, ".config", "lcg", "gpt_sys_prompts"))
return Config{
Cwd: cwd,
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
Model: getEnv("LCG_MODEL", "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"),
Prompt: getEnv("LCG_PROMPT", "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols."),
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
ResultFolder: resultFolder,
PromptFolder: promptFolder,
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
PromptID: getEnv("LCG_PROMPT_ID", "1"),
Timeout: getEnv("LCG_TIMEOUT", "300"),
ResultHistory: getEnv("LCG_RESULT_HISTORY", path.Join(resultFolder, "lcg_history.json")),
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
Cwd: cwd,
Host: getEnv("LCG_HOST", "http://192.168.87.108:11434/"),
ProxyUrl: getEnv("LCG_PROXY_URL", "/api/v1/protected/sberchat/chat"),
Completions: getEnv("LCG_COMPLETIONS_PATH", "api/chat"),
Model: getEnv("LCG_MODEL", "hf.co/yandex/YandexGPT-5-Lite-8B-instruct-GGUF:Q4_K_M"),
Prompt: getEnv("LCG_PROMPT", "Reply with linux command and nothing else. Output with plain response - no need formatting. No need explanation. No need code blocks. No need ` symbols."),
ApiKeyFile: getEnv("LCG_API_KEY_FILE", ".openai_api_key"),
ResultFolder: resultFolder,
PromptFolder: promptFolder,
ProviderType: getEnv("LCG_PROVIDER", "ollama"),
JwtToken: getEnv("LCG_JWT_TOKEN", ""),
PromptID: getEnv("LCG_PROMPT_ID", "1"),
Timeout: getEnv("LCG_TIMEOUT", "300"),
ResultHistory: getEnv("LCG_RESULT_HISTORY", path.Join(resultFolder, "lcg_history.json")),
NoHistoryEnv: getEnv("LCG_NO_HISTORY", ""),
AllowExecution: isAllowExecutionEnabled(),
Server: ServerConfig{
Port: getEnv("LCG_SERVER_PORT", "8080"),
Host: getEnv("LCG_SERVER_HOST", "localhost"),
@@ -92,6 +94,15 @@ func (c Config) IsNoHistoryEnabled() bool {
return vLower == "1" || vLower == "true"
}
func isAllowExecutionEnabled() bool {
v := strings.TrimSpace(getEnv("LCG_ALLOW_EXECUTION", ""))
if v == "" {
return false
}
vLower := strings.ToLower(v)
return vLower == "1" || vLower == "true"
}
var AppConfig Config
func init() {

1
go.sum
View File

@@ -8,6 +8,7 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -53,13 +53,11 @@ func NewPromptManager(homeDir string) *PromptManager {
// createInitialPromptsFile создает начальный файл с системными промптами и промптами подробности
func (pm *PromptManager) createInitialPromptsFile() {
// Загружаем все встроенные промпты из YAML (английские по умолчанию)
pm.Prompts = GetBuiltinPrompts()
// Устанавливаем язык по умолчанию как русский
pm.Language = "ru"
// Фикс: при первичном сохранении явно выставляем язык файла
if pm.Language == "" {
pm.Language = "en"
}
// Загружаем все встроенные промпты из YAML на русском языке
pm.Prompts = GetBuiltinPromptsByLanguage("ru")
// Сохраняем все промпты в файл
pm.saveAllPrompts()
@@ -379,3 +377,27 @@ func truncateString(s string, maxLen int) string {
}
return s[:maxLen-3] + "..."
}
// GetVerbosePromptByLevel возвращает промпт для подробного объяснения по уровню
func GetVerbosePromptByLevel(level int) string {
// Создаем PromptManager для получения текущего языка из sys_prompts (без принудительной загрузки дефолтов)
pm := NewPromptManager("")
currentLang := pm.GetCurrentLanguage()
var prompt *SystemPrompt
switch level {
case 1:
prompt = GetBuiltinPromptByIDAndLanguage(6, currentLang) // v
case 2:
prompt = GetBuiltinPromptByIDAndLanguage(7, currentLang) // vv
case 3:
prompt = GetBuiltinPromptByIDAndLanguage(8, currentLang) // vvv
default:
return ""
}
if prompt != nil {
return prompt.Content
}
return ""
}

112
main.go
View File

@@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"time"
@@ -16,6 +17,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/reader"
"github.com/direct-dev-ru/linux-command-gpt/serve"
"github.com/urfave/cli/v2"
)
@@ -517,9 +519,8 @@ func getCommands() []*cli.Command {
},
},
{
Name: "serve-result",
Aliases: []string{"serve"},
Usage: "Start HTTP server to browse saved results",
Name: "serve",
Usage: "Start HTTP server to browse saved results",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port",
@@ -533,16 +534,42 @@ func getCommands() []*cli.Command {
Usage: "Server host",
Value: config.AppConfig.Server.Host,
},
&cli.BoolFlag{
Name: "browser",
Aliases: []string{"b"},
Usage: "Open browser automatically after starting server",
Value: false,
},
},
Action: func(c *cli.Context) error {
port := c.String("port")
host := c.String("host")
openBrowser := c.Bool("browser")
// Пробрасываем глобальный флаг debug для web-сервера
// Позволяет запускать: lcg -d serve -p ...
if c.Bool("debug") {
config.AppConfig.MainFlags.Debug = true
}
printColored(fmt.Sprintf("🌐 Запускаю HTTP сервер на %s:%s\n", host, port), colorCyan)
printColored(fmt.Sprintf("📁 Папка результатов: %s\n", config.AppConfig.ResultFolder), colorYellow)
printColored(fmt.Sprintf("🔗 Откройте в браузере: http://%s:%s\n", host, port), colorGreen)
return cmdPackage.StartResultServer(host, port)
url := fmt.Sprintf("http://%s:%s", host, port)
if openBrowser {
printColored("🌍 Открываю браузер...\n", colorGreen)
if err := openBrowserURL(url); err != nil {
printColored(fmt.Sprintf("⚠️ Не удалось открыть браузер: %v\n", err), colorYellow)
printColored("📱 Откройте браузер вручную и перейдите по адресу: ", colorGreen)
printColored(url+"\n", colorYellow)
}
} else {
printColored("🔗 Откройте в браузере: ", colorGreen)
printColored(url+"\n", colorYellow)
}
return serve.StartResultServer(host, port)
},
},
}
@@ -668,7 +695,14 @@ func getCommand(gpt3 gpt.Gpt3, cmd string) (string, float64) {
}
func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, timeout int, explanation string) {
fmt.Printf("Действия: (c)копировать, (s)сохранить, (r)перегенерировать, (e)выполнить, (v|vv|vvv)подробно, (n)ничего: ")
// Формируем меню действий
menu := "Действия: (c)копировать, (s)сохранить, (r)перегенерировать"
if config.AppConfig.AllowExecution {
menu += ", (e)выполнить"
}
menu += ", (v|vv|vvv)подробно, (n)ничего: "
fmt.Print(menu)
var choice string
fmt.Scanln(&choice)
@@ -700,13 +734,17 @@ func handlePostResponse(response string, gpt3 gpt.Gpt3, system, cmd string, time
fmt.Println("🔄 Перегенерирую...")
executeMain("", system, cmd, timeout)
case "e":
executeCommand(response)
if !disableHistory {
if fromHistory {
cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation)
} else {
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
if config.AppConfig.AllowExecution {
executeCommand(response)
if !disableHistory {
if fromHistory {
cmdPackage.SaveToHistoryFromHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt, explanation)
} else {
cmdPackage.SaveToHistory(config.AppConfig.ResultHistory, config.AppConfig.ResultFolder, cmd, response, gpt3.Prompt)
}
}
} else {
fmt.Println("⚠️ Выполнение команд отключено. Установите LCG_ALLOW_EXECUTION=1 для включения этой функции.")
}
case "v", "vv", "vvv":
level := len(choice) // 1, 2, 3
@@ -782,7 +820,9 @@ func showTips() {
fmt.Println(" • Команда 'history list' покажет историю запросов")
fmt.Println(" • Команда 'config' покажет текущие настройки")
fmt.Println(" • Команда 'health' проверит доступность API")
fmt.Println(" • Команда 'serve-result' запустит HTTP сервер для просмотра результатов")
fmt.Println(" • Команда 'serve' запустит HTTP сервер для просмотра результатов")
fmt.Println(" • Используйте --browser для автоматического открытия браузера")
fmt.Println(" • Установите LCG_BROWSER_PATH для указания конкретного браузера")
}
// printDebugInfo выводит отладочную информацию о параметрах запроса
@@ -798,3 +838,49 @@ func printDebugInfo(file, system, commandInput string, timeout int) {
fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory)
printColored("────────────────────────────────────────\n", colorCyan)
}
// openBrowserURL открывает URL в браузере
func openBrowserURL(url string) error {
// Проверяем переменную окружения LCG_BROWSER_PATH
if browserPath := os.Getenv("LCG_BROWSER_PATH"); browserPath != "" {
return exec.Command(browserPath, url).Start()
}
// Список браузеров в порядке приоритета
browsers := []string{
"yandex-browser", // Яндекс.Браузер
"yandex-browser-stable", // Яндекс.Браузер (стабильная версия)
"firefox", // Mozilla Firefox
"firefox-esr", // Firefox ESR
"google-chrome", // Google Chrome
"google-chrome-stable", // Google Chrome (стабильная версия)
"chromium", // Chromium
"chromium-browser", // Chromium (Ubuntu/Debian)
}
// Стандартные пути для поиска браузеров
paths := []string{
"/usr/bin",
"/usr/local/bin",
"/opt/google/chrome",
"/opt/yandex/browser",
"/snap/bin",
"/usr/lib/chromium-browser",
}
// Ищем браузер в указанном порядке
for _, browser := range browsers {
for _, path := range paths {
fullPath := filepath.Join(path, browser)
if _, err := os.Stat(fullPath); err == nil {
return exec.Command(fullPath, url).Start()
}
}
// Также пробуем найти в PATH
if _, err := exec.LookPath(browser); err == nil {
return exec.Command(browser, url).Start()
}
}
return fmt.Errorf("не найден ни один из поддерживаемых браузеров")
}

110
serve/README.md Normal file
View File

@@ -0,0 +1,110 @@
# Пакет serve
Этот пакет содержит HTTP сервер для веб-интерфейса LCG (Linux Command GPT).
## Структура файлов
### serve.go
Основной файл пакета. Содержит:
- `StartResultServer()` - функция запуска HTTP сервера
- `registerRoutes()` - регистрация всех маршрутов
### results.go
Обработчики для результатов и файлов:
- `handleResultsPage()` - главная страница со списком файлов результатов
- `handleFileView()` - просмотр конкретного файла
- `handleDeleteFile()` - удаление файла результата
- `getResultFiles()` - получение списка файлов
- `formatFileSize()` - форматирование размера файла
### history.go
Обработчики для работы с историей запросов:
- `handleHistoryPage()` - страница истории запросов
- `handleDeleteHistoryEntry()` - удаление записи из истории
- `handleClearHistory()` - очистка всей истории
- `readHistoryEntries()` - чтение записей истории
### history_utils.go
Утилиты для работы с историей:
- `HistoryEntry` - структура записи истории
- `read()` - чтение истории из файла
- `write()` - запись истории в файл
- `DeleteHistoryEntry()` - удаление записи по индексу
### prompts.go
Обработчики для управления промптами:
- `handlePromptsPage()` - страница управления промптами
- `handleAddPrompt()` - добавление нового промпта
- `handleEditPrompt()` - редактирование промпта
- `handleDeletePrompt()` - удаление промпта
- `handleRestorePrompt()` - восстановление системного промпта к значению по умолчанию
- `handleRestoreVerbosePrompt()` - восстановление verbose промпта
- `handleSaveLang()` - сохранение промптов при переключении языка
### prompts_helpers.go
Вспомогательные функции для работы с промптами:
- `getVerbosePromptsFromFile()` - получение verbose промптов из файла
- `translateVerbosePrompt()` - перевод verbose промпта
- `getVerbosePrompts()` - получение встроенных verbose промптов (fallback)
- `getSystemPromptsWithLang()` - получение системных промптов с учетом языка
- `translateSystemPrompt()` - перевод системного промпта
## Использование
```go
import "github.com/direct-dev-ru/linux-command-gpt/serve"
// Запуск сервера на localhost:8080
err := serve.StartResultServer("localhost", "8080")
```
## Маршруты
### Результаты
- `GET /` - главная страница со списком файлов
- `GET /file/{filename}` - просмотр файла результата
- `DELETE /delete/{filename}` - удаление файла
### История
- `GET /history` - страница истории запросов
- `GET /history/view/{id}` - просмотр записи истории в развернутом виде
- `DELETE /history/delete/{id}` - удаление записи
- `DELETE /history/clear` - очистка всей истории
### Промпты
- `GET /prompts` - страница управления промптами
- `POST /prompts/add` - добавление промпта
- `PUT /prompts/edit/{id}` - редактирование промпта
- `DELETE /prompts/delete/{id}` - удаление промпта
- `POST /prompts/restore/{id}` - восстановление системного промпта
- `POST /prompts/restore-verbose/{mode}` - восстановление verbose промпта (v/vv/vvv)
- `POST /prompts/save-lang` - сохранение языка промптов
### Выполнение запросов
- `GET /run` - веб-страница для выполнения запросов
- `POST /run` - обработка выполнения запроса
- `POST /execute` - API для программного доступа (только curl)
## Особенности
- **Многоязычность**: Поддержка английского и русского языков для промптов
- **Responsive дизайн**: Адаптивный интерфейс для различных устройств
- **Markdown**: Автоматическая конвертация Markdown файлов в HTML
- **История**: Поиск дубликатов с учетом регистра
- **Промпты**: Управление встроенными и пользовательскими промптами

205
serve/api.go Normal file
View File

@@ -0,0 +1,205 @@
package serve
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
// SaveResultRequest представляет запрос на сохранение результата
type SaveResultRequest struct {
Prompt string `json:"prompt"`
Command string `json:"command"`
Explanation string `json:"explanation,omitempty"`
Model string `json:"model"`
}
// SaveResultResponse представляет ответ на сохранение результата
type SaveResultResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
File string `json:"file,omitempty"`
Error string `json:"error,omitempty"`
}
// AddToHistoryRequest представляет запрос на добавление в историю
type AddToHistoryRequest struct {
Prompt string `json:"prompt"`
Command string `json:"command"`
Response string `json:"response"`
Explanation string `json:"explanation,omitempty"`
System string `json:"system"`
}
// AddToHistoryResponse представляет ответ на добавление в историю
type AddToHistoryResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Error string `json:"error,omitempty"`
}
// handleSaveResult обрабатывает сохранение результата
func handleSaveResult(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req SaveResultRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Prompt == "" || req.Command == "" {
http.Error(w, "Prompt and command are required", http.StatusBadRequest)
return
}
// Создаем папку результатов если не существует
if err := os.MkdirAll(config.AppConfig.ResultFolder, 0755); err != nil {
apiJsonResponse(w, SaveResultResponse{
Success: false,
Error: "Failed to create result folder",
})
return
}
// Генерируем имя файла
timestamp := time.Now().Format("2006-01-02_15-04-05")
filename := fmt.Sprintf("gpt_request_%s_%s.md", req.Model, timestamp)
filePath := path.Join(config.AppConfig.ResultFolder, filename)
title := truncateTitle(req.Prompt)
// Формируем содержимое
var content string
if strings.TrimSpace(req.Explanation) != "" {
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n\n## Explanation\n\n%s\n",
title, req.Prompt, req.Command, req.Explanation)
} else {
content = fmt.Sprintf("# %s\n\n## Prompt\n\n%s\n\n## Response\n\n%s\n",
title, req.Prompt, req.Command)
}
// Сохраняем файл
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
apiJsonResponse(w, SaveResultResponse{
Success: false,
Error: "Failed to save file",
})
return
}
// Debug вывод для сохранения результата
PrintWebSaveDebugInfo("SAVE_RESULT", req.Prompt, req.Command, req.Explanation, req.Model, filename)
apiJsonResponse(w, SaveResultResponse{
Success: true,
Message: "Result saved successfully",
File: filename,
})
}
// handleAddToHistory обрабатывает добавление в историю
func handleAddToHistory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req AddToHistoryRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.Prompt == "" || req.Command == "" || req.Response == "" {
http.Error(w, "Prompt, command and response are required", http.StatusBadRequest)
return
}
// Проверяем, есть ли уже такой запрос в истории
entries, err := Read(config.AppConfig.ResultHistory)
if err != nil {
// Если файл не существует, создаем пустой массив
entries = []HistoryEntry{}
}
// Ищем дубликат
duplicateIndex := -1
for i, entry := range entries {
if strings.EqualFold(strings.TrimSpace(entry.Command), strings.TrimSpace(req.Prompt)) {
duplicateIndex = i
break
}
}
// Создаем новую запись
newEntry := HistoryEntry{
Index: len(entries) + 1,
Command: req.Prompt,
Response: req.Response,
Explanation: req.Explanation,
System: req.System,
Timestamp: time.Now(),
}
if duplicateIndex == -1 {
// Добавляем новую запись
entries = append(entries, newEntry)
} else {
// Перезаписываем существующую
newEntry.Index = entries[duplicateIndex].Index
entries[duplicateIndex] = newEntry
}
// Сохраняем историю
if err := Write(config.AppConfig.ResultHistory, entries); err != nil {
apiJsonResponse(w, AddToHistoryResponse{
Success: false,
Error: "Failed to save to history",
})
return
}
message := "Added to history successfully"
if duplicateIndex != -1 {
message = "Updated existing history entry"
}
// Debug вывод для добавления в историю
PrintWebSaveDebugInfo("ADD_TO_HISTORY", req.Prompt, req.Command, req.Explanation, req.System, "")
apiJsonResponse(w, AddToHistoryResponse{
Success: true,
Message: message,
})
}
// apiJsonResponse отправляет JSON ответ
func apiJsonResponse(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
}
}
// truncateTitle сокращает строку до 120 символов (по рунам), добавляя " ..." при усечении
func truncateTitle(s string) string {
const maxLen = 120
if runeCount := len([]rune(s)); runeCount <= maxLen {
return s
}
const head = 116
r := []rune(s)
if len(r) <= head {
return s
}
return string(r[:head]) + " ..."
}

56
serve/debug.go Normal file
View File

@@ -0,0 +1,56 @@
package serve
import (
"fmt"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
// PrintWebDebugInfo выводит отладочную информацию для веб-запросов
func PrintWebDebugInfo(operation, prompt, systemPrompt, model string, timeout int) {
if !config.AppConfig.MainFlags.Debug {
return
}
fmt.Printf("\n🔍 DEBUG WEB %s:\n", operation)
fmt.Printf("💬 Запрос: %s\n", prompt)
fmt.Printf("🤖 Системный промпт: %s\n", systemPrompt)
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
fmt.Printf("🧠 Модель: %s\n", model)
fmt.Printf("📝 История: %t\n", !config.AppConfig.MainFlags.NoHistory)
fmt.Printf("────────────────────────────────────────\n")
}
// PrintWebVerboseDebugInfo выводит отладочную информацию для verbose запросов
func PrintWebVerboseDebugInfo(operation, prompt, verbosePrompt, model string, level int, timeout int) {
if !config.AppConfig.MainFlags.Debug {
return
}
fmt.Printf("\n🔍 DEBUG WEB %s (v%d):\n", operation, level)
fmt.Printf("💬 Запрос: %s\n", prompt)
fmt.Printf("📝 Системный промпт подробности:\n%s\n", verbosePrompt)
fmt.Printf("⏱️ Таймаут: %d сек\n", timeout)
fmt.Printf("🌐 Провайдер: %s\n", config.AppConfig.ProviderType)
fmt.Printf("🏠 Хост: %s\n", config.AppConfig.Host)
fmt.Printf("🧠 Модель: %s\n", model)
fmt.Printf("🎯 Уровень подробности: %d\n", level)
fmt.Printf("────────────────────────────────────────\n")
}
// PrintWebSaveDebugInfo выводит отладочную информацию для сохранения
func PrintWebSaveDebugInfo(operation, prompt, command, explanation, model, file string) {
if !config.AppConfig.MainFlags.Debug {
return
}
fmt.Printf("\n🔍 DEBUG WEB %s:\n", operation)
fmt.Printf("💬 Запрос: %s\n", prompt)
fmt.Printf("⚡ Команда: %s\n", command)
fmt.Printf("📖 Объяснение: %s\n", explanation)
fmt.Printf("🧠 Модель: %s\n", model)
fmt.Printf("📁 Файл: %s\n", file)
fmt.Printf("────────────────────────────────────────\n")
}

178
serve/execute.go Normal file
View File

@@ -0,0 +1,178 @@
package serve
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/gpt"
)
// ExecuteRequest представляет запрос на выполнение
type ExecuteRequest struct {
Prompt string `json:"prompt"` // Пользовательский промпт
SystemID int `json:"system_id"` // ID системного промпта (1-5)
SystemText string `json:"system"` // Текст системного промпта (альтернатива system_id)
Verbose string `json:"verbose"` // Степень подробности: "v", "vv", "vvv" или пустая строка
Timeout int `json:"timeout"` // Таймаут в секундах (опционально)
}
// ExecuteResponse представляет ответ
type ExecuteResponse struct {
Success bool `json:"success"`
Command string `json:"command,omitempty"`
Explanation string `json:"explanation,omitempty"`
Error string `json:"error,omitempty"`
Model string `json:"model,omitempty"`
Elapsed float64 `json:"elapsed,omitempty"`
}
// handleExecute обрабатывает POST запросы на выполнение
func handleExecute(w http.ResponseWriter, r *http.Request) {
// Проверяем User-Agent - только curl
userAgent := r.Header.Get("User-Agent")
if !strings.Contains(strings.ToLower(userAgent), "curl") {
http.Error(w, "Only curl requests are allowed", http.StatusForbidden)
return
}
// Проверяем метод
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Парсим JSON
var req ExecuteRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Валидация обязательных полей
if req.Prompt == "" {
http.Error(w, "Prompt is required", http.StatusBadRequest)
return
}
// Определяем системный промпт
systemPrompt := ""
if req.SystemText != "" {
systemPrompt = req.SystemText
} else if req.SystemID > 0 && req.SystemID <= 5 {
// Получаем системный промпт по ID
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
prompt, err := pm.GetPromptByID(req.SystemID)
if err != nil {
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
return
}
systemPrompt = prompt.Content
} else {
// Используем промпт по умолчанию
systemPrompt = config.AppConfig.Prompt
}
// Устанавливаем таймаут
timeout := req.Timeout
if timeout <= 0 {
timeout = 120 // По умолчанию 2 минуты
}
// Создаем GPT клиент
gpt3 := gpt.NewGpt3(
config.AppConfig.ProviderType,
config.AppConfig.Host,
config.AppConfig.JwtToken,
config.AppConfig.Model,
systemPrompt,
0.01,
timeout,
)
// Выполняем запрос
response, elapsed := getCommand(*gpt3, req.Prompt)
if response == "" {
jsonResponse(w, ExecuteResponse{
Success: false,
Error: "Failed to get response from AI",
})
return
}
// Если запрошено подробное объяснение
if req.Verbose != "" {
explanation, err := getDetailedExplanation(req.Prompt, req.Verbose, timeout)
if err != nil {
jsonResponse(w, ExecuteResponse{
Success: false,
Error: fmt.Sprintf("Failed to get explanation: %v", err),
})
return
}
jsonResponse(w, ExecuteResponse{
Success: true,
Command: response,
Explanation: explanation,
Model: config.AppConfig.Model,
Elapsed: elapsed,
})
} else {
jsonResponse(w, ExecuteResponse{
Success: true,
Command: response,
Model: config.AppConfig.Model,
Elapsed: elapsed,
})
}
}
// getCommand выполняет запрос к AI
func getCommand(gpt3 gpt.Gpt3, prompt string) (string, float64) {
gpt3.InitKey()
start := time.Now()
response := gpt3.Completions(prompt)
elapsed := time.Since(start).Seconds()
return response, elapsed
}
// getDetailedExplanation получает подробное объяснение
func getDetailedExplanation(prompt, verbose string, timeout int) (string, error) {
level := len(verbose) // 1, 2, 3
// Получаем системный промпт для подробного объяснения
detailedSystem := gpt.GetVerbosePromptByLevel(level)
if detailedSystem == "" {
return "", fmt.Errorf("invalid verbose level: %s", verbose)
}
// Создаем GPT клиент для объяснения
explanationGpt := gpt.NewGpt3(
config.AppConfig.ProviderType,
config.AppConfig.Host,
config.AppConfig.JwtToken,
config.AppConfig.Model,
detailedSystem,
0.2,
timeout,
)
explanationGpt.InitKey()
explanation := explanationGpt.Completions(prompt)
if explanation == "" {
return "", fmt.Errorf("failed to get explanation")
}
return explanation, nil
}
// jsonResponse отправляет JSON ответ
func jsonResponse(w http.ResponseWriter, response ExecuteResponse) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}

293
serve/execute_page.go Normal file
View File

@@ -0,0 +1,293 @@
package serve
import (
"fmt"
"html/template"
"net/http"
"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/russross/blackfriday/v2"
)
// ExecutePageData содержит данные для страницы выполнения
type ExecutePageData struct {
Title string
Header string
CurrentPrompt string
SystemOptions []SystemPromptOption
ResultSection template.HTML
VerboseButtons template.HTML
ActionButtons template.HTML
}
// SystemPromptOption представляет опцию системного промпта
type SystemPromptOption struct {
ID int
Name string
Description string
}
// ExecuteResultData содержит результат выполнения
type ExecuteResultData struct {
Success bool
Command string
Explanation string
Error string
Model string
Elapsed float64
Verbose string
}
// handleExecutePage обрабатывает страницу выполнения
func handleExecutePage(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// Показываем форму
showExecuteForm(w)
case http.MethodPost:
// Обрабатываем выполнение
handleExecuteRequest(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
// showExecuteForm показывает форму выполнения
func showExecuteForm(w http.ResponseWriter) {
// Получаем системные промпты
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
var systemOptions []SystemPromptOption
for i := 1; i <= 5; i++ {
prompt, err := pm.GetPromptByID(i)
if err == nil {
systemOptions = append(systemOptions, SystemPromptOption{
ID: prompt.ID,
Name: prompt.Name,
Description: prompt.Description,
})
}
}
data := ExecutePageData{
Title: "Выполнение запроса",
Header: "Выполнение запроса",
CurrentPrompt: "",
SystemOptions: systemOptions,
ResultSection: template.HTML(""),
VerboseButtons: template.HTML(""),
ActionButtons: template.HTML(""),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.ExecutePageTemplate.Execute(w, data)
}
// handleExecuteRequest обрабатывает запрос на выполнение
func handleExecuteRequest(w http.ResponseWriter, r *http.Request) {
// Парсим форму
prompt := r.FormValue("prompt")
systemIDStr := r.FormValue("system_id")
verbose := r.FormValue("verbose")
// Получаем системные промпты
pm := gpt.NewPromptManager(config.AppConfig.PromptFolder)
if prompt == "" {
http.Error(w, "Prompt is required", http.StatusBadRequest)
return
}
systemID := 1
if systemIDStr != "" {
if id, err := strconv.Atoi(systemIDStr); err == nil && id >= 1 && id <= 5 {
systemID = id
}
}
// Получаем системный промпт
systemPrompt, err := pm.GetPromptByID(systemID)
if err != nil {
http.Error(w, "Failed to get system prompt", http.StatusInternalServerError)
return
}
// Создаем GPT клиент
gpt3 := gpt.NewGpt3(
config.AppConfig.ProviderType,
config.AppConfig.Host,
config.AppConfig.JwtToken,
config.AppConfig.Model,
systemPrompt.Content,
0.01,
120,
)
// Debug вывод для основного запроса
PrintWebDebugInfo("EXECUTE", prompt, systemPrompt.Content, config.AppConfig.Model, 120)
// Выполняем запрос
response, elapsed := getCommand(*gpt3, prompt)
var result ExecuteResultData
if response == "" {
result = ExecuteResultData{
Success: false,
Error: "Failed to get response from AI",
}
} else {
result = ExecuteResultData{
Success: true,
Command: response,
Model: config.AppConfig.Model,
Elapsed: elapsed,
}
}
// Если запрошено подробное объяснение
if verbose != "" {
level := len(verbose)
verbosePrompt := gpt.GetVerbosePromptByLevel(level)
// Debug вывод для verbose запроса
PrintWebVerboseDebugInfo("VERBOSE", prompt, verbosePrompt, config.AppConfig.Model, level, 120)
explanation, err := getDetailedExplanation(prompt, verbose, 120)
if err == nil {
// Конвертируем Markdown в HTML
explanationHTML := blackfriday.Run([]byte(explanation))
result.Explanation = string(explanationHTML)
result.Verbose = verbose
}
}
// Получаем системные промпты для dropdown
var systemOptions []SystemPromptOption
for i := 1; i <= 5; i++ {
prompt, err := pm.GetPromptByID(i)
if err == nil {
systemOptions = append(systemOptions, SystemPromptOption{
ID: prompt.ID,
Name: prompt.Name,
Description: prompt.Description,
})
}
}
data := ExecutePageData{
Title: "Результат выполнения",
Header: "Результат выполнения",
CurrentPrompt: prompt,
SystemOptions: systemOptions,
ResultSection: template.HTML(formatResultSection(result)),
VerboseButtons: template.HTML(formatVerboseButtons(result)),
ActionButtons: template.HTML(formatActionButtons(result)),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
templates.ExecutePageTemplate.Execute(w, data)
}
// formatResultSection форматирует секцию результата
func formatResultSection(result ExecuteResultData) string {
if !result.Success {
return fmt.Sprintf(`
<div class="result-section">
<div class="error-message">
<h3>❌ Ошибка</h3>
<p>%s</p>
</div>
</div>`, result.Error)
}
explanationSection := ""
if result.Explanation != "" {
explanationSection = fmt.Sprintf(`
<div class="explanation-section">
<h3>📖 Подробное объяснение (%s):</h3>
<div class="explanation-content">%s</div>
</div>
<script>
// Показываем кнопку "Наверх" после загрузки объяснения
showScrollToTopButton();
</script>`, result.Verbose, result.Explanation)
}
// Определяем, содержит ли результат Markdown/многострочный текст
useMarkdown := false
if strings.Contains(result.Command, "```") || strings.Contains(result.Command, "\n") || strings.Contains(result.Command, "#") || strings.Contains(result.Command, "*") || strings.Contains(result.Command, "_") {
useMarkdown = true
}
commandBlock := ""
if useMarkdown {
// Рендерим Markdown в HTML
cmdHTML := blackfriday.Run([]byte(result.Command))
commandBlock = fmt.Sprintf(`<div class="command-md">%s</div>`, string(cmdHTML))
} else {
// Оставляем как простой однострочный вывод команды
commandBlock = fmt.Sprintf(`<div class="command-code">%s</div>`, result.Command)
}
return fmt.Sprintf(`
<div class="result-section">
<div class="command-result">
<h3>✅ Команда:</h3>
%s
<div class="result-meta">
<span>Модель: %s</span>
<span>Время: %.2f сек</span>
</div>
</div>
%s
</div>
<script>
// Сохраняем результаты в скрытое поле
(function() {
const resultData = {
command: %s,
explanation: %s,
model: %s
};
const resultDataField = document.getElementById('resultData');
if (resultDataField) {
resultDataField.value = JSON.stringify(resultData);
}
})();
</script>`,
commandBlock, result.Model, result.Elapsed, explanationSection,
fmt.Sprintf(`"%s"`, strings.ReplaceAll(result.Command, `"`, `\"`)),
fmt.Sprintf(`"%s"`, strings.ReplaceAll(result.Explanation, `"`, `\"`)),
fmt.Sprintf(`"%s"`, result.Model))
}
// formatVerboseButtons форматирует кнопки подробности
func formatVerboseButtons(result ExecuteResultData) string {
if !result.Success || result.Explanation != "" {
return "" // Скрываем кнопки если есть ошибка или уже есть объяснение
}
return `
<div class="verbose-buttons">
<button onclick="requestExplanation('v')" class="verbose-btn v-btn">v - Краткое объяснение</button>
<button onclick="requestExplanation('vv')" class="verbose-btn vv-btn">vv - Подробное объяснение</button>
<button onclick="requestExplanation('vvv')" class="verbose-btn vvv-btn">vvv - Максимально подробное</button>
</div>`
}
// formatActionButtons форматирует кнопки действий
func formatActionButtons(result ExecuteResultData) string {
if !result.Success {
return "" // Скрываем кнопки если есть ошибка
}
return `
<div class="action-buttons">
<button onclick="saveResult()" class="action-btn">💾 Сохранить результат</button>
<button onclick="addToHistory()" class="action-btn">📝 Добавить в историю</button>
</div>`
}

168
serve/history.go Normal file
View File

@@ -0,0 +1,168 @@
package serve
import (
"fmt"
"html/template"
"net/http"
"os"
"strconv"
"strings"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
"github.com/russross/blackfriday/v2"
)
// HistoryEntryInfo содержит информацию о записи истории для отображения
type HistoryEntryInfo struct {
Index int
Command string
Response string
Timestamp string
}
// handleHistoryPage обрабатывает страницу истории запросов
func handleHistoryPage(w http.ResponseWriter, r *http.Request) {
historyEntries, err := readHistoryEntries()
if err != nil {
http.Error(w, fmt.Sprintf("Ошибка чтения истории: %v", err), http.StatusInternalServerError)
return
}
tmpl := templates.HistoryPageTemplate
t, err := template.New("history").Parse(tmpl)
if err != nil {
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
return
}
data := struct {
Entries []HistoryEntryInfo
}{
Entries: historyEntries,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
t.Execute(w, data)
}
// readHistoryEntries читает записи истории
func readHistoryEntries() ([]HistoryEntryInfo, error) {
entries, err := Read(config.AppConfig.ResultHistory)
if err != nil {
return nil, err
}
var result []HistoryEntryInfo
for _, entry := range entries {
result = append(result, HistoryEntryInfo{
Index: entry.Index,
Command: entry.Command,
Response: entry.Response,
Timestamp: entry.Timestamp.Format("02.01.2006 15:04:05"),
})
}
return result, nil
}
// handleDeleteHistoryEntry обрабатывает удаление записи истории
func handleDeleteHistoryEntry(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
indexStr := strings.TrimPrefix(r.URL.Path, "/history/delete/")
index, err := strconv.Atoi(indexStr)
if err != nil {
http.Error(w, "Invalid index", http.StatusBadRequest)
return
}
err = DeleteHistoryEntry(config.AppConfig.ResultHistory, index)
if err != nil {
http.Error(w, fmt.Sprintf("Ошибка удаления: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Запись успешно удалена"))
}
// handleClearHistory обрабатывает очистку всей истории
func handleClearHistory(w http.ResponseWriter, r *http.Request) {
if r.Method != "DELETE" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
err := os.WriteFile(config.AppConfig.ResultHistory, []byte("[]"), 0644)
if err != nil {
http.Error(w, fmt.Sprintf("Ошибка очистки: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("История успешно очищена"))
}
// handleHistoryView обрабатывает просмотр записи истории
func handleHistoryView(w http.ResponseWriter, r *http.Request) {
// Получаем индекс из URL
indexStr := strings.TrimPrefix(r.URL.Path, "/history/view/")
index, err := strconv.Atoi(indexStr)
if err != nil {
http.NotFound(w, r)
return
}
// Читаем записи истории
entries, err := Read(config.AppConfig.ResultHistory)
if err != nil {
http.Error(w, fmt.Sprintf("Ошибка чтения истории: %v", err), http.StatusInternalServerError)
return
}
// Ищем запись с нужным индексом
var targetEntry *HistoryEntry
for _, entry := range entries {
if entry.Index == index {
targetEntry = &entry
break
}
}
if targetEntry == nil {
http.NotFound(w, r)
return
}
// Формируем объяснение, если оно есть
explanationSection := ""
if strings.TrimSpace(targetEntry.Explanation) != "" {
// Конвертируем Markdown в HTML
explanationHTML := blackfriday.Run([]byte(targetEntry.Explanation))
explanationSection = fmt.Sprintf(`
<div class="history-explanation">
<h3>📖 Подробное объяснение:</h3>
<div class="history-explanation-content">%s</div>
</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
)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(htmlPage))
}

67
serve/history_utils.go Normal file
View File

@@ -0,0 +1,67 @@
package serve
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
// HistoryEntry представляет запись в истории
type HistoryEntry struct {
Index int `json:"index"`
Command string `json:"command"`
Response string `json:"response"`
Explanation string `json:"explanation,omitempty"`
System string `json:"system_prompt"`
Timestamp time.Time `json:"timestamp"`
}
// read читает записи истории из файла
func Read(historyPath string) ([]HistoryEntry, error) {
data, err := os.ReadFile(historyPath)
if err != nil || len(data) == 0 {
return nil, err
}
var items []HistoryEntry
if err := json.Unmarshal(data, &items); err != nil {
return nil, err
}
return items, nil
}
// write записывает записи истории в файл
func Write(historyPath string, entries []HistoryEntry) error {
for i := range entries {
entries[i].Index = i + 1
}
out, err := json.MarshalIndent(entries, "", " ")
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(historyPath), 0755); err != nil {
return err
}
return os.WriteFile(historyPath, out, 0644)
}
// DeleteHistoryEntry удаляет запись из истории по индексу
func DeleteHistoryEntry(historyPath string, id int) error {
items, err := Read(historyPath)
if err != nil || len(items) == 0 {
return fmt.Errorf("история пуста или недоступна")
}
pos := -1
for i := range items {
if items[i].Index == id {
pos = i
break
}
}
if pos == -1 {
return fmt.Errorf("запись не найдена")
}
items = append(items[:pos], items[pos+1:]...)
return Write(historyPath, items)
}

421
serve/prompts.go Normal file
View File

@@ -0,0 +1,421 @@
package serve
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"os"
"strconv"
"strings"
"github.com/direct-dev-ru/linux-command-gpt/gpt"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
)
// VerbosePrompt структура для промптов подробности
type VerbosePrompt struct {
Mode string
Name string
Description string
Content string
IsDefault bool
}
// handlePromptsPage обрабатывает страницу управления промптами
func handlePromptsPage(w http.ResponseWriter, r *http.Request) {
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
pm := gpt.NewPromptManager(homeDir)
// Получаем язык из параметра запроса, если не указан - берем из файла
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = pm.GetCurrentLanguage()
}
tmpl := templates.PromptsPageTemplate
t, err := template.New("prompts").Parse(tmpl)
if err != nil {
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
return
}
// Создаем структуру с дополнительным полем IsDefault
type PromptWithDefault struct {
gpt.SystemPrompt
IsDefault bool
}
// Получаем текущий язык из файла
currentLang := pm.GetCurrentLanguage()
// Если язык не указан в URL, используем язык из файла
if lang == "" {
lang = currentLang
}
// Получаем системные промпты с учетом языка
systemPrompts := getSystemPromptsWithLang(pm.Prompts, lang)
var promptsWithDefault []PromptWithDefault
for _, prompt := range systemPrompts {
// Показываем только системные промпты (ID 1-5) на первой вкладке
if prompt.ID >= 1 && prompt.ID <= 5 {
// Проверяем, является ли промпт встроенным и неизмененным
isDefault := gpt.IsBuiltinPrompt(prompt)
promptsWithDefault = append(promptsWithDefault, PromptWithDefault{
SystemPrompt: prompt,
IsDefault: isDefault,
})
}
}
// Получаем промпты подробности из файла sys_prompts
verbosePrompts := getVerbosePromptsFromFile(pm.Prompts, lang)
data := struct {
Prompts []PromptWithDefault
VerbosePrompts []VerbosePrompt
Lang string
}{
Prompts: promptsWithDefault,
VerbosePrompts: verbosePrompts,
Lang: lang,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
t.Execute(w, data)
}
// handleAddPrompt обрабатывает добавление нового промпта
func handleAddPrompt(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
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 := pm.AddPrompt(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("Промпт успешно добавлен"))
}
// handleEditPrompt обрабатывает редактирование промпта
func handleEditPrompt(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Получаем ID из URL
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/edit/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Неверный ID промпта", http.StatusBadRequest)
return
}
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
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 := 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" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Получаем ID из URL
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/delete/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Неверный ID промпта", http.StatusBadRequest)
return
}
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов (использует конфигурацию из config.AppConfig.PromptFolder)
pm := gpt.NewPromptManager(homeDir)
// Удаляем промпт
if err := pm.DeletePrompt(id); err != nil {
http.Error(w, fmt.Sprintf("Ошибка удаления промпта: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Промпт успешно удален"))
}
// handleRestorePrompt восстанавливает системный промпт к значению по умолчанию
func handleRestorePrompt(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Получаем ID из URL
idStr := strings.TrimPrefix(r.URL.Path, "/prompts/restore/")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid prompt ID", http.StatusBadRequest)
return
}
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов
pm := gpt.NewPromptManager(homeDir)
// Получаем текущий язык
currentLang := pm.GetCurrentLanguage()
// Получаем встроенный промпт для текущего языка
builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(id, currentLang)
if builtinPrompt == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": "Промпт не найден в встроенных",
})
return
}
// Обновляем промпт в списке
for i, prompt := range pm.Prompts {
if prompt.ID == id {
pm.Prompts[i] = *builtinPrompt
break
}
}
// Сохраняем изменения
if err := pm.SaveAllPrompts(); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": "Ошибка сохранения: " + err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
})
}
// handleRestoreVerbosePrompt восстанавливает verbose промпт к значению по умолчанию
func handleRestoreVerbosePrompt(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Получаем режим из URL
mode := strings.TrimPrefix(r.URL.Path, "/prompts/restore-verbose/")
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов
pm := gpt.NewPromptManager(homeDir)
// Получаем текущий язык
currentLang := pm.GetCurrentLanguage()
// Определяем ID по режиму
var id int
switch mode {
case "v":
id = 6
case "vv":
id = 7
case "vvv":
id = 8
default:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": "Неверный режим промпта",
})
return
}
// Получаем встроенный промпт для текущего языка
builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(id, currentLang)
if builtinPrompt == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": "Промпт не найден в встроенных",
})
return
}
// Обновляем промпт в списке
for i, prompt := range pm.Prompts {
if prompt.ID == id {
pm.Prompts[i] = *builtinPrompt
break
}
}
// Сохраняем изменения
if err := pm.SaveAllPrompts(); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]interface{}{
"success": false,
"error": "Ошибка сохранения: " + err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"success": true,
})
}
// handleSaveLang обрабатывает сохранение промптов при переключении языка
func handleSaveLang(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Получаем домашнюю директорию пользователя
homeDir, err := os.UserHomeDir()
if err != nil {
http.Error(w, "Ошибка получения домашней директории", http.StatusInternalServerError)
return
}
// Создаем менеджер промптов
pm := gpt.NewPromptManager(homeDir)
// Парсим JSON данные
var langData struct {
Lang string `json:"lang"`
}
if err := json.NewDecoder(r.Body).Decode(&langData); err != nil {
http.Error(w, "Ошибка парсинга JSON", http.StatusBadRequest)
return
}
// Устанавливаем язык файла
pm.SetLanguage(langData.Lang)
// Переводим только встроенные промпты (по ID), а пользовательские оставляем как есть
var translatedPrompts []gpt.SystemPrompt
for _, p := range pm.Prompts {
// Проверяем, является ли промпт встроенным по ID (1-8)
if pm.IsDefaultPromptByID(p) {
// System (1-5) и Verbose (6-8)
if p.ID >= 1 && p.ID <= 5 {
translatedPrompts = append(translatedPrompts, translateSystemPrompt(p, langData.Lang))
} else if p.ID >= 6 && p.ID <= 8 {
translatedPrompts = append(translatedPrompts, translateVerbosePrompt(p, langData.Lang))
} else {
translatedPrompts = append(translatedPrompts, p)
}
} else {
// Пользовательские промпты (ID > 8) не трогаем
translatedPrompts = append(translatedPrompts, p)
}
}
// Обновляем в pm и сохраняем
pm.Prompts = translatedPrompts
if err := pm.SaveAllPrompts(); err != nil {
http.Error(w, fmt.Sprintf("Ошибка сохранения: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Промпты сохранены"))
}

147
serve/prompts_helpers.go Normal file
View File

@@ -0,0 +1,147 @@
package serve
import (
"github.com/direct-dev-ru/linux-command-gpt/gpt"
)
// getVerbosePromptsFromFile возвращает промпты подробности из файла sys_prompts
func getVerbosePromptsFromFile(prompts []gpt.SystemPrompt, lang string) []VerbosePrompt {
var verbosePrompts []VerbosePrompt
// Ищем промпты подробности в загруженных промптах (ID 6, 7, 8)
for _, prompt := range prompts {
if prompt.ID >= 6 && prompt.ID <= 8 {
// Определяем режим по ID
var mode string
switch prompt.ID {
case 6:
mode = "v"
case 7:
mode = "vv"
case 8:
mode = "vvv"
}
// Переводим на нужный язык если необходимо
translatedPrompt := translateVerbosePrompt(prompt, lang)
verbosePrompts = append(verbosePrompts, VerbosePrompt{
Mode: mode,
Name: translatedPrompt.Name,
Description: translatedPrompt.Description,
Content: translatedPrompt.Content,
IsDefault: gpt.IsBuiltinPrompt(translatedPrompt), // Проверяем, является ли промпт встроенным
})
}
}
// Если промпты подробности не найдены в файле, используем встроенные
if len(verbosePrompts) == 0 {
return getVerbosePrompts(lang)
}
return verbosePrompts
}
// translateVerbosePrompt переводит промпт подробности на указанный язык
func translateVerbosePrompt(prompt gpt.SystemPrompt, lang string) gpt.SystemPrompt {
// Получаем встроенный промпт для указанного языка из YAML
if builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(prompt.ID, lang); builtinPrompt != nil {
return *builtinPrompt
}
// Если перевод не найден, возвращаем оригинал
return prompt
}
// getVerbosePrompts возвращает промпты для режимов v/vv/vvv (fallback)
func getVerbosePrompts(lang string) []VerbosePrompt {
// Английские версии (по умолчанию)
enPrompts := []VerbosePrompt{
{
Mode: "v",
Name: "Verbose Mode",
Description: "Detailed explanation of the command",
Content: "Provide a brief explanation of what this Linux command does, including what each flag and option means, and give examples of usage.",
IsDefault: true,
},
{
Mode: "vv",
Name: "Very Verbose Mode",
Description: "Comprehensive explanation with alternatives",
Content: "Provide a comprehensive explanation of this Linux command, including detailed descriptions of all flags and options, alternative approaches, common use cases, and potential pitfalls to avoid.",
IsDefault: true,
},
{
Mode: "vvv",
Name: "Maximum Verbose Mode",
Description: "Complete guide with examples and best practices",
Content: "Provide a complete guide for this Linux command, including detailed explanations of all options, multiple examples with different scenarios, alternative commands that achieve similar results, best practices, troubleshooting tips, and related commands that work well together.",
IsDefault: true,
},
}
// Русские версии
ruPrompts := []VerbosePrompt{
{
Mode: "v",
Name: "Подробный режим",
Description: "Подробное объяснение команды",
Content: "Предоставь краткое объяснение того, что делает эта Linux команда, включая значение каждого флага и опции, и приведи примеры использования.",
IsDefault: true,
},
{
Mode: "vv",
Name: "Очень подробный режим",
Description: "Исчерпывающее объяснение с альтернативами",
Content: "Предоставь исчерпывающее объяснение этой Linux команды, включая подробные описания всех флагов и опций, альтернативные подходы, распространенные случаи использования и потенциальные подводные камни, которых следует избегать.",
IsDefault: true,
},
{
Mode: "vvv",
Name: "Максимально подробный режим",
Description: "Полное руководство с примерами и лучшими практиками",
Content: "Предоставь полное руководство по этой Linux команде, включая подробные объяснения всех опций, множественные примеры с различными сценариями, альтернативные команды, которые дают аналогичные результаты, лучшие практики, советы по устранению неполадок и связанные команды, которые хорошо работают вместе.",
IsDefault: true,
},
}
if lang == "ru" {
return ruPrompts
}
return enPrompts
}
// getSystemPromptsWithLang возвращает системные промпты с учетом языка
func getSystemPromptsWithLang(prompts []gpt.SystemPrompt, lang string) []gpt.SystemPrompt {
// Если язык английский, возвращаем оригинальные промпты
if lang == "en" {
return prompts
}
// Для русского языка переводим только встроенные промпты
var translatedPrompts []gpt.SystemPrompt
for _, prompt := range prompts {
// Проверяем, является ли это встроенным промптом
if gpt.IsBuiltinPrompt(prompt) {
// Переводим встроенные промпты на русский
translated := translateSystemPrompt(prompt, lang)
translatedPrompts = append(translatedPrompts, translated)
} else {
translatedPrompts = append(translatedPrompts, prompt)
}
}
return translatedPrompts
}
// translateSystemPrompt переводит системный промпт на указанный язык
func translateSystemPrompt(prompt gpt.SystemPrompt, lang string) gpt.SystemPrompt {
// Получаем встроенный промпт для указанного языка из YAML
if builtinPrompt := gpt.GetBuiltinPromptByIDAndLanguage(prompt.ID, lang); builtinPrompt != nil {
return *builtinPrompt
}
// Если перевод не найден, возвращаем оригинал
return prompt
}

239
serve/results.go Normal file
View File

@@ -0,0 +1,239 @@
package serve
import (
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/direct-dev-ru/linux-command-gpt/config"
"github.com/direct-dev-ru/linux-command-gpt/serve/templates"
"github.com/russross/blackfriday/v2"
)
// FileInfo содержит информацию о файле
type FileInfo struct {
Name string
Size string
ModTime string
Preview string
Content string // Полное содержимое для поиска
}
// handleResultsPage обрабатывает главную страницу со списком файлов
func handleResultsPage(w http.ResponseWriter, r *http.Request) {
files, err := getResultFiles()
if err != nil {
http.Error(w, fmt.Sprintf("Ошибка чтения папки: %v", err), http.StatusInternalServerError)
return
}
tmpl := templates.ResultsPageTemplate
t, err := template.New("results").Parse(tmpl)
if err != nil {
http.Error(w, "Ошибка шаблона", http.StatusInternalServerError)
return
}
// Подсчитываем статистику
recentCount := 0
weekAgo := time.Now().AddDate(0, 0, -7)
for _, file := range files {
// Парсим время из строки для сравнения
if modTime, err := time.Parse("02.01.2006 15:04", file.ModTime); err == nil {
if modTime.After(weekAgo) {
recentCount++
}
}
}
data := struct {
Files []FileInfo
TotalFiles int
RecentFiles int
}{
Files: files,
TotalFiles: len(files),
RecentFiles: recentCount,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
t.Execute(w, data)
}
// getResultFiles возвращает список файлов из папки результатов
func getResultFiles() ([]FileInfo, error) {
entries, err := os.ReadDir(config.AppConfig.ResultFolder)
if err != nil {
return nil, err
}
var files []FileInfo
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
// Читаем превью файла (первые 200 символов) и конвертируем Markdown
preview := ""
fullContent := ""
if content, err := os.ReadFile(filepath.Join(config.AppConfig.ResultFolder, entry.Name())); err == nil {
// Сохраняем полное содержимое для поиска
fullContent = string(content)
// Конвертируем Markdown в HTML для превью
htmlContent := blackfriday.Run(content)
preview = strings.TrimSpace(string(htmlContent))
// Удаляем HTML теги для превью
preview = strings.ReplaceAll(preview, "<h1>", "")
preview = strings.ReplaceAll(preview, "</h1>", "")
preview = strings.ReplaceAll(preview, "<h2>", "")
preview = strings.ReplaceAll(preview, "</h2>", "")
preview = strings.ReplaceAll(preview, "<h3>", "")
preview = strings.ReplaceAll(preview, "</h3>", "")
preview = strings.ReplaceAll(preview, "<p>", "")
preview = strings.ReplaceAll(preview, "</p>", "")
preview = strings.ReplaceAll(preview, "<code>", "")
preview = strings.ReplaceAll(preview, "</code>", "")
preview = strings.ReplaceAll(preview, "<pre>", "")
preview = strings.ReplaceAll(preview, "</pre>", "")
preview = strings.ReplaceAll(preview, "<strong>", "")
preview = strings.ReplaceAll(preview, "</strong>", "")
preview = strings.ReplaceAll(preview, "<em>", "")
preview = strings.ReplaceAll(preview, "</em>", "")
preview = strings.ReplaceAll(preview, "<ul>", "")
preview = strings.ReplaceAll(preview, "</ul>", "")
preview = strings.ReplaceAll(preview, "<li>", "• ")
preview = strings.ReplaceAll(preview, "</li>", "")
preview = strings.ReplaceAll(preview, "<ol>", "")
preview = strings.ReplaceAll(preview, "</ol>", "")
preview = strings.ReplaceAll(preview, "<blockquote>", "")
preview = strings.ReplaceAll(preview, "</blockquote>", "")
preview = strings.ReplaceAll(preview, "<br>", "")
preview = strings.ReplaceAll(preview, "<br/>", "")
preview = strings.ReplaceAll(preview, "<br />", "")
// Очищаем от лишних пробелов и переносов
preview = strings.ReplaceAll(preview, "\n", " ")
preview = strings.ReplaceAll(preview, "\r", "")
preview = strings.ReplaceAll(preview, " ", " ")
preview = strings.TrimSpace(preview)
if len(preview) > 200 {
preview = preview[:200] + "..."
}
}
files = append(files, FileInfo{
Name: entry.Name(),
Size: formatFileSize(info.Size()),
ModTime: info.ModTime().Format("02.01.2006 15:04"),
Preview: preview,
Content: fullContent,
})
}
// Сортируем по времени изменения (новые сверху)
for i := 0; i < len(files)-1; i++ {
for j := i + 1; j < len(files); j++ {
if files[i].ModTime < files[j].ModTime {
files[i], files[j] = files[j], files[i]
}
}
}
return files, nil
}
// formatFileSize форматирует размер файла в читаемый вид
func formatFileSize(size int64) string {
const unit = 1024
if size < unit {
return fmt.Sprintf("%d B", size)
}
div, exp := int64(unit), 0
for n := size / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
}
// handleFileView обрабатывает просмотр конкретного файла
func handleFileView(w http.ResponseWriter, r *http.Request) {
filename := strings.TrimPrefix(r.URL.Path, "/file/")
if filename == "" {
http.NotFound(w, r)
return
}
// Проверяем, что файл существует и находится в папке результатов
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
http.NotFound(w, r)
return
}
content, err := os.ReadFile(filePath)
if err != nil {
http.NotFound(w, r)
return
}
// Конвертируем Markdown в HTML
htmlContent := blackfriday.Run(content)
// Создаем HTML страницу с красивым отображением
htmlPage := fmt.Sprintf(templates.FileViewTemplate, filename, filename, string(htmlContent))
// Устанавливаем заголовки для отображения HTML
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write([]byte(htmlPage))
}
// handleDeleteFile обрабатывает удаление файла
func handleDeleteFile(w http.ResponseWriter, r *http.Request) {
// Проверяем метод запроса
if r.Method != "DELETE" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
filename := strings.TrimPrefix(r.URL.Path, "/delete/")
if filename == "" {
http.NotFound(w, r)
return
}
// Проверяем, что файл существует и находится в папке результатов
filePath := filepath.Join(config.AppConfig.ResultFolder, filename)
if !strings.HasPrefix(filePath, config.AppConfig.ResultFolder) {
http.NotFound(w, r)
return
}
// Проверяем, что файл существует
if _, err := os.Stat(filePath); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
// Удаляем файл
err := os.Remove(filePath)
if err != nil {
http.Error(w, fmt.Sprintf("Ошибка удаления файла: %v", err), http.StatusInternalServerError)
return
}
// Возвращаем успешный ответ
w.WriteHeader(http.StatusOK)
w.Write([]byte("Файл успешно удален"))
}

59
serve/serve.go Normal file
View File

@@ -0,0 +1,59 @@
package serve
import (
"fmt"
"net/http"
"github.com/direct-dev-ru/linux-command-gpt/config"
)
// StartResultServer запускает HTTP сервер для просмотра сохраненных результатов
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")
} else {
fmt.Printf("🔍 DEBUG РЕЖИМ ОТКЛЮЧЕН - веб-операции не будут логироваться\n")
}
return http.ListenAndServe(addr, nil)
}
// registerRoutes регистрирует все маршруты сервера
func registerRoutes() {
// Главная страница и файлы
http.HandleFunc("/", handleResultsPage)
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/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)
}

View File

@@ -0,0 +1,600 @@
package templates
import "html/template"
// ExecutePageCSSTemplate - CSS стили для страницы выполнения запросов
var ExecutePageCSSTemplate = template.Must(template.New("execute_css").Parse(`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Динамичный плавный градиент (современный стиль) */
background: linear-gradient(135deg, #5b86e5, #36d1dc, #4a7c59, #764ba2);
background-size: 400% 400%;
animation: gradientShift 18s ease infinite;
min-height: 100vh;
padding: 20px;
}
/* Анимация плавного перелива фона */
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Учитываем системные настройки доступности */
@media (prefers-reduced-motion: reduce) {
body { animation: none; }
}
/* Улучшения для touch-устройств */
.nav-btn, .submit-btn, .reset-btn, .verbose-btn, .action-btn {
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
/* Оптимизация производительности */
.container {
will-change: transform;
}
/* Улучшение читаемости на мобильных */
@media (max-width: 768px) {
.command-result code {
font-size: 0.9em;
padding: 1px 4px;
}
.command-result pre {
font-size: 14px;
padding: 12px;
}
.explanation-content {
font-size: 15px;
line-height: 1.6;
}
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.content {
padding: 30px;
}
.nav-buttons {
display: flex;
gap: 10px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.nav-btn {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
text-decoration: none;
transition: background 0.3s ease;
display: inline-block;
}
.nav-btn:hover {
background: #2980b9;
}
.form-section {
background: #f8f9fa;
padding: 25px;
border-radius: 8px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2d5016;
}
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s ease;
}
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #2d5016;
box-shadow: 0 0 0 3px rgba(45, 80, 22, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 120px;
}
.submit-btn {
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(45, 80, 22, 0.3);
}
.submit-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.form-buttons {
display: flex;
gap: 15px;
margin-top: 20px;
}
.reset-btn {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
color: white;
border: none;
padding: 15px 30px;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
flex: 1;
}
.reset-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(231, 76, 60, 0.3);
}
.reset-btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.result-section {
margin-top: 30px;
}
.command-result {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2d5016;
margin-bottom: 20px;
}
.command-result h3 {
color: #2d5016;
margin-bottom: 15px;
}
/* Заголовки внутри результата команды */
.command-result h1,
.command-result h2,
.command-result h3,
.command-result h4,
.command-result h5,
.command-result h6 {
margin-top: 18px; /* отделяем сверху */
margin-bottom: 10px;/* и немного снизу */
line-height: 1.25;
}
/* Ритм текста внутри markdown-блока команды */
.command-result .command-md { line-height: 1.7; }
.command-result p { margin: 10px 0 14px; line-height: 1.7; }
.command-result ul,
.command-result ol { margin: 10px 0 14px 24px; line-height: 1.7; }
.command-result li { margin: 6px 0; }
.command-result hr { margin: 18px 0; border: 0; border-top: 1px solid #e1e5e9; }
/* Подсветка code внутри результата команды */
.command-result code {
background: #e6f4ea; /* светло-зеленый фон */
color: #2e7d32; /* зеленый текст */
border: 1px solid #b7dfb9;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.98em; /* немного крупнее базового */
}
.command-result pre {
background: #eaf7ee; /* мягкий зеленоватый фон */
border-left: 4px solid #2e7d32; /* зеленая полоса слева */
padding: 14px;
border-radius: 8px;
overflow-x: auto;
margin: 12px 0 16px; /* вертикальные отступы вокруг кода */
}
.command-result pre code {
background: none;
border: none;
color: #2e7d32;
font-size: 16px; /* увеличить размер шрифта в блоках */
}
.command-code {
background: #2d5016;
color: white;
padding: 15px;
border-radius: 6px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 16px;
margin-bottom: 15px;
word-break: break-all;
}
.result-meta {
display: flex;
gap: 20px;
color: #6c757d;
font-size: 14px;
}
.explanation-section {
background: #f0f8f0;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #4a7c59;
margin-top: 20px;
}
.explanation-section h3 {
color: #2d5016;
margin-bottom: 15px;
}
.explanation-content {
color: #333;
line-height: 1.6;
}
.explanation-content h1,
.explanation-content h2,
.explanation-content h3,
.explanation-content h4,
.explanation-content h5,
.explanation-content h6 {
color: #2d5016;
margin-top: 20px;
margin-bottom: 10px;
}
.explanation-content h1 {
border-bottom: 2px solid #2d5016;
padding-bottom: 5px;
}
.explanation-content h2 {
border-bottom: 1px solid #4a7c59;
padding-bottom: 3px;
}
.explanation-content code {
background: #f0f8f0;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
color: #2d5016;
border: 1px solid #a8e6cf;
}
.explanation-content pre {
background: #f0f8f0;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #2d5016;
overflow-x: auto;
}
.explanation-content pre code {
background: none;
padding: 0;
border: none;
color: #2d5016;
}
.explanation-content blockquote {
border-left: 4px solid #4a7c59;
margin: 15px 0;
padding: 10px 20px;
background: #f0f8f0;
border-radius: 0 8px 8px 0;
}
.explanation-content ul,
.explanation-content ol {
padding-left: 20px;
}
.explanation-content li {
margin: 5px 0;
}
.explanation-content strong {
color: #2d5016;
}
.explanation-content em {
color: #4a7c59;
}
.verbose-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
}
.verbose-btn {
background: #f8f9fa;
border: 1px solid #e9ecef;
color: #495057;
padding: 10px 15px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.v-btn {
background: #e3f2fd;
border: 1px solid #bbdefb;
color: #1976d2;
}
.v-btn:hover {
background: #bbdefb;
border-color: #90caf9;
}
.v-btn:disabled {
background: #f5f5f5;
border-color: #e0e0e0;
color: #9e9e9e;
cursor: not-allowed;
}
.vv-btn {
background: #e1f5fe;
border: 1px solid #b3e5fc;
color: #0277bd;
}
.vv-btn:hover {
background: #b3e5fc;
border-color: #81d4fa;
}
.vv-btn:disabled {
background: #f5f5f5;
border-color: #e0e0e0;
color: #9e9e9e;
cursor: not-allowed;
}
.vvv-btn {
background: #e8eaf6;
border: 1px solid #c5cae9;
color: #3f51b5;
}
.vvv-btn:hover {
background: #c5cae9;
border-color: #9fa8da;
}
.vvv-btn:disabled {
background: #f5f5f5;
border-color: #e0e0e0;
color: #9e9e9e;
cursor: not-allowed;
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
justify-content: center;
flex-wrap: wrap;
}
.action-btn {
background: #e8f5e8;
border: 1px solid #c8e6c9;
color: #2e7d32;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
text-decoration: none;
display: inline-block;
}
.action-btn:hover {
background: #c8e6c9;
border-color: #a5d6a7;
color: #1b5e20;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #dc3545;
}
.error-message h3 {
color: #721c24;
margin-bottom: 10px;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.loading.show {
display: block;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #2d5016;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
.verbose-loading {
display: none;
text-align: center;
padding: 10px;
margin-top: 10px;
}
.verbose-loading.show {
display: block;
}
.verbose-spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #1976d2;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
margin: 0 auto 5px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.scroll-to-top {
position: fixed;
bottom: 20px;
right: 20px;
background: #3498db;
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 20px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
z-index: 1000;
}
.scroll-to-top:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.3);
}
/* Мобильная оптимизация */
@media (max-width: 768px) {
body {
padding: 10px;
}
.container {
margin: 0;
border-radius: 8px;
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
}
.header {
padding: 20px;
}
.header h1 {
font-size: 2em;
}
.content {
padding: 20px;
}
.nav-buttons {
flex-direction: column;
gap: 8px;
}
.nav-btn {
width: 100%;
text-align: center;
padding: 12px 16px;
}
.form-buttons {
flex-direction: column;
gap: 10px;
}
.submit-btn, .reset-btn {
width: 100%;
padding: 16px 20px;
font-size: 16px;
}
.verbose-buttons {
flex-direction: column;
gap: 8px;
}
.verbose-btn {
width: 100%;
padding: 12px 16px;
font-size: 14px;
}
.action-buttons {
flex-direction: column;
gap: 8px;
}
.action-btn {
width: 100%;
padding: 12px 16px;
font-size: 14px;
}
.command-result {
padding: 15px;
margin-bottom: 15px;
}
.command-code {
font-size: 14px;
padding: 12px;
word-break: break-word;
}
.explanation-section {
padding: 15px;
}
.result-meta {
flex-direction: column;
gap: 8px;
font-size: 12px;
}
.scroll-to-top {
bottom: 15px;
right: 15px;
width: 45px;
height: 45px;
font-size: 18px;
}
}
/* Очень маленькие экраны */
@media (max-width: 480px) {
.header h1 {
font-size: 1.8em;
}
.header p {
font-size: 1em;
}
.content {
padding: 15px;
}
.form-group textarea {
min-height: 100px;
font-size: 16px; /* Предотвращает зум на iOS */
}
.form-group select {
font-size: 16px; /* Предотвращает зум на iOS */
}
.command-result h3 {
font-size: 1.2em;
}
.explanation-content h1,
.explanation-content h2,
.explanation-content h3 {
font-size: 1.3em;
}
}
`))

View File

@@ -0,0 +1,88 @@
package templates
import "html/template"
// ExecutePageTemplate - шаблон страницы выполнения запросов
var ExecutePageTemplate = template.Must(template.New("execute").Parse(`<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - Linux Command GPT</title>
<style>
{{template "execute_css" .}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>{{.Header}}</h1>
<p>Выполнение запросов к Linux Command GPT через веб-интерфейс</p>
</div>
<div class="content">
<div class="nav-buttons">
<a href="/" class="nav-btn">🏠 Главная</a>
<a href="/history" class="nav-btn">📝 История</a>
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
</div>
<form method="POST" id="executeForm">
<div class="form-section">
<div class="form-group">
<label for="system_id">🤖 Системный промпт:</label>
<select name="system_id" id="system_id" required>
{{range .SystemOptions}}
<option value="{{.ID}}">{{.ID}}. {{.Name}}</option>
{{end}}
</select>
</div>
<div class="form-group">
<label for="prompt">💬 Ваш запрос:</label>
<textarea name="prompt" id="prompt" placeholder="Опишите, что вы хотите сделать..." required>{{.CurrentPrompt}}</textarea>
</div>
<!-- Скрытое поле для хранения результатов -->
<input type="hidden" id="resultData" name="resultData" value="">
<div class="form-buttons">
<button type="submit" class="submit-btn" id="submitBtn">
🚀 Выполнить запрос
</button>
<button type="button" class="reset-btn" id="resetBtn" onclick="resetForm()">
🔄 Сброс
</button>
</div>
</div>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Обрабатываю запрос...</p>
</div>
{{.ResultSection}}
{{.VerboseButtons}}
<div class="verbose-loading" id="verboseLoading">
<div class="verbose-spinner"></div>
<p>Получаю подробное объяснение...</p>
</div>
{{.ActionButtons}}
</div>
</div>
<!-- Кнопка "Наверх" -->
<button class="scroll-to-top" id="scrollToTop" onclick="scrollToTop()" style="display: none;">↑</button>
{{template "execute_scripts" .}}
</body>
</html>`))
// Объединяем шаблоны
func init() {
template.Must(ExecutePageTemplate.AddParseTree("execute_css", ExecutePageCSSTemplate.Tree))
template.Must(ExecutePageTemplate.AddParseTree("execute_scripts", ExecutePageScriptsTemplate.Tree))
}

View File

@@ -0,0 +1,284 @@
package templates
import "html/template"
var ExecutePageScriptsTemplate = template.Must(template.New("execute_scripts").Parse(`
<script>
// Обработка отправки формы (блокируем все кнопки кроме навигации)
document.getElementById('executeForm').addEventListener('submit', function(e) {
// Предотвращаем множественные отправки на мобильных устройствах
if (this.dataset.submitting === 'true') {
e.preventDefault();
return false;
}
this.dataset.submitting = 'true';
const submitBtn = document.getElementById('submitBtn');
const loading = document.getElementById('loading');
const actionButtons = document.querySelectorAll('.action-btn');
const verboseButtons = document.querySelectorAll('.verbose-btn');
const scrollBtn = document.getElementById('scrollToTop');
submitBtn.disabled = true;
submitBtn.textContent = '⏳ Выполняется...';
loading.classList.add('show');
// Блокируем кнопки действий (сохранение/история)
actionButtons.forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.5';
});
// Блокируем кнопки подробностей (v/vv/vvv)
verboseButtons.forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.5';
});
// Прячем кнопку "Наверх" до получения нового ответа
if (scrollBtn) {
scrollBtn.style.display = 'none';
}
});
// Запрос подробного объяснения
function requestExplanation(verbose) {
const form = document.getElementById('executeForm');
const prompt = document.getElementById('prompt').value;
const systemId = document.getElementById('system_id').value;
const verboseLoading = document.getElementById('verboseLoading');
const verboseButtons = document.querySelectorAll('.verbose-btn');
const actionButtons = document.querySelectorAll('.action-btn');
if (!prompt.trim()) {
alert('Сначала выполните основной запрос');
return;
}
// Показываем лоадер и блокируем ВСЕ кнопки кроме навигации
verboseLoading.classList.add('show');
verboseButtons.forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.5';
});
actionButtons.forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.5';
});
// Создаем скрытое поле для verbose
const verboseInput = document.createElement('input');
verboseInput.type = 'hidden';
verboseInput.name = 'verbose';
verboseInput.value = verbose;
form.appendChild(verboseInput);
// Отправляем форму
form.submit();
}
// Сохранение результата
function saveResult() {
const resultDataField = document.getElementById('resultData');
const prompt = document.getElementById('prompt').value;
if (!resultDataField.value || !prompt.trim()) {
alert('Нет данных для сохранения');
return;
}
try {
const resultData = JSON.parse(resultDataField.value);
const requestData = {
prompt: prompt,
command: resultData.command,
explanation: resultData.explanation || '',
model: resultData.model || 'Unknown'
};
fetch('/api/save-result', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ Результат сохранен: ' + data.file);
} else {
alert('❌ Ошибка сохранения: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Ошибка при сохранении результата');
});
} catch (error) {
console.error('Error parsing result data:', error);
alert('❌ Ошибка при чтении данных результата');
}
}
// Добавление в историю
function addToHistory() {
const resultDataField = document.getElementById('resultData');
const prompt = document.getElementById('prompt').value;
const systemId = document.getElementById('system_id').value;
if (!resultDataField.value || !prompt.trim()) {
alert('Нет данных для сохранения в историю');
return;
}
try {
const resultData = JSON.parse(resultDataField.value);
const systemName = document.querySelector('option[value="' + systemId + '"]')?.textContent || 'Unknown';
const requestData = {
prompt: prompt,
command: resultData.command,
response: resultData.command,
explanation: resultData.explanation || '',
system: systemName
};
fetch('/api/add-to-history', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
} else {
alert('❌ Ошибка: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Ошибка при добавлении в историю');
});
} catch (error) {
console.error('Error parsing result data:', error);
alert('❌ Ошибка при чтении данных результата');
}
}
// Функция прокрутки наверх
function scrollToTop() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
}
// Показываем кнопку "Наверх" при появлении объяснения
function showScrollToTopButton() {
const scrollBtn = document.getElementById('scrollToTop');
if (scrollBtn) {
scrollBtn.style.display = 'block';
}
}
// Скрываем кнопку "Наверх" при загрузке страницы
function hideScrollToTopButton() {
const scrollBtn = document.getElementById('scrollToTop');
if (scrollBtn) {
scrollBtn.style.display = 'none';
}
}
// Сброс формы к начальному состоянию
function resetForm() {
// Очищаем поля формы
document.getElementById('prompt').value = '';
document.getElementById('resultData').value = '';
// Сбрасываем флаг отправки формы
const form = document.getElementById('executeForm');
if (form) {
form.dataset.submitting = 'false';
}
// Скрываем все секции результатов
const resultSection = document.querySelector('.result-section');
const verboseButtons = document.querySelector('.verbose-buttons');
const actionButtons = document.querySelector('.action-buttons');
const explanationSection = document.querySelector('.explanation-section');
const loading = document.getElementById('loading');
const verboseLoading = document.getElementById('verboseLoading');
const scrollBtn = document.getElementById('scrollToTop');
if (resultSection) resultSection.style.display = 'none';
if (verboseButtons) verboseButtons.style.display = 'none';
if (actionButtons) actionButtons.style.display = 'none';
if (explanationSection) explanationSection.style.display = 'none';
if (loading) loading.classList.remove('show');
if (verboseLoading) verboseLoading.classList.remove('show');
if (scrollBtn) scrollBtn.style.display = 'none';
// Разблокируем кнопки
const submitBtn = document.getElementById('submitBtn');
const resetBtn = document.getElementById('resetBtn');
const allButtons = document.querySelectorAll('.action-btn, .verbose-btn');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = '🚀 Выполнить запрос';
}
if (resetBtn) resetBtn.disabled = false;
allButtons.forEach(btn => {
btn.disabled = false;
btn.style.opacity = '1';
});
// Прокручиваем к началу формы (с проверкой поддержки smooth)
const formSection = document.querySelector('.form-section');
if (formSection) {
if ('scrollBehavior' in document.documentElement.style) {
formSection.scrollIntoView({ behavior: 'smooth' });
} else {
formSection.scrollIntoView();
}
}
}
// Сохранение результатов в скрытое поле
function saveResultToHiddenField() {
const resultDataField = document.getElementById('resultData');
const commandElement = document.querySelector('.command-code, .command-md');
const explanationElement = document.querySelector('.explanation-content');
const modelElement = document.querySelector('.result-meta span:first-child');
if (commandElement) {
const command = commandElement.textContent.trim();
const explanation = explanationElement ? explanationElement.innerHTML.trim() : '';
const model = modelElement ? modelElement.textContent.replace('Модель: ', '') : 'Unknown';
const resultData = {
command: command,
explanation: explanation,
model: model
};
resultDataField.value = JSON.stringify(resultData);
}
}
// Показываем кнопку при появлении объяснения и сохраняем результаты
document.addEventListener('DOMContentLoaded', function() {
const explanationSection = document.querySelector('.explanation-section');
if (explanationSection) {
showScrollToTopButton();
}
// Сохраняем результаты в скрытое поле при загрузке страницы
saveResultToHiddenField();
});
</script>
`))

136
serve/templates/file.go Normal file
View File

@@ -0,0 +1,136 @@
package templates
// FileViewTemplate шаблон для просмотра файла результата
const FileViewTemplate = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%s - 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%%);
min-height: 100vh;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
margin: 0;
font-size: 1.5em;
font-weight: 300;
}
.back-btn {
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
transition: background 0.3s ease;
}
.back-btn:hover {
background: rgba(255,255,255,0.3);
}
.content {
padding: 30px;
line-height: 1.6;
}
.content h1 {
color: #2d5016;
border-bottom: 2px solid #2d5016;
padding-bottom: 10px;
}
.content h2 {
color: #4a7c59;
margin-top: 30px;
}
.content h3 {
color: #2d5016;
}
.content code {
background: #f0f8f0;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
color: #2d5016;
border: 1px solid #a8e6cf;
}
.content pre {
background: #f0f8f0;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #2d5016;
overflow-x: auto;
}
.content pre code {
background: none;
padding: 0;
border: none;
color: #2d5016;
}
.content blockquote {
border-left: 4px solid #4a7c59;
margin: 20px 0;
padding: 10px 20px;
background: #f0f8f0;
border-radius: 0 8px 8px 0;
}
.content ul, .content ol {
padding-left: 20px;
}
.content li {
margin: 5px 0;
}
.content strong {
color: #2d5016;
}
.content em {
color: #4a7c59;
}
/* Мобильная адаптация */
@media (max-width: 768px) {
body { padding: 10px; }
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
.header { padding: 16px; }
.header h1 { font-size: 1.2em; }
.back-btn { padding: 6px 12px; font-size: 0.9em; }
.content { padding: 20px; }
.content pre { font-size: 14px; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1em; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📄 %s</h1>
<a href="/" class="back-btn">← Назад к списку</a>
</div>
<div class="content">
%s
</div>
</div>
</body>
</html>`

309
serve/templates/history.go Normal file
View File

@@ -0,0 +1,309 @@
package templates
// HistoryPageTemplate шаблон страницы истории запросов
const HistoryPageTemplate = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>История запросов - 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%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.content {
padding: 30px;
}
.nav-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.nav-btn {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
text-decoration: none;
transition: background 0.3s ease;
display: inline-block;
text-align: center;
}
.nav-btn:hover {
background: #2980b9;
}
.clear-btn {
background: #e74c3c;
}
.clear-btn:hover {
background: #c0392b;
}
.history-item {
background: #f0f8f0;
border: 1px solid #a8e6cf;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
position: relative;
cursor: pointer;
transition: all 0.3s ease;
}
.history-item:hover {
border-color: #2d5016;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(45,80,22,0.2);
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.history-index {
background: #2d5016;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
}
.history-timestamp {
color: #666;
font-size: 0.9em;
}
.history-command {
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.history-response {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9em;
color: #2d5016;
border-left: 3px solid #2d5016;
}
.delete-btn {
background: #e74c3c;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
transition: background 0.3s ease;
}
.delete-btn:hover {
background: #c0392b;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
/* Мобильная адаптация */
@media (max-width: 768px) {
body { padding: 10px; }
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
.header { padding: 20px; }
.header h1 { font-size: 2em; }
.content { padding: 20px; }
.nav-buttons { flex-direction: column; gap: 8px; }
.nav-btn { text-align: center; padding: 12px 16px; font-size: 14px; }
.history-header { flex-direction: column; align-items: flex-start; gap: 8px; }
.history-item { padding: 15px; }
.history-response { font-size: 0.85em; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1.8em; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📝 История запросов</h1>
<p>Управление историей запросов Linux Command GPT</p>
</div>
<div class="content">
<div class="nav-buttons">
<a href="/" class="nav-btn">🏠 Главная</a>
<a href="/run" class="nav-btn">🚀 Выполнение</a>
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
<button class="nav-btn clear-btn" onclick="clearHistory()">🗑️ Очистить всю историю</button>
</div>
<!-- Поиск -->
<div class="search-container" style="margin: 20px 0;">
<input type="text" id="searchInput" placeholder="🔍 Поиск по командам, ответам и объяснениям..."
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px;">
<div id="searchResults" style="margin-top: 10px; color: #666; font-size: 14px;"></div>
</div>
{{if .Entries}}
{{range .Entries}}
<div class="history-item" onclick="viewHistoryEntry({{.Index}})">
<div class="history-header">
<div>
<span class="history-index">#{{.Index}}</span>
<span class="history-timestamp">{{.Timestamp}}</span>
</div>
<button class="delete-btn" onclick="event.stopPropagation(); deleteHistoryEntry({{.Index}})">🗑️ Удалить</button>
</div>
<div class="history-command">{{.Command}}</div>
<div class="history-response">{{.Response}}</div>
</div>
{{end}}
{{else}}
<div class="empty-state">
<h3>📝 История пуста</h3>
<p>Здесь будут отображаться запросы после использования команды lcg</p>
</div>
{{end}}
</div>
</div>
<script>
function viewHistoryEntry(index) {
window.location.href = '/history/view/' + index;
}
function deleteHistoryEntry(index) {
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
fetch('/history/delete/' + index, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('Ошибка при удалении записи');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении записи');
});
}
}
function clearHistory() {
if (confirm('Вы уверены, что хотите очистить всю историю?\\n\\nЭто действие нельзя отменить.')) {
fetch('/history/clear', {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('Ошибка при очистке истории');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при очистке истории');
});
}
}
// Поиск по истории
function performSearch() {
const searchTerm = document.getElementById('searchInput').value.trim();
const searchResults = document.getElementById('searchResults');
const historyItems = document.querySelectorAll('.history-item');
if (searchTerm === '') {
// Показать все записи
historyItems.forEach(item => {
item.style.display = 'block';
});
searchResults.textContent = '';
return;
}
let visibleCount = 0;
let totalCount = historyItems.length;
historyItems.forEach(item => {
const command = item.querySelector('.history-command').textContent.toLowerCase();
const response = item.querySelector('.history-response').textContent.toLowerCase();
// Объединяем команду и ответ для поиска
const searchContent = command + ' ' + response;
let matches = false;
// Проверяем, есть ли фраза в кавычках
if (searchTerm.startsWith("'") && searchTerm.endsWith("'")) {
// Поиск точной фразы
const phrase = searchTerm.slice(1, -1).toLowerCase();
matches = searchContent.includes(phrase);
} else {
// Поиск по отдельным словам
const words = searchTerm.toLowerCase().split(/\s+/);
matches = words.every(word => searchContent.includes(word));
}
if (matches) {
item.style.display = 'block';
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Обновляем информацию о результатах
if (visibleCount === 0) {
searchResults.textContent = '🔍 Ничего не найдено';
searchResults.style.color = '#e74c3c';
} else if (visibleCount === totalCount) {
searchResults.textContent = '';
} else {
searchResults.textContent = '🔍 Найдено: ' + visibleCount + ' из ' + totalCount + ' записей';
searchResults.style.color = '#27ae60';
}
}
// Обработчик ввода в поле поиска
document.getElementById('searchInput').addEventListener('input', performSearch);
// Обработчик Enter в поле поиска
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
</script>
</body>
</html>`

View File

@@ -0,0 +1,279 @@
package templates
// HistoryViewTemplate шаблон для просмотра записи истории
const HistoryViewTemplate = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Запись #%d - 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%%);
min-height: 100vh;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%%, #4a7c59 100%%);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
margin: 0;
font-size: 1.5em;
font-weight: 300;
}
.back-btn {
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
transition: background 0.3s ease;
}
.back-btn:hover {
background: rgba(255,255,255,0.3);
}
.content {
padding: 30px;
line-height: 1.6;
}
.history-meta {
background: #f0f8f0;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #2d5016;
}
.history-meta-item {
margin: 5px 0;
color: #666;
}
.history-meta-label {
font-weight: 600;
color: #2d5016;
}
.history-command {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #4a7c59;
}
.history-command h3 {
margin: 0 0 10px 0;
color: #2d5016;
}
.history-command-text {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 1.1em;
color: #333;
white-space: pre-wrap;
}
.history-response {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2d5016;
}
.history-response h3 {
margin: 0 0 15px 0;
color: #2d5016;
}
.history-response-content {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.95em;
color: #333;
white-space: pre-wrap;
line-height: 1.5;
}
.history-explanation {
background: #f0f8f0;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
border-left: 4px solid #4a7c59;
}
.history-explanation h3 {
margin: 0 0 15px 0;
color: #2d5016;
}
.history-explanation-content {
color: #333;
line-height: 1.6;
}
.history-explanation-content h1,
.history-explanation-content h2,
.history-explanation-content h3,
.history-explanation-content h4,
.history-explanation-content h5,
.history-explanation-content h6 {
color: #2d5016;
margin-top: 20px;
margin-bottom: 10px;
}
.history-explanation-content h1 {
border-bottom: 2px solid #2d5016;
padding-bottom: 5px;
}
.history-explanation-content h2 {
border-bottom: 1px solid #4a7c59;
padding-bottom: 3px;
}
.history-explanation-content code {
background: #f0f8f0;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
color: #2d5016;
border: 1px solid #a8e6cf;
}
.history-explanation-content pre {
background: #f0f8f0;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #2d5016;
overflow-x: auto;
}
.history-explanation-content pre code {
background: none;
padding: 0;
border: none;
color: #2d5016;
}
.history-explanation-content blockquote {
border-left: 4px solid #4a7c59;
margin: 15px 0;
padding: 10px 20px;
background: #f0f8f0;
border-radius: 0 8px 8px 0;
}
.history-explanation-content ul,
.history-explanation-content ol {
padding-left: 20px;
}
.history-explanation-content li {
margin: 5px 0;
}
.history-explanation-content strong {
color: #2d5016;
}
.history-explanation-content em {
color: #4a7c59;
}
.actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
.action-btn {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
transition: background 0.3s ease;
display: inline-block;
}
.action-btn:hover {
background: #2980b9;
}
.delete-btn {
background: #e74c3c;
}
.delete-btn:hover {
background: #c0392b;
}
/* Мобильная адаптация */
@media (max-width: 768px) {
body { padding: 10px; }
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
.header { padding: 16px; }
.header h1 { font-size: 1.2em; }
.back-btn { padding: 6px 12px; font-size: 0.9em; }
.content { padding: 20px; }
.actions { flex-direction: column; }
.action-btn { width: 100%; text-align: center; }
.history-response-content { font-size: 0.9em; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1em; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📝 Запись #%d</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
</div>
<div class="history-meta-item">
<span class="history-meta-label">🔢 Индекс:</span> #%d
</div>
</div>
<div class="history-command">
<h3>💬 Запрос пользователя:</h3>
<div class="history-command-text">%s</div>
</div>
<div class="history-response">
<h3>🤖 Ответ Модели:</h3>
<div class="history-response-content">%s</div>
</div>
%s
<div class="actions">
<a href="/history" class="action-btn">📝 К истории</a>
<button class="action-btn delete-btn" onclick="deleteHistoryEntry(%d)">🗑️ Удалить запись</button>
</div>
</div>
</div>
<script>
function deleteHistoryEntry(index) {
if (confirm('Вы уверены, что хотите удалить запись #' + index + '?')) {
fetch('/history/delete/' + index, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
window.location.href = '/history';
} else {
alert('Ошибка при удалении записи');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении записи');
});
}
}
</script>
</body>
</html>`

515
serve/templates/prompts.go Normal file
View File

@@ -0,0 +1,515 @@
package templates
// PromptsPageTemplate шаблон страницы управления промптами
const PromptsPageTemplate = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Системные промпты - 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%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.content {
padding: 30px;
}
.nav-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.nav-btn {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
text-decoration: none;
transition: background 0.3s ease;
display: inline-block;
text-align: center;
}
.nav-btn:hover {
background: #2980b9;
}
.add-btn {
background: #27ae60;
}
.add-btn:hover {
background: #229954;
}
.prompt-item {
background: #f0f8f0;
border: 1px solid #a8e6cf;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
position: relative;
}
.prompt-item:hover {
border-color: #2d5016;
}
.prompt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.prompt-id {
background: #2d5016;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
}
.prompt-name {
font-weight: 600;
color: #333;
font-size: 1.2em;
}
.prompt-description {
color: #666;
margin-bottom: 10px;
}
.prompt-content {
background: #f8f9fa;
padding: 15px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.9em;
color: #2d5016;
border-left: 3px solid #2d5016;
white-space: pre-wrap;
}
.prompt-actions {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 8px;
}
.action-btn {
background: #4a7c59;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
transition: background 0.3s ease;
}
.action-btn:hover {
background: #2d5016;
}
.delete-btn {
background: #e74c3c;
}
.delete-btn:hover {
background: #c0392b;
}
.restore-btn {
background: #3498db;
}
.restore-btn:hover {
background: #2980b9;
}
.default-badge {
background: #28a745;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7em;
margin-left: 8px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.lang-switcher {
display: flex;
gap: 5px;
margin-left: auto;
}
.lang-btn {
background: #6c757d;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background 0.3s ease;
}
.lang-btn:hover {
background: #5a6268;
}
.lang-btn.active {
background: #3498db;
}
.lang-btn.active:hover {
background: #2980b9;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #e9ecef;
}
.tab-btn {
background: #f8f9fa;
color: #6c757d;
border: none;
padding: 12px 20px;
border-radius: 6px 6px 0 0;
cursor: pointer;
font-size: 1em;
transition: all 0.3s ease;
border-bottom: 3px solid transparent;
}
.tab-btn:hover {
background: #e9ecef;
color: #495057;
}
.tab-btn.active {
background: #3498db;
color: white;
border-bottom-color: #2980b9;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Мобильная адаптация */
@media (max-width: 768px) {
body { padding: 10px; }
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
.header { padding: 20px; }
.header h1 { font-size: 2em; }
.content { padding: 20px; }
.nav-buttons { flex-direction: column; gap: 8px; }
.nav-btn { text-align: center; padding: 12px 16px; font-size: 14px; }
.lang-switcher { margin-left: 0; }
.tabs { flex-direction: column; gap: 8px; }
.tab-btn { text-align: center; }
.prompt-item { padding: 15px; }
.prompt-content { font-size: 0.85em; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1.8em; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>⚙️ Системные промпты</h1>
<p>Управление системными промптами Linux Command GPT</p>
</div>
<div class="content">
<div class="nav-buttons">
<a href="/" class="nav-btn">🏠 Главная</a>
<a href="/run" class="nav-btn">🚀 Выполнение</a>
<a href="/history" class="nav-btn">📝 История</a>
<button class="nav-btn add-btn" onclick="showAddForm()"> Добавить промпт</button>
<div class="lang-switcher">
<button class="lang-btn {{if eq .Lang "ru"}}active{{end}}" onclick="switchLang('ru')">🇷🇺 RU</button>
<button class="lang-btn {{if eq .Lang "en"}}active{{end}}" onclick="switchLang('en')">🇺🇸 EN</button>
</div>
</div>
<!-- Вкладки -->
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('system')">⚙️ Системные промпты</button>
<button class="tab-btn" onclick="switchTab('verbose')">📝 Промпты подробности (v/vv/vvv)</button>
</div>
<!-- Вкладка системных промптов -->
<div id="system-tab" class="tab-content active">
{{if .Prompts}}
{{range .Prompts}}
<div class="prompt-item">
<div class="prompt-actions">
<button class="action-btn" onclick="editPrompt({{.ID}}, '{{.Name}}', '{{.Description}}', '{{.Content}}')">✏️</button>
<button class="action-btn restore-btn" onclick="restorePrompt({{.ID}})" title="Восстановить к значению по умолчанию">🔄</button>
<button class="action-btn delete-btn" onclick="deletePrompt({{.ID}})">🗑️</button>
</div>
<div class="prompt-header">
<div>
<span class="prompt-id">#{{.ID}}</span>
<span class="prompt-name">{{.Name}}</span>
{{if .IsDefault}}<span class="default-badge">Встроенный</span>{{end}}
</div>
</div>
<div class="prompt-description">{{.Description}}</div>
<div class="prompt-content">{{.Content}}</div>
</div>
{{end}}
{{else}}
<div class="empty-state">
<h3>⚙️ Промпты не найдены</h3>
<p>Добавьте пользовательские промпты для настройки поведения системы</p>
</div>
{{end}}
</div>
<!-- Вкладка промптов подробности -->
<div id="verbose-tab" class="tab-content">
{{if .VerbosePrompts}}
{{range .VerbosePrompts}}
<div class="prompt-item">
<div class="prompt-actions">
<button class="action-btn" onclick="editVerbosePrompt('{{.Mode}}', '{{.Content}}')">✏️</button>
<button class="action-btn restore-btn" onclick="restoreVerbosePrompt('{{.Mode}}')" title="Восстановить к значению по умолчанию">🔄</button>
</div>
<div class="prompt-header">
<div>
<span class="prompt-id">#{{.Mode}}</span>
<span class="prompt-name">{{.Name}}</span>
{{if .IsDefault}}<span class="default-badge">Встроенный</span>{{end}}
</div>
</div>
<div class="prompt-description">{{.Description}}</div>
<div class="prompt-content">{{.Content}}</div>
</div>
{{end}}
{{else}}
<div class="empty-state">
<h3>📝 Промпты подробности</h3>
<p>Промпты для режимов v, vv, vvv</p>
</div>
{{end}}
</div>
</div>
</div>
<!-- Форма добавления/редактирования -->
<div id="promptForm" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000;">
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 30px; border-radius: 12px; max-width: 600px; width: 90%;">
<h3 id="formTitle">Добавить промпт</h3>
<form id="promptFormData">
<input type="hidden" id="promptId" name="id">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Название:</label>
<input type="text" id="promptName" name="name" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Описание:</label>
<input type="text" id="promptDescription" name="description" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 5px; font-weight: 600;">Содержание:</label>
<textarea id="promptContent" name="content" rows="6" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-family: monospace;" required></textarea>
</div>
<div style="text-align: right;">
<button type="button" onclick="hideForm()" style="background: #6c757d; color: white; border: none; padding: 8px 16px; border-radius: 4px; margin-right: 10px; cursor: pointer;">Отмена</button>
<button type="submit" style="background: #2d5016; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer;">Сохранить</button>
</div>
</form>
</div>
</div>
<script>
function showAddForm() {
document.getElementById('formTitle').textContent = 'Добавить промпт';
document.getElementById('promptFormData').reset();
document.getElementById('promptId').value = '';
document.getElementById('promptForm').style.display = 'block';
}
function editPrompt(id, name, description, content) {
document.getElementById('formTitle').textContent = 'Редактировать промпт';
document.getElementById('promptId').value = id;
document.getElementById('promptName').value = name;
document.getElementById('promptDescription').value = description;
document.getElementById('promptContent').value = content;
document.getElementById('promptForm').style.display = 'block';
}
function hideForm() {
document.getElementById('promptForm').style.display = 'none';
}
function switchTab(tabName) {
// Скрываем все вкладки
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// Убираем активный класс с кнопок
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
// Показываем нужную вкладку
document.getElementById(tabName + '-tab').classList.add('active');
// Активируем нужную кнопку
event.target.classList.add('active');
}
function switchLang(lang) {
// Сохраняем текущие промпты перед переключением языка
saveCurrentPrompts(lang);
// Перезагружаем страницу с новым языком
const url = new URL(window.location);
url.searchParams.set('lang', lang);
window.location.href = url.toString();
}
function saveCurrentPrompts(lang) {
// Отправляем запрос для сохранения текущих промптов с новым языком
fetch('/prompts/save-lang', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
lang: lang
})
})
.catch(error => {
console.error('Error saving prompts:', error);
});
}
function editVerbosePrompt(mode, content) {
// Редактирование промпта подробности
alert('Редактирование промптов подробности будет реализовано');
}
function deletePrompt(id) {
if (confirm('Вы уверены, что хотите удалить промпт #' + id + '?')) {
fetch('/prompts/delete/' + id, {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('Ошибка при удалении промпта');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении промпта');
});
}
}
document.getElementById('promptFormData').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const id = formData.get('id');
const url = id ? '/prompts/edit/' + id : '/prompts/add';
const method = id ? 'PUT' : 'POST';
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: formData.get('name'),
description: formData.get('description'),
content: formData.get('content')
})
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('Ошибка при сохранении промпта');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при сохранении промпта');
});
});
// Функция восстановления системного промпта
function restorePrompt(id) {
if (confirm('Восстановить промпт к значению по умолчанию?')) {
fetch('/prompts/restore/' + id, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Промпт восстановлен');
location.reload();
} else {
alert('Ошибка: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при восстановлении промпта');
});
}
}
// Функция восстановления verbose промпта
function restoreVerbosePrompt(mode) {
if (confirm('Восстановить промпт к значению по умолчанию?')) {
fetch('/prompts/restore-verbose/' + mode, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Промпт восстановлен');
location.reload();
} else {
alert('Ошибка: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при восстановлении промпта');
});
}
}
</script>
</body>
</html>`

329
serve/templates/results.go Normal file
View File

@@ -0,0 +1,329 @@
package templates
// ResultsPageTemplate шаблон главной страницы со списком файлов
const ResultsPageTemplate = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LCG Results - Linux Command GPT</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%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #2d5016 0%, #4a7c59 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 2.5em;
font-weight: 300;
}
.header p {
margin: 10px 0 0 0;
opacity: 0.9;
font-size: 1.1em;
}
.content {
padding: 30px;
}
.nav-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: #f0f8f0;
padding: 20px;
border-radius: 8px;
text-align: center;
border-left: 4px solid #2d5016;
}
.stat-number {
font-size: 2em;
font-weight: bold;
color: #2d5016;
}
.stat-label {
color: #666;
margin-top: 5px;
}
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.file-card {
background: white;
border: 1px solid #e1e5e9;
border-radius: 8px;
padding: 20px;
transition: all 0.3s ease;
position: relative;
}
.file-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(45,80,22,0.2);
border-color: #2d5016;
}
.file-card-content {
cursor: pointer;
}
.file-actions {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 8px;
}
.delete-btn {
background: #e74c3c;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
transition: background 0.3s ease;
}
.delete-btn:hover {
background: #c0392b;
}
.file-name {
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 1.1em;
}
.file-info {
color: #666;
font-size: 0.9em;
margin-bottom: 10px;
}
.file-preview {
background: #f0f8f0;
padding: 10px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.85em;
color: #2d5016;
max-height: 100px;
overflow: hidden;
border-left: 3px solid #2d5016;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #666;
}
.empty-state h3 {
color: #333;
margin-bottom: 10px;
}
.nav-btn, .nav-button {
background: #3498db;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
text-decoration: none;
transition: background 0.3s ease;
display: inline-block;
text-align: center;
}
.nav-btn:hover, .nav-button:hover {
background: #2980b9;
}
/* Мобильная адаптация */
@media (max-width: 768px) {
body { padding: 10px; }
.container { margin: 0; border-radius: 8px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); }
.header { padding: 20px; }
.header h1 { font-size: 2em; }
.content { padding: 20px; }
.files-grid { grid-template-columns: 1fr; }
.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; }
}
@media (max-width: 480px) {
.header h1 { font-size: 1.8em; }
.stats { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 LCG Results</h1>
<p>Просмотр сохраненных результатов Linux Command GPT</p>
</div>
<div class="content">
<div class="nav-buttons">
<button class="nav-btn" onclick="location.reload()">🔄 Обновить</button>
<a href="/run" class="nav-btn">🚀 Выполнение</a>
<a href="/history" class="nav-btn">📝 История</a>
<a href="/prompts" class="nav-btn">⚙️ Промпты</a>
</div>
<!-- Поиск -->
<div class="search-container">
<input type="text" id="searchInput" placeholder="🔍 Поиск по содержимому файлов..."
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px;">
<div id="searchResults" style="margin-top: 10px; color: #666; font-size: 14px;"></div>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number">{{.TotalFiles}}</div>
<div class="stat-label">Всего файлов</div>
</div>
<div class="stat-card">
<div class="stat-number">{{.RecentFiles}}</div>
<div class="stat-label">За последние 7 дней</div>
</div>
</div>
{{if .Files}}
<div class="files-grid">
{{range .Files}}
<div class="file-card" data-content="{{.Content}}">
<div class="file-actions">
<button class="delete-btn" onclick="deleteFile('{{.Name}}')" title="Удалить файл">🗑️</button>
</div>
<div class="file-card-content" onclick="window.location.href='/file/{{.Name}}'">
<div class="file-name">{{.Name}}</div>
<div class="file-info">
📅 {{.ModTime}} | 📏 {{.Size}}
</div>
<div class="file-preview">{{.Preview}}</div>
</div>
</div>
{{end}}
</div>
{{else}}
<div class="empty-state">
<h3>📁 Папка пуста</h3>
<p>Здесь будут отображаться сохраненные результаты после использования команды lcg</p>
</div>
{{end}}
</div>
</div>
<script>
function deleteFile(filename) {
if (confirm('Вы уверены, что хотите удалить файл "' + filename + '"?\\n\\nЭто действие нельзя отменить.')) {
fetch('/delete/' + encodeURIComponent(filename), {
method: 'DELETE'
})
.then(response => {
if (response.ok) {
location.reload();
} else {
alert('Ошибка при удалении файла');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении файла');
});
}
}
// Поиск по содержимому файлов
function performSearch() {
const searchTerm = document.getElementById('searchInput').value.trim();
const searchResults = document.getElementById('searchResults');
const fileCards = document.querySelectorAll('.file-card');
if (searchTerm === '') {
// Показать все файлы
fileCards.forEach(card => {
card.style.display = 'block';
});
searchResults.textContent = '';
return;
}
let visibleCount = 0;
let totalCount = fileCards.length;
fileCards.forEach(card => {
const fileName = card.querySelector('.file-name').textContent.toLowerCase();
const fullContent = card.getAttribute('data-content').toLowerCase();
// Проверяем поиск по полному содержимому файла
const fileContent = fileName + ' ' + fullContent;
let matches = false;
// Проверяем, есть ли фраза в кавычках
if (searchTerm.startsWith("'") && searchTerm.endsWith("'")) {
// Поиск точной фразы
const phrase = searchTerm.slice(1, -1).toLowerCase();
matches = fileContent.includes(phrase);
} else {
// Поиск по отдельным словам
const words = searchTerm.toLowerCase().split(/\s+/);
matches = words.every(word => fileContent.includes(word));
}
if (matches) {
card.style.display = 'block';
visibleCount++;
} else {
card.style.display = 'none';
}
});
// Обновляем информацию о результатах
if (visibleCount === 0) {
searchResults.textContent = '🔍 Ничего не найдено';
searchResults.style.color = '#e74c3c';
} else if (visibleCount === totalCount) {
searchResults.textContent = '';
} else {
searchResults.textContent = '🔍 Найдено: ' + visibleCount + ' из ' + totalCount + ' файлов';
searchResults.style.color = '#27ae60';
}
}
// Обработчик ввода в поле поиска
document.getElementById('searchInput').addEventListener('input', performSearch);
// Обработчик Enter в поле поиска
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
performSearch();
}
});
</script>
</body>
</html>`

10
shell-code/curl.sh Normal file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
execute_command() {
curl -s -X POST "http://localhost:8085/api/execute" \
-H "Content-Type: application/json" \
-d "{\"prompt\": \"$1\", \"verbose\": \"$2\"}" | \
jq -r '.'
}
execute_command "$1" "$2"

View File

@@ -2,4 +2,4 @@
go-ansible-vault -i shell-code/build.env -a get -m GITHUB_TOKEN > /tmp/source && source /tmp/source
#GITHUB_TOKEN=$GITHUB_TOKEN python3 shell-code/release.py
GITHUB_TOKEN=$GITHUB_TOKEN python3 shell-code/release.py

View File

@@ -11,6 +11,8 @@ import requests
from pathlib import Path
# Цвета для вывода
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
@@ -18,23 +20,29 @@ class Colors:
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
def log(message):
print(f"{Colors.GREEN}[INFO]{Colors.NC} {message}")
def error(message):
print(f"{Colors.RED}[ERROR]{Colors.NC} {message}", file=sys.stderr)
def warn(message):
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {message}")
def debug(message):
print(f"{Colors.BLUE}[DEBUG]{Colors.NC} {message}")
# Конфигурация
REPO = "direct-dev-ru/go-lcg"
VERSION_FILE = "VERSION.txt"
BINARIES_DIR = "binaries-for-upload"
def check_environment():
"""Проверка переменных окружения"""
token = os.getenv('GITHUB_TOKEN')
@@ -44,6 +52,7 @@ def check_environment():
log(f"GITHUB_TOKEN установлен (длина: {len(token)} символов)")
return token
def get_version():
"""Получение версии из файла"""
version_file = Path(VERSION_FILE)
@@ -57,6 +66,7 @@ def get_version():
log(f"Тег: {tag}")
return tag
def check_files():
"""Проверка файлов для загрузки"""
binaries_path = Path(BINARIES_DIR)
@@ -77,6 +87,7 @@ def check_files():
return files
def create_github_session(token):
"""Создание сессии для GitHub API"""
session = requests.Session()
@@ -87,6 +98,7 @@ def create_github_session(token):
})
return session
def check_existing_release(session, tag):
"""Проверка существующего релиза"""
log("Проверяем существующий релиз...")
@@ -95,16 +107,17 @@ def check_existing_release(session, tag):
response = session.get(url)
if response.status_code == 200:
release_data = response.json()
log(f"Реліз {tag} уже существует")
log(f"Релиз с тегом {tag} уже существует")
return release_data
elif response.status_code == 404:
log(f"Реліз {tag} не найден, создаем новый")
log(f"Релиз с тегом {tag} не найден, создаем новый")
return None
else:
error(f"Ошибка проверки релиза: {response.status_code}")
debug(f"Ответ: {response.text}")
sys.exit(1)
def create_release(session, tag):
"""Создание нового релиза"""
log(f"Создаем новый релиз {tag}...")
@@ -122,21 +135,17 @@ def create_release(session, tag):
if response.status_code == 201:
release_data = response.json()
log("Реліз создан успешно")
log("Релиз с тегом {tag} создан успешно")
return release_data
else:
error(f"Ошибка создания релиза: {response.status_code}")
error(f"Ошибка создания релиза с тегом {tag}: {response.status_code}")
debug(f"Ответ: {response.text}")
sys.exit(1)
def upload_file(session, upload_url, file_path):
"""Загрузка файла в релиз"""
filename = file_path.name
log(f"Загружаем: {filename}")
# Убираем {?name,label} из URL
upload_url = upload_url.replace("{?name,label}", "")
with open(file_path, 'rb') as f:
headers = {'Content-Type': 'application/octet-stream'}
params = {'name': filename}
@@ -156,6 +165,7 @@ def upload_file(session, upload_url, file_path):
debug(f"Ответ: {response.text}")
return False
def main():
"""Основная функция"""
log("=== НАЧАЛО РАБОТЫ СКРИПТА ===")
@@ -199,5 +209,6 @@ def main():
log(f"Реліз доступен: https://github.com/{REPO}/releases/tag/{tag}")
log("=== СКРИПТ ЗАВЕРШЕН ===")
if __name__ == "__main__":
main()