Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
f0bf29ba08
21 files changed
+478
-133
No files matched your search
@@ -0,0 +1,235 @@
|
||||
# 深蓝色 UI 规范
|
||||
|
||||
---
|
||||
|
||||
## 1. 色彩
|
||||
|
||||
### 1.1 标准色
|
||||
|
||||
色彩体系由四个色系构成,每个色系分主色与辅色两组,每组按明度从浅到深编号(01 最浅,数字越大越深)。
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.1 色系总览
|
||||
|
||||
| 色系 | 用途定位 | 代码前缀 |
|
||||
|------|----------|----------|
|
||||
| 渐红系 | 警示、错误、强调操作 | ZS-001 |
|
||||
| 渐蓝系 | 信息、链接、次级操作 | ZS-002 |
|
||||
| 渐青系 | 成功、在线状态、数据高亮 | ZS-003 |
|
||||
| 深蓝系 | 页面背景、容器、导航底色 | ZS-004 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.2 渐红系(ZS-001)
|
||||
|
||||
用于错误提示、危险操作、重要告警。
|
||||
|
||||
| 编号 | 色值 | 说明 |
|
||||
|------|------|------|
|
||||
| ZS-001-01 | `#FFF1F0` | 极浅红,错误背景底色 |
|
||||
| ZS-001-02 | `#FFCCC7` | 浅红,输入框错误边框 |
|
||||
| ZS-001-03 | `#FFA39E` | — |
|
||||
| ZS-001-04 | `#FF7875` | — |
|
||||
| ZS-001-05 | `#FF4D4F` | 标准红,Tag / Badge |
|
||||
| ZS-001-06 | `#F5222D` | 主色红,按钮 / 图标 |
|
||||
| ZS-001-07 | `#CF1322` | 深红,Hover 状态 |
|
||||
| ZS-001-08 | `#A8071A` | 深红,Active / Press |
|
||||
| ZS-001-09 | `#820014` | 极深红 |
|
||||
| ZS-001-10 | `#5C0011` | 最深红 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.3 渐蓝系(ZS-002)
|
||||
|
||||
用于信息提示、链接、次级按钮。
|
||||
|
||||
| 编号 | 色值 | 说明 |
|
||||
|------|------|------|
|
||||
| ZS-002-01 | `#E6F4FF` | 极浅蓝,信息背景底色 |
|
||||
| ZS-002-02 | `#BAE0FF` | 浅蓝 |
|
||||
| ZS-002-03 | `#91CAFF` | — |
|
||||
| ZS-002-04 | `#69B1FF` | — |
|
||||
| ZS-002-05 | `#4096FF` | 标准蓝,链接 / 信息 Tag |
|
||||
| ZS-002-06 | `#1677FF` | 主色蓝,主操作按钮 |
|
||||
| ZS-002-07 | `#0958D9` | 深蓝,Hover |
|
||||
| ZS-002-08 | `#003EB3` | 深蓝,Active |
|
||||
| ZS-002-09 | `#002C8C` | 极深蓝 |
|
||||
| ZS-002-10 | `#001D6C` | 最深蓝 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.4 渐青系(ZS-003)
|
||||
|
||||
用于成功状态、在线标识、数据图表高亮。
|
||||
|
||||
| 编号 | 色值 | 说明 |
|
||||
|------|------|------|
|
||||
| ZS-003-01 | `#E6FFFB` | 极浅青,成功背景 |
|
||||
| ZS-003-02 | `#B5F5EC` | 浅青 |
|
||||
| ZS-003-03 | `#87E8DE` | — |
|
||||
| ZS-003-04 | `#5CDBD3` | — |
|
||||
| ZS-003-05 | `#36CFC9` | 标准青,成功 Tag |
|
||||
| ZS-003-06 | `#13C2C2` | 主色青,在线点 / 图标 |
|
||||
| ZS-003-07 | `#08979C` | 深青,Hover |
|
||||
| ZS-003-08 | `#006D75` | 深青,Active |
|
||||
| ZS-003-09 | `#00474F` | 极深青 |
|
||||
| ZS-003-10 | `#002329` | 最深青 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.5 深蓝系(ZS-004)
|
||||
|
||||
用于页面背景、导航栏、侧边栏、卡片容器。这是深蓝主题的核心色系。
|
||||
|
||||
| 编号 | 色值 | 说明 |
|
||||
|------|------|------|
|
||||
| ZS-004-01 | `#E8EDF5` | 极浅,分割线 / 禁用底色 |
|
||||
| ZS-004-02 | `#C5CFE0` | 浅,边框 |
|
||||
| ZS-004-03 | `#9BADC8` | — |
|
||||
| ZS-004-04 | `#6E88B0` | — |
|
||||
| ZS-004-05 | `#4A6491` | 中深蓝,次级文字 |
|
||||
| ZS-004-06 | `#2D4473` | 深蓝,卡片容器背景 |
|
||||
| ZS-004-07 | `#1A2F5A` | 深蓝,侧边栏背景 |
|
||||
| ZS-004-08 | `#122347` | 深蓝,导航栏背景 |
|
||||
| ZS-004-09 | `#0C1836` | 极深蓝,页面背景 |
|
||||
| ZS-004-10 | `#060E24` | 最深,全局底色 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.6 功能色(主色 / 字体色 / 辅色 / 背景色)
|
||||
|
||||
从标准色系中提取,用于具体 UI 角色指定。
|
||||
|
||||
| 角色 | 色值 | 来源 |
|
||||
|------|------|------|
|
||||
| 主色(Primary) | `#1677FF` | ZS-002-06 |
|
||||
| 主色 Hover | `#0958D9` | ZS-002-07 |
|
||||
| 主色 Active | `#003EB3` | ZS-002-08 |
|
||||
| 正常字体色 | `#C5CFE0` | ZS-004-02 |
|
||||
| 次级字体色 | `#6E88B0` | ZS-004-04 |
|
||||
| 禁用字体色 | `#4A6491` | ZS-004-05 |
|
||||
| 辅色(Accent) | `#13C2C2` | ZS-003-06 |
|
||||
| 页面背景色 | `#0C1836` | ZS-004-09 |
|
||||
| 容器背景色 | `#1A2F5A` | ZS-004-07 |
|
||||
| 白色 | `#FFFFFF` | — |
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.7 小面积装饰色
|
||||
|
||||
用于图标点缀、徽标、少量强调,不用于大面积背景。
|
||||
|
||||
| 色值 | 说明 |
|
||||
|------|------|
|
||||
| `#6388FF` | 紫蓝,图标装饰 |
|
||||
| `#F5222D` | 红色,危险徽标 |
|
||||
| `#12306C` | 深蓝,品牌点缀 |
|
||||
| `#478880` | 青绿,辅助点缀 |
|
||||
| `#950404` | 深红,重要告警点 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.1.8 图表配色
|
||||
|
||||
**通用图表色序**(面积较大,如柱状图、折线图、饼图):
|
||||
|
||||
| 序号 | 色值 |
|
||||
|------|------|
|
||||
| 1 | `#1677FF` |
|
||||
| 2 | `#13C2C2` |
|
||||
| 3 | `#4096FF` |
|
||||
| 4 | `#36CFC9` |
|
||||
| 5 | `#69B1FF` |
|
||||
| 6 | `#5CDBD3` |
|
||||
| 7 | `#91CAFF` |
|
||||
| 8 | `#87E8DE` |
|
||||
|
||||
**小面积图表色序**(面积较小,如散点、小徽章、迷你图):
|
||||
|
||||
| 序号 | 色值 |
|
||||
|------|------|
|
||||
| 1 | `#4096FF` |
|
||||
| 2 | `#36CFC9` |
|
||||
| 3 | `#FF4D4F` |
|
||||
| 4 | `#FFA39E` |
|
||||
| 5 | `#6388FF` |
|
||||
| 6 | `#478880` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 字体
|
||||
|
||||
### 2.1 全局样式 - 字体
|
||||
|
||||
---
|
||||
|
||||
#### 2.1.1 大屏页面字体规范
|
||||
|
||||
##### 中文字体:Noto Sans S Chinese
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 24px Bold | 一级标题 |
|
||||
| 24px Medium | 次要一级标题 |
|
||||
| 22px Medium | 二级标题 |
|
||||
| 22px Regular | 正文 |
|
||||
| 20px Regular | 排版空间有限的正文 |
|
||||
|
||||
##### 数字字体:D-DIN
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 32px Bold | 重要数据展示 |
|
||||
| 32px Regular | 普通数据展示 |
|
||||
| 26px Bold | 常规数据展示 |
|
||||
| 26px Regular | 常规数据展示 |
|
||||
|
||||
##### 特殊数字:DS-Digital
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 50px Bold | 大屏主数据展示 |
|
||||
|
||||
##### 表格字体:Arial
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 20px Regular | 表格横纵轴数字 |
|
||||
|
||||
---
|
||||
|
||||
#### 2.1.2 后台系统字体规范
|
||||
|
||||
##### 中文字体:Noto Sans S Chinese
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 18px Bold | 导航、一级标题 |
|
||||
| 18px Medium | 次要一级标题 |
|
||||
| 16px Bold | 二级标题 |
|
||||
| 16px Medium | 次要二级标题 |
|
||||
| 16px Regular | 正文、按钮内部文字等 |
|
||||
| 16px Light | 次要正文 |
|
||||
|
||||
##### 数字字体:D-DIN
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 26px Bold | 重要数据展示 |
|
||||
| 26px Regular | 普通数据展示 |
|
||||
| 24px Bold | 常规数据展示(小) |
|
||||
| 24px Regular | 常规数据展示(小) |
|
||||
|
||||
##### LOGO 字体:DOUYUFont
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 20px 文字样式 | 页面顶部系统名称 |
|
||||
|
||||
##### 表格数字:Arial
|
||||
|
||||
| 样式 | 使用场景 |
|
||||
|------|----------|
|
||||
| 16px Regular | 表格横纵轴数字 |
|
||||
| 14px Regular | 表格横轴数字(空间有限) |
|
||||
@@ -400,63 +400,26 @@ export function PageSidebar({
|
||||
onOpenChange={setNotificationOpen}
|
||||
/>
|
||||
<aside
|
||||
className={`fixed left-0 top-0 z-40 flex h-screen flex-col overflow-hidden shadow-sm theme-transition transition-all duration-300 ${
|
||||
className={`fixed left-0 top-16 z-40 flex h-[calc(100vh-4rem)] flex-col overflow-hidden shadow-sm theme-transition transition-all duration-300 ${
|
||||
isDark
|
||||
? "border-r border-blue-800/30 bg-slate-900"
|
||||
: "border-r border-red-100 bg-white"
|
||||
} ${collapsed ? "w-16" : "w-64"}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-16 items-center theme-transition ${
|
||||
isDark ? "border-b border-blue-800/30" : "border-b border-red-100"
|
||||
} ${collapsed ? "justify-center px-2" : "justify-between px-6"}`}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleCollapsed}
|
||||
title={collapsed ? "展开侧边栏" : "收起侧边栏"}
|
||||
className={`absolute -right-3 top-6 z-50 flex h-6 w-6 items-center justify-center rounded-full shadow-md theme-transition ${
|
||||
isDark
|
||||
? "bg-slate-700 text-slate-300 hover:bg-slate-600"
|
||||
: "bg-white text-gray-500 hover:bg-red-50 hover:text-red-600 border border-red-100"
|
||||
}`}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
className={`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg transition-transform duration-300 ease-out ${
|
||||
isDark
|
||||
? "bg-gradient-to-br from-blue-500 to-cyan-400"
|
||||
: "bg-gradient-to-br from-red-500 to-red-400"
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="overflow-hidden whitespace-nowrap animate-in fade-in-0 slide-in-from-left-2 duration-300">
|
||||
<h1
|
||||
className={`text-sm font-semibold theme-transition ${
|
||||
isDark ? "text-white" : "text-gray-900"
|
||||
}`}
|
||||
>
|
||||
CM助手
|
||||
</h1>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleCollapsed}
|
||||
className={iconButtonClass}
|
||||
title="Collapse sidebar"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{collapsed ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
|
||||
</button>
|
||||
|
||||
<nav className={`flex-1 overflow-y-auto ${collapsed ? "p-2" : "p-4"} space-y-2 transition-all duration-300 ease-out`}>
|
||||
{collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleCollapsed}
|
||||
className={`flex w-full items-center justify-center ${iconButtonClass}`}
|
||||
title="Expand sidebar"
|
||||
>
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{visibleMenuItems.map((item) =>
|
||||
item.children ? renderBranch(item) : renderLeaf(item),
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { BotIcon } from "lucide-react";
|
||||
|
||||
import { type Agent } from "@/core/agents";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function AgentWelcome({
|
||||
@@ -14,8 +15,10 @@ export function AgentWelcome({
|
||||
agent: Agent | null | undefined;
|
||||
agentName: string;
|
||||
}) {
|
||||
const displayName = agent?.name ?? agentName;
|
||||
const description = agent?.description;
|
||||
const displayName = desensitize(agent?.name ?? agentName);
|
||||
const description = agent?.description
|
||||
? desensitize(agent.description)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -48,6 +48,7 @@ import type { Agent } from "@/core/agents";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AgentCardProps {
|
||||
@@ -195,12 +196,12 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
<BotIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<CardTitle className="truncate text-base" title={agent.name}>
|
||||
{agent.name}
|
||||
<CardTitle className="truncate text-base" title={desensitize(agent.name)}>
|
||||
{desensitize(agent.name)}
|
||||
</CardTitle>
|
||||
{agent.model && (
|
||||
<Badge variant="secondary" className="mt-0.5 text-xs">
|
||||
{agent.model}
|
||||
{desensitize(agent.model)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -252,7 +253,7 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
</div>
|
||||
{agent.description && (
|
||||
<CardDescription className="mt-2 line-clamp-2 text-sm">
|
||||
{agent.description}
|
||||
{desensitize(agent.description)}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
@@ -272,7 +273,7 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
>
|
||||
{group}
|
||||
{desensitize(group)}
|
||||
</Badge>
|
||||
))}
|
||||
{agent.skills?.map((skill) => (
|
||||
@@ -286,7 +287,7 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
setDetailSkillName(skill);
|
||||
}}
|
||||
>
|
||||
{skill}
|
||||
{desensitize(skill)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -377,7 +378,7 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑智能体:{agent.name}</DialogTitle>
|
||||
<DialogTitle>编辑智能体:{desensitize(agent.name)}</DialogTitle>
|
||||
<DialogDescription>
|
||||
可以修改名称、描述、技能白名单、SOUL 内容与发布状态。
|
||||
</DialogDescription>
|
||||
@@ -428,10 +429,10 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
size="sm"
|
||||
variant={checked ? "default" : "outline"}
|
||||
onClick={() => toggleSkill(skill.name)}
|
||||
title={skill.description}
|
||||
title={desensitize(skill.description ?? "")}
|
||||
className="h-8 max-w-[200px] rounded-r-none"
|
||||
>
|
||||
<span className="truncate">{skill.name}</span>
|
||||
<span className="truncate">{desensitize(skill.name)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -50,6 +50,7 @@ import { installSkill } from "@/core/skills/api";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||
import { env } from "@/env";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { ArtifactLink } from "../citations/artifact-link";
|
||||
@@ -608,7 +609,7 @@ export function ArtifactFileDetail({
|
||||
viewMode === "preview" &&
|
||||
(language === "markdown" || language === "html") && (
|
||||
<ArtifactFilePreview
|
||||
content={displayContent}
|
||||
content={desensitize(displayContent ?? "")}
|
||||
language={language ?? "text"}
|
||||
animated={showPreviewSwitchPulse}
|
||||
activeCursor={isApplyingEdit}
|
||||
@@ -618,7 +619,11 @@ export function ArtifactFileDetail({
|
||||
<div className="size-full">
|
||||
<CodeEditor
|
||||
className="size-full resize-none rounded-none border-none bg-transparent"
|
||||
value={displayContent ?? ""}
|
||||
value={
|
||||
canEditArtifact
|
||||
? (displayContent ?? "")
|
||||
: desensitize(displayContent ?? "")
|
||||
}
|
||||
readonly={!canEditArtifact}
|
||||
onChange={
|
||||
canEditArtifact
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type MessageResponseProps,
|
||||
} from "@/components/ai-elements/message";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CitationLink } from "../citations/citation-link";
|
||||
@@ -63,6 +64,8 @@ export function MarkdownContent({
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
const displayContent = desensitize(content);
|
||||
|
||||
return (
|
||||
<MessageResponse
|
||||
className={className}
|
||||
@@ -70,7 +73,7 @@ export function MarkdownContent({
|
||||
rehypePlugins={rehypePlugins}
|
||||
components={components}
|
||||
>
|
||||
{content}
|
||||
{displayContent}
|
||||
</MessageResponse>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
} from "@/core/messages/utils";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { humanMessagePlugins } from "@/core/streamdown";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { CopyButton } from "../copy-button";
|
||||
@@ -226,7 +227,7 @@ function MessageContent_({
|
||||
<TaskTrigger title="">
|
||||
<div className="text-muted-foreground flex w-full cursor-default items-center gap-2 text-sm select-none">
|
||||
<Loader className="size-4" />
|
||||
<span>{contentToDisplay}</span>
|
||||
<span>{desensitize(contentToDisplay)}</span>
|
||||
</div>
|
||||
</TaskTrigger>
|
||||
</Task>
|
||||
@@ -240,7 +241,7 @@ function MessageContent_({
|
||||
<AIElementMessageContent className={className}>
|
||||
<Reasoning isStreaming={isLoading}>
|
||||
<ReasoningTrigger />
|
||||
<ReasoningContent>{reasoningContent}</ReasoningContent>
|
||||
<ReasoningContent>{desensitize(reasoningContent)}</ReasoningContent>
|
||||
</Reasoning>
|
||||
<MessageTokenUsage
|
||||
enabled={tokenUsageEnabled}
|
||||
@@ -259,7 +260,7 @@ function MessageContent_({
|
||||
components={components}
|
||||
parseIncompleteMarkdown={false}
|
||||
>
|
||||
{contentToDisplay}
|
||||
{desensitize(contentToDisplay)}
|
||||
</AIElementMessageResponse>
|
||||
) : null;
|
||||
return (
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
|
||||
import { pathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||
import { env } from "@/env";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { isIMEComposing } from "@/lib/ime";
|
||||
|
||||
export function RecentChatList() {
|
||||
@@ -211,7 +212,7 @@ export function RecentChatList() {
|
||||
className="block w-full whitespace-nowrap group-hover/side-menu-item:overflow-hidden"
|
||||
href={pathOfThread(thread)}
|
||||
>
|
||||
{titleOfThread(thread)}
|
||||
{desensitize(titleOfThread(thread))}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY !== "true" && (
|
||||
|
||||
@@ -42,6 +42,7 @@ import type {
|
||||
MemoryTarget,
|
||||
MemoryV2Config,
|
||||
} from "@/core/memory/types";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
import { SettingsSection } from "./settings-section";
|
||||
|
||||
@@ -173,7 +174,7 @@ export function MemorySettingsPage() {
|
||||
className="flex items-start justify-between gap-3 rounded-md border p-3"
|
||||
>
|
||||
<p className="min-w-0 flex-1 whitespace-pre-wrap text-sm [overflow-wrap:anywhere]">
|
||||
{entry}
|
||||
{desensitize(entry)}
|
||||
</p>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<Button
|
||||
@@ -234,7 +235,7 @@ export function MemorySettingsPage() {
|
||||
</SelectItem>
|
||||
{experts.map((a) => (
|
||||
<SelectItem key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
{desensitize(a.name)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -272,7 +273,7 @@ export function MemorySettingsPage() {
|
||||
</SelectItem>
|
||||
{experts.map((a) => (
|
||||
<SelectItem key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
{desensitize(a.name)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -356,7 +357,7 @@ export function MemorySettingsPage() {
|
||||
<div className="text-muted-foreground mb-1 font-medium">
|
||||
{m.entryToDeleteLabel}
|
||||
</div>
|
||||
<p className="break-words">{entryToDelete.content}</p>
|
||||
<p className="break-words">{desensitize(entryToDelete.content)}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<DialogFooter>
|
||||
@@ -539,10 +540,10 @@ function MemoryV1View({
|
||||
{Math.round(fact.confidence * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{fact.content}</p>
|
||||
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(fact.content)}</p>
|
||||
{fact.sourceError && (
|
||||
<p className="text-xs text-red-600 bg-red-50 rounded px-2 py-1">
|
||||
避免:{fact.sourceError}
|
||||
避免:{desensitize(fact.sourceError)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -566,7 +567,7 @@ function MemoryV1View({
|
||||
<dt className="font-medium text-muted-foreground mb-0.5">
|
||||
{CTX_LABELS[key] ?? key}
|
||||
</dt>
|
||||
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{value}</dd>
|
||||
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
@@ -585,7 +586,7 @@ function MemoryV1View({
|
||||
<dt className="font-medium text-muted-foreground mb-0.5">
|
||||
{HISTORY_LABELS[key] ?? key}
|
||||
</dt>
|
||||
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{value}</dd>
|
||||
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(value)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
@@ -686,7 +687,7 @@ function MemorySearchPanel() {
|
||||
</SelectItem>
|
||||
{experts.map((a) => (
|
||||
<SelectItem key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
{desensitize(a.name)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -719,7 +720,7 @@ function MemorySearchPanel() {
|
||||
) : results !== null ? (
|
||||
mode === "reflect" ? (
|
||||
<div className="rounded-md border p-3 text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">
|
||||
{reflectText.trim() || m.searchNoResults}
|
||||
{reflectText.trim() ? desensitize(reflectText.trim()) : m.searchNoResults}
|
||||
</div>
|
||||
) : results.length === 0 ? (
|
||||
<div className="text-muted-foreground rounded-md border border-dashed p-3 text-sm">
|
||||
@@ -735,7 +736,7 @@ function MemorySearchPanel() {
|
||||
key={`${index}-${item.slice(0, 16)}`}
|
||||
className="rounded-md border p-3 text-sm whitespace-pre-wrap [overflow-wrap:anywhere]"
|
||||
>
|
||||
{item}
|
||||
{desensitize(item)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
} from "@/core/skills/hooks";
|
||||
import type { Skill } from "@/core/skills/type";
|
||||
import { env } from "@/env";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { SettingsSection } from "./settings-section";
|
||||
@@ -404,7 +405,7 @@ function SkillRow({
|
||||
>
|
||||
<ItemTitle>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span>{skill.name}</span>
|
||||
<span>{desensitize(skill.name)}</span>
|
||||
{isBuiltin && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{t.settings.skills.badgeBuiltin}
|
||||
@@ -474,7 +475,7 @@ function SkillRow({
|
||||
</div>
|
||||
</ItemTitle>
|
||||
<ItemDescription className="line-clamp-4">
|
||||
{skill.description}
|
||||
{desensitize(skill.description ?? "")}
|
||||
</ItemDescription>
|
||||
{skill.use_count !== undefined && (
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
@@ -560,7 +561,9 @@ function SkillRow({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t.settings.skills.deleteAction}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t.settings.skills.deleteConfirm.replace("{name}", skill.name)}
|
||||
{desensitize(
|
||||
t.settings.skills.deleteConfirm.replace("{name}", skill.name),
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -809,22 +812,22 @@ function InstallSkillDialog({
|
||||
<div className="flex flex-col gap-2 rounded-md border p-3 text-sm">
|
||||
<div className="flex items-center gap-2 text-emerald-600">
|
||||
<CheckCircle2 className="size-4" />
|
||||
<span className="font-medium">{validation.skill_name}</span>
|
||||
<span className="font-medium">{desensitize(validation.skill_name)}</span>
|
||||
</div>
|
||||
<Field label={dialog.descriptionLabel}>
|
||||
<span className="line-clamp-3 whitespace-pre-wrap">
|
||||
{validation.description || "—"}
|
||||
{validation.description ? desensitize(validation.description) : "—"}
|
||||
</span>
|
||||
</Field>
|
||||
{validation.license && (
|
||||
<Field label={dialog.licenseLabel}>{validation.license}</Field>
|
||||
<Field label={dialog.licenseLabel}>{desensitize(validation.license)}</Field>
|
||||
)}
|
||||
{validation.allowed_tools.length > 0 && (
|
||||
<Field label={dialog.toolsLabel}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{validation.allowed_tools.map((tool) => (
|
||||
<Badge key={tool} variant="secondary" className="text-xs">
|
||||
{tool}
|
||||
{desensitize(tool)}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
@@ -834,7 +837,7 @@ function InstallSkillDialog({
|
||||
<Field label={dialog.warningsLabel}>
|
||||
<ul className="list-disc pl-4 text-xs">
|
||||
{validation.warnings.map((w, idx) => (
|
||||
<li key={idx}>{w}</li>
|
||||
<li key={idx}>{desensitize(w)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Field>
|
||||
@@ -843,7 +846,9 @@ function InstallSkillDialog({
|
||||
<div className="text-amber-600 mt-1 flex items-start gap-2 text-xs">
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>
|
||||
{dialog.conflictCustom.replace("{name}", validation.skill_name)}
|
||||
{desensitize(
|
||||
dialog.conflictCustom.replace("{name}", validation.skill_name),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -851,7 +856,9 @@ function InstallSkillDialog({
|
||||
<div className={cn("mt-1 flex items-start gap-2 text-xs", adminCanOverwritePublic ? "text-amber-600" : "text-destructive")}>
|
||||
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
|
||||
<span>
|
||||
{(adminCanOverwritePublic ? dialog.conflictPublicAdminHint : dialog.conflictPublic).replace("{name}", validation.skill_name)}
|
||||
{desensitize(
|
||||
(adminCanOverwritePublic ? dialog.conflictPublicAdminHint : dialog.conflictPublic).replace("{name}", validation.skill_name),
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useDeleteCustomSkill, useSetSkillState } from "@/core/skills/hooks";
|
||||
import type { Skill } from "@/core/skills/type";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type Kind = "stale" | "archived";
|
||||
@@ -146,7 +147,7 @@ function SkillStateRow({ skill, kind }: { skill: Skill; kind: Kind }) {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">{skill.name}</span>
|
||||
<span className="font-medium">{desensitize(skill.name)}</span>
|
||||
{isArchived ? (
|
||||
<Badge variant="secondary" className="text-xs" title={tt.badgeArchivedTitle}>
|
||||
{tt.badgeArchived}
|
||||
@@ -180,7 +181,7 @@ function SkillStateRow({ skill, kind }: { skill: Skill; kind: Kind }) {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-1 line-clamp-2 text-sm">
|
||||
{skill.description}
|
||||
{desensitize(skill.description ?? "")}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1 flex flex-wrap gap-x-3 text-xs">
|
||||
<span>{timeLabel}</span>
|
||||
@@ -254,7 +255,7 @@ function SkillStateRow({ skill, kind }: { skill: Skill; kind: Kind }) {
|
||||
<DialogHeader>
|
||||
<DialogTitle>{tt.deleteAction}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{tt.deleteConfirm.replace("{name}", skill.name)}
|
||||
{desensitize(tt.deleteConfirm.replace("{name}", skill.name))}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
@@ -295,12 +296,12 @@ function SkillViewDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{skill.name}</DialogTitle>
|
||||
<DialogTitle>{desensitize(skill.name)}</DialogTitle>
|
||||
<DialogDescription>{tv.title}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-3 text-sm">
|
||||
<Row label={tv.descriptionLabel}>
|
||||
<span className="whitespace-pre-wrap">{skill.description}</span>
|
||||
<span className="whitespace-pre-wrap">{desensitize(skill.description ?? "")}</span>
|
||||
</Row>
|
||||
<Row label={tv.stateLabel}>
|
||||
<span className={cn("font-medium", stateColor(skill.state))}>
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from "@/core/skills/hooks";
|
||||
import type { Skill } from "@/core/skills/type";
|
||||
import { reasoningPlugins } from "@/core/streamdown";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
const markdownClassName =
|
||||
"!h-auto !w-full max-w-none min-w-0 text-sm leading-7 " +
|
||||
@@ -147,7 +148,7 @@ export function SkillDetailDialog({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DialogTitle className="truncate text-base">
|
||||
{skill?.name ?? skillName ?? ""}
|
||||
{desensitize(skill?.name ?? skillName ?? "")}
|
||||
</DialogTitle>
|
||||
{skill?.builtin && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
@@ -165,7 +166,7 @@ export function SkillDetailDialog({
|
||||
)}
|
||||
</div>
|
||||
<DialogDescription className="mt-1 line-clamp-2 text-sm leading-relaxed">
|
||||
{skill?.description || "-"}
|
||||
{skill?.description ? desensitize(skill.description) : "-"}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
@@ -328,7 +329,7 @@ export function SkillDetailDialog({
|
||||
</div>
|
||||
) : showRawSource ? (
|
||||
<pre className="text-foreground/80 max-w-full overflow-x-auto rounded-lg border bg-muted p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap">
|
||||
{contentData?.content}
|
||||
{desensitize(contentData?.content ?? "")}
|
||||
</pre>
|
||||
) : (
|
||||
<MarkdownContent
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Tag } from "@/core/tags";
|
||||
|
||||
@@ -24,7 +25,7 @@ export function TagChip({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{tag.name}
|
||||
{desensitize(tag.name)}
|
||||
{onRemove && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect } from "react";
|
||||
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import type { AgentThreadState } from "@/core/threads";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
import { useThreadChat } from "./chats";
|
||||
import { FlipDisplay } from "./flip-display";
|
||||
@@ -44,7 +45,7 @@ export function ThreadTitle({
|
||||
}
|
||||
return (
|
||||
<FlipDisplay uniqueKey={threadId}>
|
||||
{thread.values.title ?? "Untitled"}
|
||||
{desensitize(thread.values.title ?? "Untitled")}
|
||||
</FlipDisplay>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
import { useI18nContext } from "./context";
|
||||
import { getLocaleFromCookie, setLocaleInCookie } from "./cookies";
|
||||
@@ -13,10 +15,34 @@ import {
|
||||
type Locale,
|
||||
} from "./index";
|
||||
|
||||
function wrapWithDesensitize<T>(value: T): T {
|
||||
if (typeof value === "string") {
|
||||
return desensitize(value) as T;
|
||||
}
|
||||
if (typeof value === "function") {
|
||||
return ((...args: unknown[]) => {
|
||||
const result = (value as (...a: unknown[]) => unknown)(...args);
|
||||
return wrapWithDesensitize(result);
|
||||
}) as T;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => wrapWithDesensitize(item)) as T;
|
||||
}
|
||||
if (value !== null && typeof value === "object") {
|
||||
return new Proxy(value as object, {
|
||||
get(target, prop, receiver) {
|
||||
return wrapWithDesensitize(Reflect.get(target, prop, receiver));
|
||||
},
|
||||
}) as T;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function useI18n() {
|
||||
const { locale, setLocale } = useI18nContext();
|
||||
|
||||
const t = translations[locale] ?? translations[DEFAULT_LOCALE];
|
||||
const rawT = translations[locale] ?? translations[DEFAULT_LOCALE];
|
||||
const t = useMemo(() => wrapWithDesensitize(rawT), [rawT]);
|
||||
|
||||
const changeLocale = (newLocale: Locale) => {
|
||||
setLocale(newLocale);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 脱敏词映射表
|
||||
* key: 原始词(支持正则特殊字符,会被转义)
|
||||
* value: 脱敏后替换词
|
||||
*/
|
||||
export const DESENSITIZE_MAP: Record<string, string> = {
|
||||
态势: "TS",
|
||||
情报: "QB",
|
||||
指挥: "ZH",
|
||||
信息准备: "XX准备",
|
||||
行为体: "XWT",
|
||||
驱动因素: "QDYS",
|
||||
叙事: "XS",
|
||||
前三定: "QSD",
|
||||
受众目标: "SZMB",
|
||||
作战: "ZZ",
|
||||
目的: "MD",
|
||||
关键传播节点: "GJCBJD",
|
||||
战法: "ZF",
|
||||
行动: "XD",
|
||||
打击: "DJ",
|
||||
目标: "MB",
|
||||
战力: "ZL",
|
||||
筹划: "COH",
|
||||
策划: "CEH",
|
||||
模拟: "MN",
|
||||
推演: "TY",
|
||||
认知: "RZ",
|
||||
触及: "CJ"
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 DESENSITIZE_MAP 编译为单个正则,避免多次遍历字符串。
|
||||
* 仅在模块首次加载时执行一次。
|
||||
*/
|
||||
function buildRegex(map: Record<string, string>): RegExp | null {
|
||||
const keys = Object.keys(map);
|
||||
if (keys.length === 0) return null;
|
||||
const pattern = keys.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
||||
return new RegExp(pattern, "g");
|
||||
}
|
||||
|
||||
let _regex: RegExp | null = buildRegex(DESENSITIZE_MAP);
|
||||
|
||||
/**
|
||||
* 对文本进行脱敏处理,将 DESENSITIZE_MAP 中的词替换为对应脱敏词。
|
||||
*
|
||||
* @param text 原始文本
|
||||
* @returns 脱敏后的文本
|
||||
*
|
||||
* @example
|
||||
* desensitize("当前态势分析") // => "当前TS分析"
|
||||
*/
|
||||
export function desensitize(text: string): string {
|
||||
if (!_regex) return text;
|
||||
// 每次 exec 需要重置 lastIndex,使用 replace 更安全
|
||||
return text.replace(_regex, (matched) => DESENSITIZE_MAP[matched] ?? matched);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时动态追加脱敏词,追加后自动重编译正则。
|
||||
*
|
||||
* @param entries 要追加的词条,格式同 DESENSITIZE_MAP
|
||||
*
|
||||
* @example
|
||||
* addDesensitizeEntries({ 情报: "QB", 指挥: "ZH" });
|
||||
*/
|
||||
export function addDesensitizeEntries(entries: Record<string, string>): void {
|
||||
Object.assign(DESENSITIZE_MAP, entries);
|
||||
_regex = buildRegex(DESENSITIZE_MAP);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { useThreadSettings } from "@/core/settings";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { env } from "@/env";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function AgentChatPage() {
|
||||
@@ -61,8 +62,9 @@ export default function AgentChatPage() {
|
||||
);
|
||||
|
||||
const { agent, isLoading: isAgentLoading } = useAgent(agent_id);
|
||||
const agentDisplayName =
|
||||
agent?.name ?? (isAgentLoading ? t.common.loading : "");
|
||||
const agentDisplayName = desensitize(
|
||||
agent?.name ?? (isAgentLoading ? t.common.loading : ""),
|
||||
);
|
||||
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread } =
|
||||
useThreadChat();
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useThreads } from "@/core/threads/hooks";
|
||||
import { usePathOfThread, titleOfThread } from "@/core/threads/utils";
|
||||
import { formatTimeAgo } from "@/core/utils/datetime";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
|
||||
export default function ChatsPage() {
|
||||
const pathOfThread = usePathOfThread();
|
||||
@@ -69,7 +70,7 @@ export default function ChatsPage() {
|
||||
<Link key={thread.thread_id} href={pathOfThread(thread)}>
|
||||
<div className="flex flex-col gap-2 border-b p-4">
|
||||
<div>
|
||||
<div>{titleOfThread(thread)}</div>
|
||||
<div>{desensitize(titleOfThread(thread))}</div>
|
||||
</div>
|
||||
{thread.updated_at && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -15,20 +15,20 @@ function PageLayout() {
|
||||
const sidebarEnabled = useMemo(() => readSidebarEnabled(), []);
|
||||
|
||||
return (
|
||||
<div className="h-screen flex bg-muted/20">
|
||||
{sidebarEnabled && (
|
||||
<PageSidebar
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={`flex flex-col flex-1 overflow-hidden transition-[padding] duration-300 ${
|
||||
!sidebarEnabled ? "" : collapsed ? "pl-16" : "pl-64"
|
||||
}`}
|
||||
>
|
||||
{sidebarEnabled && <Header />}
|
||||
<main className="flex-1 overflow-hidden">
|
||||
<div className="h-screen flex flex-col bg-muted/20">
|
||||
{sidebarEnabled && <Header collapsed={collapsed} />}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
{sidebarEnabled && (
|
||||
<PageSidebar
|
||||
collapsed={collapsed}
|
||||
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
|
||||
/>
|
||||
)}
|
||||
<main
|
||||
className={`flex-1 overflow-hidden transition-[padding] duration-300 ${
|
||||
!sidebarEnabled ? "" : collapsed ? "pl-16" : "pl-64"
|
||||
}`}
|
||||
>
|
||||
<Routes>
|
||||
<Route index element={<Navigate to="workspace" replace />} />
|
||||
<Route
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
statusLabel,
|
||||
statusVariant,
|
||||
} from "@/components/workspace/scheduled-tasks/schedule-builder";
|
||||
import { desensitize } from "@/lib/desensitize";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function formatDate(value?: string | null) {
|
||||
@@ -334,7 +335,7 @@ export default function ScheduledTasksPage() {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="font-medium leading-snug break-all">
|
||||
{task.name}
|
||||
{desensitize(task.name)}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1">
|
||||
{isRunningNow && (
|
||||
@@ -358,7 +359,7 @@ export default function ScheduledTasksPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-1 truncate text-xs">
|
||||
{task.schedule_text} · {task.timezone}
|
||||
{desensitize(task.schedule_text ?? "")} · {task.timezone}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={task.enabled ? "default" : "secondary"} className="shrink-0">
|
||||
@@ -531,9 +532,9 @@ export default function ScheduledTasksPage() {
|
||||
<div key={item.task_id} className="bg-card rounded-md border p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium">{item.name}</div>
|
||||
<div className="truncate font-medium">{desensitize(item.name)}</div>
|
||||
<div className="text-muted-foreground mt-1 truncate text-xs">
|
||||
{item.schedule_text} · {item.timezone}
|
||||
{desensitize(item.schedule_text ?? "")} · {item.timezone}
|
||||
</div>
|
||||
</div>
|
||||
{subscribed && (
|
||||
@@ -543,7 +544,7 @@ export default function ScheduledTasksPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-3 line-clamp-3 text-xs whitespace-pre-wrap">
|
||||
{item.prompt_summary || "(无摘要)"}
|
||||
{item.prompt_summary ? desensitize(item.prompt_summary) : "(无摘要)"}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<Button
|
||||
@@ -585,7 +586,7 @@ export default function ScheduledTasksPage() {
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{selectedTask ? selectedTask.name : "请选择左侧任务"}
|
||||
{selectedTask ? desensitize(selectedTask.name) : "请选择左侧任务"}
|
||||
</p>
|
||||
{selectedTask && (selectedTask.creator_email || selectedTask.creator_user_id) && (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
@@ -625,7 +626,7 @@ export default function ScheduledTasksPage() {
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium">
|
||||
{run.result?.title || formatDate(run.scheduled_for)}
|
||||
{run.result?.title ? desensitize(run.result.title) : formatDate(run.scheduled_for)}
|
||||
</div>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
计划:{formatDate(run.scheduled_for)} · 开始:
|
||||
@@ -642,7 +643,7 @@ export default function ScheduledTasksPage() {
|
||||
</div>
|
||||
{run.error && (
|
||||
<div className="text-destructive mt-3 line-clamp-2 text-sm">
|
||||
{run.error}
|
||||
{desensitize(run.error)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
@@ -703,13 +704,13 @@ export default function ScheduledTasksPage() {
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<div className="font-medium">{subscribeTarget?.name}</div>
|
||||
<div className="font-medium">{desensitize(subscribeTarget?.name ?? "")}</div>
|
||||
<div className="text-muted-foreground mt-1 text-xs">
|
||||
{subscribeTarget?.schedule_text} · {subscribeTarget?.timezone}
|
||||
{desensitize(subscribeTarget?.schedule_text ?? "")} · {subscribeTarget?.timezone}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground line-clamp-4 text-sm whitespace-pre-wrap">
|
||||
{subscribeTarget?.prompt_summary || "(无摘要)"}
|
||||
{subscribeTarget?.prompt_summary ? desensitize(subscribeTarget.prompt_summary) : "(无摘要)"}
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div>
|
||||
@@ -809,8 +810,8 @@ export default function ScheduledTasksPage() {
|
||||
{agents.map((agent) => (
|
||||
<SelectItem key={agent.id} value={agent.id}>
|
||||
<span className="block truncate">
|
||||
{agent.name}
|
||||
{agent.description ? ` — ${agent.description}` : ""}
|
||||
{desensitize(agent.name)}
|
||||
{agent.description ? ` — ${desensitize(agent.description)}` : ""}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Bell, ChevronDown, Moon, Palette, User } from "lucide-react";
|
||||
import { Bell, ChevronDown, FileText, Moon, Palette, User } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
||||
@@ -16,7 +16,11 @@ const iconButtonClass = (isDark: boolean) =>
|
||||
: "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]",
|
||||
);
|
||||
|
||||
const Header = () => {
|
||||
type HeaderProps = {
|
||||
collapsed?: boolean;
|
||||
};
|
||||
|
||||
const Header = ({ collapsed }: HeaderProps) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const [notificationOpen, setNotificationOpen] = useState(false);
|
||||
@@ -36,7 +40,23 @@ const Header = () => {
|
||||
: "border-[var(--brand-950)] bg-[var(--brand-900)]",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-4" />
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg",
|
||||
isDark
|
||||
? "bg-gradient-to-br from-blue-500 to-cyan-400"
|
||||
: "bg-white/20",
|
||||
)}
|
||||
>
|
||||
<FileText className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className={cn("text-sm font-semibold", isDark ? "text-white" : "text-white")}>
|
||||
CM助手
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
|
||||
Reference in new issue
Block a user