Files
amneziavpnphp/templates/settings/protocols_management.twig
2026-01-23 17:55:40 +03:00

522 lines
30 KiB
Twig

<div class="max-w-6xl mx-auto px-1 py-2">
<!-- Header -->
<div class="mb-8">
<div class="flex justify-between items-center">
<div>
<h1 class="text-3xl font-bold text-gray-900">{{ t('protocols.management') }}</h1>
<p class="mt-2 text-gray-600">{{ t('protocols.management_description') }}</p>
</div>
<div class="flex space-x-3">
<button id="ai-assistant-btn" class="inline-flex items-center px-4 py-2 border border-purple-300 rounded-md shadow-sm text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
{{ t('ai.assistant') }}
</button>
<a href="/settings/protocols/new" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{ t('protocols.add_protocol') }}
</a>
</div>
</div>
</div>
<!-- Success/Error Messages -->
{% if success %}
<div class="mb-4 bg-green-50 border border-green-200 rounded-md p-4">
<div class="flex">
<svg class="w-5 h-5 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
<p class="text-green-800">{{ success }}</p>
</div>
</div>
{% endif %}
{% if error %}
<div class="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
<div class="flex">
<svg class="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<p class="text-red-800">{{ error }}</p>
</div>
</div>
{% endif %}
<!-- Protocols Grid -->
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 sm:p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.available_protocols') }}</h2>
<div class="flex space-x-2">
<input type="text" id="protocol-search" placeholder="{{ t('protocols.search_protocols') }}" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<select id="protocol-filter" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">{{ t('protocols.all_protocols') }}</option>
<option value="active">{{ t('protocols.active_only') }}</option>
<option value="ubuntu">{{ t('protocols.ubuntu_compatible') }}</option>
<option value="with-ai">{{ t('protocols.with_ai_generations') }}</option>
</select>
</div>
</div>
<div id="protocols-list" class="space-y-4">
{% for protocol in protocols %}
<div class="protocol-card border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow" data-protocol-id="{{ protocol.id }}" data-protocol-name="{{ protocol.name }}" data-protocol-slug="{{ protocol.slug }}" data-active="{{ protocol.is_active }}" data-ubuntu="{{ protocol.ubuntu_compatible }}" data-ai-generations="{{ protocol.ai_generation_count }}">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center space-x-3">
<h3 class="text-lg font-semibold text-gray-900">{{ protocol.name }}</h3>
{% if protocol.is_active %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{{ t('common.active') }}
</span>
{% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ t('common.inactive') }}
</span>
{% endif %}
{% if protocol.ubuntu_compatible %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
Ubuntu 22-24
</span>
{% endif %}
{% if protocol.ai_generation_count > 0 %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
AI {{ protocol.ai_generation_count }}
</span>
{% endif %}
</div>
<p class="mt-1 text-sm text-gray-600">{{ protocol.description }}</p>
<div class="mt-2 flex items-center space-x-4 text-xs text-gray-500">
<span>{{ t('common.slug') }}: <code class="bg-gray-100 px-1 rounded">{{ protocol.slug }}</code></span>
<span>{{ t('common.servers') }}: {{ protocol.server_count }}</span>
<span>{{ t('common.templates') }}: {{ protocol.template_count }}</span>
<span>{{ t('common.variables') }}: {{ protocol.variable_count }}</span>
</div>
</div>
<div class="flex space-x-2">
<button class="ai-generate-btn text-purple-600 hover:text-purple-900" data-protocol-id="{{ protocol.id }}" title="{{ t('ai.generate_with_ai') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</button>
<a href="/settings/protocols/{{ protocol.id }}/edit" class="text-blue-600 hover:text-blue-900" title="{{ t('common.edit') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</a>
<a href="/settings/protocols/{{ protocol.id }}/template" class="text-green-600 hover:text-green-900" title="{{ t('protocols.edit_template') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
</svg>
</a>
{% if protocol.server_count == 0 %}
<button class="delete-protocol-btn text-red-600 hover:text-red-900" data-protocol-id="{{ protocol.id }}" data-protocol-name="{{ protocol.name }}" title="{{ t('common.delete') }}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
{% endif %}
</div>
</div>
</div>
{% else %}
<div class="text-center py-8">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
</svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ t('protocols.no_protocols') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.no_protocols_description') }}</p>
<div class="mt-6">
<a href="/settings/protocols/new" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
{{ t('protocols.create_first_protocol') }}
</a>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- AI Assistant Modal -->
<div id="ai-assistant-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900">{{ t('ai.assistant') }}</h3>
<button id="close-ai-modal" class="text-gray-400 hover:text-gray-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.select_model') }}</label>
<select id="ai-model-select" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="openai/gpt-3.5-turbo">{{ t('ai.model_gpt35_turbo') }}</option>
<option value="openai/gpt-4">{{ t('ai.model_gpt4') }}</option>
<option value="anthropic/claude-3-haiku">{{ t('ai.model_claude3_haiku') }}</option>
<option value="anthropic/claude-3-sonnet">{{ t('ai.model_claude3_sonnet') }}</option>
</select>
<div class="mt-2 flex items-center space-x-2">
<input id="ai-model-custom" type="text" placeholder="{{ t('ai.custom_model_placeholder') }}" class="flex-1 px-3 py-2 border border-gray-300 rounded-md">
<button id="ai-model-test-btn" type="button" class="px-3 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50">{{ t('ai.check_availability') }}</button>
</div>
<p class="mt-2 text-xs text-gray-500">
<i class="fas fa-external-link-alt"></i>
<a href="https://openrouter.ai/models" target="_blank" class="text-purple-600">openrouter.ai/models</a>
</p>
{% if not openrouter_key %}
<div class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
<div class="flex items-center justify-between">
<span class="text-xs text-yellow-800">{{ t('settings.no_api_key') }}</span>
<a href="/settings#api" class="px-2 py-1 text-xs bg-purple-600 text-white rounded">{{ t('settings.enter_api_key') }}</a>
</div>
</div>
{% endif %}
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.protocol_type') }}</label>
<select id="ai-protocol-type" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">{{ t('ai.general_vpn') }}</option>
<option value="wireguard">WireGuard</option>
<option value="openvpn">OpenVPN</option>
<option value="shadowsocks">Shadowsocks</option>
<option value="cloak">Cloak</option>
<option value="ikev2">IKEv2</option>
</select>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.describe_requirements') }}</label>
<textarea id="ai-prompt" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{{ t('ai.prompt_placeholder') }}"></textarea>
</div>
<div class="mb-4">
<button id="generate-script-btn" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
{{ t('ai.generate_script') }}
</button>
</div>
<div id="ai-loading" class="hidden text-center py-4">
<div class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ t('ai.generating_script') }}</span>
</div>
</div>
<div id="ai-result" class="hidden">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.generated_script') }}</label>
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
<pre id="generated-script" class="text-sm whitespace-pre-wrap"></pre>
</div>
</div>
<div id="ai-suggestions" class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.suggestions') }}</label>
<ul id="suggestions-list" class="list-disc list-inside space-y-1 text-sm text-gray-600"></ul>
</div>
<div class="mb-4 grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.name') }}</label>
<input id="ai-protocol-name" type="text" placeholder="Protocol Name" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.slug') }}</label>
<input id="ai-protocol-slug" type="text" placeholder="protocol-slug" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.compatibility') }}</label>
<div id="ubuntu-compatibility" class="flex items-center"></div>
</div>
<div class="flex space-x-3">
<button id="apply-to-protocol-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
{{ t('ai.apply_to_protocol') }}
</button>
<button id="create-new-protocol-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
{{ t('ai.create_new_protocol') }}
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Protocol search and filter
const searchInput = document.getElementById('protocol-search');
const filterSelect = document.getElementById('protocol-filter');
const protocolCards = document.querySelectorAll('.protocol-card');
function filterProtocols() {
const searchTerm = searchInput.value.toLowerCase();
const filterValue = filterSelect.value;
protocolCards.forEach(card => {
const name = card.dataset.protocolName.toLowerCase();
const slug = card.dataset.protocolSlug.toLowerCase();
const isActive = card.dataset.active === '1';
const isUbuntu = card.dataset.ubuntu === '1';
const hasAI = parseInt(card.dataset.aiGenerations) > 0;
let show = true;
// Search filter
if (searchTerm && !name.includes(searchTerm) && !slug.includes(searchTerm)) {
show = false;
}
// Filter by type
if (filterValue === 'active' && !isActive) show = false;
if (filterValue === 'ubuntu' && !isUbuntu) show = false;
if (filterValue === 'with-ai' && !hasAI) show = false;
card.style.display = show ? 'block' : 'none';
});
}
searchInput.addEventListener('input', filterProtocols);
filterSelect.addEventListener('change', filterProtocols);
// AI Assistant Modal
const aiModal = document.getElementById('ai-assistant-modal');
const aiAssistantBtn = document.getElementById('ai-assistant-btn');
const closeAIModal = document.getElementById('close-ai-modal');
const generateScriptBtn = document.getElementById('generate-script-btn');
const aiLoading = document.getElementById('ai-loading');
const aiResult = document.getElementById('ai-result');
const applyToProtocolBtn = document.getElementById('apply-to-protocol-btn');
const createNewProtocolBtn = document.getElementById('create-new-protocol-btn');
let currentGeneration = null;
let currentProtocolId = null;
function showAIModal() {
aiModal.classList.remove('hidden');
aiResult.classList.add('hidden');
aiLoading.classList.add('hidden');
}
function hideAIModal() {
aiModal.classList.add('hidden');
currentGeneration = null;
currentProtocolId = null;
}
aiAssistantBtn.addEventListener('click', showAIModal);
closeAIModal.addEventListener('click', hideAIModal);
// Close modal when clicking outside
aiModal.addEventListener('click', function(e) {
if (e.target === aiModal) {
hideAIModal();
}
});
// Generate script with AI
generateScriptBtn.addEventListener('click', async function() {
const model = document.getElementById('ai-model-select').value;
const customModel = document.getElementById('ai-model-custom').value.trim();
const effectiveModel = customModel !== '' ? customModel : model;
const protocolType = document.getElementById('ai-protocol-type').value;
const prompt = document.getElementById('ai-prompt').value;
if (!prompt.trim()) {
alert('{{ t('ai.please_enter_requirements') }}');
return;
}
aiLoading.classList.remove('hidden');
generateScriptBtn.disabled = true;
try {
const response = await fetch('/api/ai/assist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt: prompt,
model: effectiveModel,
protocol_type: protocolType,
protocol_id: currentProtocolId
})
});
const result = await response.json();
if (result.success) {
currentGeneration = result.data;
displayAIResult(result.data);
} else {
alert('{{ t('ai.error_generating_script') }}: ' + result.error);
}
} catch (error) {
alert('{{ t('ai.error_generating_script') }}: ' + error.message);
} finally {
aiLoading.classList.add('hidden');
generateScriptBtn.disabled = false;
}
});
function displayAIResult(data) {
document.getElementById('generated-script').textContent = data.script;
const suggestionsList = document.getElementById('suggestions-list');
suggestionsList.innerHTML = '';
data.suggestions.forEach(suggestion => {
const li = document.createElement('li');
li.textContent = suggestion;
suggestionsList.appendChild(li);
});
const compatibilityDiv = document.getElementById('ubuntu-compatibility');
if (data.ubuntu_compatible) {
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg><span class="text-green-700">Compatible with Ubuntu 22.04-24.04</span>';
} else {
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg><span class="text-red-700">May not be compatible with Ubuntu 22.04-24.04</span>';
}
aiResult.classList.remove('hidden');
}
// Apply to existing protocol
applyToProtocolBtn.addEventListener('click', function() {
if (!currentGeneration) return;
const protocolId = prompt('{{ t('ai.enter_protocol_id_to_apply') }}:');
if (!protocolId) return;
fetch(`/api/protocols/${protocolId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
install_script: currentGeneration.script,
ubuntu_compatible: currentGeneration.ubuntu_compatible
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('{{ t('ai.script_applied_successfully') }}');
location.reload();
} else {
alert('{{ t('ai.error_applying_script') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('ai.error_applying_script') }}: ' + error.message);
});
});
// Create new protocol with generated script
createNewProtocolBtn.addEventListener('click', function() {
if (!currentGeneration) return;
const nameInput = document.getElementById('ai-protocol-name');
const slugInput = document.getElementById('ai-protocol-slug');
const name = (nameInput.value || '').trim();
let slug = (slugInput.value || '').trim();
if (!name) { alert('{{ t('protocols.enter_protocol_name') }}'); return; }
if (!slug) { slug = name.toLowerCase().replace(/\s+/g, '-'); }
fetch('/api/protocols', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
slug: slug,
install_script: currentGeneration.script,
ubuntu_compatible: currentGeneration.ubuntu_compatible,
description: `Generated with AI using ${document.getElementById('ai-model-select').value}`
})
})
.then(response => response.json())
.then(result => {
if (result.success) {
alert('{{ t('protocols.protocol_created_successfully') }}');
location.reload();
} else {
alert('{{ t('protocols.error_creating_protocol') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('protocols.error_creating_protocol') }}: ' + error.message);
});
});
// AI generate for specific protocol
document.querySelectorAll('.ai-generate-btn').forEach(btn => {
btn.addEventListener('click', function() {
currentProtocolId = this.dataset.protocolId;
showAIModal();
document.getElementById('ai-prompt').value = `{{ t('ai.improve_protocol') }} ${this.closest('.protocol-card').dataset.protocolName}`;
});
});
// Test custom model availability
document.getElementById('ai-model-test-btn').addEventListener('click', async function() {
const customModel = document.getElementById('ai-model-custom').value.trim();
if (!customModel) { alert('Введите идентификатор модели'); return; }
try {
const resp = await fetch('/api/ai/test-model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: customModel })
});
const result = await resp.json();
if (result.success) {
alert('Модель доступна');
} else {
alert('Модель недоступна: ' + (result.error || result.message || ''));
}
} catch (e) {
alert('Ошибка проверки модели: ' + e.message);
}
});
// Delete protocol
document.querySelectorAll('.delete-protocol-btn').forEach(btn => {
btn.addEventListener('click', function() {
const protocolId = this.dataset.protocolId;
const protocolName = this.dataset.protocolName;
if (confirm(`{{ t('protocols.confirm_delete_protocol') }} '${protocolName}'?`)) {
fetch(`/api/protocols/${protocolId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(result => {
if (result.success) {
location.reload();
} else {
alert('{{ t('protocols.error_deleting_protocol') }}: ' + result.error);
}
})
.catch(error => {
alert('{{ t('protocols.error_deleting_protocol') }}: ' + error.message);
});
}
});
});
});
</script>