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(