新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
1.7 KiB
TypeScript
53 lines
1.7 KiB
TypeScript
import { VoicePlaybackButton } from "./VoicePlaybackButton";
|
||
|
||
interface ChatBubbleProps {
|
||
role: "user" | "character";
|
||
text: string;
|
||
characterId?: string;
|
||
userId?: string;
|
||
}
|
||
|
||
// H-3 動作描寫標記渲染:把 *…* 標記以暮空紫小字呈現,與台詞區分。
|
||
function renderContent(text: string) {
|
||
const parts = text.split(/(*[^*]+*)/g).filter(Boolean);
|
||
return parts.map((part, index) => {
|
||
const match = part.match(/^*([^*]+)*$/);
|
||
if (match) {
|
||
return (
|
||
<span key={index} className="text-xs" style={{ color: "var(--violet)" }}>
|
||
{match[1]}
|
||
</span>
|
||
);
|
||
}
|
||
return <span key={index}>{part}</span>;
|
||
});
|
||
}
|
||
|
||
export function ChatBubble({ role, text, characterId, userId }: ChatBubbleProps) {
|
||
const isUser = role === "user";
|
||
return (
|
||
<div className="flex max-w-[78%] flex-col" style={{ alignSelf: isUser ? "flex-end" : "flex-start", alignItems: isUser ? "flex-end" : "flex-start" }}>
|
||
<div
|
||
className="rounded-2xl px-3.5 py-2 text-sm leading-relaxed"
|
||
style={
|
||
isUser
|
||
? {
|
||
background: "var(--accent-bright)",
|
||
color: "#fff",
|
||
borderBottomRightRadius: 4,
|
||
}
|
||
: {
|
||
background: "var(--bg-soft)",
|
||
border: "1px solid var(--line)",
|
||
borderBottomLeftRadius: 4,
|
||
}
|
||
}
|
||
>
|
||
{renderContent(text)}
|
||
</div>
|
||
{/* P-6:語音播放介面只在角色訊息旁提供,使用者自己的訊息不需要播放。 */}
|
||
{!isUser && characterId && userId && <VoicePlaybackButton characterId={characterId} userId={userId} text={text} />}
|
||
</div>
|
||
);
|
||
}
|