Alert สวยๆ แบบ SweetAlert ใน Claude Code: dialog กลางจอมีปุ่มกด, ถามยืนยัน, ช่องกรอกข้อมูล, toast และคิว alert

Alert สวยๆ แบบ SweetAlert ใน Claude Code: dialog กลางจอ มีขอบสีตามประเภท (✅ success, ❌ error, ⚠️ warning, ℹ️ info, ❓ question, ⏳ loading) มีปุ่มให้กดจริง ถามยืนยันได้ ให้กรอกข้อมูลได้ และถ้าสั่งหลาย alert ติดกันจะแสดงทีละอันตามคิว นอกจากนี้ ถ้า Bash, Edit หรือ Write ล้มเหลว จะเด้ง toast เตือนให้เอง
| คำสั่ง | ทำอะไร | ||
|---|---|---|---|
/alert success บันทึกแล้ว | แสดง alert (ปิดเองใน 5 วินาที) | ||
| `/alert error หัวข้อ \ | ข้อความ` | แยกหัวข้อกับข้อความด้วย `\ | ` |
/alert demo | ดูตัวอย่างทุกแบบ ต่อกันตามคิว | ||
/alert history | ประวัติและผลการตอบของแต่ละ alert | ||
| `/alert answer <yes\ | no\ | ข้อความ>` | ตอบคำถามที่ค้างอยู่ด้วยคำสั่ง (ใช้ตอนเปิด dialog ไม่ได้) |
/alert clear | ล้างทั้งหมด | ||
/confirm <คำถาม> | ถามยืนยัน มีปุ่ม "ใช่" กับ "ยกเลิก" | ||
/prompt <คำถาม> | ให้กรอกข้อมูลแล้วกด Enter | ||
/toast <type> <ข้อความ> | แจ้งเตือนเล็กๆ มุมจอ |
ใน dialog: กด Tab เพื่อเลื่อนระหว่างปุ่ม, Enter เพื่อกด, y / n เป็นปุ่มลัด และ Esc เพื่อปิด (นับเป็นยกเลิก)
/plugin install sweet-alert-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
$.ui.open({ focus, closeOnEscape, holdToasts }) และปิดอัตโนมัติด้วย $.clock.afteronPress ของ Button, onSubmit ของ Input และดัก ui.close ตอนผู้ใช้กด EscBox ที่มีขอบสี (borderStyle, borderColor)Input ไม่มีบน mobile)Mod วาด HTML/CSS ไม่ได้ จึงไม่มี animation แบบ SweetAlert บนเว็บ สิ่งที่ได้คือ dialog ที่ใช้ element ของ Claude Code เอง ซึ่งใช้ได้ทั้งใน terminal, แอป Desktop, VS Code และ mobile
hooks/register.tsx 327 lines1/**
2 * ============================================================================
3 * MOD 11: Sweet Alert TH - Alert สวยๆ แบบ SweetAlert (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: SweetAlert2 (https://sweetalert2.github.io) ทำใหม่ให้ใช้ใน Claude Code
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /alert แสดง dialog กลางจอ ขอบสีตามประเภท: success, error, warning, info, question, loading
9 * 2. /confirm ถามยืนยัน มีปุ่ม "ใช่" กับ "ยกเลิก" ให้กดจริง
10 * 3. /prompt ให้กรอกข้อมูลในช่อง Input แล้วกด Enter
11 * 4. /toast แจ้งเตือนเล็กๆ มุมจอ
12 * 5. มีคิว: สั่งหลาย alert ติดกัน จะแสดงทีละอันตามลำดับ
13 * 6. ปิดอัตโนมัติได้ + เก็บประวัติว่าแต่ละ alert ถูกตอบว่าอะไร
14 * 7. tool ที่สำคัญล้มเหลว (Bash/Edit/Write) จะเด้ง toast เตือนเอง
15 *
16 * แนวคิดสำคัญ:
17 * - Mod วาด HTML/CSS ไม่ได้ แต่วาด element ของ Claude Code ได้: Box (มีขอบสี), Text, Button, Input
18 * - เปิดแผงแบบ dialog ด้วย $.ui.open({ focus, closeOnEscape, holdToasts })
19 * - ปุ่มมี onPress, ช่องกรอกมี onSubmit -> นี่คือการ "จัดการ event" เมื่อผู้ใช้กด
20 */
21
22import type { EngineInterface, Register, Timer } from 'claude-code'
23
24type AlertType = 'success' | 'error' | 'warning' | 'info' | 'question' | 'loading'
25type AlertKind = 'alert' | 'confirm' | 'prompt'
26type AlertResult = 'confirm' | 'cancel' | 'timeout'
27
28interface SweetAlert {
29 id: number
30 kind: AlertKind
31 type: AlertType
32 title: string
33 message: string
34 time: number
35 autoCloseMs?: number // ปิดอัตโนมัติหลังกี่ ms (ไม่ใส่ = รอให้กด)
36 confirmText: string
37 cancelText?: string // มีค่า = แสดงปุ่มยกเลิก
38 result?: AlertResult
39 value?: string // ค่าที่กรอก (เฉพาะ prompt)
40}
41
42type AlertOptions = Partial<Pick<SweetAlert, 'autoCloseMs' | 'confirmText' | 'cancelText'>>
43
44const PANE = 'sweet-alert-th'
45
46// ==========================================================================
47// บทที่ 1: หน้าตาของแต่ละประเภท (ไอคอน + สี + หัวข้อเริ่มต้น)
48// ==========================================================================
49const STYLE: Record<AlertType, { icon: string; color: string; title: string }> = {
50 success: { icon: '✅', color: '#2ee6a6', title: 'สำเร็จ!' },
51 error: { icon: '❌', color: '#ff7b72', title: 'เกิดข้อผิดพลาด!' },
52 warning: { icon: '⚠️', color: '#ffca28', title: 'คำเตือน!' },
53 info: { icon: 'ℹ️', color: '#4fc3f7', title: 'ข้อมูล' },
54 question: { icon: '❓', color: '#a371f7', title: 'คำถาม' },
55 loading: { icon: '⏳', color: '#8b949e', title: 'กำลังโหลด...' },
56}
57const TYPES = Object.keys(STYLE) as AlertType[]
58const TOAST_TYPES: AlertType[] = ['success', 'error', 'warning', 'info']
59
60// ==========================================================================
61// บทที่ 2: state - alert ที่แสดงอยู่, คิวที่รอ, และประวัติ
62// ==========================================================================
63let current: SweetAlert | undefined
64let queue: SweetAlert[] = []
65let history: SweetAlert[] = []
66let nextId = 0
67let autoCloseTimer: Timer | undefined
68let isPanePlaced = true // false = เปิดแผงไม่ได้ (เช่นรันแบบ claude -p) -> ตอบด้วย /alert answer แทน
69
70async function createAlert($: EngineInterface, kind: AlertKind, type: AlertType, title: string, message: string, options: AlertOptions = {}): Promise<SweetAlert> {
71 const alert: SweetAlert = {
72 id: nextId++,
73 kind,
74 type,
75 title,
76 message,
77 time: await $.clock.now(),
78 confirmText: options.confirmText ?? 'ตกลง',
79 cancelText: options.cancelText,
80 autoCloseMs: options.autoCloseMs,
81 }
82 history = [...history, alert].slice(-50) // เก็บแค่ 50 อันหลังสุด
83 queue.push(alert)
84 if (!current) await showNext($)
85 return alert
86}
87
88// ==========================================================================
89// บทที่ 3: ระบบคิว - หยิบ alert ถัดไปมาแสดง
90// ==========================================================================
91async function showNext($: EngineInterface): Promise<void> {
92 autoCloseTimer?.cancel()
93 autoCloseTimer = undefined
94 current = queue.shift()
95
96 if (!current) {
97 await $.ui.close({ id: PANE })
98 return
99 }
100
101 // dialog: รับคีย์บอร์ด, Esc ปิดได้, toast รอจนกว่า dialog จะปิด
102 const opened = await $.ui.open({ id: PANE, title: '🎨 Sweet Alert', focus: true, closeOnEscape: true, holdToasts: true })
103 isPanePlaced = opened.isPlaced
104 $.ui.invalidate('ui.render')
105
106 const shown = current
107 if (shown.autoCloseMs) {
108 // timer ทำงานแบบ sync จึงห่อ async ด้วย void
109 autoCloseTimer = $.clock.after(shown.autoCloseMs, () => void resolveAlert($, shown, 'timeout'))
110 }
111}
112
113// ==========================================================================
114// บทที่ 4: จัดการ event เมื่อผู้ใช้ตอบ (กดปุ่ม, กรอกข้อมูล, Esc, หมดเวลา)
115// ==========================================================================
116async function resolveAlert($: EngineInterface, alert: SweetAlert, result: AlertResult, value?: string): Promise<void> {
117 if (alert !== current || alert.result) return // ตอบไปแล้ว หรือไม่ใช่อันที่แสดงอยู่
118 alert.result = result
119 alert.value = value
120
121 // ผลลัพธ์ของ confirm / prompt -> แจ้งผลด้วย toast หรือ alert ต่อ
122 if (alert.kind === 'confirm') {
123 $.ui.toast(result === 'confirm' ? `✅ ยืนยันแล้ว: ${alert.message}` : `❌ ยกเลิก: ${alert.message}`)
124 }
125 if (alert.kind === 'prompt' && result === 'confirm') {
126 queue.unshift({ ...alert, id: nextId++, kind: 'alert', type: 'success', title: 'ได้รับข้อมูลแล้ว!', message: `ค่าที่กรอก: ${value || '(ว่าง)'}`, result: undefined, value: undefined, cancelText: undefined, confirmText: 'ตกลง', autoCloseMs: 3000 })
127 }
128
129 await showNext($)
130}
131
132function describe(a: SweetAlert): string {
133 const status = a.result === 'confirm' ? (a.kind === 'prompt' ? `✅ ได้ค่า "${a.value ?? ''}"` : '✅ ยืนยัน') : a.result === 'cancel' ? '❌ ยกเลิก' : a.result === 'timeout' ? '⌛ ปิดเอง' : '⏳ รอ'
134 return `${STYLE[a.type].icon} [${a.id}] ${a.title} - ${a.message.slice(0, 50)} ${status}`
135}
136
137// แยก "หัวข้อ | ข้อความ" (ถ้าไม่มี | ใช้หัวข้อเริ่มต้นของประเภทนั้น)
138function splitTitle(type: AlertType, text: string): [string, string] {
139 if (!text.includes('|')) return [STYLE[type].title, text || STYLE[type].title]
140 const [title = '', ...rest] = text.split('|')
141 return [title.trim(), rest.join('|').trim()]
142}
143
144const HELP = `🎨 Sweet Alert TH (Mod 11)
145========================================
146/alert <type> <ข้อความ>
147/alert <type> <หัวข้อ> | <ข้อความ>
148 type: success, error, warning, info, question, loading
149/alert demo ดูตัวอย่างทุกแบบ (แสดงต่อกันตามคิว)
150/alert history ประวัติและผลการตอบ
151/alert answer <yes|no|ข้อความ> ตอบ alert ที่ค้างอยู่ด้วยคำสั่ง
152/alert clear ล้างทั้งหมด
153
154/confirm <คำถาม> ถามยืนยัน ใช่/ยกเลิก
155/prompt <คำถาม> ให้กรอกข้อมูล
156/toast <type> <ข้อความ> แจ้งเตือนเล็กๆ มุมจอ
157
158ใน dialog: Tab เลื่อนปุ่ม, Enter กด, Esc ปิด (= ยกเลิก)`
159
160function pendingNote(): string {
161 return isPanePlaced ? '' : '\n(เปิด dialog ไม่ได้ในหน้าจอนี้: ตอบด้วย /alert answer yes|no|ข้อความ)'
162}
163
164export const register: Register = on => {
165 on('session.start', async ($, e, next) => {
166 current = undefined
167 queue = []
168 history = []
169 await $.command.register({ name: 'alert', description: 'แสดง Sweet Alert (/alert help)', argumentHint: '<type> [หัวข้อ |] ข้อความ' })
170 await $.command.register({ name: 'confirm', description: 'ถามยืนยัน ใช่/ยกเลิก ใน dialog', argumentHint: '<คำถาม>' })
171 await $.command.register({ name: 'prompt', description: 'ให้กรอกข้อมูลใน dialog', argumentHint: '<คำถาม>' })
172 await $.command.register({ name: 'toast', description: 'แจ้งเตือนเล็กๆ มุมจอ', argumentHint: '<type> <ข้อความ>' })
173 return next(e)
174 })
175
176 // tool สำคัญล้มเหลว -> toast เตือน (ไม่สแปมกับ tool อ่านไฟล์)
177 on('tool.call', async ($, e, next) => {
178 const ran = await next(e)
179 if (ran.isError && (e.tool === 'Bash' || e.tool === 'Edit' || e.tool === 'Write')) {
180 $.ui.toast(`❌ ${e.tool} ล้มเหลว: ${(ran.text ?? '').split('\n')[0]?.slice(0, 80) ?? ''}`)
181 }
182 return ran
183 })
184
185 // ผู้ใช้กด Esc หรือปุ่มปิดของแผง = ยกเลิก alert ที่แสดงอยู่ และทิ้งคิวที่เหลือ
186 on('ui.close', async ($, e, next) => {
187 if (e.id === PANE && e.origin.kind === 'person' && current) {
188 const closing = current
189 queue = []
190 closing.result = 'cancel'
191 current = undefined
192 autoCloseTimer?.cancel()
193 if (closing.kind === 'confirm') $.ui.toast(`❌ ยกเลิก: ${closing.message}`)
194 }
195 return next(e)
196 })
197
198 // ==========================================================================
199 // บทที่ 5: วาด dialog - Box ขอบสี + ไอคอน + หัวข้อ + ข้อความ + ปุ่ม/ช่องกรอก
200 // ==========================================================================
201 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
202 const elements = $.ui.resolve(e)
203 const { Box, Text, Button } = elements
204 const alert = current
205 if (!alert) return <Text dimColor>ไม่มี alert</Text>
206
207 const style = STYLE[alert.type]
208 // mobile ไม่มี Input -> เช็คก่อนใช้
209 const Input = 'Input' in elements ? elements.Input : undefined
210 const isPrompt = alert.kind === 'prompt'
211
212 return (
213 <Box flexDirection="column" alignItems="center" borderStyle="round" borderColor={style.color} paddingX={2} paddingY={1}>
214 <Text>{style.icon}</Text>
215 <Text bold color={style.color}>
216 {alert.title}
217 </Text>
218 <Text>{alert.message}</Text>
219 {isPrompt && Input && (
220 <Input key="value" placeholder="พิมพ์แล้วกด Enter" autoFocus onSubmit={value => void resolveAlert($, alert, 'confirm', value)} />
221 )}
222 {isPrompt && !Input && <Text dimColor>หน้าจอนี้กรอกข้อมูลไม่ได้ ใช้ /alert answer ข้อความ</Text>}
223 <Box gap={2}>
224 {alert.cancelText && <Button key="cancel" label={alert.cancelText} hotkey="n" onPress={() => void resolveAlert($, alert, 'cancel')} />}
225 {!isPrompt && <Button key="confirm" label={alert.confirmText} hotkey="y" variant="primary" autoFocus onPress={() => void resolveAlert($, alert, 'confirm')} />}
226 </Box>
227 {alert.autoCloseMs !== undefined && <Text dimColor>ปิดอัตโนมัติใน {alert.autoCloseMs / 1000} วินาที</Text>}
228 {queue.length > 0 && <Text dimColor>รออยู่ในคิวอีก {queue.length} อัน</Text>}
229 </Box>
230 )
231 })
232
233 // ==========================================================================
234 // บทที่ 6: คำสั่ง /alert
235 // ==========================================================================
236 on('command.run', { command: 'alert' }, async ($, e) => {
237 const args = e.args.trim()
238 const [sub = 'help', ...restWords] = args.split(/\s+/)
239 const rest = restWords.join(' ')
240
241 if (sub === 'help') return { text: HELP }
242
243 if (sub === 'history') {
244 return { text: history.length ? [`🎨 ประวัติ Sweet Alert (${history.length})`, ...[...history].reverse().slice(0, 15).map(describe)].join('\n') : 'ยังไม่มี alert ลอง /alert demo' }
245 }
246
247 if (sub === 'clear') {
248 queue = []
249 history = []
250 if (current) {
251 current = undefined
252 await showNext($) // คิวว่าง -> ปิดแผง
253 }
254 return { text: '🗑️ ล้าง alert ทั้งหมดแล้ว' }
255 }
256
257 if (sub === 'answer') {
258 // alert ธรรมดาที่ขวางหน้าคิวอยู่ ให้กดตกลงไปก่อน จะได้ตอบคำถามที่รออยู่จริงๆ
259 while (current?.kind === 'alert') await resolveAlert($, current, 'confirm')
260 if (!current) return { text: 'ไม่มีคำถามที่รอคำตอบ' }
261 const alert = current
262 if (alert.kind === 'prompt') await resolveAlert($, alert, rest ? 'confirm' : 'cancel', rest)
263 else await resolveAlert($, alert, /^(no|n|ไม่)$/i.test(rest) ? 'cancel' : 'confirm')
264 return { text: describe(alert) }
265 }
266
267 if (sub === 'demo') {
268 await createAlert($, 'alert', 'success', 'สำเร็จ!', 'ไฟล์ถูกบันทึกเรียบร้อยแล้ว', { autoCloseMs: 2500 })
269 await createAlert($, 'alert', 'error', 'เกิดข้อผิดพลาด!', 'ไม่สามารถเชื่อมต่อ database ได้', { autoCloseMs: 2500 })
270 await createAlert($, 'alert', 'warning', 'คำเตือน!', 'ไฟล์นี้มีขนาดใหญ่กว่า 10MB', { autoCloseMs: 2500 })
271 await createAlert($, 'alert', 'info', 'ข้อมูล', 'มี 5 ไฟล์ที่ถูกแก้ไขใน session นี้', { autoCloseMs: 2500 })
272 await createAlert($, 'confirm', 'question', 'ยืนยันไหม?', 'คุณชอบ Mod นี้ไหม?', { confirmText: 'ชอบ!', cancelText: 'เฉยๆ' })
273 return { text: `🎨 เริ่ม demo 5 แบบ (แสดงต่อกันตามคิว)${pendingNote()}` }
274 }
275
276 if (TYPES.includes(sub as AlertType)) {
277 const type = sub as AlertType
278 const [title, message] = splitTitle(type, rest)
279 const kind: AlertKind = type === 'question' ? 'confirm' : 'alert'
280 await createAlert($, kind, type, title, message, {
281 autoCloseMs: type === 'loading' || type === 'question' ? undefined : 5000,
282 cancelText: type === 'question' ? 'ยกเลิก' : undefined,
283 })
284 return { text: `${STYLE[type].icon} ${title}: ${message}${pendingNote()}` }
285 }
286
287 return { text: `❌ ไม่รู้จัก "${sub}" ใช้ได้: ${TYPES.join(', ')}\n/alert help เพื่อดูวิธีใช้` }
288 })
289
290 on('command.run', { command: 'confirm' }, async ($, e) => {
291 const message = e.args.trim() || 'คุณแน่ใจไหม?'
292 await createAlert($, 'confirm', 'question', 'ยืนยัน?', message, { confirmText: 'ใช่, ทำเลย', cancelText: 'ยกเลิก' })
293 return { text: `❓ ${message}${pendingNote()}` }
294 })
295
296 on('command.run', { command: 'prompt' }, async ($, e) => {
297 const message = e.args.trim() || 'กรุณากรอกข้อมูล:'
298 await createAlert($, 'prompt', 'question', 'กรอกข้อมูล', message, { confirmText: 'ส่ง', cancelText: 'ยกเลิก' })
299 return { text: `📝 ${message}${pendingNote()}` }
300 })
301
302 // ==========================================================================
303 // บทที่ 7: /toast - ใช้ $.ui.toast ของ Claude Code ตรงๆ
304 // ==========================================================================
305 on('command.run', { command: 'toast' }, async ($, e) => {
306 const [first = '', ...restWords] = e.args.trim().split(/\s+/)
307 const hasType = TOAST_TYPES.includes(first as AlertType)
308 const type: AlertType = hasType ? (first as AlertType) : 'info'
309 const message = hasType ? restWords.join(' ') : e.args.trim()
310 if (!message) return { text: 'วิธีใช้: /toast success บันทึกแล้ว! (type: success, error, warning, info)' }
311
312 $.ui.toast(`${STYLE[type].icon} ${message}`, { timeoutMs: 3000 })
313 return { text: `🍞 ${STYLE[type].icon} ${message}` }
314 })
315}
316
317/**
318 * ========================================================================
319 * การบ้าน / ต่อยอด
320 * ========================================================================
321 * 1. ทำ Progress Alert: เก็บเปอร์เซ็นต์ใน alert แล้วอัปเดตด้วย $.ui.invalidate ทุกครั้งที่เปลี่ยน
322 * 2. เพิ่มเสียง: $.audio.play({ asset }) ด้วยไฟล์เสียงในโฟลเดอร์ของ Mod
323 * 3. ทำเป็นด่านยืนยันก่อนทำอะไรอันตราย: ดัก tool.call ของ Bash ที่มี "git push"
324 * แล้วใช้ $.ui.ask("จะ push จริงไหม?", ["ใช่", "ไม่"]) ก่อนเรียก next(e)
325 * 4. ส่ง alert สำคัญเป็น notification ของระบบด้วย $.ui.notify(...)
326 */
327