Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
60a46a3
feat: Implement new database schema for real-time active sessions, ac…
amethystani Mar 16, 2026
c4946df
feat: add buildElevenLabsSystemPrompt utility for voice KB injection
amethystani Mar 16, 2026
c353d3b
feat: add elevenlabs-kb-sync Netlify function for agent KB document sync
amethystani Mar 16, 2026
6045675
fix: prioritize canonical env var names over VITE_ variants in kb-sync
amethystani Mar 16, 2026
a4aaa63
feat: sync documents to ElevenLabs agent KB after upload and delete
amethystani Mar 16, 2026
4bbf04e
feat: sync documents to ElevenLabs agent KB after upload and delete
amethystani Mar 16, 2026
553936d
feat: inject knowledge base system prompt and greeting into ElevenLab…
amethystani Mar 16, 2026
deb582a
fix: add knowledgeBase to handleStartCall useCallback deps to prevent…
amethystani Mar 16, 2026
7d18e4e
fix: add missing elevenLabsSystemPrompt.ts to worktree; remove VITE_ …
amethystani Mar 16, 2026
860c045
feat: ElevenLabs voice agent KB sync, system prompt injection, and EN…
amethystani Mar 16, 2026
80c1960
feat: fix webhooks, activity log, and embed iframe
amethystani Mar 16, 2026
724dfe5
feat: AI-generated rescue playbooks from call history analysis
amethystani Mar 16, 2026
4110eb9
fix: update ElevenLabs SDK and fix voice AI WebSocket disconnect
amethystani Mar 17, 2026
57cefac
fix: stop overriding ElevenLabs agent system prompt to restore voice AI
amethystani Mar 17, 2026
fe899a8
fix: switch voice AI from broken WebSocket path to ElevenLabs LiveKit…
amethystani Mar 17, 2026
f049d52
chore(deps): bump next from 15.3.5 to 16.1.7 in /blog
dependabot[bot] Mar 17, 2026
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
2 changes: 1 addition & 1 deletion blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"geist": "^1.4.2",
"lucide-react": "^0.525.0",
"motion": "^12.23.11",
"next": "15.3.5",
"next": "16.1.7",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
451 changes: 226 additions & 225 deletions blog/pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@
Referrer-Policy = "strict-origin-when-cross-origin"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' blob: data: https://app.termly.io https://www.googletagmanager.com https://checkout.razorpay.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://api.fontshare.com; font-src 'self' https://fonts.gstatic.com data: https://cdn.fontshare.com; img-src 'self' data: blob: https:; media-src 'self' data: blob: https:; connect-src 'self' https://xlzwfkgurrrspcdyqele.supabase.co wss://xlzwfkgurrrspcdyqele.supabase.co https://clerk-tts-production.up.railway.app wss://clerk-tts-production.up.railway.app https://ai.clerktree.com https://api.clerktree.com https://tts.clerktree.com https://app.termly.io https://backend-sq0u.onrender.com https://www.google-analytics.com https://*.sentry.io https://*.razorpay.com https://api.deapi.ai https://api.elevenlabs.io wss://*.elevenlabs.io https://api.pageindex.ai https://api.groq.com; worker-src 'self' blob: data:; frame-src 'self' https://api.razorpay.com https://checkout.razorpay.com; object-src 'none'; base-uri 'self'; form-action 'self';"

[[headers]]
for = "/user/*"
[headers.values]
X-Frame-Options = "ALLOWALL"
Content-Security-Policy = "frame-ancestors *; default-src 'self'; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://api.fontshare.com; font-src 'self' https://fonts.gstatic.com data: https://cdn.fontshare.com; img-src 'self' data: blob: https:; media-src 'self' data: blob: https:; connect-src 'self' https://xlzwfkgurrrspcdyqele.supabase.co wss://xlzwfkgurrrspcdyqele.supabase.co https://ai.clerktree.com https://tts.clerktree.com https://api.elevenlabs.io wss://*.elevenlabs.io https://api.groq.com https://backend-sq0u.onrender.com https://*.sentry.io; object-src 'none';"

[[headers]]
for = "/assets/*"
[headers.values]
Expand Down
228 changes: 228 additions & 0 deletions netlify/functions/elevenlabs-kb-sync.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import type { Context, Config } from "@netlify/functions";

const EL_BASE = "https://api.elevenlabs.io/v1";

const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
"Content-Type": "application/json",
};

export default async (req: Request, _context: Context) => {
// Handle CORS preflight
if (req.method === "OPTIONS") {
return new Response(null, { status: 204, headers: CORS_HEADERS });
}

if (req.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: CORS_HEADERS,
});
}

// Read env vars
const apiKey =
Netlify.env.get("ELEVENLABS_API_KEY") ||
Netlify.env.get("VITE_ELEVENLABS_API_KEY") ||
process.env.ELEVENLABS_API_KEY ||
process.env.VITE_ELEVENLABS_API_KEY;

if (!apiKey) {
return new Response(
JSON.stringify({ error: "ELEVENLABS_API_KEY not configured" }),
{ status: 500, headers: CORS_HEADERS }
);
}

const agentId =
Netlify.env.get("ELEVENLABS_AGENT_ID") ||
Netlify.env.get("VITE_ELEVENLABS_AGENT_ID") ||
process.env.ELEVENLABS_AGENT_ID ||
process.env.VITE_ELEVENLABS_AGENT_ID;

if (!agentId) {
return new Response(
JSON.stringify({
error:
"ELEVENLABS_AGENT_ID not configured. Set it as an env var.",
}),
{ status: 500, headers: CORS_HEADERS }
);
}

const supabaseUrl =
Netlify.env.get("VITE_SUPABASE_URL") ||
Netlify.env.get("SUPABASE_URL") ||
process.env.VITE_SUPABASE_URL ||
process.env.SUPABASE_URL;

if (!supabaseUrl) {
return new Response(
JSON.stringify({ error: "SUPABASE_URL not configured" }),
{ status: 500, headers: CORS_HEADERS }
);
}

// Never use VITE_ prefix for service role key — Vite would inline it into the client bundle
const supabaseServiceKey =
Netlify.env.get("SUPABASE_SERVICE_ROLE_KEY") ||
process.env.SUPABASE_SERVICE_ROLE_KEY;

if (!supabaseServiceKey) {
return new Response(
JSON.stringify({ error: "SUPABASE_SERVICE_ROLE_KEY not configured" }),
{ status: 500, headers: CORS_HEADERS }
);
}

// Parse request body
let kbId: string | undefined;
try {
const body = await req.json();
kbId = body.kbId;
} catch {
return new Response(
JSON.stringify({ error: "Invalid or missing JSON body" }),
{ status: 400, headers: CORS_HEADERS }
);
}

if (!kbId) {
return new Response(
JSON.stringify({ error: "Missing required field: kbId" }),
{ status: 400, headers: CORS_HEADERS }
);
}

try {
// Step 1: Fetch all chunks for this kbId from Supabase
const supabaseRes = await fetch(
`${supabaseUrl}/rest/v1/kb_documents?kb_id=eq.${encodeURIComponent(kbId)}&select=content,chunk_index&order=chunk_index.asc`,
{
headers: {
apikey: supabaseServiceKey,
Authorization: `Bearer ${supabaseServiceKey}`,
"Content-Type": "application/json",
},
}
);

if (!supabaseRes.ok) {
const errorText = await supabaseRes.text();
throw new Error(
`Supabase fetch failed (HTTP ${supabaseRes.status}): ${errorText}`
);
}

const chunks: { content: string; chunk_index: number }[] =
await supabaseRes.json();

// Step 2: Fetch current agent config to find existing KB item IDs
const agentRes = await fetch(`${EL_BASE}/convai/agents/${agentId}`, {
headers: { "xi-api-key": apiKey },
});

if (!agentRes.ok) {
const errorText = await agentRes.text();
throw new Error(
`Failed to fetch ElevenLabs agent config (HTTP ${agentRes.status}): ${errorText}`
);
}

const agentData = await agentRes.json();
const existingKbItems: { id: string }[] =
agentData?.conversation_config?.agent?.prompt?.knowledge_base ?? [];

// Step 3: Delete all existing KB items from the agent
for (const item of existingKbItems) {
const delRes = await fetch(
`${EL_BASE}/convai/agents/${agentId}/knowledge-base/${item.id}`,
{
method: "DELETE",
headers: { "xi-api-key": apiKey },
}
);
if (!delRes.ok) {
const errorText = await delRes.text();
throw new Error(
`Failed to delete KB item ${item.id} (HTTP ${delRes.status}): ${errorText}`
);
}
}

// Step 4: If no chunks, return early
if (chunks.length === 0) {
return new Response(
JSON.stringify({ ok: true, chunks_synced: 0 }),
{ status: 200, headers: CORS_HEADERS }
);
}

// Step 5: Combine all chunks into one text blob
const combinedText = chunks.map((c) => c.content).join("\n\n");

// Step 6: Upload combined text as a new KB document to ElevenLabs
const textBlob = new Blob([combinedText], { type: "text/plain" });
const formData = new FormData();
formData.append("file", textBlob, `kb-${kbId}.txt`);
formData.append("name", `kb-${kbId}`);

const uploadRes = await fetch(`${EL_BASE}/convai/knowledge-base`, {
method: "POST",
headers: { "xi-api-key": apiKey },
body: formData,
});

if (!uploadRes.ok) {
const errorText = await uploadRes.text();
throw new Error(
`Failed to upload KB document to ElevenLabs (HTTP ${uploadRes.status}): ${errorText}`
);
}

const uploadData = await uploadRes.json();
const kbDocId: string = uploadData.id;

// Step 7: Attach the new KB doc to the agent
const patchRes = await fetch(`${EL_BASE}/convai/agents/${agentId}`, {
method: "PATCH",
headers: {
"xi-api-key": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
conversation_config: {
agent: {
prompt: {
knowledge_base: [{ type: "file", id: kbDocId }],
},
},
},
}),
});

if (!patchRes.ok) {
const errorText = await patchRes.text();
throw new Error(
`Failed to attach KB document to ElevenLabs agent (HTTP ${patchRes.status}): ${errorText}`
);
}

return new Response(
JSON.stringify({ ok: true, chunks_synced: chunks.length, kb_doc_id: kbDocId }),
{ status: 200, headers: CORS_HEADERS }
);
} catch (error) {
console.error("elevenlabs-kb-sync error:", error);
return new Response(
JSON.stringify({ error: "Internal server error", details: String(error) }),
{ status: 500, headers: CORS_HEADERS }
);
}
};

export const config: Config = {
path: "/api/elevenlabs-kb-sync",
};
25 changes: 13 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"dependencies": {
"@11labs/client": "^0.0.4",
"@base-ui/react": "^1.2.0",
"@elevenlabs/react": "^0.14.1",
"@elevenlabs/client": "^0.15.2",
"@elevenlabs/react": "^0.14.3",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-separator": "^1.1.8",
Expand Down
4 changes: 4 additions & 0 deletions public/_headers
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://app.termly.io https://www.googletagmanager.com https://checkout.razorpay.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://api.fontshare.com; font-src 'self' https://fonts.gstatic.com data: https://cdn.fontshare.com; img-src 'self' data: blob: https:; media-src 'self' data: blob: https:; connect-src 'self' https://xlzwfkgurrrspcdyqele.supabase.co wss://xlzwfkgurrrspcdyqele.supabase.co https://clerk-tts-production.up.railway.app wss://clerk-tts-production.up.railway.app https://ai.clerktree.com https://tts.clerktree.com https://app.termly.io https://backend-sq0u.onrender.com https://www.google-analytics.com https://*.sentry.io https://*.razorpay.com; frame-src 'self' https://api.razorpay.com https://checkout.razorpay.com; object-src 'none'; base-uri 'self'; form-action 'self';

/user/*
X-Frame-Options: ALLOWALL
Content-Security-Policy: frame-ancestors *; default-src 'self'; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://api.fontshare.com; font-src 'self' https://fonts.gstatic.com data: https://cdn.fontshare.com; img-src 'self' data: blob: https:; media-src 'self' data: blob: https:; connect-src 'self' https://xlzwfkgurrrspcdyqele.supabase.co wss://xlzwfkgurrrspcdyqele.supabase.co https://ai.clerktree.com https://tts.clerktree.com https://api.elevenlabs.io wss://*.elevenlabs.io https://api.groq.com https://backend-sq0u.onrender.com https://*.sentry.io; object-src 'none';

/assets/*
Cache-Control: public, max-age=31536000, immutable

Expand Down
3 changes: 3 additions & 0 deletions src/components/DashboardViews/KeysView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Plus, Copy, Trash2, Key, Shield, Check, ArrowRight, ArrowLeft, X, Eye,
import { supabase } from '../../config/supabase';
import { useAuth } from '../../contexts/AuthContext';
import { useLanguage } from '../../contexts/LanguageContext';
import { logActivity } from '../../services/activityLogger';

interface ApiKey {
id: string;
Expand Down Expand Up @@ -130,6 +131,7 @@ export default function KeysView({ isDark = true, hasAccess = false }: { isDark?
setKeys([newKey, ...keys]);
setCreatedKeyToken(data.token);
setWizardStep('created');
logActivity({ event_type: 'api_keys', action: 'api_key_created', description: `API key "${newKeyName}" created`, metadata: { key_id: data.id, permissions: data.permissions } }).catch(() => {});
} else {
setCreateKeyError(result.error || 'Failed to create API key.');
}
Expand All @@ -151,6 +153,7 @@ export default function KeysView({ isDark = true, hasAccess = false }: { isDark?

if (res.ok) {
setKeys(keys.filter(k => k.id !== id));
logActivity({ event_type: 'api_keys', action: 'api_key_revoked', description: `API key revoked`, metadata: { key_id: id } }).catch(() => {});
}
} catch (err) {
console.error('Failed to delete API key:', err);
Expand Down
Loading
Loading