mirror of
https://github.com/lockin-bot/react-telegram.git
synced 2026-05-01 02:42:19 +08:00
feat: add input
This commit is contained in:
128
src/examples/quiz-bot.tsx
Normal file
128
src/examples/quiz-bot.tsx
Normal 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
70
src/input-example.tsx
Normal 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
11
src/jsx.d.ts
vendored
@@ -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;
|
||||||
@@ -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!);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user