feat: add input

This commit is contained in:
Kyle Fang
2025-07-01 08:44:24 +08:00
parent 5ab2a8b5aa
commit 3ea8517101
5 changed files with 229 additions and 0 deletions

128
src/examples/quiz-bot.tsx Normal file
View File

@@ -0,0 +1,128 @@
/// <reference path="../jsx.d.ts" />
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<string | null>(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 (
<>
<b>🎉 Quiz Complete!</b>
{'\n\n'}
Your final score: <b>{score}/{questions.length}</b>
{'\n\n'}
{score === questions.length ?
"Perfect score! Well done! 🌟" :
score >= questions.length / 2 ?
"Good job! 👍" :
"Better luck next time! 📚"
}
{'\n\n'}
<row>
<button onClick={restart}>Play Again</button>
</row>
</>
);
}
const currentQ = questions[currentQuestion];
return (
<>
<b>Quiz Bot 🤖</b>
{'\n'}
Question {currentQuestion + 1} of {questions.length}
{'\n'}
Score: {score}/{currentQuestion}
{'\n\n'}
<b>{currentQ?.question}</b>
{'\n\n'}
{lastAnswer !== null && (
<>
Your answer: <code>{lastAnswer}</code>
{'\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 && (
<>
<i>Reply to this message with your answer!</i>
{'\n'}
</>
)}
{/* Input handler for answers */}
{waitingForAnswer && <input onSubmit={handleAnswer} />}
</>
);
};
// 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', () => <QuizBot />);
// Start the bot
await adapter.start(process.env.BOT_TOKEN!);
console.log('Quiz bot is running! Send /quiz to start.');
}
main().catch(console.error);

70
src/input-example.tsx Normal file
View File

@@ -0,0 +1,70 @@
/// <reference path="./jsx.d.ts" />
import React, { useState } from 'react';
import { createContainer } from './reconciler';
const InputExample: React.FC = () => {
const [messages, setMessages] = useState<string[]>([]);
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 (
<>
<b>Input Example</b>
{'\n\n'}
{messages.length === 0 ? (
<>
<i>Reply to this message to send text!</i>
{'\n\n'}
The bot will echo whatever you type.
</>
) : (
<>
<b>Message History:</b>
{'\n'}
{messages.map((msg, idx) => (
<React.Fragment key={idx}>
{msg}
{'\n'}
</React.Fragment>
))}
</>
)}
{'\n\n'}
{/* Input handler - will process any reply to this message */}
{waitingForInput && <input onSubmit={handleInput} />}
{/* Button to clear history */}
{messages.length > 0 && (
<row>
<button onClick={clearMessages}>Clear History</button>
<button onClick={() => setWaitingForInput(!waitingForInput)}>
{waitingForInput ? 'Stop Input' : 'Start Input'}
</button>
</row>
)}
</>
);
};
// Create container and render
const { render, container } = createContainer();
render(<InputExample />);
// 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);

11
src/jsx.d.ts vendored
View File

@@ -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;

View File

@@ -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!);
});
});
}
});

View File

@@ -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<string, () => 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(