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; onClick?: () => void;
children?: ReactNode; children?: ReactNode;
}; };
input: {
onSubmit?: (text: string) => void;
};
} }
} }
} }
@@ -115,6 +119,11 @@ export interface TelegramRowNode {
children: TelegramButtonNode[]; children: TelegramButtonNode[];
} }
export interface TelegramInputNode {
type: 'input';
onSubmit?: (text: string) => void;
}
export interface TelegramRootNode { export interface TelegramRootNode {
type: 'root'; type: 'root';
children: ( children: (
@@ -125,6 +134,7 @@ export interface TelegramRootNode {
| TelegramCodeBlockNode | TelegramCodeBlockNode
| TelegramBlockQuoteNode | TelegramBlockQuoteNode
| TelegramRowNode | TelegramRowNode
| TelegramInputNode
)[]; )[];
} }
@@ -137,4 +147,5 @@ export type TelegramNode =
| TelegramBlockQuoteNode | TelegramBlockQuoteNode
| TelegramButtonNode | TelegramButtonNode
| TelegramRowNode | TelegramRowNode
| TelegramInputNode
| TelegramRootNode; | TelegramRootNode;

View File

@@ -57,6 +57,12 @@ export class MtcuteAdapter {
await this.sendReactMessage(msg.chat.id, app); 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, TelegramBlockQuoteNode as BlockQuoteNode,
TelegramButtonNode as ButtonNode, TelegramButtonNode as ButtonNode,
TelegramRowNode as RowNode, TelegramRowNode as RowNode,
TelegramInputNode as InputNode,
TelegramRootNode as RootNode, TelegramRootNode as RootNode,
TelegramNode as Node TelegramNode as Node
} from './jsx'; } from './jsx';
@@ -23,6 +24,7 @@ export type {
BlockQuoteNode, BlockQuoteNode,
ButtonNode, ButtonNode,
RowNode, RowNode,
InputNode,
RootNode, RootNode,
Node Node
}; };
@@ -30,6 +32,7 @@ export type {
interface Container { interface Container {
root: RootNode; root: RootNode;
buttonHandlers: Map<string, () => void>; buttonHandlers: Map<string, () => void>;
inputCallbacks: Array<(text: string) => void>;
onRenderContainer?: (root: RootNode) => void; onRenderContainer?: (root: RootNode) => void;
} }
@@ -91,6 +94,8 @@ const hostConfig: ReactReconciler.HostConfig<
return { type: 'button', id: '', text: buttonText, onClick: props.onClick }; return { type: 'button', id: '', text: buttonText, onClick: props.onClick };
case 'row': case 'row':
return { type: 'row', children: [] }; return { type: 'row', children: [] };
case 'input':
return { type: 'input', onSubmit: props.onSubmit };
default: default:
return { type: 'formatted', format: 'bold', children: [] }; 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 // Don't log automatically
}, },
@@ -313,6 +326,7 @@ export function createContainer() {
const container: Container = { const container: Container = {
root: { type: 'root', children: [] }, root: { type: 'root', children: [] },
buttonHandlers: new Map(), buttonHandlers: new Map(),
inputCallbacks: [],
}; };
const reconcilerContainer = TelegramReconciler.createContainer( const reconcilerContainer = TelegramReconciler.createContainer(