From 3ea85171017e64a4c9868c96f1deabc8b9c7a50c Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Tue, 1 Jul 2025 08:44:24 +0800 Subject: [PATCH] feat: add input --- src/examples/quiz-bot.tsx | 128 ++++++++++++++++++++++++++++++++++++++ src/input-example.tsx | 70 +++++++++++++++++++++ src/jsx.d.ts | 11 ++++ src/mtcute-adapter.ts | 6 ++ src/reconciler.ts | 14 +++++ 5 files changed, 229 insertions(+) create mode 100644 src/examples/quiz-bot.tsx create mode 100644 src/input-example.tsx diff --git a/src/examples/quiz-bot.tsx b/src/examples/quiz-bot.tsx new file mode 100644 index 0000000..84c16cb --- /dev/null +++ b/src/examples/quiz-bot.tsx @@ -0,0 +1,128 @@ +/// +import React, { useState } from 'react'; +import { MtcuteAdapter } from '../mtcute-adapter'; + +// Quiz questions +const questions = [ + { question: "What is 2 + 2?", answer: "4" }, + { question: "What is the capital of France?", answer: "paris" }, + { question: "What color is the sky?", answer: "blue" } +]; + +const QuizBot: React.FC = () => { + const [currentQuestion, setCurrentQuestion] = useState(0); + const [score, setScore] = useState(0); + const [waitingForAnswer, setWaitingForAnswer] = useState(true); + const [lastAnswer, setLastAnswer] = useState(null); + const [gameOver, setGameOver] = useState(false); + + const handleAnswer = (text: string) => { + if (!waitingForAnswer || gameOver) return; + + setWaitingForAnswer(false); + setLastAnswer(text); + + const isCorrect = text.toLowerCase().trim() === questions[currentQuestion]?.answer; + if (isCorrect) { + setScore(score + 1); + } + + // Move to next question after a short delay + setTimeout(() => { + if (currentQuestion < questions.length - 1) { + setCurrentQuestion(currentQuestion + 1); + setWaitingForAnswer(true); + setLastAnswer(null); + } else { + setGameOver(true); + } + }, 100); + }; + + const restart = () => { + setCurrentQuestion(0); + setScore(0); + setWaitingForAnswer(true); + setLastAnswer(null); + setGameOver(false); + }; + + if (gameOver) { + return ( + <> + 🎉 Quiz Complete! + {'\n\n'} + Your final score: {score}/{questions.length} + {'\n\n'} + {score === questions.length ? + "Perfect score! Well done! 🌟" : + score >= questions.length / 2 ? + "Good job! 👍" : + "Better luck next time! 📚" + } + {'\n\n'} + + + + + ); + } + + const currentQ = questions[currentQuestion]; + + return ( + <> + Quiz Bot 🤖 + {'\n'} + Question {currentQuestion + 1} of {questions.length} + {'\n'} + Score: {score}/{currentQuestion} + {'\n\n'} + + {currentQ?.question} + {'\n\n'} + + {lastAnswer !== null && ( + <> + Your answer: {lastAnswer} + {'\n'} + {lastAnswer.toLowerCase().trim() === currentQ?.answer ? + "✅ Correct!" : + `❌ Wrong! The answer was: ${currentQ?.answer}` + } + {'\n\n'} + {currentQuestion < questions.length - 1 && "Next question coming up..."} + {'\n'} + + )} + + {waitingForAnswer && !lastAnswer && ( + <> + Reply to this message with your answer! + {'\n'} + + )} + + {/* Input handler for answers */} + {waitingForAnswer && } + + ); +}; + +// Set up the bot +async function main() { + const adapter = new MtcuteAdapter({ + apiId: parseInt(process.env.API_ID!), + apiHash: process.env.API_HASH!, + botToken: process.env.BOT_TOKEN! + }); + + // Register the quiz command + adapter.onCommand('quiz', () => ); + + // Start the bot + await adapter.start(process.env.BOT_TOKEN!); + console.log('Quiz bot is running! Send /quiz to start.'); +} + +main().catch(console.error); \ No newline at end of file diff --git a/src/input-example.tsx b/src/input-example.tsx new file mode 100644 index 0000000..202205f --- /dev/null +++ b/src/input-example.tsx @@ -0,0 +1,70 @@ +/// +import React, { useState } from 'react'; +import { createContainer } from './reconciler'; + +const InputExample: React.FC = () => { + const [messages, setMessages] = useState([]); + const [waitingForInput, setWaitingForInput] = useState(true); + + const handleInput = (text: string) => { + setMessages(prev => [...prev, `You said: ${text}`]); + // Keep waiting for more input + }; + + const clearMessages = () => { + setMessages([]); + setWaitingForInput(true); + }; + + return ( + <> + Input Example + {'\n\n'} + + {messages.length === 0 ? ( + <> + Reply to this message to send text! + {'\n\n'} + The bot will echo whatever you type. + + ) : ( + <> + Message History: + {'\n'} + {messages.map((msg, idx) => ( + + {msg} + {'\n'} + + ))} + + )} + + {'\n\n'} + + {/* Input handler - will process any reply to this message */} + {waitingForInput && } + + {/* Button to clear history */} + {messages.length > 0 && ( + + + + + )} + + ); +}; + +// Create container and render +const { render, container } = createContainer(); +render(); + +// Log the output for debugging +setTimeout(() => { + console.log('Input example output:'); + console.log(JSON.stringify(container.root, null, 2)); + console.log(`Input callbacks registered: ${container.inputCallbacks.length}`); +}, 0); \ No newline at end of file diff --git a/src/jsx.d.ts b/src/jsx.d.ts index 0ab69fe..2fc386b 100644 --- a/src/jsx.d.ts +++ b/src/jsx.d.ts @@ -55,6 +55,10 @@ declare module 'react' { onClick?: () => void; children?: ReactNode; }; + + input: { + onSubmit?: (text: string) => void; + }; } } } @@ -115,6 +119,11 @@ export interface TelegramRowNode { children: TelegramButtonNode[]; } +export interface TelegramInputNode { + type: 'input'; + onSubmit?: (text: string) => void; +} + export interface TelegramRootNode { type: 'root'; children: ( @@ -125,6 +134,7 @@ export interface TelegramRootNode { | TelegramCodeBlockNode | TelegramBlockQuoteNode | TelegramRowNode + | TelegramInputNode )[]; } @@ -137,4 +147,5 @@ export type TelegramNode = | TelegramBlockQuoteNode | TelegramButtonNode | TelegramRowNode + | TelegramInputNode | TelegramRootNode; \ No newline at end of file diff --git a/src/mtcute-adapter.ts b/src/mtcute-adapter.ts index 827357a..1d47d06 100644 --- a/src/mtcute-adapter.ts +++ b/src/mtcute-adapter.ts @@ -57,6 +57,12 @@ export class MtcuteAdapter { await this.sendReactMessage(msg.chat.id, app); } } + } else if (msg.text) { + this.activeContainers.forEach(container => { + container.container.inputCallbacks.forEach(callback => { + callback(msg.text!); + }); + }); } }); diff --git a/src/reconciler.ts b/src/reconciler.ts index 26bfac8..ce08dd8 100644 --- a/src/reconciler.ts +++ b/src/reconciler.ts @@ -9,6 +9,7 @@ import type { TelegramBlockQuoteNode as BlockQuoteNode, TelegramButtonNode as ButtonNode, TelegramRowNode as RowNode, + TelegramInputNode as InputNode, TelegramRootNode as RootNode, TelegramNode as Node } from './jsx'; @@ -23,6 +24,7 @@ export type { BlockQuoteNode, ButtonNode, RowNode, + InputNode, RootNode, Node }; @@ -30,6 +32,7 @@ export type { interface Container { root: RootNode; buttonHandlers: Map void>; + inputCallbacks: Array<(text: string) => void>; onRenderContainer?: (root: RootNode) => void; } @@ -91,6 +94,8 @@ const hostConfig: ReactReconciler.HostConfig< return { type: 'button', id: '', text: buttonText, onClick: props.onClick }; case 'row': return { type: 'row', children: [] }; + case 'input': + return { type: 'input', onSubmit: props.onSubmit }; default: return { type: 'formatted', format: 'bold', children: [] }; } @@ -152,6 +157,14 @@ const hostConfig: ReactReconciler.HostConfig< } }); + // Collect input callbacks + container.inputCallbacks = []; + container.root.children.forEach((child: any) => { + if (child.type === 'input' && child.onSubmit) { + container.inputCallbacks.push(child.onSubmit); + } + }); + // Don't log automatically }, @@ -313,6 +326,7 @@ export function createContainer() { const container: Container = { root: { type: 'root', children: [] }, buttonHandlers: new Map(), + inputCallbacks: [], }; const reconcilerContainer = TelegramReconciler.createContainer(