Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca2696b23f | ||
|
|
2718915d34 | ||
|
|
ee83258b70 | ||
|
|
87505feac4 | ||
|
|
c5eefe60f0 | ||
|
|
5be21f2c3d | ||
|
|
d488e65e49 | ||
|
|
dcc3e0799d | ||
|
|
7cc6aca398 | ||
|
|
dd1064b3cb | ||
|
|
be9a490923 | ||
|
|
2ea331f9ec | ||
|
|
296974e9e5 | ||
|
|
ed5b550ead | ||
|
|
7f2c51957b | ||
|
|
644cabd23d | ||
|
|
c4d61288b4 | ||
|
|
389df2ace5 | ||
|
|
32dbdca480 | ||
|
|
28eb8f86a7 | ||
|
|
b3aa9f4bac | ||
|
|
1c0e4e9723 | ||
|
|
d19d6ced66 | ||
|
|
8e77e2da1e | ||
|
|
bc49ef6027 | ||
|
|
288ba64923 | ||
|
|
db5b6ba574 | ||
|
|
1f961ee1df | ||
|
|
6ff04ea4e3 | ||
|
|
0f667261be | ||
|
|
e412d7db3f | ||
|
|
6de3026f8f | ||
|
|
64067ea478 | ||
|
|
e0b6e1cd19 | ||
|
|
e2041c0b07 | ||
|
|
2fa7000ff7 | ||
|
|
760b88d6f1 |
No files matched your search
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" role="img" aria-labelledby="title">
|
||||||
|
<title id="title">CM 助手</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="logoTopLeft" x1="7" y1="7" x2="25" y2="20" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#5DB5FF"/>
|
||||||
|
<stop offset="1" stop-color="#328AF2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoTopRight" x1="24" y1="3" x2="43" y2="19" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#62B2FF"/>
|
||||||
|
<stop offset="1" stop-color="#4B98F4"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoLeft" x1="5" y1="14" x2="24" y2="45" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#2C7CF0"/>
|
||||||
|
<stop offset="1" stop-color="#1F5EDA"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="logoBottom" x1="9" y1="31" x2="43" y2="40" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#276DE3"/>
|
||||||
|
<stop offset="1" stop-color="#4E9DF5"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- The mark is drawn as four independent folded faces. Together they form
|
||||||
|
the open C-shaped cube used in the product reference. -->
|
||||||
|
<path d="M5 13.5 24 3v10L14 18.5 5 13.5Z" fill="url(#logoTopLeft)"/>
|
||||||
|
<path d="M24 3 43 13.5 34 18.5 24 13V3Z" fill="url(#logoTopRight)"/>
|
||||||
|
<path d="M5 13.5 14 18.5v11L24 35v10L5 34.5v-21Z" fill="url(#logoLeft)"/>
|
||||||
|
<path d="M14 29.5 24 35l10-5.5 9 5L24 45 5 34.5l9-5Z" fill="url(#logoBottom)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -27,7 +27,9 @@ import {
|
|||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||||
const SIDEBAR_WIDTH = "16rem";
|
// Keep the workbench navigation compact across every theme. The visual theme
|
||||||
|
// only changes tokens; the information density should remain consistent.
|
||||||
|
const SIDEBAR_WIDTH = "14rem";
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import agentAvatarDark from "@/assets/img/agent-avatar.png";
|
||||||
|
import agentAvatarLight from "@/assets/img/agent-avatar-red.png";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type AgentBrandIconProps = {
|
||||||
|
className?: string;
|
||||||
|
size?: "nav" | "sm" | "md" | "lg";
|
||||||
|
};
|
||||||
|
|
||||||
|
const SIZE_STYLES = {
|
||||||
|
nav: {
|
||||||
|
frame: "size-[22px] rounded-md",
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
frame: "size-10 rounded-full",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
frame: "size-12 rounded-full",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
frame: "size-16 rounded-full",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function AgentBrandIcon({
|
||||||
|
className,
|
||||||
|
size = "md",
|
||||||
|
}: AgentBrandIconProps) {
|
||||||
|
const styles = SIZE_STYLES[size];
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const avatarSrc =
|
||||||
|
theme === "light" ? agentAvatarLight : agentAvatarDark;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
data-agent-brand-icon={size}
|
||||||
|
className={cn(
|
||||||
|
"relative inline-flex shrink-0 overflow-hidden rounded-full",
|
||||||
|
styles.frame,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={avatarSrc}
|
||||||
|
alt=""
|
||||||
|
className="size-full object-contain"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BotIcon,
|
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
EyeOffIcon,
|
EyeOffIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
LockIcon,
|
LockIcon,
|
||||||
MessageSquareIcon,
|
MessageSquareIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
PinIcon,
|
PinIcon,
|
||||||
ShieldIcon,
|
ShieldIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -38,14 +38,21 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -59,7 +66,8 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { usePublishSquares } from "@/core/system-settings/hooks";
|
import { usePublishSquares } from "@/core/system-settings/hooks";
|
||||||
import { PublishSquarePickerDialog } from "@/components/workspace/publish-square-picker-dialog";
|
import { PublishSquarePickerDialog } from "@/components/workspace/publish-square-picker-dialog";
|
||||||
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog";
|
||||||
import { AgentQuestionEditor } from "@/components/workspace/agents/agent-question-editor";
|
import { AgentQuestionEditor } from "@/components/workspace/agents/agent-question-editor";
|
||||||
|
import { AgentBrandIcon } from "@/components/workspace/agents/agent-brand-icon";
|
||||||
import { SkillTransferSelector } from "@/components/workspace/agents/skill-transfer-selector";
|
import { SkillTransferSelector } from "@/components/workspace/agents/skill-transfer-selector";
|
||||||
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
import { TagChipList } from "@/components/workspace/tags/tag-chip";
|
||||||
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog";
|
||||||
@@ -548,10 +556,10 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
|||||||
内置
|
内置
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{isOwned && (
|
{isOwned && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="default"
|
variant="default"
|
||||||
className="bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300 text-xs"
|
className="border border-emerald-200 bg-emerald-50 text-xs text-emerald-700 hover:bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-500/15 dark:text-emerald-300"
|
||||||
>
|
>
|
||||||
我的
|
我的
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -559,7 +567,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
|||||||
{isFromOthers && (
|
{isFromOthers && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-blue-500/30 text-blue-700 dark:text-blue-300 text-xs"
|
className="border-primary/20 bg-primary/5 text-primary text-xs dark:border-primary/40 dark:bg-primary/10 dark:text-primary"
|
||||||
title={agent.user_id ? `来自用户 ${agent.user_id}` : "已发布"}
|
title={agent.user_id ? `来自用户 ${agent.user_id}` : "已发布"}
|
||||||
>
|
>
|
||||||
来自他人
|
来自他人
|
||||||
@@ -568,7 +576,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
|||||||
{(isOwned || isAdmin) && !isBuiltin && agent.published && !agent.is_private && (
|
{(isOwned || isAdmin) && !isBuiltin && agent.published && !agent.is_private && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
|
className="border-primary/20 bg-primary/5 text-primary text-xs dark:border-primary/40 dark:bg-primary/10 dark:text-primary"
|
||||||
title="该智能体已对所有用户可见"
|
title="该智能体已对所有用户可见"
|
||||||
>
|
>
|
||||||
<GlobeIcon className="size-3" />
|
<GlobeIcon className="size-3" />
|
||||||
@@ -663,8 +671,12 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
|||||||
const skillBadgeItems = [
|
const skillBadgeItems = [
|
||||||
...(agent.tool_groups ?? []).map((group) => ({
|
...(agent.tool_groups ?? []).map((group) => ({
|
||||||
key: `tg:${group}`,
|
key: `tg:${group}`,
|
||||||
node: (
|
node: (
|
||||||
<Badge key={`tg:${group}`} variant="outline" className="text-xs">
|
<Badge
|
||||||
|
key={`tg:${group}`}
|
||||||
|
variant="outline"
|
||||||
|
className="border-0 bg-primary/5 text-primary hover:bg-primary/10 px-2.5 py-1 text-xs font-normal"
|
||||||
|
>
|
||||||
{desensitize(group)}
|
{desensitize(group)}
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
@@ -674,8 +686,8 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
|||||||
node: (
|
node: (
|
||||||
<Badge
|
<Badge
|
||||||
key={`sk:${skill}`}
|
key={`sk:${skill}`}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="hover:bg-secondary/70 cursor-pointer text-xs"
|
className="bg-primary/5 text-primary hover:bg-primary/10 cursor-pointer border-0 px-2.5 py-1 text-xs font-normal"
|
||||||
title={t.settings.skills.viewDetailAction}
|
title={t.settings.skills.viewDetailAction}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -823,86 +835,185 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const chatButton = (
|
const chatButton = (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
variant={isList ? "default" : "outline"}
|
||||||
className={cn(isList ? "shrink-0" : "w-full")}
|
size="sm"
|
||||||
onClick={handleChat}
|
className={cn(
|
||||||
|
isList
|
||||||
|
? "bg-primary hover:bg-primary/90 shrink-0 rounded-xl px-4 font-medium text-primary-foreground"
|
||||||
|
: "border-primary/35 text-primary hover:border-primary/55 hover:bg-primary/5 h-11 w-full rounded-lg bg-white font-medium shadow-none dark:bg-slate-900 dark:text-primary",
|
||||||
|
)}
|
||||||
|
onClick={handleChat}
|
||||||
>
|
>
|
||||||
<MessageSquareIcon className="mr-1.5 h-3.5 w-3.5" />
|
<MessageSquareIcon className="mr-1.5 h-3.5 w-3.5" />
|
||||||
{t.agents.chat}
|
开始对话
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cardStatusBadge = agent.is_private ? (
|
||||||
|
<Badge className="border-0 bg-purple-50 px-2.5 py-1 text-xs font-normal text-purple-600 hover:bg-purple-50 dark:bg-purple-500/15 dark:text-purple-300">
|
||||||
|
<span className="mr-1.5 size-1.5 rounded-full bg-purple-500" />
|
||||||
|
私密
|
||||||
|
</Badge>
|
||||||
|
) : agent.published || isBuiltin ? (
|
||||||
|
<Badge className="border-0 bg-primary/5 text-primary hover:bg-primary/5 px-2.5 py-1 text-xs font-normal dark:bg-primary/15 dark:text-primary">
|
||||||
|
<span className="bg-primary mr-1.5 size-1.5 rounded-full" />
|
||||||
|
已发布
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge className="border-0 bg-emerald-50 px-2.5 py-1 text-xs font-normal text-emerald-600 hover:bg-emerald-50 dark:bg-emerald-500/15 dark:text-emerald-300">
|
||||||
|
<span className="mr-1.5 size-1.5 rounded-full bg-emerald-500" />
|
||||||
|
运行中
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedAtText = formatDateTime(agent.updated_at ?? agent.created_at);
|
||||||
|
const cardActionsMenu = (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
className="size-8 rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-900 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||||
|
title="更多操作"
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-44">
|
||||||
|
{canFavorite && (
|
||||||
|
<DropdownMenuItem onSelect={() => void handleToggleFavorite()}>
|
||||||
|
<StarIcon className={cn("size-4", isFavorite && "fill-current text-amber-500")} />
|
||||||
|
{isFavorite ? "取消收藏" : "收藏到我的"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem onSelect={() => void handleTogglePin()}>
|
||||||
|
<PinIcon className={cn("size-4", isPinned && "fill-current text-emerald-600")} />
|
||||||
|
{isPinned ? "取消置顶" : "置顶"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isAdmin && (
|
||||||
|
<DropdownMenuItem onSelect={() => void handleToggleFeatured()}>
|
||||||
|
<SparklesIcon className="size-4" />
|
||||||
|
{agent.featured_order != null ? "取消推荐" : "设为推荐"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuItem onSelect={() => setTagPickerOpen(true)}>
|
||||||
|
<TagsIcon className="size-4" />
|
||||||
|
{t.adminTags.manageTags}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{isOwned && (
|
||||||
|
<DropdownMenuItem onSelect={() => void handleTogglePublished()}>
|
||||||
|
{agent.published ? <LockIcon className="size-4" /> : <GlobeIcon className="size-4" />}
|
||||||
|
{agent.published ? "取消发布" : "发布"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{(isOwned || isAdmin) && (
|
||||||
|
<DropdownMenuItem onSelect={() => void handleTogglePrivate()}>
|
||||||
|
<ShieldIcon className="size-4" />
|
||||||
|
{agent.is_private ? "移出私密广场" : "放入私密广场"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onSelect={() => void handleDuplicate()}>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
复制智能体
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{canEdit && (
|
||||||
|
<DropdownMenuItem onSelect={() => setEditOpen(true)}>
|
||||||
|
<SquarePenIcon className="size-4" />
|
||||||
|
编辑智能体
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
onSelect={() => setDeleteOpen(true)}
|
||||||
|
>
|
||||||
|
<Trash2Icon className="size-4" />
|
||||||
|
{t.agents.delete}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
|
||||||
const cardLayout = (
|
const cardLayout = (
|
||||||
<Card
|
<Card
|
||||||
className="group flex min-w-0 flex-col transition-shadow hover:shadow-md"
|
className={cn(
|
||||||
data-agent-guide="agent-card"
|
"group flex min-h-[334px] min-w-0 flex-col gap-0 overflow-hidden rounded-2xl border-slate-200/90 bg-white py-0 shadow-[0_5px_18px_rgba(15,23,42,0.035)] transition-[background-color,border-color,box-shadow] duration-200 hover:!border-primary/30 hover:!bg-primary/5 hover:!ring-1 hover:!ring-primary/10 hover:shadow-[0_8px_24px_rgba(15,23,42,0.08)] dark:border-slate-800 dark:bg-slate-900",
|
||||||
>
|
agent.featured_order != null && "border-primary/40 ring-1 ring-primary/15 dark:border-primary/45 dark:ring-primary/20",
|
||||||
<CardHeader className="min-w-0 pb-2">
|
)}
|
||||||
<div className="flex min-w-0 items-start justify-between gap-2">
|
data-agent-guide="agent-card"
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
>
|
||||||
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
<CardHeader className="min-w-0 gap-0 px-5 pt-5 pb-2">
|
||||||
<BotIcon className="h-5 w-5" />
|
{agent.featured_order != null ? (
|
||||||
</div>
|
<>
|
||||||
<div className="min-w-0 flex-1 overflow-hidden">
|
<div className="mb-1 flex h-7 items-center justify-between">
|
||||||
<CardTitle
|
<Badge className="border border-primary/20 bg-primary/5 text-primary hover:bg-primary/5 px-2.5 py-1 text-xs font-medium dark:border-primary/40 dark:bg-primary/15 dark:text-primary">
|
||||||
className="block w-full min-w-0 overflow-hidden text-base text-ellipsis whitespace-nowrap"
|
<SparklesIcon className="mr-1 size-3.5 fill-current" />
|
||||||
title={desensitize(agent.name)}
|
推荐
|
||||||
>
|
</Badge>
|
||||||
{desensitize(agent.name)}
|
{cardActionsMenu}
|
||||||
</CardTitle>
|
</div>
|
||||||
{agent.model && (
|
<AgentBrandIcon size="lg" />
|
||||||
<Badge variant="secondary" className="mt-0.5 text-xs">
|
</>
|
||||||
{desensitize(agent.model)}
|
) : (
|
||||||
</Badge>
|
<div className="flex items-start justify-between">
|
||||||
)}
|
<AgentBrandIcon size="lg" />
|
||||||
</div>
|
{cardActionsMenu}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
|
)}
|
||||||
{statusBadges}
|
<CardTitle
|
||||||
</div>
|
className="mt-3 block w-full min-w-0 overflow-hidden text-[18px] leading-6 font-semibold tracking-[-0.015em] text-ellipsis whitespace-nowrap text-slate-950 dark:text-white"
|
||||||
</div>
|
title={desensitize(agent.name)}
|
||||||
{agent.description && (
|
>
|
||||||
<CardDescription className="mt-2 line-clamp-2 text-sm">
|
{desensitize(agent.name)}
|
||||||
{desensitize(agent.description)}
|
</CardTitle>
|
||||||
</CardDescription>
|
{agent.description && (
|
||||||
)}
|
<CardDescription className="mt-1.5 min-h-11 line-clamp-2 text-[14px] leading-[22px] text-slate-600 dark:text-slate-400">
|
||||||
{publisherNode && <div className="mt-2">{publisherNode}</div>}
|
{desensitize(agent.description)}
|
||||||
{createdAtNode && <div className="mt-1">{createdAtNode}</div>}
|
</CardDescription>
|
||||||
</CardHeader>
|
)}
|
||||||
|
</CardHeader>
|
||||||
{(agent.tags?.length ?? 0) > 0 && (
|
|
||||||
<CardContent className="pt-0 pb-2">
|
{(agent.tags?.length ?? 0) > 0 && (
|
||||||
<TagChipList tags={agent.tags!} />
|
<CardContent className="px-5 pt-0 pb-1.5">
|
||||||
|
<TagChipList tags={agent.tags!} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasSkillsOrTools && (
|
{hasSkillsOrTools && (
|
||||||
<CardContent className="pt-0 pb-2">
|
<CardContent className="px-5 pt-0 pb-2">
|
||||||
<ClampedBadgeRow items={skillBadgeItems} />
|
<ClampedBadgeRow items={skillBadgeItems} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CardFooter className="mt-auto flex flex-col items-stretch gap-2 pt-2">
|
<CardFooter className="mt-auto flex flex-col items-stretch gap-4 px-5 pt-2 pb-4">
|
||||||
{showActionRow && (
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<div className="flex flex-wrap items-center justify-end gap-1">
|
{cardStatusBadge}
|
||||||
{actionButtons}
|
<span
|
||||||
</div>
|
className="min-w-0 truncate text-xs text-slate-400"
|
||||||
)}
|
title={updatedAtText ? `更新于 ${updatedAtText}` : undefined}
|
||||||
{chatButton}
|
>
|
||||||
</CardFooter>
|
{updatedAtText ? `更新于 ${updatedAtText}` : "最近更新"}
|
||||||
|
{publisherLabel ? ` · 由 ${publisherLabel} 更新` : isBuiltin ? " · 由系统更新" : ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{chatButton}
|
||||||
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const listLayout = (
|
const listLayout = (
|
||||||
<div
|
<div
|
||||||
className="group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]"
|
className="group flex min-w-0 items-center gap-4 rounded-2xl border border-slate-200/90 bg-white p-4 shadow-[0_8px_24px_rgba(15,23,42,0.04)] transition-all hover:border-primary/25 hover:shadow-[0_12px_32px_rgba(15,23,42,0.09)] dark:border-slate-800 dark:bg-slate-900"
|
||||||
data-agent-guide="agent-card"
|
data-agent-guide="agent-card"
|
||||||
>
|
>
|
||||||
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
<AgentBrandIcon className="agent-avatar" size="md" />
|
||||||
<BotIcon className="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0 flex-1 overflow-hidden">
|
<div className="min-w-0 flex-1 overflow-hidden">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
BookOpenCheck,
|
||||||
|
Brain,
|
||||||
|
CalendarClock,
|
||||||
|
MessageSquare,
|
||||||
|
MessageSquarePlus,
|
||||||
|
PanelLeftClose,
|
||||||
|
PenLine,
|
||||||
|
Settings,
|
||||||
|
Sparkles,
|
||||||
|
UsersRound,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import type { MouseEvent } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import cmAssistantLogoDark from "@/assets/img/cm-assistant-logo.png";
|
||||||
|
import cmAssistantLogoLight from "@/assets/img/cm-assistant-logo-red.png";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
|
import { AgentBrandIcon } from "./agents/agent-brand-icon";
|
||||||
|
import { RecentChatList } from "./recent-chat-list";
|
||||||
|
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||||
|
|
||||||
|
const NEW_CHAT_PATH = "/page/workspace/chats/new";
|
||||||
|
|
||||||
|
type NavItem = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: typeof MessageSquare;
|
||||||
|
match: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NAV_ITEMS: NavItem[] = [
|
||||||
|
{ label: "新建对话", href: NEW_CHAT_PATH, icon: MessageSquarePlus, match: NEW_CHAT_PATH },
|
||||||
|
{ label: "对话", href: "/page/workspace/chats", icon: MessageSquare, match: "/page/workspace/chats" },
|
||||||
|
{ label: "智能体", href: "/page/workspace/agents", icon: Sparkles, match: "/page/workspace/agents" },
|
||||||
|
{ label: "技能", href: "/page/workspace/skills", icon: Sparkles, match: "/page/workspace/skills" },
|
||||||
|
{ label: "记忆", href: "/page/workspace/memory", icon: Brain, match: "/page/workspace/memory" },
|
||||||
|
{ label: "AI 写作", href: "/page/workspace/ai-writing", icon: PenLine, match: "/page/workspace/ai-writing" },
|
||||||
|
{
|
||||||
|
label: "多智能体规划",
|
||||||
|
href: "/page/workspace/roundtable/planning",
|
||||||
|
icon: UsersRound,
|
||||||
|
match: "/page/workspace/roundtable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "定时任务",
|
||||||
|
href: "/page/workspace/scheduled/tasks",
|
||||||
|
icon: CalendarClock,
|
||||||
|
match: "/page/workspace/scheduled",
|
||||||
|
},
|
||||||
|
{ label: "问答梳理", href: "/page/workspace/knowledge", icon: BookOpenCheck, match: "/page/workspace/knowledge" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function CmChatSidebar({
|
||||||
|
onCollapse,
|
||||||
|
collapsed = false,
|
||||||
|
forceCollapsed = false,
|
||||||
|
}: {
|
||||||
|
onCollapse?: () => void;
|
||||||
|
collapsed?: boolean;
|
||||||
|
/** Deployment-level compact navigation: retain only accessible menu icons. */
|
||||||
|
forceCollapsed?: boolean;
|
||||||
|
}) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const brandLogoSrc = theme === "light" ? cmAssistantLogoLight : cmAssistantLogoDark;
|
||||||
|
|
||||||
|
const handleNewChat = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) return;
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(NEW_CHAT_PATH, { replace: true, state: { resetAt: Date.now() } });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (collapsed) {
|
||||||
|
return (
|
||||||
|
<aside className="flex h-full min-h-0 w-14 flex-col border-r border-slate-200/80 bg-white text-slate-700 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-200">
|
||||||
|
{!forceCollapsed && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCollapse}
|
||||||
|
aria-label="展开侧边栏"
|
||||||
|
title="展开侧边栏"
|
||||||
|
className="mx-auto mt-2 flex size-11 shrink-0 items-center justify-center rounded-xl transition-colors hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 dark:hover:bg-slate-900"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={brandLogoSrc}
|
||||||
|
alt="CM 助手"
|
||||||
|
className="size-10 object-contain"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className="mx-2 mt-2 h-px shrink-0 bg-slate-200/80 dark:bg-slate-800" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<nav
|
||||||
|
className={cn(
|
||||||
|
"flex shrink-0 flex-col items-center gap-1 px-2 py-3",
|
||||||
|
forceCollapsed && "pt-4",
|
||||||
|
)}
|
||||||
|
aria-label="工作台导航"
|
||||||
|
>
|
||||||
|
{NAV_ITEMS.map((item) => {
|
||||||
|
const active = item.href === NEW_CHAT_PATH
|
||||||
|
? pathname === NEW_CHAT_PATH
|
||||||
|
: item.href === "/page/workspace/chats"
|
||||||
|
? pathname.startsWith(item.match) && pathname !== NEW_CHAT_PATH
|
||||||
|
: pathname.startsWith(item.match);
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={item.href === NEW_CHAT_PATH ? handleNewChat : undefined}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
aria-label={item.label}
|
||||||
|
title={item.label}
|
||||||
|
className={cn(
|
||||||
|
"flex size-10 items-center justify-center rounded-xl transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
|
||||||
|
active
|
||||||
|
? "bg-primary/10 text-primary dark:bg-primary/15 dark:text-primary"
|
||||||
|
: "text-slate-600 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.href === "/page/workspace/agents" ? (
|
||||||
|
<AgentBrandIcon size="nav" />
|
||||||
|
) : (
|
||||||
|
<Icon className="size-[19px]" strokeWidth={1.8} />
|
||||||
|
)}
|
||||||
|
<span className="sr-only">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="mt-auto flex shrink-0 justify-center border-t border-slate-200/80 py-3 dark:border-slate-800">
|
||||||
|
<Link
|
||||||
|
href="/page/workspace/settings"
|
||||||
|
aria-label="设置和更多"
|
||||||
|
title="设置和更多"
|
||||||
|
className="flex size-10 items-center justify-center rounded-xl text-slate-600 transition-colors hover:bg-slate-100 hover:text-slate-950 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<Settings className="size-[19px]" strokeWidth={1.8} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full min-h-0 flex-col overflow-hidden border-r border-slate-200/80 bg-white text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:text-slate-100">
|
||||||
|
<div className="flex shrink-0 items-center gap-1.5 px-4 pt-3.5 pb-2.5">
|
||||||
|
<Link href={NEW_CHAT_PATH} className="flex min-w-0 flex-1 items-center gap-2.5 outline-none focus-visible:ring-2 focus-visible:ring-primary/40">
|
||||||
|
<img
|
||||||
|
src={brandLogoSrc}
|
||||||
|
alt="CM 助手"
|
||||||
|
className="size-9 shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
<span className="truncate text-[19px] font-semibold tracking-[-0.025em]">CM 助手</span>
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCollapse}
|
||||||
|
aria-label="收起侧边栏"
|
||||||
|
title="收起侧边栏"
|
||||||
|
className="flex size-8 shrink-0 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 dark:hover:bg-slate-900 dark:hover:text-white"
|
||||||
|
>
|
||||||
|
<PanelLeftClose className="size-[18px]" strokeWidth={1.8} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="shrink-0 px-3 pt-1" aria-label="工作台导航">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{NAV_ITEMS.map((item) => {
|
||||||
|
const active = item.href === NEW_CHAT_PATH
|
||||||
|
? pathname === NEW_CHAT_PATH
|
||||||
|
: item.href === "/page/workspace/chats"
|
||||||
|
? pathname.startsWith(item.match) && pathname !== NEW_CHAT_PATH
|
||||||
|
: pathname.startsWith(item.match);
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={item.href === NEW_CHAT_PATH ? handleNewChat : undefined}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 items-center gap-3 rounded-xl px-3 text-[14px] font-medium transition-colors",
|
||||||
|
active
|
||||||
|
? "bg-primary/10 text-primary dark:bg-primary/15 dark:text-primary"
|
||||||
|
: "text-slate-600 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-300 dark:hover:bg-slate-900 dark:hover:text-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.href === "/page/workspace/agents" ? (
|
||||||
|
<AgentBrandIcon size="nav" />
|
||||||
|
) : (
|
||||||
|
<Icon className="size-[18px] shrink-0" strokeWidth={1.8} />
|
||||||
|
)}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto px-1">
|
||||||
|
<RecentChatList compact limit={5} moreHref="/page/workspace/chats" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shrink-0 border-t border-slate-200/80 px-3 py-3 dark:border-slate-800">
|
||||||
|
<WorkspaceNavMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -53,10 +53,12 @@ export function CustomPromptBar({
|
|||||||
onInsert,
|
onInsert,
|
||||||
className,
|
className,
|
||||||
currentAgentId,
|
currentAgentId,
|
||||||
|
compact = false,
|
||||||
}: {
|
}: {
|
||||||
onInsert: (content: string) => void;
|
onInsert: (content: string) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
currentAgentId?: string | null;
|
currentAgentId?: string | null;
|
||||||
|
compact?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const allPrompts = useCustomPrompts();
|
const allPrompts = useCustomPrompts();
|
||||||
const prompts = allPrompts.filter(
|
const prompts = allPrompts.filter(
|
||||||
@@ -102,7 +104,11 @@ export function CustomPromptBar({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={cn("flex min-w-0 items-center gap-1", className)}
|
className={cn(
|
||||||
|
"flex min-w-0 items-center gap-1",
|
||||||
|
compact && "min-h-9 justify-end",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-compact-guide="prompt-bar"
|
data-compact-guide="prompt-bar"
|
||||||
>
|
>
|
||||||
{prompts.length > 0 && (
|
{prompts.length > 0 && (
|
||||||
@@ -134,6 +140,7 @@ export function CustomPromptBar({
|
|||||||
title={prompt.content}
|
title={prompt.content}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-border/60 bg-muted/40 text-foreground/80 hover:bg-muted hover:text-foreground hover:border-border max-w-[200px] shrink-0 truncate rounded-full border px-3 py-1 text-xs font-medium shadow-sm transition-colors",
|
"border-border/60 bg-muted/40 text-foreground/80 hover:bg-muted hover:text-foreground hover:border-border max-w-[200px] shrink-0 truncate rounded-full border px-3 py-1 text-xs font-medium shadow-sm transition-colors",
|
||||||
|
compact && "border-slate-200 bg-white px-3.5 py-1.5 text-[12px] text-slate-600 shadow-none hover:border-primary/25 hover:bg-primary/5 hover:text-primary",
|
||||||
"dark:border-slate-600/70 dark:bg-slate-700/40 dark:text-slate-200 dark:hover:bg-slate-700",
|
"dark:border-slate-600/70 dark:bg-slate-700/40 dark:text-slate-200 dark:hover:bg-slate-700",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -162,7 +169,11 @@ export function CustomPromptBar({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="text-muted-foreground hover:text-foreground hover:bg-muted/70 h-7 w-7 shrink-0 rounded-full"
|
className={cn(
|
||||||
|
"text-muted-foreground hover:text-foreground hover:bg-muted/70 h-7 w-7 shrink-0 rounded-full",
|
||||||
|
compact &&
|
||||||
|
"h-9 w-9 rounded-xl border border-slate-200 bg-white text-slate-600 shadow-[0_6px_18px_-14px_rgba(15,23,42,0.4)] hover:border-primary/25 hover:bg-primary/5 hover:text-primary dark:border-slate-700 dark:bg-slate-900",
|
||||||
|
)}
|
||||||
title="管理常用提示词"
|
title="管理常用提示词"
|
||||||
aria-label="管理常用提示词"
|
aria-label="管理常用提示词"
|
||||||
onClick={() => setManageOpen(true)}
|
onClick={() => setManageOpen(true)}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ChatStatus } from "ai";
|
import type { ChatStatus } from "ai";
|
||||||
import {
|
import {
|
||||||
BrainCircuitIcon,
|
BarChart3Icon,
|
||||||
BookMarkedIcon,
|
BrainCircuitIcon,
|
||||||
CheckIcon,
|
BookMarkedIcon,
|
||||||
FileTextIcon,
|
CheckIcon,
|
||||||
FolderTreeIcon,
|
ClipboardCheckIcon,
|
||||||
GraduationCapIcon,
|
FileTextIcon,
|
||||||
LightbulbIcon,
|
FolderTreeIcon,
|
||||||
|
GraduationCapIcon,
|
||||||
|
LightbulbIcon,
|
||||||
|
MessageCircleQuestionIcon,
|
||||||
PaperclipIcon,
|
PaperclipIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
RefreshCwIcon,
|
||||||
ShrinkIcon,
|
ShrinkIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
@@ -106,6 +110,56 @@ import { ModeHoverGuide } from "./mode-hover-guide";
|
|||||||
import { Tooltip } from "./tooltip";
|
import { Tooltip } from "./tooltip";
|
||||||
|
|
||||||
type InputMode = "flash" | "thinking" | "pro" | "ultra";
|
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(
|
function getResolvedMode(
|
||||||
@@ -235,6 +289,7 @@ export function InputBox({
|
|||||||
const wasStreamingRef = useRef(false);
|
const wasStreamingRef = useRef(false);
|
||||||
|
|
||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
|
const [compactPromptOrder, setCompactPromptOrder] = useState(0);
|
||||||
const [pendingSuggestion, setPendingSuggestion] = useState<string | null>(
|
const [pendingSuggestion, setPendingSuggestion] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -245,6 +300,29 @@ export function InputBox({
|
|||||||
!hideAdminRecommendedQuestions &&
|
!hideAdminRecommendedQuestions &&
|
||||||
!userPreferences.hide_admin_prompt_prefix &&
|
!userPreferences.hide_admin_prompt_prefix &&
|
||||||
recommendedQuestions.length > 0;
|
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(() => {
|
useEffect(() => {
|
||||||
// Multi-round RAG is a frontend execution profile, not a model option.
|
// Multi-round RAG is a frontend execution profile, not a model option.
|
||||||
@@ -609,14 +687,53 @@ export function InputBox({
|
|||||||
}, [context.model_name, disabled, isMock, selectedSingleRoundRag, status, thread.messages, threadId]);
|
}, [context.model_name, disabled, isMock, selectedSingleRoundRag, status, thread.messages, threadId]);
|
||||||
|
|
||||||
return (
|
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 && (
|
{extraHeader && (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
{extraHeader}
|
{extraHeader}
|
||||||
</div>
|
</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]">
|
<div className="flex flex-wrap justify-center gap-2.5 px-4 mb-[10px]">
|
||||||
{recommendedQuestions.map((q) => (
|
{recommendedQuestions.map((q) => (
|
||||||
<button
|
<button
|
||||||
@@ -669,7 +786,8 @@ export function InputBox({
|
|||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{isNewThread && (
|
{isNewThread && (
|
||||||
<CustomPromptBar
|
<CustomPromptBar
|
||||||
className="px-1"
|
compact={compact}
|
||||||
|
className={cn(compact ? "px-2" : "px-1")}
|
||||||
currentAgentId={agentSelector?.selectedAgentId ?? assistantId ?? null}
|
currentAgentId={agentSelector?.selectedAgentId ?? assistantId ?? null}
|
||||||
onInsert={(content) => {
|
onInsert={(content) => {
|
||||||
const current = textInput.value?.trim() ?? "";
|
const current = textInput.value?.trim() ?? "";
|
||||||
@@ -693,7 +811,7 @@ export function InputBox({
|
|||||||
: "*:data-[slot='input-group']:rounded-3xl",
|
: "*:data-[slot='input-group']:rounded-3xl",
|
||||||
// 紧凑落地态(CM 参考图):白卡 + 细边 + 轻阴影 + 主色聚焦环(无旋转彩边)
|
// 紧凑落地态(CM 参考图):白卡 + 细边 + 轻阴影 + 主色聚焦环(无旋转彩边)
|
||||||
compact && isNewThread
|
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 提供白卡 + 旋转描边
|
: // landing 态:透明,让外层 gemini-gradient-border 提供白卡 + 旋转描边
|
||||||
// 对话态:常规白卡 + 中性边框 + 轻阴影
|
// 对话态:常规白卡 + 中性边框 + 轻阴影
|
||||||
isNewThread
|
isNewThread
|
||||||
@@ -717,7 +835,7 @@ export function InputBox({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"size-full px-2 py-1 text-[15px] leading-7 placeholder:text-muted-foreground/60",
|
"size-full px-2 py-1 text-[15px] leading-7 placeholder:text-muted-foreground/60",
|
||||||
isNewThread && "min-h-[110px]!",
|
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}
|
disabled={disabled}
|
||||||
placeholder={t.inputBox.placeholder}
|
placeholder={t.inputBox.placeholder}
|
||||||
@@ -728,7 +846,7 @@ export function InputBox({
|
|||||||
<PromptInputFooter
|
<PromptInputFooter
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center border-t-0 px-2 pt-1 pb-1.5",
|
"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
|
<PromptInputTools
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ import { useArtifactsOptional } from "../artifacts";
|
|||||||
import { CitationLink } from "../citations/citation-link";
|
import { CitationLink } from "../citations/citation-link";
|
||||||
import { FlipDisplay } from "../flip-display";
|
import { FlipDisplay } from "../flip-display";
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { SourceCard } from "../skill-display-sources";
|
import {
|
||||||
|
isWeKnoraSourcePreviewUrl,
|
||||||
|
SourceCard,
|
||||||
|
} from "../skill-display-sources";
|
||||||
|
|
||||||
import { MarkdownContent } from "./markdown-content";
|
import { MarkdownContent } from "./markdown-content";
|
||||||
import { SingleRoundRagCitationDialog } from "./message-list-item";
|
import { SingleRoundRagCitationDialog } from "./message-list-item";
|
||||||
@@ -945,19 +948,43 @@ function DisplayResultsList({ results }: { results: RuntimeDisplaySource[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchResultCard({ item }: { item: SearchResultItem }) {
|
function SearchResultCard({ item }: { item: SearchResultItem }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const content = item.snippet ?? item.content;
|
const content = item.snippet ?? item.content;
|
||||||
const date = item.published_date ?? item.publishedDate ?? item.date;
|
const date = item.published_date ?? item.publishedDate ?? item.date;
|
||||||
let sourceLabel: string | undefined = item.source;
|
let sourceLabel: string | undefined = item.source;
|
||||||
if (item.url) {
|
if (item.url) {
|
||||||
try {
|
try {
|
||||||
sourceLabel = new URL(item.url).hostname.replace(/^www\./, "");
|
sourceLabel = new URL(item.url).hostname.replace(/^www\./, "");
|
||||||
} catch {
|
} catch {
|
||||||
sourceLabel = sourceLabel ?? item.url;
|
sourceLabel = sourceLabel ?? item.url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
|
// MCP retrieval tools without a configured display adapter fall back to this
|
||||||
|
// generic result card. Preserve the normal web-search UI, but route our
|
||||||
|
// credential-free WeKnora pointer through SourceCard so its dialog exposes a
|
||||||
|
// deliberate “查看原文” action rather than rendering the internal route text.
|
||||||
|
if (isWeKnoraSourcePreviewUrl(item.url)) {
|
||||||
|
const weknoraSource: RuntimeDisplaySource = {
|
||||||
|
id: item.id ?? item.uuid ?? item.docId,
|
||||||
|
title: item.title ?? "检索结果",
|
||||||
|
type: item.source ?? "document",
|
||||||
|
snippet: item.snippet ?? item.content ?? "",
|
||||||
|
content,
|
||||||
|
source: item.source,
|
||||||
|
author: item.author,
|
||||||
|
time: date,
|
||||||
|
url: item.url,
|
||||||
|
index: 0,
|
||||||
|
skillName: "weknora-knowledge-search",
|
||||||
|
mode: "list",
|
||||||
|
raw: { ...item },
|
||||||
|
};
|
||||||
|
return <SourceCard source={weknoraSource} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ import {
|
|||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
TrendingUpIcon,
|
TrendingUpIcon,
|
||||||
Upload,
|
Upload,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
|
WorkflowIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@@ -50,13 +51,6 @@ import {
|
|||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from "@/components/ui/empty";
|
} from "@/components/ui/empty";
|
||||||
|
|
||||||
import {
|
|
||||||
Item,
|
|
||||||
ItemActions,
|
|
||||||
ItemTitle,
|
|
||||||
ItemContent,
|
|
||||||
ItemDescription,
|
|
||||||
} from "@/components/ui/item";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
@@ -756,7 +750,7 @@ export function SkillRow({
|
|||||||
|
|
||||||
const badgeGroup = (
|
const badgeGroup = (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className={cn(isCard && "truncate font-medium")}>
|
<span className="min-w-0 truncate text-[15px] font-semibold tracking-[-0.01em] text-slate-900 dark:text-slate-100">
|
||||||
{skill.name_zh?.trim() ? skill.name_zh : desensitize(skill.name)}
|
{skill.name_zh?.trim() ? skill.name_zh : desensitize(skill.name)}
|
||||||
</span>
|
</span>
|
||||||
{skill.name_zh?.trim() && (
|
{skill.name_zh?.trim() && (
|
||||||
@@ -861,65 +855,12 @@ export function SkillRow({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const metaNodes = (
|
|
||||||
<>
|
|
||||||
{!isBuiltin && skill.owner_username && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={!skill.owner_user_id || !onPublisherClick}
|
|
||||||
onClick={(e) => {
|
|
||||||
if (!skill.owner_user_id || !onPublisherClick) return;
|
|
||||||
e.stopPropagation();
|
|
||||||
onPublisherClick({
|
|
||||||
id: skill.owner_user_id,
|
|
||||||
label: desensitize(skill.owner_username!),
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground mt-1 flex max-w-full items-center gap-1 text-sm font-medium",
|
|
||||||
skill.owner_user_id && onPublisherClick &&
|
|
||||||
"hover:text-primary cursor-pointer transition-colors hover:underline",
|
|
||||||
)}
|
|
||||||
title={
|
|
||||||
skill.owner_user_id && onPublisherClick
|
|
||||||
? `点击筛选发布者:${desensitize(skill.owner_username)}`
|
|
||||||
: `发布者:${desensitize(skill.owner_username)}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<UserIcon className="size-3.5 shrink-0" />
|
|
||||||
<span className="truncate">发布者:{desensitize(skill.owner_username)}</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{skill.use_count !== undefined && (
|
|
||||||
<div className="text-muted-foreground mt-1 text-xs">
|
|
||||||
{skill.use_count > 0
|
|
||||||
? t.settings.skills.useCountInline.replace(
|
|
||||||
"{count}",
|
|
||||||
String(skill.use_count),
|
|
||||||
)
|
|
||||||
: t.settings.skills.useCountInlineZero}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{formatDateTime(skill.created_at) && (
|
|
||||||
<div className="text-muted-foreground mt-1 flex items-center gap-1 text-xs">
|
|
||||||
<ClockIcon className="size-3.5 shrink-0" />
|
|
||||||
<span className="truncate">
|
|
||||||
创建时间:{formatDateTime(skill.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(skill.tags?.length ?? 0) > 0 && (
|
|
||||||
<TagChipList tags={skill.tags!} className="mt-2" />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const actionButtons = (
|
const actionButtons = (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8 shrink-0 rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||||
onClick={() => setTagPickerOpen(true)}
|
onClick={() => setTagPickerOpen(true)}
|
||||||
title={t.adminTags.manageTags}
|
title={t.adminTags.manageTags}
|
||||||
>
|
>
|
||||||
@@ -930,7 +871,7 @@ export function SkillRow({
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-8 w-8 shrink-0",
|
"h-8 w-8 shrink-0 rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white",
|
||||||
skill.featured && "text-amber-600 hover:text-amber-700",
|
skill.featured && "text-amber-600 hover:text-amber-700",
|
||||||
)}
|
)}
|
||||||
disabled={setFeatured.isPending}
|
disabled={setFeatured.isPending}
|
||||||
@@ -951,7 +892,7 @@ export function SkillRow({
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-8 w-8 shrink-0",
|
"h-8 w-8 shrink-0 rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white",
|
||||||
skill.pinned && "text-emerald-600 hover:text-emerald-700",
|
skill.pinned && "text-emerald-600 hover:text-emerald-700",
|
||||||
)}
|
)}
|
||||||
disabled={setPinned.isPending}
|
disabled={setPinned.isPending}
|
||||||
@@ -972,7 +913,7 @@ export function SkillRow({
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-8 w-8 shrink-0",
|
"h-8 w-8 shrink-0 rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white",
|
||||||
skill.always_on && "text-amber-600 hover:text-amber-700",
|
skill.always_on && "text-amber-600 hover:text-amber-700",
|
||||||
)}
|
)}
|
||||||
disabled={setAlwaysOn.isPending}
|
disabled={setAlwaysOn.isPending}
|
||||||
@@ -992,7 +933,7 @@ export function SkillRow({
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8 shrink-0 rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||||
disabled={setPublished.isPending}
|
disabled={setPublished.isPending}
|
||||||
onClick={() => void handleTogglePublished(!skill.published)}
|
onClick={() => void handleTogglePublished(!skill.published)}
|
||||||
title={
|
title={
|
||||||
@@ -1011,7 +952,7 @@ export function SkillRow({
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8 shrink-0 rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-950 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
|
||||||
disabled={duplicateSkillMutation.isPending}
|
disabled={duplicateSkillMutation.isPending}
|
||||||
onClick={handleOpenDuplicate}
|
onClick={handleOpenDuplicate}
|
||||||
title="复制为新技能"
|
title="复制为新技能"
|
||||||
@@ -1022,7 +963,7 @@ export function SkillRow({
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
|
className="text-destructive hover:text-destructive h-8 w-8 shrink-0 rounded-lg hover:bg-red-50 dark:hover:bg-red-950/30"
|
||||||
onClick={() => setConfirmDeleteOpen(true)}
|
onClick={() => setConfirmDeleteOpen(true)}
|
||||||
title={t.settings.skills.deleteAction}
|
title={t.settings.skills.deleteAction}
|
||||||
>
|
>
|
||||||
@@ -1031,39 +972,125 @@ export function SkillRow({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const iconStyle = skill.name.includes("web") || skill.name.includes("search")
|
||||||
|
? {
|
||||||
|
Icon: GlobeIcon,
|
||||||
|
className: "bg-emerald-600 text-white shadow-[0_6px_16px_rgba(22,163,74,0.22)]",
|
||||||
|
}
|
||||||
|
: skill.name.includes("doc") || skill.name.includes("file")
|
||||||
|
? {
|
||||||
|
Icon: FileArchive,
|
||||||
|
className: "bg-violet-600 text-white shadow-[0_6px_16px_rgba(124,58,237,0.22)]",
|
||||||
|
}
|
||||||
|
: skill.source === "agent"
|
||||||
|
? {
|
||||||
|
Icon: WorkflowIcon,
|
||||||
|
className: "bg-[#2166e8] text-white shadow-[0_6px_16px_rgba(37,99,235,0.22)]",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
Icon: Upload,
|
||||||
|
className: "bg-amber-500 text-white shadow-[0_6px_16px_rgba(245,158,11,0.22)]",
|
||||||
|
};
|
||||||
|
const SkillIcon = iconStyle.Icon;
|
||||||
|
const activityAt = formatDateTime(skill.last_activity_at ?? skill.created_at);
|
||||||
|
const actionArea = (
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap items-center justify-end gap-1"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
{actionButtons}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isCard ? (
|
{isCard ? (
|
||||||
<div className="flex h-full min-w-0 flex-col rounded-lg border p-3 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
<div className="flex h-full min-w-0 flex-col rounded-xl border border-slate-200 bg-white p-5 shadow-[0_5px_18px_rgba(15,23,42,0.035)] transition-[border-color,box-shadow] hover:border-blue-200 hover:shadow-[0_10px_28px_rgba(37,99,235,0.07)] dark:border-slate-800 dark:bg-slate-900 dark:text-white">
|
||||||
<div
|
<div
|
||||||
className="min-w-0 flex-1 cursor-pointer"
|
className="min-w-0 flex-1 cursor-pointer"
|
||||||
onClick={() => setDetailOpen(true)}
|
onClick={() => setDetailOpen(true)}
|
||||||
>
|
>
|
||||||
{badgeGroup}
|
<div className="flex items-start gap-3.5">
|
||||||
<p className="text-muted-foreground mt-1 line-clamp-3 text-sm">
|
<div className={cn("flex size-12 shrink-0 items-center justify-center rounded-full", iconStyle.className)}>
|
||||||
{desensitize(skill.description ?? "")}
|
<SkillIcon className="size-6" />
|
||||||
</p>
|
</div>
|
||||||
{metaNodes}
|
<div className="min-w-0 flex-1">
|
||||||
|
{badgeGroup}
|
||||||
|
<p className="mt-1.5 line-clamp-3 text-sm leading-6 text-slate-600 dark:text-slate-400">
|
||||||
|
{desensitize(skill.description ?? "")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||||
|
<Badge className={cn("border-0 px-2.5 py-1 text-xs font-normal", skill.enabled ? "bg-emerald-50 text-emerald-600 hover:bg-emerald-50 dark:bg-emerald-500/15 dark:text-emerald-300" : "bg-slate-100 text-slate-500 hover:bg-slate-100 dark:bg-slate-800 dark:text-slate-400")}>
|
||||||
|
<span className={cn("mr-1.5 size-1.5 rounded-full", skill.enabled ? "bg-emerald-500" : "bg-slate-400")} />
|
||||||
|
{skill.enabled ? "已启用" : "未启用"}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-slate-400">{skill.use_count ? `调用 ${skill.use_count} 次` : "暂无调用"}</span>
|
||||||
|
{activityAt && <span className="text-xs text-slate-400">更新于 {activityAt}</span>}
|
||||||
|
</div>
|
||||||
|
{(skill.tags?.length ?? 0) > 0 && <TagChipList tags={skill.tags!} className="mt-3" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex flex-wrap items-center justify-end gap-1 border-t pt-2 dark:border-[#213D5E]">
|
<div className="mt-4 border-t border-slate-100 pt-3 dark:border-slate-800">
|
||||||
{actionButtons}
|
{actionArea}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
|
<div className="group/skill-card w-full rounded-xl border border-slate-200 bg-white px-5 py-4 shadow-[0_5px_18px_rgba(15,23,42,0.03)] transition-[border-color,box-shadow] hover:border-blue-200 hover:shadow-[0_10px_28px_rgba(37,99,235,0.07)] dark:border-slate-800 dark:bg-slate-900 dark:text-white">
|
||||||
<ItemContent
|
<div className="grid min-w-0 grid-cols-1 gap-4 xl:grid-cols-[minmax(0,1.65fr)_minmax(150px,0.72fr)_minmax(100px,0.42fr)_minmax(132px,0.58fr)_auto] xl:items-center xl:gap-x-6">
|
||||||
className="cursor-pointer"
|
<div className="flex min-w-0 cursor-pointer items-start gap-4" onClick={() => setDetailOpen(true)}>
|
||||||
onClick={() => setDetailOpen(true)}
|
<div className={cn("flex size-[60px] shrink-0 items-center justify-center rounded-full", iconStyle.className)}>
|
||||||
>
|
<SkillIcon className="size-7" />
|
||||||
<ItemTitle>{badgeGroup}</ItemTitle>
|
</div>
|
||||||
<ItemDescription className="line-clamp-4">
|
<div className="min-w-0 flex-1 pt-0.5">
|
||||||
{desensitize(skill.description ?? "")}
|
{badgeGroup}
|
||||||
</ItemDescription>
|
<p className="mt-1.5 line-clamp-2 text-sm leading-5 text-slate-600 dark:text-slate-400">
|
||||||
{metaNodes}
|
{desensitize(skill.description ?? "")}
|
||||||
</ItemContent>
|
</p>
|
||||||
<ItemActions>{actionButtons}</ItemActions>
|
{!isBuiltin && skill.owner_username && (
|
||||||
</Item>
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!skill.owner_user_id || !onPublisherClick}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!skill.owner_user_id || !onPublisherClick) return;
|
||||||
|
onPublisherClick({ id: skill.owner_user_id, label: desensitize(skill.owner_username!) });
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 flex max-w-full items-center gap-1 text-xs text-slate-400",
|
||||||
|
skill.owner_user_id && onPublisherClick && "cursor-pointer hover:text-blue-600 hover:underline",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<UserIcon className="size-3.5 shrink-0" />
|
||||||
|
<span className="truncate">发布者:{desensitize(skill.owner_username)}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 xl:border-l xl:border-slate-100 xl:pl-5 xl:dark:border-slate-800">
|
||||||
|
{(skill.tags?.length ?? 0) > 0 ? (
|
||||||
|
<TagChipList tags={skill.tags!} />
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-slate-400">未设置标签</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
<span className="block text-xs text-slate-400 xl:hidden">使用次数</span>
|
||||||
|
{skill.use_count ? `${skill.use_count} 次` : "暂无调用"}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<Badge className={cn("border-0 px-2.5 py-1 text-xs font-normal", skill.enabled ? "bg-emerald-50 text-emerald-600 hover:bg-emerald-50 dark:bg-emerald-500/15 dark:text-emerald-300" : "bg-slate-100 text-slate-500 hover:bg-slate-100 dark:bg-slate-800 dark:text-slate-400")}>
|
||||||
|
<span className={cn("mr-1.5 size-1.5 rounded-full", skill.enabled ? "bg-emerald-500" : "bg-slate-400")} />
|
||||||
|
{skill.enabled ? "已启用" : "未启用"}
|
||||||
|
</Badge>
|
||||||
|
{activityAt && <p className="mt-2 truncate text-xs text-slate-400" title={activityAt}>更新于 {activityAt}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 border-t border-slate-100 pt-3 xl:border-t-0 xl:pt-0 dark:border-slate-800">
|
||||||
|
{actionArea}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
|
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import type { RuntimeDisplaySource } from "@/core/skill-display/runtime";
|
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 { useLocalSettings } from "@/core/settings";
|
||||||
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||||
import { reasoningPlugins } from "@/core/streamdown";
|
import { reasoningPlugins } from "@/core/streamdown";
|
||||||
@@ -18,6 +21,11 @@ import { useState } from "react";
|
|||||||
import { WeKnoraSourceDrawer } from "@/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer";
|
import { WeKnoraSourceDrawer } from "@/strategy-components/components/resource-management/llmwiki/WeKnoraSourceDrawer";
|
||||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||||
|
|
||||||
|
/** Whether a source URL is an internal WeKnora preview pointer. */
|
||||||
|
export function isWeKnoraSourcePreviewUrl(url?: string | null): boolean {
|
||||||
|
return parseWeKnoraPreviewUrl(url) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
export function SkillDisplaySources({
|
export function SkillDisplaySources({
|
||||||
highlightedReferenceId,
|
highlightedReferenceId,
|
||||||
sources,
|
sources,
|
||||||
@@ -157,10 +165,10 @@ export function SourceCard({
|
|||||||
"mt-0.5 inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-[11px] font-medium",
|
"mt-0.5 inline-flex shrink-0 items-center gap-1 rounded-md px-1.5 py-1 text-[11px] font-medium",
|
||||||
isWikiSource
|
isWikiSource
|
||||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
|
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
|
||||||
: "bg-muted text-muted-foreground",
|
: "bg-primary/10 text-primary",
|
||||||
)}>
|
)}>
|
||||||
{isWikiSource ? <BookOpenTextIcon className="size-3.5" /> : <FileTextIcon className="size-3.5" />}
|
{isWikiSource ? <BookOpenTextIcon className="size-3.5" /> : <FileTextIcon className="size-3.5" />}
|
||||||
{isWikiSource ? "Wiki 文档" : "知识库文档"}
|
{isWikiSource ? "Wiki 文档" : "查看原文"}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<ExternalLinkIcon className="text-muted-foreground/60 group-hover:text-primary mt-1 size-3.5 shrink-0 transition-colors" />
|
<ExternalLinkIcon className="text-muted-foreground/60 group-hover:text-primary mt-1 size-3.5 shrink-0 transition-colors" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { User } from "lucide-react";
|
import { UserRound } from "lucide-react";
|
||||||
|
|
||||||
import { useLiveAccount } from "@/core/auth/use-live-account";
|
import { useLiveAccount } from "@/core/auth/use-live-account";
|
||||||
import { useMyPosition } from "@/core/positions";
|
import { useMyPosition } from "@/core/positions";
|
||||||
@@ -15,7 +15,13 @@ import { cn } from "@/lib/utils";
|
|||||||
* 配色按当前内容区前景色(foreground / muted-foreground),适配聊天页浅底顶栏,
|
* 配色按当前内容区前景色(foreground / muted-foreground),适配聊天页浅底顶栏,
|
||||||
* 而非 Header 的白字贴 banner 方案。
|
* 而非 Header 的白字贴 banner 方案。
|
||||||
*/
|
*/
|
||||||
export function UserIdentityBadge({ className }: { className?: string }) {
|
export function UserIdentityBadge({
|
||||||
|
className,
|
||||||
|
variant = "plain",
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
variant?: "plain" | "topbar";
|
||||||
|
}) {
|
||||||
// userInfo belongs to external-app route parameters and can legitimately
|
// userInfo belongs to external-app route parameters and can legitimately
|
||||||
// outlive a login switch. The visible account name must instead match the
|
// outlive a login switch. The visible account name must instead match the
|
||||||
// settings page's current auth identity.
|
// settings page's current auth identity.
|
||||||
@@ -26,13 +32,33 @@ export function UserIdentityBadge({ className }: { className?: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex shrink-0 items-center gap-2", className)}
|
data-workspace-user={variant}
|
||||||
|
className={cn(
|
||||||
|
"flex shrink-0 items-center gap-2",
|
||||||
|
variant === "topbar" &&
|
||||||
|
"h-10 max-w-[240px] rounded-lg px-1.5 py-1 text-slate-700 dark:text-slate-200",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
title={positionName ? `${displayName}·${positionName}` : displayName}
|
title={positionName ? `${displayName}·${positionName}` : displayName}
|
||||||
>
|
>
|
||||||
<div className="bg-muted text-muted-foreground flex size-7 shrink-0 items-center justify-center rounded-full">
|
<div
|
||||||
<User className="size-3.5" />
|
className={cn(
|
||||||
|
"bg-muted text-muted-foreground relative flex size-7 shrink-0 items-center justify-center rounded-full",
|
||||||
|
variant === "topbar" &&
|
||||||
|
"bg-primary text-primary-foreground size-8 shadow-[0_4px_12px_-6px_rgba(15,23,42,0.26)] ring-1 ring-primary/15",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<UserRound className={cn("size-3.5", variant === "topbar" && "size-4")} />
|
||||||
|
{variant === "topbar" && (
|
||||||
|
<span className="absolute right-0 bottom-0 size-2.5 rounded-full border-2 border-white bg-emerald-500 dark:border-slate-950" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden min-w-0 text-left sm:block">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"hidden min-w-0 text-left sm:block",
|
||||||
|
variant === "topbar" && "max-w-[165px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<p className="text-foreground truncate text-sm font-medium leading-tight">
|
<p className="text-foreground truncate text-sm font-medium leading-tight">
|
||||||
{displayName}
|
{displayName}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { env } from "@/env";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ThemeSwitcher } from "@/components/workspace/appearance-controls";
|
||||||
|
import { UserIdentityBadge } from "@/components/workspace/user-identity-badge";
|
||||||
|
|
||||||
export function WorkspaceContainer({
|
export function WorkspaceContainer({
|
||||||
className,
|
className,
|
||||||
@@ -93,9 +96,19 @@ export function WorkspaceHeader({
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
{rightActions ? (
|
<div className="flex items-center gap-2 px-4">
|
||||||
<div className="flex items-center gap-2 px-4">{rightActions}</div>
|
{rightActions}
|
||||||
) : null}
|
{!env.FORCE_COLLAPSED_SIDEBAR && (
|
||||||
|
<>
|
||||||
|
<ThemeSwitcher tone="content" />
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<UserIdentityBadge variant="topbar" />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -123,7 +136,8 @@ function nameOfSegment(
|
|||||||
t: ReturnType<typeof useI18n>["t"],
|
t: ReturnType<typeof useI18n>["t"],
|
||||||
) {
|
) {
|
||||||
if (!segment) return t.common.home;
|
if (!segment) return t.common.home;
|
||||||
if (segment === "workspace") return t.breadcrumb.workspace;
|
if (segment === "workspace") return t.breadcrumb.workspace;
|
||||||
if (segment === "chats") return t.breadcrumb.chats;
|
if (segment === "chats") return t.breadcrumb.chats;
|
||||||
return segment[0]?.toUpperCase() + segment.slice(1);
|
if (segment === "agents") return t.agents.title;
|
||||||
}
|
return segment[0]?.toUpperCase() + segment.slice(1);
|
||||||
|
}
|
||||||
@@ -5,21 +5,26 @@ import Link from "next/link";
|
|||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useSidebar } from "@/components/ui/sidebar";
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
|
import cmAssistantLogoDark from "@/assets/img/cm-assistant-logo.png";
|
||||||
|
import cmAssistantLogoLight from "@/assets/img/cm-assistant-logo-red.png";
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
function BrandMark({ className }: { className?: string }) {
|
function BrandMark({ className }: { className?: string }) {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const brandLogoSrc =
|
||||||
|
theme === "dark"
|
||||||
|
? cmAssistantLogoDark
|
||||||
|
: cmAssistantLogoLight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<img
|
||||||
className={cn(
|
src={brandLogoSrc}
|
||||||
"bg-primary text-primary-foreground flex size-9 shrink-0 items-center justify-center rounded-lg text-sm font-bold shadow-sm",
|
alt="CM 助手"
|
||||||
className,
|
className={cn("size-9 shrink-0 object-contain", className)}
|
||||||
)}
|
/>
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
C
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
SidebarFooter,
|
SidebarFooter,
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import { GeneralQaSidebar } from "./general-qa-sidebar";
|
import { GeneralQaSidebar } from "./general-qa-sidebar";
|
||||||
import { RecentChatList } from "./recent-chat-list";
|
import { RecentChatList } from "./recent-chat-list";
|
||||||
import { RecentRoundtableList } from "./recent-roundtable-list";
|
import { RecentRoundtableList } from "./recent-roundtable-list";
|
||||||
import { RecentWritingList } from "./recent-writing-list";
|
import { RecentWritingList } from "./recent-writing-list";
|
||||||
import { WorkspaceHeader } from "./workspace-header";
|
import { WorkspaceHeader } from "./workspace-header";
|
||||||
import { WorkspaceNavChatList } from "./workspace-nav-chat-list";
|
import { WorkspaceNavChatList } from "./workspace-nav-chat-list";
|
||||||
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||||
|
|
||||||
export function WorkspaceSidebar({
|
export function WorkspaceSidebar({
|
||||||
compactChrome = false,
|
compactChrome = false,
|
||||||
forceCollapsed = false,
|
forceCollapsed = false,
|
||||||
@@ -29,13 +29,13 @@ export function WorkspaceSidebar({
|
|||||||
compactChrome?: boolean;
|
compactChrome?: boolean;
|
||||||
forceCollapsed?: boolean;
|
forceCollapsed?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { open: isSidebarOpen } = useSidebar();
|
const { open: isSidebarOpen } = useSidebar();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const useWhiteSidebar = compactChrome && theme === "light";
|
const useWhiteSidebar = compactChrome && theme === "light";
|
||||||
// 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。
|
// 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。
|
||||||
// 仅在通用问答(/page/workspace/chats*)下启用三块式布局;点击常用智能体后是
|
// 仅在通用问答(/page/workspace/chats*)下启用三块式布局;点击常用智能体后是
|
||||||
// 「原地嵌入」(停留在 /chats/new?agent=...,仅右侧问答区换成智能体对话),左侧栏不变。
|
// 「原地嵌入」(停留在 /chats/new?agent=...,仅右侧问答区换成智能体对话),左侧栏不变。
|
||||||
const isGeneralQa = pathname.startsWith("/page/workspace/chats");
|
const isGeneralQa = pathname.startsWith("/page/workspace/chats");
|
||||||
|
|
||||||
if (forceCollapsed) {
|
if (forceCollapsed) {
|
||||||
@@ -56,85 +56,85 @@ export function WorkspaceSidebar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。
|
// 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。
|
||||||
// collapsible="none" 的 Sidebar 是普通 in-flow 定宽块(非 absolute,故不受本布局对
|
// collapsible="none" 的 Sidebar 是普通 in-flow 定宽块(非 absolute,故不受本布局对
|
||||||
// sidebar-container 的 absolute/left-0 覆盖影响),外层 overflow-hidden 把它从
|
// sidebar-container 的 absolute/left-0 覆盖影响),外层 overflow-hidden 把它从
|
||||||
// --sidebar-width 裁到 0、聊天区随宽度平滑回流;收起后整列不接收指针。
|
// --sidebar-width 裁到 0、聊天区随宽度平滑回流;收起后整列不接收指针。
|
||||||
if (compactChrome) {
|
if (compactChrome) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
aria-hidden={!isSidebarOpen}
|
aria-hidden={!isSidebarOpen}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out",
|
"h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out",
|
||||||
isSidebarOpen ? "w-(--sidebar-width)" : "w-0 pointer-events-none",
|
isSidebarOpen ? "w-(--sidebar-width)" : "w-0 pointer-events-none",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
variant="sidebar"
|
variant="sidebar"
|
||||||
collapsible="none"
|
collapsible="none"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-(--sidebar-width) border-r border-border",
|
"h-full w-(--sidebar-width) border-r border-border",
|
||||||
useWhiteSidebar &&
|
useWhiteSidebar &&
|
||||||
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
{isGeneralQa ? (
|
{isGeneralQa ? (
|
||||||
// 全量版通用问答(/page/workspace/chats*):新建对话侧栏独立成
|
// 全量版通用问答(/page/workspace/chats*):新建对话侧栏独立成
|
||||||
// GeneralQaSidebar(按 0616 样式参考图重画),与其余 workspace 页解耦。
|
// GeneralQaSidebar(按 0616 样式参考图重画),与其余 workspace 页解耦。
|
||||||
<GeneralQaSidebar />
|
<GeneralQaSidebar />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<WorkspaceNavChatList showCollapseTrigger compact />
|
<WorkspaceNavChatList showCollapseTrigger compact />
|
||||||
<RecentChatList compact />
|
<RecentChatList compact />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar
|
<Sidebar
|
||||||
variant="sidebar"
|
variant="sidebar"
|
||||||
// 紧凑模式:展开时为固定宽度整列(none,无内置折叠),收起由上面的条件渲染处理;
|
// 紧凑模式:展开时为固定宽度整列(none,无内置折叠),收起由上面的条件渲染处理;
|
||||||
// 非紧凑沿用 icon。
|
// 非紧凑沿用 icon。
|
||||||
collapsible={compactChrome ? "none" : "icon"}
|
collapsible={compactChrome ? "none" : "icon"}
|
||||||
className={cn(
|
className={cn(
|
||||||
useWhiteSidebar &&
|
useWhiteSidebar &&
|
||||||
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{!compactChrome && (
|
{!compactChrome && (
|
||||||
<SidebarHeader className="py-0">
|
<SidebarHeader className="py-0">
|
||||||
<WorkspaceHeader />
|
<WorkspaceHeader />
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
)}
|
)}
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<WorkspaceNavChatList
|
<WorkspaceNavChatList
|
||||||
showCollapseTrigger={compactChrome}
|
showCollapseTrigger={compactChrome}
|
||||||
compact={compactChrome}
|
compact={compactChrome}
|
||||||
/>
|
/>
|
||||||
{isSidebarOpen && <RecentChatList compact={compactChrome} />}
|
{isSidebarOpen && <RecentChatList compact={compactChrome} />}
|
||||||
{/* 「多智能体会商」「最近写作」与「智能体/技能」等菜单项保持一致的显隐:
|
{/* 「多智能体会商」「最近写作」与「智能体/技能」等菜单项保持一致的显隐:
|
||||||
仅在非紧凑模式(!compactChrome)下展示,紧凑模式(有外层页面侧栏时)一并隐藏。 */}
|
仅在非紧凑模式(!compactChrome)下展示,紧凑模式(有外层页面侧栏时)一并隐藏。 */}
|
||||||
{isSidebarOpen && !compactChrome && <RecentRoundtableList />}
|
{isSidebarOpen && !compactChrome && <RecentRoundtableList />}
|
||||||
{isSidebarOpen && !compactChrome && <RecentWritingList />}
|
{isSidebarOpen && !compactChrome && <RecentWritingList />}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
{!compactChrome && (
|
{!compactChrome && (
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<WorkspaceNavMenu />
|
<WorkspaceNavMenu />
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
)}
|
)}
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
|
import { authStorageGet, authStorageSet } from "@/core/auth/scoped-storage";
|
||||||
|
import type { ThemeType } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
export const EXTERNAL_LOGIN_STORAGE_KEYS = {
|
export const EXTERNAL_LOGIN_STORAGE_KEYS = {
|
||||||
ticket: "login.ticket",
|
ticket: "login.ticket",
|
||||||
@@ -58,6 +59,21 @@ export function readExternalLoginParams(): Required<ExternalLoginParams> {
|
|||||||
* - ``blue`` → 暂无该主题,先用浅色代替(命中 else 分支)
|
* - ``blue`` → 暂无该主题,先用浅色代替(命中 else 分支)
|
||||||
* 其余/缺省同样回退浅色。将来若新增 blue 主题,在此扩展映射即可。
|
* 其余/缺省同样回退浅色。将来若新增 blue 主题,在此扩展映射即可。
|
||||||
*/
|
*/
|
||||||
export function resolveThemeFromQuery(themeParam: string | null | undefined): "dark" | "light" {
|
export function resolveThemeFromQuery(themeParam: string | null | undefined): ThemeType {
|
||||||
return themeParam === "dark-blue" ? "dark" : "light";
|
switch (themeParam?.trim().toLowerCase()) {
|
||||||
|
case "dark":
|
||||||
|
case "dark-blue":
|
||||||
|
case "darkblue":
|
||||||
|
return "dark";
|
||||||
|
case "blue":
|
||||||
|
case "lightblue":
|
||||||
|
case "light-blue":
|
||||||
|
return "lightblue";
|
||||||
|
case "green":
|
||||||
|
case "lightgreen":
|
||||||
|
case "light-green":
|
||||||
|
return "lightgreen";
|
||||||
|
default:
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import type { ThemeType } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 公开页(被第三方 iframe 嵌套)的可配置外观:背景色 + 整体明暗主题。
|
* 公开页(被第三方 iframe 嵌套)的可配置外观:背景色 + 整体明暗主题。
|
||||||
@@ -63,19 +64,25 @@ function readFromSearch(search: string): PublicBackground {
|
|||||||
*/
|
*/
|
||||||
export function normalizePublicTheme(
|
export function normalizePublicTheme(
|
||||||
raw: string | null | undefined,
|
raw: string | null | undefined,
|
||||||
): "dark" | "light" | undefined {
|
): ThemeType | undefined {
|
||||||
const value = raw?.trim().toLowerCase();
|
const value = raw?.trim().toLowerCase();
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
if (value === "dark" || value === "dark-blue" || value === "深蓝" || value === "深色") {
|
if (value === "dark" || value === "dark-blue" || value === "深蓝" || value === "深色") {
|
||||||
return "dark";
|
return "dark";
|
||||||
}
|
}
|
||||||
|
if (value === "blue" || value === "lightblue" || value === "light-blue" || value === "浅蓝") {
|
||||||
|
return "lightblue";
|
||||||
|
}
|
||||||
|
if (value === "green" || value === "lightgreen" || value === "light-green" || value === "绿色") {
|
||||||
|
return "lightgreen";
|
||||||
|
}
|
||||||
if (value === "light" || value === "red" || value === "红白" || value === "浅色") {
|
if (value === "light" || value === "red" || value === "红白" || value === "浅色") {
|
||||||
return "light";
|
return "light";
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readThemeFromSearch(search: string): "dark" | "light" | undefined {
|
function readThemeFromSearch(search: string): ThemeType | undefined {
|
||||||
const params = new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
|
const params = new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
|
||||||
return normalizePublicTheme(params.get("theme"));
|
return normalizePublicTheme(params.get("theme"));
|
||||||
}
|
}
|
||||||
@@ -86,7 +93,7 @@ function readThemeFromSearch(search: string): "dark" | "light" | undefined {
|
|||||||
* 与 `usePublicBackground` 同样合并两处查询串(hash 路由内优先),
|
* 与 `usePublicBackground` 同样合并两处查询串(hash 路由内优先),
|
||||||
* postMessage 的动态值优先级最高。返回 undefined 表示外部未指定。
|
* postMessage 的动态值优先级最高。返回 undefined 表示外部未指定。
|
||||||
*/
|
*/
|
||||||
export function usePublicEmbedTheme(): "dark" | "light" | undefined {
|
export function usePublicEmbedTheme(): ThemeType | undefined {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const fromUrl = useMemo(() => {
|
const fromUrl = useMemo(() => {
|
||||||
@@ -97,7 +104,7 @@ export function usePublicEmbedTheme(): "dark" | "light" | undefined {
|
|||||||
return inner ?? outer;
|
return inner ?? outer;
|
||||||
}, [location.search]);
|
}, [location.search]);
|
||||||
|
|
||||||
const [override, setOverride] = useState<"dark" | "light" | undefined>(undefined);
|
const [override, setOverride] = useState<ThemeType | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onMessage(event: MessageEvent) {
|
function onMessage(event: MessageEvent) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { resolveThemeFromQuery } from "@/core/auth/external-login";
|
import { resolveThemeFromQuery } from "@/core/auth/external-login";
|
||||||
|
import type { ThemeType } from "@/contexts/ThemeContext";
|
||||||
|
|
||||||
const DEFAULT_USERNAME = "guest";
|
const DEFAULT_USERNAME = "guest";
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ export function resolveEmbedChatParams(search: string): EmbedChatParams {
|
|||||||
return { sessionId, message, username, theme };
|
return { sessionId, message, username, theme };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveEmbedTheme(themeParam: string): "dark" | "light" {
|
export function resolveEmbedTheme(themeParam: string): ThemeType {
|
||||||
return resolveThemeFromQuery(themeParam || null);
|
return resolveThemeFromQuery(themeParam || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +51,10 @@ export function resolveEmbedTheme(themeParam: string): "dark" | "light" {
|
|||||||
* 只有明确传 light/红白 才走浅色,其余(dark / dark-blue / 深蓝 / 空)一律深蓝。
|
* 只有明确传 light/红白 才走浅色,其余(dark / dark-blue / 深蓝 / 空)一律深蓝。
|
||||||
* 不复用 `resolveEmbedTheme`(它只认严格的 "dark-blue",传 "dark" 会被判成浅色)。
|
* 不复用 `resolveEmbedTheme`(它只认严格的 "dark-blue",传 "dark" 会被判成浅色)。
|
||||||
*/
|
*/
|
||||||
export function resolveEmbedRoundtableTheme(raw: string): "dark" | "light" {
|
export function resolveEmbedRoundtableTheme(raw: string): ThemeType {
|
||||||
const v = raw.trim().toLowerCase();
|
const v = raw.trim().toLowerCase();
|
||||||
if (v === "light" || v === "red" || v === "红白") return "light";
|
if (v === "light" || v === "red" || v === "红白") return "light";
|
||||||
|
if (v === "blue" || v === "lightblue" || v === "light-blue" || v === "浅蓝") return "lightblue";
|
||||||
|
if (v === "green" || v === "lightgreen" || v === "light-green" || v === "绿色") return "lightgreen";
|
||||||
return "dark";
|
return "dark";
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ function toRagSource(
|
|||||||
* A single-RAG stream is a concatenation of source envelopes, for example:
|
* A single-RAG stream is a concatenation of source envelopes, for example:
|
||||||
* `{docs:["..."], recUuid:"...", m_title:"...", m_content:"..."}`.
|
* `{docs:["..."], recUuid:"...", m_title:"...", m_content:"..."}`.
|
||||||
* The root-level metadata is the source; `docs` merely marks the envelope and
|
* The root-level metadata is the source; `docs` merely marks the envelope and
|
||||||
* supplies a fallback body. This mirrors GeneralQA's per-chunk processing.
|
* supplies a fallback body. This mirrors GeneralQA's per-chunk processing.
|
||||||
*/
|
*/
|
||||||
function normalizeSources(chunk: KnowledgeBaseChunk): RagSource[] {
|
function normalizeSources(chunk: KnowledgeBaseChunk): RagSource[] {
|
||||||
const hasRootMetadata = [
|
const hasRootMetadata = [
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { AgentGallery } from "@/components/workspace/agents/agent-gallery";
|
import { AgentGallery } from "@/components/workspace/agents/agent-gallery";
|
||||||
|
import {
|
||||||
export default function AgentsPage() {
|
WorkspaceBody,
|
||||||
return <AgentGallery />;
|
WorkspaceContainer,
|
||||||
}
|
} from "@/components/workspace/workspace-container";
|
||||||
|
|
||||||
|
export default function AgentsPage() {
|
||||||
|
return (
|
||||||
|
<WorkspaceContainer className="bg-white dark:bg-slate-950">
|
||||||
|
<WorkspaceBody>
|
||||||
|
<AgentGallery />
|
||||||
|
</WorkspaceBody>
|
||||||
|
</WorkspaceContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,7 +18,6 @@ import { ImportConversationsDialog } from "@/components/workspace/thread-share-d
|
|||||||
import {
|
import {
|
||||||
CompactShortcutButtons,
|
CompactShortcutButtons,
|
||||||
LayoutModeSwitcher,
|
LayoutModeSwitcher,
|
||||||
ThemeSwitcher,
|
|
||||||
} from "@/components/workspace/appearance-controls";
|
} from "@/components/workspace/appearance-controls";
|
||||||
import {
|
import {
|
||||||
WorkspaceBody,
|
WorkspaceBody,
|
||||||
@@ -185,7 +184,6 @@ export default function ChatsPage({
|
|||||||
compactChrome ? (
|
compactChrome ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<CompactShortcutButtons tone="content" />
|
<CompactShortcutButtons tone="content" />
|
||||||
<ThemeSwitcher tone="content" />
|
|
||||||
<LayoutModeSwitcher tone="content" />
|
<LayoutModeSwitcher tone="content" />
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function LightAppIframeView() {
|
|||||||
|
|
||||||
// 由 useTheme 的 theme 直接换算主题色并显式传入,切主题时 src 立即重建、
|
// 由 useTheme 的 theme 直接换算主题色并显式传入,切主题时 src 立即重建、
|
||||||
// 把新主题色传给被嵌套应用(不依赖 <html> 的 dark class,避免时序读旧值)。
|
// 把新主题色传给被嵌套应用(不依赖 <html> 的 dark class,避免时序读旧值)。
|
||||||
// 深色=dark-blue,浅蓝=blue,其余(红白)=red。
|
// 深色=dark-blue,绿色=green,浅蓝=blue,其余(红白)=red。
|
||||||
const themeColor: AppThemeColor =
|
const themeColor: AppThemeColor =
|
||||||
theme === "dark"
|
theme === "dark"
|
||||||
? "dark-blue"
|
? "dark-blue"
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 我方主题色键。当前启用 red(浅色) / dark-blue(深色);blue 为后续将新增的主题。
|
* 我方主题色键:red(红白)、green(WeKnora 绿色)、blue(浅蓝)、dark-blue(深蓝)。
|
||||||
* 主题参数的映射以这些键为左侧 key(如 `dark-blue=darkBlue`)。
|
* 主题参数的映射以这些键为左侧 key(如 `dark-blue=darkBlue`)。
|
||||||
*/
|
*/
|
||||||
export type AppThemeColor = "red" | "green" | "blue" | "iceblue" | "lightyellow" | "dark-blue";
|
export type AppThemeColor = "red" | "green" | "blue" | "iceblue" | "lightyellow" | "dark-blue";
|
||||||
@@ -50,6 +50,7 @@ export type AppThemeColor = "red" | "green" | "blue" | "iceblue" | "lightyellow"
|
|||||||
export const APP_THEME_COLORS: { key: AppThemeColor; label: string; dot: string }[] = [
|
export const APP_THEME_COLORS: { key: AppThemeColor; label: string; dot: string }[] = [
|
||||||
{ key: "green", label: "绿色", dot: "#07C05F" },
|
{ key: "green", label: "绿色", dot: "#07C05F" },
|
||||||
{ key: "red", label: "红色", dot: "#F5222D" },
|
{ key: "red", label: "红色", dot: "#F5222D" },
|
||||||
|
{ key: "green", label: "绿色", dot: "#07C05F" },
|
||||||
{ key: "blue", label: "蓝色", dot: "#1677FF" },
|
{ key: "blue", label: "蓝色", dot: "#1677FF" },
|
||||||
{ key: "iceblue", label: "冰川蓝", dot: "#9BCEF1" },
|
{ key: "iceblue", label: "冰川蓝", dot: "#9BCEF1" },
|
||||||
{ key: "lightyellow", label: "浅黄色", dot: "#F3FBD9" },
|
{ key: "lightyellow", label: "浅黄色", dot: "#F3FBD9" },
|
||||||
@@ -62,7 +63,7 @@ export const APP_THEME_COLORS: { key: AppThemeColor; label: string; dot: string
|
|||||||
*/
|
*/
|
||||||
const THEME_KEY_ALIASES: Record<AppThemeColor, string[]> = {
|
const THEME_KEY_ALIASES: Record<AppThemeColor, string[]> = {
|
||||||
red: ["red", "light"],
|
red: ["red", "light"],
|
||||||
green: ["green", "lightgreen"],
|
green: ["green", "lightgreen", "light-green"],
|
||||||
blue: ["blue"],
|
blue: ["blue"],
|
||||||
iceblue: ["iceblue", "ice-blue"],
|
iceblue: ["iceblue", "ice-blue"],
|
||||||
lightyellow: ["lightyellow", "light-yellow", "yellow"],
|
lightyellow: ["lightyellow", "light-yellow", "yellow"],
|
||||||
@@ -71,7 +72,7 @@ const THEME_KEY_ALIASES: Record<AppThemeColor, string[]> = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取我方当前主题色,以 ThemeContext / next-themes 写在 <html> 上的 class 为准
|
* 读取我方当前主题色,以 ThemeContext / next-themes 写在 <html> 上的 class 为准
|
||||||
* (含嵌入场景的强制主题)。深色=dark-blue,浅蓝=blue,其余(浅色/红白)=red。
|
* (含嵌入场景的强制主题)。深色=dark-blue,绿色=green,浅蓝=blue,其余(浅色/红白)=red。
|
||||||
*/
|
*/
|
||||||
export function getCurrentTheme(): AppThemeColor {
|
export function getCurrentTheme(): AppThemeColor {
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
|
|||||||