diff --git a/src/components/CommandLinkManager.tsx b/src/components/CommandLinkManager.tsx index e8d2a92..77781a0 100644 --- a/src/components/CommandLinkManager.tsx +++ b/src/components/CommandLinkManager.tsx @@ -1,14 +1,15 @@ /** * CommandLinkManager — mounts as a child of
and intercepts * clicks on :cmd[] directive spans to dispatch them via the shared - * command dispatcher. + * command dispatcher. Errors are surfaced through the shared + * dispatchError signal, shown by JournalInput above the textarea. */ import { Component, onCleanup } from "solid-js"; import { useArticleDom } from "./Article"; import { useJournalStream } from "./stores/journalStream"; import { useJournalCompletions } from "./journal/completions"; -import { dispatchCommand } from "./journal/command-dispatcher"; +import { dispatchCommand, setDispatchError } from "./journal/command-dispatcher"; export const CommandLinkManager: Component = () => { const contentDom = useArticleDom(); @@ -26,7 +27,7 @@ export const CommandLinkManager: Component = () => { e.stopPropagation(); e.stopImmediatePropagation(); - void dispatchCommand({ + dispatchCommand({ role: stream.myRole as "gm" | "player" | "observer", myName: stream.myName, command, @@ -34,6 +35,10 @@ export const CommandLinkManager: Component = () => { statValues: stream.stats, statDefs: comp.data.stats, statTemplates: comp.data.statTemplates, + }).then((result) => { + if (!result.ok) { + setDispatchError(result.error); + } }); }; diff --git a/src/components/journal/JournalInput.tsx b/src/components/journal/JournalInput.tsx index e5f7867..3a8611b 100644 --- a/src/components/journal/JournalInput.tsx +++ b/src/components/journal/JournalInput.tsx @@ -20,7 +20,7 @@ import type { CompletionItem } from "./command-parser"; import { buildCompletions } from "./command-completions"; import { CompletionsDropdown } from "./CompletionsDropdown"; import { ErrorPopup } from "./ErrorPopup"; -import { dispatchCommand } from "./command-dispatcher"; +import { dispatchCommand, dispatchError, setDispatchError } from "./command-dispatcher"; // ---- Component ---- @@ -33,7 +33,6 @@ export const JournalInput: Component = () => { const isGm = () => stream.myRole === "gm"; const [text, setText] = createSignal(""); - const [error, setError] = createSignal(null); const [showErrorPopup, setShowErrorPopup] = createSignal(false); const [sending, setSending] = createSignal(false); const [showCompletions, setShowCompletions] = createSignal(false); @@ -80,11 +79,11 @@ export const JournalInput: Component = () => { const parsed = parseInput(raw); if (parsed.error) { - setError(parsed.error); + setDispatchError(parsed.error); return; } - setError(null); + setDispatchError(null); // Players: chat + stat commands only if (isPlayer()) { @@ -96,7 +95,7 @@ export const JournalInput: Component = () => { } if (parsed.type !== "stat") { - setError("玩家只能发送聊天消息或使用 /stat 命令"); + setDispatchError("玩家只能发送聊天消息或使用 /stat 命令"); return; } } @@ -104,7 +103,7 @@ export const JournalInput: Component = () => { // GM: all commands, Player: stat commands setSending(true); - const result = await dispatchCommand({ + await dispatchCommand({ role: stream.myRole as "gm" | "player" | "observer", myName: stream.myName, command: raw, @@ -114,15 +113,22 @@ export const JournalInput: Component = () => { statTemplates: comp.data.statTemplates, }); - finish(result.ok, result.ok ? undefined : result.error); + // dispatchCommand already set the shared error if it failed. + // Only clear text on success (no error is shown). + if (!dispatchError()) { + setText(""); + } + setSending(false); + textareaRef?.focus(); } - /** Clear text + error on success, or set error on failure. */ + /** Clear text or set shared dispatch error on failure. */ function finish(success: boolean, err?: string) { if (success) { setText(""); + setDispatchError(null); } else if (err) { - setError(err); + setDispatchError(err); } setSending(false); textareaRef?.focus(); @@ -269,13 +275,13 @@ export const JournalInput: Component = () => { />
- +
@@ -295,7 +301,7 @@ export const JournalInput: Component = () => { {/* Error popup */} setShowErrorPopup(false)} /> diff --git a/src/components/journal/command-dispatcher.ts b/src/components/journal/command-dispatcher.ts index 25a67d3..637848f 100644 --- a/src/components/journal/command-dispatcher.ts +++ b/src/components/journal/command-dispatcher.ts @@ -6,6 +6,7 @@ */ import { sendMessage } from "../stores/journalStream"; +import { createSignal } from "solid-js"; import { parseInput } from "./command-parser"; import { resolveRollPayload } from "./types/roll"; import { resolveSparkPayload } from "./types/spark"; @@ -26,6 +27,14 @@ export type DispatchResult = | { ok: true } | { ok: false; error: string }; +/** + * Shared signal for dispatch errors from any source (typed or cmd-link clicks). + * Components that show errors (JournalInput) read from here; callers that + * want errors surfaced (CommandLinkManager) write to it. + */ +export const [dispatchError, setDispatchError] = + createSignal(null); + // --------------------------------------------------------------------------- // Main dispatch // --------------------------------------------------------------------------- @@ -60,25 +69,25 @@ export async function dispatchCommand( // Observers can only send chat if (ctx.role === "observer") { const result = sendMessage("chat", { text: raw }); - return unwrap(result); + return finish(unwrap(result)); } const prefixed = raw.startsWith("/") ? raw : "/" + raw; const parsed = parseInput(prefixed); - if (parsed.error) return { ok: false, error: parsed.error }; + if (parsed.error) return finish({ ok: false, error: parsed.error }); // Players: chat + stat commands only if (ctx.role === "player") { if (parsed.type === "chat") { const result = sendMessage("chat", { text: raw }); - return unwrap(result); + return finish(unwrap(result)); } if (parsed.type === "stat") { - return dispatchStat(parsed.payload as Record, ctx); + return finish(dispatchStat(parsed.payload as Record, ctx)); } - return { ok: false, error: "玩家只能发送聊天消息或使用 /stat 命令" }; + return finish({ ok: false, error: "玩家只能发送聊天消息或使用 /stat 命令" }); } // GM: all commands @@ -87,7 +96,7 @@ export async function dispatchCommand( parsed.payload as { notation: string; label?: string }, ); const result = sendMessage("roll", p); - return unwrap(result); + return finish(unwrap(result)); } if (parsed.type === "spark") { @@ -98,21 +107,27 @@ export async function dispatchCommand( const remix = match?.remix ?? false; const p = await resolveSparkPayload({ key, csvPath, remix }); const result = sendMessage("spark", p); - return unwrap(result); + return finish(unwrap(result)); } catch (e) { - return { + return finish({ ok: false, error: e instanceof Error ? e.message : "种子表掷骰失败", - }; + }); } } if (parsed.type === "stat") { - return dispatchStat(parsed.payload as Record, ctx); + return finish(dispatchStat(parsed.payload as Record, ctx)); } const result = sendMessage(parsed.type, parsed.payload); - return unwrap(result); + return finish(unwrap(result)); +} + +/** Update the shared error signal and return the result (pass-through). */ +function finish(r: DispatchResult): DispatchResult { + setDispatchError(r.ok ? null : r.error); + return r; } // ---------------------------------------------------------------------------