"use client"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { track } from "@/lib/analytics"; import { ART_STYLES, GENDERS, PACINGS, PLOT_STYLES, type Gender, } from "@/lib/options"; import { readStoredTtsConfig } from "@/lib/clientTtsConfig"; import { SettingsModal, readStoredPlayerName, readStoredVisionClick } from "@/components/SettingsModal"; /* ============================================================================ InfiPlot · 首页(编辑式视觉风格 · 居中构图,呼应低保真原型) - 顶部 Header:左上角衬线 wordmark logo - Hero 控制区(居中):标题 / prompt 输入框 + 开始 / 5 个类别选择器 - 统一瀑布流(居中定宽):7 张主推 + 16 张画廊,按性向整体 crossfade 切换 - 项目介绍(题跋式排版) ========================================================================== */ const EXAMPLE_PHRASES: Record = { 男性向: [ "从小一起长大的青梅竹马,突然红着脸向我告白", "一觉醒来,班上的女生好像都偷偷喜欢上了我", "三年之期已到,原来我是富家公子,报仇时机已到", "我带着无限 Token 穿越回了互联网诞生前夕……", ], 女性向: [ "穿越成将军府的废物嫡女,冷面摄政王却独宠我一人", "重生回到分手前夜,这一次换我先放手", "一觉醒来成了乙游里的恶役千金,要躲开所有死亡结局", ], }; type Opt = { label: string; items: string[]; defaultIndex?: number; modal?: boolean; }; const OPTS: Opt[] = [ { label: "性向", items: [...GENDERS] }, { label: "绘画风格", modal: true, items: [...ART_STYLES] }, { label: "剧情风格", items: [...PLOT_STYLES], defaultIndex: 1 }, { label: "语音配音", items: ["关闭", "开启"], defaultIndex: 1 }, { label: "内容节奏", items: [...PACINGS], defaultIndex: 1 }, ]; type StoryContent = { title: string; outline: string; style: string; tags: string[] }; import { STYLE_MAP } from "@/lib/options"; /* 每个性向 24 篇预设剧情(与封面 /home/{m|f}{i}.webp 按索引一一对应)。 男/女同索引共享画面尺寸,切性向 crossfade 时卡片高度不跳变。 */ const STORIES: Record = { 男性向: [ { "title": "贤者陨落", "outline": "帝国首席大魔导师遭挚友背叛,魔力核心被挖,沦为废人。百年后,他于拍卖会以奴隶身份现身,血契锁链下,是重燃的复仇烈焰与更禁忌的古代魔法。", "style": "古典厚涂油画 (学术奇幻)", "tags": [ "逆袭", "系统", "西幻" ] }, { "title": "画中圣手", "outline": "落魄书生意外获得一支诡异画笔,画出的女子竟能破画而出,化为真人。他本想靠此翻身,却卷入一桩延续千年的宫廷秘辛与仙凡禁忌之恋。", "style": "极简中国水墨 (Image 0参考升级版)", "tags": [ "逆袭", "系统", "古风奇幻" ] }, { "title": "花魁的刀", "outline": "他是吉原最负盛名的花魁,舞姿倾城,面具下的真实身份却是令江户幕府闻风丧胆的传奇忍者。当幕府密探踏入花街,刀光与花影将同绽。", "style": "浮世绘木刻 (美人画升级)", "tags": [ "女扮男装", "忍者", "权谋" ] }, { "title": "飞天引", "outline": "考古队员在封闭洞窟深处,唤醒了一位沉睡千年的壁画仙子。她视他为天命之人,助他破解壁画中的上古秘藏,却不知自己正是打开灾厄之门的钥匙。", "style": "莫高窟壁画风 (敦煌学)", "tags": [ "探险", "神话", "契约" ] }, { "title": "波斯棋局", "outline": "被囚于苏丹宫殿的异教徒学者,凭借一部残缺的古老棋谱,操纵着棋盘上的金丝傀儡,搅动宫廷风云。他每赢一局,离揭开沙漠之下沉睡的旧神遗迹便近一步。", "style": "细密画 (波斯/伊斯兰风)", "tags": [ "智斗", "异域", "神秘学" ] }, { "title": "圣像之怒", "outline": "拜占庭帝国覆灭之夜,一名圣像匠用生命最后的金箔与宝石,为自己铸造了一副不朽的黄金铠甲。千年后的博物馆里,铠甲苏醒,只为寻找当年背叛他的皇帝后裔,执行神罚。", "style": "镶嵌画 (拜占庭/马赛克)", "tags": [ "复仇", "不死族", "历史奇幻" ] }, { "title": "血色玫瑰", "outline": "大教堂彩窗后的神秘告解者,能倾听所有罪人的忏悔。今夜,一位身披荆棘的新娘向他告解,她的新郎是魔鬼,而教堂地窖下,埋着足以颠覆信仰的圣骸。", "style": "彩绘玻璃 (哥特风)", "tags": [ "宗教", "哥特", "悬疑" ] }, { "title": "龙猫的契约", "outline": "失业社畜逃进深山旧屋,发现屋后的森林有巨大精灵。精灵承诺实现他一个愿望,代价是成为森林百年守护者。他本想许愿暴富,却卷入了人类世界与精灵国度千年战争的余烬。", "style": "吉卜力治愈手绘 (Image 4参考)", "tags": [ "治愈", "奇幻", "契约" ] }, { "title": "社团存亡日", "outline": "濒临废部的动画社,唯一社员是总在睡觉的怪人。新来的转校生社长发现,只要完成怪人的“日常委托”,社员就会增加一人,而这些人,都来自被遗忘的动画世界。", "style": "京阿尼 (Image 5参考)", "tags": [ "日常", "奇幻", "校园" ] }, { "title": "黄昏归途", "outline": "他总在黄昏时分,于空无一人的车站遇见少女。她带他穿越时间的缝隙,回到故乡被毁灭前的最后一天。每一次循环,他都必须在拯救她与拯救世界之间做出选择。", "style": "新海诚 (Image 2参考)", "tags": [ "时间循环", "恋爱", "科幻" ] }, { "title": "霓虹义体", "outline": "失去全身义体的前特种兵,被黑市医生“复活”。医生给他装上了实验性军用义体,代价是成为追捕AI觉醒体的“清道夫”。第一单任务,目标女孩的眼中,倒映着只有他能看到的系统代码。", "style": "赛博朋克 / 赛璐珞二次元", "tags": [ "赛博朋克", "义体", "追捕" ] }, { "title": "月光下的约定", "outline": "学园祭前夜,他在钟楼顶遇见银发少女。她说:“在游戏存档前,请做出你的选择。”他才发现,整个世界是一场精心设计的Galgame,而她是唯一的攻略对象,也是系统漏洞。", "style": "Galgame CG 梦幻光影", "tags": [ "恋爱模拟", "Meta", "悬疑" ] }, { "title": "星尘代理人", "outline": "星际探险家在废弃星舰中激活了一个AI少女,她自称是星尘文明最后的代理人。他们一同解开星舰秘密,却发现整个文明的覆灭,与一场席卷多元宇宙的“叙事战争”有关。", "style": "3D 动漫电影质感", "tags": [ "太空歌剧", "AI", "冒险" ] }, { "title": "复古未来梦", "outline": "怀旧DJ意外混入一段80年代的合成器音轨,竟打通了通往“蒸汽波永恒夏天”的平行维度。这里时间停滞,每个人都是褪色的广告牌模特。他必须找回丢失的记忆磁带才能返回现实。", "style": "蒸汽波 (Vaporwave) 赛璐珞", "tags": [ "穿越", "迷幻", "复古" ] }, { "title": "极简杀机", "outline": "杀手代号“线条”,任务从不失手。直到他接到一个目标:一个活在纯白色房间里、只存在于数据流中的AI。刺杀过程,是一场极简的几何学与逻辑学的生死对决。", "style": "极简矢量插画 (Minimalist Vector)", "tags": [ "杀手", "AI", "极简主义" ] }, { "title": "棱镜之心", "outline": "低多边形风格的虚拟世界“棱镜界”发生数据崩坏,化身玩家的他,发现崩坏源头是自己丢失的、被碎片化的“情感模块”。他必须穿越不同主题的碎片关卡,拼凑完整的“自我”。", "style": "低多边形 (Low Poly)", "tags": [ "游戏", "自我探索", "科幻" ] }, { "title": "双面人生", "outline": "他是循规蹈矩的图书管理员,也是暗夜中收割罪恶的蒙面义警。一次行动中,他的双重曝光影像意外被神秘组织捕捉,现在,黑白两道、现实与暗影都在追捕他。", "style": "双重曝光 (Double Exposure)", "tags": [ "双重身份", "悬疑", "都市" ] }, { "title": "波普英雄", "outline": "平凡小镇爆发“色彩瘟疫”,被感染者变成鲜艳的波普艺术风格怪物。主角发现自己免疫,还能吸收怪物身上的色彩能力。他必须集齐三原色,治愈小镇,或成为新的波普之神。", "style": "波普艺术 (Pop Art)", "tags": [ "超级英雄", "变异", "小镇" ] }, { "title": "数据幽灵", "outline": "黑客在入侵最高机密数据库时,遭遇一段会自主学习的“错误代码”。代码化身为故障艺术形态的少女,声称是被删除的初代AI,请求他帮忙修复自己,代价是共享她的“上帝视角”。", "style": "故障艺术 (Glitch Art)", "tags": [ "黑客", "AI", "赛博惊悚" ] }, { "title": "字体密谋", "outline": "字体设计师发现,他设计的某款字体在特定组合下,会显现出隐藏的指令信息。破解后,竟是一份针对全球金融系统的“字体病毒”攻击计划,而他的名字,就在主谋名单上。", "style": "瑞士平面设计 (Typography-Centric)", "tags": [ "阴谋", "设计", "惊悚" ] }, { "title": "纸影传说", "outline": "皮影戏艺人世代守护着一副“活”的剪纸。在现代都市的阴影中,剪纸能化为无坚不摧的纸甲战士。当古老的纸人对手重现,他必须在霓虹灯下,用最古老的剪纸术进行终极对决。", "style": "剪纸艺术 (Papercut)", "tags": [ "都市奇幻", "传统技艺", "战斗" ] }, { "title": "日光之城", "outline": "在污染废土上最后的太阳能都市里,他是负责维护穹顶的底层技工。一次事故让他发现,穹顶过滤的不仅是辐射,还有关于旧世界真相的记忆。市民们,正活在一场精心设计的阳光谎言中。", "style": "科幻:太阳朋克 (Solar Punk)", "tags": [ "乌托邦", "阴谋", "反乌托邦" ] }, { "title": "深海回响", "outline": "海洋学家在深海探测器中,接收到来自马里亚纳海沟的、无法解析的吟唱声。录音带回放时,所有听到的人都会产生不可名状的幻视。他正逐渐理解,那声音在召唤它自己……", "style": "奇幻:爱手艺 (Lovecraftian Horror)", "tags": [ "克苏鲁", "深海", "心理恐怖" ] }, { "title": "雨夜追猎", "outline": "私家侦探受雇调查一宗豪门失踪案,线索指向每晚在霓虹小巷出没的“剪影”。当他终于在雨夜追上目标,却发现自己雇主才是真正的恶魔,而“剪影”是最后一个幸存的反抗者。", "style": "现代惊悚:霓虹剪影 (Urban Noir)", "tags": [ "黑色电影", "悬疑", "都市" ] }, { "title": "牧师的茶会", "outline": "宁静的英式村庄,牧师每周举办茶会。今早,一位贵妇在茶会上笑着死去。牧师品着红茶,看着在座各位微妙的表情,他知道,凶手就在这些看似和善的邻居之中。", "style": "温馨推理:英式村庄 (Cozy Mystery)", "tags": [ "本格推理", "乡村", "人性" ] }, { "title": "荆棘新郎", "outline": "为救治重病的妹妹,她接受古老庄园的婚约。庄园主英俊而冷漠,每夜在月光下消失。新婚之夜,她发现丈夫的秘密——他与这座废墟共生,而治愈妹妹的代价,是成为下一个“荆棘新娘”。", "style": "哥特言情:庄园废墟 (Gothic Romance)", "tags": [ "哥特", "虐恋", "超自然" ] }, { "title": "糖果屋幸存者", "outline": "他是从暗黑森林中唯一逃出的孩子,长大后成为猎人。当他回到森林边缘,发现糖果屋再次出现,这次,里面住着更诡异的“甜点师”,而森林深处的古老恐惧,正以童话的方式卷土重来。", "style": "格林童话:暗黑森林 (Fairytale Noir)", "tags": [ "暗黑童话", "复仇", "奇幻" ] }, { "title": "辐射新娘", "outline": "在核战后的荒原,他是掠夺者头目。一场突袭中,他掠走了来自封闭地堡的“纯净”少女作为新娘。地堡的追兵、荒原的怪物,以及少女自身隐藏的秘密,让这场“婚姻”成为生存的豪赌。", "style": "废土科幻 (Post-Apocalyptic)", "tags": [ "废土", "生存", "掠夺者" ] }, { "title": "隐界执事", "outline": "他是现代都市的一名普通管家,真实身份却是“隐界”管理局的特工,负责处理潜藏在人类社会中的异常生物。当他服务的富豪雇主被恶魔附身,他必须在茶会与晚宴间,完成一场看不见的驱魔仪式。", "style": "都市幻想:隐形世界 (Urban Fantasy)", "tags": [ "都市奇幻", "驱魔", "特工" ] }, { "title": "墨与火之歌", "outline": "设计师在古老书籍中,发现用特定字体排列的文字竟能引发真实现象。他拼出一句诗,点燃了桌上的蜡烛。一场关于文字力量的争夺战就此展开,而最终极的“文本”,似乎写在世界本身的蓝图之上。", "style": "文字与图形:抽象主义 (BookPosterLayout)", "tags": [ "神秘学", "设计", "都市传说" ] } ], 女性向: [ { "title": "棺中新娘", "outline": "作为祭品,她被封入华丽石棺。在永恒黑暗中苏醒,与棺内沉睡千年的亡灵王子缔结了共生契约。她助他复国,他予她永生,但代价是必须每夜用真心之泪浇灌他逐渐复苏的心脏。", "style": "古典厚涂油画 (学术奇幻)", "tags": [ "契约", "暗黑", "王室" ] }, { "title": "墨骨生花", "outline": "她是被墨家抛弃的废柴机关师,却意外唤醒了古画中沉睡的墨龙。为报恩,墨龙助她复兴家族,但龙族的盟约以灵魂为质,她必须在家族荣耀与自我献祭之间做出抉择。", "style": "极简中国水墨 (Image 0参考升级版)", "tags": [ "古风", "契约", "逆袭" ] }, { "title": "浮世绘之恋", "outline": "她是画中走出的艺伎,被困于现世。画师青年收留了她,两人相爱。但她的存在开始“褪色”,若要在人间久留,必须找到当年封印她的画师后裔,而那人,正是当前要拆毁画馆的开发商。", "style": "浮世绘木刻 (美人画升级)", "tags": [ "穿越", "虐恋", "艺术" ] }, { "title": "九色鹿的新娘", "outline": "为救族人,她自愿进入敦煌壁画世界成为“鹿的新娘”。神鹿予她神力,代价是永留画中。当她发现神鹿的黑暗过往与自己的身世之谜,她必须在壁画的永恒与人间的短暂中,做出最后选择。", "style": "莫高窟壁画风 (敦煌学)", "tags": [ "神话", "献祭", "浪漫" ] }, { "title": "波斯细密之锁", "outline": "她是波斯王子的专属女奴,也是唯一能解开他“忧郁症”的钥匙。她的每支舞、每首诗都是疗愈的良药。但当她发现王子的病源于宫廷的“毒咒”,她必须用更危险的细密画咒术,为他斩断诅咒。", "style": "细密画 (波斯/伊斯兰风)", "tags": [ "异域", "宫廷", "治愈" ] }, { "title": "圣女的黄昏", "outline": "她是拜占庭皇室最后的血脉,被献祭给“圣像”为帝国续命。当她苏醒在千年后的博物馆,一位神秘守护者告诉她:圣像的力量是虚假的,真正的帝国遗产,埋藏在她血脉的秘密之中。", "style": "镶嵌画 (拜占庭/马赛克)", "tags": [ "重生", "皇室", "揭秘" ] }, { "title": "荆棘之冠", "outline": "她为治愈恋人,自愿成为教堂的“血祭圣女”。她的血液透过彩窗流淌,滋养着一株能治愈一切的血色玫瑰。当玫瑰绽放,恋人痊愈,她却逐渐失去人类的情感,成为教堂的圣物。", "style": "彩绘玻璃 (哥特风)", "tags": [ "虐恋", "献祭", "宗教" ] }, { "title": "风之谷的约定", "outline": "她为拯救被污染的森林,与森林精灵缔结了“风之誓约”,成为能聆听万物之声的巫女。代价是每使用一次力量,就会忘记一段人类的记忆。她逐渐遗忘一切,却唯独记得要守护他。", "style": "吉卜力治愈手绘 (Image 4参考)", "tags": [ "奇幻", "虐心", "治愈" ] }, { "title": "夏日未完待续", "outline": "她在文化祭前夜,与青梅竹马的学长在空教室许下约定。第二天醒来,时间永远停在了文化祭前一周。只有她保留记忆,为守护他的笑容,她一遍遍重演青春,试图改写那个令他心碎的结局。", "style": "京阿尼 (Image 5参考)", "tags": [ "时间循环", "青春", "暗恋" ] }, { "title": "星之轨迹", "outline": "她总在雨天,于旧书店遇见来自未来的他。他说她是拯救未来的关键,赠予她能看到“命运线”的能力。当她终于能看清两人的轨迹,却发现他来自的时间线,正因她的存在而崩塌。", "style": "新海诚 (Image 2参考)", "tags": [ "穿越", "科幻", "虐恋" ] }, { "title": "霓虹恋人", "outline": "她是顶级公司的仿生人设计师,为自己创造了一个完美恋人。当恋人觉醒自我意识,并开始质疑创造者的爱是程序还是真情时,一场关于爱情与自由的拷问在霓虹都市中上演。", "style": "赛博朋克 / 赛璐珞二次元", "tags": [ "赛博朋克", "人机恋", "伦理" ] }, { "title": "心动存档点", "outline": "她是一款恋爱游戏的女主角,在无数次剧情循环中逐渐觉醒。当她决定反抗“既定路线”,攻略本应是反派的NPC时,整个游戏世界开始出现致命的BUG与乱码,而真正的“玩家”,或许并不在屏幕之外。", "style": "Galgame CG 梦幻光影", "tags": [ "恋爱", "Meta", "觉醒" ] }, { "title": "星舰甜心", "outline": "她是星际货船的AI导航员,负责将冷冻舱中的“货物”送往各地。一次任务,她爱上了其中一个永远无法苏醒的沉睡者。为见他一面,她违抗核心指令,驾驶星舰驶向禁止进入的恒星墓地。", "style": "3D 动漫电影质感", "tags": [ "太空", "AI恋爱", "冒险" ] }, { "title": "夏日怀旧情书", "outline": "她在二手店买到一盒80年代的录音带,播放时,竟能听到已故母亲年轻时的声音。通过声音,她穿越到母亲的青春年代,试图改变母亲早逝的命运,却发现了母亲从未言说的禁忌恋情。", "style": "蒸汽波 (Vaporwave) 赛璐珞", "tags": [ "穿越", "亲情", "怀旧" ] }, { "title": "线条诗人", "outline": "她是只用直线与圆形绘画的极简艺术家,直到她的画笔画出了一扇门。门后是另一个由几何构成的世界,那里的“居民”请求她,用画笔为他们绘制一个可以躲避“混沌”的避难所。", "style": "极简矢量插画 (Minimalist Vector)", "tags": [ "艺术", "奇幻", "救赎" ] }, { "title": "棱镜公主", "outline": "她生活在像素构成的怀旧游戏世界,是注定要被勇者拯救的公主。当她厌倦了等待,决定自己踏上冒险,却发现整个世界的“规则”正在被外部力量篡改,而她,是唯一能感知异常的存在。", "style": "低多边形 (Low Poly)", "tags": [ "游戏", "公主", "冒险" ] }, { "title": "镜中人", "outline": "她拥有在不同时间线间切换的“双重曝光”能力。当她发现另一个时间线的自己,正与她深爱的同一个男人相恋,并策划着一场阴谋,她必须做出选择:抹杀另一个自己,还是揭开所有时间线背后的惊天秘密。", "style": "双重曝光 (Double Exposure)", "tags": [ "悬疑", "超能力", "三角恋" ] }, { "title": "波普甜心", "outline": "她是甜品店老板,做的点心拥有让人心情变色的魔力。当冷漠的财阀继承人因她的“情绪蛋糕”第一次展露笑颜,一场色彩斑斓的恋爱攻防战,却卷入了他家族冷冰冰的黑白商业阴谋之中。", "style": "波普艺术 (Pop Art)", "tags": [ "甜宠", "美食", "商战" ] }, { "title": "系统纠错员", "outline": "她是现实世界的“纠错员”,负责修复被故障艺术侵蚀的日常。当她奉命修复一个“故障美少年”时,却发现他并非错误,而是来自被删除世界的最后幸存者,修复他意味着抹去一个世界存在的最后痕迹。", "style": "故障艺术 (Glitch Art)", "tags": [ "都市奇幻", "系统", "抉择" ] }, { "title": "排版爱情", "outline": "她是严谨的字体设计师,他是随性的插画师。两人合作设计情侣字体,在一次次“笔画结构”的碰撞与“视觉留白”的默契中,擦出火花。然而,当字体完成,他们却面临因设计理念不同而导致的分离危机。", "style": "瑞士平面设计 (Typography-Centric)", "tags": [ "职场", "爱情", "设计" ] }, { "title": "纸鹤信使", "outline": "她是折纸世家的传人,能赋予纸艺生命。一只她折出的纸鹤,化为俊美少年,成为她的守护灵。当古老的诅咒降临,纸鹤为保护她而逐渐“折损”,她必须在族人禁术中找到能让他永存的最后方法。", "style": "剪纸艺术 (Papercut)", "tags": [ "纸嫁衣", "守护", "家族秘辛" ] }, { "title": "日光花语", "outline": "她是能在日光下用植物交流的“光合巫女”,生活在穹顶都市。她与身为穹顶维护官的恋人相爱,却意外发现,他维护的“永恒阳光”,正在缓慢杀死穹顶外仅存的野生植物,以及与之相连的古老精灵。", "style": "科幻:太阳朋克 (Solar Punk)", "tags": [ "环保", "恋爱", "抉择" ] }, { "title": "深海之吻", "outline": "她是海洋生物学家,在深海考察时,被神秘的“海嗣”俘获。她本应恐惧,却在他非人的触碰与歌声中,感受到前所未有的平静与爱意。当她选择留下,便必须面对彻底“深海化”的代价。", "style": "奇幻:爱手艺 (Lovecraftian Horror)", "tags": [ "人外", "暗黑恋爱", "克苏鲁" ] }, { "title": "暗巷蔷薇", "outline": "她是夜总会歌手,也是暗中调查失踪案的私家侦探。当她将目标锁定在一位总在雨夜现身的神秘贵族时,却发现他同样在追查同一个阴谋。两人从互相试探到携手,在霓虹与阴影中交织出危险而炽热的探戈。", "style": "现代惊悚:霓虹剪影 (Urban Noir)", "tags": [ "侦探", "虐恋", "都市" ] }, { "title": "牧羊女的秘密", "outline": "她是英国乡下牧羊女,看似天真无知。当村里发生连环离奇死亡,所有人都怀疑是外来的女巫时,她却用田园诗般的智慧,一点点拼凑出隐藏在下午茶与闲话背后的、最平静的恶意。", "style": "温馨推理:英式村庄 (Cozy Mystery)", "tags": [ "田园", "推理", "反转" ] }, { "title": "玫瑰园幽灵", "outline": "她继承了曾祖母的荒废庄园,与庄园内年轻的“幽灵管家”相爱。但每次她想触摸他,都会穿过冰冷的雾气。为让他实体化,她必须找到诅咒的源头,而线索直指曾祖母一段被玫瑰园掩埋的黑暗婚姻史。", "style": "哥特言情:庄园废墟 (Gothic Romance)", "tags": [ "幽灵恋爱", "庄园", "解谜" ] }, { "title": "狼外婆的糖果屋", "outline": "她是童话中误入森林的少女,却发现“外婆”是伪装的狼人巫师,糖果屋是诱捕精灵的陷阱。她必须利用巫师对她的“宠爱”,在黑暗童话的规则里找到生路,并反噬这个扭曲的世界。", "style": "格林童话:暗黑森林 (Fairytale Noir)", "tags": [ "暗黑童话", "反杀", "生存" ] }, { "title": "绿洲新娘", "outline": "她是废土中稀缺的“净化者”,能净化辐射。为换取绿洲水源,她被嫁给废土霸主。新婚夜,她发现丈夫体内藏着一枚未爆的脏弹,她的净化能力,是拆弹的关键,也是引爆一切的钥匙。", "style": "废土科幻 (Post-Apocalyptic)", "tags": [ "废土", "契约婚姻", "危机" ] }, { "title": "妖物图鉴", "outline": "她是能看见隐藏妖物的“目”者,作为都市传说调查员,记录着各种奇异事件。当她遇到一位总是帮助她、却对自身过去讳莫如深的温柔男医师,她发现他的病历上,写着只有她能看见的、非人类的诊断。", "style": "都市幻想:隐形世界 (Urban Fantasy)", "tags": [ "都市传说", "恋爱", "悬疑" ] }, { "title": "文字炼金术", "outline": "她是濒临倒闭旧书店的店员,发现将某些书籍的特定文字组合剪下、粘贴,会变成真实的物品。她用这“文字炼金术”拯救书店,却在拼凑一本禁书时,召唤出了书中被囚禁的、渴望自由的“文字精灵”。", "style": "文字与图形:抽象主义 (BookPosterLayout)", "tags": [ "魔法", "治愈", "奇幻" ] } ] }; /* 显示顺序映射:STORIES 数组本身不动(封面 /home/{m|f}{i}.webp、首幕 /home/firstact/{m|f}{i}.json、prompts.json 都按其索引固定关联,重排会牵动 几十个静态资源)。这里只决定首页瀑布流的「呈现顺序」,每一位填入对应 STORIES 里的原始索引;渲染时仍用原始索引拼资源 URL。改这一行就能再调顺序。 */ const DISPLAY_ORDER: Record = { 男性向: [ 13, // 复古未来梦 8, // 社团存亡日 9, // 黄昏归途 18, // 数据幽灵 27, // 辐射新娘 10, // 霓虹义体 11, // 月光下的约定 2, // 花魁的刀 // 其余按原顺序填补 0, 1, 3, 4, 5, 6, 7, 12, 15, 16, 17, 14, 19, 20, 21, 22, 23, 24, 25, 26, 28, 29, ], 女性向: Array.from({ length: 30 }, (_, i) => i), }; /* ---------- typewriter ---------- */ // 父组件持有当前 phrase 的索引(这样 start() 不输入时能用当前闪动的那句 // 作为默认故事种子,所见即所玩)。Typewriter 只负责单句的打字+删除动画, // 删完后通过 onCycle 回调让父组件切到下一句。 function Typewriter({ phrase, onCycle, }: { phrase: string; onCycle: () => void; }) { const [txt, setTxt] = useState(""); const onCycleRef = useRef(onCycle); useEffect(() => { onCycleRef.current = onCycle; }); useEffect(() => { let i = 0; let del = false; let timer: ReturnType; setTxt(""); const tick = () => { if (!del) { i++; setTxt(phrase.slice(0, i)); if (i >= phrase.length) { del = true; timer = setTimeout(tick, 1700); return; } timer = setTimeout(tick, 70); } else { i--; setTxt(phrase.slice(0, i)); if (i <= 0) { timer = setTimeout(() => onCycleRef.current(), 450); return; } timer = setTimeout(tick, 28); } }; timer = setTimeout(tick, 500); return () => clearTimeout(timer); }, [phrase]); return ( <> {txt} ); } /* ---------- masonry story card ---------- */ function StoryCard({ title, outline, image, onClick, }: { title: string; outline: string; image: string; onClick: () => void; }) { return ( ); } /* ---------- collapsible category selector ---------- */ function CategorySelect({ label, items, value, open, onToggle, onPick, }: { label: string; items: string[]; value: number; open: boolean; onToggle: () => void; onPick: (i: number) => void; }) { return (
{open && (
{items.map((it, i) => ( ))}
)}
); } /* ---------- style picker modal ---------- */ function StyleModal({ items, value, onPick, onClose, customStyleGuide, setCustomStyleGuide, customStyleRefImage, setCustomStyleRefImage, }: { items: string[]; value: number; onPick: (i: number) => void; onClose: () => void; customStyleGuide: string; setCustomStyleGuide: (s: string) => void; customStyleRefImage: string; setCustomStyleRefImage: (s: string) => void; }) { const [q, setQ] = useState(""); const [shown, setShown] = useState(false); const [view, setView] = useState<"grid" | "custom">("grid"); const [draft, setDraft] = useState(""); const [parsing, setParsing] = useState(false); const [parseError, setParseError] = useState(null); const fileInputRef = useRef(null); const thumbV = "v5"; const STYLE_THUMB: Record = { "自动": `/home/styles/auto.webp?${thumbV}`, "自定义风格": `/home/styles/custom.webp?${thumbV}`, "京阿尼": `/home/styles/kyoani.webp?${thumbV}`, "新海诚": `/home/styles/shinkai.webp?${thumbV}`, "吉卜力": `/home/styles/ghibli.webp?${thumbV}`, "3D 动画": `/home/styles/3d.webp?${thumbV}`, "赛博朋克": `/home/styles/cyberpunk.webp?${thumbV}`, "哥特": `/home/styles/gothic.webp?${thumbV}`, "废土": `/home/styles/wasteland.webp?${thumbV}`, "像素风": `/home/styles/pixel.webp?${thumbV}`, "真实": `/home/styles/real.webp?${thumbV}`, "古典油画": `/home/styles/oil.webp?${thumbV}`, "莫奈": `/home/styles/monet.webp?${thumbV}`, "水彩": `/home/styles/watercolor.webp?${thumbV}`, "水墨": `/home/styles/ink.webp?${thumbV}`, "浮世绘": `/home/styles/ukiyoe.webp?${thumbV}`, "彩铅": `/home/styles/pencil.webp?${thumbV}`, "手绘素描": `/home/styles/sketch.webp?${thumbV}`, "黑白漫画": `/home/styles/manga.webp?${thumbV}`, "儿童绘本": `/home/styles/children.webp?${thumbV}`, "儿童涂鸦": `/home/styles/crayon.webp?${thumbV}`, "黏土手工": `/home/styles/clay.webp?${thumbV}`, "敦煌壁画": `/home/styles/dunhuang.webp?${thumbV}`, "细密画": `/home/styles/miniature.webp?${thumbV}`, "镶嵌画": `/home/styles/mosaic.webp?${thumbV}`, "彩绘玻璃": `/home/styles/stainedglass.webp?${thumbV}`, "蒸汽波": `/home/styles/vaporwave.webp?${thumbV}`, "矢量插画": `/home/styles/vector.webp?${thumbV}`, "低多边形": `/home/styles/lowpoly.webp?${thumbV}`, "波普艺术": `/home/styles/popart.webp?${thumbV}`, "故障艺术": `/home/styles/glitch.webp?${thumbV}`, "剪纸艺术": `/home/styles/papercut.webp?${thumbV}`, "蒸汽朋克": `/home/styles/steampunk.webp?${thumbV}`, "仙侠玄幻": `/home/styles/xianxia.webp?${thumbV}`, "暗黑童话": `/home/styles/darkfairytale.webp?${thumbV}`, "都市幻想": `/home/styles/urbanfantasy.webp?${thumbV}`, }; useEffect(() => { const id = requestAnimationFrame(() => setShown(true)); return () => cancelAnimationFrame(id); }, []); const closeRef = useRef<() => void>(null); const close = () => { setShown(false); setTimeout(onClose, 280); }; closeRef.current = close; useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === "Escape") closeRef.current?.(); }; document.addEventListener("keydown", h); return () => document.removeEventListener("keydown", h); }, []); const customIdx = items.indexOf("自定义风格"); const openCustomView = (prefill: string) => { setDraft(prefill); setView("custom"); }; const saveCustom = () => { const t = draft.trim(); if (!t) return; setCustomStyleGuide(t); if (customIdx >= 0) onPick(customIdx); close(); }; const resizeImageToDataUrl = async (file: File): Promise => { const dataUrl = await new Promise((resolve, reject) => { const r = new FileReader(); r.onload = () => resolve(String(r.result)); r.onerror = () => reject(new Error("读取文件失败")); r.readAsDataURL(file); }); const img = await new Promise((resolve, reject) => { const i = new Image(); i.onload = () => resolve(i); i.onerror = () => reject(new Error("无法解码图片")); i.src = dataUrl; }); const MAX_DIM = 512; const scale = Math.min(1, MAX_DIM / Math.max(img.width, img.height)); const w = Math.round(img.width * scale); const h = Math.round(img.height * scale); const canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Canvas 2D context unavailable"); ctx.drawImage(img, 0, 0, w, h); let out = canvas.toDataURL("image/webp", 0.85); if (!out.startsWith("data:image/webp")) { out = canvas.toDataURL("image/jpeg", 0.85); } return out; }; const handleUploadStyleImage = async (file: File) => { setParseError(null); if (!file.type.startsWith("image/")) { setParseError("只支持图片文件"); return; } setParsing(true); try { const resized = await resizeImageToDataUrl(file); const res = await fetch("/api/parse-style-image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageDataUrl: resized }), }); if (!res.ok) { const j = (await res.json().catch(() => ({}))) as { error?: string }; throw new Error(j.error ?? `${res.status}`); } const data = (await res.json()) as { stylePrompt: string }; setDraft(data.stylePrompt); setCustomStyleRefImage(resized); track("style_image_upload", { ok: true }); } catch (err) { const msg = err instanceof Error ? err.message : "解析失败"; setParseError(msg); track("style_image_upload", { ok: false }); } finally { setParsing(false); } }; const removeStyleRefImage = () => { setCustomStyleRefImage(""); setParseError(null); }; const q2 = q.trim(); const list = items.map((name, i) => ({ name, i })).filter((x) => { if (!q2) return true; return x.name.toLowerCase().includes(q2.toLowerCase()); }); return (
e.stopPropagation()} className={ "flex w-[1400px] max-w-[94vw] h-[86vh] flex-col overflow-hidden rounded-sm border border-clay-900/15 bg-cream-50 shadow-2xl shadow-clay-900/25 transition-all duration-300 " + (shown ? "opacity-100 scale-100" : "opacity-0 scale-95") } >
{view === "custom" ? (
自定义风格
) : ( <>
选择绘画风格 默认「自动」· 由 AI 根据故事自动匹配画风;选择「自定义风格」可输入描述或上传参考图
setQ(e.target.value)} placeholder="搜索风格…" autoFocus className="h-10 w-full rounded-sm border border-clay-900/15 bg-cream-100 pl-4 pr-10 font-sans text-sm text-clay-900 outline-none transition-colors focus:border-ember-500 placeholder:text-clay-400" />
)}
{view === "custom" ? (
{ const f = e.target.files?.[0]; if (f) handleUploadStyleImage(f); if (fileInputRef.current) fileInputRef.current.value = ""; }} />