feat: add AI model selector for social publisher content generation
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

3 models available: Gemini 3 Flash (default, cheap), Gemini 3 Pro
(best quality, 4x cost), Gemini 2.5 Flash (stable previous gen).
Model selection applies to both content and hashtag generation.
Shows which model was used after generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-19 09:46:52 +01:00
parent b73fcb59d1
commit ce8bb1109f
3 changed files with 62 additions and 19 deletions

View File

@ -123,7 +123,7 @@ def social_publisher_list():
@role_required(SystemRole.MANAGER)
def social_publisher_new():
"""Tworzenie nowego posta."""
from services.social_publisher_service import social_publisher, POST_TYPES, POST_TONES, DEFAULT_TONE
from services.social_publisher_service import social_publisher, POST_TYPES, POST_TONES, DEFAULT_TONE, AI_MODELS, DEFAULT_AI_MODEL
db = SessionLocal()
try:
@ -149,8 +149,8 @@ def social_publisher_new():
flash('Treść posta jest wymagana.', 'danger')
return render_template('admin/social_publisher_form.html',
post=None, companies=companies, events=events,
post_types=POST_TYPES, post_tones=POST_TONES,
default_tone=DEFAULT_TONE,
post_types=POST_TYPES, post_tones=POST_TONES, default_tone=DEFAULT_TONE,
ai_models=AI_MODELS, default_ai_model=DEFAULT_AI_MODEL,
configured_companies=configured_companies)
post = social_publisher.create_post(
@ -185,7 +185,7 @@ def social_publisher_new():
@role_required(SystemRole.MANAGER)
def social_publisher_edit(post_id):
"""Edycja istniejacego posta."""
from services.social_publisher_service import social_publisher, POST_TYPES, POST_TONES, DEFAULT_TONE
from services.social_publisher_service import social_publisher, POST_TYPES, POST_TONES, DEFAULT_TONE, AI_MODELS, DEFAULT_AI_MODEL
db = SessionLocal()
try:
@ -238,8 +238,8 @@ def social_publisher_edit(post_id):
flash('Treść posta jest wymagana.', 'danger')
return render_template('admin/social_publisher_form.html',
post=post, companies=companies, events=events,
post_types=POST_TYPES, post_tones=POST_TONES,
default_tone=DEFAULT_TONE,
post_types=POST_TYPES, post_tones=POST_TONES, default_tone=DEFAULT_TONE,
ai_models=AI_MODELS, default_ai_model=DEFAULT_AI_MODEL,
configured_companies=configured_companies)
social_publisher.update_post(
@ -346,6 +346,7 @@ def social_publisher_generate():
company_id = request.json.get('company_id')
event_id = request.json.get('event_id')
tone = request.json.get('tone', '')
ai_model = request.json.get('ai_model', '')
custom_context = request.json.get('custom_context', {})
try:
@ -368,7 +369,7 @@ def social_publisher_generate():
for key, default in defaults.items():
context.setdefault(key, default)
content, hashtags, model = social_publisher.generate_content(post_type, context, tone=tone)
content, hashtags, model = social_publisher.generate_content(post_type, context, tone=tone, ai_model=ai_model)
return jsonify({'success': True, 'content': content, 'hashtags': hashtags, 'model': model})
except Exception as e:
logger.error(f"AI generation failed: {e}")
@ -384,12 +385,13 @@ def social_publisher_generate_hashtags():
content = request.json.get('content', '').strip()
post_type = request.json.get('post_type', '')
ai_model = request.json.get('ai_model', '')
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)
hashtags, model = social_publisher.generate_hashtags(content, post_type, ai_model=ai_model)
return jsonify({'success': True, 'hashtags': hashtags, 'model': model})
except Exception as e:
logger.error(f"Hashtag generation failed: {e}")

View File

@ -117,6 +117,24 @@ Odpowiedz WYŁĄCZNIE tekstem postu, BEZ hashtagów.""",
}
AI_MODELS = {
'3-flash': {
'label': 'Gemini 3 Flash',
'description': 'Szybki, tani — dobry do większości postów',
},
'3-pro': {
'label': 'Gemini 3 Pro',
'description': 'Najwyższa jakość — lepszy styl i kreatywność (4x droższy)',
},
'flash': {
'label': 'Gemini 2.5 Flash',
'description': 'Poprzednia generacja — stabilny, sprawdzony',
},
}
DEFAULT_AI_MODEL = '3-flash'
POST_TONES = {
'professional': {
'label': 'Profesjonalny',
@ -452,13 +470,15 @@ class SocialPublisherService:
return content, ' '.join(unique_tags)
def generate_content(self, post_type: str, context: dict, tone: str = None) -> Tuple[str, str, str]:
def generate_content(self, post_type: str, context: dict, tone: str = None,
ai_model: str = None) -> Tuple[str, str, str]:
"""Generate post content using AI.
Args:
post_type: One of POST_TYPES keys
context: Dict with template variables
tone: One of POST_TONES keys (default: DEFAULT_TONE)
ai_model: One of AI_MODELS keys (default: DEFAULT_AI_MODEL)
Returns:
(content: str, hashtags: str, ai_model: str)
@ -478,23 +498,30 @@ class SocialPublisherService:
tone_info = POST_TONES[tone_key]
prompt += f"\n\nTONACJA: {tone_info['instruction']}"
# Select model
model_key = ai_model if ai_model in AI_MODELS else DEFAULT_AI_MODEL
# Generate with Gemini
from gemini_service import generate_text
result = generate_text(prompt)
result = generate_text(prompt, model=model_key)
if not result:
raise RuntimeError("AI nie wygenerował treści. Spróbuj ponownie.")
# Split out any hashtags AI may have included despite instructions
content, hashtags = self._split_hashtags(result)
return content, hashtags, 'gemini-3-flash'
model_label = AI_MODELS[model_key]['label']
return content, hashtags, model_label
def generate_hashtags(self, content: str, post_type: str = '') -> Tuple[str, str]:
def generate_hashtags(self, content: str, post_type: str = '',
ai_model: str = None) -> Tuple[str, str]:
"""Generate hashtags for given post content using AI.
Returns:
(hashtags: str, ai_model: str)
(hashtags: str, ai_model_label: str)
"""
model_key = ai_model if ai_model in AI_MODELS else DEFAULT_AI_MODEL
prompt = f"""Na podstawie poniższej treści posta na Facebook Izby Gospodarczej NORDA Biznes,
wygeneruj 5-8 trafnych hashtagów.
@ -509,14 +536,14 @@ Zasady:
- Odpowiedz WYŁĄCZNIE hashtagami, nic więcej"""
from gemini_service import generate_text
result = generate_text(prompt)
result = generate_text(prompt, model=model_key)
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'
return tags, AI_MODELS[model_key]['label']
def get_company_context(self, company_id: int) -> dict:
"""Get company data for AI prompt context."""

View File

@ -308,7 +308,7 @@
</div>
</div>
<!-- Tonacja + Przycisk generowania AI -->
<!-- Tonacja + Model + Przycisk generowania AI -->
<div class="form-group" style="display: flex; align-items: center; justify-content: flex-end; gap: var(--spacing-sm); flex-wrap: wrap;">
<label for="tone" style="margin: 0; white-space: nowrap;">Tonacja:</label>
<select id="tone" name="tone" style="width: auto; min-width: 160px; padding: var(--spacing-xs) var(--spacing-sm); border: 1px solid var(--border); border-radius: var(--radius-md); font-size: var(--font-size-sm);">
@ -316,6 +316,12 @@
<option value="{{ key }}" {% if key == default_tone %}selected{% endif %}>{{ tone.label }}</option>
{% endfor %}
</select>
<label for="ai_model" style="margin: 0; white-space: nowrap;">Model:</label>
<select id="ai_model" name="ai_model" style="width: auto; min-width: 170px; padding: var(--spacing-xs) var(--spacing-sm); border: 1px solid var(--border); border-radius: var(--radius-md); font-size: var(--font-size-sm);">
{% for key, model in ai_models.items() %}
<option value="{{ key }}" {% if key == default_ai_model %}selected{% endif %} title="{{ model.description }}">{{ model.label }}</option>
{% endfor %}
</select>
<button type="button" id="btn-generate-ai" class="btn btn-generate">
Generuj AI
</button>
@ -347,8 +353,9 @@
<!-- 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).
Silnik AI: <strong>Google Gemini</strong>. Domyslny model (Gemini 3 Flash) jest szybki i tani.
Dla lepszej jakosci wybierz Gemini 3 Pro — lepsza kreatywnosc i styl, ale ~4x wyzszy koszt.
<span id="ai-model-used" style="display:none;"> Ostatnio uzyty: <strong id="ai-model-used-name"></strong></span>
</div>
<!-- Akcje -->
@ -429,6 +436,7 @@
const companyId = document.getElementById('company_id')?.value;
const eventId = document.getElementById('event_id')?.value;
const tone = document.getElementById('tone')?.value || '';
const aiModel = document.getElementById('ai_model')?.value || '';
if (!postType) {
showAiError('Wybierz typ posta przed generowaniem.');
@ -460,6 +468,7 @@
company_id: companyId || null,
event_id: eventId || null,
tone: tone,
ai_model: aiModel,
custom_context: customContext
})
});
@ -471,6 +480,10 @@
}
updateContentCounter();
hideAiError();
if (data.model) {
document.getElementById('ai-model-used-name').textContent = data.model;
document.getElementById('ai-model-used').style.display = 'inline';
}
} else {
showAiError(data.error || 'Nie udalo sie wygenerowac tresci. Sprobuj ponownie.');
}
@ -506,7 +519,8 @@
},
body: JSON.stringify({
content: content,
post_type: document.getElementById('post_type')?.value || ''
post_type: document.getElementById('post_type')?.value || '',
ai_model: document.getElementById('ai_model')?.value || ''
})
});
const data = await resp.json();