feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux legacy, usage multi-features, hints de coût, anti-flash thème et hydratation admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
This commit is contained in:
@@ -229,6 +229,9 @@ export function ContextualAIChat({
|
||||
const [slideTheme, setSlideTheme] = useState('auto')
|
||||
const [slideStyle, setSlideStyle] = useState('professional')
|
||||
const [slideTemplate, setSlideTemplate] = useState('auto')
|
||||
const [slidePurpose, setSlidePurpose] = useState('auto')
|
||||
const [slideAudience, setSlideAudience] = useState('auto')
|
||||
const [slideCount, setSlideCount] = useState('auto')
|
||||
const [diagramType, setDiagramType] = useState('logic_flow')
|
||||
const [diagramStyle, setDiagramStyle] = useState('polished')
|
||||
const [diagramEmbedLoading, setDiagramEmbedLoading] = useState(false)
|
||||
@@ -444,6 +447,12 @@ export function ContextualAIChat({
|
||||
style: type === 'slides' ? slideStyle : diagramStyle,
|
||||
language: language === 'fr' ? 'French' : 'English',
|
||||
template: type === 'slides' ? slideTemplate : undefined,
|
||||
purpose: type === 'slides' ? slidePurpose : undefined,
|
||||
audience: type === 'slides' ? slideAudience : undefined,
|
||||
slideCount:
|
||||
type === 'slides' && slideCount !== 'auto'
|
||||
? parseInt(slideCount, 10)
|
||||
: undefined,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
@@ -1105,16 +1114,66 @@ export function ContextualAIChat({
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.template')}</span>
|
||||
<select value={slideTemplate} onChange={e => setSlideTemplate(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="auto">{t('ai.generate.templateAuto')}</option>
|
||||
<option value="board-update">{t('ai.generate.templateBoard')}</option>
|
||||
<option value="project-status">{t('ai.generate.templateProject')}</option>
|
||||
<option value="strategy-review">{t('ai.generate.templateStrategy')}</option>
|
||||
<option value="quarterly-results">{t('ai.generate.templateQuarterly')}</option>
|
||||
</select>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.purpose') || 'Type de deck'}</span>
|
||||
<select value={slidePurpose} onChange={e => setSlidePurpose(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="auto">{t('ai.generate.purposeAuto') || 'Auto (selon la note)'}</option>
|
||||
<option value="course">{t('ai.generate.purposeCourse') || 'Cours / leçon'}</option>
|
||||
<option value="board">{t('ai.generate.purposeBoard') || 'Comité / board'}</option>
|
||||
<option value="project">{t('ai.generate.purposeProject') || 'Suivi projet'}</option>
|
||||
<option value="strategy">{t('ai.generate.purposeStrategy') || 'Stratégie'}</option>
|
||||
<option value="pitch">{t('ai.generate.purposePitch') || 'Pitch'}</option>
|
||||
<option value="summary">{t('ai.generate.purposeSummary') || 'Synthèse courte'}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.audience') || 'Audience'}</span>
|
||||
<select value={slideAudience} onChange={e => setSlideAudience(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="auto">{t('ai.generate.audienceAuto') || 'Auto'}</option>
|
||||
<option value="student">{t('ai.generate.audienceStudent') || 'Étudiants'}</option>
|
||||
<option value="board">{t('ai.generate.audienceBoard') || 'Direction'}</option>
|
||||
<option value="team">{t('ai.generate.audienceTeam') || 'Équipe'}</option>
|
||||
<option value="client">{t('ai.generate.audienceClient') || 'Client'}</option>
|
||||
<option value="general">{t('ai.generate.audienceGeneral') || 'Grand public'}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.slideCount') || 'Nb de slides'}</span>
|
||||
<select value={slideCount} onChange={e => setSlideCount(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="auto">{t('ai.generate.slideCountAuto') || 'Auto (selon longueur)'}</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<span className="text-[8px] uppercase tracking-[0.2em] font-bold text-foreground/40 px-1">{t('ai.generate.template')}</span>
|
||||
<select value={slideTemplate} onChange={e => setSlideTemplate(e.target.value)} className="w-full bg-card/60 border border-border rounded-lg px-2 py-2 text-[10px] outline-none focus:ring-1 ring-brand-accent/10 transition-all cursor-pointer text-foreground">
|
||||
<option value="auto">{t('ai.generate.templateAuto')}</option>
|
||||
<option value="board-update">{t('ai.generate.templateBoard')}</option>
|
||||
<option value="project-status">{t('ai.generate.templateProject')}</option>
|
||||
<option value="strategy-review">{t('ai.generate.templateStrategy')}</option>
|
||||
<option value="quarterly-results">{t('ai.generate.templateQuarterly')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const n = slideCount === 'auto' ? 6 : Math.min(8, Math.max(3, parseInt(slideCount, 10) || 6))
|
||||
const creditCost = 1 + n
|
||||
return (
|
||||
<p className="text-[10px] text-muted-foreground px-0.5 leading-relaxed">
|
||||
{t('ai.generate.creditCostEstimate', {
|
||||
cost: String(creditCost),
|
||||
slides: String(n),
|
||||
})}
|
||||
</p>
|
||||
)
|
||||
})()}
|
||||
<button onClick={() => handleGenerate('slides')} disabled={!!generateLoading} className="w-full py-3.5 bg-brand-accent text-white rounded-xl text-[10px] font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all shadow-lg shadow-brand-accent/20 uppercase tracking-[0.2em] disabled:opacity-50">
|
||||
{generateLoading === 'slides' ? <Loader2 size={14} className="animate-spin" /> : <><Presentation size={14} className="opacity-80" /> {t('ai.generating')}</>}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user