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