Merge branch 'dhk-1' into 'master'

样式修改

See merge request knowledge-base-bj/deerflow-server!29
This commit is contained in:
董宏凯 committed 2026-05-20 14:47:15 +00:00
commit 628bbe14a8
24 files changed
+936 -154

No files matched your search

@@ -514,9 +514,15 @@ export function InputBox({
</div>
</div>
)}
<div className={cn(isNewThread && "gemini-gradient-border gemini-shadow")}>
<PromptInput
className={cn(
"rounded-[1.85rem] border border-[#eadfce] bg-white shadow-[0_24px_70px_-34px_rgba(120,83,42,0.32)] ring-1 ring-[#f6efe5] transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-[1.85rem] dark:bg-slate-800 dark:border-slate-700 dark:ring-slate-700 dark:shadow-none",
"transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-3xl",
// landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边
// 对话态:常规白卡 + 中性边框 + 轻阴影
isNewThread
? "rounded-3xl bg-transparent border-0 shadow-none ring-0"
: "rounded-3xl bg-background border border-border shadow-sm dark:bg-slate-800 dark:border-slate-700",
className,
)}
disabled={disabled}
@@ -531,7 +537,8 @@ export function InputBox({
<PromptInputBody className="absolute top-0 right-0 left-0 z-3">
<PromptInputTextarea
className={cn(
"size-full px-2 py-1 text-[15px] leading-7 placeholder:text-[#b7ab9a] dark:placeholder:text-slate-500",
"size-full px-2 py-1 text-[15px] leading-7 placeholder:text-muted-foreground/60",
isNewThread && "min-h-[110px]!",
)}
disabled={disabled}
placeholder={t.inputBox.placeholder}
@@ -539,7 +546,7 @@ export function InputBox({
defaultValue={initialValue}
/>
</PromptInputBody>
<PromptInputFooter className="flex items-center border-t border-[#f4ede4] dark:border-slate-700 px-2 pt-2.5 pb-1.5">
<PromptInputFooter className="flex items-center border-t-0 px-2 pt-1 pb-1.5">
<PromptInputTools className="gap-1.5">
{/* TODO: Add more connectors here
<PromptInputActionMenu>
@@ -550,7 +557,7 @@ export function InputBox({
/>
</PromptInputActionMenuContent>
</PromptInputActionMenu> */}
<AddAttachmentsButton className="rounded-full border border-[#eee3d5] bg-[#fffaf5] px-2.5! shadow-none hover:bg-white dark:border-slate-600 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700" />
<AddAttachmentsButton className="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" />
<Tooltip
content={
context.writing_mode
@@ -560,10 +567,10 @@ export function InputBox({
>
<PromptInputButton
className={cn(
"gap-1.5! rounded-full border px-3! shadow-none",
"gap-1.5! rounded-full px-3! shadow-none",
context.writing_mode
? "border-[#d8b878] bg-[#fff3cf] text-[#6d5625] hover:bg-[#ffedbd]"
: "border-[#eee3d5] bg-[#fffaf5] text-[#7e6a57] hover:bg-white",
? "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 dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
)}
onClick={handleWritingModeToggle}
>
@@ -582,7 +589,7 @@ export function InputBox({
: "flash"
}
>
<PromptInputActionMenuTrigger className="gap-1.5! rounded-full border border-[#eee3d5] bg-[#fffaf5] px-3! text-[#7e6a57] shadow-none hover:bg-white dark:border-slate-600 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700">
<PromptInputActionMenuTrigger className="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">
<div>
{context.mode === "flash" && <ZapIcon className="size-3" />}
{context.mode === "thinking" && (
@@ -744,7 +751,7 @@ export function InputBox({
</PromptInputActionMenu>
{supportReasoningEffort && context.mode !== "flash" && (
<PromptInputActionMenu>
<PromptInputActionMenuTrigger className="gap-1.5! rounded-full border border-[#eee3d5] bg-[#fffaf5] px-3! text-[#7e6a57] shadow-none hover:bg-white dark:border-slate-600 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700">
<PromptInputActionMenuTrigger className="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">
<div className="text-xs font-normal">
{t.inputBox.reasoningEffort}:
{context.reasoning_effort === "minimal" &&
@@ -873,17 +880,17 @@ export function InputBox({
onClick={promptPrefix.onToggle}
aria-pressed={promptPrefix.on}
className={cn(
"rounded-full border px-3 shadow-none transition-colors",
"rounded-full px-3 shadow-none transition-colors",
promptPrefix.on
? "border-[#d6a868] bg-[#fff3df] text-[#7a5417] hover:bg-[#ffe8c4]"
: "border-[#eee3d5] bg-[#fffaf5] text-[#8a7558] hover:bg-white",
? "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-[#7a5417]" />
<CheckIcon className="ml-1 size-3.5 text-amber-700 dark:text-amber-200" />
)}
</PromptInputButton>
</Tooltip>
@@ -893,7 +900,7 @@ export function InputBox({
onOpenChange={setModelDialogOpen}
>
<ModelSelectorTrigger asChild>
<PromptInputButton className="rounded-full border border-[#eee3d5] bg-[#fffaf5] px-3 shadow-none hover:bg-white dark:border-slate-600 dark:bg-slate-700/50 dark:text-slate-300 dark:hover:bg-slate-700">
<PromptInputButton className="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">
<div className="flex min-w-0 flex-col items-start text-left">
<ModelSelectorName className="text-xs font-normal">
{selectedModel?.display_name}
@@ -927,7 +934,7 @@ export function InputBox({
</ModelSelectorContent>
</ModelSelector>
<PromptInputSubmit
className="rounded-full border-[#e7d9c7] bg-white shadow-[0_10px_24px_-18px_rgba(120,83,42,0.34)] dark:bg-blue-500 dark:border-blue-500 dark:text-white dark:hover:bg-blue-400 dark:shadow-none"
className="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"
disabled={disabled}
variant="outline"
status={status}
@@ -935,9 +942,10 @@ export function InputBox({
</PromptInputTools>
</PromptInputFooter>
{!isNewThread && (
<div className="absolute right-0 -bottom-[17px] left-0 z-0 h-4 bg-[#fffdfa] dark:bg-background"></div>
<div className="absolute right-0 -bottom-[17px] left-0 z-0 h-4 bg-background"></div>
)}
</PromptInput>
</div>
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
<DialogContent>
@@ -1,6 +1,7 @@
import type { Message } from "@langchain/langgraph-sdk";
import {
BookOpenTextIcon,
ChevronDownIcon,
ChevronUp,
FolderOpenIcon,
GlobeIcon,
@@ -23,6 +24,18 @@ import {
} from "@/components/ai-elements/chain-of-thought";
import { CodeBlock } from "@/components/ai-elements/code-block";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useI18n } from "@/core/i18n/hooks";
import {
extractReasoningContentFromMessage,
@@ -208,19 +221,10 @@ function ToolCall({
if (typeof args.query === "string") {
label = t.toolCalls.searchOnWebFor(args.query);
}
const items = normalizeSearchResults(result);
return (
<ChainOfThoughtStep key={id} label={label} icon={SearchIcon}>
{Array.isArray(result) && (
<ChainOfThoughtSearchResults>
{result.map((item) => (
<ChainOfThoughtSearchResult key={item.url}>
<a href={item.url} target="_blank" rel="noopener noreferrer">
{item.title}
</a>
</ChainOfThoughtSearchResult>
))}
</ChainOfThoughtSearchResults>
)}
{items.length > 0 && <WebSearchResultsList results={items} />}
</ChainOfThoughtStep>
);
} else if (name === "image_search") {
@@ -496,3 +500,117 @@ function convertToSteps(messages: Message[]): CoTStep[] {
}
return steps;
}
type WebSearchItem = {
title?: string;
url?: string;
snippet?: string;
content?: string;
published_date?: string;
publishedDate?: string;
date?: string;
};
function normalizeSearchResults(
result: string | Record<string, unknown> | undefined,
): WebSearchItem[] {
if (Array.isArray(result)) {
return result as WebSearchItem[];
}
if (result && typeof result === "object") {
const nested = (result as { results?: unknown }).results;
if (Array.isArray(nested)) {
return nested as WebSearchItem[];
}
}
return [];
}
function WebSearchResultsList({ results }: { results: WebSearchItem[] }) {
const [open, setOpen] = useState(false);
return (
<Collapsible open={open} onOpenChange={setOpen} className="w-full">
<CollapsibleTrigger className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs">
<ChevronDownIcon
className={cn(
"size-3.5 transition-transform duration-200",
open && "rotate-180",
)}
/>
<span>{results.length} 条检索结果</span>
</CollapsibleTrigger>
<CollapsibleContent className="mt-2 flex flex-col gap-2">
{results.map((item, idx) => (
<SearchResultCard key={item.url ?? idx} item={item} />
))}
</CollapsibleContent>
</Collapsible>
);
}
function SearchResultCard({ item }: { item: WebSearchItem }) {
const [open, setOpen] = useState(false);
const content = item.snippet ?? item.content;
const date = item.published_date ?? item.publishedDate ?? item.date;
let source: string | undefined;
if (item.url) {
try {
source = new URL(item.url).hostname.replace(/^www\./, "");
} catch {
source = undefined;
}
}
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className="bg-card hover:bg-accent block w-full cursor-pointer rounded-lg border p-3 text-left transition-colors"
>
{item.title && (
<div className="text-foreground line-clamp-1 text-sm font-medium">
{item.title}
</div>
)}
{(source || date) && (
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-xs">
{source && <span className="truncate">{source}</span>}
{source && date && <span>·</span>}
{date && <span>{date}</span>}
</div>
)}
{content && (
<div className="text-muted-foreground mt-1.5 line-clamp-2 text-xs leading-relaxed">
{content}
</div>
)}
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="pr-6 text-base leading-relaxed">
{item.title ?? "检索结果"}
</DialogTitle>
{(source || date) && (
<DialogDescription className="flex items-center gap-2 text-xs">
{source && <span>{source}</span>}
{source && date && <span>·</span>}
{date && <span>{date}</span>}
</DialogDescription>
)}
</DialogHeader>
{content && (
<div className="text-foreground max-h-[60vh] overflow-y-auto whitespace-pre-wrap text-sm leading-relaxed">
{content}
</div>
)}
{item.url && (
<div className="text-muted-foreground border-t pt-3 text-xs break-all">
{item.url}
</div>
)}
</DialogContent>
</Dialog>
</>
);
}
@@ -158,15 +158,15 @@ export function RecentChatList() {
}
return (
<>
<SidebarGroup className="px-2">
<SidebarGroupLabel className="text-muted-foreground/90 px-2 text-[11px] font-semibold tracking-wide uppercase">
<SidebarGroup className="px-2 pt-3 mt-2 border-t border-sidebar-border/60">
<SidebarGroupLabel className="text-muted-foreground/70 px-3 mb-1 text-[10px] font-semibold tracking-[0.12em] uppercase">
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true"
? t.sidebar.recentChats
: t.sidebar.demoChats}
</SidebarGroupLabel>
<SidebarGroupContent className="group-data-[collapsible=icon]:pointer-events-none group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0">
<SidebarMenu>
<div className="flex w-full flex-col gap-1">
<div className="flex w-full flex-col gap-0.5">
{visibleThreads.map((thread) => {
const isActive = pathOfThread(thread) === pathname;
return (
@@ -174,9 +174,17 @@ export function RecentChatList() {
key={thread.thread_id}
className="group/side-menu-item"
>
<SidebarMenuButton isActive={isActive} asChild>
<SidebarMenuButton
isActive={isActive}
asChild
className={
"rounded-lg h-7 px-3 text-[13px] text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/60 " +
"data-[active=true]:bg-white data-[active=true]:text-foreground data-[active=true]:shadow-[0_1px_2px_rgba(15,23,42,0.05)] " +
"dark:data-[active=true]:bg-slate-700/80 dark:data-[active=true]:text-slate-100 dark:data-[active=true]:shadow-none"
}
>
<Link
className="text-muted-foreground block w-full whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
className="block w-full whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
href={pathOfThread(thread)}
>
{titleOfThread(thread)}
@@ -36,12 +36,12 @@ export function Welcome({
className,
)}
>
<div className="text-2xl font-bold">
<div className="text-[26px] font-medium tracking-tight text-foreground/85">
{searchParams.get("mode") === "skill" ? (
`✨ ${t.welcome.createYourOwnSkill} ✨`
) : (
<div className="flex items-center gap-2">
<div className={cn("inline-block", !waved ? "animate-wave" : "")}>
<div className="flex items-center gap-2.5">
<div className={cn("inline-block text-2xl", !waved ? "animate-wave" : "")}>
{isUltra ? "🚀" : "👋"}
</div>
<AuroraText colors={colors}>{t.welcome.greeting}</AuroraText>
@@ -15,26 +15,43 @@ import { useI18n } from "@/core/i18n/hooks";
// import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon, UsersIcon } from "lucide-react";
import { BotIcon, BrainIcon, CalendarClock, ListChecks, MessageSquarePlus, MessagesSquare, SparklesIcon, UsersIcon } from "lucide-react";
// 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标
const NAV_ITEM_CLASS =
"text-sidebar-foreground/80 hover:text-primary rounded-xl h-9 px-3 transition-colors hover:bg-primary/10 " +
"data-[active=true]:bg-primary/15 data-[active=true]:text-primary data-[active=true]:shadow-none " +
"dark:hover:bg-primary/15 dark:data-[active=true]:bg-primary/25 dark:data-[active=true]:text-primary-foreground";
// 「新对话」专属:默认仅主色文字/图标(显眼但不是选中态),命中 /chats/new 时才上主色底
const NEW_CHAT_CLASS =
"rounded-xl h-9 px-3 text-primary transition-colors hover:bg-primary/10 hover:text-primary " +
"data-[active=true]:bg-primary/15 data-[active=true]:text-primary data-[active=true]:shadow-none " +
"dark:text-primary-foreground dark:hover:bg-primary/15 dark:data-[active=true]:bg-primary/25";
export function WorkspaceNavChatList() {
const { t } = useI18n();
const pathname = usePathname();
return (
<SidebarGroup className="px-2 pt-2">
<SidebarMenu>
<SidebarMenu className="gap-1">
<SidebarMenuItem>
<SidebarMenuButton
isActive={pathname === "/page/workspace/chats/new"}
asChild
className={NEW_CHAT_CLASS}
>
<Link className="text-sidebar-foreground" href="/page/workspace/chats/new">
<Link href="/page/workspace/chats/new">
<MessageSquarePlus />
<span>{t.sidebar.newChat}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton isActive={pathname === "/page/workspace/chats"} asChild>
<Link className="text-sidebar-foreground" href="/page/workspace/chats">
<SidebarMenuButton
isActive={pathname === "/page/workspace/chats"}
asChild
className={NAV_ITEM_CLASS}
>
<Link href="/page/workspace/chats">
<MessagesSquare />
<span>{t.sidebar.chats}</span>
</Link>
@@ -44,8 +61,9 @@ export function WorkspaceNavChatList() {
<SidebarMenuButton
isActive={pathname.startsWith("/page/workspace/agents")}
asChild
className={NAV_ITEM_CLASS}
>
<Link className="text-sidebar-foreground" href="/page/workspace/agents">
<Link href="/page/workspace/agents">
<BotIcon />
<span>{t.sidebar.agents}</span>
</Link>
@@ -55,8 +73,9 @@ export function WorkspaceNavChatList() {
<SidebarMenuButton
isActive={pathname.startsWith("/page/workspace/skills")}
asChild
className={NAV_ITEM_CLASS}
>
<Link className="text-sidebar-foreground" href="/page/workspace/skills">
<Link href="/page/workspace/skills">
<SparklesIcon />
<span>技能</span>
</Link>
@@ -66,8 +85,9 @@ export function WorkspaceNavChatList() {
<SidebarMenuButton
isActive={pathname.startsWith("/page/workspace/memory")}
asChild
className={NAV_ITEM_CLASS}
>
<Link className="text-sidebar-foreground" href="/page/workspace/memory">
<Link href="/page/workspace/memory">
<BrainIcon />
<span>记忆</span>
</Link>
@@ -77,8 +97,9 @@ export function WorkspaceNavChatList() {
<SidebarMenuButton
isActive={pathname === "/page/workspace/scheduled/tasks"}
asChild
className={NAV_ITEM_CLASS}
>
<Link className="text-sidebar-foreground" href="/page/workspace/scheduled/tasks">
<Link href="/page/workspace/scheduled/tasks">
<ListChecks />
<span>任务管理</span>
</Link>
@@ -0,0 +1,34 @@
import type { ReactNode } from 'react';
import { cn } from '@/lib/utils';
interface CollapseTransitionProps {
open: boolean;
className?: string;
innerClassName?: string;
duration?: number;
children: ReactNode;
}
export function CollapseTransition({
open,
className,
innerClassName,
duration = 200,
children,
}: CollapseTransitionProps) {
return (
<div
aria-hidden={!open}
style={{ transitionDuration: `${duration}ms` }}
className={cn(
'grid overflow-hidden transition-[grid-template-rows,opacity] ease-out',
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0 pointer-events-none',
className,
)}
>
<div className={cn('min-h-0 overflow-hidden', innerClassName)}>
{children}
</div>
</div>
);
}
@@ -1,14 +1,15 @@
import { useState } from 'react';
import {
CheckCircle2, ChevronRight, ChevronDown,
CheckCircle2, ChevronRight,
ExternalLink, ListTree, FileText,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { buildIntranetSourceUrl } from '@/core/config';
import type { CompletedIntervention } from '../../types/ai-writing';
import type { AIMaterial, CompletedIntervention } from '../../types/ai-writing';
import { OutlineCard } from './OutlineCard';
import { ReviewCard } from './ReviewCard';
import { ScoreBar } from './ScoreBar';
import { CollapseTransition } from './CollapseTransition';
interface Props {
record: CompletedIntervention;
@@ -57,45 +58,95 @@ function MaterialCompleted({ record, expanded, onToggle }: { record: CompletedIn
)}
</span>
{count > 0 && (
expanded
? <ChevronDown className="size-3 text-muted-foreground" />
: <ChevronRight className="size-3 text-muted-foreground" />
<ChevronRight
className={cn(
'size-3 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
)}
</button>
{expanded && record.materials && record.materials.length > 0 && (
<div className="px-2.5 pb-2.5 pt-1 space-y-1.5 border-t border-green-200/40 dark:border-green-900/20">
{record.materials.map((m) => {
const pct = Math.round(m.relevanceScore * 100);
const barColor =
m.relevanceScore >= 0.8 ? 'bg-green-500'
: m.relevanceScore >= 0.5 ? 'bg-orange-400'
: 'bg-red-400';
return (
<div key={m.id} className="rounded border border-border/50 bg-background px-2.5 py-1.5 space-y-1">
<p className="font-medium leading-snug line-clamp-2">{m.title}</p>
<div className="flex items-center gap-2 text-muted-foreground">
<span className="truncate max-w-[100px]">{m.source}</span>
{m.publishedAt && <><span className="text-muted-foreground/40">·</span><span>{m.publishedAt}</span></>}
<div className="flex flex-1 items-center gap-1 min-w-0">
<div className="flex-1 h-1 rounded-full bg-muted overflow-hidden min-w-[40px]">
<div className={cn('h-full rounded-full', barColor)} style={{ width: `${pct}%` }} />
</div>
<span className={cn('shrink-0 text-[10px]', barColor.replace('bg-', 'text-'))}>{pct}%</span>
</div>
{m.recUuid && (
<button type="button"
onClick={() => window.open(buildIntranetSourceUrl(m.recUuid), '_blank', 'noopener,noreferrer')}
className="shrink-0 text-primary hover:underline inline-flex items-center gap-0.5">
原文 <ExternalLink className="size-2.5" />
</button>
)}
</div>
</div>
);
})}
<CollapseTransition open={expanded && !!record.materials && record.materials.length > 0}>
<div className="px-2.5 pb-2.5 pt-1.5 space-y-1.5 border-t border-green-200/40 dark:border-green-900/20">
{record.materials?.map((m) => (
<CompletedMaterialItem key={m.id} material={m} />
))}
</div>
)}
</CollapseTransition>
</div>
);
}
function relevanceLabel(score: number): string {
if (score >= 0.8) return '高相关';
if (score >= 0.5) return '中相关';
return '低相关';
}
function CompletedMaterialItem({ material }: { material: AIMaterial }) {
const [open, setOpen] = useState(false);
const pct = Math.round(material.relevanceScore * 100);
const barColor =
material.relevanceScore >= 0.8 ? 'bg-green-500'
: material.relevanceScore >= 0.5 ? 'bg-orange-400'
: 'bg-red-400';
const hasDetail = !!material.content || !!material.recUuid;
return (
<div className="rounded border border-border/50 bg-background overflow-hidden">
<button
type="button"
onClick={hasDetail ? () => setOpen(v => !v) : undefined}
className={cn(
'w-full px-2.5 py-1.5 space-y-1 text-left',
hasDetail && 'hover:bg-muted/30 transition-colors',
)}
>
<div className="flex items-start gap-1.5">
<span className="flex-1 font-medium leading-snug line-clamp-2">{material.title}</span>
{hasDetail && (
<ChevronRight
className={cn(
'mt-0.5 size-3 shrink-0 text-muted-foreground transition-transform duration-200',
open && 'rotate-90',
)}
/>
)}
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<span className="truncate max-w-[100px]">{material.source}</span>
{material.publishedAt && <><span className="text-muted-foreground/40">·</span><span>{material.publishedAt}</span></>}
<div className="flex flex-1 items-center gap-1 min-w-0">
<div className="flex-1 h-1 rounded-full bg-muted overflow-hidden min-w-[40px]">
<div className={cn('h-full rounded-full', barColor)} style={{ width: `${pct}%` }} />
</div>
<span className={cn('shrink-0 text-[10px]', barColor.replace('bg-', 'text-'))}>{pct}%</span>
<span className="text-muted-foreground/60 shrink-0 text-[10px]">({relevanceLabel(material.relevanceScore)})</span>
</div>
</div>
</button>
<CollapseTransition open={open && hasDetail}>
<div className="px-2.5 pb-2 pt-1.5 space-y-1.5 border-t border-border/40">
{material.content && (
<p className="text-muted-foreground leading-relaxed line-clamp-6">
{material.content.slice(0, 300)}
{material.content.length > 300 && '…'}
</p>
)}
{material.recUuid && (
<button
type="button"
onClick={() => window.open(buildIntranetSourceUrl(material.recUuid), '_blank', 'noopener,noreferrer')}
className="inline-flex items-center gap-1 text-primary hover:underline"
>
查看原文
<ExternalLink className="size-2.5" />
</button>
)}
</div>
</CollapseTransition>
</div>
);
}
@@ -124,17 +175,20 @@ function OutlineCompleted({ record, expanded, onToggle }: { record: CompletedInt
<span className="ml-1 text-muted-foreground font-normal">共 {sectionCount} 章节</span>
</span>
{record.outline && (
expanded
? <ChevronDown className="size-3 text-muted-foreground" />
: <ChevronRight className="size-3 text-muted-foreground" />
<ChevronRight
className={cn(
'size-3 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
)}
</button>
{expanded && record.outline && (
<CollapseTransition open={expanded && !!record.outline}>
<div className="px-2.5 pb-2.5 pt-1 border-t border-green-200/40 dark:border-green-900/20">
<OutlineCard outline={record.outline} readOnly />
{record.outline && <OutlineCard outline={record.outline} readOnly />}
</div>
)}
</CollapseTransition>
</div>
);
}
@@ -173,17 +227,20 @@ function DraftCompleted({ record, expanded, onToggle }: { record: CompletedInter
{wordCount > 0 && <span className="ml-1">({wordCount} 字)</span>}
</span>
{hasReview && (
expanded
? <ChevronDown className="size-3 text-muted-foreground" />
: <ChevronRight className="size-3 text-muted-foreground" />
<ChevronRight
className={cn(
'size-3 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
)}
</button>
{expanded && record.reviewResult && (
<CollapseTransition open={expanded && !!record.reviewResult}>
<div className="px-2.5 pb-2.5 pt-1 border-t border-green-200/40 dark:border-green-900/20">
<ReviewCard review={record.reviewResult} />
{record.reviewResult && <ReviewCard review={record.reviewResult} />}
</div>
)}
</CollapseTransition>
</div>
);
}
@@ -215,19 +272,26 @@ function ReviewCompleted({ record, expanded, onToggle }: { record: CompletedInte
</span>
)}
{hasReview && (
expanded
? <ChevronDown className="size-3 text-muted-foreground" />
: <ChevronRight className="size-3 text-muted-foreground" />
<ChevronRight
className={cn(
'size-3 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
)}
</button>
{expanded && record.reviewResult && (
<CollapseTransition open={expanded && !!record.reviewResult}>
<div className="px-2.5 pb-2.5 pt-1 space-y-1.5 border-t border-green-200/40 dark:border-green-900/20">
<ScoreBar label="事实准确性" score={record.reviewResult.factScore} />
<ScoreBar label="逻辑结构" score={record.reviewResult.logicScore} />
<ScoreBar label="语言文字" score={record.reviewResult.languageScore} />
{record.reviewResult && (
<>
<ScoreBar label="事实准确性" score={record.reviewResult.factScore} />
<ScoreBar label="逻辑结构" score={record.reviewResult.logicScore} />
<ScoreBar label="语言文字" score={record.reviewResult.languageScore} />
</>
)}
</div>
)}
</CollapseTransition>
</div>
);
}
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState } from 'react';
import { Brain, ChevronDown, ChevronRight } from 'lucide-react';
import { Brain, ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { StreamingDraftSection } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
interface DraftStreamingViewProps {
title: string;
@@ -40,10 +41,10 @@ function SectionThinking({ thinkingText, isDone }: { thinkingText: string; isDon
<Brain className="size-3 text-amber-500 shrink-0" />
<span className="text-[11px] font-medium text-amber-600 dark:text-amber-400">思考过程</span>
<span className="ml-auto text-amber-400">
{open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
<ChevronRight className={cn('size-3 transition-transform duration-200', open && 'rotate-90')} />
</span>
</button>
{open && (
<CollapseTransition open={open}>
<pre
ref={preRef}
className="text-[10px] font-mono leading-relaxed px-2.5 py-2 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-32 overflow-y-auto italic"
@@ -53,7 +54,7 @@ function SectionThinking({ thinkingText, isDone }: { thinkingText: string; isDon
<span className="inline-block w-0.5 h-3 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
)}
</pre>
)}
</CollapseTransition>
</div>
);
}
@@ -1,8 +1,9 @@
import { useState } from 'react';
import { ChevronRight, ChevronDown, ExternalLink } from 'lucide-react';
import { ChevronRight, ExternalLink } from 'lucide-react';
import { cn } from '@/lib/utils';
import { buildIntranetSourceUrl } from '@/core/config';
import type { AIMaterial } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
interface MaterialItemProps {
material: AIMaterial;
@@ -52,9 +53,12 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
<span className={cn('flex-1 font-medium leading-snug line-clamp-2', !checked && 'text-muted-foreground')}>
{material.title}
</span>
{expanded
? <ChevronDown className="mt-0.5 size-3 shrink-0 text-muted-foreground" />
: <ChevronRight className="mt-0.5 size-3 shrink-0 text-muted-foreground" />}
<ChevronRight
className={cn(
'mt-0.5 size-3 shrink-0 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
</button>
</div>
@@ -83,7 +87,7 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
</div>
{/* 展开内容 */}
{expanded && (
<CollapseTransition open={expanded}>
<div className="px-2.5 pb-2.5 pl-8 space-y-1.5 border-t">
<p className="text-muted-foreground leading-relaxed pt-2 line-clamp-6">
{material.content.slice(0, 300)}
@@ -102,7 +106,7 @@ export function MaterialItem({ material, checked, onToggle }: MaterialItemProps)
</button>
)}
</div>
)}
</CollapseTransition>
</div>
);
}
@@ -1,7 +1,9 @@
import { useState } from 'react';
import { ChevronDown, ChevronRight, FileText } from 'lucide-react';
import { ChevronRight, FileText } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { AIMaterial } from '../../types/ai-writing';
import { MaterialItem } from './MaterialItem';
import { CollapseTransition } from './CollapseTransition';
interface MaterialsCardProps {
materials: AIMaterial[];
@@ -27,14 +29,17 @@ export function MaterialsCard({ materials, selectedIds, onToggle }: MaterialsCar
(共 {materials.length} 条,已选 {selectedIds.size} 条)
</span>
</span>
{expanded
? <ChevronDown className="size-3 text-muted-foreground" />
: <ChevronRight className="size-3 text-muted-foreground" />}
<ChevronRight
className={cn(
'size-3 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
</button>
{/* 列表 */}
{expanded && (
<div className="px-2.5 pb-2.5 space-y-1.5 border-t">
<CollapseTransition open={expanded}>
<div className="px-2.5 pb-2.5 pt-1.5 space-y-1.5 border-t">
{materials.length === 0 ? (
<p className="text-xs text-muted-foreground py-2 text-center">暂无素材</p>
) : (
@@ -48,7 +53,7 @@ export function MaterialsCard({ materials, selectedIds, onToggle }: MaterialsCar
))
)}
</div>
)}
</CollapseTransition>
</div>
);
}
@@ -3,6 +3,7 @@ import { ListTree, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import type { AIOutline, OutlineSection as OutlineSectionType } from '../../types/ai-writing';
import { OutlineSection } from './OutlineSection';
import { CollapseTransition } from './CollapseTransition';
interface OutlineCardProps {
outline: AIOutline;
@@ -69,7 +70,7 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
</button>
</div>
{!collapsed && (
<CollapseTransition open={!collapsed}>
<div className="px-3 py-2.5 space-y-2">
{/* 文章标题 */}
<div className="flex items-center gap-1.5 text-xs">
@@ -137,7 +138,7 @@ export function OutlineCard({ outline, onChange, readOnly = false }: OutlineCard
</Button>
)}
</div>
)}
</CollapseTransition>
</div>
);
}
@@ -1,7 +1,8 @@
import { useState } from 'react';
import { ChevronRight, ChevronDown, Plus, X, GripVertical } from 'lucide-react';
import { ChevronRight, Plus, X, GripVertical } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { OutlineSection as OutlineSectionType } from '../../types/ai-writing';
import { CollapseTransition } from './CollapseTransition';
interface OutlineSectionProps {
section: OutlineSectionType;
@@ -72,9 +73,12 @@ export function OutlineSection({
onClick={() => setExpanded((v) => !v)}
className="shrink-0"
>
{expanded
? <ChevronDown className="size-3.5 text-muted-foreground" />
: <ChevronRight className="size-3.5 text-muted-foreground" />}
<ChevronRight
className={cn(
'size-3.5 text-muted-foreground transition-transform duration-200',
expanded && 'rotate-90',
)}
/>
</button>
<span className="text-muted-foreground/60 shrink-0 w-4 text-center">{index + 1}.</span>
@@ -109,7 +113,7 @@ export function OutlineSection({
</div>
{/* 要点列表 */}
{expanded && (
<CollapseTransition open={expanded}>
<div className="px-8 pb-2.5 space-y-1 border-t">
{section.keyPoints.map((kp, i) => (
<div key={i} className="flex items-center gap-1.5 pt-1.5">
@@ -142,7 +146,7 @@ export function OutlineSection({
</button>
)}
</div>
)}
</CollapseTransition>
</div>
);
}
+34 -29
View File
@@ -229,40 +229,45 @@ export default function ChatPage() {
/>
</div>
</div>
{mounted ? (
<InputBox
className={cn("bg-background/5 w-full -translate-y-4")}
isNewThread={isLandingState}
threadId={threadId}
autoFocus={isLandingState}
status={
thread.error
? "error"
: thread.isLoading
? "streaming"
: "ready"
}
context={settings.context}
extraHeader={
isLandingState && <Welcome mode={settings.context.mode} />
}
disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
isUploading
}
onContextChange={(context) =>
setSettings("context", context)
}
promptPrefix={promptPrefixProp}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
{isLandingState && mounted && (
<Welcome
mode={settings.context.mode}
className="mb-6 -translate-y-4"
/>
)}
{mounted ? (
<div className="w-full -translate-y-4">
<InputBox
className="w-full"
isNewThread={isLandingState}
threadId={threadId}
autoFocus={isLandingState}
status={
thread.error
? "error"
: thread.isLoading
? "streaming"
: "ready"
}
context={settings.context}
disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
isUploading
}
onContextChange={(context) =>
setSettings("context", context)
}
promptPrefix={promptPrefixProp}
onFollowupsVisibilityChange={setShowFollowups}
onSubmit={handleSubmit}
onStop={handleStop}
/>
</div>
) : (
<div
aria-hidden="true"
className={cn(
"bg-background/5 h-32 w-full -translate-y-4 rounded-2xl border",
"bg-background/5 h-32 w-full -translate-y-4 rounded-3xl border",
)}
/>
)}
+59
View File
@@ -752,3 +752,62 @@
--container-width-md: calc(var(--spacing) * 204);
--container-width-lg: calc(var(--spacing) * 256);
}
/* ── Landing 态输入框 Gemini 风格:旋转彩色描边 + 多色柔光阴影 ───────── */
/* 关键:::before 转动的彩色锥形渐变 + ::after 内嵌白板遮罩中央,只在 1.5px 边缘可见模糊彩光 */
.gemini-shadow {
box-shadow:
0 0 20px rgba(66, 133, 244, 0.10),
0 0 40px rgba(219, 68, 55, 0.05),
0 0 60px rgba(244, 180, 0, 0.05),
0 0 80px rgba(15, 157, 88, 0.05);
}
.dark .gemini-shadow {
box-shadow:
0 0 24px rgba(66, 133, 244, 0.20),
0 0 48px rgba(219, 68, 55, 0.10),
0 0 72px rgba(244, 180, 0, 0.10),
0 0 96px rgba(15, 157, 88, 0.10);
}
.gemini-gradient-border {
position: relative;
background: var(--background);
border-radius: 1.5rem;
z-index: 0;
overflow: hidden;
}
.gemini-gradient-border::before {
content: '';
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background: conic-gradient(
from 0deg,
#4285F4 0deg,
#DB4437 90deg,
#F4B400 180deg,
#0F9D58 270deg,
#4285F4 360deg
);
z-index: -2;
animation: gemini-rotate 4s linear infinite;
filter: blur(10px);
pointer-events: none;
}
.gemini-gradient-border::after {
content: '';
position: absolute;
inset: 2.5px;
background: var(--background);
border-radius: calc(1.5rem - 2px);
z-index: -1;
}
@keyframes gemini-rotate {
to {
transform: rotate(360deg);
}
}