Skip to content

Commit 3ea8517

Browse files
committed
feat: add input
1 parent 5ab2a8b commit 3ea8517

5 files changed

Lines changed: 229 additions & 0 deletions

File tree

src/examples/quiz-bot.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/// <reference path="../jsx.d.ts" />
2+
import React, { useState } from 'react';
3+
import { MtcuteAdapter } from '../mtcute-adapter';
4+
5+
// Quiz questions
6+
const questions = [
7+
{ question: "What is 2 + 2?", answer: "4" },
8+
{ question: "What is the capital of France?", answer: "paris" },
9+
{ question: "What color is the sky?", answer: "blue" }
10+
];
11+
12+
const QuizBot: React.FC = () => {
13+
const [currentQuestion, setCurrentQuestion] = useState(0);
14+
const [score, setScore] = useState(0);
15+
const [waitingForAnswer, setWaitingForAnswer] = useState(true);
16+
const [lastAnswer, setLastAnswer] = useState<string | null>(null);
17+
const [gameOver, setGameOver] = useState(false);
18+
19+
const handleAnswer = (text: string) => {
20+
if (!waitingForAnswer || gameOver) return;
21+
22+
setWaitingForAnswer(false);
23+
setLastAnswer(text);
24+
25+
const isCorrect = text.toLowerCase().trim() === questions[currentQuestion]?.answer;
26+
if (isCorrect) {
27+
setScore(score + 1);
28+
}
29+
30+
// Move to next question after a short delay
31+
setTimeout(() => {
32+
if (currentQuestion < questions.length - 1) {
33+
setCurrentQuestion(currentQuestion + 1);
34+
setWaitingForAnswer(true);
35+
setLastAnswer(null);
36+
} else {
37+
setGameOver(true);
38+
}
39+
}, 100);
40+
};
41+
42+
const restart = () => {
43+
setCurrentQuestion(0);
44+
setScore(0);
45+
setWaitingForAnswer(true);
46+
setLastAnswer(null);
47+
setGameOver(false);
48+
};
49+
50+
if (gameOver) {
51+
return (
52+
<>
53+
<b>🎉 Quiz Complete!</b>
54+
{'\n\n'}
55+
Your final score: <b>{score}/{questions.length}</b>
56+
{'\n\n'}
57+
{score === questions.length ?
58+
"Perfect score! Well done! 🌟" :
59+
score >= questions.length / 2 ?
60+
"Good job! 👍" :
61+
"Better luck next time! 📚"
62+
}
63+
{'\n\n'}
64+
<row>
65+
<button onClick={restart}>Play Again</button>
66+
</row>
67+
</>
68+
);
69+
}
70+
71+
const currentQ = questions[currentQuestion];
72+
73+
return (
74+
<>
75+
<b>Quiz Bot 🤖</b>
76+
{'\n'}
77+
Question {currentQuestion + 1} of {questions.length}
78+
{'\n'}
79+
Score: {score}/{currentQuestion}
80+
{'\n\n'}
81+
82+
<b>{currentQ?.question}</b>
83+
{'\n\n'}
84+
85+
{lastAnswer !== null && (
86+
<>
87+
Your answer: <code>{lastAnswer}</code>
88+
{'\n'}
89+
{lastAnswer.toLowerCase().trim() === currentQ?.answer ?
90+
"✅ Correct!" :
91+
`❌ Wrong! The answer was: ${currentQ?.answer}`
92+
}
93+
{'\n\n'}
94+
{currentQuestion < questions.length - 1 && "Next question coming up..."}
95+
{'\n'}
96+
</>
97+
)}
98+
99+
{waitingForAnswer && !lastAnswer && (
100+
<>
101+
<i>Reply to this message with your answer!</i>
102+
{'\n'}
103+
</>
104+
)}
105+
106+
{/* Input handler for answers */}
107+
{waitingForAnswer && <input onSubmit={handleAnswer} />}
108+
</>
109+
);
110+
};
111+
112+
// Set up the bot
113+
async function main() {
114+
const adapter = new MtcuteAdapter({
115+
apiId: parseInt(process.env.API_ID!),
116+
apiHash: process.env.API_HASH!,
117+
botToken: process.env.BOT_TOKEN!
118+
});
119+
120+
// Register the quiz command
121+
adapter.onCommand('quiz', () => <QuizBot />);
122+
123+
// Start the bot
124+
await adapter.start(process.env.BOT_TOKEN!);
125+
console.log('Quiz bot is running! Send /quiz to start.');
126+
}
127+
128+
main().catch(console.error);

src/input-example.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/// <reference path="./jsx.d.ts" />
2+
import React, { useState } from 'react';
3+
import { createContainer } from './reconciler';
4+
5+
const InputExample: React.FC = () => {
6+
const [messages, setMessages] = useState<string[]>([]);
7+
const [waitingForInput, setWaitingForInput] = useState(true);
8+
9+
const handleInput = (text: string) => {
10+
setMessages(prev => [...prev, `You said: ${text}`]);
11+
// Keep waiting for more input
12+
};
13+
14+
const clearMessages = () => {
15+
setMessages([]);
16+
setWaitingForInput(true);
17+
};
18+
19+
return (
20+
<>
21+
<b>Input Example</b>
22+
{'\n\n'}
23+
24+
{messages.length === 0 ? (
25+
<>
26+
<i>Reply to this message to send text!</i>
27+
{'\n\n'}
28+
The bot will echo whatever you type.
29+
</>
30+
) : (
31+
<>
32+
<b>Message History:</b>
33+
{'\n'}
34+
{messages.map((msg, idx) => (
35+
<React.Fragment key={idx}>
36+
{msg}
37+
{'\n'}
38+
</React.Fragment>
39+
))}
40+
</>
41+
)}
42+
43+
{'\n\n'}
44+
45+
{/* Input handler - will process any reply to this message */}
46+
{waitingForInput && <input onSubmit={handleInput} />}
47+
48+
{/* Button to clear history */}
49+
{messages.length > 0 && (
50+
<row>
51+
<button onClick={clearMessages}>Clear History</button>
52+
<button onClick={() => setWaitingForInput(!waitingForInput)}>
53+
{waitingForInput ? 'Stop Input' : 'Start Input'}
54+
</button>
55+
</row>
56+
)}
57+
</>
58+
);
59+
};
60+
61+
// Create container and render
62+
const { render, container } = createContainer();
63+
render(<InputExample />);
64+
65+
// Log the output for debugging
66+
setTimeout(() => {
67+
console.log('Input example output:');
68+
console.log(JSON.stringify(container.root, null, 2));
69+
console.log(`Input callbacks registered: ${container.inputCallbacks.length}`);
70+
}, 0);

src/jsx.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ declare module 'react' {
5555
onClick?: () => void;
5656
children?: ReactNode;
5757
};
58+
59+
input: {
60+
onSubmit?: (text: string) => void;
61+
};
5862
}
5963
}
6064
}
@@ -115,6 +119,11 @@ export interface TelegramRowNode {
115119
children: TelegramButtonNode[];
116120
}
117121

122+
export interface TelegramInputNode {
123+
type: 'input';
124+
onSubmit?: (text: string) => void;
125+
}
126+
118127
export interface TelegramRootNode {
119128
type: 'root';
120129
children: (
@@ -125,6 +134,7 @@ export interface TelegramRootNode {
125134
| TelegramCodeBlockNode
126135
| TelegramBlockQuoteNode
127136
| TelegramRowNode
137+
| TelegramInputNode
128138
)[];
129139
}
130140

@@ -137,4 +147,5 @@ export type TelegramNode =
137147
| TelegramBlockQuoteNode
138148
| TelegramButtonNode
139149
| TelegramRowNode
150+
| TelegramInputNode
140151
| TelegramRootNode;

src/mtcute-adapter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export class MtcuteAdapter {
5757
await this.sendReactMessage(msg.chat.id, app);
5858
}
5959
}
60+
} else if (msg.text) {
61+
this.activeContainers.forEach(container => {
62+
container.container.inputCallbacks.forEach(callback => {
63+
callback(msg.text!);
64+
});
65+
});
6066
}
6167
});
6268

src/reconciler.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
TelegramBlockQuoteNode as BlockQuoteNode,
1010
TelegramButtonNode as ButtonNode,
1111
TelegramRowNode as RowNode,
12+
TelegramInputNode as InputNode,
1213
TelegramRootNode as RootNode,
1314
TelegramNode as Node
1415
} from './jsx';
@@ -23,13 +24,15 @@ export type {
2324
BlockQuoteNode,
2425
ButtonNode,
2526
RowNode,
27+
InputNode,
2628
RootNode,
2729
Node
2830
};
2931

3032
interface Container {
3133
root: RootNode;
3234
buttonHandlers: Map<string, () => void>;
35+
inputCallbacks: Array<(text: string) => void>;
3336
onRenderContainer?: (root: RootNode) => void;
3437
}
3538

@@ -91,6 +94,8 @@ const hostConfig: ReactReconciler.HostConfig<
9194
return { type: 'button', id: '', text: buttonText, onClick: props.onClick };
9295
case 'row':
9396
return { type: 'row', children: [] };
97+
case 'input':
98+
return { type: 'input', onSubmit: props.onSubmit };
9499
default:
95100
return { type: 'formatted', format: 'bold', children: [] };
96101
}
@@ -152,6 +157,14 @@ const hostConfig: ReactReconciler.HostConfig<
152157
}
153158
});
154159

160+
// Collect input callbacks
161+
container.inputCallbacks = [];
162+
container.root.children.forEach((child: any) => {
163+
if (child.type === 'input' && child.onSubmit) {
164+
container.inputCallbacks.push(child.onSubmit);
165+
}
166+
});
167+
155168
// Don't log automatically
156169
},
157170

@@ -313,6 +326,7 @@ export function createContainer() {
313326
const container: Container = {
314327
root: { type: 'root', children: [] },
315328
buttonHandlers: new Map(),
329+
inputCallbacks: [],
316330
};
317331

318332
const reconcilerContainer = TelegramReconciler.createContainer(

0 commit comments

Comments
 (0)