Makes ai rule generation content work good.
This commit is contained in:
104
app/templates/partials/ai_rule_result.html
Normal file
104
app/templates/partials/ai_rule_result.html
Normal file
@@ -0,0 +1,104 @@
|
||||
{% if result.success %}
|
||||
<div>
|
||||
{% if result.cached %}
|
||||
<div class="alert alert-info mb-2" role="status" aria-live="polite">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
Using cached rule
|
||||
</div>
|
||||
{% elif result.fallback %}
|
||||
<div class="alert alert-warning mb-2" role="status" aria-live="polite">
|
||||
<i class="fas fa-exclamation-triangle mr-1" aria-hidden="true"></i>
|
||||
{{ result.message | default('Using fallback rule') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if result.rules %}
|
||||
<div class="grid grid-cols-1 gap-2 mb-2" role="list" aria-label="AI-generated rule options">
|
||||
{% for rule in result.rules %}
|
||||
<div class="bg-white border rounded-lg p-3" role="listitem"
|
||||
x-data="{}"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h4 class="font-medium text-sm">Option {{ loop.index }}</h4>
|
||||
<span class="badge {% if rule.quality_score >= 80 %}badge-success{% elif rule.quality_score >= 60 %}badge-warning{% else %}badge-error{% endif %}"
|
||||
role="status" aria-live="polite">
|
||||
{{ rule.quality_score }}%
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-700 mb-2 whitespace-pre-line">{{ rule.text }}</p>
|
||||
{% if rule.key_criteria %}
|
||||
<div class="text-xs text-gray-500 mb-2">
|
||||
<i class="fas fa-info-circle mr-1" aria-hidden="true"></i>
|
||||
{{ rule.key_criteria }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="flex gap-1">
|
||||
<button type="button"
|
||||
class="btn btn-xs btn-primary"
|
||||
@click="rule_text = $el.getAttribute('data-rule-text'); show_ai_rules=false "
|
||||
data-rule-text="{{ rule.text|safe }}"
|
||||
aria-label="Use rule option {{ loop.index }}">
|
||||
<i class="fas fa-check mr-1" aria-hidden="true"></i>
|
||||
Use
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
// Alpine.js data and methods
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('aiRuleResult', () => ({
|
||||
copyRuleText() {
|
||||
const ruleText = document.getElementById('generated-rule-text').textContent;
|
||||
navigator.clipboard.writeText(ruleText).then(() => {
|
||||
// Show feedback
|
||||
const button = event.target.closest('button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalContent;
|
||||
}, 2000);
|
||||
// Announce to screen readers
|
||||
announceToScreenReader('Rule copied to clipboard');
|
||||
}).catch(() => {
|
||||
announceToScreenReader('Failed to copy rule to clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}))
|
||||
});
|
||||
|
||||
function announceToScreenReader(message) {
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('role', 'status');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.className = 'sr-only';
|
||||
announcement.textContent = message;
|
||||
document.body.appendChild(announcement);
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(announcement);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Add keyboard support for buttons
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
const focusedElement = document.activeElement;
|
||||
if (focusedElement && focusedElement.getAttribute('role') === 'button') {
|
||||
event.preventDefault();
|
||||
focusedElement.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-error mb-2">
|
||||
<i class="fas fa-exclamation-circle mr-1"></i>
|
||||
{{ result.error | default('Failed to generate rule') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,4 +1,10 @@
|
||||
<div id="folder-modal" @click.away="$refs.modal.close()" class="modal-box" x-data="{ errors: {{ 'true' if errors else 'false' }} }" x-init="$nextTick(() => { if (errors) { document.querySelector('#submit-btn').classList.add('shake'); } })">
|
||||
<!-- x-data="{ errors: {{ 'true' if errors else 'false' }},
|
||||
ruleText:{% if folder %}{{ folder.rule_text|tojson }}{% endif %},
|
||||
showAiResults: true }"
|
||||
-->
|
||||
<div id="folder-modal" @click.away="$refs.modal.close()" class="modal-box"
|
||||
x-data='{{ folder_data|tojson }}'
|
||||
x-init="$nextTick(() => { if (errors) { document.querySelector('#submit-btn').classList.add('shake'); } })">
|
||||
<h3 class="font-bold text-lg mb-4" id="modal-title">
|
||||
{% if folder %}Edit Folder{% else %}Add New Folder{% endif %}
|
||||
</h3>
|
||||
@@ -10,8 +16,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form id="folder-form"
|
||||
{% if folder %} hx-put="/api/folders/{{ folder.id }}" {% else %} hx-post="/api/folders" {% endif %}
|
||||
<form id="folder-form"
|
||||
{% if folder %} hx-put="/api/folders/{{ folder.id }}" {% else %} hx-post="/api/folders" {% endif %}
|
||||
hx-target="#folder-modal"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
@@ -30,10 +36,34 @@
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="folder-rule" class="block text-sm font-medium mb-1">Rule (Natural Language)</label>
|
||||
<div class="flex gap-2 mb-2">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline btn-secondary"
|
||||
id="generate-multiple-rules"
|
||||
hx-post="/api/folders/generate-rule"
|
||||
hx-vals='{"folder_name": "{{ name if name is defined else '' }}", "folder_type": "{{ 'tidy' if (name is defined and name.strip().lower() == 'inbox') else 'destination' }}", "rule_type": "multiple"}'
|
||||
hx-target="#rule-generation-result"
|
||||
hx-swap="innerHTML"
|
||||
data-loading-disable
|
||||
aria-label="Generate multiple AI-powered email rule options"
|
||||
aria-describedby="ai-rule-help">
|
||||
<i class="fas fa-th mr-1" data-loading-class="!hidden"></i>
|
||||
<span data-loading-class="!hidden">Enhance my rules</span>
|
||||
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span>
|
||||
</button>
|
||||
<div id="ai-rule-help" class="hidden">
|
||||
AI-powered rule generation creates email organization rules based on your folder name and type.
|
||||
</div>
|
||||
</div>
|
||||
<div id="rule-generation-result" class="mb-2" x-show="show_ai_rules">
|
||||
<!-- AI rule results will be injected here -->
|
||||
</div>
|
||||
<textarea id="folder-rule" name="rule_text"
|
||||
class="textarea textarea-bordered w-full h-24 {% if errors and errors.rule_text %}textarea-error{% endif %}"
|
||||
placeholder="e.g., Move emails from 'newsletter@company.com' to this folder"
|
||||
required>{% if rule_text is defined %}{{ rule_text }}{% elif folder %}{{ folder.rule_text }}{% endif %}</textarea>
|
||||
required
|
||||
x-model="rule_text"
|
||||
>{% if rule_text is defined %}{{ rule_text }}{% elif folder %}{{ folder.rule_text }}{% endif %}</textarea>
|
||||
{% if errors and errors.rule_text %}
|
||||
<div class="text-error text-sm mt-1">{{ errors.rule_text }}</div>
|
||||
{% endif %}
|
||||
@@ -55,4 +85,19 @@
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Alpine.js event listener for AI rule usage -->
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
// Listen for the custom event when an AI rule is used
|
||||
window.addEventListener('ai-rule-used', (event) => {
|
||||
// Set the textarea value with the selected text
|
||||
document.getElementById('folder-rule').value = event.detail.text;
|
||||
// Trigger validation
|
||||
document.getElementById('folder-rule').dispatchEvent(new Event('input'));
|
||||
// Hide AI results
|
||||
document.querySelector('[x-data]').__x.$data.showAiResults = false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
Reference in New Issue
Block a user