eklavya 1.85.2 → 1.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/assets/dashboard.html +56 -8
- package/dist/assets/mascot/expressions/celebrate-body.svg +24 -8
- package/dist/assets/mascot/expressions/celebrate-face.svg +24 -8
- package/dist/assets/mascot/expressions/confused-body.svg +24 -8
- package/dist/assets/mascot/expressions/confused-face.svg +24 -8
- package/dist/assets/mascot/expressions/cool-body.svg +85 -0
- package/dist/assets/mascot/expressions/cool-face.svg +59 -0
- package/dist/assets/mascot/expressions/curious-body.svg +24 -8
- package/dist/assets/mascot/expressions/curious-face.svg +24 -8
- package/dist/assets/mascot/expressions/determined-body.svg +85 -0
- package/dist/assets/mascot/expressions/determined-face.svg +59 -0
- package/dist/assets/mascot/expressions/excited-body.svg +24 -8
- package/dist/assets/mascot/expressions/excited-face.svg +24 -8
- package/dist/assets/mascot/expressions/focused-body.svg +24 -8
- package/dist/assets/mascot/expressions/focused-face.svg +24 -8
- package/dist/assets/mascot/expressions/happy-body.svg +24 -8
- package/dist/assets/mascot/expressions/happy-face.svg +24 -8
- package/dist/assets/mascot/expressions/hopeful-body.svg +85 -0
- package/dist/assets/mascot/expressions/hopeful-face.svg +59 -0
- package/dist/assets/mascot/expressions/idea-body.svg +85 -0
- package/dist/assets/mascot/expressions/idea-face.svg +59 -0
- package/dist/assets/mascot/expressions/laugh-body.svg +85 -0
- package/dist/assets/mascot/expressions/laugh-face.svg +59 -0
- package/dist/assets/mascot/expressions/love-body.svg +85 -0
- package/dist/assets/mascot/expressions/love-face.svg +59 -0
- package/dist/assets/mascot/expressions/nervous-body.svg +85 -0
- package/dist/assets/mascot/expressions/nervous-face.svg +59 -0
- package/dist/assets/mascot/expressions/neutral-body.svg +24 -8
- package/dist/assets/mascot/expressions/neutral-face.svg +24 -8
- package/dist/assets/mascot/expressions/proud-body.svg +85 -0
- package/dist/assets/mascot/expressions/proud-face.svg +59 -0
- package/dist/assets/mascot/expressions/relieved-body.svg +85 -0
- package/dist/assets/mascot/expressions/relieved-face.svg +59 -0
- package/dist/assets/mascot/expressions/sad-body.svg +24 -8
- package/dist/assets/mascot/expressions/sad-face.svg +24 -8
- package/dist/assets/mascot/expressions/shy-body.svg +85 -0
- package/dist/assets/mascot/expressions/shy-face.svg +59 -0
- package/dist/assets/mascot/expressions/sleepy-body.svg +24 -8
- package/dist/assets/mascot/expressions/sleepy-face.svg +24 -8
- package/dist/assets/mascot/expressions/starstruck-body.svg +85 -0
- package/dist/assets/mascot/expressions/starstruck-face.svg +59 -0
- package/dist/assets/mascot/expressions/surprised-body.svg +24 -8
- package/dist/assets/mascot/expressions/surprised-face.svg +24 -8
- package/dist/assets/mascot/expressions/thinking-body.svg +24 -8
- package/dist/assets/mascot/expressions/thinking-face.svg +24 -8
- package/dist/assets/mascot/expressions/wink-body.svg +24 -8
- package/dist/assets/mascot/expressions/wink-face.svg +24 -8
- package/dist/assets/mascot/expressions/worried-body.svg +24 -8
- package/dist/assets/mascot/expressions/worried-face.svg +24 -8
- package/dist/assets/mascot/loading.svg +59 -36
- package/dist/assets/mascot/mascot.css +24 -8
- package/dist/assets/mascot/mascot.js +64 -7
- package/dist/assets/mascot.html +15 -11
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/hooks/CLAUDE.md +1 -0
- package/dist/plugin/hooks/panel/card.tsx +47 -0
- package/dist/plugin/hooks/panel/model.ts +43 -1
- package/dist/plugin/hooks/panel/register.tsx +101 -3
- package/dist/plugin/hooks/panel/types/index.d.ts +5 -0
- package/package.json +1 -1
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
STR,
|
|
14
14
|
WORDMARK,
|
|
15
15
|
CLOSE_MS,
|
|
16
|
+
DEMO_QUESTION,
|
|
16
17
|
answerWas,
|
|
18
|
+
demoLast,
|
|
19
|
+
demoResult,
|
|
17
20
|
explainerBrief,
|
|
18
21
|
codify,
|
|
19
22
|
gradingRequest,
|
|
@@ -30,6 +33,7 @@ import {
|
|
|
30
33
|
hintLines,
|
|
31
34
|
} from './model'
|
|
32
35
|
import type { PanelPending, PanelQuestion, PanelState } from './types'
|
|
36
|
+
import type { CardProps } from './card'
|
|
33
37
|
|
|
34
38
|
/**
|
|
35
39
|
* The Eklavya quiz panel: a pane that shows the question the tutor model handed
|
|
@@ -168,9 +172,24 @@ async function sync($: EngineInterface): Promise<'disabled' | 'none' | 'question
|
|
|
168
172
|
/** A throttled sync for busy events: the heartbeat, and the way a question presented elsewhere finds its pane. */
|
|
169
173
|
async function beat($: EngineInterface): Promise<void> {
|
|
170
174
|
if (Date.now() - lastSyncAt < HEARTBEAT_MS) return
|
|
175
|
+
// A mock question is not in the database: a sync would find nothing waiting and clear it.
|
|
176
|
+
if (await inDemo($)) return
|
|
171
177
|
await sync($)
|
|
172
178
|
}
|
|
173
179
|
|
|
180
|
+
async function inDemo($: EngineInterface): Promise<boolean> {
|
|
181
|
+
const s: PanelState = (await $.state.get(REF)).value ?? EMPTY
|
|
182
|
+
return s.demo !== undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The mock question, drawn with the given answer-card style. Opened from the person's own command, so at any width. */
|
|
186
|
+
async function startDemo($: EngineInterface, style: 'button' | 'client'): Promise<void> {
|
|
187
|
+
disarm()
|
|
188
|
+
openedFor = null
|
|
189
|
+
await update($, quiz, () => ({ ...EMPTY, step: 'awaiting', question: DEMO_QUESTION, demo: { style, last: STR.demoReady } }))
|
|
190
|
+
await $.ui.open({ id: PANE, title: STR.brand, closeOnEscape: true })
|
|
191
|
+
}
|
|
192
|
+
|
|
174
193
|
/** Says what went wrong, in words, and keeps whatever the learner had written. */
|
|
175
194
|
async function fail($: EngineInterface, message: string): Promise<void> {
|
|
176
195
|
await update($, quiz, (s: PanelState) => ({ ...s, step: 'error', message }))
|
|
@@ -203,6 +222,21 @@ async function submit($: EngineInterface, wanted: PanelPending | null): Promise<
|
|
|
203
222
|
}
|
|
204
223
|
}
|
|
205
224
|
|
|
225
|
+
// The mock question is answered here: nothing is sent, graded by the server or recorded.
|
|
226
|
+
if (state.demo) {
|
|
227
|
+
const kind = pending ? pending.kind : state.draft.other ? 'text' : 'choice'
|
|
228
|
+
const result = demoResult(kind, pending ? null : state.draft.picked)
|
|
229
|
+
await update($, quiz, (s: PanelState) => ({
|
|
230
|
+
...s,
|
|
231
|
+
step: result.phase === 'skipped' ? 'skipped' : 'feedback',
|
|
232
|
+
result,
|
|
233
|
+
pending: null,
|
|
234
|
+
message: null,
|
|
235
|
+
demo: s.demo && { ...s.demo, last: demoLast(`${kind} answer submitted`) },
|
|
236
|
+
}))
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
206
240
|
await update($, quiz, (s: PanelState) => ({ ...s, step: 'grading', message: null }))
|
|
207
241
|
const where = await whereAmI($)
|
|
208
242
|
if (epoch !== mine) return
|
|
@@ -284,6 +318,11 @@ export const register: Register = on => {
|
|
|
284
318
|
} catch {
|
|
285
319
|
/* Without the command the pane still opens by itself; only reopening is lost. */
|
|
286
320
|
}
|
|
321
|
+
try {
|
|
322
|
+
await $.command.register({ name: 'eklavya-panel-demo', description: 'Open the Eklavya quiz panel with a mock question, to try its clicks' })
|
|
323
|
+
} catch {
|
|
324
|
+
/* Only the test shortcut is lost. */
|
|
325
|
+
}
|
|
287
326
|
try {
|
|
288
327
|
await sync($)
|
|
289
328
|
} catch {
|
|
@@ -348,6 +387,8 @@ export const register: Register = on => {
|
|
|
348
387
|
on('command.run', { command: 'eklavya-panel' }, async $ => {
|
|
349
388
|
try {
|
|
350
389
|
openedFor = null
|
|
390
|
+
// A mock question on screen gives way to the real state.
|
|
391
|
+
if (await inDemo($)) await update($, quiz, () => EMPTY)
|
|
351
392
|
// The person typed this, so the pane is seated at any width. Off is inert:
|
|
352
393
|
// the server says so and nothing is opened.
|
|
353
394
|
const found = await sync($)
|
|
@@ -359,6 +400,30 @@ export const register: Register = on => {
|
|
|
359
400
|
}
|
|
360
401
|
})
|
|
361
402
|
|
|
403
|
+
// The mock question: opens at once, answered on this machine, so a card or a click can be tried with no real question waiting.
|
|
404
|
+
on('command.run', { command: 'eklavya-panel-demo' }, async $ => {
|
|
405
|
+
try {
|
|
406
|
+
await startDemo($, 'button')
|
|
407
|
+
return { text: STR.demoOpened }
|
|
408
|
+
} catch {
|
|
409
|
+
return { text: STR.demoFailed }
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// A press on a whole-card `Client` (card.tsx) arrives here as data, never as a fact: only an id of the open question counts.
|
|
414
|
+
on('ui.message', { component: 'Pane' }, async ($, e, next) => {
|
|
415
|
+
if (e.requestId === PANE) {
|
|
416
|
+
try {
|
|
417
|
+
const id = (e.data as { pick?: unknown } | null)?.pick
|
|
418
|
+
const s: PanelState = (await $.state.get(REF)).value ?? EMPTY
|
|
419
|
+
if (typeof id === 'string' && s.question?.options.some(o => o.id === id)) await pick($, id, 'whole card')
|
|
420
|
+
} catch {
|
|
421
|
+
/* Fail open: the card stays as it was. */
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return next(e)
|
|
425
|
+
})
|
|
426
|
+
|
|
362
427
|
// The mod's queued prompts are instructions for the model, not words for the
|
|
363
428
|
// learner: their transcript row shows one short line, as a tool call does.
|
|
364
429
|
// Only the drawing changes; the model still reads the whole message.
|
|
@@ -373,7 +438,7 @@ export const register: Register = on => {
|
|
|
373
438
|
on('ui.render', { component: 'Pane', requestId: 'eklavya-quiz' }, async ($, e, next) => {
|
|
374
439
|
// Only our pane: every other pane, and everything else, is the host's.
|
|
375
440
|
if (e.requestId !== PANE) return next(e)
|
|
376
|
-
const { Box, Text, Button, Input, Markdown } = $.ui.resolve(e) as any
|
|
441
|
+
const { Box, Text, Button, Input, Markdown, Client } = $.ui.resolve(e) as any
|
|
377
442
|
const s: PanelState = await read($, quiz)
|
|
378
443
|
const q = s.question
|
|
379
444
|
|
|
@@ -454,9 +519,24 @@ export const register: Register = on => {
|
|
|
454
519
|
{body}
|
|
455
520
|
</Box>
|
|
456
521
|
)
|
|
522
|
+
// The mock question's tester bar: what it is, the last click, and (desktop only) how the answer cards are drawn.
|
|
523
|
+
const demoBar = s.demo ? (
|
|
524
|
+
<Box key="demo-bar" flexDirection="column" gap={app ? 1 : 0}>
|
|
525
|
+
<Text color="warning">{STR.demoBanner}</Text>
|
|
526
|
+
<Text dimColor>{s.demo.last}</Text>
|
|
527
|
+
{app ? (
|
|
528
|
+
<Box flexDirection="row" gap={2} alignItems="center">
|
|
529
|
+
<Text dimColor>{STR.demoStyleLabel}</Text>
|
|
530
|
+
<Button key="style-button" label={STR.demoStyleButton} variant={s.demo.style === 'button' ? 'primary' : 'secondary'} onPress={() => setStyle($, 'button')} />
|
|
531
|
+
<Button key="style-client" label={STR.demoStyleClient} variant={s.demo.style === 'client' ? 'primary' : 'secondary'} onPress={() => setStyle($, 'client')} />
|
|
532
|
+
</Box>
|
|
533
|
+
) : null}
|
|
534
|
+
</Box>
|
|
535
|
+
) : null
|
|
457
536
|
const frame = (...body: unknown[]) => (
|
|
458
537
|
<Box flexDirection="column" paddingX={1} gap={1}>
|
|
459
538
|
{head(q ? topicLabel(q) : undefined)}
|
|
539
|
+
{demoBar}
|
|
460
540
|
{body}
|
|
461
541
|
{hint}
|
|
462
542
|
</Box>
|
|
@@ -520,8 +600,14 @@ export const register: Register = on => {
|
|
|
520
600
|
{note && !app ? <Text wrap="wrap" dimColor={!chosen} color={chosen ? PICKED.note : undefined}>{`\n ${note}`}</Text> : null}
|
|
521
601
|
</Button>
|
|
522
602
|
)
|
|
603
|
+
const wholeCard = app && s.demo?.style === 'client'
|
|
523
604
|
const options = q.options.map((o, i) => {
|
|
524
605
|
const chosen = !s.draft.other && s.draft.picked === o.id
|
|
606
|
+
// The whole card is one pressable region, so a click on its padding or note picks too (card.tsx).
|
|
607
|
+
if (wholeCard) {
|
|
608
|
+
const props: CardProps = { id: o.id, label: plainLabel(o.label), note: o.note ? plainLabel(o.note) : '', chosen, locked, brand: BRAND, fill: PICKED.bg, text: PICKED.text, noteText: PICKED.note }
|
|
609
|
+
return <Client key={`card-${o.id}`} module="./card.tsx" width="100%" props={props} />
|
|
610
|
+
}
|
|
525
611
|
return card(
|
|
526
612
|
o.id,
|
|
527
613
|
chosen,
|
|
@@ -591,9 +677,18 @@ export const register: Register = on => {
|
|
|
591
677
|
})
|
|
592
678
|
}
|
|
593
679
|
|
|
680
|
+
/** The demo's switch between the two ways of drawing an answer card on the desktop. */
|
|
681
|
+
async function setStyle($: EngineInterface, style: 'button' | 'client'): Promise<void> {
|
|
682
|
+
await update($, quiz, (s: PanelState) => (s.demo ? { ...s, demo: { ...s.demo, style, last: demoLast(`card style set to ${style}`) } } : s))
|
|
683
|
+
}
|
|
684
|
+
|
|
594
685
|
/** Selecting an option records a draft and nothing else: it never submits, grades or skips. */
|
|
595
|
-
async function pick($: EngineInterface, id: string): Promise<void> {
|
|
596
|
-
await update($, quiz, (s: PanelState) =>
|
|
686
|
+
async function pick($: EngineInterface, id: string, via = 'button'): Promise<void> {
|
|
687
|
+
await update($, quiz, (s: PanelState) =>
|
|
688
|
+
s.step === 'awaiting' || s.step === 'error'
|
|
689
|
+
? { ...s, draft: { ...s.draft, picked: id, other: false }, message: null, ...(s.demo ? { demo: { ...s.demo, last: demoLast(`${id} picked (${via})`) } } : {}) }
|
|
690
|
+
: s,
|
|
691
|
+
)
|
|
597
692
|
try {
|
|
598
693
|
// The host's focus mark follows the pick, so the highlighted answer is the chosen one even after a hotkey.
|
|
599
694
|
await $.ui.focus({ requestId: PANE, key: `opt-${id}` })
|
|
@@ -675,6 +770,9 @@ async function askNext($: EngineInterface): Promise<void> {
|
|
|
675
770
|
disarm()
|
|
676
771
|
try {
|
|
677
772
|
openedFor = null
|
|
773
|
+
const now: PanelState = (await $.state.get(REF)).value ?? EMPTY
|
|
774
|
+
// Next in the demo starts the mock again: it must not ask the model for a real question.
|
|
775
|
+
if (now.demo) return await startDemo($, now.demo.style)
|
|
678
776
|
await update($, quiz, () => ({ ...EMPTY, step: 'loading' }))
|
|
679
777
|
await $.prompt.submit({ text: NEXT_PROMPT })
|
|
680
778
|
} catch {
|
|
@@ -55,6 +55,11 @@ export type PanelState = {
|
|
|
55
55
|
result: PanelResult | null
|
|
56
56
|
/** Words for the learner: why the pane is in `error`, or a one-line notice in `none`. */
|
|
57
57
|
message: string | null
|
|
58
|
+
/**
|
|
59
|
+
* Set only by `/eklavya-panel-demo`: the question is a mock, answered on this machine and never sent to the
|
|
60
|
+
* server. `style` is how the desktop draws the answer cards; `last` is the latest click, for the tester.
|
|
61
|
+
*/
|
|
62
|
+
demo?: { style: 'button' | 'client'; last: string }
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
declare module 'claude-code' {
|
package/package.json
CHANGED