Merge branch 'ji' into master
This commit is contained in:
commit
af8db1d24c
3 files changed
+142
-41
No files matched your search
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { User } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useMyPosition } from "@/core/positions";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { readUserInfo } from "@/strategy-components/lib/light-app";
|
||||
|
||||
/**
|
||||
* 名称 + 岗位 用户标识。全量模式由 strategy-components/Header.tsx 顶栏右侧展示;
|
||||
* 紧凑模式(不带全局顶栏)无处承载,故抽成共享组件,复用同一套数据来源
|
||||
* (`readUserInfo().userName` 优先、回退 auth 邮箱前缀;岗位取 `useMyPosition`),
|
||||
* 供页面级顶栏(如 ChatPage)右侧复用,保证两套布局一致。
|
||||
*
|
||||
* 配色按当前内容区前景色(foreground / muted-foreground),适配聊天页浅底顶栏,
|
||||
* 而非 Header 的白字贴 banner 方案。
|
||||
*/
|
||||
export function UserIdentityBadge({ className }: { className?: string }) {
|
||||
const auth = getStoredAuth();
|
||||
const { data: myPosition } = useMyPosition();
|
||||
const positionName = myPosition?.name?.trim() || "";
|
||||
|
||||
// 与 Header.tsx 一致:外部登录写入的真实用户名优先,缺省回退邮箱前缀。
|
||||
const displayName = useMemo(() => {
|
||||
const info = readUserInfo();
|
||||
const name = typeof info.userName === "string" ? info.userName.trim() : "";
|
||||
if (name) return name;
|
||||
const email = auth?.email?.trim();
|
||||
return email ? email.split("@")[0] : "用户";
|
||||
}, [auth?.email]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex shrink-0 items-center gap-2", className)}
|
||||
title={positionName ? `${displayName}·${positionName}` : displayName}
|
||||
>
|
||||
<div className="bg-muted text-muted-foreground flex size-7 shrink-0 items-center justify-center rounded-full">
|
||||
<User className="size-3.5" />
|
||||
</div>
|
||||
<div className="hidden min-w-0 text-left sm:block">
|
||||
<p className="text-foreground truncate text-sm font-medium leading-tight">
|
||||
{displayName}
|
||||
</p>
|
||||
{positionName && (
|
||||
<p className="text-muted-foreground truncate text-[11px] leading-tight">
|
||||
{positionName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { QuestionNavigator } from "@/components/workspace/question-navigator";
|
||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { TodoList } from "@/components/workspace/todo-list";
|
||||
import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator";
|
||||
import { UserIdentityBadge } from "@/components/workspace/user-identity-badge";
|
||||
import { Welcome } from "@/components/workspace/welcome";
|
||||
import { CompactWelcome } from "@/components/workspace/compact-welcome";
|
||||
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
|
||||
@@ -481,6 +482,17 @@ export default function ChatPage() {
|
||||
<ExportTrigger threadId={threadId} />
|
||||
<KnowledgeSaveTrigger threadId={threadId} />
|
||||
<ArtifactTrigger />
|
||||
{/* 不带全局顶栏(紧凑模式)时,名称+岗位回落到聊天页顶栏右侧,
|
||||
与全量模式 Header 右上角保持一致。全量模式已有顶栏则不重复展示。 */}
|
||||
{!compactChrome && (
|
||||
<>
|
||||
<div
|
||||
className="bg-border/70 mx-1 h-6 w-px shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
<UserIdentityBadge />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main className="flex min-h-0 max-w-full grow flex-col">
|
||||
|
||||
+76
-41
@@ -2,7 +2,7 @@
|
||||
* 任务深链(rwfx / 任务FX)的「任务工作区」:一个任务挂多个对话(1:N)。
|
||||
*
|
||||
* 左侧 = 该任务下的对话列表(按 metadata.taskId 过滤)+「新建对话」;
|
||||
* 右下角 = 常驻浮层红色按钮(编辑 / 下一步 / 清除任务记录),absolute 定位,时刻可见、不被内容挤走;
|
||||
* 右下角 = 常驻浮动操作条(编辑 / 下一步 / 清除任务记录),absolute 定位,时刻可见、不被内容挤走;
|
||||
* 右侧 = <Outlet/> 渲染选中的对话(AgentChatPage,新建对话顶部带目的树闸门面板)。
|
||||
*
|
||||
* - 进入 chats 索引:有历史 → 跳最新;无历史 → 跳 new(=新建,走闸门)。
|
||||
@@ -61,44 +61,69 @@ function TaskConversationsList({ taskId, chatsBase }: { taskId: string; chatsBas
|
||||
const currentThreadId = useMemo(() => parseThreadId(pathname, chatsBase), [pathname, chatsBase]);
|
||||
|
||||
return (
|
||||
<div className="border-border/70 bg-card/40 flex w-60 shrink-0 flex-col border-r">
|
||||
<div className="border-border/70 border-b p-2">
|
||||
<Button size="sm" className="w-full" onClick={() => navigate(`${chatsBase}/new`)}>
|
||||
<div className="bg-background/85 border-border/70 flex w-64 shrink-0 flex-col border-r shadow-[12px_0_30px_-30px_rgba(15,23,42,0.45)] backdrop-blur-xl dark:border-[#1b477e]/70 dark:bg-[#061b35]/82">
|
||||
<div className="border-border/70 border-b p-3 dark:border-[#1b477e]/70">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-foreground text-sm font-semibold">任务对话</div>
|
||||
<div className="text-muted-foreground mt-0.5 text-xs tabular-nums">
|
||||
{isLoading ? "同步中" : `${threads.length} 个会话`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-9 w-full bg-[var(--color-main)] text-white shadow-sm hover:bg-[var(--brand-950)]"
|
||||
onClick={() => navigate(`${chatsBase}/new`)}
|
||||
>
|
||||
<PlusSquare className="h-4 w-4" /> 新建对话
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-2">
|
||||
{isLoading ? (
|
||||
<div className="text-muted-foreground p-2 text-sm">加载中…</div>
|
||||
<div className="text-muted-foreground rounded-md border border-dashed p-3 text-center text-sm dark:border-[#1b477e]/60">
|
||||
加载中…
|
||||
</div>
|
||||
) : threads.length === 0 ? (
|
||||
<div className="text-muted-foreground p-2 text-sm">暂无对话</div>
|
||||
<div className="text-muted-foreground rounded-md border border-dashed p-3 text-center text-sm dark:border-[#1b477e]/60">
|
||||
暂无对话
|
||||
</div>
|
||||
) : (
|
||||
threads.map((thread) => (
|
||||
<button
|
||||
key={thread.thread_id}
|
||||
onClick={() => navigate(`${chatsBase}/${thread.thread_id}`)}
|
||||
className={cn(
|
||||
"hover:bg-accent flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm",
|
||||
thread.thread_id === currentThreadId && "bg-accent font-medium",
|
||||
"group relative flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm transition-colors",
|
||||
"hover:bg-[var(--bg-color)] dark:hover:bg-[#0d2a4d]/85",
|
||||
thread.thread_id === currentThreadId &&
|
||||
"bg-[var(--bg-color)] font-medium text-[var(--color-main)] shadow-sm dark:bg-[#0d2a4d] dark:text-[#9fe3ff]",
|
||||
)}
|
||||
title={desensitize(titleOfThread(thread, "新对话"))}
|
||||
>
|
||||
<MessageSquare className="h-3.5 w-3.5 shrink-0" />
|
||||
<span
|
||||
className={cn(
|
||||
"absolute inset-y-2 left-0 w-0.5 rounded-full bg-transparent transition-colors",
|
||||
thread.thread_id === currentThreadId && "bg-[var(--color-main)] dark:bg-[#64cdf5]",
|
||||
)}
|
||||
/>
|
||||
<MessageSquare className="text-muted-foreground group-hover:text-foreground h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">{desensitize(titleOfThread(thread, "新对话"))}</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<TaskActionsPanel taskId={taskId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 右下角常驻红色操作浮层:编辑 / 下一步 / 清除任务记录。
|
||||
* absolute 定位在工作区右侧面板内,z-40,时刻可见、不被聊天内容挤走。
|
||||
* 左侧任务栏底部操作区:编辑 / 下一步 / 清除任务记录。
|
||||
* 跟随任务对话面板布局,不再覆盖右侧聊天输入框。
|
||||
*/
|
||||
function TaskFloatingActions({ taskId }: { taskId: string }) {
|
||||
function TaskActionsPanel({ taskId }: { taskId: string }) {
|
||||
const [clearOpen, setClearOpen] = useState(false);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
|
||||
@@ -123,32 +148,43 @@ function TaskFloatingActions({ taskId }: { taskId: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-none absolute bottom-4 right-4 z-40 flex flex-col items-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="pointer-events-auto shadow-lg"
|
||||
onClick={() => openExternal(buildTreemapEditUrl(taskId), "编辑")}
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" /> 编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="pointer-events-auto shadow-lg"
|
||||
onClick={() => openExternal(buildTreemapNextUrl(taskId), "下一步")}
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" /> 下一步
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="pointer-events-auto shadow-lg"
|
||||
onClick={() => setClearOpen(true)}
|
||||
disabled={!taskId}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" /> 清除任务记录
|
||||
</Button>
|
||||
<div className="border-border/70 shrink-0 border-t p-3 dark:border-[#1b477e]/70">
|
||||
<div className="rounded-md border border-[var(--card-border)] bg-background/80 p-2 shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#0b2546]/72">
|
||||
<div className="text-muted-foreground mb-2 px-1 text-xs font-medium">
|
||||
任务操作
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-8 border-[var(--card-border)] bg-background/80 text-[var(--color-main)] hover:bg-[var(--bg-color)] hover:text-[var(--color-main)] dark:border-[#2b8ffe]/50 dark:bg-[#0b2546]/80 dark:text-[#bde9ff] dark:hover:bg-[#10375f]"
|
||||
onClick={() => openExternal(buildTreemapEditUrl(taskId), "编辑")}
|
||||
disabled={!taskId}
|
||||
title="编辑任务"
|
||||
>
|
||||
<ExternalLink className="h-3.5 w-3.5" /> 编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-[var(--color-main)] text-white shadow-[0_8px_18px_-12px_var(--color-main)] hover:bg-[var(--brand-950)]"
|
||||
onClick={() => openExternal(buildTreemapNextUrl(taskId), "下一步")}
|
||||
disabled={!taskId}
|
||||
title="进入下一步"
|
||||
>
|
||||
下一步 <ArrowRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="col-span-2 h-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive dark:text-[#ccd2da] dark:hover:bg-[#5c0011]/35"
|
||||
onClick={() => setClearOpen(true)}
|
||||
disabled={!taskId}
|
||||
title="清除任务记录"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" /> 清除记录
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={clearOpen} onOpenChange={(o) => !clearing && setClearOpen(o)}>
|
||||
@@ -173,7 +209,7 @@ function TaskFloatingActions({ taskId }: { taskId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** 任务工作区布局:左列表 + 右 Outlet + 右下角常驻红色操作浮层。 */
|
||||
/** 任务工作区布局:左列表 + 右 Outlet。 */
|
||||
export function TaskFxWorkspace({ taskBase }: { taskBase: string }) {
|
||||
const { taskId, chatsBase } = useChatsBase(taskBase);
|
||||
// 预拉配置,保证「编辑/下一步」的同步 URL 构造能读到缓存。
|
||||
@@ -185,7 +221,6 @@ export function TaskFxWorkspace({ taskBase }: { taskBase: string }) {
|
||||
<TaskConversationsList taskId={taskId} chatsBase={chatsBase} />
|
||||
<div className="relative min-h-0 min-w-0 flex-1">
|
||||
<Outlet />
|
||||
<TaskFloatingActions taskId={taskId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user