diff --git a/src/src/lib/components/annotations/3dAnnotations.svelte b/src/src/lib/components/annotations/3dAnnotations.svelte
index 780a39ae..8fa3ac12 100644
--- a/src/src/lib/components/annotations/3dAnnotations.svelte
+++ b/src/src/lib/components/annotations/3dAnnotations.svelte
@@ -1,221 +1,260 @@
-
-
-
-
-
-
-
- {#if annotationMode && activePoint}
-
-
-
-
- {/if}
-
-
-
-
\ No newline at end of file
+ import { onMount } from 'svelte';
+ import * as THREE from 'three';
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
+ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
+ import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
+ import Menu from '$lib/components/hotspot/3dMenu.svelte';
+ import { Button } from 'flowbite-svelte';
+
+
+ let canvas: HTMLCanvasElement;
+ let camera: THREE.PerspectiveCamera, scene: THREE.Scene, renderer: THREE.WebGLRenderer;
+ let controls: OrbitControls;
+ let labelRenderer: CSS2DRenderer;
+ let raycaster: THREE.Raycaster;
+ let mouse: THREE.Vector2;
+ let annotationMode = false; // Toggle for annotation mode
+ let annotationText = '';
+ let activePoint: THREE.Vector3 | null = null;
+ let tooltipX: number = 0;
+ let tooltipY: number = 0;
+
+ export let data: {
+ role: string;
+ models: { title: string; file_path: string; description: string }[];
+ };
+
+ let { models } = data;
+ let selectedModel: string | null = null;
+ const annotations: {
+ [key: string]: { position: THREE.Vector3; text: string; labelDiv: HTMLDivElement };
+ } = {};
+
+ function toggleAnnotationMode() {
+ annotationMode = !annotationMode;
+ if (!annotationMode) {
+ activePoint = null; // Clear active point when exiting annotation mode
+ }
+ }
+
+ function addAnnotation() {
+ if (annotationText.trim() && activePoint) {
+ createAnnotation(activePoint, annotationText);
+ annotationText = ''; // Clear text after adding
+ activePoint = null; // Clear active point
+ }
+ }
+
+ function createAnnotation(position: THREE.Vector3, text: string) {
+ // Create a circle as a THREE.Sprite
+ const circleTexture = new THREE.TextureLoader().load('/images/circle.png');
+ const spriteMaterial = new THREE.SpriteMaterial({
+ map: circleTexture,
+ depthTest: false,
+ depthWrite: false,
+ sizeAttenuation: false
+ });
+ const sprite = new THREE.Sprite(spriteMaterial);
+ sprite.position.copy(position);
+ sprite.scale.set(0.05, 0.05, 0.05);
+ scene.add(sprite);
+
+ const labelDiv = document.createElement('div');
+ labelDiv.className = 'annotation-label';
+ labelDiv.textContent = text;
+ const label = new CSS2DObject(labelDiv);
+ label.position.copy(position);
+ scene.add(label);
+
+ annotations[text] = { position, text, labelDiv };
+ }
+
+ function onMouseClick(event: MouseEvent) {
+ if (!annotationMode) return;
+
+ const rect = canvas.getBoundingClientRect();
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
+
+ raycaster.setFromCamera(mouse, camera);
+ const intersects = raycaster.intersectObjects(scene.children, true);
+
+ if (intersects.length > 0) {
+ const point = intersects[0].point;
+ activePoint = point;
+
+ const vector = new THREE.Vector3();
+ vector.copy(activePoint).project(camera);
+
+ const canvas = renderer.domElement;
+ const widthHalf = 0.5 * canvas.width;
+ const heightHalf = 0.5 * canvas.height;
+
+ tooltipX = vector.x * widthHalf + widthHalf;
+ tooltipY = -(vector.y * heightHalf) + heightHalf;
+ }
+ }
+
+ onMount(() => {
+ initScene();
+ animate();
+
+ window.addEventListener('click', onMouseClick);
+ });
+
+ function initScene() {
+ scene = new THREE.Scene();
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
+ camera.position.z = 5;
+
+ renderer = new THREE.WebGLRenderer({ canvas });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setClearColor(0xffffff);
+
+ labelRenderer = new CSS2DRenderer();
+ labelRenderer.setSize(window.innerWidth, window.innerHeight);
+ labelRenderer.domElement.style.position = 'absolute';
+ labelRenderer.domElement.style.top = '0';
+ labelRenderer.domElement.style.pointerEvents = 'none';
+ document.body.appendChild(labelRenderer.domElement);
+
+ raycaster = new THREE.Raycaster();
+ mouse = new THREE.Vector2();
+
+ const ambientLight = new THREE.AmbientLight(0x404040);
+ scene.add(ambientLight);
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
+ directionalLight.position.set(1, 1, 1).normalize();
+ scene.add(directionalLight);
+
+ controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ controls.dampingFactor = 0.25;
+ controls.enableRotate = true;
+
+ window.addEventListener('resize', onWindowResize, false);
+ }
+
+ function loadModel(file_path: string) {
+ const loader = new GLTFLoader();
+ loader.load(file_path, (gltf) => {
+ scene.add(gltf.scene);
+ });
+ }
+
+ function handleModelSelection(file_path: string) {
+ selectedModel = file_path;
+ localStorage.setItem('selectedModel', selectedModel);
+ loadModel(file_path);
+ }
+
+ function animate() {
+ requestAnimationFrame(animate);
+
+ // Get the canvas's bounding rect
+ if (!canvas) return;
+ const rect = canvas.getBoundingClientRect();
+ const canvasWidth = rect.width;
+ const canvasHeight = rect.height;
+
+ Object.values(annotations).forEach(({ position, labelDiv }) => {
+ const spriteScreenPosition = position.clone().project(camera);
+
+ // normalized coordinates to pixel coordinates
+ const widthHalf = canvasWidth / 2;
+ const heightHalf = canvasHeight / 2;
+ const spriteX = spriteScreenPosition.x * widthHalf + widthHalf;
+ const spriteY = -(spriteScreenPosition.y * heightHalf) + heightHalf;
+
+ // Update the label's position
+ labelDiv.style.position = 'absolute';
+ labelDiv.style.left = `${spriteX + rect.left}px`;
+ labelDiv.style.top = `${spriteY + rect.top}px`;
+
+ console.log(`Sprite Position: ${spriteX}, ${spriteY}`);
+ console.log(`Canvas Bounds: ${rect.left}, ${rect.top}, ${rect.width}, ${rect.height}`);
+
+ // Show/hide labels based on visibility
+ if (
+ spriteScreenPosition.z < 0 ||
+ spriteX < 0 ||
+ spriteX > canvasWidth ||
+ spriteY < 0 ||
+ spriteY > canvasHeight
+ ) {
+ labelDiv.style.display = 'none';
+ } else {
+ labelDiv.style.display = 'block';
+ }
+ });
+
+ // Render the scene and labels
+ renderer.render(scene, camera);
+ labelRenderer.render(scene, camera);
+ }
+
+ function onWindowResize() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ labelRenderer.setSize(window.innerWidth, window.innerHeight);
+ }
+
+
+
+
+
+
+
+
+ {#if annotationMode && activePoint}
+
+
+
+
+ {/if}
+
+
+
diff --git a/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts
index f5685adb..8e3f7ab6 100644
--- a/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts
+++ b/src/src/routes/(app)/workspaces/[workspace]/quizzes/[quiz]/+page.server.ts
@@ -38,7 +38,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
content: option.content,
points: option.points
}))
- : null // if null
+ : null
})),
workspaceID,
models
diff --git a/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts b/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts
index 31350fdd..21430624 100644
--- a/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts
+++ b/src/src/routes/(app)/workspaces/[workspace]/quizzes/page.server.test.ts
@@ -1,4 +1,4 @@
-import { fail } from '@sveltejs/kit';
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
//import mongoose from 'mongoose';
@@ -116,48 +116,5 @@ describe('Quizzes Management', () => {
expect(Quizzes.findByIdAndDelete).toHaveBeenCalledWith('123');
});
});
-
- describe('actions.toggleAvailability', () => {
- it('should toggle quiz availability successfully', async () => {
- const mockFormData = new FormData();
- mockFormData.append('quizId', '123');
- mockFormData.append('isAvailable', 'true');
-
- const mockRequest = {
- formData: vi.fn().mockResolvedValue(mockFormData)
- };
-
- const mockQuiz = {
- isAvailable: false,
- save: vi.fn()
- };
-
- (Quizzes.findById as any).mockResolvedValue(mockQuiz);
-
- const result = await quizzesModule.actions.toggleAvailability({
- request: mockRequest
- } as any);
-
- expect(result).toEqual({ success: true });
- expect(mockQuiz.isAvailable).toBe(true);
- expect(mockQuiz.save).toHaveBeenCalled();
- });
-
- it('should fail if quiz not found', async () => {
- const mockFormData = new FormData();
- mockFormData.append('quizId', '123');
- mockFormData.append('isAvailable', 'true');
-
- const mockRequest = {
- formData: vi.fn().mockResolvedValue(mockFormData)
- };
-
- (Quizzes.findById as any).mockResolvedValue(null);
-
- await quizzesModule.actions.toggleAvailability({ request: mockRequest } as any);
-
- expect(fail).toHaveBeenCalledWith(404, { error: 'Quiz not found' });
- });
- });
});
});
diff --git a/src/src/tests/unit/components/modal/createQuiz.test.ts b/src/src/tests/unit/components/modal/createQuiz.test.ts
new file mode 100644
index 00000000..b599313e
--- /dev/null
+++ b/src/src/tests/unit/components/modal/createQuiz.test.ts
@@ -0,0 +1,62 @@
+import { describe, it, expect } from 'vitest';
+import { render, fireEvent } from '@testing-library/svelte';
+import ModalComponent from '$lib/components/modals/quizzes/Add.svelte';
+
+describe('ModalComponent - Add Quiz Modal', () => {
+ const defaultProps = {
+ open: true
+ };
+
+ it('should open modal when open is true', () => {
+ const { getByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ expect(getByText('Add Quiz')).toBeInTheDocument();
+ });
+
+ it('should hide modal when open is false', () => {
+ const { queryByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: false }
+ });
+
+ expect(queryByText('Add Quiz')).not.toBeInTheDocument();
+ });
+
+
+
+ it('should submit form with valid inputs', async () => {
+ const { getByLabelText, getByRole, getByText } = render(ModalComponent, {
+ props: { ...defaultProps }
+ });
+
+ const titleInput = getByLabelText('Title') as HTMLInputElement;
+ const durationInput = getByLabelText('Duration (in minutes)') as HTMLInputElement;
+ const instructionsTextarea = getByLabelText('Add Instructions') as HTMLTextAreaElement;
+ const submitButton = getByRole('button', { name: 'Create Quiz' });
+
+
+ await fireEvent.input(titleInput, { target: { value: 'New Quiz' } });
+ await fireEvent.input(durationInput, { target: { value: '45' } });
+ await fireEvent.input(instructionsTextarea, { target: { value: 'These are instructions.' } });
+
+ // Simulate form submission
+ await fireEvent.click(submitButton);
+
+ expect(titleInput.value).toBe('New Quiz');
+ expect(durationInput.value).toBe('45');
+ expect(instructionsTextarea.value).toBe('These are instructions.');
+ });
+
+ it('should display toolbar buttons correctly', () => {
+ const { getByRole } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ expect(getByRole('button', { name: 'Attach file' })).toBeInTheDocument();
+ expect(getByRole('button', { name: 'Embed map' })).toBeInTheDocument();
+ expect(getByRole('button', { name: 'Upload image' })).toBeInTheDocument();
+ expect(getByRole('button', { name: 'Format code' })).toBeInTheDocument();
+ expect(getByRole('button', { name: 'Add emoji' })).toBeInTheDocument();
+ });
+});
diff --git a/src/src/tests/unit/components/modal/questionType.test.ts b/src/src/tests/unit/components/modal/questionType.test.ts
new file mode 100644
index 00000000..6d6e8776
--- /dev/null
+++ b/src/src/tests/unit/components/modal/questionType.test.ts
@@ -0,0 +1,67 @@
+import { describe, it, expect } from 'vitest';
+import { render, fireEvent } from '@testing-library/svelte';
+import ModalComponent from '$lib/components/modals/quizzes/Edit.svelte';
+
+describe('ModalComponent - Question Type Modal', () => {
+ const defaultProps = {
+ open: true
+ };
+
+ it('should open modal when open is true', () => {
+ const { getByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ expect(getByText('Choose Question Type')).toBeInTheDocument();
+ });
+
+ it('should display "Add" button and disable it when no option is selected', () => {
+ const { getByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ const addButton = getByText('Add');
+ expect(addButton).toBeInTheDocument();
+ expect(addButton).toBeDisabled();
+ });
+
+ it('should enable "Add" button when a question type is selected', async () => {
+ const { getByText, getByLabelText } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ const selectInput = getByLabelText('Select Question Type');
+ const addButton = getByText('Add');
+
+ // Initially disabled
+ expect(addButton).toBeDisabled();
+
+ // Simulate selecting a question type
+ await fireEvent.change(selectInput, { target: { value: 'multiple-choice' } });
+
+ // Now the button should be enabled
+ expect(addButton).not.toBeDisabled();
+ });
+
+ it('should close modal when "Add" button is clicked with a valid selection', async () => {
+ const { getByText, getByLabelText, component } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ const selectInput = getByLabelText('Select Question Type');
+ const addButton = getByText('Add');
+
+ // Mock the dispatch event listener
+ let dispatchedEvent = null;
+ component.$on('select', (event) => {
+ dispatchedEvent = event.detail;
+ });
+
+ // Simulate selecting a question type and clicking the "Add" button
+ await fireEvent.change(selectInput, { target: { value: '3d-hotspot' } });
+ await fireEvent.click(addButton);
+
+ // Check if the event was dispatched correctly with the selected type
+ expect(dispatchedEvent).toEqual({ type: '3d-hotspot' });
+ });
+});
diff --git a/src/src/tests/unit/components/modal/submission.test.ts b/src/src/tests/unit/components/modal/submission.test.ts
new file mode 100644
index 00000000..08bab411
--- /dev/null
+++ b/src/src/tests/unit/components/modal/submission.test.ts
@@ -0,0 +1,45 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent } from '@testing-library/svelte';
+import ModalComponent from '$lib/components/modals/quizzes/Submission.svelte'; // Adjust the import path as needed
+
+// Mock the navigation function
+vi.mock('$utils/navigation', () => ({
+ navigateToParentRoute: vi.fn()
+}));
+
+describe('ModalComponent - Quiz Submission Modal', () => {
+ const defaultProps = {
+ open: true,
+ submissionMessage: 'Your quiz has been submitted successfully!',
+ percentageScore: 85.5
+ };
+
+ it('should open modal when open is true', () => {
+ const { getByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ expect(getByText('Quiz Submission')).toBeInTheDocument();
+ });
+
+ it('should hide modal when open is false', () => {
+ const { queryByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: false }
+ });
+
+ expect(queryByText('Quiz Submission')).not.toBeInTheDocument();
+ });
+
+
+
+ it('should display submission message and percentage score correctly', () => {
+ const { getByText } = render(ModalComponent, {
+ props: defaultProps
+ });
+
+ expect(getByText('Your quiz has been submitted successfully!')).toBeInTheDocument();
+ expect(getByText('Percentage Score: 85.50%')).toBeInTheDocument();
+ });
+
+
+});
diff --git a/src/src/tests/unit/components/modal/timeElasped.test.ts b/src/src/tests/unit/components/modal/timeElasped.test.ts
new file mode 100644
index 00000000..fe0fedf0
--- /dev/null
+++ b/src/src/tests/unit/components/modal/timeElasped.test.ts
@@ -0,0 +1,45 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render, fireEvent } from '@testing-library/svelte';
+import ModalComponent from '$lib/components/modals/quizzes/TimeElapsed.svelte'; // Adjust the import path as needed
+
+// Mock the navigation function
+vi.mock('$utils/navigation', () => ({
+ navigateToParentRoute: vi.fn()
+}));
+
+describe('ModalComponent - Time Elapsed Modal', () => {
+ const defaultProps = {
+ open: true,
+ submissionMessage: 'Time is up! You have completed the quiz.',
+ totalPoints: 85
+ };
+
+ it('should open modal when open is true', () => {
+ const { getByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: true }
+ });
+
+ expect(getByText('Time Elapsed')).toBeInTheDocument();
+ });
+
+ it('should hide modal when open is false', () => {
+ const { queryByText } = render(ModalComponent, {
+ props: { ...defaultProps, open: false }
+ });
+
+ expect(queryByText('Time Elapsed')).not.toBeInTheDocument();
+ });
+
+
+
+ it('should display submission message and total points correctly', () => {
+ const { getByText } = render(ModalComponent, {
+ props: defaultProps
+ });
+
+ expect(getByText('Time is up! You have completed the quiz.')).toBeInTheDocument();
+ expect(getByText('Total Points: 85')).toBeInTheDocument();
+ });
+
+
+});
diff --git a/src/static/images/circle.png b/src/static/images/circle.png
new file mode 100644
index 00000000..8d5722a6
Binary files /dev/null and b/src/static/images/circle.png differ