diff --git a/next.config.js b/next.config.js
index 5a9c23d..0798869 100644
--- a/next.config.js
+++ b/next.config.js
@@ -3,6 +3,7 @@ const nextConfig = {
reactStrictMode: true,
swcMinify: true,
experimental: {
+ instrumentationHook: true,
serverActions: {
bodySizeLimit: '2mb',
},
diff --git a/src/components/layout/SidebarContent.tsx b/src/components/layout/SidebarContent.tsx
index e6ea9c4..b6e5594 100644
--- a/src/components/layout/SidebarContent.tsx
+++ b/src/components/layout/SidebarContent.tsx
@@ -9,7 +9,9 @@ import {
FlaskConical,
TrendingUp,
GraduationCap,
- ArrowLeftRight
+ ArrowLeftRight,
+ Github,
+ Bug
} from 'lucide-react';
import { StudyTreeNav } from './StudyTreeNav';
import { ThemeToggle } from '@/components/ui/theme-toggle';
@@ -54,6 +56,10 @@ export function SidebarContent({ sidebarHierarchy, examId, onNavigate }: Sidebar
},
];
+ const repoUrl = 'https://github.com/atbrace/sa-pro-study-companion';
+ const issueBody = `**Page:** ${pathname}\n**Exam:** ${config.shortName}\n\n**Description:**\n`;
+ const issueUrl = `${repoUrl}/issues/new?title=&body=${encodeURIComponent(issueBody)}&labels=bug`;
+
return (
<>
{/* Logo/Brand */}
@@ -138,6 +144,29 @@ export function SidebarContent({ sidebarHierarchy, examId, onNavigate }: Sidebar
+
>
);
diff --git a/src/instrumentation.ts b/src/instrumentation.ts
new file mode 100644
index 0000000..8fbd507
--- /dev/null
+++ b/src/instrumentation.ts
@@ -0,0 +1,6 @@
+export async function register() {
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
+ const { validateAndLogEnv } = await import('./lib/env');
+ validateAndLogEnv();
+ }
+}
diff --git a/src/lib/env.ts b/src/lib/env.ts
new file mode 100644
index 0000000..352388b
--- /dev/null
+++ b/src/lib/env.ts
@@ -0,0 +1,68 @@
+import type { ProviderName } from './llm/types';
+
+const VALID_PROVIDERS: readonly ProviderName[] = ['claude', 'gemini'];
+
+interface EnvValidationResult {
+ provider: ProviderName;
+ warnings: string[];
+ errors: string[];
+}
+
+/**
+ * Validate LLM-related environment variables.
+ * Returns structured results so callers can decide how to handle warnings vs errors.
+ */
+export function validateLLMEnv(): EnvValidationResult {
+ const warnings: string[] = [];
+ const errors: string[] = [];
+
+ const rawProvider = process.env.LLM_PROVIDER;
+ let provider: ProviderName = 'claude';
+
+ if (!rawProvider) {
+ warnings.push(
+ 'LLM_PROVIDER is not set. Defaulting to "claude". AI tutor features require a valid provider and API key.'
+ );
+ } else if (!VALID_PROVIDERS.includes(rawProvider as ProviderName)) {
+ errors.push(
+ `Unknown LLM_PROVIDER: "${rawProvider}". Valid options: ${VALID_PROVIDERS.join(', ')}`
+ );
+ return { provider, warnings, errors };
+ } else {
+ provider = rawProvider as ProviderName;
+ }
+
+ if (provider === 'claude' && !process.env.ANTHROPIC_API_KEY) {
+ errors.push(
+ 'ANTHROPIC_API_KEY is required when LLM_PROVIDER=claude. Set it in .env.local to enable AI tutor features.'
+ );
+ }
+
+ if (provider === 'gemini' && !process.env.GOOGLE_AI_API_KEY) {
+ errors.push(
+ 'GOOGLE_AI_API_KEY is required when LLM_PROVIDER=gemini. Set it in .env.local to enable AI tutor features.'
+ );
+ }
+
+ return { provider, warnings, errors };
+}
+
+/**
+ * Run env validation and log results. Throws on errors if strict mode is enabled.
+ * In startup context (instrumentation), we warn but don't crash — the app works for
+ * study content without LLM. The provider factory still throws on actual use.
+ */
+export function validateAndLogEnv(options: { strict?: boolean } = {}): void {
+ const { warnings, errors } = validateLLMEnv();
+
+ for (const warning of warnings) {
+ console.warn(`[env] WARNING: ${warning}`);
+ }
+
+ for (const error of errors) {
+ if (options.strict) {
+ throw new Error(error);
+ }
+ console.error(`[env] ERROR: ${error}`);
+ }
+}
diff --git a/src/lib/llm/__tests__/provider.test.ts b/src/lib/llm/__tests__/provider.test.ts
index 85854c9..26e9aae 100644
--- a/src/lib/llm/__tests__/provider.test.ts
+++ b/src/lib/llm/__tests__/provider.test.ts
@@ -37,7 +37,7 @@ describe('provider factory', () => {
const { getProvider, resetProvider } = await import('../provider');
resetProvider();
- expect(() => getProvider()).toThrow('ANTHROPIC_API_KEY required');
+ expect(() => getProvider()).toThrow('ANTHROPIC_API_KEY is required when LLM_PROVIDER=claude');
});
it('throws on missing GOOGLE_AI_API_KEY for gemini', async () => {
@@ -47,7 +47,7 @@ describe('provider factory', () => {
const { getProvider, resetProvider } = await import('../provider');
resetProvider();
- expect(() => getProvider()).toThrow('GOOGLE_AI_API_KEY required');
+ expect(() => getProvider()).toThrow('GOOGLE_AI_API_KEY is required when LLM_PROVIDER=gemini');
});
it('throws on unknown provider', async () => {
@@ -56,7 +56,7 @@ describe('provider factory', () => {
const { getProvider, resetProvider } = await import('../provider');
resetProvider();
- expect(() => getProvider()).toThrow('Unknown LLM provider: unknown');
+ expect(() => getProvider()).toThrow('Unknown LLM_PROVIDER: "unknown"');
});
it('caches provider instance', async () => {
diff --git a/src/lib/llm/provider.ts b/src/lib/llm/provider.ts
index 86091cb..e96d1f5 100644
--- a/src/lib/llm/provider.ts
+++ b/src/lib/llm/provider.ts
@@ -1,6 +1,7 @@
import type { LLMProvider, ProviderName } from './types';
import { claudeProvider } from './providers/claude';
import { geminiProvider } from './providers/gemini';
+import { validateLLMEnv } from '../env';
const providers: Record = {
claude: claudeProvider,
@@ -12,20 +13,10 @@ let cachedProvider: LLMProvider | null = null;
export function getProvider(): LLMProvider {
if (cachedProvider) return cachedProvider;
- const name = (process.env.LLM_PROVIDER || 'claude') as ProviderName;
+ const { provider: name, errors } = validateLLMEnv();
- if (!providers[name]) {
- throw new Error(
- `Unknown LLM provider: ${name}. Valid options: ${Object.keys(providers).join(', ')}`
- );
- }
-
- // Validate required env vars
- if (name === 'claude' && !process.env.ANTHROPIC_API_KEY) {
- throw new Error('ANTHROPIC_API_KEY required when LLM_PROVIDER=claude');
- }
- if (name === 'gemini' && !process.env.GOOGLE_AI_API_KEY) {
- throw new Error('GOOGLE_AI_API_KEY required when LLM_PROVIDER=gemini');
+ if (errors.length > 0) {
+ throw new Error(errors[0]);
}
cachedProvider = providers[name];