feat: add AI hashtag generation and AI engine info to social publisher
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

- New generate_hashtags() method in SocialPublisherService
- New /social-publisher/generate-hashtags AJAX endpoint
- "Generuj hashtagi AI" button next to hashtags field
- Small print info about AI engine (Gemini 3 Flash) with note
  about future model selection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-19 09:32:29 +01:00
parent 921f72f569
commit c59fe04572
3 changed files with 140 additions and 1 deletions

View File

@ -370,6 +370,27 @@ def social_publisher_generate():
return jsonify({'success': False, 'error': 'Nie udalo sie wygenerowac tresci. Sprobuj ponownie lub wpisz tresc recznie.'}), 500
@bp.route('/social-publisher/generate-hashtags', methods=['POST'])
@login_required
@role_required(SystemRole.MANAGER)
def social_publisher_generate_hashtags():
"""Generuj hashtagi AI na podstawie tresci posta (AJAX endpoint)."""
from services.social_publisher_service import social_publisher
content = request.json.get('content', '').strip()
post_type = request.json.get('post_type', '')
if not content:
return jsonify({'success': False, 'error': 'Wpisz najpierw tresc posta, aby wygenerowac hashtagi.'}), 400
try:
hashtags, model = social_publisher.generate_hashtags(content, post_type)
return jsonify({'success': True, 'hashtags': hashtags, 'model': model})
except Exception as e:
logger.error(f"Hashtag generation failed: {e}")
return jsonify({'success': False, 'error': 'Nie udalo sie wygenerowac hashtagow. Sprobuj ponownie.'}), 500
# ============================================================
# SOCIAL PUBLISHER - SETTINGS (per company)
# ============================================================

View File

@ -410,6 +410,35 @@ class SocialPublisherService:
return result.strip(), 'gemini-3-flash'
def generate_hashtags(self, content: str, post_type: str = '') -> Tuple[str, str]:
"""Generate hashtags for given post content using AI.
Returns:
(hashtags: str, ai_model: str)
"""
prompt = f"""Na podstawie poniższej treści posta na Facebook Izby Gospodarczej NORDA Biznes,
wygeneruj 5-8 trafnych hashtagów.
Treść posta:
{content}
Zasady:
- Zawsze uwzględnij #NordaBiznes i #IzbaGospodarcza
- Dodaj hashtagi branżowe i lokalne (#Wejherowo, #Pomorze, #Kaszuby)
- Hashtagi powinny zwiększać zasięg i widoczność posta
- Każdy hashtag zaczynaj od #, oddzielaj spacjami
- Odpowiedz WYŁĄCZNIE hashtagami, nic więcej"""
from gemini_service import generate_text
result = generate_text(prompt)
if not result:
raise RuntimeError("AI nie wygenerował hashtagów. Spróbuj ponownie.")
# Clean up - ensure only hashtags
tags = ' '.join(w for w in result.strip().split() if w.startswith('#'))
return tags, 'gemini-3-flash'
def get_company_context(self, company_id: int) -> dict:
"""Get company data for AI prompt context."""
db = SessionLocal()

View File

@ -101,6 +101,39 @@
cursor: wait;
}
.btn-generate-sm {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
cursor: pointer;
font-size: var(--font-size-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius);
}
.btn-generate-sm:hover {
opacity: 0.9;
}
.btn-generate-sm:disabled {
opacity: 0.6;
cursor: wait;
}
.ai-engine-info {
font-size: 11px;
color: var(--text-tertiary, #9ca3af);
margin-top: var(--spacing-xs);
line-height: 1.4;
}
.hashtag-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
}
.status-info {
background: var(--background);
padding: var(--spacing-md);
@ -293,11 +326,23 @@
<!-- Hashtagi -->
<div class="form-group">
<label for="hashtags">Hashtagi</label>
<div class="hashtag-header">
<label for="hashtags">Hashtagi</label>
<button type="button" id="btn-generate-hashtags" class="btn-generate-sm">
Generuj hashtagi AI
</button>
</div>
<input type="text" id="hashtags" name="hashtags"
value="{{ post.hashtags if post else '' }}"
placeholder="#NordaBiznes #Wejherowo #Pomorze">
<p class="form-hint">Hashtagi oddzielone spacjami</p>
<div id="hashtag-error-msg" style="display:none; padding: var(--spacing-xs) var(--spacing-sm); margin-top: var(--spacing-xs); border-radius: var(--radius); background: var(--error-bg, #fef2f2); color: var(--error, #dc2626); border: 1px solid var(--error, #dc2626); font-size: var(--font-size-xs);"></div>
</div>
<!-- Info o silniku AI -->
<div class="ai-engine-info">
Generowanie tresc i hashtagow: <strong>Google Gemini 3 Flash</strong> (model: gemini-3-flash-preview).
Obecnie jest to jedyny dostepny silnik AI. W przyszlosci planowane jest dodanie wyboru miedzy modelami (np. GPT, Claude).
</div>
<!-- Akcje -->
@ -429,6 +474,50 @@
}
});
// Hashtag generation
document.getElementById('btn-generate-hashtags')?.addEventListener('click', async function() {
const content = document.getElementById('content').value.trim();
const errEl = document.getElementById('hashtag-error-msg');
if (!content) {
errEl.textContent = 'Wpisz najpierw tresc posta, aby wygenerowac hashtagi.';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
this.disabled = true;
this.textContent = 'Generowanie...';
try {
const resp = await fetch("{{ url_for('admin.social_publisher_generate_hashtags') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('[name=csrf_token]')?.value || ''
},
body: JSON.stringify({
content: content,
post_type: document.getElementById('post_type')?.value || ''
})
});
const data = await resp.json();
if (data.success) {
document.getElementById('hashtags').value = data.hashtags;
errEl.style.display = 'none';
} else {
errEl.textContent = data.error || 'Nie udalo sie wygenerowac hashtagow.';
errEl.style.display = 'block';
}
} catch (err) {
errEl.textContent = 'Blad polaczenia z serwerem.';
errEl.style.display = 'block';
} finally {
this.disabled = false;
this.textContent = 'Generuj hashtagi AI';
}
});
// Show/hide context fields based on post type
document.getElementById('post_type')?.addEventListener('change', function() {
const type = this.value;