Enhance DebugDrawer and DebugVoicePanel with unsaved changes handling

- Add `hasUnsavedChanges` prop to `DebugDrawer` to indicate unsaved changes in the AssistantPage.
- Update `DebugVoicePanel` to display appropriate messages and disable actions when there are unsaved changes.
- Refactor related components to ensure consistent handling of unsaved changes across the AssistantPage, improving user experience during interactions.
This commit is contained in:
Xin Wang
2026-07-07 20:16:30 +08:00
parent c5db918830
commit c51a70e134

View File

@@ -1302,6 +1302,7 @@ export function AssistantPage(props: AssistantPageProps) {
<DebugDrawer
assistantId={editingId}
asSheet
hasUnsavedChanges={dirty}
onNodeActive={setActiveNodeId}
/>
</SheetContent>
@@ -1403,7 +1404,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
</div>
<DebugDrawer assistantId={editingId} />
<DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
</div>
</div>
);
@@ -1517,7 +1518,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
</div>
<DebugDrawer assistantId={editingId} />
<DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
</div>
</div>
);
@@ -1642,7 +1643,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
</div>
<DebugDrawer assistantId={editingId} />
<DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
</div>
</div>
);
@@ -1859,7 +1860,11 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
</div>
<DebugDrawer assistantId={editingId} vision={visionEnabled} />
<DebugDrawer
assistantId={editingId}
hasUnsavedChanges={dirty}
vision={visionEnabled}
/>
</div>
</div>
);
@@ -1934,11 +1939,13 @@ function SegmentedIconButton({
function DebugDrawer({
assistantId,
asSheet = false,
hasUnsavedChanges = false,
onNodeActive,
vision = false,
}: {
assistantId: string | null;
asSheet?: boolean;
hasUnsavedChanges?: boolean;
onNodeActive?: (nodeId: string | null) => void;
vision?: boolean;
}) {
@@ -2050,6 +2057,7 @@ function DebugDrawer({
assistantId={assistantId}
preview={preview}
camera={camera}
hasUnsavedChanges={hasUnsavedChanges}
vision={vision}
/>
</aside>
@@ -2180,6 +2188,7 @@ function DebugVoicePanel({
assistantId,
preview,
camera,
hasUnsavedChanges,
vision,
}: {
view: DebugView;
@@ -2188,6 +2197,7 @@ function DebugVoicePanel({
assistantId: string | null;
preview: VoicePreview;
camera: CameraPreview;
hasUnsavedChanges: boolean;
vision: boolean;
}) {
const {
@@ -2210,6 +2220,12 @@ function DebugVoicePanel({
const showIdleHub =
idleOrFailed &&
(view === "video" || (messages.length === 0 && inChatView));
const startBlockedMessage = !assistantId
? "请先保存助手,再开始对话。"
: hasUnsavedChanges
? "请先保存当前改动,再开始对话。"
: "";
const startDisabled = status === "connecting" || Boolean(startBlockedMessage);
function handleSendText() {
if (sendText(textDraft)) {
@@ -2218,12 +2234,13 @@ function DebugVoicePanel({
}
const startConversation = useCallback(async () => {
if (!assistantId || hasUnsavedChanges) return;
const videoStream = vision ? await camera.start() : null;
await connect({
visionEnabled: vision,
videoStream,
});
}, [camera, connect, vision]);
}, [assistantId, camera, connect, hasUnsavedChanges, vision]);
return (
<div className="flex min-h-0 flex-1 flex-col">
@@ -2234,7 +2251,7 @@ function DebugVoicePanel({
<DebugIdleHub
status={status}
error={error}
assistantId={assistantId}
message={startBlockedMessage}
connect={startConversation}
/>
) : (
@@ -2245,7 +2262,7 @@ function DebugVoicePanel({
<DebugIdleHub
status={status}
error={error}
assistantId={assistantId}
message={startBlockedMessage}
connect={startConversation}
/>
) : (
@@ -2307,6 +2324,8 @@ function DebugVoicePanel({
"连接失败,请确认后端已启动且助手已保存后重试。"
: !assistantId
? "请先保存助手,再开始语音预览。"
: hasUnsavedChanges
? "请先保存当前改动,再开始语音预览。"
: micWarning
? `${micWarning} 可接收助手播报,但无法发送语音。`
: recording
@@ -2316,7 +2335,7 @@ function DebugVoicePanel({
</div>
<Button
disabled={!assistantId || status === "connecting"}
disabled={recording ? status === "connecting" : startDisabled}
onClick={() => {
if (recording) {
disconnect();
@@ -2403,33 +2422,40 @@ function DebugVoicePanel({
</Button>
)}
{!showIdleHub && (
<Button
size="sm"
disabled={!assistantId || status === "connecting"}
onClick={() => {
if (recording) {
disconnect();
} else {
void startConversation();
}
}}
className={[
"h-10 shrink-0 gap-1.5 rounded-full px-4",
recording
? "bg-destructive text-white hover:bg-destructive/90"
: "",
].join(" ")}
aria-label={recording ? "结束语音测试" : "开始语音测试"}
>
{status === "connecting" ? (
<Loader2 size={14} className="animate-spin" />
) : recording ? (
<PhoneOff size={14} />
) : (
<Mic size={14} />
<div className="flex shrink-0 flex-col items-end gap-1">
<Button
size="sm"
disabled={recording ? status === "connecting" : startDisabled}
onClick={() => {
if (recording) {
disconnect();
} else {
void startConversation();
}
}}
className={[
"h-10 gap-1.5 rounded-full px-4",
recording
? "bg-destructive text-white hover:bg-destructive/90"
: "",
].join(" ")}
aria-label={recording ? "结束语音测试" : "开始语音测试"}
>
{status === "connecting" ? (
<Loader2 size={14} className="animate-spin" />
) : recording ? (
<PhoneOff size={14} />
) : (
<Mic size={14} />
)}
{recording ? "结束对话" : "开始对话"}
</Button>
{!recording && startBlockedMessage && (
<span className="max-w-40 text-right text-[11px] leading-4 text-destructive">
{startBlockedMessage}
</span>
)}
{recording ? "结束对话" : "开始对话"}
</Button>
</div>
)}
</div>
</div>
@@ -2449,18 +2475,25 @@ function DebugVoicePanel({
// 空闲态只保留一个明确入口,避免中间区域显得拥挤。
function DebugIdleHub({
status,
assistantId,
error,
message,
connect,
}: {
status: VoicePreviewStatus;
error: string | null;
assistantId: string | null;
message: string;
connect: () => Promise<void>;
}) {
const helperText =
message ||
(status === "failed"
? error || "连接失败,请确认后端已启动且助手已保存后重试。"
: "");
return (
<div className="flex min-h-0 flex-1 items-center justify-center px-6 py-8">
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 py-8 text-center">
<Button
disabled={!assistantId}
disabled={Boolean(message)}
onClick={() => void connect()}
className="h-11 gap-2 rounded-full px-6 text-sm font-medium shadow-sm"
aria-label={status === "failed" ? "重新连接" : "开始语音测试"}
@@ -2468,6 +2501,16 @@ function DebugIdleHub({
<Mic size={18} />
{status === "failed" ? "重新连接" : "开始对话"}
</Button>
{helperText && (
<p
className={[
"max-w-xs text-xs leading-5",
message ? "text-destructive" : "text-muted-foreground",
].join(" ")}
>
{helperText}
</p>
)}
</div>
);
}