From 3aa754a84d0a070cc9cb31a8b1a537baecb3da32 Mon Sep 17 00:00:00 2001 From: sachith Date: Tue, 23 Jun 2026 15:59:27 +0530 Subject: [PATCH] feat: improve upload validation and error handling --- src/app/api/analyze/route.ts | 60 +++++++++++++++------------------ src/app/api/upload/route.ts | 5 +-- src/app/layout.tsx | 3 +- src/app/page.tsx | 4 +-- src/components/AppShell.tsx | 15 +++++++-- src/components/UploadZone.tsx | 7 +++- src/lib/data-parser.ts | 8 +++-- src/lib/env-validation.ts | 30 +++++++++++++++++ src/lib/hooks/useAnalyze.ts | 19 ++++++++--- src/lib/vertex/intents/intro.ts | 4 +-- src/lib/vertex/prompt.ts | 2 +- 11 files changed, 107 insertions(+), 50 deletions(-) create mode 100644 src/lib/env-validation.ts diff --git a/src/app/api/analyze/route.ts b/src/app/api/analyze/route.ts index b0d944f..8327884 100644 --- a/src/app/api/analyze/route.ts +++ b/src/app/api/analyze/route.ts @@ -1,5 +1,6 @@ import { NextRequest } from 'next/server'; import { streamAnalysis } from '@/lib/vertex'; +import '@/lib/env-validation'; import type { DataProfile } from '@/types'; export const runtime = 'nodejs'; @@ -23,43 +24,38 @@ export async function POST(request: NextRequest) { } let resultText = ''; - for await (const chunk of streamAnalysis( - question, - profile, - dataSample, - history - )) { - resultText += chunk; - } + try { + for await (const chunk of streamAnalysis( + question, + profile, + dataSample, + history + )) { + resultText += chunk; + } - resultText = resultText.trim(); + resultText = resultText.trim(); - // Strip markdown code fences if AI added them - if (resultText.startsWith('```')) { - resultText = resultText - .replace(/^```(?:json)?\s*/i, '') - .replace(/```\s*$/, '') - .trim(); - } + // Strip markdown code fences if AI added them + if (resultText.startsWith('```')) { + resultText = resultText + .replace(/^```(?:json)?\s*/i, '') + .replace(/```\s*$/, '') + .trim(); + } - if (!resultText) { - return new Response(JSON.stringify({ error: 'Empty response from AI' }), { - status: 500, - }); - } + if (!resultText) { + throw new Error('Empty response from AI'); + } - // Validate JSON before sending - try { + // Validate JSON before sending JSON.parse(resultText); - } catch { + } catch (e) { + const errorMsg = e instanceof Error ? e.message : String(e); + console.error('[AI Data Lens] Pipeline error:', errorMsg); return new Response( - JSON.stringify({ - chartConfig: null, - findings: resultText.substring(0, 500) || 'No response from AI', - limitations: 'The AI response could not be parsed as JSON.', - stats: {}, - }), - { headers: { 'Content-Type': 'application/json' } } + JSON.stringify({ error: `Analysis failed: unable to interpret dataset. Details: ${errorMsg}` }), + { status: 500, headers: { 'Content-Type': 'application/json' } } ); } @@ -72,6 +68,6 @@ export async function POST(request: NextRequest) { } catch (error) { const msg = error instanceof Error ? error.message : String(error); console.error('Analysis error:', msg); - return new Response(JSON.stringify({ error: msg }), { status: 500 }); + return new Response(JSON.stringify({ error: `Analysis failed: ${msg}` }), { status: 500 }); } } diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 5fd9e7d..1389d4a 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { parseFile, getDataSample } from '@/lib/data-parser'; +import '@/lib/env-validation'; export const runtime = 'nodejs'; export const maxDuration = 30; @@ -12,9 +13,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'No file provided' }, { status: 400 }); const ext = file.name.split('.').pop()?.toLowerCase(); - if (!['csv', 'json', 'xlsx', 'xls'].includes(ext || '')) { + if (!['csv', 'json', 'xlsx'].includes(ext || '')) { return NextResponse.json( - { error: 'Unsupported file type. Use CSV, JSON, or Excel.' }, + { error: 'Unsupported file type. Only CSV, JSON, and XLSX files are allowed.' }, { status: 400 } ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9899150..bff3f3c 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next'; import { Inter, JetBrains_Mono } from 'next/font/google'; +import '@/lib/env-validation'; import './globals.css'; const inter = Inter({ @@ -14,7 +15,7 @@ const mono = JetBrains_Mono({ }); export const metadata: Metadata = { - title: 'DataLensAI - See your data. Ask anything.', + title: 'AI Data Lens - See your data. Ask anything.', description: 'Drop in a CSV. Ask a business question. Get the full analysis.', }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 8485ca3..1dd55c5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,11 +4,11 @@ import { Hero } from '@/components/Hero'; import { AppShell } from '@/components/AppShell'; export default function Home() { - const data = useSessionStore((s) => s.data); + const currentSession = useSessionStore((s) => s.currentSession); return (
- {data.length === 0 ? : } + {!currentSession ? : }
); } diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 94f94ec..dc30c9c 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -27,7 +27,18 @@ export function AppShell() { }); }, [messages]); - if (!currentSession) return null; + if (!currentSession) { + return ( +
+
+

No dataset uploaded yet

+

+ Please upload a dataset (.csv, .xlsx, or .json) on the home page to start analyzing your data with AI Data Lens. +

+
+
+ ); + } return ( <> @@ -50,7 +61,7 @@ export function AppShell() {

- DataLensAI + AI Data Lens

Conversational analytics diff --git a/src/components/UploadZone.tsx b/src/components/UploadZone.tsx index 6af848a..49d21e5 100644 --- a/src/components/UploadZone.tsx +++ b/src/components/UploadZone.tsx @@ -15,6 +15,11 @@ export function UploadZone() { const handleFile = useCallback( async (file: File) => { + const ext = file.name.split('.').pop()?.toLowerCase(); + if (!ext || !['csv', 'json', 'xlsx'].includes(ext)) { + setError(`Unsupported file type: .${ext || ''}. Only .csv, .json, and .xlsx files are allowed.`); + return; + } setIsLoading(true); setError(null); try { @@ -71,7 +76,7 @@ export function UploadZone() { e.target.files?.[0] && handleFile(e.target.files[0]) diff --git a/src/lib/data-parser.ts b/src/lib/data-parser.ts index 4c198fb..272b0a7 100644 --- a/src/lib/data-parser.ts +++ b/src/lib/data-parser.ts @@ -6,11 +6,13 @@ export async function parseFile( file: File ): Promise<{ data: Record[]; profile: DataProfile }> { const ext = file.name.split('.').pop()?.toLowerCase(); - let data: Record[] = []; // eslint-disable-line no-useless-assignment + if (!ext || !['csv', 'json', 'xlsx'].includes(ext)) { + throw new Error(`Unsupported file type: .${ext || ''}. Only .csv, .json, and .xlsx files are allowed.`); + } + let data: Record[] = []; if (ext === 'csv') data = await parseCSV(file); else if (ext === 'json') data = await parseJSON(file); - else if (ext === 'xlsx' || ext === 'xls') data = await parseExcel(file); - else throw new Error(`Unsupported format: ${ext}`); + else if (ext === 'xlsx') data = await parseExcel(file); const profile = createProfile(data, file.name, file.size); return { data, profile }; } diff --git a/src/lib/env-validation.ts b/src/lib/env-validation.ts new file mode 100644 index 0000000..e4a4990 --- /dev/null +++ b/src/lib/env-validation.ts @@ -0,0 +1,30 @@ +if (typeof window === 'undefined') { + const required = ['GCP_PROJECT_ID', 'GCP_LOCATION', 'GCP_JSON_BASE64']; + const missing: string[] = []; + + for (const name of required) { + const val = process.env[name]; + if (!val) { + missing.push(name); + } else if (name === 'GCP_JSON_BASE64') { + try { + const decoded = Buffer.from(val, 'base64').toString('utf-8'); + const parsed = JSON.parse(decoded); + if (!parsed.client_email || !parsed.private_key) { + missing.push('GCP_JSON_BASE64 (missing client_email or private_key)'); + } + } catch { + console.error( + '[env-validation] GCP_JSON_BASE64 is set but contains invalid or malformed base64/JSON' + ); + missing.push('GCP_JSON_BASE64 (invalid/malformed format)'); + } + } + } + + if (missing.length > 0) { + console.error( + `[AI Data Lens] Environment variable validation failed. Missing or invalid variables: ${missing.join(', ')}` + ); + } +} diff --git a/src/lib/hooks/useAnalyze.ts b/src/lib/hooks/useAnalyze.ts index 779c374..d8bb518 100644 --- a/src/lib/hooks/useAnalyze.ts +++ b/src/lib/hooks/useAnalyze.ts @@ -33,11 +33,21 @@ export function useAnalyze() { }), }); if (!res.ok) { + const errorText = await res.text(); + let errorMessage = 'unable to interpret dataset'; + try { + const errorObj = JSON.parse(errorText); + if (errorObj.error) { + errorMessage = errorObj.error.replace(/^Analysis failed:\s*/i, ''); + } + } catch { + // ignore parsing error, fallback to default error message + } finishStreaming( id, errorResult( - 'Analysis service unavailable.', - 'Check API configuration.' + `Analysis failed: ${errorMessage}`, + 'Please try again with a different question or verify the dataset structure.' ) ); return; @@ -54,10 +64,11 @@ export function useAnalyze() { ) ); } - } catch { + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); finishStreaming( id, - errorResult('Network error.', 'Check your connection and try again.') + errorResult('Network error.', `Details: ${msg}. Check your connection and try again.`) ); } }; diff --git a/src/lib/vertex/intents/intro.ts b/src/lib/vertex/intents/intro.ts index 0adad4e..e2f25b9 100644 --- a/src/lib/vertex/intents/intro.ts +++ b/src/lib/vertex/intents/intro.ts @@ -12,7 +12,7 @@ export const introIntent: IntentHandler = { handle() { const result: IntentResult = { chartType: 'bar', - title: 'DataLensAI - at a glance', + title: 'AI Data Lens - at a glance', data: [ { name: 'CSV / JSON / Excel parsing', value: 100 }, { name: 'Natural-language Q&A', value: 95 }, @@ -20,7 +20,7 @@ export const introIntent: IntentHandler = { { name: 'Limitations surfaced', value: 100 }, ], findings: - "I'm DataLensAI - your autonomous data analysis partner. Upload any data file and ask questions in plain English. I surface the right visualization, synthesize findings, and flag limitations honestly. No SQL, no Python - just answers.\n\n**Try asking me**:\n• Show me the top performers\n• Plot a trend over time\n• Break down by category", + "I'm AI Data Lens - your autonomous data analysis partner. Upload any data file and ask questions in plain English. I surface the right visualization, synthesize findings, and flag limitations honestly. No SQL, no Python - just answers.\n\n**Try asking me**:\n• Show me the top performers\n• Plot a trend over time\n• Break down by category", }; return result; }, diff --git a/src/lib/vertex/prompt.ts b/src/lib/vertex/prompt.ts index a4082ac..3c417b2 100644 --- a/src/lib/vertex/prompt.ts +++ b/src/lib/vertex/prompt.ts @@ -12,7 +12,7 @@ export function buildSystemPrompt( p?.columns?.map((c) => `${c.name} (${c.type})`).join(', ') || 'unknown'; const sample = dataSample.slice(0, 3); - return `You are DataLensAI, a rigorous data analyst. Always respond with valid JSON only (no markdown, no prose outside the JSON). + return `You are AI Data Lens, a rigorous data analyst. Always respond with valid JSON only (no markdown, no prose outside the JSON). Schema (${p?.rowCount || '?'} rows): ${schema}