diff --git a/src/api/ai.ts b/src/api/ai.ts
new file mode 100644
index 00000000..6f7f2b46
--- /dev/null
+++ b/src/api/ai.ts
@@ -0,0 +1,117 @@
+/**
+ * AI API Layer
+ *
+ * Maps to the actual backend endpoints:
+ * - POST /forms/ai-generate → AI form generation (creates form + fields server-side)
+ * - POST /forms/:id/analytics → AI analytics report for a form
+ */
+
+const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
+
+// ---- Helpers ----
+
+async function handleResponse(response: Response): Promise {
+ if (!response.ok) {
+ const errorData = (await response.json().catch(() => ({}))) as {
+ message?: string
+ }
+ throw new Error(
+ errorData.message || `Request failed: ${response.statusText}`,
+ )
+ }
+ const result = (await response.json()) as {
+ success: boolean
+ data: T
+ message?: string
+ }
+ if (!result.success) {
+ throw new Error(result.message || 'Request failed')
+ }
+ return result.data
+}
+
+// ---- Exported API ----
+
+export interface AIGeneratedField {
+ id: string
+ fieldName: string
+ label: string
+ fieldType: string
+ fieldValueType: string
+ validation?: Record
+ options?: Array
+}
+
+export interface AIGeneratedForm {
+ id: string
+ title: string
+ description: string
+ isPublished: boolean
+ createdAt: string
+ fields: Array
+}
+
+export interface AnalyticsInsight {
+ question: string
+ metric: string
+ value: string | number
+}
+
+export interface AnalyticsTheme {
+ theme: string
+ description: string
+ frequency: string
+}
+
+export interface AnalyticsReport {
+ totalResponsesAnalyzed: number
+ executiveSummary: string
+ quantitativeInsights: Array
+ qualitativeThemes: Array
+}
+
+export const aiApi = {
+ /**
+ * Generates a complete form (title + fields) from a text prompt.
+ * The backend creates the form and all fields in a single transaction.
+ * POST /forms/ai-generate
+ * Body: { prompt: string }
+ * Returns: { id, title, description, fields, ... }
+ */
+ generateForm: async (prompt: string): Promise => {
+ const response = await fetch(`${API_URL}/forms/ai-generate`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt }),
+ credentials: 'include',
+ })
+ return handleResponse(response)
+ },
+
+ /**
+ * Generates an AI analytics report for a specific form's responses.
+ * POST /forms/:formId/analytics
+ * Body: {} (formId is in the URL)
+ * Query: ?format=json (default)
+ * Returns: AnalyticsReport
+ */
+ generateSummary: async (formId: string): Promise => {
+ const response = await fetch(`${API_URL}/forms/${formId}/analytics`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({}),
+ credentials: 'include',
+ })
+ return handleResponse(response)
+ },
+
+ /**
+ * Placeholder — no backend endpoint exists yet.
+ * Returns an empty array so AISuggestionPanel renders gracefully.
+ */
+ suggestFields: (
+ _fields: Array,
+ ): Promise<{ suggestions: Array<{ label: string; type: string }> }> => {
+ return Promise.resolve({ suggestions: [] })
+ },
+}
diff --git a/src/api/forms.ts b/src/api/forms.ts
index 03e0becd..ef9fc583 100644
--- a/src/api/forms.ts
+++ b/src/api/forms.ts
@@ -117,8 +117,8 @@ async function handleResponse(response: Response): Promise {
// Fallbacks ensure we always have some error to show
throw new Error(
errorData.message ||
- errorData.error ||
- `Request failed: ${response.statusText}`,
+ errorData.error ||
+ `Request failed: ${response.statusText}`,
)
}
diff --git a/src/api/responses.ts b/src/api/responses.ts
index 4ddf3478..f8ef7afe 100644
--- a/src/api/responses.ts
+++ b/src/api/responses.ts
@@ -144,7 +144,9 @@ export const responsesApi = {
// GET /responses/received - Get all responses RECEIVED for forms owned by the user
// Falls back to fetching per-form if endpoint doesn't exist on deployed backend
- getAllReceived: async (formIds?: Array): Promise> => {
+ getAllReceived: async (
+ formIds?: Array,
+ ): Promise> => {
try {
const response = await fetch(`${API_URL}/responses/received`, {
method: 'GET',
diff --git a/src/components/editor-sidebar-tabs.test.tsx b/src/components/editor-sidebar-tabs.test.tsx
index dfcaeb25..ade37998 100644
--- a/src/components/editor-sidebar-tabs.test.tsx
+++ b/src/components/editor-sidebar-tabs.test.tsx
@@ -1,33 +1,35 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { describe, expect, it, vi } from 'vitest';
-import { TabsLine } from './editor-sidebar-tabs';
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { describe, expect, it, vi } from 'vitest'
+import { TabsLine } from './editor-sidebar-tabs'
describe('TabsLine', () => {
- it('renders the initial tab (Fields) correctly', () => {
- render();
- expect(screen.getByText('Short Text')).toBeInTheDocument();
- });
+ const mockProps = {}
- it('switches tabs correctly', async () => {
- const user = userEvent.setup();
- render();
+ it('renders the initial tab (Fields) correctly', () => {
+ render()
+ expect(screen.getByText('Short Text')).toBeInTheDocument()
+ })
- const templatesTab = screen.getByRole('tab', { name: /templates/i });
- await user.click(templatesTab);
+ it('switches tabs correctly', async () => {
+ const user = userEvent.setup()
+ render()
- expect(screen.getByText('Contact Us Form')).toBeInTheDocument();
- expect(screen.queryByText('Short Text')).not.toBeInTheDocument();
- });
+ const templatesTab = screen.getByRole('tab', { name: /templates/i })
+ await user.click(templatesTab)
- it('calls onFieldClick when a field is clicked', async () => {
- const onFieldClick = vi.fn();
- const user = userEvent.setup();
- render();
+ expect(screen.getByText('Contact Us Form')).toBeInTheDocument()
+ expect(screen.queryByText('Short Text')).not.toBeInTheDocument()
+ })
- const textFieldButton = screen.getByText('Short Text');
- await user.click(textFieldButton);
+ it('calls onFieldClick when a field is clicked', async () => {
+ const onFieldClick = vi.fn()
+ const user = userEvent.setup()
+ render()
- expect(onFieldClick).toHaveBeenCalledWith('text');
- });
-});
+ const textFieldButton = screen.getByText('Short Text')
+ await user.click(textFieldButton)
+
+ expect(onFieldClick).toHaveBeenCalledWith('text')
+ })
+})
diff --git a/src/components/editor-sidebar-tabs.tsx b/src/components/editor-sidebar-tabs.tsx
index 6302b378..856874a8 100644
--- a/src/components/editor-sidebar-tabs.tsx
+++ b/src/components/editor-sidebar-tabs.tsx
@@ -1,17 +1,35 @@
+import { useState } from 'react'
+import { Loader2, Sparkles } from 'lucide-react'
import { FieldItems } from './fields/field-items'
import { TemplateItems } from './fields/template-items'
import type { Template } from '@/api/templates'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Button } from '@/components/ui/button'
interface TabsLineProps {
onFieldClick?: (fieldId: string) => void
onTemplateClick?: (template: Template) => void
+ onAIGenerate?: (prompt: string) => void
+ isAIGenerating?: boolean
}
-export function TabsLine({ onFieldClick, onTemplateClick }: TabsLineProps) {
+export function TabsLine({
+ onFieldClick,
+ onTemplateClick,
+ onAIGenerate,
+ isAIGenerating,
+}: TabsLineProps) {
+ const [prompt, setPrompt] = useState('')
+
+ const handleGenerate = () => {
+ if (prompt.trim() && onAIGenerate) {
+ onAIGenerate(prompt.trim())
+ }
+ }
+
return (
-
-
+
+
Fields
Templates
Generate
@@ -33,9 +51,42 @@ export function TabsLine({ onFieldClick, onTemplateClick }: TabsLineProps) {
- Generate coming soon
+
+
+
+ AI Form Generator
+
+
+ Describe the form you want to create and AI will generate it with appropriate fields. A new form will be created that you can then edit.
+
+
)
diff --git a/src/components/field-sidebar.tsx b/src/components/field-sidebar.tsx
index 5ca13418..e7e86dd7 100644
--- a/src/components/field-sidebar.tsx
+++ b/src/components/field-sidebar.tsx
@@ -4,15 +4,24 @@ import type { Template } from '@/api/templates'
interface FieldSidebarProps {
onFieldClick?: (fieldId: string) => void
onTemplateClick?: (template: Template) => void
+ onAIGenerate?: (prompt: string) => void
+ isAIGenerating?: boolean
}
export function FieldSidebar({
onFieldClick,
onTemplateClick,
+ onAIGenerate,
+ isAIGenerating,
}: FieldSidebarProps) {
return (
-
+
)
}
diff --git a/src/components/reports/AIInsightsSection.tsx b/src/components/reports/AIInsightsSection.tsx
new file mode 100644
index 00000000..05b8c287
--- /dev/null
+++ b/src/components/reports/AIInsightsSection.tsx
@@ -0,0 +1,217 @@
+import {
+ BrainCircuit,
+ Loader2,
+ MessageSquare,
+ Sparkles,
+ TrendingUp,
+} from 'lucide-react'
+import { KeyTakeaways } from './KeyTakeaways'
+import { ResponseCharts } from './ResponseCharts'
+import type { AnalyticsReport } from '@/api/ai'
+import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+
+interface AIInsightsSectionProps {
+ onGenerate: () => Promise
+ report: AnalyticsReport | null
+ isLoading: boolean
+ responseCount?: number
+}
+
+export function AIInsightsSection({
+ onGenerate,
+ report,
+ isLoading,
+ responseCount = 0,
+}: AIInsightsSectionProps) {
+ const hasResponses = responseCount > 0
+
+ return (
+
+
+
+
+
+
+
+
+ AI Response Insights
+
+
+ Deep-dive patterns extracted from {report?.totalResponsesAnalyzed || responseCount} submissions.
+
+
+
+
+
+
+ {!hasResponses ? (
+
+
+
+
+
+
Awaiting Responses
+
+ Connect your form and start collecting data. Once you have submissions, AI will help you understand the trends.
+
+
+
+ ) : isLoading ? (
+
+
+
+
+ Synthesizing Insights...
+
+
+
+
+
+
+
+ Our AI model is processing open-ended answers and identifying patterns in your dataset.
+
+
+
+ ) : report ? (
+
+ {/* Context meta - Hidden for Premium PDF feel */}
+
+
+
+ Dataset: {report.totalResponsesAnalyzed} entries
+
+
+
+
+ {/* 1. Executive Summary */}
+
+
+
+ Executive Summary
+
+
+ {report.executiveSummary}
+
+
+
+ {/* 2. Key Takeaways Section (New Enhancement) */}
+
+
+
+
+ {/* 3. Quantitative Insights */}
+ {report.quantitativeInsights.length > 0 && (
+
+
+
+ Quantitative Metrics
+
+
+ {report.quantitativeInsights.map((ins, i) => (
+
+
+ {ins.metric}
+
+
+
+ {ins.question}
+
+
+ {ins.value}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* 4. Response Visualizations Section (New Enhancement) */}
+
+
+
+
+ {/* 5. Qualitative Themes */}
+ {report.qualitativeThemes.length > 0 && (
+
+
+
+ Qualitative Sentiment Themes
+
+
+ Analysis results
+
+
+
+ {report.qualitativeThemes.map((theme, i) => (
+
+
+
+ {theme.theme}
+
+
+
+ {theme.frequency} Mentioned
+
+
+
+ {theme.description}
+
+
+ ))}
+
+
+ )}
+
+ ) : (
+
+
+
+
+
+
Insights Dashboard Ready
+
+ Generate an AI summary to see deep behavioral patterns here.
+
+
+
+ )}
+
+
+ )
+}
diff --git a/src/components/reports/ExportReportButton.tsx b/src/components/reports/ExportReportButton.tsx
new file mode 100644
index 00000000..3f8100ae
--- /dev/null
+++ b/src/components/reports/ExportReportButton.tsx
@@ -0,0 +1,22 @@
+import { Download } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+
+interface ExportReportButtonProps {
+ onExport: () => void
+ disabled?: boolean
+}
+
+export function ExportReportButton({ onExport, disabled }: ExportReportButtonProps) {
+ return (
+
+ )
+}
diff --git a/src/components/reports/KeyTakeaways.tsx b/src/components/reports/KeyTakeaways.tsx
new file mode 100644
index 00000000..904c0e65
--- /dev/null
+++ b/src/components/reports/KeyTakeaways.tsx
@@ -0,0 +1,26 @@
+import { Lightbulb } from 'lucide-react'
+
+interface KeyTakeawaysProps {
+ takeaways: Array
+}
+
+export function KeyTakeaways({ takeaways }: KeyTakeawaysProps) {
+ if (takeaways.length === 0) return null
+
+ return (
+
+
+
+ Key Takeaways
+
+
+ {takeaways.map((takeaway, i) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/reports/ReportHeader.tsx b/src/components/reports/ReportHeader.tsx
new file mode 100644
index 00000000..7316bda3
--- /dev/null
+++ b/src/components/reports/ReportHeader.tsx
@@ -0,0 +1,51 @@
+import { Download } from 'lucide-react'
+import { Button } from '@/components/ui/button'
+
+interface ReportHeaderProps {
+ title: string
+ onExport: () => void
+ isLoading?: boolean
+ hasReport?: boolean
+ isPdf?: boolean
+}
+
+export function ReportHeader({
+ title,
+ onExport,
+ isLoading,
+ hasReport,
+ isPdf = false
+}: ReportHeaderProps) {
+ if (isPdf) {
+ return (
+
+
{title}
+
+ AI-Generated Analytics Report
+
+
+ )
+ }
+
+ return (
+
+
+
+ {title}
+
+
+ AI-Generated Analytics Report
+
+
+
+
+ )
+}
diff --git a/src/components/reports/ReportViewer.tsx b/src/components/reports/ReportViewer.tsx
new file mode 100644
index 00000000..5d040135
--- /dev/null
+++ b/src/components/reports/ReportViewer.tsx
@@ -0,0 +1,119 @@
+import { AlertCircle, FileText } from 'lucide-react'
+import { AIInsightsSection } from './AIInsightsSection'
+import { ReportHeader } from './ReportHeader'
+import { ResponseStatistics } from './ResponseStatistics'
+import type { AnalyticsReport } from '@/api/ai'
+
+interface Form {
+ id: string
+ title: string
+ responseCount?: number
+}
+
+interface ReportViewerProps {
+ form: Form | null
+ report: AnalyticsReport | null
+ isLoading: boolean
+ onGenerate: () => Promise
+ onExport: () => void
+ responseCount?: number
+}
+
+export function ReportViewer({ form, report, isLoading, onGenerate, onExport, responseCount }: ReportViewerProps) {
+ if (!form) {
+ return (
+
+
+
+
+
+
No Report Selected
+
+ Please select a form from the sidebar to view its detailed AI insights and metrics.
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Standard Report Styling for Print */}
+
+
+ {/* Interactive Header */}
+
+
+ {/* Content */}
+
+
+ {/* PDF Header Only */}
+
+
+
+
+
+ {/* 1. Response Statistics Section */}
+
+
+ {/* 2. AI Response Insights Section */}
+
+
+ {!report && !isLoading && (
+
+
+
+
No insights generated yet
+
+ Click "Generate AI Summary" above to perform deep analysis on your responses.
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/src/components/reports/ReportsSidebar.tsx b/src/components/reports/ReportsSidebar.tsx
new file mode 100644
index 00000000..28407fcd
--- /dev/null
+++ b/src/components/reports/ReportsSidebar.tsx
@@ -0,0 +1,61 @@
+import { FileText } from 'lucide-react'
+import { cn } from '@/lib/utils'
+
+interface Form {
+ id: string
+ title: string
+ responseCount?: number
+}
+
+interface ReportsSidebarProps {
+ forms: Array
-
-
+
{/* Filters */}
@@ -206,9 +237,11 @@ function ResponsesPage() {
onChange={(e) => setFilterDate(e.target.value)}
/>