feat: Introduce AI code hinting, enforce single-tenant organization model, and add a Code Lab block component.

This commit is contained in:
2026-03-09 17:24:15 -03:00
parent b9c17ce67b
commit bde5be22e7
26 changed files with 822 additions and 1378 deletions
@@ -38,6 +38,7 @@ import MemoryBlock from "@/components/blocks/MemoryBlock";
import RolePlayingBlock from "@/components/blocks/RolePlayingBlock";
import PeerReviewBlock from "@/components/blocks/PeerReviewBlock";
import MermaidBlock from "@/components/blocks/MermaidBlock";
import CodeLabBlock from "@/components/blocks/CodeLabBlock";
import SaveToLibraryModal from "@/components/modals/SaveToLibraryModal";
import LibraryPanel from "@/components/LibraryPanel";
import Modal from "@/components/Modal";
@@ -1106,6 +1107,20 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
onUpdate={(updates) => updateBlock(block.id, updates)}
/>
)}
{block.type === 'code-lab' && (
<CodeLabBlock
id={block.id}
title={block.title}
language={block.language}
instructions={block.instructions}
initial_code={block.initial_code}
solution={block.solution}
test_cases={block.test_cases}
editMode={editMode}
lessonId={params.lessonId}
onChange={(updates) => updateBlock(block.id, updates)}
/>
)}
</div>
</div>
))}
@@ -1148,6 +1163,7 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
{ type: 'audio-response', icon: '🎤', label: 'Oral Practice', color: 'blue' },
{ type: 'memory-match', icon: '🧩', label: 'Logic Game', color: 'indigo' },
{ type: 'peer-review', icon: '👥', label: 'Peer Review', color: 'slate' },
{ type: 'code-lab', icon: '🧑‍💻', label: 'Code Lab', color: 'indigo' },
{ type: 'mermaid', icon: '📊', label: 'Mermaid Diagram', color: 'indigo' },
{ type: 'role-playing', icon: '🎭', label: 'Role-Playing AI', color: 'purple' },
].map((item) => (
+5 -36
View File
@@ -21,7 +21,6 @@ import {
Send,
} from "lucide-react";
import CourseEditorLayout from "@/components/CourseEditorLayout";
import OrganizationSelector from "@/components/OrganizationSelector";
interface FullModule extends Module {
lessons: Lesson[];
@@ -35,8 +34,6 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
const [error, setError] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [editValue, setEditValue] = useState("");
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [isOrgModalOpen, setIsOrgModalOpen] = useState(false);
const [saving, setSaving] = useState(false); // Added saving state
const { user } = useAuth();
@@ -63,19 +60,7 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
loadData();
}, [params.id]);
useEffect(() => {
const loadOrgs = async () => {
if (user?.role === 'admin' && user?.organization_id === '00000000-0000-0000-0000-000000000001') {
try {
const orgs = await cmsApi.getOrganizations();
setOrganizations(orgs);
} catch (err) {
console.error("Failed to load organizations", err);
}
}
};
loadOrgs();
}, [user]);
const handleAddModule = async () => {
const title = "";
@@ -194,27 +179,19 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
const handlePublish = async () => {
if (!course) return;
const isSuperAdmin = user?.role === 'admin' && user?.organization_id === '00000000-0000-0000-0000-000000000001';
if (isSuperAdmin && organizations.length > 0) {
setIsOrgModalOpen(true);
} else {
publishCourse();
}
publishCourse();
};
const publishCourse = async (targetOrgId?: string) => {
const publishCourse = async () => {
try {
setSaving(true);
await cmsApi.publishCourse(params.id as string, targetOrgId);
await cmsApi.publishCourse(params.id as string);
alert("Course published successfully!");
} catch (err) {
console.error("Failed to publish course", err);
alert("Failed to publish course.");
} finally {
setSaving(false);
setIsOrgModalOpen(false); // Close modal after publishing attempt
}
};
@@ -429,15 +406,7 @@ export default function CourseEditor({ params }: { params: { id: string } }) {
</button>
</div>
</CourseEditorLayout>
{/* Organization Selector Modal */}
<OrganizationSelector
isOpen={isOrgModalOpen}
onClose={() => setIsOrgModalOpen(false)}
organizations={organizations}
title="Publish to Organization"
actionLabel="Publish Course"
onConfirm={(orgId) => publishCourse(orgId)}
/>
</>
);
}