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.
Files changed (61) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/dashboard.html +56 -8
  3. package/dist/assets/mascot/expressions/celebrate-body.svg +24 -8
  4. package/dist/assets/mascot/expressions/celebrate-face.svg +24 -8
  5. package/dist/assets/mascot/expressions/confused-body.svg +24 -8
  6. package/dist/assets/mascot/expressions/confused-face.svg +24 -8
  7. package/dist/assets/mascot/expressions/cool-body.svg +85 -0
  8. package/dist/assets/mascot/expressions/cool-face.svg +59 -0
  9. package/dist/assets/mascot/expressions/curious-body.svg +24 -8
  10. package/dist/assets/mascot/expressions/curious-face.svg +24 -8
  11. package/dist/assets/mascot/expressions/determined-body.svg +85 -0
  12. package/dist/assets/mascot/expressions/determined-face.svg +59 -0
  13. package/dist/assets/mascot/expressions/excited-body.svg +24 -8
  14. package/dist/assets/mascot/expressions/excited-face.svg +24 -8
  15. package/dist/assets/mascot/expressions/focused-body.svg +24 -8
  16. package/dist/assets/mascot/expressions/focused-face.svg +24 -8
  17. package/dist/assets/mascot/expressions/happy-body.svg +24 -8
  18. package/dist/assets/mascot/expressions/happy-face.svg +24 -8
  19. package/dist/assets/mascot/expressions/hopeful-body.svg +85 -0
  20. package/dist/assets/mascot/expressions/hopeful-face.svg +59 -0
  21. package/dist/assets/mascot/expressions/idea-body.svg +85 -0
  22. package/dist/assets/mascot/expressions/idea-face.svg +59 -0
  23. package/dist/assets/mascot/expressions/laugh-body.svg +85 -0
  24. package/dist/assets/mascot/expressions/laugh-face.svg +59 -0
  25. package/dist/assets/mascot/expressions/love-body.svg +85 -0
  26. package/dist/assets/mascot/expressions/love-face.svg +59 -0
  27. package/dist/assets/mascot/expressions/nervous-body.svg +85 -0
  28. package/dist/assets/mascot/expressions/nervous-face.svg +59 -0
  29. package/dist/assets/mascot/expressions/neutral-body.svg +24 -8
  30. package/dist/assets/mascot/expressions/neutral-face.svg +24 -8
  31. package/dist/assets/mascot/expressions/proud-body.svg +85 -0
  32. package/dist/assets/mascot/expressions/proud-face.svg +59 -0
  33. package/dist/assets/mascot/expressions/relieved-body.svg +85 -0
  34. package/dist/assets/mascot/expressions/relieved-face.svg +59 -0
  35. package/dist/assets/mascot/expressions/sad-body.svg +24 -8
  36. package/dist/assets/mascot/expressions/sad-face.svg +24 -8
  37. package/dist/assets/mascot/expressions/shy-body.svg +85 -0
  38. package/dist/assets/mascot/expressions/shy-face.svg +59 -0
  39. package/dist/assets/mascot/expressions/sleepy-body.svg +24 -8
  40. package/dist/assets/mascot/expressions/sleepy-face.svg +24 -8
  41. package/dist/assets/mascot/expressions/starstruck-body.svg +85 -0
  42. package/dist/assets/mascot/expressions/starstruck-face.svg +59 -0
  43. package/dist/assets/mascot/expressions/surprised-body.svg +24 -8
  44. package/dist/assets/mascot/expressions/surprised-face.svg +24 -8
  45. package/dist/assets/mascot/expressions/thinking-body.svg +24 -8
  46. package/dist/assets/mascot/expressions/thinking-face.svg +24 -8
  47. package/dist/assets/mascot/expressions/wink-body.svg +24 -8
  48. package/dist/assets/mascot/expressions/wink-face.svg +24 -8
  49. package/dist/assets/mascot/expressions/worried-body.svg +24 -8
  50. package/dist/assets/mascot/expressions/worried-face.svg +24 -8
  51. package/dist/assets/mascot/loading.svg +59 -36
  52. package/dist/assets/mascot/mascot.css +24 -8
  53. package/dist/assets/mascot/mascot.js +64 -7
  54. package/dist/assets/mascot.html +15 -11
  55. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  56. package/dist/plugin/hooks/CLAUDE.md +1 -0
  57. package/dist/plugin/hooks/panel/card.tsx +47 -0
  58. package/dist/plugin/hooks/panel/model.ts +43 -1
  59. package/dist/plugin/hooks/panel/register.tsx +101 -3
  60. package/dist/plugin/hooks/panel/types/index.d.ts +5 -0
  61. 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) => (s.step === 'awaiting' || s.step === 'error' ? { ...s, draft: { ...s.draft, picked: id, other: false }, message: null } : s))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eklavya",
3
- "version": "1.85.2",
3
+ "version": "1.87.0",
4
4
  "description": "Learn while your agent works — local knowledge graph, spaced repetition, and quiz gating for agent-assisted development.",
5
5
  "license": "MIT",
6
6
  "repository": {