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 pool from '@/lib/db'
|
||||
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_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.`
|
||||
|
||||
// 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> {
|
||||
try {
|
||||
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 {
|
||||
// Version-specific data path
|
||||
if (versionId) {
|
||||
@@ -225,11 +238,11 @@ async function loadPitchContext(versionId?: string | null): Promise<PitchContext
|
||||
|
||||
const versionName = vNameRes.rows[0]?.name ?? ''
|
||||
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 {
|
||||
contextString: buildContextString(company, team, financials, market, products, funding, features, fpSummary),
|
||||
meta,
|
||||
contextString: buildContextString(company, team, financials, market, products, funding, features, fpSummary, isShowcase),
|
||||
meta: isShowcase ? { ...DEFAULT_META, versionName } : meta,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,9 +262,9 @@ async function loadPitchContext(versionId?: string | null): Promise<PitchContext
|
||||
return {
|
||||
contextString: buildContextString(
|
||||
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 {
|
||||
client.release()
|
||||
@@ -264,8 +277,19 @@ async function loadPitchContext(versionId?: string | null): Promise<PitchContext
|
||||
|
||||
function buildContextString(
|
||||
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 {
|
||||
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 `
|
||||
## Unternehmensdaten (für präzise Antworten nutzen)
|
||||
|
||||
@@ -274,22 +298,16 @@ ${JSON.stringify(company, null, 2)}
|
||||
|
||||
### Team
|
||||
${JSON.stringify(team, null, 2)}
|
||||
|
||||
### Finanzprognosen (5-Jahres-Plan)
|
||||
${JSON.stringify(financials, null, 2)}
|
||||
|
||||
${finSection}
|
||||
### Markt (TAM/SAM/SOM)
|
||||
${JSON.stringify(market, null, 2)}
|
||||
|
||||
### Produkte
|
||||
${JSON.stringify(products, null, 2)}
|
||||
|
||||
### Finanzierung
|
||||
${JSON.stringify(funding, null, 2)}
|
||||
|
||||
${fundSection}
|
||||
### Differenzierende Features (nur bei ComplAI)
|
||||
${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 })
|
||||
}
|
||||
|
||||
// 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 isShowcase = false
|
||||
try {
|
||||
const session = await getSessionFromCookie()
|
||||
if (session?.sub) {
|
||||
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]
|
||||
)
|
||||
versionId = inv.rows[0]?.assigned_version_id ?? null
|
||||
isShowcase = inv.rows[0]?.is_showcase === true
|
||||
}
|
||||
} catch {
|
||||
// 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
|
||||
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}."`
|
||||
|
||||
let systemContent = SYSTEM_PROMPT_PART1
|
||||
+ '\n' + dynamicFinanzplanKernbotschaft
|
||||
+ SYSTEM_PROMPT_PART2
|
||||
+ '\n\n' + dynamicVersionIsolation
|
||||
+ SYSTEM_PROMPT_PART3
|
||||
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
|
||||
+ SYSTEM_PROMPT_PART2
|
||||
+ '\n\n' + dynamicVersionIsolation
|
||||
+ SYSTEM_PROMPT_PART3
|
||||
}
|
||||
|
||||
if (contextString) {
|
||||
systemContent += '\n' + contextString
|
||||
}
|
||||
|
||||
// FAQ context: relevant pre-researched answers as basis for the LLM
|
||||
// IMPORTANT: FAQ entries contain hardcoded numbers written for specific scenarios.
|
||||
// They are hints only — the version-specific Unternehmensdaten above always take precedence.
|
||||
if (faqContext && typeof faqContext === 'string') {
|
||||
// FAQ context — skip for showcase (may contain financial hints)
|
||||
if (!isShowcase && faqContext && typeof faqContext === 'string') {
|
||||
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.'
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const visited: number[] = slideContext.visitedSlides || []
|
||||
const currentSlideId = slideContext.currentSlide
|
||||
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 }))
|
||||
.filter(s => !visited.includes(s.idx))
|
||||
.map(s => `${s.idx + 1}. ${s.name}`)
|
||||
|
||||
systemContent += `\n\n## Slide-Kontext (WICHTIG für kontextuelle Antworten)
|
||||
- Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von ${slideCount})
|
||||
- Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_DISPLAY_NAMES[SLIDE_ORDER[i]]?.[lang] || SLIDE_ORDER[i]).filter(Boolean).join(', ')}
|
||||
- Aktuelle Slide: "${currentSlideName}" (Nr. ${slideContext.currentIndex + 1} von ${visibleSlideOrder.length})
|
||||
- Bereits besuchte Slides: ${visited.map((i: number) => SLIDE_DISPLAY_NAMES[visibleSlideOrder[i]]?.[lang] || visibleSlideOrder[i]).filter(Boolean).join(', ')}
|
||||
- Noch nicht gesehene Slides: ${notYetSeen.join(', ')}
|
||||
- Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Investor hat gerade erst den Pitch geöffnet' : 'Nein'}
|
||||
- Verfügbare Slide-IDs für [GOTO:id]: ${SLIDE_ORDER.join(', ')}
|
||||
- Ist Erstbesuch: ${visited.length <= 1 ? 'JA — Besucher hat gerade erst die Präsentation geöffnet' : 'Nein'}
|
||||
- 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 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: 20 }}
|
||||
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
|
||||
shadow-2xl shadow-black/50"
|
||||
>
|
||||
{/* 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>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
@@ -93,7 +93,7 @@ export default function NavigationFAB({
|
||||
</div>
|
||||
|
||||
{/* 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) => {
|
||||
const isActive = idx === currentIndex
|
||||
const isVisited = visitedSlides.has(idx)
|
||||
@@ -134,7 +134,7 @@ export default function NavigationFAB({
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<button
|
||||
onClick={onToggleLanguage}
|
||||
|
||||
@@ -105,6 +105,7 @@ export default function PitchDeck({ lang, onToggleLanguage, investor, onLogout,
|
||||
currentSlide: nav.currentIndex,
|
||||
totalSlides: nav.totalSlides,
|
||||
language: lang,
|
||||
slideOrder: activeSlideOrder,
|
||||
})
|
||||
|
||||
// Audit tracking
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Language } from '../types'
|
||||
import { PresenterState, SlideScript } from '../presenter/types'
|
||||
import { PRESENTER_SCRIPT } from '../presenter/presenter-script'
|
||||
import { SLIDE_ORDER } from './useSlideNavigation'
|
||||
import { SlideId } from '../types'
|
||||
|
||||
interface UsePresenterModeConfig {
|
||||
goToSlide: (index: number) => void
|
||||
@@ -12,6 +13,7 @@ interface UsePresenterModeConfig {
|
||||
totalSlides: number
|
||||
language: Language
|
||||
ttsEnabled?: boolean
|
||||
slideOrder?: SlideId[]
|
||||
}
|
||||
|
||||
interface UsePresenterModeReturn {
|
||||
@@ -57,8 +59,8 @@ interface SlideAudioPlan {
|
||||
segments: AudioSegment[]
|
||||
}
|
||||
|
||||
function buildSlideAudioPlan(slideIdx: number, lang: Language): SlideAudioPlan | null {
|
||||
const slideId = SLIDE_ORDER[slideIdx]
|
||||
function buildSlideAudioPlan(slideIdx: number, lang: Language, activeSlideOrder: SlideId[]): SlideAudioPlan | null {
|
||||
const slideId = activeSlideOrder[slideIdx]
|
||||
const script = PRESENTER_SCRIPT.find(s => s.slideId === slideId)
|
||||
if (!script || script.paragraphs.length === 0) return null
|
||||
|
||||
@@ -121,7 +123,9 @@ export function usePresenterMode({
|
||||
totalSlides,
|
||||
language,
|
||||
ttsEnabled: initialTtsEnabled = true,
|
||||
slideOrder: slideOrderProp,
|
||||
}: UsePresenterModeConfig): UsePresenterModeReturn {
|
||||
const activeSlideOrder = slideOrderProp ?? SLIDE_ORDER
|
||||
const [state, setState] = useState<PresenterState>('idle')
|
||||
const [currentParagraph, setCurrentParagraph] = useState(0)
|
||||
const [displayText, setDisplayText] = useState('')
|
||||
@@ -193,7 +197,7 @@ export function usePresenterMode({
|
||||
playSlideRef.current = async (slideIdx: number) => {
|
||||
if (stateRef.current !== 'presenting') return
|
||||
|
||||
const plan = buildSlideAudioPlan(slideIdx, language)
|
||||
const plan = buildSlideAudioPlan(slideIdx, language, activeSlideOrder)
|
||||
if (!plan) {
|
||||
// No script for this slide — skip to next
|
||||
if (slideIdx < totalSlides - 1) {
|
||||
@@ -216,7 +220,7 @@ export function usePresenterMode({
|
||||
|
||||
// Pre-fetch next slide's audio in background
|
||||
if (slideIdx < totalSlides - 1) {
|
||||
const nextPlan = buildSlideAudioPlan(slideIdx + 1, language)
|
||||
const nextPlan = buildSlideAudioPlan(slideIdx + 1, language, activeSlideOrder)
|
||||
if (nextPlan) fetchAudio(nextPlan.fullText, language).catch(() => {})
|
||||
}
|
||||
|
||||
@@ -329,7 +333,7 @@ export function usePresenterMode({
|
||||
setIsSpeaking(false)
|
||||
}
|
||||
}
|
||||
}, [language, totalSlides, goToSlide, ttsAvailable, ttsEnabled])
|
||||
}, [language, totalSlides, goToSlide, ttsAvailable, ttsEnabled, activeSlideOrder])
|
||||
|
||||
const start = useCallback(() => {
|
||||
unlockAudio()
|
||||
@@ -410,14 +414,18 @@ export function usePresenterMode({
|
||||
}
|
||||
}, [unlockAudio, start, stop])
|
||||
|
||||
// Calculate overall progress
|
||||
// Calculate overall progress against the active slide order's scripts
|
||||
const progress = (() => {
|
||||
if (state === 'idle') return 0
|
||||
const totalScripts = PRESENTER_SCRIPT.length
|
||||
const currentScriptIdx = PRESENTER_SCRIPT.findIndex(s => s.slideId === SLIDE_ORDER[currentSlide])
|
||||
const currentSlideId = activeSlideOrder[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
|
||||
|
||||
const script = PRESENTER_SCRIPT[currentScriptIdx]
|
||||
const script = activeScripts[currentScriptIdx]
|
||||
const slideProgress = script.paragraphs.length > 0
|
||||
? currentParagraph / script.paragraphs.length
|
||||
: 0
|
||||
|
||||
Reference in New Issue
Block a user