feat: Phase B learning + hashtag profiles Pro-only lock

- Approve action saves post as reference example in character's content_rules
- Keep last 5 approved examples per character (auto-rotating)
- Inject last 3 approved examples as few-shot in LLM system prompt
- Lock YouTube/TikTok hashtag profile tabs for Freemium users (Pro only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michele
2026-04-04 19:48:04 +02:00
parent befa8b4adc
commit 16c7c4404c
3 changed files with 41 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { api } from '../api'
import { useAuth } from '../AuthContext'
const EMPTY_FORM = {
name: '',
@@ -37,6 +38,7 @@ export default function CharacterForm() {
const { id } = useParams()
const isEdit = Boolean(id)
const navigate = useNavigate()
const { isPro } = useAuth()
const [form, setForm] = useState(EMPTY_FORM)
const [topicInput, setTopicInput] = useState('')
@@ -291,6 +293,7 @@ export default function CharacterForm() {
<HashtagProfileEditor
profiles={form.hashtag_profiles || {}}
onChange={p => handleChange('hashtag_profiles', p)}
isPro={isPro}
/>
</Section>
@@ -432,9 +435,11 @@ function RulesEditor({ doRules, dontRules, onChange }) {
)
}
function HashtagProfileEditor({ profiles, onChange }) {
function HashtagProfileEditor({ profiles, onChange, isPro }) {
const [activeTab, setActiveTab] = useState('instagram')
const [tagInput, setTagInput] = useState('')
const availablePlatforms = isPro ? HASHTAG_PLATFORMS : HASHTAG_PLATFORMS.filter(p => ['instagram', 'facebook'].includes(p))
const proOnlyPlatforms = ['youtube', 'tiktok']
const getProfile = (platform) => profiles[platform] || { always: [], max_generated: 12 }
const setProfile = (platform, profile) => onChange({ ...profiles, [platform]: profile })
@@ -460,18 +465,22 @@ function HashtagProfileEditor({ profiles, onChange }) {
return (
<div>
<div style={{ display: 'flex', gap: '0', borderBottom: '2px solid var(--border)', marginBottom: '1rem' }}>
{HASHTAG_PLATFORMS.map(p => (
<button key={p} type="button" onClick={() => { setActiveTab(p); setTagInput('') }} style={{
{HASHTAG_PLATFORMS.map(p => {
const isLocked = !isPro && proOnlyPlatforms.includes(p)
return (
<button key={p} type="button" onClick={() => { if (!isLocked) { setActiveTab(p); setTagInput('') } }} style={{
padding: '0.4rem 0.85rem', fontSize: '0.78rem', fontWeight: activeTab === p ? 700 : 400,
fontFamily: "'DM Sans', sans-serif", border: 'none', cursor: 'pointer',
backgroundColor: activeTab === p ? 'var(--surface)' : 'transparent',
color: activeTab === p ? 'var(--ink)' : 'var(--ink-muted)',
color: isLocked ? 'var(--border-strong)' : (activeTab === p ? 'var(--ink)' : 'var(--ink-muted)'),
borderBottom: activeTab === p ? '2px solid var(--accent)' : '2px solid transparent',
marginBottom: '-2px', textTransform: 'capitalize',
opacity: isLocked ? 0.5 : 1, cursor: isLocked ? 'not-allowed' : 'pointer',
}}>
{p}
{p}{isLocked ? ' 🔒' : ''}
</button>
))}
)
})}
</div>
<div style={{ display: 'flex', gap: '0.35rem', marginBottom: '0.5rem' }}>