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
60 changes: 28 additions & 32 deletions src/app/api/analyze/route.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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' } }
);
}

Expand All @@ -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 });
}
}
5 changes: 3 additions & 2 deletions src/app/api/upload/route.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 }
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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.',
};

Expand Down
4 changes: 2 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="h-screen flex flex-col bg-black">
{data.length === 0 ? <Hero /> : <AppShell />}
{!currentSession ? <Hero /> : <AppShell />}
</div>
);
}
15 changes: 13 additions & 2 deletions src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,18 @@ export function AppShell() {
});
}, [messages]);

if (!currentSession) return null;
if (!currentSession) {
return (
<div className="flex-1 flex flex-col items-center justify-center p-8 text-center text-muted-foreground">
<div className="max-w-md mx-auto space-y-4">
<p className="text-lg font-medium text-white">No dataset uploaded yet</p>
<p className="text-sm">
Please upload a dataset (.csv, .xlsx, or .json) on the home page to start analyzing your data with AI Data Lens.
</p>
</div>
</div>
);
}

return (
<>
Expand All @@ -50,7 +61,7 @@ export function AppShell() {
</div>
<div>
<h1 className="text-sm font-semibold tracking-tight">
DataLensAI
AI Data Lens
</h1>
<p className="text-[10px] text-muted-foreground leading-none">
Conversational analytics
Expand Down
7 changes: 6 additions & 1 deletion src/components/UploadZone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -71,7 +76,7 @@ export function UploadZone() {
<input
ref={inputRef}
type="file"
accept=".csv,.json,.xlsx,.xls"
accept=".csv,.json,.xlsx"
className="hidden"
onChange={(e) =>
e.target.files?.[0] && handleFile(e.target.files[0])
Expand Down
8 changes: 5 additions & 3 deletions src/lib/data-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ export async function parseFile(
file: File
): Promise<{ data: Record<string, unknown>[]; profile: DataProfile }> {
const ext = file.name.split('.').pop()?.toLowerCase();
let data: Record<string, unknown>[] = []; // 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<string, unknown>[] = [];
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 };
}
Expand Down
30 changes: 30 additions & 0 deletions src/lib/env-validation.ts
Original file line number Diff line number Diff line change
@@ -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(', ')}`
);
}
}
19 changes: 15 additions & 4 deletions src/lib/hooks/useAnalyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.`)
);
}
};
Expand Down
4 changes: 2 additions & 2 deletions src/lib/vertex/intents/intro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ 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 },
{ name: 'Auto chart selection', value: 90 },
{ 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;
},
Expand Down
2 changes: 1 addition & 1 deletion src/lib/vertex/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down
Loading