Generation Page (complete rebuild): - Full production-parity exam generation wizard with 4 IELTS modules - Reading: AI passage gen, 5 exercise types (MCQ, Fill, Write, T/F, Match) - Listening: 4 section types, AI context gen, TTS audio gen (ElevenLabs) - Writing: Task 1/2, AI instruction gen, word limits, marks - Speaking: 3 parts, AI script gen, avatar video gen (7 avatars) - Per-module config: timer, CEFR difficulty, access, approval, rubrics - Exam submission workflow (draft/published) Exam Structures: - New encoach.exam.structure model + CRUD controller - ExamStructuresPage wired to real API AI Module (encoach_ai): - OpenAI service, ElevenLabs TTS, AWS Polly, ELAI avatars - AI settings model with Odoo config parameters - 7 generation endpoints (passage, exercises, instructions, scripts, context) Vector Module (encoach_vector): - pgvector integration for RAG-based content search - Embedding service with sentence-transformers Exam Session Fixes: - Fixed ExamSession.tsx field mapping (question_type→type, exam_title→title) - Fixed submit payload to include attempt_id and answers - Fixed normalizeType to handle null/undefined Tested: 12/12 API tests passed, browser-verified with real OpenAI calls Made-with: Cursor
104 lines
5.1 KiB
TypeScript
104 lines
5.1 KiB
TypeScript
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
import { Plus, Trash2, Copy } from "lucide-react";
|
|
import AiTipBanner from "@/components/ai/AiTipBanner";
|
|
|
|
const codes = [
|
|
{ code: "ENCOACH-2025-A1B2", type: "Single", used: false, created: "2025-01-10" },
|
|
{ code: "ENCOACH-2025-C3D4", type: "Single", used: true, created: "2025-01-12" },
|
|
{ code: "BATCH-2025-E5F6", type: "Batch", used: false, created: "2025-02-01" },
|
|
{ code: "BATCH-2025-G7H8", type: "Batch", used: false, created: "2025-02-01" },
|
|
];
|
|
|
|
const packages = [
|
|
{ id: 1, name: "IELTS Starter", price: 99, duration: "1 month", discount: 0 },
|
|
{ id: 2, name: "IELTS Pro", price: 249, duration: "3 months", discount: 15 },
|
|
{ id: 3, name: "Corporate Bundle", price: 1999, duration: "12 months", discount: 25 },
|
|
];
|
|
|
|
export default function SettingsPage() {
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Settings</h1>
|
|
<p className="text-muted-foreground">Manage codes, packages, discounts, and grading system.</p>
|
|
</div>
|
|
|
|
<Tabs defaultValue="codes">
|
|
<TabsList>
|
|
<TabsTrigger value="codes">Codes</TabsTrigger>
|
|
<TabsTrigger value="packages">Packages & Discounts</TabsTrigger>
|
|
<TabsTrigger value="grading">Grading System</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="codes" className="mt-4 space-y-4">
|
|
<AiTipBanner context="settings-codes" variant="insight" />
|
|
<div className="flex gap-2">
|
|
<Button size="sm"><Plus className="h-4 w-4 mr-1" /> Generate Single</Button>
|
|
<Button size="sm" variant="outline"><Copy className="h-4 w-4 mr-1" /> Generate Batch</Button>
|
|
</div>
|
|
<Card className="border-0 shadow-sm">
|
|
<CardContent className="p-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow><TableHead>Code</TableHead><TableHead>Type</TableHead><TableHead>Used</TableHead><TableHead>Created</TableHead><TableHead className="w-10"></TableHead></TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{codes.map((c) => (
|
|
<TableRow key={c.code}>
|
|
<TableCell className="font-mono text-xs">{c.code}</TableCell>
|
|
<TableCell><Badge variant="outline">{c.type}</Badge></TableCell>
|
|
<TableCell><Badge variant={c.used ? "secondary" : "default"}>{c.used ? "Used" : "Available"}</Badge></TableCell>
|
|
<TableCell>{c.created}</TableCell>
|
|
<TableCell><Button variant="ghost" size="icon" className="h-8 w-8 text-destructive"><Trash2 className="h-4 w-4" /></Button></TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="packages" className="mt-4 space-y-4">
|
|
<AiTipBanner context="settings-packages" variant="recommendation" />
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{packages.map((p) => (
|
|
<Card key={p.id} className="border-0 shadow-sm">
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-base">{p.name}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<p className="text-2xl font-bold">${p.price}</p>
|
|
<p className="text-sm text-muted-foreground">{p.duration}</p>
|
|
{p.discount > 0 && <Badge variant="default">{p.discount}% OFF</Badge>}
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="grading" className="mt-4 space-y-4">
|
|
<AiTipBanner context="settings-grading" variant="tip" />
|
|
<Card className="border-0 shadow-sm max-w-lg">
|
|
<CardHeader><CardTitle className="text-base">Scoring Scale</CardTitle></CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-2"><Label>Min Score</Label><Input type="number" defaultValue="0" /></div>
|
|
<div className="space-y-2"><Label>Max Score</Label><Input type="number" defaultValue="9" /></div>
|
|
</div>
|
|
<div className="space-y-2"><Label>Score Increment</Label><Input type="number" defaultValue="0.5" step="0.5" /></div>
|
|
<Button>Save Grading Configuration</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
}
|