Files
ai-app-skr/views/HomeView.tsx
2025-12-25 17:04:01 +08:00

152 lines
8.0 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import { AppMode, Language } from '../types';
import { MessageCircle, Mic2, BookOpen, Headphones, ScanText, Languages, Palette, Play, Square, Loader2, Sparkles, Home } from 'lucide-react';
import { translations } from '../utils/localization';
import { geminiService, decodeAudioData } from '../services/geminiService';
interface HomeViewProps {
language: Language;
onNavigate: (mode: AppMode) => void;
}
const PHRASES = [
{ ja: "継続は力なり", reading: "Keizoku wa chikara nari", en: "Perseverance is power.", zh: "坚持就是力量。" },
{ ja: "一期一会", reading: "Ichigo ichie", en: "Treasure every meeting, for it will never recur.", zh: "一期一会 (珍惜每次相遇)。" },
{ ja: "千里の道も一歩から", reading: "Senri no michi mo ippo kara", en: "A journey of a thousand miles begins with a single step.", zh: "千里之行,始于足下。" },
{ ja: "七転び八起き", reading: "Nanakorobi yaoki", en: "Fall seven times, stand up eight.", zh: "七颠八起 (百折不挠)。" },
{ ja: "知らぬが仏", reading: "Shiranu ga hotoke", en: "Ignorance is bliss.", zh: "不知者是佛 (眼不见为净)。" },
{ ja: "花鳥風月", reading: "Kachou fuugetsu", en: "Experience the beauties of nature.", zh: "花鸟风月 (大自然的美景)。" },
{ ja: "以心伝心", reading: "Ishin denshin", en: "Telepathy / Tacit understanding.", zh: "以心传心 (心领神会)。" }
];
const HomeView: React.FC<HomeViewProps> = ({ language, onNavigate }) => {
const t = translations[language].home;
const tNav = translations[language].nav;
const [phrase, setPhrase] = useState(PHRASES[0]);
const [isPlaying, setIsPlaying] = useState(false);
const [isLoadingAudio, setIsLoadingAudio] = useState(false);
const audioContextRef = useRef<AudioContext | null>(null);
const audioSourceRef = useRef<AudioBufferSourceNode | null>(null);
useEffect(() => {
// Select phrase based on day of year to change daily
const dayOfYear = Math.floor((Date.now() - new Date(new Date().getFullYear(), 0, 0).getTime()) / 86400000);
setPhrase(PHRASES[dayOfYear % PHRASES.length]);
}, []);
useEffect(() => {
return () => {
if (audioSourceRef.current) audioSourceRef.current.stop();
};
}, []);
const playAudio = async () => {
if (isPlaying) {
if (audioSourceRef.current) audioSourceRef.current.stop();
setIsPlaying(false);
return;
}
setIsLoadingAudio(true);
try {
const audioBase64 = await geminiService.generateSpeech(phrase.ja);
if (audioBase64) {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
const ctx = audioContextRef.current;
if (ctx.state === 'suspended') await ctx.resume();
const buffer = await decodeAudioData(audioBase64, ctx);
const source = ctx.createBufferSource();
source.buffer = buffer;
source.connect(ctx.destination);
source.onended = () => setIsPlaying(false);
source.start();
audioSourceRef.current = source;
setIsPlaying(true);
}
} catch (e) {
console.error("Audio playback failed", e);
} finally {
setIsLoadingAudio(false);
}
};
const features = [
{ mode: AppMode.CHAT, title: t.features.chatTitle, desc: t.features.chatDesc, icon: MessageCircle, color: 'text-indigo-500', bg: 'bg-indigo-50 border-indigo-100' },
{ mode: AppMode.READING, title: t.features.readingTitle, desc: t.features.readingDesc, icon: BookOpen, color: 'text-emerald-500', bg: 'bg-emerald-50 border-emerald-100' },
{ mode: AppMode.LISTENING, title: t.features.listeningTitle, desc: t.features.listeningDesc, icon: Headphones, color: 'text-sky-500', bg: 'bg-sky-50 border-sky-100' },
{ mode: AppMode.SPEAKING, title: t.features.speakingTitle, desc: t.features.speakingDesc, icon: Mic2, color: 'text-orange-500', bg: 'bg-orange-50 border-orange-100' },
{ mode: AppMode.TRANSLATION, title: t.features.toolsTitle, desc: t.features.toolsDesc, icon: Languages, color: 'text-blue-500', bg: 'bg-blue-50 border-blue-100' },
{ mode: AppMode.CREATIVE, title: t.features.creativeTitle, desc: t.features.creativeDesc, icon: Palette, color: 'text-purple-500', bg: 'bg-purple-50 border-purple-100' },
];
return (
<div className="h-full overflow-y-auto bg-slate-50/50 p-6 md:p-10">
<div className="max-w-6xl mx-auto space-y-10 pb-20">
{/* Header */}
<div className="animate-fade-in-up">
<h1 className="text-3xl md:text-4xl font-extrabold text-slate-800 mb-2">{t.welcome}</h1>
<p className="text-slate-500 text-lg">{t.subtitle}</p>
</div>
{/* Daily Phrase Card */}
<div className="bg-white rounded-3xl p-8 border border-slate-100 shadow-xl shadow-slate-200/50 relative overflow-hidden animate-scale-in group">
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-bl from-pink-50 to-transparent -mr-20 -mt-20 rounded-full opacity-70 group-hover:scale-110 transition-transform duration-700 pointer-events-none"></div>
<div className="relative z-10">
<div className="flex items-center gap-2 mb-4">
<Sparkles className="text-pink-400 animate-pulse" size={20} />
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-widest">{t.dailyPhrase}</h3>
</div>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<p className="text-4xl md:text-5xl font-black text-slate-800 mb-3 font-serif leading-tight">{phrase.ja}</p>
<p className="text-lg text-slate-500 font-medium mb-1">{phrase.reading}</p>
<p className="text-slate-400 italic">{language === 'zh' ? phrase.zh : phrase.en}</p>
</div>
<button
onClick={playAudio}
disabled={isLoadingAudio}
className={`w-16 h-16 rounded-full flex items-center justify-center transition-all shadow-lg hover:scale-105 active:scale-95 flex-shrink-0 ${
isPlaying
? 'bg-pink-500 text-white shadow-pink-200'
: 'bg-white border-2 border-slate-100 text-slate-400 hover:border-pink-200 hover:text-pink-500'
}`}
>
{isLoadingAudio ? <Loader2 size={24} className="animate-spin" /> : isPlaying ? <Square size={24} fill="currentColor" /> : <Play size={28} fill="currentColor" className="ml-1" />}
</button>
</div>
</div>
</div>
{/* Features Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{features.map((feat, idx) => (
<button
key={idx}
onClick={() => onNavigate(feat.mode)}
className={`p-6 rounded-2xl text-left transition-all duration-300 hover:-translate-y-1 hover:shadow-lg border group ${feat.bg} animate-fade-in-up`}
style={{ animationDelay: `${idx * 100}ms` }}
>
<div className={`w-12 h-12 rounded-xl bg-white shadow-sm flex items-center justify-center mb-4 ${feat.color} group-hover:scale-110 transition-transform`}>
<feat.icon size={24} />
</div>
<h3 className="text-lg font-bold text-slate-800 mb-1 group-hover:text-indigo-600 transition-colors">{feat.title}</h3>
<p className="text-sm text-slate-500 leading-relaxed">{feat.desc}</p>
</button>
))}
</div>
</div>
</div>
);
};
export default HomeView;