fix(showcase): block financial data from AI Q&A, fix FAB overflow, fix presenter slide mapping
Build pitch-deck / build-push-deploy (push) Successful in 1m47s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 41s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
Build pitch-deck / build-push-deploy (push) Successful in 1m47s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 41s
CI / test-python-voice (push) Successful in 32s
CI / test-bqas (push) Successful in 32s
AI Q&A: fetch is_showcase from DB; showcase sessions receive no financial/funding context and have an explicit LLM guard refusing to discuss investment details. FAQ context and financial slide IDs stripped from system prompt. FAB: flex layout so Fullscreen button is always visible regardless of panel height. Presenter: pass activeSlideOrder to usePresenterMode so buildSlideAudioPlan maps slideIdx → slideId from the filtered list, not the full SLIDE_ORDER. Progress calculation also filters to active scripts only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import pool from '@/lib/db'
|
import pool from '@/lib/db'
|
||||||
import { getSessionFromCookie } from '@/lib/auth'
|
import { getSessionFromCookie } from '@/lib/auth'
|
||||||
import { SLIDE_ORDER } from '@/lib/slide-order'
|
import { SLIDE_ORDER, SHOWCASE_HIDDEN_SLIDES } from '@/lib/slide-order'
|
||||||
|
|
||||||
const LITELLM_URL = process.env.LITELLM_URL || 'https://llm-dev.meghsakha.com'
|
const LITELLM_URL = process.env.LITELLM_URL || 'https://llm-dev.meghsakha.com'
|
||||||
const LITELLM_MODEL = process.env.LITELLM_MODEL || 'gpt-oss-120b'
|
const LITELLM_MODEL = process.env.LITELLM_MODEL || 'gpt-oss-120b'
|
||||||
@@ -115,6 +115,19 @@ KONKRETES BEISPIEL einer vollständigen Antwort:
|
|||||||
|
|
||||||
WICHTIG: Vergiss NIEMALS die Folgefragen! Sie sind PFLICHT.`
|
WICHTIG: Vergiss NIEMALS die Folgefragen! Sie sind PFLICHT.`
|
||||||
|
|
||||||
|
// Prepended to system content for showcase (customer demo) sessions
|
||||||
|
const SHOWCASE_GUARD = `## MODUS: PRODUKT-DEMO (KEIN INVESTOREN-PITCH)
|
||||||
|
Du bist im Showcase-Modus. Du beantwortest Fragen potenzieller Kunden über das Produkt — NICHT über Investitionsdetails.
|
||||||
|
|
||||||
|
ABSOLUTES VERBOT — niemals erwähnen oder beantworten:
|
||||||
|
- Finanzierungssumme, Investitionsbedarf, The Ask, Bewertung, Equity, Cap Table
|
||||||
|
- Finanzprognosen, Umsatzziele, Cashflow, Liquidität, Burn Rate
|
||||||
|
- Wandeldarlehen, SAFE, Gesellschafteranteile oder Kapitalstruktur
|
||||||
|
- Irgendwelche Zahlen aus dem Finanzplan oder Finanzierungsrunden
|
||||||
|
|
||||||
|
Wenn danach gefragt wird: "Für geschäftliche Details wenden Sie sich gerne direkt an unser Team."
|
||||||
|
Fokus: Produkt-Features, Compliance-Module, technische Architektur, Kundennutzen, Markt, Team.`
|
||||||
|
|
||||||
async function loadFpLiquiditaetSummary(scenarioName: string): Promise<string> {
|
async function loadFpLiquiditaetSummary(scenarioName: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
@@ -199,7 +212,7 @@ function extractMeta(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPitchContext(versionId?: string | null): Promise<PitchContextResult> {
|
async function loadPitchContext(versionId?: string | null, isShowcase = false): Promise<PitchContextResult> {
|
||||||
try {
|
try {
|
||||||
// Version-specific data path
|
// Version-specific data path
|
||||||
if (versionId) {
|
if (versionId) {
|
||||||
@@ -225,11 +238,11 @@ async function loadPitchContext(versionId?: string | null): Promise<PitchContext
|
|||||||
|
|
||||||
const versionName = vNameRes.rows[0]?.name ?? ''
|
const versionName = vNameRes.rows[0]?.name ?? ''
|
||||||
const meta = extractMeta(versionName, fmScenarios, funding, financials)
|
const meta = extractMeta(versionName, fmScenarios, funding, financials)
|
||||||
const fpSummary = meta.scenarioName ? await loadFpLiquiditaetSummary(meta.scenarioName) : ''
|
const fpSummary = (!isShowcase && meta.scenarioName) ? await loadFpLiquiditaetSummary(meta.scenarioName) : ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contextString: buildContextString(company, team, financials, market, products, funding, features, fpSummary),
|
contextString: buildContextString(company, team, financials, market, products, funding, features, fpSummary, isShowcase),
|
||||||
meta,
|
meta: isShowcase ? { ...DEFAULT_META, versionName } : meta,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,9 +262,9 @@ async function loadPitchContext(versionId?: string | null): Promise<PitchContext
|
|||||||
return {
|
return {
|
||||||
contextString: buildContextString(
|
contextString: buildContextString(
|
||||||
company.rows[0], team.rows, financials.rows, market.rows,
|
company.rows[0], team.rows, financials.rows, market.rows,
|
||||||
products.rows, funding.rows[0], features.rows, ''
|
products.rows, funding.rows[0], features.rows, '', isShowcase
|
||||||
),
|
),
|
||||||
meta,
|
meta: isShowcase ? DEFAULT_META : meta,
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
client.release()
|
client.release()
|
||||||
@@ -264,8 +277,19 @@ async function loadPitchContext(versionId?: string | null): Promise<PitchContext
|
|||||||
|
|
||||||
function buildContextString(
|
function buildContextString(
|
||||||
company: unknown, team: unknown, financials: unknown, market: unknown,
|
company: unknown, team: unknown, financials: unknown, market: unknown,
|
||||||
products: unknown, funding: unknown, features: unknown, fpSummary: string
|
products: unknown, funding: unknown, features: unknown, fpSummary: string,
|
||||||
|
omitFinancials = false
|
||||||
): string {
|
): string {
|
||||||
|
const finSection = omitFinancials ? '' : `
|
||||||
|
### Finanzprognosen (5-Jahres-Plan)
|
||||||
|
${JSON.stringify(financials, null, 2)}
|
||||||
|
`
|
||||||
|
const fundSection = omitFinancials ? '' : `
|
||||||
|
### Finanzierung
|
||||||
|
${JSON.stringify(funding, null, 2)}
|
||||||
|
`
|
||||||
|
const fpSection = omitFinancials ? '' : (fpSummary ? '\n' + fpSummary : '')
|
||||||
|
|
||||||
return `
|
return `
|
||||||
## Unternehmensdaten (für präzise Antworten nutzen)
|
## Unternehmensdaten (für präzise Antworten nutzen)
|
||||||
|
|
||||||
@@ -274,22 +298,16 @@ ${JSON.stringify(company, null, 2)}
|
|||||||
|
|
||||||
### Team
|
### Team
|
||||||
${JSON.stringify(team, null, 2)}
|
${JSON.stringify(team, null, 2)}
|
||||||
|
${finSection}
|
||||||
### Finanzprognosen (5-Jahres-Plan)
|
|
||||||
${JSON.stringify(financials, null, 2)}
|
|
||||||
|
|
||||||
### Markt (TAM/SAM/SOM)
|
### Markt (TAM/SAM/SOM)
|
||||||
${JSON.stringify(market, null, 2)}
|
${JSON.stringify(market, null, 2)}
|
||||||
|
|
||||||
### Produkte
|
### Produkte
|
||||||
${JSON.stringify(products, null, 2)}
|
${JSON.stringify(products, null, 2)}
|
||||||
|
${fundSection}
|
||||||
### Finanzierung
|
|
||||||
${JSON.stringify(funding, null, 2)}
|
|
||||||
|
|
||||||
### Differenzierende Features (nur bei ComplAI)
|
### Differenzierende Features (nur bei ComplAI)
|
||||||
${JSON.stringify(features, null, 2)}
|
${JSON.stringify(features, null, 2)}
|
||||||
${fpSummary ? '\n' + fpSummary : ''}
|
${fpSection}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,22 +321,24 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve investor's assigned version so the AI sees the correct scenario data
|
// Resolve investor's assigned version and showcase flag
|
||||||
let versionId: string | null = null
|
let versionId: string | null = null
|
||||||
|
let isShowcase = false
|
||||||
try {
|
try {
|
||||||
const session = await getSessionFromCookie()
|
const session = await getSessionFromCookie()
|
||||||
if (session?.sub) {
|
if (session?.sub) {
|
||||||
const inv = await pool.query(
|
const inv = await pool.query(
|
||||||
`SELECT assigned_version_id FROM pitch_investors WHERE id = $1`,
|
`SELECT assigned_version_id, is_showcase FROM pitch_investors WHERE id = $1`,
|
||||||
[session.sub]
|
[session.sub]
|
||||||
)
|
)
|
||||||
versionId = inv.rows[0]?.assigned_version_id ?? null
|
versionId = inv.rows[0]?.assigned_version_id ?? null
|
||||||
|
isShowcase = inv.rows[0]?.is_showcase === true
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal: fall back to base tables
|
// Non-fatal: fall back to base tables
|
||||||
}
|
}
|
||||||
|
|
||||||
const { contextString, meta } = await loadPitchContext(versionId)
|
const { contextString, meta } = await loadPitchContext(versionId, isShowcase)
|
||||||
|
|
||||||
// Build dynamic VERSIONS-ISOLATION and Kernbotschaft #9 from actual version data
|
// Build dynamic VERSIONS-ISOLATION and Kernbotschaft #9 from actual version data
|
||||||
const fmt = (n: number) => n.toLocaleString('de-DE')
|
const fmt = (n: number) => n.toLocaleString('de-DE')
|
||||||
@@ -340,40 +360,51 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
const dynamicFinanzplanKernbotschaft = `9. Finanzplan: "Gründung August 2026. Pre-Seed über ${fundingStr}. ${customersStr} und ${revM} Umsatz bis 2030. ${employeesStr}."`
|
const dynamicFinanzplanKernbotschaft = `9. Finanzplan: "Gründung August 2026. Pre-Seed über ${fundingStr}. ${customersStr} und ${revM} Umsatz bis 2030. ${employeesStr}."`
|
||||||
|
|
||||||
let systemContent = SYSTEM_PROMPT_PART1
|
let systemContent: string
|
||||||
|
if (isShowcase) {
|
||||||
|
// Showcase: product-only context, no financial details anywhere
|
||||||
|
systemContent = SHOWCASE_GUARD
|
||||||
|
+ '\n\n' + SYSTEM_PROMPT_PART1
|
||||||
|
+ SYSTEM_PROMPT_PART2
|
||||||
|
+ SYSTEM_PROMPT_PART3
|
||||||
|
} else {
|
||||||
|
systemContent = SYSTEM_PROMPT_PART1
|
||||||
+ '\n' + dynamicFinanzplanKernbotschaft
|
+ '\n' + dynamicFinanzplanKernbotschaft
|
||||||
+ SYSTEM_PROMPT_PART2
|
+ SYSTEM_PROMPT_PART2
|
||||||
+ '\n\n' + dynamicVersionIsolation
|
+ '\n\n' + dynamicVersionIsolation
|
||||||
+ SYSTEM_PROMPT_PART3
|
+ SYSTEM_PROMPT_PART3
|
||||||
|
}
|
||||||
|
|
||||||
if (contextString) {
|
if (contextString) {
|
||||||
systemContent += '\n' + contextString
|
systemContent += '\n' + contextString
|
||||||
}
|
}
|
||||||
|
|
||||||
// FAQ context: relevant pre-researched answers as basis for the LLM
|
// FAQ context — skip for showcase (may contain financial hints)
|
||||||
// IMPORTANT: FAQ entries contain hardcoded numbers written for specific scenarios.
|
if (!isShowcase && faqContext && typeof faqContext === 'string') {
|
||||||
// They are hints only — the version-specific Unternehmensdaten above always take precedence.
|
|
||||||
if (faqContext && typeof faqContext === 'string') {
|
|
||||||
systemContent += '\n' + faqContext
|
systemContent += '\n' + faqContext
|
||||||
systemContent += '\n\n## Versions-Datenvorrang (ABSOLUT VERBINDLICH)\nWenn die vorrecherchierten Antworten oben Zahlen, Beträge oder Details nennen, die von den "Unternehmensdaten" oder dem "Finanzplan-Liquidität" weiter oben abweichen, haben die Unternehmensdaten IMMER Vorrang. Die FAQ-Antworten sind allgemein formuliert und könnten veraltete oder szenario-fremde Zahlen enthalten. Nutze sie nur für Struktur und Formulierung — die konkreten Zahlen kommen ausschließlich aus den Unternehmensdaten dieses Investors.'
|
systemContent += '\n\n## Versions-Datenvorrang (ABSOLUT VERBINDLICH)\nWenn die vorrecherchierten Antworten oben Zahlen, Beträge oder Details nennen, die von den "Unternehmensdaten" oder dem "Finanzplan-Liquidität" weiter oben abweichen, haben die Unternehmensdaten IMMER Vorrang. Die FAQ-Antworten sind allgemein formuliert und könnten veraltete oder szenario-fremde Zahlen enthalten. Nutze sie nur für Struktur und Formulierung — die konkreten Zahlen kommen ausschließlich aus den Unternehmensdaten dieses Investors.'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slide context for contextual awareness
|
// Slide context for contextual awareness — filter hidden slides for showcase
|
||||||
|
const visibleSlideOrder = isShowcase
|
||||||
|
? SLIDE_ORDER.filter(id => !SHOWCASE_HIDDEN_SLIDES.has(id))
|
||||||
|
: SLIDE_ORDER
|
||||||
|
|
||||||
if (slideContext) {
|
if (slideContext) {
|
||||||
const visited: number[] = slideContext.visitedSlides || []
|
const visited: number[] = slideContext.visitedSlides || []
|
||||||
const currentSlideId = slideContext.currentSlide
|
const currentSlideId = slideContext.currentSlide
|
||||||
const currentSlideName = SLIDE_DISPLAY_NAMES[currentSlideId]?.[lang] || currentSlideId
|
const currentSlideName = SLIDE_DISPLAY_NAMES[currentSlideId]?.[lang] || currentSlideId
|
||||||
const notYetSeen = SLIDE_ORDER
|
const notYetSeen = visibleSlideOrder
|
||||||
.map((id, idx) => ({ id, idx, name: SLIDE_DISPLAY_NAMES[id]?.[lang] || id }))
|
.map((id, idx) => ({ id, idx, name: SLIDE_DISPLAY_NAMES[id]?.[lang] || id }))
|
||||||
.filter(s => !visited.includes(s.idx))
|
.filter(s => !visited.includes(s.idx))
|
||||||
.map(s => `${s.idx + 1}. ${s.name}`)
|
.map(s => `${s.idx + 1}. ${s.name}`)
|
||||||
|
|
||||||
systemContent += `\n\n## Slide-Kontext (WICHTIG für kontextuelle Antworten)
|
systemContent += `\n\n## Slide-Kontext (WICHTIG für kontextuelle Antworten)
|
||||||
- Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von ${slideCount})
|
- Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von ${visibleSlideOrder.length})
|
||||||
- Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_DISPLAY_NAMES[SLIDE_ORDER[i]]?.[lang] || SLIDE_ORDER[i]).filter(Boolean).join(', ')}
|
- Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_DISPLAY_NAMES[visibleSlideOrder[i]]?.[lang] || visibleSlideOrder[i]).filter(Boolean).join(', ')}
|
||||||
- Noch nicht gesehene Slides: ${notYetSeen.join(', ')}
|
- Noch nicht gesehene Slides: ${notYetSeen.join(', ')}
|
||||||
- Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Investor hat gerade erst den Pitch geöffnet' : 'Nein'}
|
- Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Besucher hat gerade erst die Präsentation geöffnet' : 'Nein'}
|
||||||
- Verfügbare Slide-IDs für [GOTO:id]: ${SLIDE_ORDER.join(', ')}
|
- Verfügbare Slide-IDs für [GOTO:id]: ${visibleSlideOrder.join(', ')}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,12 +76,12 @@ export default function NavigationFAB({
|
|||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="w-[300px] max-h-[80vh] rounded-2xl overflow-hidden
|
className="w-[300px] max-h-[80vh] flex flex-col rounded-2xl overflow-hidden
|
||||||
bg-black/80 backdrop-blur-xl border border-white/10
|
bg-black/80 backdrop-blur-xl border border-white/10
|
||||||
shadow-2xl shadow-black/50"
|
shadow-2xl shadow-black/50"
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
<div className="shrink-0 flex items-center justify-between px-4 py-3 border-b border-white/10">
|
||||||
<span className="text-sm font-semibold text-white">{i.nav.slides}</span>
|
<span className="text-sm font-semibold text-white">{i.nav.slides}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
@@ -93,7 +93,7 @@ export default function NavigationFAB({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Slide List */}
|
{/* Slide List */}
|
||||||
<div className="overflow-y-auto max-h-[55vh] py-2">
|
<div className="flex-1 overflow-y-auto py-2">
|
||||||
{activeSlideNames.map((name, idx) => {
|
{activeSlideNames.map((name, idx) => {
|
||||||
const isActive = idx === currentIndex
|
const isActive = idx === currentIndex
|
||||||
const isVisited = visitedSlides.has(idx)
|
const isVisited = visitedSlides.has(idx)
|
||||||
@@ -134,7 +134,7 @@ export default function NavigationFAB({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="border-t border-white/10 px-4 py-3 space-y-2">
|
<div className="shrink-0 border-t border-white/10 px-4 py-3 space-y-2">
|
||||||
{/* Language Toggle */}
|
{/* Language Toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={onToggleLanguage}
|
onClick={onToggleLanguage}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
|||||||
currentSlide: nav.currentIndex,
|
currentSlide: nav.currentIndex,
|
||||||
totalSlides: nav.totalSlides,
|
totalSlides: nav.totalSlides,
|
||||||
language: lang,
|
language: lang,
|
||||||
|
slideOrder: activeSlideOrder,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Audit tracking
|
// Audit tracking
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Language } from '../types'
|
|||||||
import { PresenterState, SlideScript } from '../presenter/types'
|
import { PresenterState, SlideScript } from '../presenter/types'
|
||||||
import { PRESENTER_SCRIPT } from '../presenter/presenter-script'
|
import { PRESENTER_SCRIPT } from '../presenter/presenter-script'
|
||||||
import { SLIDE_ORDER } from './useSlideNavigation'
|
import { SLIDE_ORDER } from './useSlideNavigation'
|
||||||
|
import { SlideId } from '../types'
|
||||||
|
|
||||||
interface UsePresenterModeConfig {
|
interface UsePresenterModeConfig {
|
||||||
goToSlide: (index: number) => void
|
goToSlide: (index: number) => void
|
||||||
@@ -12,6 +13,7 @@ interface UsePresenterModeConfig {
|
|||||||
totalSlides: number
|
totalSlides: number
|
||||||
language: Language
|
language: Language
|
||||||
ttsEnabled?: boolean
|
ttsEnabled?: boolean
|
||||||
|
slideOrder?: SlideId[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UsePresenterModeReturn {
|
interface UsePresenterModeReturn {
|
||||||
@@ -57,8 +59,8 @@ interface SlideAudioPlan {
|
|||||||
segments: AudioSegment[]
|
segments: AudioSegment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSlideAudioPlan(slideIdx: number, lang: Language): SlideAudioPlan | null {
|
function buildSlideAudioPlan(slideIdx: number, lang: Language, activeSlideOrder: SlideId[]): SlideAudioPlan | null {
|
||||||
const slideId = SLIDE_ORDER[slideIdx]
|
const slideId = activeSlideOrder[slideIdx]
|
||||||
const script = PRESENTER_SCRIPT.find(s => s.slideId === slideId)
|
const script = PRESENTER_SCRIPT.find(s => s.slideId === slideId)
|
||||||
if (!script || script.paragraphs.length === 0) return null
|
if (!script || script.paragraphs.length === 0) return null
|
||||||
|
|
||||||
@@ -121,7 +123,9 @@ export function usePresenterMode({
|
|||||||
totalSlides,
|
totalSlides,
|
||||||
language,
|
language,
|
||||||
ttsEnabled: initialTtsEnabled = true,
|
ttsEnabled: initialTtsEnabled = true,
|
||||||
|
slideOrder: slideOrderProp,
|
||||||
}: UsePresenterModeConfig): UsePresenterModeReturn {
|
}: UsePresenterModeConfig): UsePresenterModeReturn {
|
||||||
|
const activeSlideOrder = slideOrderProp ?? SLIDE_ORDER
|
||||||
const [state, setState] = useState<PresenterState>('idle')
|
const [state, setState] = useState<PresenterState>('idle')
|
||||||
const [currentParagraph, setCurrentParagraph] = useState(0)
|
const [currentParagraph, setCurrentParagraph] = useState(0)
|
||||||
const [displayText, setDisplayText] = useState('')
|
const [displayText, setDisplayText] = useState('')
|
||||||
@@ -193,7 +197,7 @@ export function usePresenterMode({
|
|||||||
playSlideRef.current = async (slideIdx: number) => {
|
playSlideRef.current = async (slideIdx: number) => {
|
||||||
if (stateRef.current !== 'presenting') return
|
if (stateRef.current !== 'presenting') return
|
||||||
|
|
||||||
const plan = buildSlideAudioPlan(slideIdx, language)
|
const plan = buildSlideAudioPlan(slideIdx, language, activeSlideOrder)
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
// No script for this slide — skip to next
|
// No script for this slide — skip to next
|
||||||
if (slideIdx < totalSlides - 1) {
|
if (slideIdx < totalSlides - 1) {
|
||||||
@@ -216,7 +220,7 @@ export function usePresenterMode({
|
|||||||
|
|
||||||
// Pre-fetch next slide's audio in background
|
// Pre-fetch next slide's audio in background
|
||||||
if (slideIdx < totalSlides - 1) {
|
if (slideIdx < totalSlides - 1) {
|
||||||
const nextPlan = buildSlideAudioPlan(slideIdx + 1, language)
|
const nextPlan = buildSlideAudioPlan(slideIdx + 1, language, activeSlideOrder)
|
||||||
if (nextPlan) fetchAudio(nextPlan.fullText, language).catch(() => {})
|
if (nextPlan) fetchAudio(nextPlan.fullText, language).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +333,7 @@ export function usePresenterMode({
|
|||||||
setIsSpeaking(false)
|
setIsSpeaking(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [language, totalSlides, goToSlide, ttsAvailable, ttsEnabled])
|
}, [language, totalSlides, goToSlide, ttsAvailable, ttsEnabled, activeSlideOrder])
|
||||||
|
|
||||||
const start = useCallback(() => {
|
const start = useCallback(() => {
|
||||||
unlockAudio()
|
unlockAudio()
|
||||||
@@ -410,14 +414,18 @@ export function usePresenterMode({
|
|||||||
}
|
}
|
||||||
}, [unlockAudio, start, stop])
|
}, [unlockAudio, start, stop])
|
||||||
|
|
||||||
// Calculate overall progress
|
// Calculate overall progress against the active slide order's scripts
|
||||||
const progress = (() => {
|
const progress = (() => {
|
||||||
if (state === 'idle') return 0
|
if (state === 'idle') return 0
|
||||||
const totalScripts = PRESENTER_SCRIPT.length
|
const currentSlideId = activeSlideOrder[currentSlide]
|
||||||
const currentScriptIdx = PRESENTER_SCRIPT.findIndex(s => s.slideId === SLIDE_ORDER[currentSlide])
|
const activeScripts = activeSlideOrder
|
||||||
|
.map(id => PRESENTER_SCRIPT.find(s => s.slideId === id))
|
||||||
|
.filter(Boolean) as typeof PRESENTER_SCRIPT
|
||||||
|
const totalScripts = activeScripts.length || 1
|
||||||
|
const currentScriptIdx = activeScripts.findIndex(s => s.slideId === currentSlideId)
|
||||||
if (currentScriptIdx < 0) return (currentSlide / totalSlides) * 100
|
if (currentScriptIdx < 0) return (currentSlide / totalSlides) * 100
|
||||||
|
|
||||||
const script = PRESENTER_SCRIPT[currentScriptIdx]
|
const script = activeScripts[currentScriptIdx]
|
||||||
const slideProgress = script.paragraphs.length > 0
|
const slideProgress = script.paragraphs.length > 0
|
||||||
? currentParagraph / script.paragraphs.length
|
? currentParagraph / script.paragraphs.length
|
||||||
: 0
|
: 0
|
||||||
|
|||||||
Reference in New Issue
Block a user