代码合并

This commit is contained in:
fengxiangdebaozi committed 2026-08-18 16:39:56 +08:00
1 parent e0b6e1cd19
commit e412d7db3f
3 files changed
+147 -18

No files matched your search

@@ -2,14 +2,18 @@
import type { ChatStatus } from "ai";
import {
BarChart3Icon,
BrainCircuitIcon,
BookMarkedIcon,
CheckIcon,
ClipboardCheckIcon,
FileTextIcon,
GraduationCapIcon,
LightbulbIcon,
MessageCircleQuestionIcon,
PaperclipIcon,
PlusIcon,
RefreshCwIcon,
ShrinkIcon,
SparklesIcon,
RocketIcon,
@@ -105,6 +109,56 @@ import { ModeHoverGuide } from "./mode-hover-guide";
import { Tooltip } from "./tooltip";
type InputMode = "flash" | "thinking" | "pro" | "ultra";
const COMPACT_QUICK_STARTS = [
{
id: "report",
title: "撰写报告",
content: "请帮我撰写一份结构清晰的专业报告,先向我确认主题、受众和篇幅要求。",
icon: FileTextIcon,
},
{
id: "knowledge",
title: "知识问答",
content: "请基于已有资料回答我的问题,并明确区分已知事实、推断和需要补充的信息。",
icon: MessageCircleQuestionIcon,
},
{
id: "planning",
title: "任务规划",
content: "请把我的目标拆解为可执行的任务计划,列出优先级、里程碑和风险。",
icon: ClipboardCheckIcon,
},
{
id: "analysis",
title: "数据分析",
content: "请帮我分析数据中的关键趋势、异常和可能原因,并给出下一步验证建议。",
icon: BarChart3Icon,
},
{
id: "solution",
title: "方案策划",
content: "请围绕我的目标设计一套完整方案,包含背景、目标、实施步骤、资源需求和风险预案。",
icon: LightbulbIcon,
},
{
id: "summary",
title: "内容总结",
content: "请提炼我提供内容的核心观点、关键结论和待办事项,并按重要程度整理。",
icon: BookMarkedIcon,
},
{
id: "research",
title: "调研提纲",
content: "请根据我的调研主题生成一份结构完整的调研提纲,覆盖对象、问题、方法和输出形式。",
icon: GraduationCapIcon,
},
{
id: "polish",
title: "文案润色",
content: "请在不改变原意的前提下润色我提供的内容,使表达更清晰、专业、简洁。",
icon: SparklesIcon,
},
] as const;
function getResolvedMode(
@@ -227,6 +281,7 @@ export function InputBox({
const wasStreamingRef = useRef(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [compactPromptOrder, setCompactPromptOrder] = useState(0);
const [pendingSuggestion, setPendingSuggestion] = useState<string | null>(
null,
);
@@ -237,6 +292,29 @@ export function InputBox({
!hideAdminRecommendedQuestions &&
!userPreferences.hide_admin_prompt_prefix &&
recommendedQuestions.length > 0;
const compactQuickStarts = useMemo(() => {
if (showAdminRecommendedQuestions) {
return recommendedQuestions.map((configured, index) => {
const fallback =
COMPACT_QUICK_STARTS[index % COMPACT_QUICK_STARTS.length]!;
return {
...fallback,
id: configured.id,
title: desensitize(configured.title) || `提示词 ${index + 1}`,
content: configured.content || fallback.content,
};
});
}
return [...COMPACT_QUICK_STARTS];
}, [recommendedQuestions, showAdminRecommendedQuestions]);
const orderedCompactQuickStarts = useMemo(() => {
if (compactQuickStarts.length < 2) return compactQuickStarts;
const offset = compactPromptOrder % compactQuickStarts.length;
return [
...compactQuickStarts.slice(offset),
...compactQuickStarts.slice(0, offset),
];
}, [compactPromptOrder, compactQuickStarts]);
useEffect(() => {
// Multi-round RAG is a frontend execution profile, not a model option.
@@ -593,14 +671,53 @@ export function InputBox({
}, [context.model_name, disabled, isMock, selectedSingleRoundRag, status, thread.messages, threadId]);
return (
<div ref={promptRootRef} className="relative flex flex-col gap-6">
<div ref={promptRootRef} className={cn("relative flex flex-col", compact ? "gap-[clamp(32px,6vh,58px)]" : "gap-6")}>
{extraHeader && (
<div className="flex items-center justify-center">
{extraHeader}
</div>
)}
{isNewThread && searchParams.get("mode") !== "skill" && showAdminRecommendedQuestions && (
{isNewThread && searchParams.get("mode") !== "skill" && compact && (
<section className="px-2" aria-labelledby="recommended-prompts-title">
<div className="mb-3 flex items-center justify-center gap-1.5">
<h2 id="recommended-prompts-title" className="text-[14px] font-normal text-slate-400 dark:text-slate-500">
你可以这样问我
</h2>
<button
type="button"
data-prompt-order-refresh="true"
title="调整提示词顺序"
aria-label="调整提示词顺序"
onClick={() => setCompactPromptOrder((current) => current + 1)}
className="flex size-7 items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-primary focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/15 dark:hover:bg-slate-800 dark:hover:text-primary"
>
<RefreshCwIcon className="size-3.5" strokeWidth={1.8} />
</button>
</div>
<div className="mx-auto flex max-w-[900px] flex-wrap justify-center gap-x-3 gap-y-3">
{orderedCompactQuickStarts.map((item) => (
<button
key={item.id}
type="button"
data-quick-prompt={item.id}
title={item.content}
className="inline-flex min-h-11 max-w-full items-center justify-center rounded-xl border border-[#e5e7eb] bg-white px-4 py-2 text-center text-[14px] font-normal leading-5 text-slate-700 shadow-[0_2px_6px_rgba(15,23,42,0.025)] transition-[background-color,border-color,box-shadow] hover:border-primary/25 hover:bg-primary/5 hover:text-slate-900 hover:shadow-[0_4px_10px_rgba(15,23,42,0.04)] focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-primary/15 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-primary/35 dark:hover:bg-primary/10"
onClick={() => {
textInput.setInput(item.content);
requestAnimationFrame(() =>
document.querySelector<HTMLTextAreaElement>("textarea[name='message']")?.focus(),
);
}}
>
<span className="max-w-full whitespace-normal">{item.title}</span>
</button>
))}
</div>
</section>
)}
{isNewThread && searchParams.get("mode") !== "skill" && !compact && showAdminRecommendedQuestions && (
<div className="flex flex-wrap justify-center gap-2.5 px-4 mb-[10px]">
{recommendedQuestions.map((q) => (
<button
@@ -653,7 +770,8 @@ export function InputBox({
<div className="flex flex-col gap-1.5">
{isNewThread && (
<CustomPromptBar
className="px-1"
compact={compact}
className={cn(compact ? "px-2" : "px-1")}
currentAgentId={agentSelector?.selectedAgentId ?? assistantId ?? null}
onInsert={(content) => {
const current = textInput.value?.trim() ?? "";
@@ -677,7 +795,7 @@ export function InputBox({
: "*: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"
? "overflow-hidden rounded-[24px] border border-slate-200/90 bg-white shadow-[0_18px_50px_-32px_rgba(15,23,42,0.22),0_8px_24px_-20px_rgba(15,23,42,0.35)] transition-all duration-300 focus-within:border-primary/35 focus-within:ring-4 focus-within:ring-primary/10 dark:border-slate-700 dark:bg-slate-800"
: // landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边
// 对话态:常规白卡 + 中性边框 + 轻阴影
isNewThread
@@ -701,7 +819,7 @@ export function InputBox({
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]",
compact && "min-h-[clamp(150px,18vh,176px)]! px-7 pt-7 pb-2 text-[15px]",
)}
disabled={disabled}
placeholder={t.inputBox.placeholder}
@@ -712,7 +830,7 @@ export function InputBox({
<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",
compact && "gap-2 px-6 pt-2 pb-5",
)}
>
<PromptInputTools
@@ -9,13 +9,21 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import type { RuntimeDisplaySource } from "@/core/skill-display/runtime";
import { resolveWeKnoraSourceTarget } from "@/core/llmwiki/source-links";
import {
parseWeKnoraPreviewUrl,
resolveWeKnoraSourceTarget,
} from "@/core/llmwiki/source-links";
import { useLocalSettings } from "@/core/settings";
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
import { cn } from "@/lib/utils";
import { useState } from "react";
import { WeKnoraSourceDrawer } from "@/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer";
/** Whether a source URL is an internal WeKnora preview pointer. */
export function isWeKnoraSourcePreviewUrl(url?: string | null): boolean {
return parseWeKnoraPreviewUrl(url) !== null;
}
export function SkillDisplaySources({
highlightedReferenceId,
sources,
@@ -130,9 +138,9 @@ export function SourceCard({
</div>
{source.url && (
previewTarget ? (
<span className="text-muted-foreground bg-muted mt-0.5 inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-[11px] font-medium">
<span className="text-primary bg-primary/10 mt-0.5 inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-[11px] font-medium">
<FileTextIcon className="size-3.5" />
知识库文档
查看原文
</span>
) : (
<ExternalLinkIcon className="text-muted-foreground/60 group-hover:text-primary mt-1 size-3.5 shrink-0 transition-colors" />
@@ -5,21 +5,24 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { useSidebar } from "@/components/ui/sidebar";
import { useTheme } from "@/contexts/ThemeContext";
import { useI18n } from "@/core/i18n/hooks";
import { env } from "@/env";
import { cn } from "@/lib/utils";
function BrandMark({ className }: { className?: string }) {
const { theme } = useTheme();
const brandLogoSrc =
theme === "dark"
? "/cm-assistant-logo.png"
: "/cm-assistant-logo-red.png";
return (
<div
className={cn(
"bg-primary text-primary-foreground flex size-9 shrink-0 items-center justify-center rounded-lg text-sm font-bold shadow-sm",
className,
)}
aria-hidden
>
C
</div>
<img
src={brandLogoSrc}
alt="CM 助手"
className={cn("size-9 shrink-0 object-contain", className)}
/>
);
}