feat: add guarded image generation controls
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useLayoutEffect, useRef, useState, type ClipboardEvent } from 'react';
|
||||
import { BrainCircuit, Database } from 'lucide-react';
|
||||
import { BrainCircuit, Database, Image, ImageOff, ImagePlus } from 'lucide-react';
|
||||
import { useNetworkStatus } from '../hooks/useNetworkStatus';
|
||||
import { openAvatarPicker } from '../utils/userAvatar';
|
||||
import { CHAT_SKILL_OPTIONS, filterChatSkills } from '../utils/chatSkills';
|
||||
import { getMessageSaveActions } from '../utils/messageSave';
|
||||
import { getDisplayText } from '../utils/message';
|
||||
import {
|
||||
IMAGE_GENERATION_MODE_LABELS,
|
||||
nextImageGenerationMode,
|
||||
type ImageGenerationMode,
|
||||
} from '../utils/imageGeneration';
|
||||
import {
|
||||
downloadChatMessageDocx,
|
||||
} from '../api/client';
|
||||
@@ -186,6 +191,7 @@ export function ChatPanel({
|
||||
messageId?: string;
|
||||
forceDeepReasoning?: boolean;
|
||||
pgRequired?: boolean;
|
||||
imageGenerationMode?: ImageGenerationMode;
|
||||
selectedChatSkill?: string;
|
||||
fileAttachments?: ChatFileAttachment[];
|
||||
},
|
||||
@@ -219,6 +225,7 @@ export function ChatPanel({
|
||||
const [voiceNotice, setVoiceNotice] = useState<string | null>(null);
|
||||
const [forceDeepReasoning, setForceDeepReasoning] = useState(false);
|
||||
const [pgRequired, setPgRequired] = useState(false);
|
||||
const [imageGenerationMode, setImageGenerationMode] = useState<ImageGenerationMode>('auto');
|
||||
const [chatControlOnboardingStep, setChatControlOnboardingStep] = useState<0 | 1 | 2 | null>(null);
|
||||
const [pendingImages, setPendingImages] = useState<PendingChatImage[]>([]);
|
||||
const [pendingFiles, setPendingFiles] = useState<PendingChatFile[]>([]);
|
||||
@@ -745,6 +752,7 @@ export function ChatPanel({
|
||||
messageId: outgoingMessageId,
|
||||
forceDeepReasoning,
|
||||
pgRequired,
|
||||
imageGenerationMode,
|
||||
selectedChatSkill,
|
||||
fileAttachments: fileAttachmentsToSend,
|
||||
});
|
||||
@@ -774,6 +782,7 @@ export function ChatPanel({
|
||||
}, [
|
||||
forceDeepReasoning,
|
||||
pgRequired,
|
||||
imageGenerationMode,
|
||||
input,
|
||||
onSubmit,
|
||||
onUploadFile,
|
||||
@@ -1229,6 +1238,27 @@ export function ChatPanel({
|
||||
<span className="chat-control-onboarding-tip" role="status">需要长期使用的数据,放进专属空间</span>
|
||||
)}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className={`chat-deep-reasoning-toggle chat-image-generation-toggle is-${imageGenerationMode}${imageGenerationMode === 'required' ? ' is-active' : ''}`}
|
||||
disabled={voiceDisabled}
|
||||
role="checkbox"
|
||||
aria-checked={imageGenerationMode === 'auto' ? 'mixed' : imageGenerationMode === 'required'}
|
||||
aria-label={`AI 配图:${IMAGE_GENERATION_MODE_LABELS[imageGenerationMode]}`}
|
||||
title={`AI 配图:${IMAGE_GENERATION_MODE_LABELS[imageGenerationMode]}。点击切换为${IMAGE_GENERATION_MODE_LABELS[nextImageGenerationMode(imageGenerationMode)]}`}
|
||||
onClick={() => setImageGenerationMode((current) => nextImageGenerationMode(current))}
|
||||
>
|
||||
{imageGenerationMode === 'required' ? (
|
||||
<ImagePlus className="chat-deep-reasoning-toggle-icon" aria-hidden="true" />
|
||||
) : imageGenerationMode === 'disabled' ? (
|
||||
<ImageOff className="chat-deep-reasoning-toggle-icon" aria-hidden="true" />
|
||||
) : (
|
||||
<Image className="chat-deep-reasoning-toggle-icon" aria-hidden="true" />
|
||||
)}
|
||||
<span className="chat-image-generation-state" aria-hidden="true">
|
||||
{imageGenerationMode === 'auto' ? 'A' : imageGenerationMode === 'required' ? '✓' : '×'}
|
||||
</span>
|
||||
</button>
|
||||
<label
|
||||
className={`chat-deep-reasoning-toggle${forceDeepReasoning ? ' is-active' : ''}${chatControlOnboardingStep === 2 ? ' chat-control-onboarding-active' : ''}`}
|
||||
title="勾选后,本轮消息使用深度推理完成"
|
||||
|
||||
@@ -548,6 +548,7 @@ export function ChatView({
|
||||
messageId: options?.messageId,
|
||||
forceDeepReasoning: options?.forceDeepReasoning,
|
||||
pgRequired: options?.pgRequired,
|
||||
imageGenerationMode: options?.imageGenerationMode,
|
||||
selectedChatSkill: options?.selectedChatSkill,
|
||||
fileAttachments: options?.fileAttachments,
|
||||
},
|
||||
|
||||
@@ -148,6 +148,7 @@ export function SpaceChatPanel({
|
||||
messageId: options?.messageId,
|
||||
forceDeepReasoning: options?.forceDeepReasoning,
|
||||
pgRequired: options?.pgRequired,
|
||||
imageGenerationMode: options?.imageGenerationMode,
|
||||
fileAttachments: options?.fileAttachments,
|
||||
},
|
||||
imageUrls,
|
||||
@@ -160,6 +161,7 @@ export function SpaceChatPanel({
|
||||
messageId: options?.messageId,
|
||||
forceDeepReasoning: options?.forceDeepReasoning,
|
||||
pgRequired: options?.pgRequired,
|
||||
imageGenerationMode: options?.imageGenerationMode,
|
||||
fileAttachments: options?.fileAttachments,
|
||||
},
|
||||
imageUrls,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../api/client';
|
||||
import type { AgentRun } from '../api/client';
|
||||
import type { ChatState, Message, MindSpaceChatContext, PortalUser, Session, SessionEvent } from '../types';
|
||||
import type { ImageGenerationMode } from '../utils/imageGeneration';
|
||||
import { buildContextPrefix } from '../utils/mindspaceChatContext';
|
||||
import { buildUserAddressPrefix } from '../utils/userAddress';
|
||||
import {
|
||||
@@ -307,7 +308,7 @@ export function usePageEditSubChat({
|
||||
const submit = useCallback(
|
||||
async (
|
||||
text: string,
|
||||
context: MindSpaceChatContext & { messageId?: string },
|
||||
context: MindSpaceChatContext & { messageId?: string; imageGenerationMode?: ImageGenerationMode },
|
||||
imageUrls?: string[],
|
||||
previewImageUrls?: string[],
|
||||
) => {
|
||||
@@ -335,6 +336,15 @@ export function usePageEditSubChat({
|
||||
imageUrls: normalizedImageUrls,
|
||||
previewImageUrls: normalizedPreviewImageUrls,
|
||||
});
|
||||
userMessage.metadata = {
|
||||
...userMessage.metadata,
|
||||
memindRun: {
|
||||
...(userMessage.metadata?.memindRun && typeof userMessage.metadata.memindRun === 'object'
|
||||
? userMessage.metadata.memindRun
|
||||
: {}),
|
||||
imageGenerationMode: context.imageGenerationMode ?? 'auto',
|
||||
},
|
||||
};
|
||||
const requestId = crypto.randomUUID();
|
||||
activeRequestId.current = requestId;
|
||||
messagesRef.current = [...messagesRef.current, userMessage];
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
readStoredSessionId,
|
||||
writeStoredSessionId,
|
||||
} from '../utils/sessionStorage';
|
||||
import type { ImageGenerationMode } from '../utils/imageGeneration';
|
||||
import type {
|
||||
CapabilityMap,
|
||||
ChatFileAttachment,
|
||||
@@ -1396,6 +1397,7 @@ export function useTKMindChat(
|
||||
messageId?: string;
|
||||
forceDeepReasoning?: boolean;
|
||||
pgRequired?: boolean;
|
||||
imageGenerationMode?: ImageGenerationMode;
|
||||
selectedChatSkill?: string;
|
||||
fileAttachments?: ChatFileAttachment[];
|
||||
},
|
||||
@@ -1446,6 +1448,7 @@ export function useTKMindChat(
|
||||
: {}),
|
||||
sessionMessageCount: priorMessageCount,
|
||||
...(options?.pgRequired ? { pgRequired: true } : {}),
|
||||
imageGenerationMode: options?.imageGenerationMode ?? 'auto',
|
||||
...(options?.selectedChatSkill ? { selectedChatSkill: options.selectedChatSkill } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3177,6 +3177,37 @@ body,
|
||||
background: rgba(121, 183, 255, 0.14);
|
||||
}
|
||||
|
||||
.chat-image-generation-toggle {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-image-generation-toggle.is-disabled {
|
||||
color: var(--color-text-tertiary);
|
||||
border-color: color-mix(in srgb, var(--color-border-input) 72%, transparent);
|
||||
background: color-mix(in srgb, var(--color-bg-elevated) 82%, transparent);
|
||||
}
|
||||
|
||||
.chat-image-generation-toggle:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.chat-image-generation-state {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
bottom: 1px;
|
||||
min-width: 10px;
|
||||
height: 10px;
|
||||
padding: 0 1px;
|
||||
border-radius: 5px;
|
||||
color: currentColor;
|
||||
background: var(--color-bg-elevated);
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
line-height: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat-deep-reasoning-toggle:has(input:disabled) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export type ImageGenerationMode = 'auto' | 'required' | 'disabled';
|
||||
|
||||
export const IMAGE_GENERATION_MODE_LABELS: Record<ImageGenerationMode, string> = {
|
||||
auto: '自动识别',
|
||||
required: '强制开启',
|
||||
disabled: '关闭',
|
||||
};
|
||||
|
||||
export function nextImageGenerationMode(mode: ImageGenerationMode): ImageGenerationMode {
|
||||
if (mode === 'auto') return 'required';
|
||||
if (mode === 'required') return 'disabled';
|
||||
return 'auto';
|
||||
}
|
||||
Reference in New Issue
Block a user