Skip to content

Commit de5c1f1

Browse files
committed
Improve example
1 parent 5f675c9 commit de5c1f1

4 files changed

Lines changed: 81 additions & 52 deletions

File tree

apps/test/src/App.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createSignal, snapshot, type Component } from 'solid-js'
1+
import { createSignal, type Component } from 'solid-js'
22
import { Factor } from './Exercise'
33

44
const App: Component = () => {
@@ -9,17 +9,7 @@ const App: Component = () => {
99
})
1010
return (
1111
<>
12-
<Factor
13-
fetch={() => {
14-
console.log('Fetching exercise')
15-
return data()
16-
}}
17-
save={(exercise) => {
18-
const ex = snapshot(exercise)
19-
console.log('Saving', JSON.stringify(ex, null, 2))
20-
setData(ex)
21-
}}
22-
/>
12+
<Factor fetch={data} save={setData} />
2313
</>
2414
)
2515
}

apps/test/src/Exercise.tsx

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createView, defineFeedback, defineSchema, expr, useExerciseContext } from '@learning/core'
2-
import { createMemo } from 'solid-js'
2+
import { createMemo, Show } from 'solid-js'
33
import * as v from 'valibot'
44

55
const schema = defineSchema({
@@ -15,6 +15,12 @@ const schema = defineSchema({
1515
attempt: v.string(),
1616
},
1717
},
18+
root: {
19+
previous: ['start'],
20+
state: {
21+
root: v.string(),
22+
},
23+
},
1824
},
1925
})
2026

@@ -23,7 +29,11 @@ const feedback = defineFeedback<typeof schema>({
2329
const equal = await expr(state.attempt).isEqual(question.expr)
2430
const factored = await expr(state.attempt).isFactored()
2531
const correct = equal && factored
26-
return { correct, score: [Number(correct), 1], next: null }
32+
return { correct, score: [Number(correct), 1], next: correct ? null : 'root' }
33+
},
34+
root: async ({ question, state }) => {
35+
const correct = await expr(question.expr).checkRoot(state.root)
36+
return { correct, score: [0, 0], next: null }
2737
},
2838
})
2939

@@ -32,7 +42,9 @@ export const Factor = createView(schema, feedback, {
3242
const question = () => expr(props.question.expr)
3343
const attempt = () => expr(props.state?.attempt)
3444
const answer = createMemo(() => attempt() && question().factor().latex())
35-
const correct = createMemo(() => attempt()?.isEqual(question()) && attempt()?.isFactored())
45+
const equal = createMemo(() => attempt()?.isEqual(question()))
46+
const factored = createMemo(() => attempt()?.isFactored())
47+
const correct = () => equal() && factored()
3648
const exercise = useExerciseContext()
3749
return (
3850
<>
@@ -43,14 +55,43 @@ export const Factor = createView(schema, feedback, {
4355
value={props.state?.attempt ?? ''}
4456
readonly={props.state !== undefined}
4557
onInput={(e) => {
58+
// @ts-ignore
4659
exercise.setState((state) => {
4760
state.attempt = e.target.value
4861
})
4962
}}
5063
/>
5164
</p>
52-
<p>La réponse est {answer()}</p>
53-
<p>Correct: {correct() ? 'Oui' : 'Non'}</p>
65+
<Show when={props.state}>
66+
<p>La réponse est {answer()}</p>
67+
</Show>
68+
<Show when={correct() !== undefined}>
69+
<p>Correct: {correct() ? 'Oui' : 'Non'}</p>
70+
</Show>
71+
</>
72+
)
73+
},
74+
root: (props) => {
75+
const exercise = useExerciseContext()
76+
const attempt = createMemo(() => expr(props.state?.root))
77+
const correct = createMemo(
78+
() => props.state && expr(props.question.expr).checkRoot(props.state?.root ?? ''),
79+
)
80+
return (
81+
<>
82+
<p>Trouvez une racine de {props.question.expr}</p>
83+
<input
84+
value={props.state?.root ?? ''}
85+
onInput={(e) => {
86+
// @ts-ignore
87+
exercise.setState((state) => {
88+
state.root = e.target.value
89+
})
90+
}}
91+
/>
92+
<Show when={correct() !== undefined}>
93+
<p>Correct: {correct() ? 'Oui' : 'Non'}</p>
94+
</Show>
5495
</>
5596
)
5697
},

packages/core/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
},
1515
"scripts": {
1616
"codegen": "openapi-typescript http://localhost:8088/openapi.json -o ./src/symapi.d.ts",
17-
"dev": "bun scripts/api_codegen.ts",
1817
"test": "bun test --coverage --coverage-reporter=lcov --preload ./test.setup.ts",
1918
"test:watch": "bun test --watch --coverage --preload ./test.setup.ts"
2019
},

packages/core/src/exercise/base.tsx

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Dynamic } from '@solidjs/web'
22
import { mapAsync } from 'es-toolkit'
33
import {
44
createMemo,
5-
createProjection,
65
createStore,
76
For,
87
Loading,
@@ -61,43 +60,41 @@ type Schema<
6160
} = any,
6261
> = ReturnType<typeof defineSchema<N, Q, T, S>>
6362

64-
function Part<T extends Schema, K extends keyof T['steps'], G extends boolean = false>(
63+
function Part<T extends Schema, K extends keyof T['steps'], S extends boolean = false>(
6564
schema: T,
6665
step: K,
67-
graded?: G,
66+
withState?: S,
6867
) {
6968
const base = v.object({
7069
step: v.literal(step as K),
71-
state: v.object(schema.steps[step].state as T['steps'][K]['state']),
7270
})
7371
const extended = v.object({
7472
...base.entries,
75-
correct: v.boolean(),
76-
score: v.strictTuple([v.number(), v.number()]),
73+
state: v.object(schema.steps[step].state as T['steps'][K]['state']),
7774
})
78-
return (graded ? extended : base) as G extends true ? typeof extended : typeof base
75+
return (withState ? extended : base) as S extends true ? typeof extended : typeof base
7976
}
8077

81-
type Part<T extends Schema, K extends keyof T['steps'], G extends boolean = false> = Infer<
82-
typeof Part<T, K, G>
78+
type Part<T extends Schema, K extends keyof T['steps'], S extends boolean = false> = Infer<
79+
typeof Part<T, K, S>
8380
>
8481

8582
function PartUnion<
8683
T extends Schema,
8784
K extends readonly (keyof T['steps'])[],
88-
G extends boolean = false,
89-
>(schema: T, steps: K, graded?: G) {
85+
S extends boolean = false,
86+
>(schema: T, steps: K, withState?: S) {
9087
return v.variant(
9188
'step',
92-
steps.map((step) => Part(schema, step, graded)),
93-
) as v.VariantSchema<'step', { [I in keyof K]: ReturnType<typeof Part<T, K[I], G>> }, undefined>
89+
steps.map((step) => Part(schema, step, withState)),
90+
) as v.VariantSchema<'step', { [I in keyof K]: ReturnType<typeof Part<T, K[I], S>> }, undefined>
9491
}
9592

9693
type PartUnion<
9794
T extends Schema,
9895
K extends readonly (keyof T['steps'])[] = readonly (keyof T['steps'])[],
99-
G extends boolean = false,
100-
> = Infer<typeof PartUnion<T, K, G>>
96+
S extends boolean = false,
97+
> = Infer<typeof PartUnion<T, K, S>>
10198

10299
type Props<T extends Schema, K extends keyof T['steps'], F extends boolean = true> = {
103100
question: InferFromShape<T['question']>
@@ -127,20 +124,18 @@ export function buildSchemas<T extends Schema>(
127124
v.object({
128125
name: v.literal(schema.name as T['name']),
129126
question: v.object(schema.question as T['question']),
130-
attempt: v.array(PartUnion(schema, steps, false)),
127+
attempt: v.array(
128+
v.union([PartUnion(schema, steps, true), PartUnion(schema, steps, false)]),
129+
),
131130
}),
132131
// TODO: calls need to be deduped, waiting for solid-router release?
133132
v.transformAsync(async ({ attempt, question, ...exercise }) => {
134-
let modifiedAttempt: (
135-
| Part<T, T['steps'][keyof T['steps']], true>
136-
| { step: keyof T['steps'] }
137-
)[] = attempt ?? []
133+
let modifiedAttempt = attempt
138134
if (attempt.length === 0 && schema.transform) {
139135
question = await schema.transform(question)
140-
modifiedAttempt = [{ step: 'start' }]
141136
}
142137
modifiedAttempt = await mapAsync(modifiedAttempt, async (part, i) => {
143-
if ('state' in part) {
138+
if ('state' in part && part.state) {
144139
const result = await feedback[part.step]({
145140
question: question,
146141
state: part.state,
@@ -150,6 +145,11 @@ export function buildSchemas<T extends Schema>(
150145
}
151146
return part
152147
})
148+
if (modifiedAttempt.length === 0) modifiedAttempt.push({ step: 'start' })
149+
const lastPart = modifiedAttempt.at(-1)
150+
if (lastPart && 'next' in lastPart && lastPart.next) {
151+
modifiedAttempt.push({ step: lastPart.next })
152+
}
153153
return { ...exercise, question, attempt: modifiedAttempt }
154154
}),
155155
),
@@ -175,10 +175,11 @@ export function createView<T extends Schema>(
175175
) {
176176
const { Student, grade } = buildSchemas(schema, feedback)
177177
return function Component(props: FinalViewProps<T>) {
178-
const exercise = createProjection(async () => grade(await props.fetch()))
178+
const exercise = createMemo(async () => grade(await props.fetch()))
179179
return (
180180
<Loading fallback="Génération de l'exercice...">
181-
<For each={exercise.attempt}>
181+
<p>{exercise().attempt.length} étapes</p>
182+
<For each={exercise().attempt}>
182183
{<K extends keyof T['steps']>(
183184
part: () =>
184185
| Part<T, K, true>
@@ -187,18 +188,19 @@ export function createView<T extends Schema>(
187188
) => {
188189
const [state, setState] = createStore<Partial<Part<T, K>>>(() => part().state ?? {})
189190
const validated = createMemo(() =>
190-
v.safeParse(Part(schema, part().step), {
191+
v.safeParse(Part(schema, part().step, true), {
191192
...part(),
192193
state,
193194
}),
194195
)
195196
const submit = async () => {
196197
if (!validated().success) return
197198
await props.save({
198-
...exercise,
199-
attempt: exercise.attempt.map((p, j) =>
200-
j === i() ? { ...p, ...(validated().output as Part<T, K>) } : p,
201-
),
199+
...exercise(),
200+
attempt: [
201+
...exercise().attempt.toSpliced(-1),
202+
validated().output as Part<T, K, true>,
203+
],
202204
})
203205
refresh(() => exercise)
204206
}
@@ -207,11 +209,9 @@ export function createView<T extends Schema>(
207209
<Dynamic
208210
component={view[part().step]}
209211
{...({
210-
question: exercise.question,
212+
question: exercise().question,
211213
state: part().state,
212-
previous: exercise.attempt.slice(0, i()).toReversed() as any,
213-
correct: part().correct,
214-
score: part().score,
214+
previous: exercise().attempt.slice(0, i()).toReversed() as any,
215215
} satisfies Props<T, K> as ComponentProps<View<T>[K]>)}
216216
/>
217217
<Show when={!part().state && validated().success}>
@@ -221,7 +221,6 @@ export function createView<T extends Schema>(
221221
)
222222
}}
223223
</For>
224-
<pre>{JSON.stringify({ ...exercise }, null, 2)}</pre>
225224
</Loading>
226225
)
227226
}

0 commit comments

Comments
 (0)