```html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=400, initial-scale=1.0">
<title>🌸🍎 生育調査アプリ 🍎🌸</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.22.20/babel.min.js"></script>
<style>
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {transform: translateY(0);}
40% {transform: translateY(-10px);}
60% {transform: translateY(-5px);}
}
@keyframes glow {
0% { box-shadow: 0 0 5px #ff0;}
50% { box-shadow: 0 0 20px #ff0;}
100% { box-shadow: 0 0 5px #ff0;}
}
.bounce {
animation: bounce 1s infinite;
}
.glow {
animation: glow 2s infinite;
}
.fade-in {
animation: fadeIn 1s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef } = React;
function PlantGrowthTracker() {
const [records, setRecords] = useState([]);
const [isListening, setIsListening] = useState(false);
const [inputType, setInputType] = useState('🌸 花');
const [inputValue, setInputValue] = useState('');
const [message, setMessage] = useState('');
const recognitionRef = useRef(null);
useEffect(() => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (SpeechRecognition) {
const recognition = new SpeechRecognition();
recognition.lang = 'ja-JP';
recognition.continuous = true;
recognition.interimResults = false;
recognition.onresult = (event) => {
const transcript = event.results[event.results.length - 1][0].transcript.trim();
processVoiceCommand(transcript);
};
recognition.onerror = (event) => {
console.error('音声認識エラー:', event.error);
setMessage(`⚠️ 音声認識エラー: ${event.error}`);
setIsListening(false);
};
recognition.onend = () => {
if (isListening) {
recognition.start();
} else {
setMessage('🔇 音声認識停止');
}
};
recognitionRef.current = recognition;
} else {
setMessage('🚫 お使いのブラウザは音声認識をサポートしていません');
}
return () => {
if (recognitionRef.current) {
recognitionRef.current.stop();
}
};
}, []);
useEffect(() => {
if (isListening) {
if (recognitionRef.current) {
try {
recognitionRef.current.start();
setMessage('🎙️ 音声認識中... 「🌸 花 5」「🍎 実 3」「停止」などと話してください');
} catch (error) {
console.error('音声認識開始エラー:', error);
setMessage('⚠️ 音声認識の開始に失敗しました');
setIsListening(false);
}
}
} else {
if (recognitionRef.current) {
try {
recognitionRef.current.stop();
} catch (error) {
console.error('音声認識停止エラー:', error);
}
}
}
}, [isListening]);
const toggleListening = () => {
setIsListening(prev => !prev);
};
const processVoiceCommand = (command) => {
console.log('認識されたコマンド:', command);
setMessage(`🔊 認識: "${command}"`);
const flowerMatch = command.match(/🌸?\s*花\s*(\d+)/);
if (flowerMatch) {
addRecord('🌸 花', flowerMatch[1]);
return;
}
const fruitMatch = command.match(/🍎?\s*実\s*(\d+)/);
if (fruitMatch) {
addRecord('🍎 実', fruitMatch[1]);
return;
}
if (command.includes('停止')) {
setIsListening(false);
return;
}
if (command.includes('取り消し')) {
undoLastRecord();
return;
}
setMessage(`❓ コマンドとして認識できませんでした: "${command}"`);
};
const addRecord = (type, value) => {
if (!value || isNaN(value)) {
setMessage('⚠️ 有効な数値を入力してください');
return;
}
const now = new Date();
const date = now.toLocaleDateString();
const time = now.toLocaleTimeString();
setRecords(prev => [
...prev,
{ date, time, type, value: parseInt(value, 10) }
]);
setMessage(`${type} 🌟 ${value} を記録しました! 🎉`);
setInputValue('');
};
const handleManualAdd = () => {
addRecord(inputType, inputValue);
};
const undoLastRecord = () => {
if (records.length > 0) {
const newRecords = [...records];
newRecords.pop();
setRecords(newRecords);
setMessage('🗑️ 最後の記録を取り消しました');
} else {
setMessage('⚠️ 取り消す記録がありません');
}
};
const downloadCSV = () => {
if (records.length === 0) {
setMessage('⚠️ 記録がありません');
return;
}
const header = '日付,時間,種類,数値\n';
const csvContent = records.map(r => `${r.date},${r.time},${r.type},${r.value}`).join('\n');
const blob = new Blob([header + csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `生育調査_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
setMessage('💾 CSVファイルをダウンロードしました');
};
const clearAllRecords = () => {
if (window.confirm('❓ すべての記録を削除してもよろしいですか?')) {
setRecords([]);
setMessage('🧹 全記録をクリアしました');
}
};
return (
<div style={{
width: '400px',
height: '400px',
margin: '0 auto',
padding: '10px',
backgroundColor: '#fff8f0',
borderRadius: '15px',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
overflowY: 'auto',
fontFamily: 'Arial, sans-serif',
position: 'relative'
}}>
<h1 style={{
textAlign: 'center',
color: '#ff6f61',
fontSize: '24px',
marginBottom: '10px',
animation: 'bounce 2s infinite'
}}>
🌸🍎 生育調査アプリ 🍎🌸
</h1>
<div style={{
textAlign: 'center',
padding: '8px',
backgroundColor: isListening ? '#a3d9a5' : '#d3d3d3',
borderRadius: '8px',
color: '#fff',
marginBottom: '10px',
transition: 'background-color 0.3s',
fontWeight: 'bold'
}}>
{isListening ? '🎙️ 音声認識中...' : '🔇 音声認識停止中'}
</div>
<div style={{
display: 'flex',
justifyContent: 'space-around',
marginBottom: '10px'
}}>
<button
onClick={toggleListening}
style={{
padding: '10px',
borderRadius: '8px',
border: 'none',
backgroundColor: isListening ? '#ff4d4d' : '#4da6ff',
color: '#fff',
fontSize: '16px',
cursor: 'pointer',
animation: 'glow 2s infinite',
transition: 'background-color 0.3s'
}}
>
{isListening ? '🔴 停止' : '🟢 開始'}
</button>
<button
onClick={undoLastRecord}
style={{
padding: '10px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#f0ad4e',
color: '#fff',
fontSize: '16px',
cursor: 'pointer',
transition: 'background-color 0.3s'
}}
>
↩️ 取り消し
</button>
</div>
{message && (
<div style={{
padding: '8px',
backgroundColor: '#e6f7ff',
borderRadius: '8px',
color: '#005c99',
textAlign: 'center',
marginBottom: '10px',
animation: 'fadeIn 1s'
}}>
{message}
</div>
)}
<div style={{
marginBottom: '10px'
}}>
<h2 style={{ fontSize: '18px', color: '#ff6f61', marginBottom: '5px' }}>➕ 手動入力</h2>
<div style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '5px'
}}>
<select
value={inputType}
onChange={(e) => setInputType(e.target.value)}
style={{
flex: '1',
padding: '8px',
borderRadius: '8px',
border: '1px solid #ccc',
marginRight: '5px'
}}
>
<option value="🌸 花">🌸 花</option>
<option value="🍎 実">🍎 実</option>
</select>
<input
type="number"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="数値"
min="0"
style={{
flex: '1',
padding: '8px',
borderRadius: '8px',
border: '1px solid #ccc'
}}
/>
</div>
<button
onClick={handleManualAdd}
style={{
width: '100%',
padding: '10px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#5cb85c',
color: '#fff',
fontSize: '16px',
cursor: 'pointer',
transition: 'background-color 0.3s'
}}
>
➕ 追加
</button>
</div>
<div style={{
marginBottom: '10px'
}}>
<h2 style={{ fontSize: '18px', color: '#ff6f61', marginBottom: '5px' }}>📋 記録一覧</h2>
<div style={{
maxHeight: '120px',
overflowY: 'auto',
border: '1px solid #ccc',
borderRadius: '8px',
padding: '5px',
backgroundColor: '#f9f9f9'
}}>
{records.length === 0 ? (
<p style={{ textAlign: 'center', color: '#999' }}>📭 記録がありません</p>
) : (
<ul style={{ listStyle: 'none', padding: '0', margin: '0' }}>
{records.map((record, index) => (
<li key={index} style={{
padding: '5px',
borderBottom: '1px solid #eee',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
animation: 'fadeIn 0.5s'
}}>
<span>{record.date} {record.time}</span>
<span>{record.type}: {record.value}</span>
</li>
))}
</ul>
)}
</div>
</div>
<div style={{
display: 'flex',
justifyContent: 'space-between'
}}>
<button
onClick={downloadCSV}
style={{
padding: '8px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#0275d8',
color: '#fff',
fontSize: '14px',
cursor: 'pointer',
flex: '1',
marginRight: '5px',
transition: 'background-color 0.3s'
}}
>
💾 CSV保存
</button>
<button
onClick={clearAllRecords}
style={{
padding: '8px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#d9534f',
color: '#fff',
fontSize: '14px',
cursor: 'pointer',
flex: '1',
marginLeft: '5px',
transition: 'background-color 0.3s'
}}
>
🧹 クリア
</button>
</div>
<div style={{
marginTop: '10px',
padding: '10px',
backgroundColor: '#fff3cd',
borderRadius: '8px',
color: '#856404',
fontSize: '14px',
animation: 'fadeIn 1s'
}}>
<h3 style={{ marginTop: '0' }}>📝 音声認識の使い方</h3>
<ol>
<li>🔵 「🟢 開始」ボタンをクリックして音声認識を開始します。</li>
<li>🎤 マイクのアクセス許可を求められたら「許可」をクリックします。</li>
<li>🌸 「花 5」または 🍎 「実 3」 のように話して記録を追加します。</li>
<li>🔴 「停止」ボタンをクリックするか、「停止」と言って音声認識を終了します。</li>
</ol>
<p>⚠️ 注意: 音声認識はChrome、Edge、Safariなど一部のブラウザでのみ動作します。また、インターネット接続が必要です。</p>
</div>
</div>
);
}
ReactDOM.render(<PlantGrowthTracker />, document.getElementById('root'));
</script>
</body>
</html>
```