
【学習支援】AIの要約、読みっぱなしで終わらせない。NotebookLM/Gemini連携で知識をクイズ化する実践法 全コード公開
私が開発した2つの学習支援アプリケーションについて、その開発思想と、特にAIを活用した知的生産における新たな活用法をご提案させていただきます。
NotebookLMのソースグラウンディング機能や、GeminiによるDeep Researchによって、私たちはかつてない速度で膨大な情報を収集・要約できるようになりました。
しかし、そのAIが生成した高密度なテキストを、一度読んだだけで終わらせてはいないでしょうか。インプットの効率が劇的に向上した一方で、その知識をいかにして自身の血肉として定着させるかという、新たな課題が生まれていると私は考えます。
この「AIによるインプットの高速化」と「知識定着のボトルネック」というギャップを埋めるため、私は2つのツールを開発しました。AIの生成物をインタラクティブな学習体験に転換し、皆様の知的生産プロセスを完成させるためのソリューションです。
1. AIの生成物を「問い」に変換する — 『クイズ台本ジェネレーター』
知的生産の最初のステップは、AIが生成したテキストを、検証可能な「問い」の形に構造化することです。そのプロセスを自動化するのが、この『クイズ台本ジェネレーター』です。
【NotebookLM/Geminiとの連携を前提とした設計】
本ツールは、特に以下のようなAI生成テキストをインプットソースとすることを想定して設計されています。
NotebookLMで複数のPDF資料から生成した、要約ノート
GeminiのDeep Research機能で出力された、特定のテーマに関する詳細レポート
Webサイトやドキュメントをソースとした、AIによる長文の要約
これらの高密度なテキストを本ツールに入力するだけで、AI(Gemini)がその内容を解釈し、本質的な理解を問う5択クイズを自動で生成します。
これにより、ユーザーは「AIの生成物が本当に重要な点を押さえているか」を客観的に検証しつつ、次の学習ステップへの準備を効率的に進めることができます。
【ご使用方法】
APIキーの設定: 初回ご利用時に、ご自身の「Gemini APIキー」をご設定ください。
AI生成テキストの入力: NotebookLMやGeminiから出力したテキストをコピーし、本ツールのテキストエリアに貼り付けます。
クイズ台本の生成: 問題数を指定し、「クイズを生成」ボタンをクリックします。
ファイルの保存: 生成されたクイズ台本(.txt形式)をPCにダウンロードします。
2. 「問い」をインタラクティブな「体験」に — 『QuizMaster』
AIが生成した静的なテキストを、記憶に深く刻み込む動的な学習体験へと転換させる。そのためのプラットフォームが『Quiz Master』です。
【知識定着のための設計】
教育心理学では、能動的なアウトプットが記憶の定着を促す「テスト効果」の有効性が示されています。『Quiz Master』は、この効果を最大化するために設計されました。
カスタムクイズ機能: 『クイズ台本ジェネレーター』で作成したファイルを読み込むことで、NotebookLM/Geminiの生成テキストを基にしたオリジナルクイズを即座に開始できます。
学習継続を支援するUI/UX: 学習者のモチベーションを削がないよう、中断・再開が容易なレジューム機能や、直感的に操作できるインターフェースにこだわりました。
AIが生成した無機質なテキストが、洗練された学習体験へと変わることで、反復学習の心理的負担を軽減します。
【ご使用方法】
アプリケーションの起動: 保存したHTMLファイルをブラウザで開きます。
クイズファイルの読込: 「ファイル読込」ボタンから、先ほど作成したクイズ台本を選択します。
学習の開始: すぐにクイズが始まります。スコアや正答率を確認しながら、知識が定着するまで繰り返し挑戦してください。
【実践ワークフロー】AI時代の新しい知的生産サイクル
本ツールは、現代のAI活用ワークフローにシームレスに組み込むことができます。
情報収集・要約フェーズ (NotebookLM / Gemini)
リサーチしたいテーマに関する論文や記事をNotebookLMにアップロードし、要約やノートを生成させます。
あるいは、Geminiのディープリサーチ機能を用いて網羅的なレポートを作成します。
クイズ生成フェーズ (クイズ台本ジェネレーター)
上記で生成されたテキストをコピーし、『クイズ台本ジェネレーター』に入力。ワンクリックでクイズ台本(.txtファイル)を生成します。
知識定着フェーズ (Quiz Master)
生成されたクイズ台本を『Quiz Master』で読み込み、ゲーム感覚で繰り返し解きます。これにより、AIが収集した情報が、能動的なアウトプットを通じて自身の長期記憶へと転換されます。
この3ステップのサイクルこそ、AI時代の情報洪水の中で、真に価値ある知識を自らのものにするための、合理的かつ効果的なアプローチです。
おわりに
AIは、私たちの情報収集能力を飛躍的に高めてくれる強力なパートナーです。しかし、それだけでは知的生産は完結しません。
AIが提供する情報を鵜呑みにせず、自ら問いを立て、能動的に働きかけることで初めて、その知識は真に自分のものとなります。
私が開発したこれらのアプリケーションは、その「最後のひと押し」を支援するために生まれました。AIと共に、より深く、より効率的に学ぶ新しい知的生産のスタイルを、ぜひ皆様もご体験ください。
🎁 アプリケーションのダウンロード
以下の手順で、アプリケーションをご自身のPCに保存してご利用いただけます。
HTMLファイル形式ですので、ウェブブラウザで開くだけで実行可能です。
【重要】コードのコピー手順
コピーしたいコードブロックの中を**トリプルクリック(3回連続で素早くクリック)**します。すると、コード全体が自動で選択されます。
選択された状態で、右クリックして「コピー」を選ぶか、キーボードのショートカット**「Ctrl+C」(Macの場合は「Cmd+C」**)でコピーします。
テキストエディタ(Windowsのメモ帳、Macのテキストエディットなど)に貼り付けます。
下記の指定されたファイル名で、PCにファイルを保存してください。
クイズ台本ジェネレーター → quiz_generator.html
Quiz Master → quiz_master.html
保存したHTMLファイルをダブルクリックすると、ブラウザでアプリケーションが起動します。
1. クイズ台本ジェネレーター (quiz_generator.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>クイズジェネレーター SPA (Gemini AI連携版)</title>
<style>
:root {
--primary-color: #007bff ;
--secondary-color: #6c757d ;
--background-color: #f8f9fa ;
--text-color: #212529 ;
--white-color: #ffffff ;
--border-color: #dee2e6 ;
--danger-color: #dc3545 ;
--success-color: #28a745 ;
--light-gray: #e9ecef ;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
body {
font-family: var(--font-family);
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 1rem;
line-height: 1.6;
}
.app-container {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background-color: var(--white-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.header {
text-align: center;
border-bottom: 1px solid var(--border-color);
padding-bottom: 1rem;
margin-bottom: 2rem;
}
.header__title {
color: var(--text-color);
margin: 0;
font-size: 2rem;
}
.header__subtitle {
color: var(--secondary-color);
font-weight: 400;
margin-top: 0.5rem;
}
.view {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.form__group {
margin-bottom: 1.5rem;
}
.form__label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
.form__input, .form__textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form__input:focus, .form__textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
.form__textarea {
min-height: 250px;
resize: vertical;
}
.form__hint {
font-size: 0.85rem;
color: var(--secondary-color);
margin-top: 0.5rem;
}
.form__controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
padding: 1rem;
background-color: var(--light-gray);
border-radius: 4px;
}
.button {
display: inline-block;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
color: var(--white-color);
background-color: var(--primary-color);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
text-align: center;
text-decoration: none;
transition: background-color 0.2s, transform 0.1s;
}
.button:hover {
background-color: #0056b3 ;
}
.button:active {
transform: scale(0.98);
}
.button:disabled {
background-color: var(--secondary-color);
cursor: not-allowed;
}
.button--success {
background-color: var(--success-color);
}
.button--success:hover {
background-color: #218838 ;
}
.message-area {
margin-top: 1rem;
padding: 1rem;
border-radius: 4px;
display: none; /* Initially hidden */
}
.message-area--error {
color: var(--danger-color);
background-color: #f8d7da ;
border: 1px solid #f5c6cb ;
display: block;
}
.message-area--info {
color: #0c5460 ;
background-color: #d1ecf1 ;
border: 1px solid #bee5eb ;
display: block;
}
.footer {
text-align: center;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
font-size: 0.9rem;
color: var(--secondary-color);
}
</style>
</head>
<body>
<div id="app">
</div>
<script>
const appState = {
sourceText: "",
generatedQuiz: "",
numQuestions: 10,
isLoading: false,
errorMessage: "",
infoMessage: "",
apiKey: localStorage.getItem('geminiApiKey') || ""
};
class GeminiQuizGenerator {
async generate(text, numQuestions, apiKey) {
if (!apiKey) {
return { error: "APIキーが設定されていません。ブラウザで実行する場合は、APIキーを入力してください。" };
}
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${apiKey}`;
const prompt = `
以下のテキストに基づいて、高品質な5択のクイズを${numQuestions}問作成してください。
【制約条件】
- 各問題には、1つの質問文、5つの選択肢、そして1つの正解が含まれている必要があります。
- 質問文は、テキストの内容を理解しているかを問う、意味のあるものでなければなりません。
- 5つの選択肢は、すべてもっともらしいものに見えるように作成してください。
- 5つの選択肢は、正解・不正解に関わらず、すべて同じくらいの文字数になるように調整してください。
- 正解は、必ず5つの選択肢のうちの1つでなければなりません。
- 出力は、指定されたJSONスキーマに厳密に従ってください。
【テキスト】
${text}
`;
const schema = {
type: "ARRAY",
items: {
type: "OBJECT",
properties: {
question: { type: "STRING", description: "クイズの問題文" },
options: { type: "ARRAY", description: "5つの選択肢の配列", items: { type: "STRING" } },
answer: { type: "STRING", description: "正解の選択肢" },
},
required: ["question", "options", "answer"],
},
};
const payload = {
contents: [{ role: "user", parts: [{ text: prompt }] }],
generationConfig: {
responseMimeType: "application/json",
responseSchema: schema,
},
};
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.json();
console.error("API Error:", errorBody);
throw new Error(`APIリクエストに失敗しました (ステータス: ${response.status})。APIキーが正しいか、入力テキストが長すぎないか確認してください。`);
}
const result = await response.json();
if (!result.candidates || !result.candidates[0].content.parts[0].text) {
throw new Error("APIからの応答が予期せぬ形式です。");
}
const quizData = JSON.parse(result.candidates[0].content.parts[0].text);
return this.formatQuiz(quizData);
} catch (error) {
console.error("クイズ生成中にエラーが発生しました:", error);
return { error: error.message || "クイズの生成に失敗しました。時間をおいて再度お試しください。" };
}
}
shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
formatQuiz(quizData) {
const quizItems = quizData.map(item => {
if (!item.question || !Array.isArray(item.options) || !item.answer) {
return null;
}
const shuffledOptions = this.shuffleArray([...item.options]);
const lines = [];
lines.push(item.question);
lines.push(...shuffledOptions);
lines.push(item.answer);
return lines.join('\n');
}).filter(Boolean);
return { quiz: quizItems.join('\n\n') };
}
}
function MainView(state) {
return `
<div class="view">
<div class="form__group">
<label for="api-key-input" class="form__label">Gemini APIキー</label>
<input type="password" id="api-key-input" class="form__input" placeholder="ブラウザで実行する場合、ここにAPIキーを入力" value="${state.apiKey}">
<p class="form__hint">
ブラウザで直接使用するにはAPIキーが必要です。<a href="https://aistudio.google.com/app/apikey" target="_blank" rel="noopener">Google AI Studio</a>から無料で取得できます。入力したキーはブラウザに保存されます。
</p>
</div>
<div class="form__group">
<label for="file-input" class="form__label">ステップ1: ファイルを選択するか、テキストを貼り付け</label>
<input type="file" id="file-input" class="form__input" accept=".txt,.md,text/*">
<p class="form__hint">注: Googleドキュメントの場合は、ファイルを選択するのではなく、ドキュメントの内容をコピーして下のテキストエリアに直接貼り付けてください。</p>
</div>
<div class="form__group">
<textarea id="source-text" class="form__textarea" placeholder="ここにテキストを貼り付け...">${state.sourceText}</textarea>
</div>
<div class="form__group">
<label for="quiz-output" class="form__label">ステップ2: クイズを生成</label>
<div class="form__controls">
<label for="num-questions">問題数:</label>
<input type="number" id="num-questions" class="form__input" value="${state.numQuestions}" min="1" max="20" style="width: 80px;">
<button id="generate-btn" class="button" ${!state.sourceText || state.isLoading ? 'disabled' : ''}>
${state.isLoading ? 'Gemini AIに生成依頼中...' : 'クイズを生成'}
</button>
</div>
</div>
${state.errorMessage ? `<div class="message-area message-area--error">${state.errorMessage}</div>` : ''}
${state.infoMessage ? `<div class="message-area message-area--info">${state.infoMessage}</div>` : ''}
<div class="form__group">
<label for="quiz-output" class="form__label">ステップ3: 結果を確認して保存</label>
<textarea id="quiz-output" class="form__textarea" readonly placeholder="ここに生成されたクイズが表示されます...">${state.generatedQuiz}</textarea>
</div>
<button id="save-btn" class="button button--success" ${!state.generatedQuiz ? 'disabled' : ''}>
テキストファイルとして保存
</button>
</div>
`;
}
function App(state) {
return `
<div class="app-container">
<header class="header">
<h1 class="header__title">クイズジェネレーター SPA</h1>
<h2 class="header__subtitle">テキストから自動で5択クイズを作成します (Gemini AI連携版)</h2>
</header>
<main>
${MainView(state)}
</main>
<footer class="footer">
<p>© 2025 SPA Generator. All rights reserved.</p>
</footer>
</div>
`;
}
const appRoot = document.getElementById('app');
const quizGenerator = new GeminiQuizGenerator();
function setState(newState) {
Object.assign(appState, newState);
render();
}
function render() {
appRoot.innerHTML = App(appState);
addEventListeners();
}
function addEventListeners() {
document.getElementById('api-key-input')?.addEventListener('input', handleApiKeyChange);
document.getElementById('file-input')?.addEventListener('change', handleFileSelect);
document.getElementById('source-text')?.addEventListener('input', handleTextareaInput);
document.getElementById('generate-btn')?.addEventListener('click', handleGenerateQuiz);
document.getElementById('save-btn')?.addEventListener('click', handleSaveQuiz);
document.getElementById('num-questions')?.addEventListener('change', handleNumQuestionsChange);
}
function handleApiKeyChange(event) {
const newKey = event.target.value;
appState.apiKey = newKey;
localStorage.setItem('geminiApiKey', newKey);
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file) return;
if (file.name.endsWith('.gdoc')) {
setState({
errorMessage: "Googleドキュメントファイルは直接読み込めません。お手数ですが、ドキュメントの内容をコピーして下のエリアに貼り付けてください。",
infoMessage: ""
});
event.target.value = "";
return;
}
const reader = new FileReader();
reader.onload = (e) => {
setState({ sourceText: e.target.result, errorMessage: "", infoMessage: "" });
};
reader.onerror = () => {
setState({ errorMessage: "ファイルの読み込みに失敗しました。" });
};
reader.readAsText(file);
}
function handleTextareaInput(event) {
appState.sourceText = event.target.value;
document.getElementById('generate-btn').disabled = !appState.sourceText || appState.isLoading;
}
async function handleGenerateQuiz() {
if (!appState.sourceText) {
setState({ errorMessage: "クイズの元になるテキストを入力してください。" });
return;
}
setState({ isLoading: true, errorMessage: "", infoMessage: "Gemini AIにクイズ生成を依頼しています。しばらくお待ちください...", generatedQuiz: "" });
const apiKeyToUse = appState.apiKey || "";
const result = await quizGenerator.generate(appState.sourceText, appState.numQuestions, apiKeyToUse);
if (result.error) {
setState({ isLoading: false, errorMessage: result.error, infoMessage: "" });
} else {
setState({ isLoading: false, generatedQuiz: result.quiz.trim(), infoMessage: "クイズの生成が完了しました!" });
}
}
function handleSaveQuiz() {
if (!appState.generatedQuiz) return;
const blob = new Blob([appState.generatedQuiz], { type: 'text/plain;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'generated-quiz.txt';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function handleNumQuestionsChange(event) {
const num = parseInt(event.target.value, 10);
if (num > 0 && num <= 20) {
setState({ numQuestions: num });
}
}
document.addEventListener('DOMContentLoaded', () => {
render();
});
</script>
</body>
</html>
2. Quiz Master (quiz_master.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quiz Master - レジューム機能付き</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
box-sizing: border-box;
position: relative;
overflow-x: hidden;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="0.5" fill="white" opacity="0.1"/><circle cx="20" cy="20" r="0.3" fill="white" opacity="0.05"/><circle cx="80" cy="30" r="0.4" fill="white" opacity="0.08"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
pointer-events: none;
}
.quiz-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.15),
0 8px 25px rgba(0, 0, 0, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
padding: 40px;
width: 100%;
max-width: 700px;
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
border: 1px solid rgba(255, 255, 255, 0.2);
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.header {
text-align: center;
margin-bottom: 35px;
position: relative;
}
.app-title {
font-size: 36px;
font-weight: 700;
background: linear-gradient(135deg, #667eea , #764ba2 );
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.app-subtitle {
font-size: 16px;
color: #6b7280 ;
font-weight: 400;
margin: 0;
}
#score {
text-align: center;
font-size: 22px;
font-weight: 600;
margin-bottom: 25px;
padding: 12px 24px;
background: linear-gradient(135deg, #667eea , #764ba2 );
color: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
}
#score ::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
.progress-container {
margin-bottom: 30px;
background: #f1f5f9 ;
border-radius: 12px;
padding: 4px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.06);
}
.progress-bar {
width: 100%;
height: 12px;
background-color: transparent;
border-radius: 8px;
overflow: hidden;
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea , #764ba2 , #f093fb );
border-radius: 8px;
transition: width 0.6s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
}
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
animation: progressShimmer 1.5s infinite;
}
@keyframes progressShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
#question {
font-size: 24px;
font-weight: 600;
margin-bottom: 30px;
color: #1f2937 ;
line-height: 1.5;
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #f8fafc , #f1f5f9 );
border-radius: 18px;
border: 1px solid #e2e8f0 ;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
}
#options {
margin-bottom: 25px;
display: grid;
gap: 12px;
}
.option-btn {
width: 100%;
padding: 18px 24px;
font-size: 16px;
font-weight: 500;
background: linear-gradient(135deg, #ffffff , #f8fafc );
color: #374151 ;
border: 2px solid #e5e7eb ;
border-radius: 16px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
text-align: left;
position: relative;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.option-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.1), transparent);
transition: left 0.5s;
}
.option-btn:hover {
background: linear-gradient(135deg, #667eea , #764ba2 );
color: white;
border-color: #667eea ;
transform: translateY(-2px) scale(1.02);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.25);
}
.option-btn:hover::before {
left: 100%;
}
.option-btn:active {
transform: translateY(0) scale(1);
}
.option-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
#result {
margin: 25px 0;
font-size: 18px;
font-weight: 600;
text-align: center;
padding: 20px;
border-radius: 16px;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
transform: scale(0.95);
opacity: 0;
}
#result .show {
transform: scale(1);
opacity: 1;
}
.result-correct {
background: linear-gradient(135deg, #dcfce7 , #bbf7d0 );
color: #166534 ;
border: 2px solid #86efac ;
box-shadow: 0 4px 20px rgba(34, 197, 94, 0.2);
}
.result-incorrect {
background: linear-gradient(135deg, #fef2f2 , #fecaca );
color: #dc2626 ;
border: 2px solid #fca5a5 ;
box-shadow: 0 4px 20px rgba(239, 68, 68, 0.2);
}
.resume-dialog {
background: linear-gradient(135deg, #fffbeb , #fef3c7 );
border: 2px solid #fbbf24 ;
border-radius: 20px;
padding: 30px;
margin-bottom: 25px;
text-align: center;
box-shadow: 0 8px 25px rgba(251, 191, 36, 0.2);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 8px 25px rgba(251, 191, 36, 0.2); }
50% { box-shadow: 0 8px 35px rgba(251, 191, 36, 0.3); }
}
.resume-dialog h3 {
margin: 0 0 15px 0;
color: #92400e ;
font-size: 20px;
font-weight: 600;
}
.resume-dialog p {
margin: 0 0 25px 0;
color: #92400e ;
font-size: 16px;
}
.control-btn {
background: linear-gradient(135deg, #667eea , #764ba2 );
color: white;
border: none;
padding: 14px 28px;
font-size: 16px;
font-weight: 600;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
margin: 6px;
position: relative;
overflow: hidden;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.control-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}
.control-btn:hover::before {
left: 100%;
}
.control-btn.secondary {
background: linear-gradient(135deg, #6b7280 , #4b5563 );
box-shadow: 0 4px 15px rgba(75, 85, 99, 0.3);
}
.control-btn.secondary:hover {
box-shadow: 0 8px 25px rgba(75, 85, 99, 0.4);
}
.final-score {
text-align: center;
padding: 40px;
background: linear-gradient(135deg, #667eea , #764ba2 );
color: white;
border-radius: 20px;
margin: 25px 0;
box-shadow: 0 12px 40px rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
}
.final-score::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: rotate 20s linear infinite;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.final-score h2 {
margin: 0 0 20px 0;
font-size: 32px;
font-weight: 700;
position: relative;
z-index: 1;
}
.final-score p {
margin: 8px 0;
font-size: 18px;
position: relative;
z-index: 1;
}
.file-upload-area {
margin-top: 25px;
padding: 30px;
border: 2px dashed #d1d5db ;
border-radius: 16px;
text-align: center;
background: linear-gradient(135deg, #f9fafb , #f3f4f6 );
transition: all 0.3s ease;
cursor: pointer;
}
.file-upload-area:hover {
border-color: #667eea ;
background: linear-gradient(135deg, #f0f4ff , #e0e7ff );
}
.file-upload-active {
border-color: #667eea !important;
background: linear-gradient(135deg, #ede9fe , #ddd6fe ) !important;
transform: scale(1.02);
}
.file-status {
margin-top: 15px;
padding: 16px;
border-radius: 12px;
font-size: 14px;
font-weight: 500;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.file-status.info {
background: linear-gradient(135deg, #dbeafe , #bfdbfe );
color: #1e40af ;
border: 2px solid #93c5fd ;
}
.file-status.success {
background: linear-gradient(135deg, #dcfce7 , #bbf7d0 );
color: #166534 ;
border: 2px solid #86efac ;
}
.file-status.error {
background: linear-gradient(135deg, #fef2f2 , #fecaca );
color: #dc2626 ;
border: 2px solid #fca5a5 ;
}
.hidden {
display: none;
}
.controls-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 25px;
}
@media (max-width: 640px) {
.quiz-container {
padding: 25px;
margin: 10px;
}
.app-title {
font-size: 28px;
}
#question {
font-size: 20px;
padding: 16px;
}
.option-btn {
padding: 16px 20px;
font-size: 15px;
}
.controls-grid {
grid-template-columns: 1fr;
}
.control-btn {
width: 100%;
}
}
@media (max-width: 480px) {
.quiz-container {
padding: 20px;
border-radius: 16px;
}
.app-title {
font-size: 24px;
}
#question {
font-size: 18px;
padding: 14px;
}
.option-btn {
padding: 14px 18px;
font-size: 14px;
}
}
</style>
</head>
<body>
<div class="quiz-container">
<div class="header">
<h1 class="app-title">🧠 Quiz Master</h1>
<p class="app-subtitle">挑戦と学習の場</p>
</div>
<div id="resume-dialog" class="resume-dialog hidden">
<h3>🔄 前回の続きから始めますか?</h3>
<p>前回のゲームが保存されています</p>
<div>
<button class="control-btn" onclick="resumeGame()">続きから開始</button>
<button class="control-btn secondary" onclick="startNewGame()">新しく開始</button>
</div>
</div>
<div id="game-area">
<div id="score">Score: 0</div>
<div class="progress-container">
<div class="progress-bar">
<div id="progress-fill" class="progress-fill" style="width: 0%"></div>
</div>
</div>
<div id="question"></div>
<div id="options"></div>
<div id="result"></div>
<div class="controls-grid">
<button class="control-btn secondary" onclick="resetGame()">🔄 リセット</button>
<button class="control-btn secondary" onclick="document.getElementById('fileInput').click()">📁 ファイル読込</button>
</div>
<div id="file-upload-area" class="file-upload-area">
<input type="file" id="fileInput" accept=".txt" style="display: none;">
<div style="font-size: 24px; margin-bottom: 10px;">📄</div>
<p style="margin: 0; color: #6b7280 ; font-weight: 500;">クイズファイル(.txt)をドラッグ&ドロップ</p>
<p style="margin: 8px 0 0 0; font-size: 14px; color: #9ca3af ;">
<strong>フォーマット例(複数問題対応):</strong><br>
質問1 → 選択肢5個 → 正解 → 質問2 → ...<br>
<em>※7行ずつのセットで複数問題を記載可能</em>
</p>
</div>
</div>
</div>
<script>
let quizData = [
{
question: "日本の首都はどこですか?",
options: ["東京", "京都", "大阪", "福岡", "札幌"],
answer: "東京"
},
{
question: "世界で一番高い山は?",
options: ["エベレスト", "富士山", "キリマンジャロ", "マッキンリー", "モンブラン"],
answer: "エベレスト"
},
{
question: "太陽系の惑星の数は?",
options: ["8", "7", "9", "10", "6"],
answer: "8"
}
];
let currentQuestion = 0;
let score = 0;
let gameStartTime = null;
const scoreEl = document.getElementById('score');
const progressEl = document.getElementById('progress-fill');
const questionEl = document.getElementById('question');
const optionsEl = document.getElementById('options');
const resultEl = document.getElementById('result');
const resumeDialogEl = document.getElementById('resume-dialog');
const gameAreaEl = document.getElementById('game-area');
function initGame() {
const savedQuestion = localStorage.getItem('currentQuestion');
const savedScore = localStorage.getItem('score');
const savedStartTime = localStorage.getItem('gameStartTime');
const gameCompleted = localStorage.getItem('gameCompleted');
if (savedQuestion !== null && savedScore !== null && savedStartTime !== null) {
showResumeDialog(gameCompleted === 'true');
} else {
startNewGame();
}
}
function showResumeDialog(isCompleted = false) {
const dialogTitle = document.querySelector('#resume-dialog h3');
const dialogText = document.querySelector('#resume-dialog p');
const resumeBtn = document.querySelector('#resume-dialog .control-btn:first-child');
if (isCompleted) {
dialogTitle.textContent = '🏆 前回のゲーム結果';
dialogText.textContent = '前回のゲームが完了しています。結果を確認しますか?';
resumeBtn.textContent = '結果を確認';
} else {
dialogTitle.textContent = '🔄 前回の続きから始めますか?';
dialogText.textContent = '前回のゲームが保存されています';
resumeBtn.textContent = '続きから開始';
}
resumeDialogEl.classList.remove('hidden');
gameAreaEl.style.opacity = '0.5';
}
function hideResumeDialog() {
resumeDialogEl.classList.add('hidden');
gameAreaEl.style.opacity = '1';
}
function resumeGame() {
currentQuestion = parseInt(localStorage.getItem('currentQuestion')) || 0;
score = parseInt(localStorage.getItem('score')) || 0;
gameStartTime = parseInt(localStorage.getItem('gameStartTime')) || Date.now();
const gameCompleted = localStorage.getItem('gameCompleted') === 'true';
hideResumeDialog();
updateScore();
updateProgress();
if (gameCompleted) {
displayFinalResult();
} else {
displayQuestion();
}
}
function startNewGame() {
currentQuestion = 0;
score = 0;
gameStartTime = Date.now();
clearGameData();
hideResumeDialog();
saveGameData();
updateScore();
updateProgress();
displayQuestion();
}
function resetGame() {
if (confirm('本当にゲームをリセットしますか?進行状況は失われます。')) {
clearGameData();
startNewGame();
}
}
function updateScore() {
scoreEl.textContent = `Score: ${score}`;
}
function updateProgress() {
const progress = ((currentQuestion) / quizData.length) * 100;
progressEl.style.width = `${progress}%`;
}
function displayQuestion() {
if (currentQuestion >= quizData.length) {
displayFinalResult();
return;
}
const questionData = quizData[currentQuestion];
questionEl.textContent = `問題 ${currentQuestion + 1}: ${questionData.question}`;
resultEl.textContent = '';
resultEl.className = '';
optionsEl.innerHTML = '';
questionData.options.forEach((option, index) => {
const button = document.createElement('button');
button.textContent = option;
button.className = 'option-btn';
button.onclick = () => checkAnswer(index);
optionsEl.appendChild(button);
});
updateProgress();
}
function showResult(message, isCorrect) {
resultEl.textContent = message;
resultEl.className = isCorrect ? 'result-correct show' : 'result-incorrect show';
setTimeout(() => {
resultEl.classList.add('show');
}, 100);
}
function checkAnswer(selectedIndex) {
const questionData = quizData[currentQuestion];
const selectedOption = questionData.options[selectedIndex];
const isCorrect = selectedOption === questionData.answer;
const buttons = optionsEl.querySelectorAll('.option-btn');
buttons.forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.6';
});
if (isCorrect) {
showResult('🎉 正解!', true);
score++;
updateScore();
} else {
showResult(`❌ 不正解。正解は「${questionData.answer}」です。`, false);
}
currentQuestion++;
saveGameData();
setTimeout(() => {
resultEl.classList.remove('show');
if (currentQuestion < quizData.length) {
displayQuestion();
} else {
displayFinalResult();
}
}, 2500);
}
function displayFinalResult() {
const totalQuestions = quizData.length;
const percentage = Math.round((score / totalQuestions) * 100);
const playTime = Math.round((Date.now() - gameStartTime) / 1000);
let performanceMessage = '';
let emoji = '';
if (percentage >= 90) {
performanceMessage = '素晴らしい!完璧です!';
emoji = '🏆';
} else if (percentage >= 70) {
performanceMessage = 'よくできました!';
emoji = '🎉';
} else if (percentage >= 50) {
performanceMessage = 'まずまずですね!';
emoji = '👍';
} else {
performanceMessage = '次回頑張りましょう!';
emoji = '💪';
}
questionEl.innerHTML = `
<div class="final-score">
<h2>${emoji} クイズ完了!</h2>
<p><strong>スコア:</strong> ${score} / ${totalQuestions} 問正解</p>
<p><strong>正答率:</strong> ${percentage}%</p>
<p><strong>プレイ時間:</strong> ${playTime}秒</p>
<p style="margin-top: 20px; font-size: 20px;">${performanceMessage}</p>
</div>
`;
optionsEl.innerHTML = `
<button class="control-btn" onclick="startNewGame()" style="width: 100%; margin-top: 20px; font-size: 18px; padding: 16px;">
🚀 新しいゲームを始める
</button>
`;
resultEl.textContent = '';
resultEl.className = '';
updateProgress();
localStorage.setItem('gameCompleted', 'true');
saveGameData();
}
function saveGameData() {
localStorage.setItem('currentQuestion', currentQuestion.toString());
localStorage.setItem('score', score.toString());
localStorage.setItem('gameStartTime', gameStartTime.toString());
if (currentQuestion < quizData.length) {
localStorage.removeItem('gameCompleted');
}
}
function clearGameData() {
localStorage.removeItem('currentQuestion');
localStorage.removeItem('score');
localStorage.removeItem('gameStartTime');
localStorage.removeItem('gameCompleted');
}
document.addEventListener('DOMContentLoaded', function() {
initGame();
initFileUpload();
});
function initFileUpload() {
const fileInput = document.getElementById('fileInput');
const uploadArea = document.getElementById('file-upload-area');
fileInput.addEventListener('change', handleFileSelect);
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', handleDragOver);
uploadArea.addEventListener('dragleave', handleDragLeave);
uploadArea.addEventListener('drop', handleFileDrop);
}
function handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
processFile(file);
}
}
function handleDragOver(event) {
event.preventDefault();
document.getElementById('file-upload-area').classList.add('file-upload-active');
}
function handleDragLeave(event) {
event.preventDefault();
document.getElementById('file-upload-area').classList.remove('file-upload-active');
}
function handleFileDrop(event) {
event.preventDefault();
document.getElementById('file-upload-area').classList.remove('file-upload-active');
const files = event.dataTransfer.files;
if (files.length > 0) {
processFile(files[0]);
}
}
function processFile(file) {
if (!file.name.toLowerCase().endsWith('.txt')) {
showFileStatus('❌ テキストファイル(.txt)を選択してください', 'error');
return;
}
showFileStatus('📖 ファイルを読み込み中...', 'info');
const reader = new FileReader();
reader.onload = function(e) {
try {
const content = e.target.result;
const newQuizData = parseQuizFile(content);
if (newQuizData.length > 0) {
quizData = newQuizData;
showFileStatus(`✅ ${newQuizData.length}問のクイズを読み込みました`, 'success');
setTimeout(() => {
if (confirm('新しいクイズデータが読み込まれました。ゲームを最初から始めますか?')) {
clearGameData();
startNewGame();
}
}, 1000);
} else {
showFileStatus('❌ 有効なクイズデータが見つかりませんでした。フォーマットを確認してください。', 'error');
}
} catch (error) {
showFileStatus('❌ ファイルの解析に失敗しました', 'error');
}
};
reader.readAsText(file, 'UTF-8');
}
function parseQuizFile(content) {
const lines = content.split('\n').map(line => line.trim()).filter(line => line.length > 0);
const quizItems = [];
for (let i = 0; i < lines.length; i += 7) {
if (i + 6 < lines.length) {
const question = lines[i];
const options = lines.slice(i + 1, i + 6);
const answer = lines[i + 6];
if (question && options.length === 5 && answer && options.includes(answer)) {
quizItems.push({ question, options, answer });
}
}
}
return quizItems;
}
function showFileStatus(message, type) {
const uploadArea = document.getElementById('file-upload-area');
const existingStatus = uploadArea.querySelector('.file-status');
if (existingStatus) existingStatus.remove();
const statusDiv = document.createElement('div');
statusDiv.className = `file-status ${type}`;
statusDiv.textContent = message;
uploadArea.appendChild(statusDiv);
if (type === 'success') {
setTimeout(() => statusDiv.remove(), 3000);
}
}
window.addEventListener('beforeunload', function() {
if (currentQuestion > 0 && currentQuestion < quizData.length && !localStorage.getItem('gameCompleted')) {
saveGameData();
}
});
</script>
</body>
</html>
ツールは思考の拡張であり、学びは創造の始まりである。
#自作アプリ #AI活用 #NotebookLM #Gemini #知的生産術
- #AIとやってみた
- #FFFFFF
- #question
- #e2e8f0
- #score
- #f8fafc
- #result
- #f1f5f9
- #007bff
- #f8f9fa
- #dc2626
- #764ba2
- #f9fafb
- #667eea
- #fbbf24
- #4b5563
- #options
- #1f2937
- #28a745
- #0056b3
- #dc3545
- #e5e7eb
- #e9ecef
- #6c757d
- #6b7280
- #fef3c7
- #374151
- #f093fb
- #f8d7da
- #f3f4f6
- #1e40af
- #dee2e6
- #f5c6cb
- #92400e
- #d1d5db
- #bbf7d0
- #9ca3af
- #dbeafe
- #fef2f2
- #FFFBEB
- #212529
- #218838
- #bfdbfe
- #fecaca
- #d1ecf1
- #166534
- #dcfce7
- #fca5a5
- #0c5460
- #bee5eb
- #e0e7ff
- #93c5fd
- #86efac
- #f0f4ff
- #ede9fe
- #ddd6fe