Skip to content
Merged
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
8 changes: 8 additions & 0 deletions ai-pdf-chatbot/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ INSFORGE_JWT_SECRET=replace-with-output-of-cli-secrets-get-JWT_SECRET
# crashing. Embeddings + chat still route through InsForge AI without
# this key.
OPENAI_API_KEY=

# ─── PostHog Analytics (optional) ──────────────────────────────────────
# Visitor / event analytics. Leave blank to disable; the app still
# working, no events are sent. Get a project token from
# https://us.posthog.com/project/<your-id>/settings/project (or your
# self-hosted instance).
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN=
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
16 changes: 16 additions & 0 deletions ai-pdf-chatbot/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { retrieveForQuestion, toCitations } from '@/lib/rag/retrieve';
import { RAG_SYSTEM_PROMPT, buildUserPrompt } from '@/lib/ai/prompts';
import { CHAT_MODEL } from '@/lib/ai/constants';
import { encodeNdjson } from '@/lib/stream/ndjson';
import { getPostHogClient } from '@/lib/posthog-server';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -99,6 +100,21 @@ export async function POST(req: Request) {
const resolvedChatId = chatId;
const inputText = body.input.trim();
const docIds = body.documentIds;
const isNewChat = !body.chatId;

const posthog = getPostHogClient();
posthog.capture({
distinctId: ownerId,
event: 'chat_message_sent',
properties: {
chat_id: resolvedChatId,
workspace_id: workspaceId,
is_new_chat: isNewChat,
},
});
// Analytics flush failure must not surface as a 500 for the user;
// the write above already succeeded. Swallow the error.
await posthog.shutdown().catch(() => undefined);

const stream = new ReadableStream<Uint8Array>({
async start(controller) {
Expand Down
18 changes: 18 additions & 0 deletions ai-pdf-chatbot/app/api/documents/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { createInsforgeServerClient } from '@/lib/insforge';
import { getCurrentAuthState } from '@/lib/auth-state';
import { ingestPdf } from '@/lib/rag/ingest';
import { getPostHogClient } from '@/lib/posthog-server';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -86,6 +87,23 @@ export async function POST(req: Request) {
buffer,
});

const posthog = getPostHogClient();
posthog.capture({
distinctId: auth.viewer.id,
event: 'document_uploaded',
properties: {
document_id: doc.id,
file_name: file.name,
file_size_bytes: file.size,
workspace_id: workspaceId,
ingest_status: ingestResult.status,
chunk_count: ingestResult.status === 'ready' ? ingestResult.chunkCount : 0,
},
});
// Analytics flush failure must not surface as a 500 for the user;
// the write above already succeeded. Swallow the error.
await posthog.shutdown().catch(() => undefined);

return NextResponse.json({
document: { id: doc.id, file_name: file.name, file_size: file.size, status: ingestResult.status },
ingest: ingestResult,
Expand Down
17 changes: 17 additions & 0 deletions ai-pdf-chatbot/app/api/flashcards/[id]/grade/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { createInsforgeServerClient } from '@/lib/insforge';
import { getCurrentAuthState } from '@/lib/auth-state';
import { schedule, type Grade } from '@/lib/srs/schedule';
import { getPostHogClient } from '@/lib/posthog-server';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -53,6 +54,22 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
.eq('id', id);

if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 });

const posthog = getPostHogClient();
posthog.capture({
distinctId: auth.viewer.id ?? 'anonymous',
event: 'flashcard_graded',
properties: {
card_id: id,
grade,
next_interval_days: next.interval_days,
reps: next.reps,
},
});
// Analytics flush failure must not surface as a 500 for the user;
// the write above already succeeded. Swallow the error.
await posthog.shutdown().catch(() => undefined);

return NextResponse.json({
ok: true,
next: {
Expand Down
11 changes: 11 additions & 0 deletions ai-pdf-chatbot/app/api/workspaces/[id]/audio/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createInsforgeServerClient } from '@/lib/insforge';
import { getCurrentAuthState } from '@/lib/auth-state';
import { generateAudioScript } from '@/lib/ai/audio-script';
import { synthesizeScript } from '@/lib/audio/tts';
import { getPostHogClient } from '@/lib/posthog-server';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -98,5 +99,15 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
.eq('id', id);
if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 });

const posthog = getPostHogClient();
posthog.capture({
distinctId: auth.viewer.id,
event: 'audio_overview_generated',
properties: { workspace_id: id, workspace_name: ws.name, document_count: docs.length, script_turn_count: script.length },
});
// Analytics flush failure must not surface as a 500 for the user;
// the write above already succeeded. Swallow the error.
await posthog.shutdown().catch(() => undefined);

return NextResponse.json({ audio_url: audioUrl, script, generated_at: now });
}
11 changes: 11 additions & 0 deletions ai-pdf-chatbot/app/api/workspaces/[id]/mindmap/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { createInsforgeServerClient } from '@/lib/insforge';
import { getCurrentAuthState } from '@/lib/auth-state';
import { UTILITY_MODEL } from '@/lib/ai/constants';
import { getPostHogClient } from '@/lib/posthog-server';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -90,5 +91,15 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
.eq('id', id);
if (upd.error) return NextResponse.json({ error: upd.error.message }, { status: 500 });

const posthog = getPostHogClient();
posthog.capture({
distinctId: auth.viewer.id ?? 'anonymous',
event: 'mindmap_generated',
properties: { workspace_id: id, workspace_name: ws.name, document_count: docs.length },
});
// Analytics flush failure must not surface as a 500 for the user;
// the write above already succeeded. Swallow the error.
await posthog.shutdown().catch(() => undefined);

return NextResponse.json({ markdown, generated_at: now });
}
13 changes: 13 additions & 0 deletions ai-pdf-chatbot/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { createInsforgeServerClient } from '@/lib/insforge';
import { getCurrentAuthState } from '@/lib/auth-state';
import { getPostHogClient } from '@/lib/posthog-server';

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -48,5 +49,17 @@ export async function POST(req: Request) {
.single();

if (error || !data) return NextResponse.json({ error: error?.message ?? 'Insert failed' }, { status: 500 });

const posthog = getPostHogClient();
const ws = data as { id: string };
posthog.capture({
distinctId: auth.viewer.id,
event: 'workspace_created',
properties: { workspace_id: ws.id, workspace_name: name },
});
// Analytics flush failure must not surface as a 500 for the user;
// the write above already succeeded. Swallow the error.
await posthog.shutdown().catch(() => undefined);

return NextResponse.json({ workspace: data });
}
5 changes: 4 additions & 1 deletion ai-pdf-chatbot/components/flashcards-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Sparkles, X } from 'luci
import { useCallback, useEffect, useState } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import posthog from 'posthog-js';

type Card = { id: string; question: string; answer: string; sort_order: number };

Expand Down Expand Up @@ -49,9 +50,11 @@ export function FlashcardsModal({
toast.error(data.error ?? 'Failed to generate flashcards');
return;
}
setCards(data.flashcards ?? []);
const cards = data.flashcards ?? [];
setCards(cards);
setIndex(0);
setRevealed(false);
posthog.capture('flashcards_generated', { document_id: documentId, card_count: cards.length });
} catch {
toast.error('Failed to generate flashcards');
} finally {
Expand Down
6 changes: 6 additions & 0 deletions ai-pdf-chatbot/components/share-chat-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Check, Copy, Link2, Loader2, X } from 'lucide-react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import posthog from 'posthog-js';

export function ShareChatButton({
chatId,
Expand Down Expand Up @@ -56,6 +57,11 @@ export function ShareChatButton({
const next = data.chat?.share_token ?? null;
setShareToken(next);
onShareTokenChange?.(next);
if (enable) {
posthog.capture('chat_share_link_created', { chat_id: chatId });
} else {
posthog.capture('chat_share_link_revoked', { chat_id: chatId });
}
toast.success(enable ? 'Share link created' : 'Share link revoked');
} catch {
toast.error('Failed to update share state');
Expand Down
7 changes: 6 additions & 1 deletion ai-pdf-chatbot/components/sign-in-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { authClient } from '@/lib/auth-client';
import posthog from 'posthog-js';

export function SignInForm() {
const [email, setEmail] = useState('');
Expand All @@ -15,17 +16,21 @@ export function SignInForm() {
e.preventDefault();
setIsLoading(true);

const { error } = await authClient.signIn.email({
const { data, error } = await authClient.signIn.email({
email: email.trim(),
password,
});

if (error) {
toast.error(error.message ?? 'Sign in failed');
posthog.captureException(new Error(error.message ?? 'Sign in failed'));
setIsLoading(false);
return;
}

const userId = (data as { user?: { id?: string } } | null)?.user?.id ?? email.trim();
posthog.identify(userId, { email: email.trim() });
posthog.capture('user_signed_in', { email: email.trim() });
window.location.href = '/chat';
}

Expand Down
7 changes: 6 additions & 1 deletion ai-pdf-chatbot/components/sign-up-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useState } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { authClient } from '@/lib/auth-client';
import posthog from 'posthog-js';

export function SignUpForm() {
const [name, setName] = useState('');
Expand All @@ -16,18 +17,22 @@ export function SignUpForm() {
e.preventDefault();
setIsLoading(true);

const { error } = await authClient.signUp.email({
const { data, error } = await authClient.signUp.email({
email: email.trim(),
password,
name: name.trim(),
});

if (error) {
toast.error(error.message ?? 'Sign up failed');
posthog.captureException(new Error(error.message ?? 'Sign up failed'));
setIsLoading(false);
return;
}

const userId = (data as { user?: { id?: string } } | null)?.user?.id ?? email.trim();
posthog.identify(userId, { email: email.trim(), name: name.trim() });
posthog.capture('user_signed_up', { email: email.trim(), name: name.trim() });
window.location.href = '/chat';
}

Expand Down
9 changes: 9 additions & 0 deletions ai-pdf-chatbot/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import posthog from 'posthog-js';

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, {
api_host: '/ingest',
ui_host: 'https://us.posthog.com',
defaults: '2026-01-30',
capture_exceptions: true,
debug: process.env.NODE_ENV === 'development',
});
9 changes: 9 additions & 0 deletions ai-pdf-chatbot/lib/posthog-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { PostHog } from 'posthog-node';

export function getPostHogClient(): PostHog {
return new PostHog(process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN!, {
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
flushAt: 1,
flushInterval: 0,
});
}
27 changes: 27 additions & 0 deletions ai-pdf-chatbot/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ const nextConfig = {
{ protocol: 'https', hostname: 'cdn.insforge.dev' },
],
},
async rewrites() {
// PostHog reverse-proxy hosts. NEXT_PUBLIC_POSTHOG_HOST is read at
// build time, so changing it requires a rebuild. We derive the
// matching assets host for cloud (us / eu); self-hosted instances
// typically serve assets from the same origin, so the unchanged
// host falls through naturally.
const POSTHOG_HOST =
process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com';
const POSTHOG_ASSETS_HOST = POSTHOG_HOST
.replace('//us.i.posthog.com', '//us-assets.i.posthog.com')
.replace('//eu.i.posthog.com', '//eu-assets.i.posthog.com');
return [
{
source: '/ingest/static/:path*',
destination: `${POSTHOG_ASSETS_HOST}/static/:path*`,
},
{
source: '/ingest/array/:path*',
destination: `${POSTHOG_ASSETS_HOST}/array/:path*`,
},
{
source: '/ingest/:path*',
destination: `${POSTHOG_HOST}/:path*`,
},
];
},
skipTrailingSlashRedirect: true,
};

module.exports = nextConfig;
Loading
Loading