import React, { useState, useEffect } from 'react';
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInAnonymously,
signInWithCustomToken,
onAuthStateChanged
} from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { Loader2 } from 'lucide-react';
const apiKey = "AIzaSyBMNfw2FMkzsNe7cQOKZvH_nKD29S_-Lg4"; // 執行環境自動提供
// Firebase 配置
const firebaseConfig = {
apiKey: "AIzaSyAlb9zh3rLQRmgsmWlVOx_SqqI7CclIJ48",
authDomain: "snowman-414.firebaseapp.com",
projectId: "snowman-414",
storageBucket: "snowman-414.firebasestorage.app",
messagingSenderId: "988896874230",
appId: "1:988896874230:web:21643335057763016ba6d7",
measurementId: "G-D6W742E2YW"
};
// 全域雪花組件
const Snowfall = () => {
const [snowflakes, setSnowflakes] = useState([]);
useEffect(() => {
const count = 60;
const flakes = Array.from({ length: count }).map((_, i) => ({
id: i,
left: Math.random() * 100,
size: Math.random() * 3 + 2,
duration: Math.random() * 8 + 10,
delay: Math.random() * 10,
opacity: Math.random() * 0.5 + 0.3
}));
setSnowflakes(flakes);
}, []);
return (
{snowflakes.map(flake => (
))}
);
};
const App = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState({});
const [images, setImages] = useState({});
// 新增一個狀態來追蹤重試次數,若需要顯示在 UI 上的話
const [retryingIds, setRetryingIds] = useState({});
const CHAR_WUSONG = "18yo Asian boy, tall, dark navy wool coat, messy black hair, melancholic eyes, natural face, NO earrings, NO facial jewelry.";
const CHAR_WENQIAN = "16yo Asian girl, light blue oversized punk denim jacket, red collar, red canvas shoes, reddish cheeks.";
const STYLE = "Anime manga style, high quality digital illustration, cinematic night lighting, 2035 futuristic atmosphere, heavy snow.";
const scenes = [
{
id: "img-1",
speaker: "旁白",
p: "Wide angle snowy night city street, futuristic lamppost with glowing white light, snowflakes like fireflies.",
text: "下雪了,好恬淡的雪。街燈映出一簇飛白,像雀躍的螢火蟲。"
},
{
id: "img-2",
speaker: "旁白",
p: `${CHAR_WUSONG} and ${CHAR_WENQIAN} walking on slippery snowy sidewalk, girl in red shoes balancing with arms out, boy looking at her.`,
text: "伍松扭頭看走在他右邊的妹妹。即使撒過了溶解劑,人行道上還是濕滑。這個臉頰透紅,聳著肩膀的小女孩,哆哆嗦嗦地踏著老式紅色帆布鞋,在驚心膽顫的邁步和偶爾的打滑中試圖用手維持身體的平衡。她揣著的手十分不安地抽出又塞回,讓他感覺好笑又溫馨。"
},
{
id: "img-3",
speaker: "伍松",
p: `Side profile of ${CHAR_WUSONG} talking to ${CHAR_WENQIAN}, winter night, snow falling.`,
text: "「標新立異是要付出代價的呀,小龐克。霜降都過去了,你也不戴副手套。非得穿這沒有口袋,領子又灌風的牛仔服,自討苦吃。」"
},
{
id: "img-4",
speaker: "伍文倩",
p: `Close up of ${CHAR_WENQIAN} looking annoyed, shivering in the snow.`,
text: "「一點不冷的好嗎!要你管,別消耗我的體力跟你說話。」"
},
{
id: "img-5",
speaker: "旁白",
p: "Futuristic high school building entrance, glowing blue digital panels, snow covered architecture, architecture only, NO text, NO letters, NO signage, clean facade.",
text: "伍文倩,小他哥兩歲,兩人同在離家不遠的博大附中上學。博大附中響應各大政企合作項目降低人才同質化的需求,對學生定制化培養。學習環境也比較靈活,晚自習時間可以自由選擇。但是大部分家長對孩子還是傾向於約束和託管,省得費心思,動腦子了。"
},
{
id: "img-6",
speaker: "伍鵬飛",
p: "Hologram of a middle-aged man smiling casually, digital interface background, NO text on screen.",
text: "伍松的班主任皺著眉給伍鵬飛打影像,你孩子都要高考了,還不讓他在家集訓?伍鵬飛慢悠悠地講,「哎,天要下雨,娃要進城,隨孩子便吧。」"
},
{
id: "img-7",
speaker: "伍松",
p: `${CHAR_WUSONG} holding hands with ${CHAR_WENQIAN} near a neon restaurant sign, snow falling, neon glow only, NO legible text.`,
text: "伍松牽住妹妹的手,哈出一口冷氣。「你感覺剛才那餛飩好吃不,科技感怎麼樣?」\n「太一般了,蟹黃吃著像雞蛋黃,墨魚吃著像麵筋卷。湯也淡得離譜!」"
},
{
id: "img-8",
speaker: "伍松",
p: `${CHAR_WUSONG} walking in snow, philosophical look.`,
text: "「哈哈,我也覺得。媽只讓咱們吃2035年新規食標的館子,這味比減脂餐好不到哪去。世界上不存在絕對的真理,但存在絕對的標準。」\n「切,無聊。」"
},
{
id: "img-9",
speaker: "旁白",
p: "A boy's POV looking up into a swirl of heavy snow, surreal wormhole effect.",
text: "又一陣寒風吹來,伍松敏銳地抬起頭。雪意更濃了,他被天空中揮灑的落白吸引,痴痴凝望。雪花不緊不慢地逼近,像在凝視,卻在他身旁倏乎落下,讓他彷彿置身光怪陸離的蟲洞。"
},
{
id: "img-10",
speaker: "伍松",
p: `${CHAR_WUSONG} looking lost in a white void of snow, crying with only one eye, ONE tear falling from ONE eye only, other eye dry, NO text, NO captions, NO English letters, pure visual.`,
text: "持續的吸引力讓我的意識遊走於現實之外。他在虛無縹緲的時空裡亂抓,可惜斑駁的光點從他指縫中全部溜走了。一種強烈的宿命感覆蓋住了他所有的感官,什麼都抓不住的挫敗感填充了他的世界,讓他絕望。"
},
{
id: "img-11",
speaker: "旁白",
p: `Close up of ${CHAR_WENQIAN}'s hand tightly grasping ${CHAR_WUSONG}'s hand, snow on gloves.`,
text: "在手足無措中他的指縫終於被緊緊地彌合住,那股熟悉又陌生的溫暖讓他激靈。纖細而有力,像是媽媽的手。"
},
{
id: "img-12",
speaker: "伍文倩",
p: `Extreme close up of ${CHAR_WUSONG}'s face, natural skin, NO earrings, only ONE single tear falling from ONLY the left eye, right eye dry, snow background.`,
text: "「哥你又哭,你可真行。還只有一隻眼睛淌水,真搞笑。」母親秦舒為了預防孩子近視,給兩個孩子在眼球上裝了矯正器。伍松總會止不住流眼淚,伍鵬飛說是有輕微的刺激,可妹妹就沒事。伍松緊握著和妹妹十指交叉的手,用袖子一抹臉。「哎,偶爾釋放一下情緒,有益身心健康,你不懂。」"
}
];
useEffect(() => {
const initAuth = async () => {
try {
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
} catch (error) {
console.error("Auth initialization failed:", error);
}
};
initAuth();
const unsubscribe = onAuthStateChanged(auth, setUser);
return () => unsubscribe();
}, []);
useEffect(() => {
if (!user) return;
scenes.forEach((scene, index) => {
setTimeout(() => generateImageWithRetry(scene.p, scene.id), index * 2000);
});
}, [user]);
const generateImageWithRetry = async (prompt, id, retryCount = 0) => {
setLoading(prev => ({ ...prev, [id]: true }));
if (retryCount > 0) {
setRetryingIds(prev => ({ ...prev, [id]: retryCount }));
}
const maxRetries = 5;
const backoffDelays = [1000, 2000, 4000, 8000, 16000];
try {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
instances: { prompt: `${STYLE} ${prompt}` },
parameters: { sampleCount: 1 }
})
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText || 'Empty response'}`);
}
const result = await response.json();
if (result.predictions && result.predictions[0]) {
const url = `data:image/png;base64,${result.predictions[0].bytesBase64Encoded}`;
setImages(prev => ({ ...prev, [id]: url }));
setLoading(prev => ({ ...prev, [id]: false }));
setRetryingIds(prev => {
const next = { ...prev };
delete next[id];
return next;
});
} else {
throw new Error("No predictions in result");
}
} catch (error) {
if (retryCount < maxRetries) {
setTimeout(() => {
generateImageWithRetry(prompt, id, retryCount + 1);
}, backoffDelays[retryCount]);
} else {
console.error(`Final image generation error for ${id}:`, error);
setLoading(prev => ({ ...prev, [id]: false }));
}
}
};
return (
{scenes.map((scene, index) => (
{loading[scene.id] ? (
{retryingIds[scene.id] ? `Retrying Sync (${retryingIds[scene.id]})...` : 'Syncing Reality...'}
) : (
images[scene.id] ? (

) : (
Waiting for visualization...
)
)}
{/* 遊戲風格對話框 */}
{/* 角色標籤 */}
{scene.speaker}
{/* 對話主體 */}
{/* 裝飾元素 */}
{scene.text}
{/* 翻頁指示器 */}
FRAME_{String(index + 1).padStart(2, '0')}
))}
);
};
export default App;