@kidsinai/kids-client 0.0.1

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.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * §3.2 Mission-in-progress screen.
3
+ *
4
+ * Layout: Header (mission progress + Stars) on top, chat stream in the
5
+ * middle, input box at the bottom, optional Toast under that. Streaming
6
+ * AI replies render in-place via ChatStream's <Static>/live split.
7
+ *
8
+ * Esc key while thinking → calls onAbort (wired by App-level to
9
+ * client.session.abort()).
10
+ */
11
+
12
+ import React, { useState } from "react"
13
+ import { Box, Text, useInput } from "ink"
14
+ import { Header } from "../components/Header.tsx"
15
+ import { ChatStream } from "../components/ChatStream.tsx"
16
+ import { Input } from "../components/Input.tsx"
17
+ import { Thinking } from "../components/Thinking.tsx"
18
+ import { Toast } from "../components/Toast.tsx"
19
+ import { getTheme } from "../theme.ts"
20
+ import type { KidsClientState } from "../../../core/store.ts"
21
+
22
+ interface MissionScreenProps {
23
+ state: KidsClientState
24
+ locale: "zh-Hans" | "en"
25
+ onPrompt: (text: string) => void
26
+ onAbort: () => void
27
+ }
28
+
29
+ export function MissionScreen({ state, locale, onPrompt, onAbort }: MissionScreenProps): React.ReactElement {
30
+ const theme = getTheme()
31
+ const [draft, setDraft] = useState("")
32
+ const placeholder = locale === "zh-Hans" ? "想做什么?告诉我吧(中文/英文都行)" : "What would you like to make? (English or Chinese)"
33
+
34
+ useInput((_, key) => {
35
+ if (key.escape && state.thinking) onAbort()
36
+ })
37
+
38
+ const hint = locale === "zh-Hans"
39
+ ? "提示:做完一关时打 /check 或「我做完了」就能验收 · 按 Esc 打断 AI"
40
+ : "Tip: type /check or 'I'm done' to validate · Esc interrupts the AI"
41
+
42
+ return (
43
+ <Box flexDirection="column">
44
+ <Header
45
+ packTitle={state.packTitle}
46
+ missionTitle={state.missionTitle}
47
+ missionIndex={state.missionIndex}
48
+ missionTotal={state.missionTotal}
49
+ starsBalance={state.starsBalance}
50
+ starsBudget={state.starsBudget}
51
+ />
52
+ <Box marginTop={1} flexDirection="column" flexGrow={1}>
53
+ <ChatStream messages={state.messages} />
54
+ {state.thinking && (
55
+ <Box marginTop={1}>
56
+ <Thinking locale={locale} />
57
+ </Box>
58
+ )}
59
+ </Box>
60
+ <Box marginTop={1}>
61
+ <Input
62
+ value={draft}
63
+ onChange={setDraft}
64
+ onSubmit={(v) => {
65
+ const text = v.trim()
66
+ if (!text) return
67
+ setDraft("")
68
+ onPrompt(text)
69
+ }}
70
+ placeholder={placeholder}
71
+ disabled={state.thinking || state.pendingPermission !== null}
72
+ />
73
+ </Box>
74
+ {state.toast ? (
75
+ <Box marginTop={1}>
76
+ <Toast toast={state.toast} />
77
+ </Box>
78
+ ) : (
79
+ <Box marginTop={1}>
80
+ <Text color={theme.fgDim} dimColor>{hint}</Text>
81
+ </Box>
82
+ )}
83
+ </Box>
84
+ )
85
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * §3.4 Permission confirmation. Modal — blocks input until kid chooses
3
+ * y (allow once) / n (deny) / e (edit / I'll do it myself).
4
+ *
5
+ * y → client.permission.reply(id, { reply: "once" })
6
+ * n → client.permission.reply(id, { reply: "reject" })
7
+ * e → close modal, send a fresh kid prompt of the form "no, do it differently"
8
+ *
9
+ * The PRD uses "y/n/e" to avoid the engineering-vibes of allow/deny.
10
+ */
11
+
12
+ import React from "react"
13
+ import { Box, Text, useInput } from "ink"
14
+ import { getTheme } from "../theme.ts"
15
+ import type { PendingPermission } from "../../../core/store.ts"
16
+
17
+ interface PermissionModalProps {
18
+ permission: PendingPermission
19
+ locale: "zh-Hans" | "en"
20
+ onAllow: () => void
21
+ onDeny: () => void
22
+ onEdit: () => void
23
+ }
24
+
25
+ export function PermissionModal({ permission, locale, onAllow, onDeny, onEdit }: PermissionModalProps): React.ReactElement {
26
+ const theme = getTheme()
27
+ useInput((input) => {
28
+ const ch = input.toLowerCase()
29
+ if (ch === "y") onAllow()
30
+ else if (ch === "n") onDeny()
31
+ else if (ch === "e") onEdit()
32
+ })
33
+ const t = STRINGS[locale]
34
+ return (
35
+ <Box flexDirection="column" borderStyle="double" borderColor={theme.warn} paddingX={2} paddingY={1}>
36
+ <Text color={theme.warn} bold>{t.title}</Text>
37
+ <Box marginTop={1}>
38
+ <Text color={theme.fg}>{permission.summary}</Text>
39
+ </Box>
40
+ {permission.tool && (
41
+ <Box marginTop={1}>
42
+ <Text color={theme.fgDim}>tool: {permission.tool}</Text>
43
+ </Box>
44
+ )}
45
+ {permission.starsEstimated && permission.starsEstimated > 0 && (
46
+ <Box marginTop={1}>
47
+ <Text color={theme.stars}>{t.starsCost(permission.starsEstimated)}</Text>
48
+ </Box>
49
+ )}
50
+ <Box marginTop={1}>
51
+ <Text color={theme.accent}>[y]</Text>
52
+ <Text color={theme.fg}> {t.yes} </Text>
53
+ <Text color={theme.accent}>[n]</Text>
54
+ <Text color={theme.fg}> {t.no} </Text>
55
+ <Text color={theme.accent}>[e]</Text>
56
+ <Text color={theme.fg}> {t.edit}</Text>
57
+ </Box>
58
+ </Box>
59
+ )
60
+ }
61
+
62
+ const STRINGS = {
63
+ "zh-Hans": {
64
+ title: "AI 想做这件事",
65
+ yes: "可以做",
66
+ no: "不要",
67
+ edit: "我来改",
68
+ starsCost: (n: number) => `预估消耗 ${n}⭐`,
69
+ },
70
+ en: {
71
+ title: "The AI wants to do this",
72
+ yes: "Go ahead",
73
+ no: "Stop",
74
+ edit: "I'll do it",
75
+ starsCost: (n: number) => `Estimated cost: ${n}⭐`,
76
+ },
77
+ } as const
@@ -0,0 +1,83 @@
1
+ /**
2
+ * §3.1 Startup screen — first impression. Must paint within 5s.
3
+ *
4
+ * Quick keys:
5
+ * Enter → start a free-play session
6
+ * c → choose a Course Pack (V0 MVP: hardcoded portfolio-site)
7
+ * r → resume the last session (V0 MVP: not implemented yet)
8
+ * h → show help (kid-friendly)
9
+ */
10
+
11
+ import React from "react"
12
+ import { Box, Text, useInput } from "ink"
13
+ import { getTheme } from "../theme.ts"
14
+ import { KeyHints } from "../components/KeyHints.tsx"
15
+
16
+ interface StartupScreenProps {
17
+ locale: "zh-Hans" | "en"
18
+ coursePack: string | null
19
+ onStart: (mode: "free" | "course" | "resume" | "help") => void
20
+ }
21
+
22
+ export function StartupScreen({ locale, coursePack, onStart }: StartupScreenProps): React.ReactElement {
23
+ const theme = getTheme()
24
+ useInput((input, key) => {
25
+ if (key.return) onStart(coursePack ? "course" : "free")
26
+ else if (input === "c") onStart("course")
27
+ else if (input === "r") onStart("resume")
28
+ else if (input === "h") onStart("help")
29
+ })
30
+ const t = STRINGS[locale]
31
+ return (
32
+ <Box flexDirection="column" paddingY={1}>
33
+ <Box borderStyle="double" borderColor={theme.border} paddingX={2} paddingY={1} flexDirection="column">
34
+ <Text color={theme.accent} bold>
35
+ Airbotix Kids OpenCode
36
+ </Text>
37
+ <Box marginTop={1} flexDirection="column">
38
+ <Text color={theme.fg}>{t.greet1}</Text>
39
+ <Text color={theme.fg}>{t.greet2}</Text>
40
+ </Box>
41
+ <Box marginTop={1}>
42
+ <Text color={theme.fgDim}>{t.disclaim}</Text>
43
+ </Box>
44
+ <Box marginTop={1}>
45
+ <Text color={theme.warn}>{t.helpline}</Text>
46
+ </Box>
47
+ </Box>
48
+ <Box marginTop={1}>
49
+ <KeyHints hints={[
50
+ { key: "Enter", label: coursePack ? t.startCourse : t.startFree },
51
+ { key: "c", label: t.pickCourse },
52
+ { key: "r", label: t.resume },
53
+ { key: "h", label: t.help },
54
+ ]} />
55
+ </Box>
56
+ </Box>
57
+ )
58
+ }
59
+
60
+ const STRINGS = {
61
+ "zh-Hans": {
62
+ greet1: "你好!我是 Kids OpenCode —— 帮你做编程项目的 AI 老师。",
63
+ greet2: "",
64
+ disclaim: "我不是真人,有时候会答错。遇到不懂的,问家长或老师。",
65
+ helpline: "澳大利亚紧急求助:Kids Helpline 1800 55 1800",
66
+ startFree: "开始新项目",
67
+ startCourse: "继续 Course Pack",
68
+ pickCourse: "选 Course Pack",
69
+ resume: "继续上次",
70
+ help: "帮助",
71
+ },
72
+ en: {
73
+ greet1: "Hi! I'm Kids OpenCode — the AI teacher who helps you build coding projects.",
74
+ greet2: "",
75
+ disclaim: "I'm not a real person and I can be wrong. Ask a parent or teacher if you're unsure.",
76
+ helpline: "In Australia: Kids Helpline 1800 55 1800",
77
+ startFree: "Start a new project",
78
+ startCourse: "Continue Course Pack",
79
+ pickCourse: "Pick a Course Pack",
80
+ resume: "Resume last session",
81
+ help: "Help",
82
+ },
83
+ } as const
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Kid-warm color tokens. Hand-tuned to satisfy WCAG AA on most terminals
3
+ * with dark backgrounds. High-contrast variant available via $KIDS_HC=1.
4
+ *
5
+ * Sourced conceptually from kids-tui-plugin/themes/kids-warm.json but
6
+ * inlined here because Ink consumes raw ANSI/chalk color names, not
7
+ * opentui theme JSON.
8
+ */
9
+
10
+ export interface Theme {
11
+ fg: string
12
+ bg: string
13
+ fgDim: string
14
+ accent: string
15
+ warn: string
16
+ danger: string
17
+ success: string
18
+ agent: string
19
+ kid: string
20
+ system: string
21
+ border: string
22
+ stars: string
23
+ }
24
+
25
+ const DEFAULT: Theme = {
26
+ fg: "white",
27
+ bg: "black",
28
+ fgDim: "gray",
29
+ accent: "yellow",
30
+ warn: "yellow",
31
+ danger: "red",
32
+ success: "green",
33
+ agent: "cyan",
34
+ kid: "magenta",
35
+ system: "blueBright",
36
+ border: "yellow",
37
+ stars: "yellowBright",
38
+ }
39
+
40
+ const HIGH_CONTRAST: Theme = {
41
+ fg: "whiteBright",
42
+ bg: "black",
43
+ fgDim: "white",
44
+ accent: "yellowBright",
45
+ warn: "yellowBright",
46
+ danger: "redBright",
47
+ success: "greenBright",
48
+ agent: "cyanBright",
49
+ kid: "magentaBright",
50
+ system: "blueBright",
51
+ border: "whiteBright",
52
+ stars: "yellowBright",
53
+ }
54
+
55
+ export function getTheme(): Theme {
56
+ if (process.env.KIDS_HC === "1") return HIGH_CONTRAST
57
+ return DEFAULT
58
+ }