mobile version styled -ready for new version 2.0.1

This commit is contained in:
2025-10-22 18:37:08 +06:00
parent 344f763bb4
commit e37599d3ef
28 changed files with 4998 additions and 2224 deletions

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>`