Skip to content

Commit 8b4ea54

Browse files
committed
Copy the type file of a type named as a chat with no skill text names it
The copy for the chat carried the type file of the source's `type` as written, so a type named "interactive-book", or under `contentType`, as a chat with no skill text writes it, had none. The page now asks h5p-author 0.8.0 for the source's own type, `sourceType`, the build's reading of it, and below it takes every listed machine name, under any key. A name is read against the list of type files, so `index.json` is fetched first. The 14 rounds that failed in h5p-offline-player#174's test, with no skill text, were built again on this page, and each copied what the model had been given. Of the 39 sources the type-file test replayed before, 36 copy as they did; the other three carry the same type files, and their problems read better since h5p-author 0.7.0 and 0.7.1. Closes #13.
1 parent 99d653c commit 8b4ea54

6 files changed

Lines changed: 75 additions & 27 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ The page and the bundles were embed-my.github.io's `/build` and `/bundles/` unti
7171

7272
### The type files
7373

74-
A chat in a browser, such as Gemini in Chrome, has the skill's text and none of its files, and can run nothing. It guesses how a content type is written, and each round of problems tells it the names of one level. So **Copy the problems for the chat** copies more than the list: after the problems come the skill's type files of the source's own content type and of the libraries it names, each a type's notes, an example and its fields.
74+
A chat in a browser, such as Gemini in Chrome, has the skill's text and none of its files, and can run nothing. It guesses how a content type is written, and each round of problems tells it the names of one level. So **Copy the problems for the chat** copies more than the list: after the problems come the skill's type files of the source's own content type and of the libraries named below it, each a type's notes, an example and its fields. The source's own type is the one the build takes it to be, h5p-author's `sourceType` (since 0.8.0): its `type` as written or as it is spoken of, `"interactive-book"`, or with none the one under `contentType` or `library`, as a chat with no skill text writes it. Below it, a library counts by its machine name, under any key: a slide's element `{ "type": "text" }` is no H5P.Text.
7575

7676
```
7777
The build page found 2 problems in the source. Fix them all and write the whole source again:
@@ -86,7 +86,9 @@ How these content types are written, from the h5p-author skill's type files:
8686

8787
`vite.config.ts` writes them to `/h5p/build/types/<Type>.md` from the pinned `@missing-elements/h5p-author`, which carries them since 0.7.0, with `index.json`, the author's version and the types there is a file for. They are that release's, so they describe the fields of the build the page runs, and a Dependabot update of the package moves both. The page fetches them when it lists a source's problems, so the copy is written within the button's press; where they cannot be fetched, the problems are copied alone.
8888

89-
Measured in [h5p-offline-player#172](https://github.com/missing-elements/h5p-offline-player/issues/172) on the weakest Claude model, Haiku 5.5, in that position: with the files beside the first problems, 42 of 46 sources of 23 content types were right by its second reply, and 29 without them. The page copies that text byte for byte: the 39 sources that failed their first build in the test's runs were built again on this page, and all but one copied what the model had been given, the one a Game Map whose problems 0.7.0 says better.
89+
Measured in [h5p-offline-player#172](https://github.com/missing-elements/h5p-offline-player/issues/172) on the weakest Claude model, Haiku 5.5, in that position: with the files beside the first problems, 42 of 46 sources of 23 content types were right by its second reply, and 29 without them. And in [#174](https://github.com/missing-elements/h5p-offline-player/issues/174), with no skill text at all, the position of a chat given only a link to the skill: with h5p-author 0.7.1 and the file of a type named as it is spoken of, 10 of 12 sources were right by the second reply and all 12 by the third; with 0.7.0 and the page's rule before, 3 and 8 by the fourth, four never.
90+
91+
The page copies that text byte for byte. The 14 rounds that failed in the second test were built again on this page, and each copied what the model had been given. Of the 39 sources that failed their first build in the first, 36 did; the other three carry the same type files, and their problems read better since: a Game Map (0.7.0), and two books whose chapters had a title beside them, 13 problems now 7 (0.7.1).
9092

9193
## The bundles
9294

‎e2e/build.spec.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ test('the copy carries the type files of the source’s type and of the librarie
147147
await page.locator('#source').fill(JSON.stringify(book))
148148
await page.locator('#build').click()
149149
await expect(page.locator('#status')).toHaveAttribute('data-state', 'error')
150-
// The build says what to write, as h5p-author 0.7.0 does.
150+
// The build says what to write, as h5p-author does since 0.7.0.
151151
await expect(page.locator('#problems li').first()).toContainText('write params.chapters[0] as { "library": "H5P.Column", "params": { "content": … } }')
152152
await expect(page.locator('#copy-types')).toHaveText('The copy also carries the skill’s notes on H5P.InteractiveBook, H5P.AdvancedText and H5P.MultiChoice, with their fields and an example, so the chat does not have to guess.')
153153
const [list, files] = (await copied(page)).split(`\n\n${TYPES_HEADING}\n\n`)
@@ -166,6 +166,44 @@ test('the copy carries the type files of the source’s type and of the librarie
166166
expect(problems, problems.join('\n')).toEqual([])
167167
})
168168

169+
test('the copy carries the type file of a type named as it is spoken of, or under contentType, as the build reads it', async ({ page, context, request, baseURL }) => {
170+
const { problems } = watch(page, baseURL)
171+
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
172+
const META = { title: 'Physics', language: 'en', license: 'CC BY', authors: [{ name: 'Ann Author', role: 'Author' }] }
173+
/** The type files a source's copy carries after its problems, once a problem of it is shown. */
174+
const filesOf = async (source: unknown, said: string): Promise<string | undefined> => {
175+
await page.goto('/h5p/build/')
176+
await page.locator('#source').fill(JSON.stringify(source))
177+
await page.locator('#build').click()
178+
await expect(page.locator('#problems')).toContainText(said)
179+
return (await copied(page)).split(`\n\n${TYPES_HEADING}\n\n`)[1]
180+
}
181+
const files = async (...types: string[]) => (await Promise.all(types.map((type) => typeFile(request, type)))).join('\n\n---\n\n')
182+
183+
// As the weakest model wrote one with no skill text (h5p-offline-player#174): the content's
184+
// fields beside `type`, the type as it is spoken of, a question named by its machine name.
185+
const spoken = { type: 'interactive-book', title: 'Physics', chapters: [{ title: 'Newton', content: [{ type: 'H5P.MultiChoice', question: 'Which law?', answers: [{ text: 'F = ma', correct: true }] }] }] }
186+
expect(await filesOf(spoken, 'takes the machine name, "H5P.InteractiveBook"')).toBe(await files('H5P.InteractiveBook', 'H5P.MultiChoice'))
187+
await expect(page.locator('#copy-types')).toHaveText('The copy also carries the skill’s notes on H5P.InteractiveBook and H5P.MultiChoice, with their fields and an example, so the chat does not have to guess.')
188+
189+
// As Gemini in Chrome wrote one from a link to the skill: the type under `contentType`, the
190+
// source's own fields in `metadata`, sub-content naming its library the same way.
191+
const gemini = {
192+
contentType: 'H5P.InteractiveBook',
193+
metadata: { title: 'Physics', license: 'CC BY', authors: META.authors },
194+
params: { chapters: [{ title: 'Newton', content: [{ contentType: 'H5P.AdvancedText', params: { text: '<p>F = ma</p>' } }] }] }
195+
}
196+
expect(await filesOf(gemini, 'the content type is its type: write "type": "H5P.InteractiveBook"')).toBe(await files('H5P.InteractiveBook', 'H5P.AdvancedText'))
197+
198+
// A name the build does not read for the type, under another key, is none; nor below it is a
199+
// text that only reads like one: a slide's element `{ "type": "text" }` is no H5P.Text.
200+
expect(await filesOf({ ...META, contentTypeName: 'interactive book', params: {} }, 'contentTypeName')).toBeUndefined()
201+
await expect(page.locator('#copy-types')).toBeHidden()
202+
const slides = { ...META, type: 'H5P.CoursePresentation', params: { presentation: { slides: [{ elements: [{ type: 'text', x: 5, y: 5, text: 'Newton' }] }] } } }
203+
expect(await filesOf(slides, 'params.presentation.slides[0].elements[0]')).toBe(await files('H5P.CoursePresentation'))
204+
expect(problems, problems.join('\n')).toEqual([])
205+
})
206+
169207
test('the problems are copied alone where the type files cannot be fetched, and with them once they can', async ({ page, context, request }) => {
170208
await context.grantPermissions(['clipboard-read', 'clipboard-write'])
171209
await page.route('**/h5p/build/types/**', (route) => route.fulfill({ status: 503, body: 'unavailable' }))

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"vite": "8.3.5"
2323
},
2424
"dependencies": {
25-
"@missing-elements/h5p-author": "0.7.0",
25+
"@missing-elements/h5p-author": "0.8.0",
2626
"@missing-elements/h5p-libraries": "1.1.0",
2727
"@missing-elements/h5p-offline-player": "1.6.0",
2828
"@missing-elements/h5p-runtime": "1.0.1",

‎pnpm-lock.yaml‎

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/h5p/build/build-page.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@ function fillList(list: HTMLUListElement, items: Problem[]): void {
9090
* copied without them.
9191
*/
9292
async function typeFilesOf(source: unknown): Promise<TypeFile[]> {
93-
const names = typesNamed(source)
94-
if (!names.length) return []
93+
if (!source || typeof source !== 'object') return []
9594
const fetched = async (path: string): Promise<Response> => {
9695
const response = await fetch(`${TYPES}${path}`)
9796
if (!response.ok) throw new Error(`${path}: ${response.status}`)
9897
return response
9998
}
10099
try {
101100
typeIndex ??= fetched('index.json').then(async (response) => new Set((await response.json() as { types: string[] }).types))
102-
const known = await typeIndex
103-
return await Promise.all(names.filter((name) => known.has(name)).map(async (type) => {
101+
// The listed types are what a name is read against, as it is or as it is spoken of.
102+
const names = typesNamed(source, await typeIndex)
103+
return await Promise.all(names.map(async (type) => {
104104
let text = typeTexts.get(type)
105105
if (!text) typeTexts.set(type, text = fetched(`${type}.md`).then((response) => response.text()))
106106
return { type, text: await text }

‎src/h5p/build/build-source.ts‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* written in, and the text a chat is given back. The browser tests reach it through the page.
55
*/
66

7+
import { sourceType } from '@missing-elements/h5p-author/browser'
8+
79
/** A problem or a note, as the author package reports them: where in the source, and what. */
810
export interface Problem {
911
path: string
@@ -110,24 +112,30 @@ export function matchFiles(paths: string[], chosen: File[]): { files: Record<str
110112
}
111113

112114
/**
113-
* The content types a source is written in, by machine name: its own, then every library it names
114-
* as sub-content, in the order they come, each once. Their type files are what the chat that wrote
115-
* the source lacked.
115+
* The content types a source is written in, of those listed, by machine name: its own as the build
116+
* reads it, then every one named below it as it is, under any key, in the order they come, each
117+
* once. Their type files are what the chat that wrote the source lacked.
118+
*
119+
* The source's own type is h5p-author's reading of it, `sourceType`: its `type` as it is or as it is
120+
* spoken of, "interactive-book", or with none the one under `contentType` or `library`. A chat with
121+
* no skill text writes it so, and a rule of the page's own, `type` as written, gave its first copy
122+
* no file (h5p-offline-player#176). Below it a name counts only as it is, a listed machine name with
123+
* or without a major and minor: what reads like one, a slide's element `{ "type": "text" }`, is no
124+
* H5P.Text.
116125
*/
117-
export function typesNamed(source: unknown): string[] {
126+
export function typesNamed(source: unknown, listed: Iterable<string>): string[] {
127+
const names = new Set(listed)
118128
const found = new Set<string>()
119-
const add = (value: unknown): void => {
120-
const name = typeof value === 'string' ? value.trim().split(/\s+/)[0] : ''
121-
if (name) found.add(name)
122-
}
129+
const own = sourceType(source, names)
130+
if (own) found.add(own)
123131
const walk = (value: unknown): void => {
124-
if (Array.isArray(value)) value.forEach(walk)
125-
else if (value && typeof value === 'object') {
126-
add((value as { library?: unknown }).library)
127-
Object.values(value).forEach(walk)
128-
}
132+
if (typeof value === 'string') {
133+
// A name is short: a sentence that happens to hold one is not it.
134+
const name = value.length <= 64 ? /^\s*(\S+)(?:\s+\d+\.\d+)?\s*$/.exec(value)?.[1] : undefined
135+
if (name && names.has(name)) found.add(name)
136+
} else if (Array.isArray(value)) value.forEach(walk)
137+
else if (value && typeof value === 'object') Object.values(value).forEach(walk)
129138
}
130-
if (source && typeof source === 'object' && !Array.isArray(source)) add((source as { type?: unknown }).type)
131139
walk(source)
132140
return [...found]
133141
}

0 commit comments

Comments
 (0)