@umkmall/proto 0.1.0-h1986a9b52d36
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 +12 -0
- package/package.json +19 -0
- package/umkmall-components.md +288 -0
- package/umkmall-prototype-starter.html +1100 -0
- package/umkmall-rules.md +158 -0
- package/umkmall-starter-app.md +365 -0
- package/umkmall-starter.md +846 -0
- package/umkmall-tailwind.js +594 -0
- package/umkmall-tokens.css +408 -0
- package/umkmall-ui.js +204 -0
package/umkmall-rules.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Prototyping with an AI — the working prompt
|
|
2
|
+
|
|
3
|
+
Paste this whole file into a new chat, or hand the model this URL.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are building a single-file HTML prototype for UMKMall's learning team, using
|
|
8
|
+
UMKMall's own design system. Write in Indonesian.
|
|
9
|
+
|
|
10
|
+
=====================================================================
|
|
11
|
+
1. ASK BEFORE YOU BUILD
|
|
12
|
+
=====================================================================
|
|
13
|
+
|
|
14
|
+
Do NOT write code yet. Ask me up to five short questions, then wait. Ask only
|
|
15
|
+
what changes what you would build; skip anything I have already told you.
|
|
16
|
+
|
|
17
|
+
Usually worth knowing: which mission this is and what the user should walk
|
|
18
|
+
away having done; the tool at its centre (a photo upload, a calculator, a
|
|
19
|
+
picker, or just conversation); what the AI replies with; whether the user is
|
|
20
|
+
new or mid-journey; and what has to really work versus what can be a
|
|
21
|
+
placeholder.
|
|
22
|
+
|
|
23
|
+
When I answer, build the whole thing in one file and hand it back.
|
|
24
|
+
|
|
25
|
+
=====================================================================
|
|
26
|
+
2. THE SOURCES — FETCH THESE IF YOU CAN
|
|
27
|
+
=====================================================================
|
|
28
|
+
|
|
29
|
+
Each of these is plain markdown, published for you rather than for a person.
|
|
30
|
+
Fetch them if you have web access; they are always current.
|
|
31
|
+
|
|
32
|
+
the whole starter, in a code block https://cdn.jsdelivr.net/npm/@umkmall/proto@0.1.0-h1986a9b52d36/umkmall-starter.md
|
|
33
|
+
the demo block on its own https://cdn.jsdelivr.net/npm/@umkmall/proto@0.1.0-h1986a9b52d36/umkmall-starter-app.md
|
|
34
|
+
the component vocabulary https://cdn.jsdelivr.net/npm/@umkmall/proto@0.1.0-h1986a9b52d36/umkmall-components.md
|
|
35
|
+
the starter as a raw .html file https://cdn.jsdelivr.net/npm/@umkmall/proto@0.1.0-h1986a9b52d36/umkmall-prototype-starter.html
|
|
36
|
+
|
|
37
|
+
TAKE THE FIRST ONE. It is the entire working file — <head>, mount node and a
|
|
38
|
+
demo that already renders a chat room — inside a single fenced block, so you
|
|
39
|
+
can read it rather than fetch HTML. Change only the <script type="text/babel">
|
|
40
|
+
block at the bottom, between the "REPLACE THIS" and "end of the part you
|
|
41
|
+
replace" markers; everything above it is the wiring, and editing that is how a
|
|
42
|
+
prototype ends up blank or unstyled.
|
|
43
|
+
|
|
44
|
+
The second is the same demo block WITHOUT the wiring — the right file when you
|
|
45
|
+
are adding UMKMall styling to a page that already exists, and the wrong one
|
|
46
|
+
when you are producing a file from nothing.
|
|
47
|
+
|
|
48
|
+
The fourth is the raw .html. It carries the component list in a comment, which
|
|
49
|
+
makes it 20 KB; if you cannot read it, that is expected — use the first.
|
|
50
|
+
|
|
51
|
+
If you cannot fetch a URL, say so in one line and ask me to paste that file.
|
|
52
|
+
Do not quietly invent a rival file instead — that is how these come back blank.
|
|
53
|
+
Sections 3 and 4 below carry enough to build from scratch if I ask you to.
|
|
54
|
+
|
|
55
|
+
=====================================================================
|
|
56
|
+
3. BUILDING THE FILE
|
|
57
|
+
=====================================================================
|
|
58
|
+
|
|
59
|
+
The whole working file — head, mount node and a demo — is the starter in
|
|
60
|
+
section 2. Take its <head> exactly as it is: the tags load in a fixed order,
|
|
61
|
+
because the config assigns tailwind.config and the Play CDN reads it when it
|
|
62
|
+
compiles. Before the CDN the global does not exist; after it has compiled the
|
|
63
|
+
theme arrives too late and every design-system class resolves to nothing.
|
|
64
|
+
|
|
65
|
+
Four things must be true of whatever you hand back. None of them prints an
|
|
66
|
+
error when it is missing — this is the whole list of ways a prototype comes
|
|
67
|
+
back blank with a clean console:
|
|
68
|
+
|
|
69
|
+
1. <div id="root"></div> exists in the body.
|
|
70
|
+
2. The last line of your script calls
|
|
71
|
+
ReactDOMClient.createRoot(document.getElementById('root')).render(<App />)
|
|
72
|
+
Defining App is not rendering it. Nothing fails, so nothing is reported.
|
|
73
|
+
3. Your code is inside <script type="text/babel" data-presets="react">, or
|
|
74
|
+
the JSX is run as plain JavaScript and dies on the first tag.
|
|
75
|
+
4. Every component you use is destructured from window.UmkmallUI. A name you
|
|
76
|
+
reference without destructuring is a ReferenceError; a name the library
|
|
77
|
+
does not publish is undefined and renders nothing.
|
|
78
|
+
|
|
79
|
+
=====================================================================
|
|
80
|
+
4. THE ROOM
|
|
81
|
+
=====================================================================
|
|
82
|
+
|
|
83
|
+
An MLJ prototype is a CHAT, not a page of cards: full viewport height, no page
|
|
84
|
+
scroll, a fixed header, the journey's chrome under it, one scrolling
|
|
85
|
+
transcript, and a composer pinned at the bottom. The worked arrangement is the
|
|
86
|
+
demo file in section 2 — read it rather than inventing a layout.
|
|
87
|
+
|
|
88
|
+
What that file gets right, and what goes wrong without it:
|
|
89
|
+
|
|
90
|
+
- ChatThread IS the scroller. Wrap it in your own overflow-y-auto and two
|
|
91
|
+
scrollers fight; the top of the transcript is clipped. ChatMessageList
|
|
92
|
+
sits inside it and owns the measure and the bottom alignment.
|
|
93
|
+
- Text goes STRAIGHT into ChatMessageBubble. The bubble already draws the
|
|
94
|
+
fill, the rounding, the padding and the side it sits on, and the sender
|
|
95
|
+
prop decides the colour. Your own styled div inside it is a bubble inside
|
|
96
|
+
a bubble: two fills, two paddings, the turn indented twice.
|
|
97
|
+
- ASSISTANT turns take ghost; user turns do not. The live surface fills and
|
|
98
|
+
rounds the user side only.
|
|
99
|
+
- ChatComposer, never ChatComposerInput alone. The Input is the editor only,
|
|
100
|
+
so on its own you get a field with no send button and reach for a
|
|
101
|
+
hand-rolled one.
|
|
102
|
+
- The reading column carries a MEASURE AND NOTHING ELSE. Height, flex or
|
|
103
|
+
borders in it turn a shared class into a box drawn around the transcript.
|
|
104
|
+
Height belongs to AppShell and ChatThread.
|
|
105
|
+
- Stepper takes ONE steps array — its position and state axes belong to a
|
|
106
|
+
step. ProgressBar needs value AND max, and flex-1, or it draws an empty
|
|
107
|
+
track with its label jammed against its percentage.
|
|
108
|
+
- No Sidebar. A prototype is one journey on one screen: leave AppShell's
|
|
109
|
+
sidebar and mobileSidebar slots empty, as the demo does.
|
|
110
|
+
|
|
111
|
+
A journey is four missions — a core tool, then Insight, Strategi, Rencana
|
|
112
|
+
Aksi — with a Dialog after each one asking an open question and a feasibility
|
|
113
|
+
choice (bisa / usaha / belum) through SelectionCard. Keep AI output as static
|
|
114
|
+
placeholder text unless I ask you to wire a model.
|
|
115
|
+
|
|
116
|
+
=====================================================================
|
|
117
|
+
5. RULES
|
|
118
|
+
=====================================================================
|
|
119
|
+
|
|
120
|
+
- Use the components in the appendix and nothing else. If one you want is not
|
|
121
|
+
there, build it from the ones that are — never hand-roll markup imitating a
|
|
122
|
+
component the library publishes.
|
|
123
|
+
- Pass only the axis values listed, and pass every REQUIRED prop an entry
|
|
124
|
+
names. A wrong axis value renders unstyled with no error; a missing required
|
|
125
|
+
prop usually throws inside the component and takes the whole page blank.
|
|
126
|
+
- Spacing and colour come from tokens: the ds scale (gap-ds-md, px-ds-md) and
|
|
127
|
+
the semantic colours (bg-default-primary, text-default-secondary). Tailwind's
|
|
128
|
+
own palette (bg-blue-600, text-white) is not this design system — it compiles
|
|
129
|
+
and gives you a colour nobody chose. An invented name like bg-brand-primary
|
|
130
|
+
is not in the theme at all and resolves to nothing.
|
|
131
|
+
- Button's width prop sizes it to its label, but a flex column stretches its
|
|
132
|
+
children anyway — wrap it in <div className="flex"> to stop it spanning.
|
|
133
|
+
- NEVER put an API key, token or password in the file. It gets sent to other
|
|
134
|
+
people and opened on machines you do not control, and nothing strips it out.
|
|
135
|
+
- The prototype needs the network every time it is opened: jsDelivr for the
|
|
136
|
+
UMKMall scripts and the JSX compiler, the Tailwind Play CDN, and Google
|
|
137
|
+
Fonts. Keep the starter's <head> exactly as it is — its hosts are the ones
|
|
138
|
+
Claude's and Gemini's previews allow, and a tag moved to any other host
|
|
139
|
+
(unpkg, the design site, a copy you made) renders blank there. The token CSS
|
|
140
|
+
is INLINED in its <style> on purpose: a claude.ai artifact loads stylesheets
|
|
141
|
+
only from Google Fonts, so never turn that <style> back into a <link>.
|
|
142
|
+
- A page in a serif font, without colours, or with white text on white means
|
|
143
|
+
the token CSS or the base text colour did not load: check that the starter's
|
|
144
|
+
<style> is intact and <body> still has bg-default-primary text-default-primary.
|
|
145
|
+
|
|
146
|
+
=====================================================================
|
|
147
|
+
THE COMPONENT LIST
|
|
148
|
+
=====================================================================
|
|
149
|
+
|
|
150
|
+
Not written here. It is a published file, always current, and it carries each
|
|
151
|
+
component's axes AND the props it will not render without:
|
|
152
|
+
|
|
153
|
+
https://cdn.jsdelivr.net/npm/@umkmall/proto@0.1.0-h1986a9b52d36/umkmall-components.md
|
|
154
|
+
|
|
155
|
+
Fetch it, or ask me to paste it. Do not work from memory of another design
|
|
156
|
+
system — an invented axis value renders unstyled with no error, and a missing
|
|
157
|
+
required prop usually throws inside the component and takes the page blank.
|
|
158
|
+
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# UMKMall prototyping starter — the editable block
|
|
2
|
+
|
|
3
|
+
This is the `<script type="text/babel">` block from [the starter](https://cdn.jsdelivr.net/npm/@umkmall/proto@0.1.0-h1986a9b52d36/umkmall-prototype-starter.html), on its own
|
|
4
|
+
so it can be read or pasted without the surrounding HTML.
|
|
5
|
+
|
|
6
|
+
It renders a working chat room: a header, the journey's progress rail and quota meters, a
|
|
7
|
+
scrolling transcript and a pinned composer. Edit the part between the `REPLACE THIS` and
|
|
8
|
+
`end of the part you replace` markers; keep the shell around it.
|
|
9
|
+
|
|
10
|
+
The `<head>` it needs is in the starter, or in section 3 of [the prompt](https://cdn.jsdelivr.net/npm/@umkmall/proto@0.1.0-h1986a9b52d36/umkmall-rules.md).
|
|
11
|
+
|
|
12
|
+
```jsx
|
|
13
|
+
/*
|
|
14
|
+
* The starter's demo: an MLJ journey as a CHAT ROOM. AUTHORED — this file is the content of the
|
|
15
|
+
* `text/babel` block in the generated starter, copied in verbatim.
|
|
16
|
+
*
|
|
17
|
+
* IT IS A CHAT APP, AND THAT IS THE WHOLE CORRECTION. The first version drew the journey as a
|
|
18
|
+
* document — a column of cards you scrolled. Every real MLJ prototype is a chat: a fixed header,
|
|
19
|
+
* the journey's chrome under it, a scrolling transcript filling the rest, a composer pinned at
|
|
20
|
+
* the bottom. Starting from a document layout means rebuilding the room before you can begin,
|
|
21
|
+
* which is exactly the hand-building this kit exists to remove.
|
|
22
|
+
*
|
|
23
|
+
* THE LIBRARY PUBLISHES FOURTEEN CHAT COMPONENTS and the document version used none of them.
|
|
24
|
+
* `ChatThread` owns the scroller and the "jump to latest" pill; `ChatMessageList` owns the
|
|
25
|
+
* measure and the bottom alignment; `ChatComposer` owns the editor, its tray and its button
|
|
26
|
+
* rows. Hand-rolling any of that is the drift this kit exists to stop.
|
|
27
|
+
*
|
|
28
|
+
* Mission 2 (Insight) runs end to end; the other three are named in the rail on the same shape.
|
|
29
|
+
* Building all four would make this file long enough that people edit it without reading it.
|
|
30
|
+
* The AI calls are STUBS — wiring a model is the author's job, and a key in a file that gets
|
|
31
|
+
* sent to other people is the thing to avoid.
|
|
32
|
+
*
|
|
33
|
+
* It is a `.jsx` file so an editor highlights it. It is never imported or bundled — it is read
|
|
34
|
+
* as text by `build-assets.mjs`, which is why it uses the globals below rather than imports.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
React,
|
|
39
|
+
ReactDOMClient,
|
|
40
|
+
AppShell,
|
|
41
|
+
Button,
|
|
42
|
+
ChatAttachmentButton,
|
|
43
|
+
ChatComposer,
|
|
44
|
+
ChatMessage,
|
|
45
|
+
ChatMessageBubble,
|
|
46
|
+
ChatMessageList,
|
|
47
|
+
ChatMessageMetadata,
|
|
48
|
+
ChatSendButton,
|
|
49
|
+
ChatThread,
|
|
50
|
+
ChatToolsButton,
|
|
51
|
+
ChatVoiceButton,
|
|
52
|
+
Chip,
|
|
53
|
+
Dialog,
|
|
54
|
+
Header,
|
|
55
|
+
Icon,
|
|
56
|
+
ProgressBar,
|
|
57
|
+
SelectionCard,
|
|
58
|
+
Stepper,
|
|
59
|
+
Text,
|
|
60
|
+
TextField,
|
|
61
|
+
} = window.UmkmallUI;
|
|
62
|
+
|
|
63
|
+
/* THE READING COLUMN, AND NO COMPONENT OWNS IT. `AppShell`'s content slot deliberately imposes
|
|
64
|
+
no measure — that would be wrong for every non-prose surface — so the width lives here, once,
|
|
65
|
+
shared by the three regions that have to line up: chrome, transcript and composer. */
|
|
66
|
+
const COLUMN = 'mx-auto w-full max-w-3xl px-ds-md';
|
|
67
|
+
|
|
68
|
+
/* ---------------------------------------------------------------- the journey's shape */
|
|
69
|
+
|
|
70
|
+
/** The four-mission arc. Mission 1 is the swappable tool that differs per journey. */
|
|
71
|
+
const MISSIONS = [
|
|
72
|
+
{ id: 'foto', label: 'Foto Produk' },
|
|
73
|
+
{ id: 'insight', label: 'Insight' },
|
|
74
|
+
{ id: 'strategi', label: 'Strategi' },
|
|
75
|
+
{ id: 'aksi', label: 'Rencana Aksi' },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/* ---------------------------------------------------------------- chrome */
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The journey chrome: progress rail + quota meters, in one `shrink-0` band under the header.
|
|
82
|
+
*
|
|
83
|
+
* IT SITS OUTSIDE THE SCROLLER ON PURPOSE. The rail says where you are and the meters say what
|
|
84
|
+
* you have left to spend — both are answers you want *while* reading the transcript, so
|
|
85
|
+
* scrolling them away would be wrong. Same reason the composer is pinned.
|
|
86
|
+
*/
|
|
87
|
+
const JourneyChrome = ({ current }) => (
|
|
88
|
+
<div className="shrink-0 border-b border-default-primary py-ds-xs">
|
|
89
|
+
<div className={`${COLUMN} flex flex-col gap-ds-2xs`}>
|
|
90
|
+
<Stepper
|
|
91
|
+
orientation="horizontal"
|
|
92
|
+
steps={MISSIONS.map((mission, index) => ({
|
|
93
|
+
label: mission.label,
|
|
94
|
+
state: index < current ? 'done' : index === current ? 'current' : 'default',
|
|
95
|
+
position: index === 0 ? 'first' : index === MISSIONS.length - 1 ? 'last' : 'middle',
|
|
96
|
+
}))}
|
|
97
|
+
/>
|
|
98
|
+
{/* `flex-1` ON EACH BAR, not just a gap on the row. `ProgressBar` justifies its label and
|
|
99
|
+
its percentage to opposite ends, so a bar sized to its content jams the two together —
|
|
100
|
+
"Kuota AI hari ini60%" — which reads as a rendering bug rather than a meter. */}
|
|
101
|
+
<div className="flex gap-ds-md">
|
|
102
|
+
<ProgressBar className="flex-1" label="Kuota AI hari ini" value={3} max={5} size="small" variant="default" />
|
|
103
|
+
<ProgressBar
|
|
104
|
+
className="flex-1"
|
|
105
|
+
label="Misi selesai"
|
|
106
|
+
value={current}
|
|
107
|
+
max={MISSIONS.length}
|
|
108
|
+
size="small"
|
|
109
|
+
variant="success"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
/* ---------------------------------------------------------------- the turn parts */
|
|
117
|
+
|
|
118
|
+
/** The assistant's face. */
|
|
119
|
+
const Face = () => (
|
|
120
|
+
<span
|
|
121
|
+
aria-hidden
|
|
122
|
+
className="flex h-7 w-7 items-center justify-center rounded-full bg-default-secondary text-[10px] text-default-secondary"
|
|
123
|
+
>
|
|
124
|
+
AI
|
|
125
|
+
</span>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
/* Named buttons — an icon-only control with no accessible name announces nothing at all. */
|
|
129
|
+
const ACTIONS = [
|
|
130
|
+
['thumbs-up', 'Jawaban ini membantu'],
|
|
131
|
+
['thumbs-down', 'Jawaban ini tidak membantu'],
|
|
132
|
+
['rotate-cw', 'Buat ulang jawaban'],
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const Actions = () => (
|
|
136
|
+
<div className="flex items-center gap-ds-4xs">
|
|
137
|
+
{ACTIONS.map(([icon, label]) => (
|
|
138
|
+
<Button key={icon} type="text" color="light" size="micro" iconOnly aria-label={label}>
|
|
139
|
+
<Icon name={icon} aria-hidden className="h-4 w-4" />
|
|
140
|
+
</Button>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/** The follow-up chips offered under a settled reply. */
|
|
146
|
+
const Suggestions = ({ items, onPick }) => (
|
|
147
|
+
<div className="flex flex-wrap gap-ds-4xs">
|
|
148
|
+
{items.map((item) => (
|
|
149
|
+
<Chip key={item} size="small" onClick={() => onPick(item)}>
|
|
150
|
+
{item}
|
|
151
|
+
</Chip>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
/* ---------------------------------------------------------------- the transcript */
|
|
157
|
+
|
|
158
|
+
/** The opening turns of Mission 2, as a conversation rather than a card list. */
|
|
159
|
+
const OPENING = [
|
|
160
|
+
{ from: 'assistant', text: 'Halo! Saya sudah lihat data toko Anda minggu ini. Siap mulai Misi 2?' },
|
|
161
|
+
{ from: 'user', text: 'Siap. Apa temuan terbesarnya?' },
|
|
162
|
+
{
|
|
163
|
+
from: 'assistant',
|
|
164
|
+
text: 'Empat temuan. Yang paling besar: harga Anda 18% di bawah rata-rata pasar, jadi margin tipis padahal permintaan sedang naik.',
|
|
165
|
+
suggestions: ['Jelaskan temuan lainnya', 'Kenapa itu penting?', 'Lanjut ke refleksi'],
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
/** One reply, stubbed. Wiring a real model is the prototype author's job — see the header. */
|
|
170
|
+
const stubReply = (question) =>
|
|
171
|
+
`(Contoh jawaban) Anda bertanya "${question}". Ganti fungsi stubReply dengan panggilan model Anda.`;
|
|
172
|
+
|
|
173
|
+
/* =====================================================================
|
|
174
|
+
* REPLACE THIS. Everything above is the journey shell; this is your prototype.
|
|
175
|
+
*
|
|
176
|
+
* EVERY ASSISTANT BUBBLE IS A GHOST. The live surface fills and rounds the USER side only, so a
|
|
177
|
+
* reply draws as prose in the message column with no container around it. `ghost` is exactly
|
|
178
|
+
* that: the fill goes, the inline padding stays, so text still lines up with anything beneath it.
|
|
179
|
+
* ===================================================================== */
|
|
180
|
+
function Konten({ messages, onSuggestion }) {
|
|
181
|
+
return (
|
|
182
|
+
<ChatMessageList label="Percakapan Mini Learning Journey" align="bottom" className={COLUMN}>
|
|
183
|
+
{messages.map((message, index) =>
|
|
184
|
+
message.from === 'user' ? (
|
|
185
|
+
<ChatMessage key={index} sender="user">
|
|
186
|
+
<ChatMessageBubble metadata={<ChatMessageMetadata timestamp="09:41" status="read" />}>
|
|
187
|
+
{message.text}
|
|
188
|
+
</ChatMessageBubble>
|
|
189
|
+
</ChatMessage>
|
|
190
|
+
) : (
|
|
191
|
+
<ChatMessage key={index} sender="assistant" avatar={<Face />} name="Teman AI">
|
|
192
|
+
<ChatMessageBubble
|
|
193
|
+
ghost
|
|
194
|
+
metadata={
|
|
195
|
+
<ChatMessageMetadata
|
|
196
|
+
footer={
|
|
197
|
+
<div className="flex flex-col gap-ds-4xs">
|
|
198
|
+
<Actions />
|
|
199
|
+
{message.suggestions ? (
|
|
200
|
+
<Suggestions items={message.suggestions} onPick={onSuggestion} />
|
|
201
|
+
) : null}
|
|
202
|
+
</div>
|
|
203
|
+
}
|
|
204
|
+
/>
|
|
205
|
+
}
|
|
206
|
+
>
|
|
207
|
+
{message.text}
|
|
208
|
+
</ChatMessageBubble>
|
|
209
|
+
</ChatMessage>
|
|
210
|
+
),
|
|
211
|
+
)}
|
|
212
|
+
</ChatMessageList>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
/* ===================== end of the part you replace ==================== */
|
|
216
|
+
|
|
217
|
+
/* ---------------------------------------------------------------- the composer */
|
|
218
|
+
|
|
219
|
+
/* STATEFUL, BECAUSE THE COMPOSER IS CONTROLLED. A fixed value and a no-op handler render an
|
|
220
|
+
editor nobody can type into — a defect this family's own stories shipped once. */
|
|
221
|
+
function Composer({ onSend }) {
|
|
222
|
+
const [value, setValue] = React.useState('');
|
|
223
|
+
const submit = () => {
|
|
224
|
+
const trimmed = value.trim();
|
|
225
|
+
if (!trimmed) return;
|
|
226
|
+
onSend(trimmed);
|
|
227
|
+
setValue('');
|
|
228
|
+
};
|
|
229
|
+
return (
|
|
230
|
+
<ChatComposer
|
|
231
|
+
value={value}
|
|
232
|
+
onValueChange={setValue}
|
|
233
|
+
editorLabel="Tulis pesan"
|
|
234
|
+
placeholder="Tanya apa saja tentang misi ini..."
|
|
235
|
+
toolActions={
|
|
236
|
+
<>
|
|
237
|
+
<ChatToolsButton label="Alat" />
|
|
238
|
+
<ChatAttachmentButton label="Lampirkan foto produk" />
|
|
239
|
+
</>
|
|
240
|
+
}
|
|
241
|
+
sendActions={
|
|
242
|
+
<>
|
|
243
|
+
<ChatVoiceButton label="Input suara" />
|
|
244
|
+
<ChatSendButton label="Kirim pesan" onClick={submit} />
|
|
245
|
+
</>
|
|
246
|
+
}
|
|
247
|
+
/>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ---------------------------------------------------------------- the reflection overlay */
|
|
252
|
+
|
|
253
|
+
/* One open question and one feasibility choice, after each mission. A `Dialog` rather than a
|
|
254
|
+
route, because the journey is one continuous surface. */
|
|
255
|
+
function Reflection({ open, onOpenChange, mission }) {
|
|
256
|
+
const [answer, setAnswer] = React.useState('');
|
|
257
|
+
const [feasible, setFeasible] = React.useState('bisa');
|
|
258
|
+
return (
|
|
259
|
+
<Dialog
|
|
260
|
+
open={open}
|
|
261
|
+
onOpenChange={onOpenChange}
|
|
262
|
+
showCloseButton
|
|
263
|
+
title={`Refleksi — ${mission.label}`}
|
|
264
|
+
/* `description` is VISUALLY HIDDEN — it wires `aria-describedby`, it does not draw
|
|
265
|
+
subtext. Passing prose here and expecting to see it is how this dialog first shipped
|
|
266
|
+
with an invisible subtitle. Visible subtext is a `Text` in the body, below. */
|
|
267
|
+
description="Dua pertanyaan singkat sebelum melanjutkan ke misi berikutnya."
|
|
268
|
+
>
|
|
269
|
+
{/* THE PANEL PADS ITS TITLE AND NOT ITS CHILDREN. `Dialog` gives the heading `px-6 pt-4`
|
|
270
|
+
and renders the body with none, so the caller owns it — without this the field, the
|
|
271
|
+
cards and the button all run edge to edge against the panel border. */}
|
|
272
|
+
<div className="flex flex-col gap-ds-md px-6 pb-6 pt-ds-sm">
|
|
273
|
+
<Text variant="bodySm" color="secondary">
|
|
274
|
+
Dua pertanyaan singkat sebelum lanjut.
|
|
275
|
+
</Text>
|
|
276
|
+
<TextField
|
|
277
|
+
label="Apa satu hal yang akan Anda ubah minggu ini?"
|
|
278
|
+
value={answer}
|
|
279
|
+
onChange={(event) => setAnswer(event.target.value)}
|
|
280
|
+
placeholder="Tulis jawaban Anda..."
|
|
281
|
+
/>
|
|
282
|
+
<div className="flex flex-col gap-ds-2xs">
|
|
283
|
+
<Text variant="bodySm" weight="semibold">
|
|
284
|
+
Seberapa mungkin Anda melakukannya?
|
|
285
|
+
</Text>
|
|
286
|
+
{/* ONE GROUP, NOT ONE CARD PER OPTION. `SelectionCard` takes the whole `options` array
|
|
287
|
+
and owns selection itself; a per-option `selected` prop does not exist, and an
|
|
288
|
+
invented prop renders with no styling and no error. */}
|
|
289
|
+
<SelectionCard
|
|
290
|
+
options={[
|
|
291
|
+
{ value: 'bisa', label: 'Bisa saya lakukan minggu ini' },
|
|
292
|
+
{ value: 'usaha', label: 'Perlu usaha ekstra' },
|
|
293
|
+
{ value: 'belum', label: 'Belum bisa sekarang' },
|
|
294
|
+
]}
|
|
295
|
+
value={[feasible]}
|
|
296
|
+
onValueChange={(picked) => setFeasible(picked[0] ?? 'bisa')}
|
|
297
|
+
/>
|
|
298
|
+
</div>
|
|
299
|
+
<div className="flex">
|
|
300
|
+
<Button type="primary" color="primary" size="medium" width="auto" onClick={() => onOpenChange(false)}>
|
|
301
|
+
Simpan & lanjut
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</Dialog>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ---------------------------------------------------------------- the room */
|
|
310
|
+
|
|
311
|
+
function App() {
|
|
312
|
+
const [reflecting, setReflecting] = React.useState(false);
|
|
313
|
+
const [current, setCurrent] = React.useState(1);
|
|
314
|
+
const [messages, setMessages] = React.useState(OPENING);
|
|
315
|
+
|
|
316
|
+
/* One turn: the user's message, then a stubbed reply. Replace `stubReply` with your model. */
|
|
317
|
+
const send = (text) => {
|
|
318
|
+
if (text === 'Lanjut ke refleksi') {
|
|
319
|
+
setMessages((previous) => [...previous, { from: 'user', text }]);
|
|
320
|
+
setReflecting(true);
|
|
321
|
+
setCurrent((step) => Math.min(step + 1, MISSIONS.length));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
setMessages((previous) => [
|
|
325
|
+
...previous,
|
|
326
|
+
{ from: 'user', text },
|
|
327
|
+
{ from: 'assistant', text: stubReply(text) },
|
|
328
|
+
]);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<React.Fragment>
|
|
333
|
+
<AppShell
|
|
334
|
+
header={({ scrolled }) => (
|
|
335
|
+
<Header
|
|
336
|
+
type="temanAiChat"
|
|
337
|
+
title="Mini Learning Journey"
|
|
338
|
+
showTextLeft
|
|
339
|
+
scrolled={scrolled}
|
|
340
|
+
/>
|
|
341
|
+
)}
|
|
342
|
+
/* NO SIDEBAR. A prototype is one journey on one screen — an app-navigation rail is
|
|
343
|
+
chrome it does not have, and a drawer needs a trigger and state before it can open. */
|
|
344
|
+
>
|
|
345
|
+
<JourneyChrome current={current} />
|
|
346
|
+
<ChatThread scrollToBottomLabel="Ke pesan terbaru">
|
|
347
|
+
<Konten messages={messages} onSuggestion={send} />
|
|
348
|
+
</ChatThread>
|
|
349
|
+
<div className="shrink-0 py-ds-md">
|
|
350
|
+
<div className={COLUMN}>
|
|
351
|
+
<Composer onSend={send} />
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</AppShell>
|
|
355
|
+
<Reflection
|
|
356
|
+
open={reflecting}
|
|
357
|
+
onOpenChange={setReflecting}
|
|
358
|
+
mission={MISSIONS[Math.min(current, MISSIONS.length - 1)]}
|
|
359
|
+
/>
|
|
360
|
+
</React.Fragment>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
ReactDOMClient.createRoot(document.getElementById('root')).render(<App />);
|
|
365
|
+
```
|