Add configurable LlamaCloud API URL for EU region support (#44)

* Add agentic mode support for LlamaParse and fix eligibility extraction

- Add agenticMode option to ParseOptions interface and Zod schema
- Enable agentic parsing by default for better multi-sheet Excel support
- Fix eligibility extraction to gracefully handle documents without
  eligibility criteria by returning empty array instead of throwing error

* Refactor project index selection to single-select with auto-save

- Change ProjectIndexSelector from multi-select checkboxes to single-select
  clickable rows with radio-style indicators
- Implement debounced auto-save (800ms) when selection changes
- Remove Edit/Save/Cancel buttons in favor of automatic persistence
- Add refreshKey prop to ProjectDocuments for efficient refetch on changes
- Update DocumentsSection to wire components with onSaveSuccess callback
- Simplify Questions page index selector to show index name with Active badge

* Fix ESLint configuration and resolve all lint errors

- Add eslint and eslint-config-next as dev dependencies
- Create .eslintrc.json with next/core-web-vitals preset
- Fix 43 unescaped entity errors by replacing quotes and apostrophes
  with HTML entities across 14 component files
- Fix 9 react-hooks/exhaustive-deps warnings by wrapping fetch
  functions in useCallback with proper dependency arrays

* Add Vitest testing infrastructure with 173 unit tests

- Configure Vitest with coverage reporting and path aliases
- Add test scripts (test, test:run, test:coverage) to package.json
- Install vitest, @vitest/coverage-v8, vite-tsconfig-paths, vitest-mock-extended

Test coverage includes:
- validators: extract-questions, generate-response, llamaparse, multi-step-response
- errors: all API error classes with type guard
- services: FileValidator (file type/size validation), DefaultResponseService
- middleware: apiHandler and withApiHandler request validation

Add mock infrastructure for Prisma, OpenAI, and test fixtures

* Fix missing agenticMode in LlamaParse processing service

Add agentic_mode field to request schema and form data parser to ensure
the agenticMode option is properly passed through the parsing pipeline.
This resolves TypeScript compilation error where agenticMode was required
in LlamaParseOptions but not provided by the processing service.

* Fix Add Manually button routing and improve Vercel preview URL handling

- Fix 404 error when clicking Add Manually button by using correct route path
  /projects/{projectId}/questions/create instead of /questions/create?projectId=
- Update magic link auth to use VERCEL_URL for preview deployments

* Simplify IndexSelector to display-only mode

Remove index configuration UI since only single file selection is now supported:
- Remove Configure/Hide toggle button and collapsible panel
- Remove Select All/Deselect All functionality
- Remove checkbox selection grid for indexes
- Clean up handleIndexToggle and handleSelectAllIndexes from provider
- Keep project card display showing active index name and status

* Update .gitignore (with .devcontainer)

* Initial implementation of API_URL

* Fix console errors on login page

Prevent auth session errors on public pages and remove form method attribute that conflicts with Server Actions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Roland Tritsch <roland@tritsch.email>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
boyang-zhang
2025-12-27 12:18:21 -06:00
committed by GitHub
parent 1151372e5c
commit 86e0a09873
11 changed files with 33 additions and 14 deletions
+2
View File
@@ -40,3 +40,5 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
CLAUDE.md
.devcontainer
.pnpm-store/
+3
View File
@@ -82,6 +82,9 @@ OPENAI_API_KEY=<your-openai-api-key>
# LlamaCloud
LLAMACLOUD_API_KEY=<your-llamacloud-api-key>
# Optional: LlamaCloud API URL (defaults to US: https://api.cloud.llamaindex.ai)
# For EU region, use: https://api.cloud.eu.llamaindex.ai
# LLAMACLOUD_API_URL=https://api.cloud.eu.llamaindex.ai
# Optional: Internal API key and domain for internal users
# LLAMACLOUD_API_KEY_INTERNAL=<your-internal-llamacloud-api-key>
# INTERNAL_EMAIL_DOMAIN=<your-domain> # Defaults to @runllama.ai
+2 -2
View File
@@ -20,14 +20,14 @@ export async function GET(request: NextRequest) {
// Fetch projects and organizations from LlamaCloud
const [projectsResponse, organizationsResponse] = await Promise.all([
fetch('https://api.cloud.llamaindex.ai/api/v1/projects', {
fetch(`${env.LLAMACLOUD_API_URL}/api/v1/projects`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
}),
fetch('https://api.cloud.llamaindex.ai/api/v1/organizations', {
fetch(`${env.LLAMACLOUD_API_URL}/api/v1/organizations`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
+2 -2
View File
@@ -86,14 +86,14 @@ async function fetchLlamaCloudProjects(userEmail?: string) {
const apiKey = getLlamaCloudApiKey(userEmail);
const [projectsResponse, organizationsResponse] = await Promise.all([
fetch('https://api.cloud.llamaindex.ai/api/v1/projects', {
fetch(`${env.LLAMACLOUD_API_URL}/api/v1/projects`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
}),
fetch('https://api.cloud.llamaindex.ai/api/v1/organizations', {
fetch(`${env.LLAMACLOUD_API_URL}/api/v1/organizations`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
@@ -74,7 +74,7 @@ export async function GET(
// Get the appropriate API key based on user's email
const apiKey = getLlamaCloudApiKey(currentUser.email);
const pipelinesResponse = await fetch('https://api.cloud.llamaindex.ai/api/v1/pipelines', {
const pipelinesResponse = await fetch(`${env.LLAMACLOUD_API_URL}/api/v1/pipelines`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
@@ -244,8 +244,8 @@ export async function POST(
try {
// Get the appropriate API key based on user's email
const apiKey = getLlamaCloudApiKey(currentUser.email);
const pipelinesResponse = await fetch('https://api.cloud.llamaindex.ai/api/v1/pipelines', {
const pipelinesResponse = await fetch(`${env.LLAMACLOUD_API_URL}/api/v1/pipelines`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
@@ -263,9 +263,9 @@ export async function POST(
}
const pipelines = await pipelinesResponse.json();
// Filter pipelines to only include those from the connected LlamaCloud project
const filteredPipelines = pipelines.filter((pipeline: any) =>
const filteredPipelines = pipelines.filter((pipeline: any) =>
pipeline.project_id === project.organization.llamaCloudProjectId
);
+1 -1
View File
@@ -24,7 +24,7 @@ export default function LoginPage() {
<form action={signInWithMagicLink} method="POST" className="space-y-6">
<form action={signInWithMagicLink} className="space-y-6">
<div>
<Label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address
+7 -2
View File
@@ -36,14 +36,19 @@ function GlobalHeaderContent() {
const [userEmail, setUserEmail] = useState<string | null>(null);
const { currentOrganization, currentProject } = useOrganization();
// Fetch user email on component mount
// Fetch user email on component mount (skip on public pages)
useEffect(() => {
// Don't fetch user on public pages
if (pathname === '/' || pathname === '/login' || pathname === '/signup') {
return;
}
const fetchUserEmail = async () => {
const email = await getCurrentUserEmail();
setUserEmail(email);
};
fetchUserEmail();
}, []);
}, [pathname]);
// Build dynamic breadcrumbs based on current route
const buildBreadcrumbs = (): BreadcrumbItem[] => {
+1
View File
@@ -2,6 +2,7 @@
export const env = {
LLAMACLOUD_API_KEY: process.env.LLAMACLOUD_API_KEY || '',
LLAMACLOUD_API_KEY_INTERNAL: process.env.LLAMACLOUD_API_KEY_INTERNAL || '',
LLAMACLOUD_API_URL: process.env.LLAMACLOUD_API_URL || 'https://api.cloud.llamaindex.ai',
INTERNAL_EMAIL_DOMAIN: process.env.INTERNAL_EMAIL_DOMAIN || '@runllama.ai',
};
+5 -1
View File
@@ -46,15 +46,19 @@ export class LlamaIndexService implements ILlamaIndexService {
private initializeIndexes(): void {
try {
// Extract hostname from LLAMACLOUD_API_URL for LlamaCloudIndex
// The SDK expects just the hostname (e.g., 'api.cloud.eu.llamaindex.ai')
const baseUrlHostname = new URL(env.LLAMACLOUD_API_URL).hostname;
console.log('Initializing LlamaCloud indexes with config:', this.config);
if (this.config.indexNames && this.config.indexNames.length > 0) {
this.indexes.push(
...this.config.indexNames.map(indexName =>
...this.config.indexNames.map(indexName =>
new LlamaCloudIndex({
name: indexName,
projectName: this.config.projectName,
apiKey: this.config.apiKey,
baseUrl: baseUrlHostname,
})
)
);
+3
View File
@@ -57,10 +57,13 @@ export class LlamaParseService {
// Default to agentic mode for better multi-sheet/multi-page parsing
const useAgentic = options.agenticMode !== false;
// LlamaParseReader uses protocol + hostname format (no /api/v1)
// env.LLAMACLOUD_API_URL is already in this format
let readerOptions: Record<string, any> = {
apiKey: this.apiKey,
resultType: "markdown",
useAgenticParse: useAgentic,
baseUrl: env.LLAMACLOUD_API_URL,
};
// Add mode-specific options
+2 -1
View File
@@ -9,6 +9,7 @@ import {
} from '@/lib/validators/llamacloud';
import { ExternalServiceError, LlamaCloudConnectionError } from '@/lib/errors/api-errors';
import { z } from 'zod';
import { env } from '@/lib/env';
/**
* LlamaCloud API client implementation
@@ -18,7 +19,7 @@ export class LlamaCloudClient implements ILlamaCloudClient {
constructor(config: Partial<LlamaCloudClientConfig> = {}) {
this.config = {
baseUrl: 'https://api.cloud.llamaindex.ai/api/v1',
baseUrl: `${env.LLAMACLOUD_API_URL}/api/v1`,
timeout: 30000,
retryAttempts: 3,
...config,