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
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:
parent
b73fcb59d1
commit
ce8bb1109f
@ -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}")
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user