1497 lines
62 KiB
TypeScript
1497 lines
62 KiB
TypeScript
"use client";
|
|
|
|
import type { ChatStatus } from "ai";
|
|
import {
|
|
BrainCircuitIcon,
|
|
BookMarkedIcon,
|
|
CheckIcon,
|
|
FileTextIcon,
|
|
FolderTreeIcon,
|
|
GraduationCapIcon,
|
|
LightbulbIcon,
|
|
PaperclipIcon,
|
|
PlusIcon,
|
|
ShrinkIcon,
|
|
SparklesIcon,
|
|
RocketIcon,
|
|
XIcon,
|
|
ZapIcon,
|
|
} from "lucide-react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type ComponentProps,
|
|
type ReactNode,
|
|
} from "react";
|
|
|
|
import {
|
|
PromptInput,
|
|
PromptInputActionMenu,
|
|
PromptInputActionMenuContent,
|
|
PromptInputActionMenuItem,
|
|
PromptInputActionMenuTrigger,
|
|
PromptInputAttachment,
|
|
PromptInputAttachments,
|
|
PromptInputBody,
|
|
PromptInputButton,
|
|
PromptInputFooter,
|
|
PromptInputSubmit,
|
|
PromptInputTextarea,
|
|
PromptInputTools,
|
|
usePromptInputAttachments,
|
|
usePromptInputController,
|
|
type PromptInputMessage,
|
|
} from "@/components/ai-elements/prompt-input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ConfettiButton } from "@/components/ui/confetti-button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
DropdownMenuGroup,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { apiFetch } from "@/core/api";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
import { useI18n } from "@/core/i18n/hooks";
|
|
import { isHiddenFromUIMessage } from "@/core/messages/utils";
|
|
import { useModels } from "@/core/models/hooks";
|
|
import type { Model } from "@/core/models/types";
|
|
import type { AgentThreadContext } from "@/core/threads";
|
|
import { textOfMessage } from "@/core/threads/utils";
|
|
import { desensitize } from "@/lib/desensitize";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { AgentSelector } from "@/components/workspace/agents/agent-selector";
|
|
|
|
import {
|
|
ModelSelector,
|
|
ModelSelectorContent,
|
|
ModelSelectorInput,
|
|
ModelSelectorItem,
|
|
ModelSelectorList,
|
|
ModelSelectorName,
|
|
ModelSelectorTrigger,
|
|
} from "../ai-elements/model-selector";
|
|
import { Suggestion, Suggestions } from "../ai-elements/suggestion";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "../ui/dropdown-menu";
|
|
|
|
import { useRecommendedQuestions } from "@/core/recommended-questions/hooks";
|
|
import { useUserPreferences } from "@/core/user-preferences";
|
|
import {
|
|
isThreadRagModeLocked,
|
|
lockThreadRagMode,
|
|
useLocalSettings,
|
|
} from "@/core/settings";
|
|
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
|
|
|
import { CustomPromptBar } from "./custom-prompt-menu";
|
|
import { useThread } from "./messages/context";
|
|
import { ModeHoverGuide } from "./mode-hover-guide";
|
|
import { Tooltip } from "./tooltip";
|
|
|
|
type InputMode = "flash" | "thinking" | "pro" | "ultra";
|
|
|
|
|
|
function getResolvedMode(
|
|
mode: InputMode | undefined,
|
|
supportsThinking: boolean,
|
|
): InputMode {
|
|
if (!supportsThinking && mode !== "flash") {
|
|
return "flash";
|
|
}
|
|
if (mode) {
|
|
return mode;
|
|
}
|
|
return supportsThinking ? "pro" : "flash";
|
|
}
|
|
|
|
export function InputBox({
|
|
className,
|
|
disabled,
|
|
autoFocus,
|
|
status = "ready",
|
|
context,
|
|
extraHeader,
|
|
isNewThread,
|
|
threadId,
|
|
initialValue,
|
|
onContextChange,
|
|
onFollowupsVisibilityChange,
|
|
onSubmit,
|
|
onStop,
|
|
promptPrefix,
|
|
agentSelector,
|
|
knowledgeSelector,
|
|
extraModels = [],
|
|
assistantId,
|
|
hideAdminRecommendedQuestions,
|
|
compact,
|
|
hideContextTools,
|
|
hideMemoryTool = true,
|
|
...props
|
|
}: Omit<ComponentProps<typeof PromptInput>, "onSubmit"> & {
|
|
assistantId?: string | null;
|
|
status?: ChatStatus;
|
|
/** 紧凑模式(正常 App)新对话落地态:去掉旋转彩边、用 CM 参考图的白卡输入框,
|
|
* 并在推荐问题上方加「试试这些高频业务场景」标题。仅落地态生效,默认 false。 */
|
|
compact?: boolean;
|
|
/** 隐藏输入框工具条上的「写作 / 记忆 / 参考文献」上下文开关按钮(任务深链工作区等
|
|
* 受控场景用;文件/模型等其他按钮不受影响)。 */
|
|
hideContextTools?: boolean;
|
|
/**
|
|
* 仅控制「记忆」开关的可见性。产品当前全局隐藏,传入 false 即可无损恢复。
|
|
* 写作和参考文献开关不受影响。
|
|
*/
|
|
hideMemoryTool?: boolean;
|
|
disabled?: boolean;
|
|
context: Omit<
|
|
AgentThreadContext,
|
|
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
|
|
> & {
|
|
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
|
|
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
|
writing_mode?: boolean;
|
|
memory_injection_enabled?: boolean;
|
|
};
|
|
extraHeader?: React.ReactNode;
|
|
isNewThread?: boolean;
|
|
/** Suppress the global admin "recommended questions" row. The agent chat page
|
|
* sets this so only the agent's own common questions surface on its home. */
|
|
hideAdminRecommendedQuestions?: boolean;
|
|
threadId: string;
|
|
initialValue?: string;
|
|
onContextChange?: (
|
|
context: Omit<
|
|
AgentThreadContext,
|
|
"thread_id" | "is_plan_mode" | "thinking_enabled" | "subagent_enabled"
|
|
> & {
|
|
mode: "flash" | "thinking" | "pro" | "ultra" | undefined;
|
|
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
|
writing_mode?: boolean;
|
|
memory_injection_enabled?: boolean;
|
|
},
|
|
) => void;
|
|
onFollowupsVisibilityChange?: (visible: boolean) => void;
|
|
onSubmit?: (message: PromptInputMessage) => void;
|
|
onStop?: () => void;
|
|
/**
|
|
* Optional toggle rendered next to the model selector. Used for the
|
|
* admin-configurable prompt-prefix switch on the chat input. Omitting the
|
|
* prop hides the button entirely.
|
|
*/
|
|
promptPrefix?: {
|
|
label: string;
|
|
on: boolean;
|
|
onToggle: () => void;
|
|
};
|
|
/**
|
|
* Optional agent picker shown alongside the model selector. Only the new-chat
|
|
* homepage (``ChatPage``) passes this; ``AgentChatPage`` omits it because the
|
|
* thread is already bound to one agent.
|
|
*/
|
|
agentSelector?: {
|
|
selectedAgentId: string | null;
|
|
onAgentChange: (agentId: string | null) => void;
|
|
};
|
|
/** WeKnora-style knowledge-space picker rendered inside the composer toolbar. */
|
|
knowledgeSelector?: ReactNode;
|
|
/** Page-scoped model choices implemented outside the DeerFlow model API. */
|
|
extraModels?: Model[];
|
|
}) {
|
|
const { t } = useI18n();
|
|
const [localSettings, setLocalSettings] = useLocalSettings();
|
|
const searchParams = useSearchParams();
|
|
const [modelDialogOpen, setModelDialogOpen] = useState(false);
|
|
const { models } = useModels();
|
|
const singleRoundRagActive =
|
|
context.execution_profile === "single_round_rag";
|
|
const multiRoundRagActive =
|
|
context.execution_profile === "multi_round_rag";
|
|
const frontendRagActive = singleRoundRagActive || multiRoundRagActive;
|
|
const modelOptions = useMemo(() => {
|
|
return singleRoundRagActive ? extraModels : models;
|
|
}, [extraModels, models, singleRoundRagActive]);
|
|
const { thread, isMock } = useThread();
|
|
const { textInput } = usePromptInputController();
|
|
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
|
const { data: citationDisplay } = useCitationDisplaySettings();
|
|
const referenceModeEntryEnabled =
|
|
citationDisplay?.reference_mode_enabled ?? false;
|
|
|
|
const [followups, setFollowups] = useState<string[]>([]);
|
|
const [followupsHidden, setFollowupsHidden] = useState(false);
|
|
const [followupsLoading, setFollowupsLoading] = useState(false);
|
|
const lastGeneratedForAiIdRef = useRef<string | null>(null);
|
|
const wasStreamingRef = useRef(false);
|
|
|
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
const [pendingSuggestion, setPendingSuggestion] = useState<string | null>(
|
|
null,
|
|
);
|
|
|
|
const { questions: recommendedQuestions } = useRecommendedQuestions();
|
|
const userPreferences = useUserPreferences();
|
|
const showAdminRecommendedQuestions =
|
|
!hideAdminRecommendedQuestions &&
|
|
!userPreferences.hide_admin_prompt_prefix &&
|
|
recommendedQuestions.length > 0;
|
|
|
|
useEffect(() => {
|
|
// Multi-round RAG is a frontend execution profile, not a model option.
|
|
// Do not let the normal model fallback clear it on every render.
|
|
if (multiRoundRagActive) {
|
|
return;
|
|
}
|
|
if (modelOptions.length === 0) {
|
|
return;
|
|
}
|
|
const currentModel = modelOptions.find((m) => m.name === context.model_name);
|
|
const fallbackModel = currentModel ?? modelOptions[0]!;
|
|
const supportsThinking = fallbackModel.supports_thinking ?? false;
|
|
const nextModelName = fallbackModel.name;
|
|
const nextMode = getResolvedMode(context.mode, supportsThinking);
|
|
|
|
if (
|
|
context.model_name === nextModelName &&
|
|
context.mode === nextMode &&
|
|
context.execution_profile === fallbackModel.execution_profile
|
|
) {
|
|
return;
|
|
}
|
|
|
|
onContextChange?.({
|
|
...context,
|
|
model_name: nextModelName,
|
|
mode: nextMode,
|
|
execution_profile: fallbackModel.execution_profile,
|
|
});
|
|
}, [context, modelOptions, multiRoundRagActive, onContextChange]);
|
|
|
|
const selectedModel = useMemo(() => {
|
|
if (modelOptions.length === 0) {
|
|
return undefined;
|
|
}
|
|
return modelOptions.find((m) => m.name === context.model_name) ?? modelOptions[0];
|
|
}, [context.model_name, modelOptions]);
|
|
|
|
const resolvedModelName = selectedModel?.name;
|
|
const selectedSingleRoundRag = singleRoundRagActive;
|
|
|
|
const supportThinking = useMemo(
|
|
() => selectedModel?.supports_thinking ?? false,
|
|
[selectedModel],
|
|
);
|
|
|
|
const supportReasoningEffort = useMemo(
|
|
() => selectedModel?.supports_reasoning_effort ?? false,
|
|
[selectedModel],
|
|
);
|
|
|
|
const handleModelSelect = useCallback(
|
|
(model_name: string) => {
|
|
const model = modelOptions.find((m) => m.name === model_name);
|
|
if (!model) {
|
|
return;
|
|
}
|
|
onContextChange?.({
|
|
...context,
|
|
model_name,
|
|
mode: getResolvedMode(context.mode, model.supports_thinking ?? false),
|
|
reasoning_effort: context.reasoning_effort,
|
|
execution_profile: model.execution_profile,
|
|
});
|
|
setModelDialogOpen(false);
|
|
},
|
|
[onContextChange, context, modelOptions],
|
|
);
|
|
|
|
const handleModeSelect = useCallback(
|
|
(mode: InputMode) => {
|
|
const ordinaryModel =
|
|
models.find((model) => model.name === context.model_name) ?? models[0];
|
|
onContextChange?.({
|
|
...context,
|
|
model_name: ordinaryModel?.name ?? context.model_name,
|
|
mode: getResolvedMode(
|
|
mode,
|
|
ordinaryModel?.supports_thinking ?? supportThinking,
|
|
),
|
|
reasoning_effort:
|
|
mode === "ultra"
|
|
? "high"
|
|
: mode === "pro"
|
|
? "medium"
|
|
: mode === "thinking"
|
|
? "low"
|
|
: "minimal",
|
|
execution_profile: undefined,
|
|
});
|
|
},
|
|
[onContextChange, context, models, supportThinking],
|
|
);
|
|
|
|
const handleSingleRoundRagSelect = useCallback(() => {
|
|
const ragModel =
|
|
extraModels.find((model) => model.name === context.model_name) ??
|
|
extraModels[0];
|
|
if (!ragModel) {
|
|
return;
|
|
}
|
|
|
|
onContextChange?.({
|
|
...context,
|
|
model_name: ragModel.name,
|
|
mode: getResolvedMode(
|
|
context.mode,
|
|
ragModel.supports_thinking ?? false,
|
|
),
|
|
execution_profile: "single_round_rag",
|
|
});
|
|
}, [context, extraModels, onContextChange]);
|
|
|
|
const handleMultiRoundRagSelect = useCallback(() => {
|
|
onContextChange?.({
|
|
...context,
|
|
execution_profile: "multi_round_rag",
|
|
});
|
|
}, [context, onContextChange]);
|
|
|
|
const handleReasoningEffortSelect = useCallback(
|
|
(effort: "minimal" | "low" | "medium" | "high") => {
|
|
onContextChange?.({
|
|
...context,
|
|
reasoning_effort: effort,
|
|
});
|
|
},
|
|
[onContextChange, context],
|
|
);
|
|
|
|
const handleWritingModeToggle = useCallback(() => {
|
|
onContextChange?.({
|
|
...context,
|
|
writing_mode: !context.writing_mode,
|
|
});
|
|
}, [context, onContextChange]);
|
|
|
|
const artifactDirectoryModeEnabled =
|
|
localSettings.display.artifactDirectoryModeEnabled ?? false;
|
|
const handleArtifactDirectoryModeToggle = useCallback(() => {
|
|
setLocalSettings("display", {
|
|
artifactDirectoryModeEnabled: !artifactDirectoryModeEnabled,
|
|
});
|
|
}, [artifactDirectoryModeEnabled, setLocalSettings]);
|
|
|
|
const memoryInjectionEnabled = context.memory_injection_enabled ?? true;
|
|
const handleMemoryInjectionToggle = useCallback(() => {
|
|
onContextChange?.({
|
|
...context,
|
|
memory_injection_enabled: !memoryInjectionEnabled,
|
|
});
|
|
}, [context, memoryInjectionEnabled, onContextChange]);
|
|
|
|
const ragModeLocked = isThreadRagModeLocked(threadId);
|
|
const ragModeEnabled = localSettings.display.ragModeEnabled || ragModeLocked;
|
|
const handleRagModeToggle = useCallback(() => {
|
|
if (ragModeLocked) {
|
|
return;
|
|
}
|
|
setLocalSettings("display", { ragModeEnabled: !ragModeEnabled });
|
|
}, [ragModeEnabled, ragModeLocked, setLocalSettings]);
|
|
|
|
const contextCompressionEnabled =
|
|
localSettings.display.contextCompressionEnabled ?? false;
|
|
const handleContextCompressionToggle = useCallback(() => {
|
|
setLocalSettings("display", {
|
|
contextCompressionEnabled: !contextCompressionEnabled,
|
|
});
|
|
}, [contextCompressionEnabled, setLocalSettings]);
|
|
|
|
const handleSubmit = useCallback(
|
|
async (message: PromptInputMessage) => {
|
|
if (status === "streaming") {
|
|
onStop?.();
|
|
return;
|
|
}
|
|
if (!message.text) {
|
|
return;
|
|
}
|
|
setFollowups([]);
|
|
setFollowupsHidden(false);
|
|
setFollowupsLoading(false);
|
|
if (ragModeEnabled) {
|
|
lockThreadRagMode(threadId);
|
|
}
|
|
|
|
// Guard against submitting before the initial model auto-selection
|
|
// effect has flushed thread settings to storage/state.
|
|
if (resolvedModelName && context.model_name !== resolvedModelName) {
|
|
onContextChange?.({
|
|
...context,
|
|
model_name: resolvedModelName,
|
|
mode: getResolvedMode(
|
|
context.mode,
|
|
selectedModel?.supports_thinking ?? false,
|
|
),
|
|
execution_profile: selectedModel?.execution_profile,
|
|
});
|
|
setTimeout(() => onSubmit?.(message), 0);
|
|
return;
|
|
}
|
|
|
|
onSubmit?.(message);
|
|
},
|
|
[
|
|
context,
|
|
onContextChange,
|
|
onSubmit,
|
|
onStop,
|
|
resolvedModelName,
|
|
ragModeEnabled,
|
|
selectedModel?.supports_thinking,
|
|
selectedModel?.execution_profile,
|
|
status,
|
|
threadId,
|
|
],
|
|
);
|
|
|
|
const requestFormSubmit = useCallback(() => {
|
|
const form = promptRootRef.current?.querySelector("form");
|
|
form?.requestSubmit();
|
|
}, []);
|
|
|
|
const handleFollowupClick = useCallback(
|
|
(suggestion: string) => {
|
|
if (status === "streaming") {
|
|
return;
|
|
}
|
|
const current = (textInput.value ?? "").trim();
|
|
if (current) {
|
|
setPendingSuggestion(suggestion);
|
|
setConfirmOpen(true);
|
|
return;
|
|
}
|
|
textInput.setInput(suggestion);
|
|
setFollowupsHidden(true);
|
|
setTimeout(() => requestFormSubmit(), 0);
|
|
},
|
|
[requestFormSubmit, status, textInput],
|
|
);
|
|
|
|
const confirmReplaceAndSend = useCallback(() => {
|
|
if (!pendingSuggestion) {
|
|
setConfirmOpen(false);
|
|
return;
|
|
}
|
|
textInput.setInput(pendingSuggestion);
|
|
setFollowupsHidden(true);
|
|
setConfirmOpen(false);
|
|
setPendingSuggestion(null);
|
|
setTimeout(() => requestFormSubmit(), 0);
|
|
}, [pendingSuggestion, requestFormSubmit, textInput]);
|
|
|
|
const confirmAppendAndSend = useCallback(() => {
|
|
if (!pendingSuggestion) {
|
|
setConfirmOpen(false);
|
|
return;
|
|
}
|
|
const current = (textInput.value ?? "").trim();
|
|
const next = current
|
|
? `${current}\n${pendingSuggestion}`
|
|
: pendingSuggestion;
|
|
textInput.setInput(next);
|
|
setFollowupsHidden(true);
|
|
setConfirmOpen(false);
|
|
setPendingSuggestion(null);
|
|
setTimeout(() => requestFormSubmit(), 0);
|
|
}, [pendingSuggestion, requestFormSubmit, textInput]);
|
|
|
|
const showFollowups =
|
|
!disabled &&
|
|
!isNewThread &&
|
|
!selectedSingleRoundRag &&
|
|
!followupsHidden &&
|
|
(followupsLoading || followups.length > 0);
|
|
|
|
const followupsVisibilityChangeRef = useRef(onFollowupsVisibilityChange);
|
|
|
|
useEffect(() => {
|
|
followupsVisibilityChangeRef.current = onFollowupsVisibilityChange;
|
|
}, [onFollowupsVisibilityChange]);
|
|
|
|
useEffect(() => {
|
|
followupsVisibilityChangeRef.current?.(showFollowups);
|
|
}, [showFollowups]);
|
|
|
|
useEffect(() => {
|
|
return () => followupsVisibilityChangeRef.current?.(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const streaming = status === "streaming";
|
|
const wasStreaming = wasStreamingRef.current;
|
|
wasStreamingRef.current = streaming;
|
|
if (!wasStreaming || streaming) {
|
|
return;
|
|
}
|
|
|
|
if (disabled || isMock || selectedSingleRoundRag) {
|
|
return;
|
|
}
|
|
|
|
const visibleMessages = thread.messages.filter((m) => !isHiddenFromUIMessage(m));
|
|
const lastAi = [...visibleMessages].reverse().find((m) => m.type === "ai");
|
|
const lastAiId = lastAi?.id ?? null;
|
|
if (!lastAiId || lastAiId === lastGeneratedForAiIdRef.current) {
|
|
return;
|
|
}
|
|
lastGeneratedForAiIdRef.current = lastAiId;
|
|
|
|
const recent = visibleMessages
|
|
.filter((m) => m.type === "human" || m.type === "ai")
|
|
.map((m) => {
|
|
const role = m.type === "human" ? "user" : "assistant";
|
|
const content = textOfMessage(m) ?? "";
|
|
return { role, content };
|
|
})
|
|
.filter((m) => m.content.trim().length > 0)
|
|
.slice(-6);
|
|
|
|
if (recent.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
setFollowupsHidden(false);
|
|
setFollowupsLoading(true);
|
|
setFollowups([]);
|
|
|
|
apiFetch(`${getBackendBaseURL()}/api/threads/${threadId}/suggestions`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
messages: recent,
|
|
n: 3,
|
|
model_name: context.model_name ?? undefined,
|
|
}),
|
|
signal: controller.signal,
|
|
})
|
|
.then(async (res) => {
|
|
if (!res.ok) {
|
|
return { suggestions: [] as string[] };
|
|
}
|
|
return (await res.json()) as { suggestions?: string[] };
|
|
})
|
|
.then((data) => {
|
|
const suggestions = (data.suggestions ?? [])
|
|
.map((s) => (typeof s === "string" ? s.trim() : ""))
|
|
.filter((s) => s.length > 0)
|
|
.slice(0, 5);
|
|
setFollowups(suggestions);
|
|
})
|
|
.catch(() => {
|
|
setFollowups([]);
|
|
})
|
|
.finally(() => {
|
|
setFollowupsLoading(false);
|
|
});
|
|
|
|
return () => controller.abort();
|
|
}, [context.model_name, disabled, isMock, selectedSingleRoundRag, status, thread.messages, threadId]);
|
|
|
|
return (
|
|
<div ref={promptRootRef} className="relative flex flex-col gap-6">
|
|
{extraHeader && (
|
|
<div className="flex items-center justify-center">
|
|
{extraHeader}
|
|
</div>
|
|
)}
|
|
|
|
{isNewThread && searchParams.get("mode") !== "skill" && showAdminRecommendedQuestions && (
|
|
<div className="flex flex-wrap justify-center gap-2.5 px-4 mb-[10px]">
|
|
{recommendedQuestions.map((q) => (
|
|
<button
|
|
key={q.id}
|
|
type="button"
|
|
className="rounded-full border border-[#e4e4e7] bg-white px-4 py-2 text-sm text-[#18181b] shadow-none transition-colors hover:bg-[#f4f4f5] cursor-pointer dark:border-slate-600 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700"
|
|
onClick={() => {
|
|
textInput.setInput(q.content);
|
|
setFollowupsHidden(true);
|
|
setTimeout(() => requestFormSubmit(), 0);
|
|
}}
|
|
>
|
|
{desensitize(q.title)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{showFollowups && (
|
|
<div className="flex items-center justify-center pb-2">
|
|
<div className="flex items-center gap-2">
|
|
{followupsLoading ? (
|
|
<div className="text-muted-foreground bg-background/80 rounded-full border px-4 py-2 text-xs backdrop-blur-sm">
|
|
{t.inputBox.followupLoading}
|
|
</div>
|
|
) : (
|
|
<Suggestions className="min-h-16 w-fit items-start">
|
|
{followups.map((s) => (
|
|
<Suggestion
|
|
key={s}
|
|
suggestion={s}
|
|
onClick={() => handleFollowupClick(s)}
|
|
/>
|
|
))}
|
|
<Button
|
|
aria-label={t.common.close}
|
|
className="text-muted-foreground cursor-pointer rounded-full px-3 text-xs font-normal"
|
|
variant="outline"
|
|
size="sm"
|
|
type="button"
|
|
onClick={() => setFollowupsHidden(true)}
|
|
>
|
|
<XIcon className="size-4" />
|
|
</Button>
|
|
</Suggestions>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col gap-1.5">
|
|
{isNewThread && (
|
|
<CustomPromptBar
|
|
className="px-1"
|
|
currentAgentId={agentSelector?.selectedAgentId ?? assistantId ?? null}
|
|
onInsert={(content) => {
|
|
const current = textInput.value?.trim() ?? "";
|
|
textInput.setInput(current ? `${current}\n${content}` : content);
|
|
setTimeout(() => {
|
|
document
|
|
.querySelector<HTMLTextAreaElement>("textarea[name='message']")
|
|
?.focus();
|
|
}, 0);
|
|
}}
|
|
/>
|
|
)}
|
|
<div className={cn(isNewThread && !compact && "gemini-gradient-border gemini-shadow")}>
|
|
<PromptInput
|
|
className={cn(
|
|
"transition-all duration-300 ease-out",
|
|
// 紧凑落地态(CM 参考图):表单作为白卡,内部 input-group 透明无边框,
|
|
// 避免双层描边;圆角对齐 2xl。
|
|
compact && isNewThread
|
|
? "*:data-[slot='input-group']:rounded-2xl *:data-[slot='input-group']:border-0 *:data-[slot='input-group']:bg-transparent *:data-[slot='input-group']:shadow-none"
|
|
: "*:data-[slot='input-group']:rounded-3xl",
|
|
// 紧凑落地态(CM 参考图):白卡 + 细边 + 轻阴影 + 主色聚焦环(无旋转彩边)
|
|
compact && isNewThread
|
|
? "rounded-2xl border border-gray-200/70 bg-white overflow-hidden shadow-[0_4.5px_22px_rgba(0,0,0,0.025)] transition-all duration-300 focus-within:border-[rgb(var(--legacy-primary-rgb)/35%)] focus-within:ring-4 focus-within:ring-[rgb(var(--legacy-primary-rgb)/5%)] focus-within:shadow-[0_10px_35px_rgb(var(--legacy-primary-rgb)/4%)] dark:border-slate-700 dark:bg-slate-800"
|
|
: // landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边
|
|
// 对话态:常规白卡 + 中性边框 + 轻阴影
|
|
isNewThread
|
|
? "rounded-3xl bg-transparent border-0 shadow-none ring-0"
|
|
: "rounded-3xl bg-background border border-border shadow-sm transition-shadow hover:shadow-md focus-within:border-ring/40 focus-within:shadow-md focus-within:ring-1 focus-within:ring-ring/20 dark:bg-slate-800 dark:border-slate-700",
|
|
className,
|
|
)}
|
|
disabled={disabled}
|
|
globalDrop
|
|
multiple
|
|
onSubmit={handleSubmit}
|
|
{...props}
|
|
>
|
|
<PromptInputAttachments>
|
|
{(attachment) => <PromptInputAttachment data={attachment} />}
|
|
</PromptInputAttachments>
|
|
<PromptInputBody
|
|
className={cn(!compact && "absolute top-0 right-0 left-0 z-3")}
|
|
>
|
|
<PromptInputTextarea
|
|
className={cn(
|
|
"size-full px-2 py-1 text-[15px] leading-7 placeholder:text-muted-foreground/60",
|
|
isNewThread && "min-h-[110px]!",
|
|
compact && "min-h-[84px]! px-3 pt-3 pb-1 text-[14px]",
|
|
)}
|
|
disabled={disabled}
|
|
placeholder={t.inputBox.placeholder}
|
|
autoFocus={autoFocus}
|
|
defaultValue={initialValue}
|
|
/>
|
|
</PromptInputBody>
|
|
<PromptInputFooter
|
|
className={cn(
|
|
"flex items-center border-t-0 px-2 pt-1 pb-1.5",
|
|
compact && "gap-2 px-3 pt-1.5 pb-3",
|
|
)}
|
|
>
|
|
<PromptInputTools
|
|
className={cn("gap-1.5", compact && "flex-1 flex-wrap gap-2")}
|
|
>
|
|
{/* TODO: Add more connectors here
|
|
<PromptInputActionMenu>
|
|
<PromptInputActionMenuTrigger className="px-2!" />
|
|
<PromptInputActionMenuContent>
|
|
<PromptInputActionAddAttachments
|
|
label={t.inputBox.addAttachments}
|
|
/>
|
|
</PromptInputActionMenuContent>
|
|
</PromptInputActionMenu> */}
|
|
{!frontendRagActive && (
|
|
<AddAttachmentsButton
|
|
className={cn(
|
|
"rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
compact &&
|
|
"gap-1.5! rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
|
)}
|
|
/>
|
|
)}
|
|
{!frontendRagActive && !hideContextTools && <Tooltip
|
|
content={
|
|
context.writing_mode
|
|
? "写作模式已开启:报告由深度研究管线按「通用分析结构」撰写,优先使用对话中已检索的资料"
|
|
: "开启写作模式:对话照常进行,交付时由深度研究管线撰写报告并保存为 Markdown 进入画布"
|
|
}
|
|
>
|
|
<PromptInputButton
|
|
className={cn(
|
|
"gap-1.5! rounded-full px-3! shadow-none",
|
|
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
|
context.writing_mode
|
|
? "border border-amber-300 bg-amber-50 text-amber-700 ring-1 ring-amber-300/35 hover:bg-amber-100 dark:border-amber-300/70 dark:bg-amber-400/20 dark:text-amber-100 dark:ring-amber-300/35 dark:shadow-[0_0_0_1px_rgba(251,191,36,0.18),0_0_18px_rgba(251,191,36,0.18)] dark:hover:bg-amber-400/25"
|
|
: compact
|
|
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
)}
|
|
onClick={handleWritingModeToggle}
|
|
>
|
|
{context.writing_mode && (
|
|
<CheckIcon className="size-3 text-amber-700 dark:text-amber-100" />
|
|
)}
|
|
<FileTextIcon className="size-3" />
|
|
<span className="text-xs font-normal">写作</span>
|
|
</PromptInputButton>
|
|
</Tooltip>}
|
|
{!frontendRagActive && <Tooltip
|
|
content={
|
|
artifactDirectoryModeEnabled
|
|
? "文件模式已开启:右侧会显示当前对话生成的全部文件目录"
|
|
: "开启文件模式:在右侧目录中浏览当前对话生成的全部文件"
|
|
}
|
|
>
|
|
<PromptInputButton
|
|
type="button"
|
|
aria-pressed={artifactDirectoryModeEnabled}
|
|
disabled={disabled}
|
|
className={cn(
|
|
"gap-1.5! rounded-full px-3! shadow-none",
|
|
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
|
artifactDirectoryModeEnabled
|
|
? "border border-sky-300 bg-sky-50 text-sky-700 hover:bg-sky-100 dark:border-sky-600 dark:bg-sky-950/45 dark:text-sky-200"
|
|
: compact
|
|
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
)}
|
|
onClick={handleArtifactDirectoryModeToggle}
|
|
>
|
|
{artifactDirectoryModeEnabled && (
|
|
<CheckIcon className="size-3 text-sky-700 dark:text-sky-200" />
|
|
)}
|
|
<FolderTreeIcon className="size-3" />
|
|
<span className="text-xs font-normal">文件</span>
|
|
</PromptInputButton>
|
|
</Tooltip>}
|
|
{!frontendRagActive && !hideContextTools && !hideMemoryTool && <Tooltip
|
|
content={
|
|
memoryInjectionEnabled
|
|
? "记忆注入已开启:对话前会注入历史记忆"
|
|
: "记忆注入已关闭:本次对话不注入记忆"
|
|
}
|
|
>
|
|
<PromptInputButton
|
|
className={cn(
|
|
"gap-1.5! rounded-full px-3! shadow-none",
|
|
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
|
memoryInjectionEnabled
|
|
? "border border-blue-300 bg-blue-50 text-blue-700 ring-1 ring-blue-300/35 hover:bg-blue-100 dark:border-cyan-300/70 dark:bg-cyan-400/20 dark:text-cyan-50 dark:ring-cyan-300/35 dark:shadow-[0_0_0_1px_rgba(34,211,238,0.18),0_0_18px_rgba(34,211,238,0.18)] dark:hover:bg-cyan-400/25"
|
|
: compact
|
|
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
)}
|
|
onClick={handleMemoryInjectionToggle}
|
|
>
|
|
{memoryInjectionEnabled && (
|
|
<CheckIcon className="size-3 text-blue-700 dark:text-cyan-50" />
|
|
)}
|
|
<BrainCircuitIcon className="size-3" />
|
|
<span className="text-xs font-normal">记忆</span>
|
|
</PromptInputButton>
|
|
</Tooltip>}
|
|
{referenceModeEntryEnabled && !frontendRagActive && !hideContextTools && <Tooltip
|
|
content={
|
|
ragModeEnabled
|
|
? "参考文献模式已开启:检索类结果会按编号引用展示"
|
|
: "参考文献模式已关闭:点击开启"
|
|
}
|
|
>
|
|
<PromptInputButton
|
|
type="button"
|
|
aria-pressed={ragModeEnabled}
|
|
disabled={ragModeLocked || disabled}
|
|
className={cn(
|
|
"gap-1.5! rounded-full px-3! shadow-none",
|
|
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
|
ragModeLocked && "cursor-not-allowed opacity-80",
|
|
ragModeEnabled
|
|
? "border border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
|
|
: compact
|
|
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
)}
|
|
onClick={handleRagModeToggle}
|
|
>
|
|
{ragModeEnabled && (
|
|
<CheckIcon className="size-3 text-emerald-700 dark:text-emerald-200" />
|
|
)}
|
|
<BookMarkedIcon className="size-3" />
|
|
<span className="text-xs font-normal">参考文献</span>
|
|
</PromptInputButton>
|
|
</Tooltip>}
|
|
{!frontendRagActive && <Tooltip
|
|
content={
|
|
contextCompressionEnabled
|
|
? "上下文压缩已开启:超长对话会自动压缩历史以节省 token"
|
|
: "上下文压缩已关闭:点击开启"
|
|
}
|
|
>
|
|
<PromptInputButton
|
|
type="button"
|
|
aria-pressed={contextCompressionEnabled}
|
|
disabled={disabled}
|
|
className={cn(
|
|
"gap-1.5! rounded-full px-3! shadow-none",
|
|
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
|
contextCompressionEnabled
|
|
? "border border-violet-300 bg-violet-50 text-violet-700 hover:bg-violet-100 dark:border-violet-700 dark:bg-violet-950/40 dark:text-violet-200"
|
|
: compact
|
|
? "border border-transparent bg-muted/40 text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
)}
|
|
onClick={handleContextCompressionToggle}
|
|
>
|
|
{contextCompressionEnabled && (
|
|
<CheckIcon className="size-3 text-violet-700 dark:text-violet-200" />
|
|
)}
|
|
<ShrinkIcon className="size-3" />
|
|
<span className="text-xs font-normal">上下文压缩</span>
|
|
</PromptInputButton>
|
|
</Tooltip>}
|
|
<PromptInputActionMenu>
|
|
<ModeHoverGuide
|
|
mode={
|
|
context.mode === "flash" ||
|
|
context.mode === "thinking" ||
|
|
context.mode === "pro" ||
|
|
context.mode === "ultra"
|
|
? context.mode
|
|
: "flash"
|
|
}
|
|
tooltipContent={
|
|
singleRoundRagActive
|
|
? "单轮 RAG 问答:基于知识库检索生成答案,并展示参考资料"
|
|
: multiRoundRagActive
|
|
? "多轮 RAG 问答:多次检索并展示检索过程与参考资料"
|
|
: undefined
|
|
}
|
|
>
|
|
<PromptInputActionMenuTrigger
|
|
className={cn(
|
|
"gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
compact && "rounded-lg border px-2.5! py-1! text-[11px] font-medium",
|
|
compact &&
|
|
(frontendRagActive
|
|
? "border-emerald-200 bg-emerald-50/60 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
|
|
: context.mode === "thinking"
|
|
? "border-indigo-200 bg-indigo-50/60 text-indigo-700 hover:bg-indigo-100 dark:border-indigo-700 dark:bg-indigo-950/40 dark:text-indigo-200"
|
|
: context.mode === "pro"
|
|
? "border-violet-200 bg-violet-50/60 text-violet-700 hover:bg-violet-100 dark:border-violet-700 dark:bg-violet-950/40 dark:text-violet-200"
|
|
: context.mode === "ultra"
|
|
? "border-amber-200 bg-amber-50/60 text-amber-700 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200"
|
|
: "border-transparent bg-muted/40 hover:bg-muted hover:text-foreground"),
|
|
)}
|
|
>
|
|
<div>
|
|
{frontendRagActive ? (
|
|
<BookMarkedIcon className="size-3" />
|
|
) : context.mode === "flash" ? (
|
|
<ZapIcon className="size-3" />
|
|
) : context.mode === "thinking" ? (
|
|
<LightbulbIcon className="size-3" />
|
|
) : context.mode === "pro" ? (
|
|
<GraduationCapIcon className="size-3" />
|
|
) : context.mode === "ultra" ? (
|
|
<RocketIcon className="size-3 text-[#dabb5e]" />
|
|
) : null}
|
|
</div>
|
|
<div
|
|
className={cn(
|
|
"text-xs font-normal",
|
|
context.mode === "ultra" ? "golden-text" : "",
|
|
)}
|
|
>
|
|
{frontendRagActive
|
|
? multiRoundRagActive
|
|
? "多轮 RAG"
|
|
: "单轮 RAG"
|
|
: (context.mode === "flash" && t.inputBox.flashMode) ||
|
|
(context.mode === "thinking" &&
|
|
t.inputBox.reasoningMode) ||
|
|
(context.mode === "pro" && t.inputBox.proMode) ||
|
|
(context.mode === "ultra" && t.inputBox.ultraMode)}
|
|
</div>
|
|
</PromptInputActionMenuTrigger>
|
|
</ModeHoverGuide>
|
|
<PromptInputActionMenuContent className="w-80">
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
{t.inputBox.mode}
|
|
</DropdownMenuLabel>
|
|
<PromptInputActionMenu>
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
!frontendRagActive && context.mode === "flash"
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleModeSelect("flash")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
<ZapIcon
|
|
className={cn(
|
|
"mr-2 size-4",
|
|
!frontendRagActive && context.mode === "flash" &&
|
|
"text-accent-foreground",
|
|
)}
|
|
/>
|
|
{t.inputBox.flashMode}
|
|
</div>
|
|
<div className="pl-7 text-xs">
|
|
{t.inputBox.flashModeDescription}
|
|
</div>
|
|
</div>
|
|
{!frontendRagActive && context.mode === "flash" ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
{supportThinking && (
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
!frontendRagActive && context.mode === "thinking"
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleModeSelect("thinking")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
<LightbulbIcon
|
|
className={cn(
|
|
"mr-2 size-4",
|
|
!frontendRagActive && context.mode === "thinking" &&
|
|
"text-accent-foreground",
|
|
)}
|
|
/>
|
|
{t.inputBox.reasoningMode}
|
|
</div>
|
|
<div className="pl-7 text-xs">
|
|
{t.inputBox.reasoningModeDescription}
|
|
</div>
|
|
</div>
|
|
{!frontendRagActive && context.mode === "thinking" ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
)}
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
!frontendRagActive && context.mode === "pro"
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleModeSelect("pro")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
<GraduationCapIcon
|
|
className={cn(
|
|
"mr-2 size-4",
|
|
!frontendRagActive && context.mode === "pro" &&
|
|
"text-accent-foreground",
|
|
)}
|
|
/>
|
|
{t.inputBox.proMode}
|
|
</div>
|
|
<div className="pl-7 text-xs">
|
|
{t.inputBox.proModeDescription}
|
|
</div>
|
|
</div>
|
|
{!frontendRagActive && context.mode === "pro" ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
!frontendRagActive && context.mode === "ultra"
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleModeSelect("ultra")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
<RocketIcon
|
|
className={cn(
|
|
"mr-2 size-4",
|
|
!frontendRagActive &&
|
|
context.mode === "ultra" &&
|
|
"text-[#dabb5e]",
|
|
)}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
!frontendRagActive &&
|
|
context.mode === "ultra" &&
|
|
"golden-text",
|
|
)}
|
|
>
|
|
{t.inputBox.ultraMode}
|
|
</div>
|
|
</div>
|
|
<div className="pl-7 text-xs">
|
|
{t.inputBox.ultraModeDescription}
|
|
</div>
|
|
</div>
|
|
{!frontendRagActive && context.mode === "ultra" ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
{extraModels.length > 0 && (
|
|
<>
|
|
<DropdownMenuSeparator className="my-2" />
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
selectedSingleRoundRag
|
|
? "bg-emerald-50 text-emerald-800 hover:bg-emerald-100 focus:bg-emerald-100 dark:bg-emerald-950/40 dark:text-emerald-200 dark:hover:bg-emerald-950/60 dark:focus:bg-emerald-950/60"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={handleSingleRoundRagSelect}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
<BookMarkedIcon
|
|
className={cn(
|
|
"mr-2 size-4",
|
|
selectedSingleRoundRag && "text-emerald-700 dark:text-emerald-200",
|
|
)}
|
|
/>
|
|
单轮 RAG 问答
|
|
</div>
|
|
<div className="pl-7 text-xs">
|
|
基于知识库检索生成答案,并展示思考内容与参考资料
|
|
</div>
|
|
</div>
|
|
{selectedSingleRoundRag ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
</>
|
|
)}
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
multiRoundRagActive
|
|
? "bg-emerald-50 text-emerald-800 hover:bg-emerald-100 focus:bg-emerald-100 dark:bg-emerald-950/40 dark:text-emerald-200 dark:hover:bg-emerald-950/60 dark:focus:bg-emerald-950/60"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={handleMultiRoundRagSelect}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
<BookMarkedIcon
|
|
className={cn(
|
|
"mr-2 size-4",
|
|
multiRoundRagActive && "text-emerald-700 dark:text-emerald-200",
|
|
)}
|
|
/>
|
|
多轮 RAG 问答
|
|
</div>
|
|
<div className="pl-7 text-xs">
|
|
多次检索、展示过程与参考资料,适合复杂问题
|
|
</div>
|
|
</div>
|
|
{multiRoundRagActive ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
</PromptInputActionMenu>
|
|
</DropdownMenuGroup>
|
|
</PromptInputActionMenuContent>
|
|
</PromptInputActionMenu>
|
|
{!frontendRagActive && supportReasoningEffort && context.mode !== "flash" && (
|
|
<PromptInputActionMenu>
|
|
<PromptInputActionMenuTrigger className={cn("gap-1.5! rounded-full border-0 bg-transparent px-3! text-muted-foreground shadow-none hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100", compact && "rounded-lg border border-border px-2.5! py-1! text-[13px]")}>
|
|
<div className="text-xs font-normal">
|
|
{t.inputBox.reasoningEffort}:
|
|
{context.reasoning_effort === "minimal" &&
|
|
" " + t.inputBox.reasoningEffortMinimal}
|
|
{context.reasoning_effort === "low" &&
|
|
" " + t.inputBox.reasoningEffortLow}
|
|
{context.reasoning_effort === "medium" &&
|
|
" " + t.inputBox.reasoningEffortMedium}
|
|
{context.reasoning_effort === "high" &&
|
|
" " + t.inputBox.reasoningEffortHigh}
|
|
</div>
|
|
</PromptInputActionMenuTrigger>
|
|
<PromptInputActionMenuContent className="w-70">
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
{t.inputBox.reasoningEffort}
|
|
</DropdownMenuLabel>
|
|
<PromptInputActionMenu>
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
context.reasoning_effort === "minimal"
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleReasoningEffortSelect("minimal")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
{t.inputBox.reasoningEffortMinimal}
|
|
</div>
|
|
<div className="pl-2 text-xs">
|
|
{t.inputBox.reasoningEffortMinimalDescription}
|
|
</div>
|
|
</div>
|
|
{context.reasoning_effort === "minimal" ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
context.reasoning_effort === "low"
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleReasoningEffortSelect("low")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
{t.inputBox.reasoningEffortLow}
|
|
</div>
|
|
<div className="pl-2 text-xs">
|
|
{t.inputBox.reasoningEffortLowDescription}
|
|
</div>
|
|
</div>
|
|
{context.reasoning_effort === "low" ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
context.reasoning_effort === "medium" ||
|
|
!context.reasoning_effort
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleReasoningEffortSelect("medium")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
{t.inputBox.reasoningEffortMedium}
|
|
</div>
|
|
<div className="pl-2 text-xs">
|
|
{t.inputBox.reasoningEffortMediumDescription}
|
|
</div>
|
|
</div>
|
|
{context.reasoning_effort === "medium" ||
|
|
!context.reasoning_effort ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
<PromptInputActionMenuItem
|
|
className={cn(
|
|
context.reasoning_effort === "high"
|
|
? "text-accent-foreground"
|
|
: "text-muted-foreground/65",
|
|
)}
|
|
onSelect={() => handleReasoningEffortSelect("high")}
|
|
>
|
|
<div className="flex flex-col gap-2">
|
|
<div className="flex items-center gap-1 font-bold">
|
|
{t.inputBox.reasoningEffortHigh}
|
|
</div>
|
|
<div className="pl-2 text-xs">
|
|
{t.inputBox.reasoningEffortHighDescription}
|
|
</div>
|
|
</div>
|
|
{context.reasoning_effort === "high" ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</PromptInputActionMenuItem>
|
|
</PromptInputActionMenu>
|
|
</DropdownMenuGroup>
|
|
</PromptInputActionMenuContent>
|
|
</PromptInputActionMenu>
|
|
)}
|
|
</PromptInputTools>
|
|
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
|
|
{!frontendRagActive && agentSelector && (
|
|
<AgentSelector
|
|
selectedAgentId={agentSelector.selectedAgentId}
|
|
onAgentChange={agentSelector.onAgentChange}
|
|
disabled={disabled}
|
|
compact={compact}
|
|
/>
|
|
)}
|
|
{!frontendRagActive && knowledgeSelector}
|
|
{!frontendRagActive && promptPrefix && (
|
|
<Tooltip
|
|
content={
|
|
promptPrefix.on
|
|
? `已开启:${promptPrefix.label}`
|
|
: `点击开启:${promptPrefix.label}`
|
|
}
|
|
>
|
|
<PromptInputButton
|
|
type="button"
|
|
onClick={promptPrefix.onToggle}
|
|
aria-pressed={promptPrefix.on}
|
|
className={cn(
|
|
"rounded-full px-3 shadow-none transition-colors",
|
|
promptPrefix.on
|
|
? "border border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-950/40 dark:text-amber-200"
|
|
: "border-0 bg-transparent text-muted-foreground hover:bg-muted/70 hover:text-foreground",
|
|
)}
|
|
>
|
|
<span className="max-w-[160px] truncate text-xs font-normal">
|
|
{promptPrefix.label}
|
|
</span>
|
|
{promptPrefix.on && (
|
|
<CheckIcon className="ml-1 size-3.5 text-amber-700 dark:text-amber-200" />
|
|
)}
|
|
</PromptInputButton>
|
|
</Tooltip>
|
|
)}
|
|
{!multiRoundRagActive && <ModelSelector
|
|
open={modelDialogOpen}
|
|
onOpenChange={setModelDialogOpen}
|
|
>
|
|
<ModelSelectorTrigger asChild>
|
|
<PromptInputButton
|
|
className={cn(
|
|
"rounded-full border-0 bg-transparent px-3 shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
|
compact &&
|
|
"rounded-lg border border-transparent bg-gray-50/60 px-2.5! py-1! font-mono text-gray-600 hover:bg-gray-100/80 hover:text-gray-800 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700",
|
|
)}
|
|
>
|
|
<div className="flex min-w-0 flex-col items-start text-left">
|
|
<ModelSelectorName
|
|
className={cn(
|
|
"text-xs font-normal",
|
|
compact && "text-[11px]",
|
|
)}
|
|
>
|
|
{selectedModel?.display_name}
|
|
</ModelSelectorName>
|
|
</div>
|
|
</PromptInputButton>
|
|
</ModelSelectorTrigger>
|
|
<ModelSelectorContent>
|
|
<ModelSelectorInput placeholder={t.inputBox.searchModels} />
|
|
<ModelSelectorList>
|
|
{modelOptions.map((m) => (
|
|
<ModelSelectorItem
|
|
key={m.name}
|
|
value={m.name}
|
|
onSelect={() => handleModelSelect(m.name)}
|
|
>
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<ModelSelectorName>{m.display_name}</ModelSelectorName>
|
|
<span className="text-muted-foreground truncate text-[10px]">
|
|
{selectedSingleRoundRag
|
|
? `单轮知识库问答 · ${m.name}`
|
|
: m.model}
|
|
</span>
|
|
</div>
|
|
{m.name === context.model_name ? (
|
|
<CheckIcon className="ml-auto size-4" />
|
|
) : (
|
|
<div className="ml-auto size-4" />
|
|
)}
|
|
</ModelSelectorItem>
|
|
))}
|
|
</ModelSelectorList>
|
|
</ModelSelectorContent>
|
|
</ModelSelector>}
|
|
<PromptInputSubmit
|
|
className={cn(
|
|
"rounded-full border-border bg-background shadow-sm hover:bg-muted dark:bg-blue-500 dark:border-blue-500 dark:text-white dark:hover:bg-blue-400 dark:shadow-none",
|
|
compact &&
|
|
"size-7 rounded-lg border-0 bg-[var(--primary)] text-[var(--button-foreground,var(--primary-foreground))] hover:opacity-90 disabled:bg-muted disabled:text-muted-foreground/40 dark:bg-[var(--primary)] dark:border-0 dark:text-[var(--button-foreground,var(--primary-foreground))] dark:hover:opacity-90 dark:disabled:bg-muted",
|
|
)}
|
|
disabled={disabled}
|
|
variant="outline"
|
|
status={status}
|
|
/>
|
|
</PromptInputTools>
|
|
</PromptInputFooter>
|
|
{!isNewThread && (
|
|
<div className="absolute right-0 -bottom-[17px] left-0 z-0 h-4 bg-background"></div>
|
|
)}
|
|
</PromptInput>
|
|
</div>
|
|
</div>
|
|
|
|
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{t.inputBox.followupConfirmTitle}</DialogTitle>
|
|
<DialogDescription>
|
|
{t.inputBox.followupConfirmDescription}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
|
|
{t.common.cancel}
|
|
</Button>
|
|
<Button variant="secondary" onClick={confirmAppendAndSend}>
|
|
{t.inputBox.followupConfirmAppend}
|
|
</Button>
|
|
<Button onClick={confirmReplaceAndSend}>
|
|
{t.inputBox.followupConfirmReplace}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SuggestionList() {
|
|
const { t } = useI18n();
|
|
const { textInput } = usePromptInputController();
|
|
const handleSuggestionClick = useCallback(
|
|
(prompt: string | undefined) => {
|
|
if (!prompt) return;
|
|
textInput.setInput(prompt);
|
|
setTimeout(() => {
|
|
const textarea = document.querySelector<HTMLTextAreaElement>(
|
|
"textarea[name='message']",
|
|
);
|
|
if (textarea) {
|
|
const selStart = prompt.indexOf("[");
|
|
const selEnd = prompt.indexOf("]");
|
|
if (selStart !== -1 && selEnd !== -1) {
|
|
textarea.setSelectionRange(selStart, selEnd + 1);
|
|
textarea.focus();
|
|
}
|
|
}
|
|
}, 500);
|
|
},
|
|
[textInput],
|
|
);
|
|
return (
|
|
<Suggestions className="min-h-16 w-fit items-start">
|
|
<ConfettiButton
|
|
className="text-muted-foreground cursor-pointer rounded-full px-4 text-xs font-normal"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleSuggestionClick(t.inputBox.surpriseMePrompt)}
|
|
>
|
|
<SparklesIcon className="size-4" /> {t.inputBox.surpriseMe}
|
|
</ConfettiButton>
|
|
{t.inputBox.suggestions.map((suggestion) => (
|
|
<Suggestion
|
|
key={suggestion.suggestion}
|
|
icon={suggestion.icon}
|
|
suggestion={suggestion.suggestion}
|
|
onClick={() => handleSuggestionClick(suggestion.prompt)}
|
|
/>
|
|
))}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Suggestion icon={PlusIcon} suggestion={t.common.create} />
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start">
|
|
<DropdownMenuGroup>
|
|
{t.inputBox.suggestionsCreate.map((suggestion, index) =>
|
|
"type" in suggestion && suggestion.type === "separator" ? (
|
|
<DropdownMenuSeparator key={index} />
|
|
) : (
|
|
!("type" in suggestion) && (
|
|
<DropdownMenuItem
|
|
key={suggestion.suggestion}
|
|
onClick={() => handleSuggestionClick(suggestion.prompt)}
|
|
>
|
|
{suggestion.icon && <suggestion.icon className="size-4" />}
|
|
{suggestion.suggestion}
|
|
</DropdownMenuItem>
|
|
)
|
|
),
|
|
)}
|
|
</DropdownMenuGroup>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</Suggestions>
|
|
);
|
|
}
|
|
|
|
function AddAttachmentsButton({ className }: { className?: string }) {
|
|
const { t } = useI18n();
|
|
const attachments = usePromptInputAttachments();
|
|
return (
|
|
<Tooltip content={t.inputBox.addAttachments}>
|
|
<PromptInputButton
|
|
className={cn("px-2!", className)}
|
|
onClick={() => attachments.openFileDialog()}
|
|
>
|
|
<PaperclipIcon className="size-3" />
|
|
</PromptInputButton>
|
|
</Tooltip>
|
|
);
|
|
}
|