Shih-hungg commited on
Commit
5a88b6d
·
1 Parent(s): 1269f33

Refactor the question card

Browse files
package-lock.json CHANGED
@@ -8,9 +8,13 @@
8
  "name": "quizflash",
9
  "version": "0.1.0",
10
  "dependencies": {
 
 
 
11
  "next": "15.4.5",
12
  "react": "19.1.0",
13
- "react-dom": "19.1.0"
 
14
  },
15
  "devDependencies": {
16
  "@eslint/eslintrc": "^3",
@@ -24,6 +28,109 @@
24
  "typescript": "^5"
25
  }
26
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  "node_modules/@alloc/quick-lru": {
28
  "version": "5.2.0",
29
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -634,6 +741,14 @@
634
  "node": ">=12.4.0"
635
  }
636
  },
 
 
 
 
 
 
 
 
637
  "node_modules/@rtsao/scc": {
638
  "version": "1.1.0",
639
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -646,6 +761,11 @@
646
  "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
647
  "dev": true
648
  },
 
 
 
 
 
649
  "node_modules/@swc/helpers": {
650
  "version": "0.5.15",
651
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1351,6 +1471,55 @@
1351
  "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1352
  }
1353
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1354
  "node_modules/ajv": {
1355
  "version": "6.12.6",
1356
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/ajv/-/ajv-6.12.6.tgz",
@@ -1842,6 +2011,14 @@
1842
  "node": ">= 0.4"
1843
  }
1844
  },
 
 
 
 
 
 
 
 
1845
  "node_modules/detect-libc": {
1846
  "version": "2.0.4",
1847
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -2454,6 +2631,14 @@
2454
  "node": ">=0.10.0"
2455
  }
2456
  },
 
 
 
 
 
 
 
 
2457
  "node_modules/fast-deep-equal": {
2458
  "version": "3.1.3",
2459
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3212,6 +3397,11 @@
3212
  "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
3213
  "dev": true
3214
  },
 
 
 
 
 
3215
  "node_modules/json-schema-traverse": {
3216
  "version": "0.4.1",
3217
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -4463,6 +4653,14 @@
4463
  "node": ">=18"
4464
  }
4465
  },
 
 
 
 
 
 
 
 
4466
  "node_modules/tinyglobby": {
4467
  "version": "0.2.14",
4468
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -4805,9 +5003,93 @@
4805
  "engines": {
4806
  "node": ">=10"
4807
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
4808
  }
4809
  },
4810
  "dependencies": {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4811
  "@alloc/quick-lru": {
4812
  "version": "5.2.0",
4813
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -5287,6 +5569,11 @@
5287
  "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
5288
  "dev": true
5289
  },
 
 
 
 
 
5290
  "@rtsao/scc": {
5291
  "version": "1.1.0",
5292
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -5299,6 +5586,11 @@
5299
  "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
5300
  "dev": true
5301
  },
 
 
 
 
 
5302
  "@swc/helpers": {
5303
  "version": "0.5.15",
5304
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -5874,6 +6166,39 @@
5874
  "dev": true,
5875
  "requires": {}
5876
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5877
  "ajv": {
5878
  "version": "6.12.6",
5879
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/ajv/-/ajv-6.12.6.tgz",
@@ -6270,6 +6595,11 @@
6270
  "object-keys": "^1.1.1"
6271
  }
6272
  },
 
 
 
 
 
6273
  "detect-libc": {
6274
  "version": "2.0.4",
6275
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/detect-libc/-/detect-libc-2.0.4.tgz",
@@ -6751,6 +7081,11 @@
6751
  "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
6752
  "dev": true
6753
  },
 
 
 
 
 
6754
  "fast-deep-equal": {
6755
  "version": "3.1.3",
6756
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -7352,6 +7687,11 @@
7352
  "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
7353
  "dev": true
7354
  },
 
 
 
 
 
7355
  "json-schema-traverse": {
7356
  "version": "0.4.1",
7357
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -8297,6 +8637,11 @@
8297
  "yallist": "^5.0.0"
8298
  }
8299
  },
 
 
 
 
 
8300
  "tinyglobby": {
8301
  "version": "0.2.14",
8302
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -8565,6 +8910,17 @@
8565
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/yocto-queue/-/yocto-queue-0.1.0.tgz",
8566
  "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
8567
  "dev": true
 
 
 
 
 
 
 
 
 
 
 
8568
  }
8569
  }
8570
  }
 
8
  "name": "quizflash",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@ai-sdk/openai": "^2.0.0",
12
+ "@ai-sdk/react": "^2.0.1",
13
+ "ai": "^5.0.1",
14
  "next": "15.4.5",
15
  "react": "19.1.0",
16
+ "react-dom": "19.1.0",
17
+ "zod": "^3.25.76"
18
  },
19
  "devDependencies": {
20
  "@eslint/eslintrc": "^3",
 
28
  "typescript": "^5"
29
  }
30
  },
31
+ "node_modules/@ai-sdk/openai": {
32
+ "version": "2.0.0",
33
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/openai/-/openai-2.0.0.tgz",
34
+ "integrity": "sha512-G0WY5K81JwGpuX9HEmP2VTdt3N9m43qPnGT4fWkXcpu6Y2B05nnjs8k1r/csCJd8+TkYC6esjBABQYHxdMOejQ==",
35
+ "dependencies": {
36
+ "@ai-sdk/provider": "2.0.0",
37
+ "@ai-sdk/provider-utils": "3.0.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "peerDependencies": {
43
+ "zod": "^3.25.76 || ^4"
44
+ }
45
+ },
46
+ "node_modules/@ai-sdk/openai/node_modules/@ai-sdk/provider-utils": {
47
+ "version": "3.0.0",
48
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider-utils/-/provider-utils-3.0.0.tgz",
49
+ "integrity": "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==",
50
+ "dependencies": {
51
+ "@ai-sdk/provider": "2.0.0",
52
+ "@standard-schema/spec": "^1.0.0",
53
+ "eventsource-parser": "^3.0.3",
54
+ "zod-to-json-schema": "^3.24.1"
55
+ },
56
+ "engines": {
57
+ "node": ">=18"
58
+ },
59
+ "peerDependencies": {
60
+ "zod": "^3.25.76 || ^4"
61
+ }
62
+ },
63
+ "node_modules/@ai-sdk/provider": {
64
+ "version": "2.0.0",
65
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider/-/provider-2.0.0.tgz",
66
+ "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==",
67
+ "dependencies": {
68
+ "json-schema": "^0.4.0"
69
+ },
70
+ "engines": {
71
+ "node": ">=18"
72
+ }
73
+ },
74
+ "node_modules/@ai-sdk/react": {
75
+ "version": "2.0.1",
76
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/react/-/react-2.0.1.tgz",
77
+ "integrity": "sha512-eq2ABukavfeD2mK0ovGrg8hBVynVJ7P42KnfGBScooef0264TLicM6Hv8yahOzwl/kUTBJlot8jYe3t3rWs8lg==",
78
+ "dependencies": {
79
+ "@ai-sdk/provider-utils": "3.0.0",
80
+ "ai": "5.0.1",
81
+ "swr": "^2.2.5",
82
+ "throttleit": "2.1.0"
83
+ },
84
+ "engines": {
85
+ "node": ">=18"
86
+ },
87
+ "peerDependencies": {
88
+ "react": "^18 || ^19 || ^19.0.0-rc",
89
+ "zod": "^3.25.76 || ^4"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "zod": {
93
+ "optional": true
94
+ }
95
+ }
96
+ },
97
+ "node_modules/@ai-sdk/react/node_modules/@ai-sdk/provider-utils": {
98
+ "version": "3.0.0",
99
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider-utils/-/provider-utils-3.0.0.tgz",
100
+ "integrity": "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==",
101
+ "dependencies": {
102
+ "@ai-sdk/provider": "2.0.0",
103
+ "@standard-schema/spec": "^1.0.0",
104
+ "eventsource-parser": "^3.0.3",
105
+ "zod-to-json-schema": "^3.24.1"
106
+ },
107
+ "engines": {
108
+ "node": ">=18"
109
+ },
110
+ "peerDependencies": {
111
+ "zod": "^3.25.76 || ^4"
112
+ }
113
+ },
114
+ "node_modules/@ai-sdk/react/node_modules/swr": {
115
+ "version": "2.3.4",
116
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/swr/-/swr-2.3.4.tgz",
117
+ "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==",
118
+ "dependencies": {
119
+ "dequal": "^2.0.3",
120
+ "use-sync-external-store": "^1.4.0"
121
+ },
122
+ "peerDependencies": {
123
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
124
+ }
125
+ },
126
+ "node_modules/@ai-sdk/react/node_modules/swr/node_modules/use-sync-external-store": {
127
+ "version": "1.5.0",
128
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
129
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
130
+ "peerDependencies": {
131
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
132
+ }
133
+ },
134
  "node_modules/@alloc/quick-lru": {
135
  "version": "5.2.0",
136
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
 
741
  "node": ">=12.4.0"
742
  }
743
  },
744
+ "node_modules/@opentelemetry/api": {
745
+ "version": "1.9.0",
746
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@opentelemetry/api/-/api-1.9.0.tgz",
747
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
748
+ "engines": {
749
+ "node": ">=8.0.0"
750
+ }
751
+ },
752
  "node_modules/@rtsao/scc": {
753
  "version": "1.1.0",
754
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@rtsao/scc/-/scc-1.1.0.tgz",
 
761
  "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
762
  "dev": true
763
  },
764
+ "node_modules/@standard-schema/spec": {
765
+ "version": "1.0.0",
766
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@standard-schema/spec/-/spec-1.0.0.tgz",
767
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="
768
+ },
769
  "node_modules/@swc/helpers": {
770
  "version": "0.5.15",
771
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@swc/helpers/-/helpers-0.5.15.tgz",
 
1471
  "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1472
  }
1473
  },
1474
+ "node_modules/ai": {
1475
+ "version": "5.0.1",
1476
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/ai/-/ai-5.0.1.tgz",
1477
+ "integrity": "sha512-h3bGaysgeD0N/jFnCwvtKP8oyhv0Put8KtouYl8k03oq822rlsNxz58/LWF1FZt+0DUU77vdq47B6P94+ETwzw==",
1478
+ "dependencies": {
1479
+ "@ai-sdk/gateway": "1.0.0",
1480
+ "@ai-sdk/provider": "2.0.0",
1481
+ "@ai-sdk/provider-utils": "3.0.0",
1482
+ "@opentelemetry/api": "1.9.0"
1483
+ },
1484
+ "engines": {
1485
+ "node": ">=18"
1486
+ },
1487
+ "peerDependencies": {
1488
+ "zod": "^3.25.76 || ^4"
1489
+ }
1490
+ },
1491
+ "node_modules/ai/node_modules/@ai-sdk/gateway": {
1492
+ "version": "1.0.0",
1493
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/gateway/-/gateway-1.0.0.tgz",
1494
+ "integrity": "sha512-VEm87DyRx1yIPywbTy8ntoyh4jEDv1rJ88m+2I7zOm08jJI5BhFtAWh0OF6YzZu1Vu4NxhOWO4ssGdsqydDQ3A==",
1495
+ "dependencies": {
1496
+ "@ai-sdk/provider": "2.0.0",
1497
+ "@ai-sdk/provider-utils": "3.0.0"
1498
+ },
1499
+ "engines": {
1500
+ "node": ">=18"
1501
+ },
1502
+ "peerDependencies": {
1503
+ "zod": "^3.25.76 || ^4"
1504
+ }
1505
+ },
1506
+ "node_modules/ai/node_modules/@ai-sdk/provider-utils": {
1507
+ "version": "3.0.0",
1508
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider-utils/-/provider-utils-3.0.0.tgz",
1509
+ "integrity": "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==",
1510
+ "dependencies": {
1511
+ "@ai-sdk/provider": "2.0.0",
1512
+ "@standard-schema/spec": "^1.0.0",
1513
+ "eventsource-parser": "^3.0.3",
1514
+ "zod-to-json-schema": "^3.24.1"
1515
+ },
1516
+ "engines": {
1517
+ "node": ">=18"
1518
+ },
1519
+ "peerDependencies": {
1520
+ "zod": "^3.25.76 || ^4"
1521
+ }
1522
+ },
1523
  "node_modules/ajv": {
1524
  "version": "6.12.6",
1525
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/ajv/-/ajv-6.12.6.tgz",
 
2011
  "node": ">= 0.4"
2012
  }
2013
  },
2014
+ "node_modules/dequal": {
2015
+ "version": "2.0.3",
2016
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/dequal/-/dequal-2.0.3.tgz",
2017
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
2018
+ "engines": {
2019
+ "node": ">=6"
2020
+ }
2021
+ },
2022
  "node_modules/detect-libc": {
2023
  "version": "2.0.4",
2024
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/detect-libc/-/detect-libc-2.0.4.tgz",
 
2631
  "node": ">=0.10.0"
2632
  }
2633
  },
2634
+ "node_modules/eventsource-parser": {
2635
+ "version": "3.0.3",
2636
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/eventsource-parser/-/eventsource-parser-3.0.3.tgz",
2637
+ "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==",
2638
+ "engines": {
2639
+ "node": ">=20.0.0"
2640
+ }
2641
+ },
2642
  "node_modules/fast-deep-equal": {
2643
  "version": "3.1.3",
2644
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 
3397
  "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
3398
  "dev": true
3399
  },
3400
+ "node_modules/json-schema": {
3401
+ "version": "0.4.0",
3402
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/json-schema/-/json-schema-0.4.0.tgz",
3403
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
3404
+ },
3405
  "node_modules/json-schema-traverse": {
3406
  "version": "0.4.1",
3407
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
 
4653
  "node": ">=18"
4654
  }
4655
  },
4656
+ "node_modules/throttleit": {
4657
+ "version": "2.1.0",
4658
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/throttleit/-/throttleit-2.1.0.tgz",
4659
+ "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
4660
+ "engines": {
4661
+ "node": ">=18"
4662
+ }
4663
+ },
4664
  "node_modules/tinyglobby": {
4665
  "version": "0.2.14",
4666
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/tinyglobby/-/tinyglobby-0.2.14.tgz",
 
5003
  "engines": {
5004
  "node": ">=10"
5005
  }
5006
+ },
5007
+ "node_modules/zod": {
5008
+ "version": "3.25.76",
5009
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/zod/-/zod-3.25.76.tgz",
5010
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
5011
+ },
5012
+ "node_modules/zod-to-json-schema": {
5013
+ "version": "3.24.6",
5014
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
5015
+ "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
5016
+ "peerDependencies": {
5017
+ "zod": "^3.24.1"
5018
+ }
5019
  }
5020
  },
5021
  "dependencies": {
5022
+ "@ai-sdk/openai": {
5023
+ "version": "2.0.0",
5024
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/openai/-/openai-2.0.0.tgz",
5025
+ "integrity": "sha512-G0WY5K81JwGpuX9HEmP2VTdt3N9m43qPnGT4fWkXcpu6Y2B05nnjs8k1r/csCJd8+TkYC6esjBABQYHxdMOejQ==",
5026
+ "requires": {
5027
+ "@ai-sdk/provider": "2.0.0",
5028
+ "@ai-sdk/provider-utils": "3.0.0"
5029
+ },
5030
+ "dependencies": {
5031
+ "@ai-sdk/provider-utils": {
5032
+ "version": "3.0.0",
5033
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider-utils/-/provider-utils-3.0.0.tgz",
5034
+ "integrity": "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==",
5035
+ "requires": {
5036
+ "@ai-sdk/provider": "2.0.0",
5037
+ "@standard-schema/spec": "^1.0.0",
5038
+ "eventsource-parser": "^3.0.3",
5039
+ "zod-to-json-schema": "^3.24.1"
5040
+ }
5041
+ }
5042
+ }
5043
+ },
5044
+ "@ai-sdk/provider": {
5045
+ "version": "2.0.0",
5046
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider/-/provider-2.0.0.tgz",
5047
+ "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==",
5048
+ "requires": {
5049
+ "json-schema": "^0.4.0"
5050
+ }
5051
+ },
5052
+ "@ai-sdk/react": {
5053
+ "version": "2.0.1",
5054
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/react/-/react-2.0.1.tgz",
5055
+ "integrity": "sha512-eq2ABukavfeD2mK0ovGrg8hBVynVJ7P42KnfGBScooef0264TLicM6Hv8yahOzwl/kUTBJlot8jYe3t3rWs8lg==",
5056
+ "requires": {
5057
+ "@ai-sdk/provider-utils": "3.0.0",
5058
+ "ai": "5.0.1",
5059
+ "swr": "^2.2.5",
5060
+ "throttleit": "2.1.0"
5061
+ },
5062
+ "dependencies": {
5063
+ "@ai-sdk/provider-utils": {
5064
+ "version": "3.0.0",
5065
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider-utils/-/provider-utils-3.0.0.tgz",
5066
+ "integrity": "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==",
5067
+ "requires": {
5068
+ "@ai-sdk/provider": "2.0.0",
5069
+ "@standard-schema/spec": "^1.0.0",
5070
+ "eventsource-parser": "^3.0.3",
5071
+ "zod-to-json-schema": "^3.24.1"
5072
+ }
5073
+ },
5074
+ "swr": {
5075
+ "version": "2.3.4",
5076
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/swr/-/swr-2.3.4.tgz",
5077
+ "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==",
5078
+ "requires": {
5079
+ "dequal": "^2.0.3",
5080
+ "use-sync-external-store": "^1.4.0"
5081
+ },
5082
+ "dependencies": {
5083
+ "use-sync-external-store": {
5084
+ "version": "1.5.0",
5085
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
5086
+ "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
5087
+ "requires": {}
5088
+ }
5089
+ }
5090
+ }
5091
+ }
5092
+ },
5093
  "@alloc/quick-lru": {
5094
  "version": "5.2.0",
5095
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
 
5569
  "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==",
5570
  "dev": true
5571
  },
5572
+ "@opentelemetry/api": {
5573
+ "version": "1.9.0",
5574
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@opentelemetry/api/-/api-1.9.0.tgz",
5575
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="
5576
+ },
5577
  "@rtsao/scc": {
5578
  "version": "1.1.0",
5579
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@rtsao/scc/-/scc-1.1.0.tgz",
 
5586
  "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
5587
  "dev": true
5588
  },
5589
+ "@standard-schema/spec": {
5590
+ "version": "1.0.0",
5591
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@standard-schema/spec/-/spec-1.0.0.tgz",
5592
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="
5593
+ },
5594
  "@swc/helpers": {
5595
  "version": "0.5.15",
5596
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@swc/helpers/-/helpers-0.5.15.tgz",
 
6166
  "dev": true,
6167
  "requires": {}
6168
  },
6169
+ "ai": {
6170
+ "version": "5.0.1",
6171
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/ai/-/ai-5.0.1.tgz",
6172
+ "integrity": "sha512-h3bGaysgeD0N/jFnCwvtKP8oyhv0Put8KtouYl8k03oq822rlsNxz58/LWF1FZt+0DUU77vdq47B6P94+ETwzw==",
6173
+ "requires": {
6174
+ "@ai-sdk/gateway": "1.0.0",
6175
+ "@ai-sdk/provider": "2.0.0",
6176
+ "@ai-sdk/provider-utils": "3.0.0",
6177
+ "@opentelemetry/api": "1.9.0"
6178
+ },
6179
+ "dependencies": {
6180
+ "@ai-sdk/gateway": {
6181
+ "version": "1.0.0",
6182
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/gateway/-/gateway-1.0.0.tgz",
6183
+ "integrity": "sha512-VEm87DyRx1yIPywbTy8ntoyh4jEDv1rJ88m+2I7zOm08jJI5BhFtAWh0OF6YzZu1Vu4NxhOWO4ssGdsqydDQ3A==",
6184
+ "requires": {
6185
+ "@ai-sdk/provider": "2.0.0",
6186
+ "@ai-sdk/provider-utils": "3.0.0"
6187
+ }
6188
+ },
6189
+ "@ai-sdk/provider-utils": {
6190
+ "version": "3.0.0",
6191
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/@ai-sdk/provider-utils/-/provider-utils-3.0.0.tgz",
6192
+ "integrity": "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw==",
6193
+ "requires": {
6194
+ "@ai-sdk/provider": "2.0.0",
6195
+ "@standard-schema/spec": "^1.0.0",
6196
+ "eventsource-parser": "^3.0.3",
6197
+ "zod-to-json-schema": "^3.24.1"
6198
+ }
6199
+ }
6200
+ }
6201
+ },
6202
  "ajv": {
6203
  "version": "6.12.6",
6204
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/ajv/-/ajv-6.12.6.tgz",
 
6595
  "object-keys": "^1.1.1"
6596
  }
6597
  },
6598
+ "dequal": {
6599
+ "version": "2.0.3",
6600
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/dequal/-/dequal-2.0.3.tgz",
6601
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="
6602
+ },
6603
  "detect-libc": {
6604
  "version": "2.0.4",
6605
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/detect-libc/-/detect-libc-2.0.4.tgz",
 
7081
  "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
7082
  "dev": true
7083
  },
7084
+ "eventsource-parser": {
7085
+ "version": "3.0.3",
7086
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/eventsource-parser/-/eventsource-parser-3.0.3.tgz",
7087
+ "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="
7088
+ },
7089
  "fast-deep-equal": {
7090
  "version": "3.1.3",
7091
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
 
7687
  "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
7688
  "dev": true
7689
  },
7690
+ "json-schema": {
7691
+ "version": "0.4.0",
7692
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/json-schema/-/json-schema-0.4.0.tgz",
7693
+ "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
7694
+ },
7695
  "json-schema-traverse": {
7696
  "version": "0.4.1",
7697
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
 
8637
  "yallist": "^5.0.0"
8638
  }
8639
  },
8640
+ "throttleit": {
8641
+ "version": "2.1.0",
8642
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/throttleit/-/throttleit-2.1.0.tgz",
8643
+ "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="
8644
+ },
8645
  "tinyglobby": {
8646
  "version": "0.2.14",
8647
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/tinyglobby/-/tinyglobby-0.2.14.tgz",
 
8910
  "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/yocto-queue/-/yocto-queue-0.1.0.tgz",
8911
  "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
8912
  "dev": true
8913
+ },
8914
+ "zod": {
8915
+ "version": "3.25.76",
8916
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/zod/-/zod-3.25.76.tgz",
8917
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
8918
+ },
8919
+ "zod-to-json-schema": {
8920
+ "version": "3.24.6",
8921
+ "resolved": "http://artifactory-build.taboolasyndication.com:80/artifactory/api/npm/npm-public/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
8922
+ "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
8923
+ "requires": {}
8924
  }
8925
  }
8926
  }
package.json CHANGED
@@ -9,19 +9,23 @@
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
 
 
 
 
12
  "react": "19.1.0",
13
  "react-dom": "19.1.0",
14
- "next": "15.4.5"
15
  },
16
  "devDependencies": {
17
- "typescript": "^5",
 
18
  "@types/node": "^20",
19
  "@types/react": "^19",
20
  "@types/react-dom": "^19",
21
- "@tailwindcss/postcss": "^4",
22
- "tailwindcss": "^4",
23
  "eslint": "^9",
24
  "eslint-config-next": "15.4.5",
25
- "@eslint/eslintrc": "^3"
 
26
  }
27
  }
 
9
  "lint": "next lint"
10
  },
11
  "dependencies": {
12
+ "@ai-sdk/openai": "^2.0.0",
13
+ "@ai-sdk/react": "^2.0.1",
14
+ "ai": "^5.0.1",
15
+ "next": "15.4.5",
16
  "react": "19.1.0",
17
  "react-dom": "19.1.0",
18
+ "zod": "^3.25.76"
19
  },
20
  "devDependencies": {
21
+ "@eslint/eslintrc": "^3",
22
+ "@tailwindcss/postcss": "^4",
23
  "@types/node": "^20",
24
  "@types/react": "^19",
25
  "@types/react-dom": "^19",
 
 
26
  "eslint": "^9",
27
  "eslint-config-next": "15.4.5",
28
+ "tailwindcss": "^4",
29
+ "typescript": "^5"
30
  }
31
  }
src/app/api/chat/route.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { openai } from '@ai-sdk/openai';
2
+ import { streamText } from 'ai';
3
+
4
+ // Allow streaming responses up to 30 seconds
5
+ export const maxDuration = 30;
6
+
7
+ export async function POST(req: Request) {
8
+ const { messages } = await req.json();
9
+
10
+ const result = await streamText({
11
+ model: openai('gpt-4o-mini'),
12
+ messages,
13
+ });
14
+
15
+ return result.toDataStreamResponse();
16
+ }
src/app/api/generate-question/route.ts ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { openai } from '@ai-sdk/openai';
2
+ import { generateObject } from 'ai';
3
+ import { z } from 'zod';
4
+
5
+ // Schemas for different question types
6
+ const MultipleChoiceSchema = z.object({
7
+ stem: z.string(),
8
+ options: z.array(z.string()),
9
+ correctAnswer: z.number(),
10
+ explanation: z.string(),
11
+ points: z.number().default(1),
12
+ });
13
+
14
+ const ClozeSchema = z.object({
15
+ stem: z.string(),
16
+ blanks: z.array(z.object({
17
+ position: z.number(),
18
+ answer: z.string(),
19
+ alternatives: z.array(z.string()).optional(),
20
+ })),
21
+ explanation: z.string(),
22
+ points: z.number().default(1),
23
+ });
24
+
25
+ const GrammarSchema = z.object({
26
+ stem: z.string(),
27
+ incorrectSentence: z.string(),
28
+ correctSentence: z.string(),
29
+ errors: z.array(z.object({
30
+ type: z.string(),
31
+ position: z.number(),
32
+ correction: z.string(),
33
+ })),
34
+ explanation: z.string(),
35
+ points: z.number().default(1),
36
+ });
37
+
38
+ const EssaySchema = z.object({
39
+ stem: z.string(),
40
+ prompt: z.string(),
41
+ rubric: z.array(z.object({
42
+ criterion: z.string(),
43
+ description: z.string(),
44
+ points: z.number(),
45
+ })),
46
+ sampleAnswer: z.string().optional(),
47
+ points: z.number().default(5),
48
+ });
49
+
50
+ export async function POST(req: Request) {
51
+ try {
52
+ const { type, parameters, sourceMaterial } = await req.json();
53
+
54
+ let schema;
55
+ let prompt = '';
56
+
57
+ switch (type) {
58
+ case 'multiple-choice':
59
+ schema = MultipleChoiceSchema;
60
+ prompt = `Create a ${parameters.difficulty || 'intermediate'} level multiple choice question with ${parameters.numOptions || 4} options.
61
+ ${sourceMaterial ? `Base the question on this source material: "${sourceMaterial}"` : 'Create an educational question on any relevant topic.'}
62
+ Make sure one answer is clearly correct and the others are plausible distractors.
63
+ Provide a clear explanation for why the correct answer is right.`;
64
+ break;
65
+
66
+ case 'cloze':
67
+ schema = ClozeSchema;
68
+ prompt = `Create a cloze (fill-in-the-blank) question with ${parameters.numBlanks || 1} blank(s).
69
+ ${parameters.partOfSpeech !== 'any' ? `Focus on ${parameters.partOfSpeech} words.` : ''}
70
+ ${sourceMaterial ? `Use this source material: "${sourceMaterial}"` : 'Create an educational text.'}
71
+ Mark blanks with their position and provide the correct answers.
72
+ Include alternative acceptable answers where appropriate.`;
73
+ break;
74
+
75
+ case 'grammar':
76
+ schema = GrammarSchema;
77
+ prompt = `Create a grammar correction question at ${parameters.difficulty || 'intermediate'} level.
78
+ Present a sentence with grammatical errors for students to identify and correct.
79
+ ${sourceMaterial ? `Base it on this text style: "${sourceMaterial}"` : 'Use common grammatical concepts.'}
80
+ Specify the type of error, its position, and the correction needed.`;
81
+ break;
82
+
83
+ case 'essay':
84
+ schema = EssaySchema;
85
+ prompt = `Create an essay question at ${parameters.difficulty || 'intermediate'} level.
86
+ ${sourceMaterial ? `Base the essay prompt on this source material: "${sourceMaterial}"` : 'Create a thought-provoking prompt.'}
87
+ Include a detailed rubric with specific criteria and point values.
88
+ Optionally provide a sample answer outline.`;
89
+ break;
90
+
91
+ case 'reading-comp':
92
+ schema = MultipleChoiceSchema;
93
+ prompt = `Create a reading comprehension question based on the provided text.
94
+ ${sourceMaterial || 'Please provide source material for reading comprehension questions.'}
95
+ Create a question that tests understanding, inference, or analysis of the text.
96
+ Include ${parameters.numOptions || 4} options with clear explanations.`;
97
+ break;
98
+
99
+ default:
100
+ return Response.json(
101
+ { error: 'Unsupported question type' },
102
+ { status: 400 }
103
+ );
104
+ }
105
+
106
+ const result = await generateObject({
107
+ model: openai('gpt-4o-mini'),
108
+ schema,
109
+ prompt,
110
+ });
111
+
112
+ return Response.json({
113
+ type,
114
+ ...result.object,
115
+ });
116
+
117
+ } catch (error) {
118
+ console.error('Error generating question:', error);
119
+ return Response.json(
120
+ { error: 'Failed to generate question' },
121
+ { status: 500 }
122
+ );
123
+ }
124
+ }
src/app/api/generate-quiz/route.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { openai } from '@ai-sdk/openai';
2
+ import { generateObject } from 'ai';
3
+ import { z } from 'zod';
4
+ import { generateQuizPrompt } from '@/prompts/quiz-generation';
5
+
6
+ // Schema for quiz questions
7
+ const QuizSchema = z.object({
8
+ questions: z.array(
9
+ z.object({
10
+ question: z.string(),
11
+ options: z.array(z.string()).length(4),
12
+ correctAnswer: z.number().min(0).max(3),
13
+ explanation: z.string(),
14
+ })
15
+ ),
16
+ });
17
+
18
+ export async function POST(req: Request) {
19
+ try {
20
+ const { topic, difficulty, numQuestions } = await req.json();
21
+
22
+ const result = await generateObject({
23
+ model: openai('gpt-4o-mini'),
24
+ schema: QuizSchema,
25
+ prompt: generateQuizPrompt({ topic, difficulty, numQuestions }, 'basic'),
26
+ });
27
+
28
+ return Response.json(result.object);
29
+ } catch (error) {
30
+ console.error('Error generating quiz:', error);
31
+ return Response.json(
32
+ { error: 'Failed to generate quiz' },
33
+ { status: 500 }
34
+ );
35
+ }
36
+ }
src/app/layout.tsx CHANGED
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
13
  });
14
 
15
  export const metadata: Metadata = {
16
- title: "Create Next App",
17
- description: "Generated by create next app",
18
  };
19
 
20
  export default function RootLayout({
 
13
  });
14
 
15
  export const metadata: Metadata = {
16
+ title: "QuizFlash - AI-Powered Quiz Generation",
17
+ description: "Generate interactive quizzes on any topic using AI. Test your knowledge with QuizFlash!",
18
  };
19
 
20
  export default function RootLayout({
src/app/page.tsx CHANGED
@@ -1,103 +1,319 @@
1
- import Image from "next/image";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- export default function Home() {
4
  return (
5
- <div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
6
- <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
7
- <Image
8
- className="dark:invert"
9
- src="/next.svg"
10
- alt="Next.js logo"
11
- width={180}
12
- height={38}
13
- priority
14
- />
15
- <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
16
- <li className="mb-2 tracking-[-.01em]">
17
- Get started by editing{" "}
18
- <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
19
- src/app/page.tsx
20
- </code>
21
- .
22
- </li>
23
- <li className="tracking-[-.01em]">
24
- Save and see your changes instantly.
25
- </li>
26
- </ol>
27
-
28
- <div className="flex gap-4 items-center flex-col sm:flex-row">
29
- <a
30
- className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
31
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
32
- target="_blank"
33
- rel="noopener noreferrer"
34
- >
35
- <Image
36
- className="dark:invert"
37
- src="/vercel.svg"
38
- alt="Vercel logomark"
39
- width={20}
40
- height={20}
41
- />
42
- Deploy now
43
- </a>
44
- <a
45
- className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
46
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
47
- target="_blank"
48
- rel="noopener noreferrer"
49
- >
50
- Read our docs
51
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
- </main>
54
- <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
55
- <a
56
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
57
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
58
- target="_blank"
59
- rel="noopener noreferrer"
60
- >
61
- <Image
62
- aria-hidden
63
- src="/file.svg"
64
- alt="File icon"
65
- width={16}
66
- height={16}
67
- />
68
- Learn
69
- </a>
70
- <a
71
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
72
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
73
- target="_blank"
74
- rel="noopener noreferrer"
75
- >
76
- <Image
77
- aria-hidden
78
- src="/window.svg"
79
- alt="Window icon"
80
- width={16}
81
- height={16}
82
- />
83
- Examples
84
- </a>
85
- <a
86
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
87
- href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
88
- target="_blank"
89
- rel="noopener noreferrer"
90
- >
91
- <Image
92
- aria-hidden
93
- src="/globe.svg"
94
- alt="Globe icon"
95
- width={16}
96
- height={16}
97
- />
98
- Go to nextjs.org →
99
- </a>
100
- </footer>
101
  </div>
102
  );
103
  }
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useChat } from '@ai-sdk/react';
5
+ import QuestionParameterForm from '@/components/QuestionParameterForm';
6
+ import { QuestionType, QuestionParameters, GeneratedQuestion } from '@/types/quiz';
7
+
8
+ const QUESTION_TYPES: QuestionType[] = [
9
+ { id: 'multiple-choice', name: 'Multiple Choice', icon: '☑️', description: 'Traditional A, B, C, D questions' },
10
+ { id: 'cloze', name: 'Cloze (Fill-in-the-blank)', icon: '🔤', description: 'Fill in missing words or phrases' },
11
+ { id: 'grammar', name: 'Grammar Check', icon: '📝', description: 'Identify and correct grammatical errors' },
12
+ { id: 'reading-comp', name: 'Reading Comprehension', icon: '📖', description: 'Questions based on text passages' },
13
+ { id: 'essay', name: 'Essay Question', icon: '✍️', description: 'Open-ended written responses' },
14
+ ];
15
+
16
+ export default function QuestionBuilder() {
17
+ // Chat builder state
18
+ const [currentStep, setCurrentStep] = useState<'type-selection' | 'parameters' | 'generation' | 'editing'>('type-selection');
19
+ const [selectedQuestionType, setSelectedQuestionType] = useState<QuestionType | null>(null);
20
+ const [questionParameters, setQuestionParameters] = useState<QuestionParameters>({});
21
+ const [isGenerating, setIsGenerating] = useState(false);
22
+
23
+ // Source material state
24
+ const [sourceMaterial, setSourceMaterial] = useState('');
25
+ const [isSourceLocked, setIsSourceLocked] = useState(false);
26
+ const [wordCount, setWordCount] = useState(0);
27
+
28
+ // Question bank state
29
+ const [questions, setQuestions] = useState<GeneratedQuestion[]>([]);
30
+ const [selectedQuestions, setSelectedQuestions] = useState<Set<string>>(new Set());
31
+
32
+ // Chat functionality
33
+ const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
34
+
35
+ const updateWordCount = (text: string) => {
36
+ const words = text.trim().split(/\s+/).length;
37
+ setWordCount(text.trim() === '' ? 0 : words);
38
+ };
39
+
40
+ const handleSourceMaterialChange = (text: string) => {
41
+ if (!isSourceLocked) {
42
+ setSourceMaterial(text);
43
+ updateWordCount(text);
44
+ }
45
+ };
46
+
47
+ const handleQuestionTypeSelect = (questionType: QuestionType) => {
48
+ setSelectedQuestionType(questionType);
49
+ setCurrentStep('parameters');
50
+ };
51
+
52
+ const generateQuestion = async () => {
53
+ if (!selectedQuestionType) return;
54
+
55
+ setIsGenerating(true);
56
+ try {
57
+ const response = await fetch('/api/generate-question', {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify({
61
+ type: selectedQuestionType.id,
62
+ parameters: questionParameters,
63
+ sourceMaterial,
64
+ }),
65
+ });
66
+
67
+ if (response.ok) {
68
+ const newQuestion = await response.json();
69
+ setQuestions(prev => [...prev, {
70
+ ...newQuestion,
71
+ id: Date.now().toString(),
72
+ createdAt: new Date(),
73
+ }]);
74
+ setCurrentStep('type-selection');
75
+ setSelectedQuestionType(null);
76
+ setQuestionParameters({});
77
+ }
78
+ } catch (error) {
79
+ console.error('Error generating question:', error);
80
+ } finally {
81
+ setIsGenerating(false);
82
+ }
83
+ };
84
+
85
+ const removeQuestion = (id: string) => {
86
+ setQuestions(prev => prev.filter(q => q.id !== id));
87
+ setSelectedQuestions(prev => {
88
+ const newSet = new Set(prev);
89
+ newSet.delete(id);
90
+ return newSet;
91
+ });
92
+ };
93
+
94
+
95
 
 
96
  return (
97
+ <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
98
+ {/* Header */}
99
+ <div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
100
+ <div className="px-6 py-4">
101
+ <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
102
+ 🎓 QuizFlash - Question Builder
103
+ </h1>
104
+ <p className="text-gray-600 dark:text-gray-300">
105
+ AI-powered assessment creation tool for educators
106
+ </p>
107
+ </div>
108
+ </div>
109
+
110
+ {/* Three-panel layout */}
111
+ <div className="flex h-[calc(100vh-120px)]">
112
+ {/* Left Panel: Chat-Driven Question Builder */}
113
+ <div className="w-1/3 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 flex flex-col">
114
+ <div className="p-4 border-b border-gray-200 dark:border-gray-700">
115
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
116
+ 💬 Question Builder
117
+ </h2>
118
+ </div>
119
+
120
+ <div className="flex-1 overflow-y-auto p-4">
121
+ {currentStep === 'type-selection' && (
122
+ <div className="space-y-4">
123
+ <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
124
+ <p className="text-blue-800 dark:text-blue-200">
125
+ 🤖 Hi! What type of question would you like to create?
126
+ </p>
127
+ </div>
128
+
129
+ <div className="space-y-2">
130
+ {QUESTION_TYPES.map((type) => (
131
+ <button
132
+ key={type.id}
133
+ onClick={() => handleQuestionTypeSelect(type)}
134
+ className="w-full p-3 text-left border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
135
+ >
136
+ <div className="flex items-center space-x-3">
137
+ <span className="text-lg">{type.icon}</span>
138
+ <div>
139
+ <div className="font-medium text-gray-900 dark:text-white">{type.name}</div>
140
+ <div className="text-sm text-gray-500 dark:text-gray-400">{type.description}</div>
141
+ </div>
142
+ </div>
143
+ </button>
144
+ ))}
145
+ </div>
146
+ </div>
147
+ )}
148
+
149
+ {currentStep === 'parameters' && selectedQuestionType && (
150
+ <div className="space-y-4">
151
+ <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
152
+ <p className="text-blue-800 dark:text-blue-200">
153
+ Great choice! Let's configure your {selectedQuestionType.name} question.
154
+ </p>
155
+ </div>
156
+
157
+ <QuestionParameterForm
158
+ questionType={selectedQuestionType}
159
+ parameters={questionParameters}
160
+ onParametersChange={setQuestionParameters}
161
+ />
162
+
163
+ <div className="flex space-x-2">
164
+ <button
165
+ onClick={() => setCurrentStep('type-selection')}
166
+ className="px-4 py-2 text-gray-600 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700"
167
+ >
168
+ Back
169
+ </button>
170
+ <button
171
+ onClick={generateQuestion}
172
+ disabled={isGenerating}
173
+ className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
174
+ >
175
+ {isGenerating ? 'Generating...' : 'Generate Question'}
176
+ </button>
177
+ </div>
178
+ </div>
179
+ )}
180
+
181
+ {/* Chat History */}
182
+ <div className="mt-6 space-y-4">
183
+ {messages.map((message, index) => (
184
+ <div
185
+ key={index}
186
+ className={`p-3 rounded-lg ${
187
+ message.role === 'user'
188
+ ? 'bg-blue-100 dark:bg-blue-900 ml-4'
189
+ : 'bg-gray-100 dark:bg-gray-700 mr-4'
190
+ }`}
191
+ >
192
+ <div className="text-sm font-medium mb-1">
193
+ {message.role === 'user' ? 'You' : 'AI Assistant'}
194
+ </div>
195
+ <div>{message.content}</div>
196
+ </div>
197
+ ))}
198
+ </div>
199
+ </div>
200
+
201
+ {/* Chat Input */}
202
+ <div className="p-4 border-t border-gray-200 dark:border-gray-700">
203
+ <form onSubmit={handleSubmit} className="flex space-x-2">
204
+ <input
205
+ value={input}
206
+ onChange={handleInputChange}
207
+ placeholder="Ask for modifications or help..."
208
+ className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white"
209
+ />
210
+ <button
211
+ type="submit"
212
+ disabled={isLoading}
213
+ className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
214
+ >
215
+ Send
216
+ </button>
217
+ </form>
218
+ </div>
219
+ </div>
220
+
221
+ {/* Right side: Two-panel stack */}
222
+ <div className="flex-1 flex flex-col">
223
+ {/* Upper Right Panel: Source Material Editor */}
224
+ <div className="h-1/2 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
225
+ <div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
226
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
227
+ 📄 Source Material
228
+ </h2>
229
+ <div className="flex items-center space-x-4">
230
+ <span className="text-sm text-gray-500 dark:text-gray-400">
231
+ {wordCount} words
232
+ </span>
233
+ <label className="flex items-center space-x-2">
234
+ <input
235
+ type="checkbox"
236
+ checked={isSourceLocked}
237
+ onChange={(e) => setIsSourceLocked(e.target.checked)}
238
+ className="rounded"
239
+ />
240
+ <span className="text-sm">🔒 Lock</span>
241
+ </label>
242
+ </div>
243
+ </div>
244
+
245
+ <div className="h-[calc(100%-80px)] p-4">
246
+ <textarea
247
+ value={sourceMaterial}
248
+ onChange={(e) => handleSourceMaterialChange(e.target.value)}
249
+ placeholder="Paste or type your source material here (articles, passages, etc.)..."
250
+ disabled={isSourceLocked}
251
+ className="w-full h-full resize-none border border-gray-300 dark:border-gray-600 rounded-lg p-4 focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-white disabled:bg-gray-100 dark:disabled:bg-gray-600"
252
+ />
253
+ </div>
254
+ </div>
255
+
256
+ {/* Lower Right Panel: Question Bank */}
257
+ <div className="h-1/2 bg-white dark:bg-gray-800">
258
+ <div className="p-4 border-b border-gray-200 dark:border-gray-700">
259
+ <h2 className="text-lg font-semibold text-gray-900 dark:text-white">
260
+ 🗂️ Question Bank ({questions.length})
261
+ </h2>
262
+ </div>
263
+
264
+ <div className="h-[calc(100%-80px)] overflow-y-auto p-4">
265
+ {questions.length === 0 ? (
266
+ <div className="text-center text-gray-500 dark:text-gray-400 mt-8">
267
+ <div className="text-4xl mb-4">📝</div>
268
+ <p>No questions yet. Start by creating your first question!</p>
269
+ </div>
270
+ ) : (
271
+ <div className="space-y-3">
272
+ {questions.map((question) => (
273
+ <div
274
+ key={question.id}
275
+ className="border border-gray-200 dark:border-gray-600 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-700 group"
276
+ >
277
+ <div className="flex items-start justify-between">
278
+ <div className="flex-1">
279
+ <div className="flex items-center space-x-2 mb-2">
280
+ <span className="text-sm font-medium text-blue-600 dark:text-blue-400">
281
+ {QUESTION_TYPES.find(t => t.id === question.type)?.name}
282
+ </span>
283
+ <span className="text-xs text-gray-500 dark:text-gray-400">
284
+ {question.points} pts
285
+ </span>
286
+ </div>
287
+ <p className="text-gray-900 dark:text-white line-clamp-2">
288
+ {question.stem}
289
+ </p>
290
+ </div>
291
+ <div className="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
292
+ <button className="p-1 text-gray-400 hover:text-blue-600">
293
+ ✏️
294
+ </button>
295
+ <button className="p-1 text-gray-400 hover:text-green-600">
296
+ 📄
297
+ </button>
298
+ <button className="p-1 text-gray-400 hover:text-blue-600">
299
+ 👁️
300
+ </button>
301
+ <button
302
+ onClick={() => removeQuestion(question.id)}
303
+ className="p-1 text-gray-400 hover:text-red-600"
304
+ >
305
+
306
+ </button>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ ))}
311
+ </div>
312
+ )}
313
+ </div>
314
+ </div>
315
  </div>
316
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  </div>
318
  );
319
  }
src/components/QuestionParameterForm.tsx ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { QuestionType, QuestionParameters } from '@/types/quiz';
4
+
5
+ interface QuestionParameterFormProps {
6
+ questionType: QuestionType;
7
+ parameters: QuestionParameters;
8
+ onParametersChange: (parameters: QuestionParameters) => void;
9
+ }
10
+
11
+ export default function QuestionParameterForm({
12
+ questionType,
13
+ parameters,
14
+ onParametersChange,
15
+ }: QuestionParameterFormProps) {
16
+ const updateParameter = (key: string, value: any) => {
17
+ onParametersChange({ ...parameters, [key]: value });
18
+ };
19
+
20
+ switch (questionType.id) {
21
+ case 'multiple-choice':
22
+ return (
23
+ <div className="space-y-4">
24
+ <div>
25
+ <label className="block text-sm font-medium mb-2">Difficulty Level</label>
26
+ <select
27
+ value={parameters.difficulty || 'intermediate'}
28
+ onChange={(e) => updateParameter('difficulty', e.target.value)}
29
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
30
+ >
31
+ <option value="beginner">Beginner</option>
32
+ <option value="intermediate">Intermediate</option>
33
+ <option value="advanced">Advanced</option>
34
+ </select>
35
+ </div>
36
+ <div>
37
+ <label className="block text-sm font-medium mb-2">Number of Options</label>
38
+ <select
39
+ value={parameters.numOptions || 4}
40
+ onChange={(e) => updateParameter('numOptions', parseInt(e.target.value))}
41
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
42
+ >
43
+ <option value={3}>3 Options</option>
44
+ <option value={4}>4 Options</option>
45
+ <option value={5}>5 Options</option>
46
+ </select>
47
+ </div>
48
+ </div>
49
+ );
50
+
51
+ case 'cloze':
52
+ return (
53
+ <div className="space-y-4">
54
+ <div>
55
+ <label className="block text-sm font-medium mb-2">Target Part of Speech</label>
56
+ <select
57
+ value={parameters.partOfSpeech || 'any'}
58
+ onChange={(e) => updateParameter('partOfSpeech', e.target.value)}
59
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
60
+ >
61
+ <option value="any">Any</option>
62
+ <option value="noun">Nouns</option>
63
+ <option value="verb">Verbs</option>
64
+ <option value="adjective">Adjectives</option>
65
+ <option value="adverb">Adverbs</option>
66
+ </select>
67
+ </div>
68
+ <div>
69
+ <label className="block text-sm font-medium mb-2">Number of Blanks</label>
70
+ <input
71
+ type="range"
72
+ min="1"
73
+ max="5"
74
+ value={parameters.numBlanks || 1}
75
+ onChange={(e) => updateParameter('numBlanks', parseInt(e.target.value))}
76
+ className="w-full"
77
+ />
78
+ <div className="text-center text-sm text-gray-600">{parameters.numBlanks || 1} blank(s)</div>
79
+ </div>
80
+ </div>
81
+ );
82
+
83
+ case 'grammar':
84
+ return (
85
+ <div className="space-y-4">
86
+ <div>
87
+ <label className="block text-sm font-medium mb-2">Difficulty Level</label>
88
+ <select
89
+ value={parameters.difficulty || 'intermediate'}
90
+ onChange={(e) => updateParameter('difficulty', e.target.value)}
91
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
92
+ >
93
+ <option value="beginner">Beginner</option>
94
+ <option value="intermediate">Intermediate</option>
95
+ <option value="advanced">Advanced</option>
96
+ </select>
97
+ </div>
98
+ <div>
99
+ <label className="block text-sm font-medium mb-2">Grammar Focus</label>
100
+ <select
101
+ value={parameters.grammarFocus || 'any'}
102
+ onChange={(e) => updateParameter('grammarFocus', e.target.value)}
103
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
104
+ >
105
+ <option value="any">Any Grammar Rule</option>
106
+ <option value="verb-tenses">Verb Tenses</option>
107
+ <option value="subject-verb">Subject-Verb Agreement</option>
108
+ <option value="punctuation">Punctuation</option>
109
+ <option value="sentence-structure">Sentence Structure</option>
110
+ </select>
111
+ </div>
112
+ </div>
113
+ );
114
+
115
+ case 'reading-comp':
116
+ return (
117
+ <div className="space-y-4">
118
+ <div>
119
+ <label className="block text-sm font-medium mb-2">Difficulty Level</label>
120
+ <select
121
+ value={parameters.difficulty || 'intermediate'}
122
+ onChange={(e) => updateParameter('difficulty', e.target.value)}
123
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
124
+ >
125
+ <option value="beginner">Beginner</option>
126
+ <option value="intermediate">Intermediate</option>
127
+ <option value="advanced">Advanced</option>
128
+ </select>
129
+ </div>
130
+ <div>
131
+ <label className="block text-sm font-medium mb-2">Question Focus</label>
132
+ <select
133
+ value={parameters.focus || 'comprehension'}
134
+ onChange={(e) => updateParameter('focus', e.target.value)}
135
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
136
+ >
137
+ <option value="comprehension">General Comprehension</option>
138
+ <option value="inference">Making Inferences</option>
139
+ <option value="main-idea">Main Ideas</option>
140
+ <option value="details">Supporting Details</option>
141
+ <option value="vocabulary">Vocabulary in Context</option>
142
+ </select>
143
+ </div>
144
+ </div>
145
+ );
146
+
147
+ case 'essay':
148
+ return (
149
+ <div className="space-y-4">
150
+ <div>
151
+ <label className="block text-sm font-medium mb-2">Difficulty Level</label>
152
+ <select
153
+ value={parameters.difficulty || 'intermediate'}
154
+ onChange={(e) => updateParameter('difficulty', e.target.value)}
155
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
156
+ >
157
+ <option value="beginner">Beginner</option>
158
+ <option value="intermediate">Intermediate</option>
159
+ <option value="advanced">Advanced</option>
160
+ </select>
161
+ </div>
162
+ <div>
163
+ <label className="block text-sm font-medium mb-2">Essay Type</label>
164
+ <select
165
+ value={parameters.essayType || 'argumentative'}
166
+ onChange={(e) => updateParameter('essayType', e.target.value)}
167
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
168
+ >
169
+ <option value="argumentative">Argumentative</option>
170
+ <option value="descriptive">Descriptive</option>
171
+ <option value="narrative">Narrative</option>
172
+ <option value="expository">Expository</option>
173
+ <option value="compare-contrast">Compare & Contrast</option>
174
+ </select>
175
+ </div>
176
+ <div>
177
+ <label className="block text-sm font-medium mb-2">Word Count Range</label>
178
+ <select
179
+ value={parameters.wordCount || '300-500'}
180
+ onChange={(e) => updateParameter('wordCount', e.target.value)}
181
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
182
+ >
183
+ <option value="150-250">150-250 words</option>
184
+ <option value="300-500">300-500 words</option>
185
+ <option value="500-750">500-750 words</option>
186
+ <option value="750-1000">750-1000 words</option>
187
+ </select>
188
+ </div>
189
+ </div>
190
+ );
191
+
192
+ default:
193
+ return (
194
+ <div className="space-y-4">
195
+ <div>
196
+ <label className="block text-sm font-medium mb-2">Difficulty Level</label>
197
+ <select
198
+ value={parameters.difficulty || 'intermediate'}
199
+ onChange={(e) => updateParameter('difficulty', e.target.value)}
200
+ className="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
201
+ >
202
+ <option value="beginner">Beginner</option>
203
+ <option value="intermediate">Intermediate</option>
204
+ <option value="advanced">Advanced</option>
205
+ </select>
206
+ </div>
207
+ </div>
208
+ );
209
+ }
210
+ }
src/prompts/quiz-generation.ts ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface QuizPromptParams {
2
+ topic: string;
3
+ difficulty: string;
4
+ numQuestions: number;
5
+ questionType?: 'multiple-choice' | 'true-false' | 'fill-blank' | 'short-answer';
6
+ focus?: 'conceptual' | 'practical' | 'analytical' | 'memorization';
7
+ }
8
+
9
+ export interface PromptTemplate {
10
+ template: string;
11
+ description: string;
12
+ }
13
+
14
+ export const QUIZ_PROMPTS = {
15
+ 'multiple-choice': {
16
+ basic: {
17
+ template: `Generate {{numQuestions}} {{difficulty}} level multiple-choice quiz questions about {{topic}}.
18
+
19
+ For each question, provide:
20
+ - A clear, well-written question
21
+ - Exactly 4 answer options (labeled A, B, C, D)
22
+ - The correct answer should be indicated by its index (0 for A, 1 for B, 2 for C, 3 for D)
23
+ - A detailed explanation of why the correct answer is right and why the others are wrong
24
+
25
+ Make the questions educational and engaging. Ensure incorrect options are plausible but clearly wrong.
26
+
27
+ Return the data in this exact structure:
28
+ {
29
+ "questions": [
30
+ {
31
+ "question": "Your question here?",
32
+ "options": ["Option A", "Option B", "Option C", "Option D"],
33
+ "correctAnswer": 0,
34
+ "explanation": "Detailed explanation here..."
35
+ }
36
+ ]
37
+ }`,
38
+ description: 'Standard multiple choice questions'
39
+ },
40
+ conceptual: {
41
+ template: `Create {{numQuestions}} {{difficulty}} level conceptual multiple-choice questions about {{topic}}.
42
+
43
+ Focus on understanding core principles and concepts rather than memorization.
44
+
45
+ For each question, provide:
46
+ - A thought-provoking question that tests deep understanding
47
+ - Exactly 4 answer options that require conceptual reasoning
48
+ - The correct answer index (0-3)
49
+ - A detailed explanation that reinforces the underlying concepts
50
+
51
+ Make incorrect options represent common misconceptions.
52
+
53
+ Return the data in this exact structure:
54
+ {
55
+ "questions": [
56
+ {
57
+ "question": "Your conceptual question here?",
58
+ "options": ["Option A", "Option B", "Option C", "Option D"],
59
+ "correctAnswer": 0,
60
+ "explanation": "Detailed conceptual explanation here..."
61
+ }
62
+ ]
63
+ }`,
64
+ description: 'Conceptual understanding focused questions'
65
+ },
66
+ practical: {
67
+ template: `Generate {{numQuestions}} {{difficulty}} level practical multiple-choice questions about {{topic}}.
68
+
69
+ Focus on real-world applications and problem-solving scenarios.
70
+
71
+ For each question, provide:
72
+ - A scenario-based question from real-world contexts
73
+ - Exactly 4 realistic solution options
74
+ - The correct answer index (0-3)
75
+ - An explanation with practical examples and applications
76
+
77
+ Make options represent realistic choices someone might face in practice.
78
+
79
+ Return the data in this exact structure:
80
+ {
81
+ "questions": [
82
+ {
83
+ "question": "Your practical scenario question here?",
84
+ "options": ["Option A", "Option B", "Option C", "Option D"],
85
+ "correctAnswer": 0,
86
+ "explanation": "Practical explanation with real-world examples..."
87
+ }
88
+ ]
89
+ }`,
90
+ description: 'Practical application focused questions'
91
+ }
92
+ },
93
+ 'true-false': {
94
+ basic: {
95
+ template: `Create {{numQuestions}} {{difficulty}} level true/false questions about {{topic}}.
96
+
97
+ For each question, provide:
98
+ - A clear statement that can be evaluated as true or false
99
+ - Exactly 4 answer options: "True", "False", and 2 additional options like "Partially True" or "Cannot be determined"
100
+ - The correct answer index (0-3)
101
+ - A detailed explanation of why the statement is true or false
102
+
103
+ Make statements clear and unambiguous.
104
+
105
+ Return the data in this exact structure:
106
+ {
107
+ "questions": [
108
+ {
109
+ "question": "Your true/false statement here",
110
+ "options": ["True", "False", "Partially True", "Cannot be determined"],
111
+ "correctAnswer": 0,
112
+ "explanation": "Detailed explanation here..."
113
+ }
114
+ ]
115
+ }`,
116
+ description: 'Standard true/false questions'
117
+ },
118
+ conceptual: {
119
+ template: `Create {{numQuestions}} {{difficulty}} level conceptual true/false questions about {{topic}}.
120
+
121
+ Focus on testing understanding of core principles and concepts.
122
+
123
+ For each question, provide:
124
+ - A nuanced statement requiring deep conceptual thinking
125
+ - Exactly 4 answer options: "True", "False", "Depends on context", "Partially True"
126
+ - The correct answer index (0-3)
127
+ - A detailed explanation reinforcing the underlying concepts
128
+
129
+ Ensure statements require deep thinking to evaluate correctly.
130
+
131
+ Return the data in this exact structure:
132
+ {
133
+ "questions": [
134
+ {
135
+ "question": "Your conceptual statement here",
136
+ "options": ["True", "False", "Depends on context", "Partially True"],
137
+ "correctAnswer": 0,
138
+ "explanation": "Detailed conceptual explanation here..."
139
+ }
140
+ ]
141
+ }`,
142
+ description: 'Conceptual true/false questions'
143
+ },
144
+ practical: {
145
+ template: `Generate {{numQuestions}} {{difficulty}} level practical true/false questions about {{topic}}.
146
+
147
+ Focus on real-world scenarios and applications.
148
+
149
+ For each question, provide:
150
+ - A statement about practical applications or real-world scenarios
151
+ - Exactly 4 answer options: "True", "False", "Sometimes True", "Rarely True"
152
+ - The correct answer index (0-3)
153
+ - An explanation with practical examples
154
+
155
+ Include statements that test practical knowledge and experience.
156
+
157
+ Return the data in this exact structure:
158
+ {
159
+ "questions": [
160
+ {
161
+ "question": "Your practical statement here",
162
+ "options": ["True", "False", "Sometimes True", "Rarely True"],
163
+ "correctAnswer": 0,
164
+ "explanation": "Practical explanation with examples..."
165
+ }
166
+ ]
167
+ }`,
168
+ description: 'Practical true/false questions'
169
+ }
170
+ },
171
+ 'fill-blank': {
172
+ basic: {
173
+ template: `Create {{numQuestions}} {{difficulty}} level fill-in-the-blank questions about {{topic}}.
174
+
175
+ For each question, provide:
176
+ - A sentence with one important term or phrase replaced with a blank (use _____ for the blank)
177
+ - Exactly 4 answer options: the correct term and 3 plausible distractors
178
+ - The correct answer index (0-3)
179
+ - An explanation of why the correct term fits and why others don't
180
+
181
+ Make sure the sentence context gives enough clues for the correct answer.
182
+
183
+ Return the data in this exact structure:
184
+ {
185
+ "questions": [
186
+ {
187
+ "question": "The _____ is responsible for...",
188
+ "options": ["correct term", "distractor 1", "distractor 2", "distractor 3"],
189
+ "correctAnswer": 0,
190
+ "explanation": "Detailed explanation here..."
191
+ }
192
+ ]
193
+ }`,
194
+ description: 'Fill-in-the-blank questions'
195
+ },
196
+ conceptual: {
197
+ template: `Create {{numQuestions}} {{difficulty}} level conceptual fill-in-the-blank questions about {{topic}}.
198
+
199
+ Focus on key concepts and principles.
200
+
201
+ For each question, provide:
202
+ - A sentence testing understanding of important terminology (use _____ for the blank)
203
+ - Exactly 4 answer options: the correct concept and 3 related but incorrect terms
204
+ - The correct answer index (0-3)
205
+ - A detailed explanation demonstrating conceptual knowledge
206
+
207
+ Create sentences that test deep understanding of terminology.
208
+
209
+ Return the data in this exact structure:
210
+ {
211
+ "questions": [
212
+ {
213
+ "question": "The fundamental concept of _____ explains...",
214
+ "options": ["correct concept", "related concept 1", "related concept 2", "related concept 3"],
215
+ "correctAnswer": 0,
216
+ "explanation": "Detailed conceptual explanation here..."
217
+ }
218
+ ]
219
+ }`,
220
+ description: 'Conceptual fill-in-the-blank questions'
221
+ },
222
+ practical: {
223
+ template: `Generate {{numQuestions}} {{difficulty}} level practical fill-in-the-blank questions about {{topic}}.
224
+
225
+ Focus on real-world applications and scenarios.
226
+
227
+ For each question, provide:
228
+ - A practical sentence from real-world contexts (use _____ for the blank)
229
+ - Exactly 4 answer options: the correct practical term and 3 realistic alternatives
230
+ - The correct answer index (0-3)
231
+ - An explanation showing how the term applies in practice
232
+
233
+ Create sentences from practical examples and applications.
234
+
235
+ Return the data in this exact structure:
236
+ {
237
+ "questions": [
238
+ {
239
+ "question": "In practice, you would use _____ to...",
240
+ "options": ["correct tool/method", "alternative 1", "alternative 2", "alternative 3"],
241
+ "correctAnswer": 0,
242
+ "explanation": "Practical explanation with examples..."
243
+ }
244
+ ]
245
+ }`,
246
+ description: 'Practical fill-in-the-blank questions'
247
+ }
248
+ },
249
+ 'short-answer': {
250
+ basic: {
251
+ template: `Generate {{numQuestions}} {{difficulty}} level short-answer questions about {{topic}}.
252
+
253
+ For each question, provide:
254
+ - A question that normally requires a 2-3 sentence response
255
+ - Exactly 4 answer options: one comprehensive correct answer and 3 incomplete/incorrect responses
256
+ - The correct answer index (0-3)
257
+ - An explanation of why the correct answer is complete and accurate
258
+
259
+ Make the correct option a well-structured short answer covering key points.
260
+
261
+ Return the data in this exact structure:
262
+ {
263
+ "questions": [
264
+ {
265
+ "question": "Explain why/how/what...",
266
+ "options": [
267
+ "Complete correct answer covering all key points...",
268
+ "Incomplete answer missing key points...",
269
+ "Incorrect answer with misconceptions...",
270
+ "Partially correct but lacking depth..."
271
+ ],
272
+ "correctAnswer": 0,
273
+ "explanation": "Detailed explanation here..."
274
+ }
275
+ ]
276
+ }`,
277
+ description: 'Short answer questions requiring brief explanations'
278
+ },
279
+ conceptual: {
280
+ template: `Create {{numQuestions}} {{difficulty}} level conceptual short-answer questions about {{topic}}.
281
+
282
+ Focus on testing deep understanding of principles and concepts.
283
+
284
+ For each question, provide:
285
+ - A question requiring explanation of 'why' and 'how' rather than just 'what'
286
+ - Exactly 4 answer options: one that demonstrates deep conceptual understanding and 3 superficial responses
287
+ - The correct answer index (0-3)
288
+ - An explanation reinforcing the underlying concepts
289
+
290
+ Questions should test deep understanding, not memorization.
291
+
292
+ Return the data in this exact structure:
293
+ {
294
+ "questions": [
295
+ {
296
+ "question": "Explain the underlying principle of...",
297
+ "options": [
298
+ "Deep conceptual answer explaining principles...",
299
+ "Surface-level answer without depth...",
300
+ "Answer with conceptual errors...",
301
+ "Answer missing key concepts..."
302
+ ],
303
+ "correctAnswer": 0,
304
+ "explanation": "Detailed conceptual explanation here..."
305
+ }
306
+ ]
307
+ }`,
308
+ description: 'Conceptual short answer questions'
309
+ },
310
+ practical: {
311
+ template: `Generate {{numQuestions}} {{difficulty}} level practical short-answer questions about {{topic}}.
312
+
313
+ Focus on real-world applications and problem-solving scenarios.
314
+
315
+ For each question, provide:
316
+ - A question asking students to apply knowledge to practical situations
317
+ - Exactly 4 answer options: one practical, actionable solution and 3 less effective approaches
318
+ - The correct answer index (0-3)
319
+ - An explanation with real-world examples and applications
320
+
321
+ Questions should test practical application, not theoretical knowledge.
322
+
323
+ Return the data in this exact structure:
324
+ {
325
+ "questions": [
326
+ {
327
+ "question": "How would you solve/approach/handle...",
328
+ "options": [
329
+ "Practical, actionable solution with clear steps...",
330
+ "Theoretical approach lacking practicality...",
331
+ "Incomplete solution missing key steps...",
332
+ "Impractical approach with flaws..."
333
+ ],
334
+ "correctAnswer": 0,
335
+ "explanation": "Practical explanation with examples..."
336
+ }
337
+ ]
338
+ }`,
339
+ description: 'Practical short answer questions'
340
+ }
341
+ }
342
+ } as const;
343
+
344
+ // Template rendering function
345
+ export const renderTemplate = (template: string, params: Record<string, any>): string => {
346
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
347
+ return params[key]?.toString() || match;
348
+ });
349
+ };
350
+
351
+ // Main prompt generation function
352
+ export const generateQuizPrompt = (params: QuizPromptParams, promptType: 'basic' | 'conceptual' | 'practical' = 'basic'): string => {
353
+ const questionType = params.questionType || 'multiple-choice';
354
+ const promptTemplate = QUIZ_PROMPTS[questionType]?.[promptType];
355
+
356
+ if (!promptTemplate) {
357
+ throw new Error(`Prompt not found for question type: ${questionType}, prompt type: ${promptType}`);
358
+ }
359
+
360
+ return renderTemplate(promptTemplate.template, params);
361
+ };
362
+
363
+ // Helper function to get available prompts
364
+ export const getAvailablePrompts = () => {
365
+ return Object.entries(QUIZ_PROMPTS).map(([questionType, prompts]) => ({
366
+ questionType,
367
+ prompts: Object.entries(prompts).map(([type, config]) => ({
368
+ type,
369
+ description: config.description
370
+ }))
371
+ }));
372
+ };
src/types/quiz.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface QuestionType {
2
+ id: string;
3
+ name: string;
4
+ icon: string;
5
+ description: string;
6
+ }
7
+
8
+ export interface QuestionParameters {
9
+ [key: string]: any;
10
+ }
11
+
12
+ export interface GeneratedQuestion {
13
+ id: string;
14
+ type: string;
15
+ stem: string;
16
+ content: any;
17
+ points: number;
18
+ createdAt: Date;
19
+ }