Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
from slowapi.errors import RateLimitExceeded

from supabase_store import GuideStore
from routes import guides, webhook, demos
from routes import client_config, guides, webhook, demos

# Load environment variables
_BACKEND_ENV_PATH = os.path.join(os.path.dirname(__file__), ".env")
load_dotenv(_BACKEND_ENV_PATH)
load_dotenv()

# Configure logging
Expand Down Expand Up @@ -87,6 +89,7 @@
app.include_router(guides.router)
app.include_router(webhook.router)
app.include_router(demos.router)
app.include_router(client_config.router)


# ========================================
Expand Down
29 changes: 29 additions & 0 deletions backend/routes/client_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Client-safe configuration endpoints."""

import os

from fastapi import APIRouter

router = APIRouter()


@router.get("/client-config")
async def get_client_config():
"""Return non-secret config needed by the browser client."""
public_key = os.getenv("VAPI_PUBLIC_KEY", "").strip()
assistant_id = os.getenv("VAPI_ASSISTANT_ID", "").strip()

missing = []
if not public_key:
missing.append("VAPI_PUBLIC_KEY")
if not assistant_id:
missing.append("VAPI_ASSISTANT_ID")

return {
"voice": {
"enabled": not missing,
"public_key": public_key,
"assistant_id": assistant_id,
},
"missing": missing,
}
4 changes: 2 additions & 2 deletions frontend/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet">
<script type="module" crossorigin src="/assets/index-BskSWJzD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-s51mC5ZB.css">
<script type="module" crossorigin src="/assets/index-Bs5Pj9CB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BN4xpms2.css">
</head>

<body>
Expand Down
21 changes: 17 additions & 4 deletions frontend/src/InterviewView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,16 @@ function mapErrorMessage(raw) {
}

export default function InterviewView({ onInterviewComplete, onBack }) {
const { callStatus, voiceState, transcript, formattedTranscript, error, startCall, endCall } = useVapi();
const {
callStatus,
voiceState,
transcript,
formattedTranscript,
error,
configStatus,
startCall,
endCall,
} = useVapi();

// Check if user said enough (both character and word minimums)
const userText = transcript
Expand Down Expand Up @@ -142,9 +151,13 @@ export default function InterviewView({ onInterviewComplete, onBack }) {

{/* Call controls */}
{callStatus === 'idle' && (
<button onClick={startCall} className="btn-primary flex items-center gap-2 !py-2 !px-5 !text-sm">
<Phone size={14} />
Start
<button
onClick={startCall}
disabled={configStatus === 'loading'}
className="btn-primary flex items-center gap-2 !py-2 !px-5 !text-sm disabled:opacity-60 disabled:cursor-not-allowed"
>
<Phone size={14} className={configStatus === 'loading' ? 'animate-pulse' : ''} />
{configStatus === 'loading' ? 'Loading Voice' : 'Start'}
</button>
)}
{callStatus === 'connecting' && (
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/components/MarkdownRenderer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,24 @@ function ClickableHeading({ level, id, children, ...props }) {
);
}

function stripChecklistMarkers(markdown) {
return markdown
.split(/(```[\s\S]*?```)/g)
.map((block) => {
if (block.startsWith('```')) return block;
return block
.replace(/^(\s*[-*+]\s+)\[\s*\]\s+/gm, '$1')
.replace(/^(\s*\d+\.\s+)\[\s*\]\s+/gm, '$1')
.replace(/^(\s*)\[\s*\]\s+/gm, '$1');
})
.join('');
}

export default function MarkdownRenderer({ content }) {
if (!content) return null;
const normalizedContent = stripChecklistMarkers(content);
return (
<div className="prose prose-sm prose-dark max-w-none">
<div className="prose prose-dark max-w-none text-[18px] leading-[1.7]">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
Expand Down Expand Up @@ -120,7 +134,7 @@ export default function MarkdownRenderer({ content }) {
},
}}
>
{content}
{normalizedContent}
</ReactMarkdown>
</div>
);
Expand Down
Loading