feat: ssh auth, protocol management, and cleanup
This commit is contained in:
@@ -0,0 +1,522 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user