feat: add tone selector for AI content generation in 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

6 tones: Profesjonalny (default), Przyjazny, Oficjalny, Entuzjastyczny,
Informacyjny, Inspirujący. Tone instruction is appended to the AI prompt.
Dropdown appears next to "Generuj AI" button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-19 09:36:07 +01:00
parent c59fe04572
commit c1a8cb6183
3 changed files with 60 additions and 11 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
from services.social_publisher_service import social_publisher, POST_TYPES, POST_TONES, DEFAULT_TONE
db = SessionLocal()
try:
@ -149,7 +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_types=POST_TYPES, post_tones=POST_TONES,
default_tone=DEFAULT_TONE,
configured_companies=configured_companies)
post = social_publisher.create_post(
@ -172,7 +173,8 @@ def social_publisher_new():
return render_template('admin/social_publisher_form.html',
post=None, companies=companies, events=events,
post_types=POST_TYPES,
post_types=POST_TYPES, post_tones=POST_TONES,
default_tone=DEFAULT_TONE,
configured_companies=configured_companies)
finally:
db.close()
@ -183,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
from services.social_publisher_service import social_publisher, POST_TYPES, POST_TONES, DEFAULT_TONE
db = SessionLocal()
try:
@ -236,7 +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_types=POST_TYPES, post_tones=POST_TONES,
default_tone=DEFAULT_TONE,
configured_companies=configured_companies)
social_publisher.update_post(
@ -253,7 +256,8 @@ def social_publisher_edit(post_id):
return render_template('admin/social_publisher_form.html',
post=post, companies=companies, events=events,
post_types=POST_TYPES,
post_types=POST_TYPES, post_tones=POST_TONES,
default_tone=DEFAULT_TONE,
configured_companies=configured_companies)
finally:
db.close()
@ -341,6 +345,7 @@ def social_publisher_generate():
post_type = request.json.get('post_type')
company_id = request.json.get('company_id')
event_id = request.json.get('event_id')
tone = request.json.get('tone', '')
custom_context = request.json.get('custom_context', {})
try:
@ -363,7 +368,7 @@ def social_publisher_generate():
for key, default in defaults.items():
context.setdefault(key, default)
content, model = social_publisher.generate_content(post_type, context)
content, model = social_publisher.generate_content(post_type, context, tone=tone)
return jsonify({'success': True, 'content': content, 'model': model})
except Exception as e:
logger.error(f"AI generation failed: {e}")

View File

@ -116,6 +116,36 @@ Odpowiedz WYŁĄCZNIE tekstem postu.""",
}
POST_TONES = {
'professional': {
'label': 'Profesjonalny',
'instruction': 'Pisz w tonie profesjonalnym, rzeczowym i biznesowym. Zachowaj powagę, ale bądź przystępny.',
},
'friendly': {
'label': 'Przyjazny',
'instruction': 'Pisz ciepło i przyjaźnie, jakbyś rozmawiał z dobrym znajomym. Używaj bezpośredniego zwrotu do czytelnika.',
},
'formal': {
'label': 'Oficjalny',
'instruction': 'Pisz formalnie i oficjalnie, jak komunikat prasowy. Zachowaj dystans i powagę instytucji.',
},
'enthusiastic': {
'label': 'Entuzjastyczny',
'instruction': 'Pisz z energią i entuzjazmem! Używaj wykrzykników, pozytywnych emocji i dynamicznego języka.',
},
'informative': {
'label': 'Informacyjny',
'instruction': 'Pisz rzeczowo i informacyjnie, jak artykuł prasowy. Skup się na faktach, danych i konkretach.',
},
'inspiring': {
'label': 'Inspirujący',
'instruction': 'Pisz inspirująco i motywująco. Podkreślaj wartości, wizję i potencjał. Zachęcaj do działania.',
},
}
DEFAULT_TONE = 'professional'
class SocialPublisherService:
"""Service for managing social media posts with per-company FB configuration."""
@ -381,12 +411,13 @@ class SocialPublisherService:
# ---- AI Content Generation ----
def generate_content(self, post_type: str, context: dict) -> Tuple[str, str]:
def generate_content(self, post_type: str, context: dict, tone: str = None) -> Tuple[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)
Returns:
(content: str, ai_model: str)
@ -401,6 +432,11 @@ class SocialPublisherService:
except KeyError as e:
raise ValueError(f"Missing context field: {e}")
# Add tone instruction
tone_key = tone if tone in POST_TONES else DEFAULT_TONE
tone_info = POST_TONES[tone_key]
prompt += f"\n\nTONACJA: {tone_info['instruction']}"
# Generate with Gemini
from gemini_service import generate_text
result = generate_text(prompt)

View File

@ -308,8 +308,14 @@
</div>
</div>
<!-- Przycisk generowania AI -->
<div class="form-group" style="text-align: right;">
<!-- Tonacja + 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);">
{% for key, tone in post_tones.items() %}
<option value="{{ key }}" {% if key == default_tone %}selected{% endif %}>{{ tone.label }}</option>
{% endfor %}
</select>
<button type="button" id="btn-generate-ai" class="btn btn-generate">
Generuj AI
</button>
@ -422,9 +428,10 @@
const postType = document.getElementById('post_type').value;
const companyId = document.getElementById('company_id')?.value;
const eventId = document.getElementById('event_id')?.value;
const tone = document.getElementById('tone')?.value || '';
if (!postType) {
alert('Wybierz typ posta przed generowaniem.');
showAiError('Wybierz typ posta przed generowaniem.');
return;
}
@ -452,6 +459,7 @@
post_type: postType,
company_id: companyId || null,
event_id: eventId || null,
tone: tone,
custom_context: customContext
})
});