新增角色大廳(lobby)邀約與通話、附身(possession)、時鐘(clock)抽象、 時間流與睡眠負債、後日談進度、行為選擇與旁觀反應等模組,並補齊對應冒煙 測試(R~V)與單元測試;同步調整 web/mobile 對應頁面與元件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
97 lines
3.7 KiB
TypeScript
97 lines
3.7 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { createRoom, listCharacters, type CharacterSummary, type RoomMode } from "../../lib/api";
|
||
|
||
const USER_ID = "seed-user-primary";
|
||
|
||
// K-7 建房介面:建立後導向 /room/[roomId](T19-P2 起的通用房間畫面,含 SSE/立繪舞台/語音/靜音,
|
||
// 見 T19-P3),不再自己維護一份較舊、功能較少的房間顯示邏輯。
|
||
export function RoomView() {
|
||
const router = useRouter();
|
||
const [characters, setCharacters] = useState<CharacterSummary[]>([]);
|
||
const [selectedCharacterIds, setSelectedCharacterIds] = useState<string[]>([]);
|
||
const [mode, setMode] = useState<RoomMode>("GROUP");
|
||
const [topicSeed, setTopicSeed] = useState("");
|
||
const [pending, setPending] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
void listCharacters().then(setCharacters);
|
||
}, []);
|
||
|
||
async function handleCreateRoom() {
|
||
if (selectedCharacterIds.length === 0 || pending) return;
|
||
setPending(true);
|
||
setError(null);
|
||
try {
|
||
const created = await createRoom(USER_ID, selectedCharacterIds, mode, topicSeed || undefined);
|
||
router.push(`/room/${created.id}`);
|
||
} catch {
|
||
setError("房間建立失敗,請確認 api 服務是否已啟動");
|
||
setPending(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-3 rounded-2xl border p-4" style={{ borderColor: "var(--line)" }}>
|
||
<h2 className="text-sm font-bold" style={{ color: "var(--ink)" }}>
|
||
建立房間
|
||
</h2>
|
||
<div className="flex flex-wrap gap-2 text-xs">
|
||
{characters.map((character) => {
|
||
const checked = selectedCharacterIds.includes(character.id);
|
||
return (
|
||
<label
|
||
key={character.id}
|
||
className="flex items-center gap-1 rounded-full border px-2 py-1"
|
||
style={{ borderColor: "var(--line)", color: "var(--ink)" }}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={checked}
|
||
onChange={() =>
|
||
setSelectedCharacterIds((prev) => (checked ? prev.filter((id) => id !== character.id) : [...prev, character.id]))
|
||
}
|
||
/>
|
||
{character.aliases[0]?.formalName ?? character.id}
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
<div className="flex items-center gap-3 text-xs">
|
||
<label className="flex items-center gap-1" style={{ color: "var(--ink)" }}>
|
||
<input type="radio" checked={mode === "GROUP"} onChange={() => setMode("GROUP")} />
|
||
群聊
|
||
</label>
|
||
<label className="flex items-center gap-1" style={{ color: "var(--ink)" }}>
|
||
<input type="radio" checked={mode === "SELF_CHAT"} onChange={() => setMode("SELF_CHAT")} />
|
||
角色自聊(旁觀)
|
||
</label>
|
||
</div>
|
||
<input
|
||
value={topicSeed}
|
||
onChange={(event) => setTopicSeed(event.target.value)}
|
||
placeholder="話題種子(選填)"
|
||
className="rounded-full border bg-transparent px-3 py-1.5 text-xs"
|
||
style={{ borderColor: "var(--line)", color: "var(--ink)" }}
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => void handleCreateRoom()}
|
||
disabled={pending || selectedCharacterIds.length === 0}
|
||
className="self-start rounded-full px-4 py-1.5 text-xs font-bold text-white disabled:opacity-50"
|
||
style={{ background: "var(--accent-bright)" }}
|
||
>
|
||
建立房間
|
||
</button>
|
||
{error && (
|
||
<p className="text-xs" style={{ color: "var(--accent)" }}>
|
||
{error}
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|