代码提交

This commit is contained in:
董宏凯 committed 2026-09-06 22:17:53 +08:00
1 parent 75f288b241
commit 56bb4f141d
11 files changed
+1066 -3

No files matched your search

+3
View File
@@ -17,6 +17,9 @@ VITE_MOCK_COP_TASK_DETAIL=true
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
# 开发环境普通登录后进入 Vite 内隔离的官方风格页面;/page/* 仍访问当前定制版。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=true
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
# CMZS后端接口
+6
View File
@@ -60,3 +60,9 @@
# 不填或留空时按类型 A,继续使用原来的简介菜单数据。
# VITE_COMPACT_MENU_TYPE=A
# VITE_GITHUB_OAUTH_TOKEN=
# --- 官方 DeerFlow 风格页面入口(仅 Vite,不引入 Next.js)---
# true 时,普通登录后进入同一 Vite 应用中隔离的 /official/*。
# 已指定 redirect 或 taskId/goPath 的登录仍保持原来的优先级。
# 仅 .env.development 默认 true;其余环境保持 false。
# VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
+3
View File
@@ -17,6 +17,9 @@ VITE_MOCK_COP_TASK_DETAIL=false
VITE_BACKEND_BASE_URL=http://10.19.200.5:2026
VITE_LANGGRAPH_BASE_URL=http://10.19.200.5:2026/api
# 内网环境保持当前定制版默认入口。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
# CMZS后端接口
+3
View File
@@ -14,6 +14,9 @@ VITE_MOCK_SQ_REPORT=false
VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow
VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api
# 线上保持当前定制版默认入口。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
# CMZS后端接口
+3
View File
@@ -17,6 +17,9 @@ VITE_MOCK_COP_TASK_DETAIL=false
VITE_BACKEND_BASE_URL=https://ch1.b.sit.4.cn/deerflow
VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api
# SIT 环境保持当前定制版默认入口。
VITE_USE_OFFICIAL_DEERFLOW_ENTRY=false
# 轻应用「新建笔记」系列接口
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
# CMZS后端接口
+2
View File
@@ -45,6 +45,7 @@ import {
} from "@/position-roundtable/demo/position-roundtable-demo-config";
import { ChatRuntime } from "./pages/WorkspaceRoutes";
import { NotFoundPage } from "./pages/NotFoundPage";
import OfficialDeerFlowEntry from "./pages/OfficialDeerFlowEntry";
const PUBLIC_EMBED_PREFIXES = [
"/share/",
@@ -203,6 +204,7 @@ export function App() {
path="/roundtable-dashboard"
element={<DashboardStandalonePage />}
/>
<Route path="/official/*" element={<OfficialDeerFlowEntry />} />
<Route path="/page/*" element={pageElement} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
@@ -0,0 +1,13 @@
/**
* Hash-router entry used by the customized Vite application for the isolated
* official-style UI. Keeping this prefix distinct from `/page/*` prevents its
* routes and mock state from altering the customized application.
*/
export const OFFICIAL_DEERFLOW_ROUTE_PREFIX = "/official";
/** The official-style workspace home shown after a normal login. */
export const OFFICIAL_DEERFLOW_HOME_ROUTE =
`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/new`;
/** Existing customized application's normal post-login destination. */
export const CUSTOMIZED_DEERFLOW_HOME_ROUTE = "/page/workspace/chats/new";
+7
View File
@@ -41,6 +41,13 @@ export const env = {
* flag so operations can switch the shell without rebuilding the frontend.
*/
FORCE_COLLAPSED_SIDEBAR: readRuntimeFlag("VITE_FORCE_COLLAPSED_SIDEBAR"),
/**
* Development-only default entry switch for the isolated Vite-native
* official-style workspace. It never starts or embeds a Next.js app.
*/
USE_OFFICIAL_DEERFLOW_ENTRY: readRuntimeFlag(
"VITE_USE_OFFICIAL_DEERFLOW_ENTRY",
),
/**
* Compact-menu variant for this website. Pair with the admin 简介模式
* type (A / B) so two deployments can show different left-nav items.
+11 -3
View File
@@ -37,7 +37,6 @@ import { isWujieEnv, useWujieProps } from '@/core/embed/wujie';
import { isEmbeddedHost } from '@/core/embed/frame-context';
import { readEmbedParam } from '@/core/embed/embed-session';
import {
GENERAL_QA_ROUTE,
loginByYUserId,
persistAccessToken,
persistToolsToken,
@@ -61,6 +60,11 @@ import { persistYLogToken } from '@/core/log/y-log-token';
import { persistLoginCredentials } from '@/core/auth/login-credentials';
import { OptType, writeYLog } from '@/core/log/y-log';
import { authStorageGet } from '@/core/auth/scoped-storage';
import {
CUSTOMIZED_DEERFLOW_HOME_ROUTE,
OFFICIAL_DEERFLOW_HOME_ROUTE,
} from '@/core/official-deerflow/routes';
import { env } from '@/env';
/** 令牌脱敏:日志里只露头尾,避免完整令牌泄漏。 */
function maskLoginToken(value: string): string {
@@ -103,7 +107,11 @@ function summarizeStoredUserInfo(): Record<string, unknown> | null {
const DEFAULT_USERNAME = 'guest';
const IFRAME_CHAT_MODE = 'iframe';
const DEFAULT_REDIRECT_PATH = '/page/workspace/chats/new';
// Explicit redirects and task deep links retain their existing higher
// priority. This only picks the destination for a standard completed login.
const DEFAULT_REDIRECT_PATH = env.USE_OFFICIAL_DEERFLOW_ENTRY
? OFFICIAL_DEERFLOW_HOME_ROUTE
: CUSTOMIZED_DEERFLOW_HOME_ROUTE;
const FULL_CHAT_MODE = 'full';
type LoginStatus = 'loading' | 'success' | 'error';
@@ -191,7 +199,7 @@ export default function LoginPage() {
// External iframe callers frequently also provide yUserId for login.
// That identity parameter must not steal a deliberate iframe entry and
// send it to the generic full-chat home before the route split above.
if (yUserId) return GENERAL_QA_ROUTE;
if (yUserId) return DEFAULT_REDIRECT_PATH;
return resolvePostLoginRedirectPath(
searchParams.toString(),
DEFAULT_REDIRECT_PATH,
@@ -0,0 +1,776 @@
import type { Message } from "@langchain/langgraph-sdk";
import {
Bot,
CalendarClock,
ChevronDown,
CircleHelp,
Clock3,
LoaderCircle,
MessageSquareText,
PanelLeftClose,
Pause,
Play,
PlusSquare,
Search,
SendHorizontal,
Settings2,
Sparkles,
UserRound,
X,
} from "lucide-react";
import { type FormEvent, useCallback, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useAgent, useAgents } from "@/core/agents";
import { getAccountDisplayName } from "@/core/auth";
import {
type ScheduledTask,
useScheduledTaskActions,
useScheduledTaskRuns,
useScheduledTasks,
} from "@/core/scheduled-tasks";
import { useLocalSettings } from "@/core/settings";
import {
useAppearanceSettings,
useCitationDisplaySettings,
} from "@/core/system-settings/hooks";
import { useThreadStream, useThreads } from "@/core/threads/hooks";
import { textOfMessage, titleOfThread } from "@/core/threads/utils";
import { uuid } from "@/core/utils/uuid";
import {
OFFICIAL_DEERFLOW_HOME_ROUTE,
OFFICIAL_DEERFLOW_ROUTE_PREFIX,
} from "@/core/official-deerflow/routes";
import "./official-deerflow-entry.css";
type OfficialPage = "chat" | "agents" | "agent-chat" | "scheduled" | "settings";
function pageFromPath(pathname: string): OfficialPage {
const path = pathname.slice(OFFICIAL_DEERFLOW_ROUTE_PREFIX.length);
if (path.startsWith("/workspace/agents/") && path.includes("/chats")) {
return "agent-chat";
}
if (path.startsWith("/workspace/agents")) return "agents";
if (path.startsWith("/workspace/scheduled-tasks")) return "scheduled";
if (path.startsWith("/workspace/settings")) return "settings";
return "chat";
}
function parseChatRoute(pathname: string) {
const chatMatch = pathname.match(/\/workspace\/chats\/([^/]+)/);
if (chatMatch) return { threadId: decodeURIComponent(chatMatch[1]!), agentId: null };
const agentMatch = pathname.match(
/\/workspace\/agents\/([^/]+)\/chats\/([^/]+)/,
);
if (agentMatch) {
return {
agentId: decodeURIComponent(agentMatch[1]!),
threadId: decodeURIComponent(agentMatch[2]!),
};
}
return { threadId: "new", agentId: null };
}
function parseScheduledTaskId(pathname: string) {
const match = pathname.match(/\/workspace\/scheduled-tasks\/([^/]+)/);
return match ? decodeURIComponent(match[1]!) : null;
}
function readableError(error: unknown, fallback: string) {
return error instanceof Error && error.message ? error.message : fallback;
}
function formatTimestamp(value: string | null | undefined) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
const elapsed = Date.now() - date.getTime();
if (elapsed < 60_000) return "刚刚";
if (elapsed < 3_600_000) return `${Math.max(1, Math.floor(elapsed / 60_000))} 分钟前`;
if (elapsed < 86_400_000) return `${Math.floor(elapsed / 3_600_000)} 小时前`;
return new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric" }).format(date);
}
function statusForTask(task: ScheduledTask) {
const status = task.last_status;
if (status === "running" || status === "pending") {
return { className: status, label: "执行中" };
}
if (!task.enabled) return { className: "paused", label: "已暂停" };
if (status === "failed") return { className: "failed", label: "执行失败" };
if (status === "succeeded") return { className: "succeeded", label: "已完成" };
return { className: "ready", label: "已启用" };
}
function runStatusText(status: string) {
if (status === "running" || status === "pending") return "执行中";
if (status === "succeeded") return "已完成";
if (status === "failed") return "执行失败";
return status || "等待执行";
}
function OfficialNavItem({
active,
icon: Icon,
label,
onClick,
}: {
active: boolean;
icon: typeof MessageSquareText;
label: string;
onClick: () => void;
}) {
return (
<button
className={`official-nav-item${active ? " is-active" : ""}`}
onClick={onClick}
type="button"
>
<Icon aria-hidden="true" size={18} strokeWidth={1.8} />
<span>{label}</span>
</button>
);
}
function ThreadMessage({ message }: { message: Message }) {
const text = textOfMessage(message)?.trim();
if (!text) return null;
const isUser = message.type === "human";
return (
<div className={`official-message official-message-${isUser ? "user" : "assistant"}`}>
{!isUser && (
<div className="official-assistant-mark">
<Sparkles aria-hidden="true" size={16} />
DeerFlow
</div>
)}
<p>{text}</p>
</div>
);
}
function ChatWorkspace({
agentId,
routeThreadId,
}: {
agentId: string | null;
routeThreadId: string;
}) {
const navigate = useNavigate();
const [prompt, setPrompt] = useState("");
const [sendError, setSendError] = useState<string | null>(null);
const isNewThread = routeThreadId === "new";
// A new conversation uses the same client-generated id approach as the
// existing ChatPage. The backend owns the persisted id once streaming starts.
const [draftThreadId] = useState<string>(() => uuid() as string);
const activeThreadId = isNewThread ? draftThreadId : routeThreadId;
const { agent } = useAgent(agentId, { enabled: !!agentId });
const [localSettings] = useLocalSettings();
const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : routeThreadId,
assistantId: agentId ?? undefined,
context: {
...localSettings.context,
...(agentId ? { agent_id: agentId } : {}),
},
onStart: (createdThreadId) => {
const nextPath = agentId
? `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents/${encodeURIComponent(agentId)}/chats/${encodeURIComponent(createdThreadId)}`
: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/${encodeURIComponent(createdThreadId)}`;
navigate(nextPath, { replace: true });
},
});
const messages = useMemo(
() =>
(thread.messages ?? []).filter(
(message): message is Message =>
message.type === "human" || message.type === "ai",
),
[thread.messages],
);
const title = isNewThread
? agent?.name ?? "新对话"
: thread.values?.title ?? agent?.name ?? "对话";
const streamError = thread.error
? readableError(thread.error, "对话加载失败,请稍后重试。")
: null;
const handleSubmit = useCallback(
(event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const text = prompt.trim();
if (!text || thread.isLoading || isUploading) return;
setPrompt("");
setSendError(null);
void sendMessage(activeThreadId, { text, files: [] }).catch((error) => {
setSendError(readableError(error, "消息发送失败,请稍后重试。"));
});
},
[activeThreadId, isUploading, prompt, sendMessage, thread.isLoading],
);
return (
<section className="official-chat-page">
<header className="official-page-header official-chat-header">
<div className="official-thread-title">
{title}
<ChevronDown aria-hidden="true" size={16} />
</div>
<div className="official-header-actions">
<button
className="official-outline-button"
onClick={() => navigate(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks`)}
type="button"
>
<CalendarClock aria-hidden="true" size={17} />
定时任务
</button>
<button aria-label="帮助" className="official-icon-button" type="button">
<CircleHelp aria-hidden="true" size={18} />
</button>
</div>
</header>
<div className={`official-chat-canvas${messages.length ? " has-history" : ""}`}>
{messages.length ? (
<div className="official-message-list">
{messages.map((message, index) => (
<ThreadMessage key={message.id ?? `${message.type}-${index}`} message={message} />
))}
{thread.isLoading && (
<div className="official-streaming-indicator">
<LoaderCircle aria-hidden="true" size={15} />
DeerFlow 正在回复…
</div>
)}
</div>
) : (
<div className="official-chat-welcome">
<div className="official-logo-orb">
<Sparkles aria-hidden="true" size={28} />
</div>
<h1>{agent?.name ? `与 ${agent.name} 开始对话` : "今天想一起完成什么?"}</h1>
<p>直接连接当前 DeerFlow 后端;消息与历史记录会同步保存。</p>
{!agentId && (
<div className="official-suggestion-grid">
{[
["做一份市场调研", "比较行业趋势并形成要点"],
["规划项目", "把目标拆解成清晰步骤"],
["写一篇初稿", "从想法到结构化内容"],
].map(([suggestion, detail]) => (
<button key={suggestion} onClick={() => setPrompt(suggestion)} type="button">
<strong>{suggestion}</strong>
<span>{detail}</span>
</button>
))}
</div>
)}
</div>
)}
</div>
{(streamError || sendError) && (
<p className="official-inline-error" role="alert">
{sendError ?? streamError}
</p>
)}
<form className="official-composer-shell" onSubmit={handleSubmit}>
<div className="official-composer">
<textarea
aria-label="输入消息"
disabled={thread.isLoading || isUploading}
onChange={(event) => setPrompt(event.target.value)}
placeholder="给 DeerFlow 发送消息…"
rows={1}
value={prompt}
/>
<button
aria-label="发送"
disabled={!prompt.trim() || thread.isLoading || isUploading}
type="submit"
>
<SendHorizontal aria-hidden="true" size={18} />
</button>
</div>
<p>连接当前 DeerFlow 后端</p>
</form>
</section>
);
}
function AgentsPage({ openAgent }: { openAgent: (id: string) => void }) {
const navigate = useNavigate();
const [query, setQuery] = useState("");
const { agents, isLoading, error } = useAgents({ search: query });
return (
<section className="official-content-page">
<header className="official-page-header">
<div>
<p className="official-eyebrow">工作区</p>
<h1>智能体</h1>
<p>以下智能体直接来自当前 DeerFlow 后端。</p>
</div>
<button
className="official-primary-button"
onClick={() => navigate("/page/workspace/agents/new")}
type="button"
>
<PlusSquare aria-hidden="true" size={17} />
创建智能体
</button>
</header>
<div className="official-toolbar">
<label className="official-search">
<Search aria-hidden="true" size={17} />
<input
onChange={(event) => setQuery(event.target.value)}
placeholder="搜索智能体"
value={query}
/>
</label>
</div>
{error ? (
<p className="official-query-error" role="alert">
{readableError(error, "智能体列表加载失败。")}
</p>
) : isLoading ? (
<div className="official-loading-state"><LoaderCircle aria-hidden="true" size={18} /> 正在加载智能体…</div>
) : agents.length ? (
<div className="official-agent-grid">
{agents.map((agent, index) => (
<article className="official-agent-card" key={agent.id}>
<div className={`official-agent-avatar ${["violet", "sky", "amber"][index % 3]}`}>
{agent.name.slice(0, 2).toLocaleUpperCase()}
</div>
<h2>{agent.name}</h2>
<p>{agent.description || "暂无简介"}</p>
<button onClick={() => openAgent(agent.id)} type="button">
开始对话
</button>
</article>
))}
</div>
) : (
<div className="official-empty-panel">
<div className="official-empty-icon"><Bot aria-hidden="true" size={24} /></div>
<h2>暂无可用智能体</h2>
<p>可从“创建智能体”进入现有的完整配置页面。</p>
</div>
)}
</section>
);
}
function ScheduledTasksPage({ selectedTaskId }: { selectedTaskId: string | null }) {
const navigate = useNavigate();
const { data: tasks = [], isLoading, error } = useScheduledTasks();
const { data: runs = [], isLoading: runsLoading, error: runsError } =
useScheduledTaskRuns(selectedTaskId);
const actions = useScheduledTaskActions();
const [actionTaskId, setActionTaskId] = useState<string | null>(null);
const [actionError, setActionError] = useState<string | null>(null);
const selectedTask = tasks.find((task) => task.task_id === selectedTaskId) ?? null;
const openTask = useCallback(
(taskId: string) => {
navigate(
`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks/${encodeURIComponent(taskId)}`,
);
},
[navigate],
);
const closeTask = useCallback(() => {
navigate(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks`);
}, [navigate]);
const runTaskAction = useCallback(
async (task: ScheduledTask, action: "trigger" | "pause" | "resume") => {
setActionTaskId(task.task_id);
setActionError(null);
try {
if (action === "trigger") {
await actions.trigger.mutateAsync(task.task_id);
openTask(task.task_id);
} else if (action === "pause") {
await actions.pause.mutateAsync(task.task_id);
} else {
await actions.resume.mutateAsync(task.task_id);
}
} catch (actionFailure) {
setActionError(
readableError(
actionFailure,
action === "trigger" ? "任务触发失败。" : "任务状态更新失败。",
),
);
} finally {
setActionTaskId(null);
}
},
[actions.pause, actions.resume, actions.trigger, openTask],
);
const actionIsPending = (taskId: string) => actionTaskId === taskId;
return (
<section className="official-content-page">
<header className="official-page-header">
<div>
<p className="official-eyebrow">工作区</p>
<h1>定时任务</h1>
<p>任务、运行记录和控制操作均直接请求当前 DeerFlow 后端。</p>
</div>
<button
className="official-primary-button"
onClick={() => navigate("/page/workspace/scheduled/tasks")}
type="button"
>
<PlusSquare aria-hidden="true" size={17} />
新建任务
</button>
</header>
{actionError && (
<p className="official-query-error" role="alert">
{actionError}
</p>
)}
{selectedTask && (
<section className="official-task-detail" aria-label={`${selectedTask.name}详情`}>
<header>
<div>
<p className="official-eyebrow">任务详情</p>
<h2>{selectedTask.name}</h2>
<p>{selectedTask.description || selectedTask.prompt}</p>
</div>
<button
aria-label="关闭任务详情"
className="official-icon-button"
onClick={closeTask}
type="button"
>
<X aria-hidden="true" size={18} />
</button>
</header>
<div className="official-task-meta">
<span>计划:{selectedTask.schedule_text || selectedTask.cron_expr}</span>
<span>时区:{selectedTask.timezone}</span>
<span>下次执行:{formatTimestamp(selectedTask.next_run_at) || "未计划"}</span>
</div>
<div className="official-task-detail-actions">
<button
className="official-secondary-button"
disabled={actionIsPending(selectedTask.task_id)}
onClick={() => void runTaskAction(selectedTask, "trigger")}
type="button"
>
{actionIsPending(selectedTask.task_id) ? <LoaderCircle aria-hidden="true" size={15} /> : <Play aria-hidden="true" size={15} />}
立即执行
</button>
<button
className="official-secondary-button"
disabled={actionIsPending(selectedTask.task_id)}
onClick={() =>
void runTaskAction(
selectedTask,
selectedTask.enabled ? "pause" : "resume",
)
}
type="button"
>
{selectedTask.enabled ? <Pause aria-hidden="true" size={15} /> : <Play aria-hidden="true" size={15} />}
{selectedTask.enabled ? "暂停任务" : "恢复任务"}
</button>
</div>
<div className="official-run-list">
<h3>最近运行</h3>
{runsError ? (
<p className="official-query-error" role="alert">
{readableError(runsError, "运行记录加载失败。")}
</p>
) : runsLoading ? (
<p className="official-run-empty"><LoaderCircle aria-hidden="true" size={15} /> 正在加载运行记录…</p>
) : runs.length ? (
runs.slice(0, 8).map((run) => (
<button
className="official-run-row"
key={run.id}
onClick={() => navigate(`/page/workspace/scheduled/runs/${encodeURIComponent(run.id)}`)}
type="button"
>
<span className={`official-run-status is-${run.status}`}>{runStatusText(run.status)}</span>
<span>{formatTimestamp(run.started_at ?? run.scheduled_for) || "等待执行"}</span>
<span>{run.result?.title || run.error || "查看完整运行结果"}</span>
</button>
))
) : (
<p className="official-run-empty">该任务暂时没有运行记录。</p>
)}
</div>
</section>
)}
{error ? (
<p className="official-query-error" role="alert">
{readableError(error, "定时任务列表加载失败。")}
</p>
) : isLoading ? (
<div className="official-loading-state"><LoaderCircle aria-hidden="true" size={18} /> 正在加载定时任务…</div>
) : tasks.length ? (
<div className="official-task-list">
{tasks.map((task) => {
const status = statusForTask(task);
const pending = actionIsPending(task.task_id);
return (
<article
className={`official-task-card${selectedTaskId === task.task_id ? " is-selected" : ""}`}
key={task.task_id}
>
<button
className="official-task-card-main"
onClick={() => openTask(task.task_id)}
type="button"
>
<span className="official-task-icon"><Clock3 aria-hidden="true" size={18} /></span>
<span className="official-task-copy">
<strong>{task.name}</strong>
<small>{task.schedule_text || task.cron_expr}</small>
</span>
<span className={`official-task-status is-${status.className}`}>{status.label}</span>
<time>{formatTimestamp(task.next_run_at ?? task.updated_at)}</time>
</button>
<div className="official-task-card-actions">
<button
aria-label={`立即执行 ${task.name}`}
disabled={pending}
onClick={() => void runTaskAction(task, "trigger")}
title="立即执行"
type="button"
>
{pending ? <LoaderCircle aria-hidden="true" size={16} /> : <Play aria-hidden="true" size={16} />}
</button>
<button
aria-label={task.enabled ? `暂停 ${task.name}` : `恢复 ${task.name}`}
disabled={pending}
onClick={() => void runTaskAction(task, task.enabled ? "pause" : "resume")}
title={task.enabled ? "暂停任务" : "恢复任务"}
type="button"
>
{task.enabled ? <Pause aria-hidden="true" size={16} /> : <Play aria-hidden="true" size={16} />}
</button>
</div>
</article>
);
})}
</div>
) : (
<div className="official-empty-panel">
<div className="official-empty-icon"><Clock3 aria-hidden="true" size={24} /></div>
<h2>还没有定时任务</h2>
<p>创建一个任务,按天、周或自定义时间自动执行。</p>
<button
className="official-secondary-button"
onClick={() => navigate("/page/workspace/scheduled/tasks")}
type="button"
>
创建定时任务
</button>
</div>
)}
</section>
);
}
function SettingsPage() {
const { data: appearance, isLoading: appearanceLoading, error: appearanceError } =
useAppearanceSettings();
const { data: citation, isLoading: citationLoading, error: citationError } =
useCitationDisplaySettings();
const settings = [
appearance && {
label: "默认外观",
detail: `主题:${appearance.default_theme};布局:${appearance.default_layout_mode}`,
},
appearance && {
label: "默认展示方式",
detail: `智能体:${appearance.default_agent_view};技能:${appearance.default_skill_view}`,
},
citation && {
label: "引用展示",
detail: citation.reference_mode_enabled ? "已启用引用模式" : "未启用引用模式",
},
].filter(Boolean) as Array<{ label: string; detail: string }>;
const isLoading = appearanceLoading || citationLoading;
return (
<section className="official-content-page">
<header className="official-page-header">
<div>
<p className="official-eyebrow">工作区</p>
<h1>设置</h1>
<p>只展示已由当前 DeerFlow 后端提供的配置,不使用模拟值。</p>
</div>
</header>
{isLoading && !settings.length ? (
<div className="official-loading-state"><LoaderCircle aria-hidden="true" size={18} /> 正在加载系统配置…</div>
) : settings.length ? (
<div className="official-settings-list">
{settings.map((setting) => (
<div className="official-setting-row" key={setting.label}>
<strong>{setting.label}</strong>
<small>{setting.detail}</small>
</div>
))}
</div>
) : (
<div className="official-empty-panel">
<div className="official-empty-icon"><Settings2 aria-hidden="true" size={24} /></div>
<h2>暂时没有可读取的系统配置</h2>
<p>不会用静态数据替代未返回的后端配置。</p>
</div>
)}
{(appearanceError || citationError) && (
<p className="official-query-error" role="alert">
部分系统配置不可读取:{readableError(appearanceError ?? citationError, "请检查登录权限。")}
</p>
)}
</section>
);
}
/**
* Vite-native official-style workspace. It reuses the current frontend's
* backend clients and stream adapter; only reference-only UI has local state.
*/
export default function OfficialDeerFlowEntry() {
const location = useLocation();
const navigate = useNavigate();
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const page = pageFromPath(location.pathname);
const chatRoute = useMemo(
() => parseChatRoute(location.pathname),
[location.pathname],
);
const scheduledTaskId = useMemo(
() => parseScheduledTaskId(location.pathname),
[location.pathname],
);
const { data: threads = [], isLoading: threadsLoading, error: threadsError } =
useThreads({
limit: 20,
sortBy: "updated_at",
sortOrder: "desc",
select: ["thread_id", "updated_at", "values", "metadata"],
});
const go = (path: string) => navigate(path);
const nav = [
{ icon: PlusSquare, label: "新对话", path: OFFICIAL_DEERFLOW_HOME_ROUTE, pages: ["chat"] },
{ icon: MessageSquareText, label: "对话", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/new`, pages: ["chat"] },
{ icon: Bot, label: "智能体", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents`, pages: ["agents", "agent-chat"] },
{ icon: CalendarClock, label: "定时任务", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks`, pages: ["scheduled"] },
] as const;
return (
<main className={`official-deerflow${sidebarCollapsed ? " sidebar-collapsed" : ""}`}>
<aside className="official-sidebar">
<div className="official-brand-row">
<div className="official-brand">DeerFlow</div>
<button
aria-label="折叠侧栏"
className="official-collapse-button"
onClick={() => setSidebarCollapsed((value) => !value)}
type="button"
>
<PanelLeftClose aria-hidden="true" size={17} />
</button>
</div>
<nav aria-label="官方页面导航" className="official-nav">
{nav.map((item) => (
<OfficialNavItem
active={item.pages.some((candidate) => candidate === page)}
icon={item.icon}
key={item.label}
label={item.label}
onClick={() => go(item.path)}
/>
))}
</nav>
<div className="official-recent-section">
<p>最近对话</p>
{threadsLoading ? (
<div className="official-sidebar-loading"><LoaderCircle aria-hidden="true" size={14} /> 加载中…</div>
) : threadsError ? (
<p className="official-sidebar-error">对话列表加载失败</p>
) : threads.length ? (
threads.map((chat) => (
<button
className={`official-recent-chat${chatRoute.threadId === chat.thread_id ? " is-active" : ""}`}
key={chat.thread_id}
onClick={() => {
const agentId = typeof chat.context?.agent_id === "string"
? chat.context.agent_id
: typeof chat.metadata?.agent_id === "string"
? chat.metadata.agent_id
: null;
go(
agentId
? `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents/${encodeURIComponent(agentId)}/chats/${encodeURIComponent(chat.thread_id)}`
: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/${encodeURIComponent(chat.thread_id)}`,
);
}}
type="button"
>
<MessageSquareText aria-hidden="true" size={15} />
<span>{titleOfThread(chat, "未命名对话")}</span>
<small>{formatTimestamp(chat.updated_at)}</small>
</button>
))
) : (
<p className="official-sidebar-empty">暂无历史对话</p>
)}
</div>
<div className="official-sidebar-footer">
<button onClick={() => go(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/settings`)} type="button">
<Settings2 aria-hidden="true" size={18} />
<span>设置和更多</span>
<ChevronDown aria-hidden="true" size={15} />
</button>
<div className="official-user-row">
<span className="official-user-avatar"><UserRound aria-hidden="true" size={15} /></span>
<span>{getAccountDisplayName()}</span>
</div>
</div>
</aside>
<div className="official-main-panel">
{page === "chat" && (
<ChatWorkspace
agentId={null}
key={location.key}
routeThreadId={chatRoute.threadId}
/>
)}
{page === "agents" && (
<AgentsPage
openAgent={(agentId) =>
go(`${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents/${encodeURIComponent(agentId)}/chats/new`)
}
/>
)}
{page === "agent-chat" && (
<ChatWorkspace
agentId={chatRoute.agentId}
key={location.key}
routeThreadId={chatRoute.threadId}
/>
)}
{page === "scheduled" && <ScheduledTasksPage selectedTaskId={scheduledTaskId} />}
{page === "settings" && <SettingsPage />}
</div>
</main>
);
}
@@ -0,0 +1,239 @@
.official-deerflow {
--official-ink: #232323;
--official-muted: #787878;
--official-border: #eeeeeb;
--official-sidebar: #fbfbfa;
--official-accent: #111111;
background: #fff;
color: var(--official-ink);
display: flex;
font-family: Inter, "Noto Sans SC", "PingFang SC", system-ui, sans-serif;
height: 100vh;
isolation: isolate;
overflow: hidden;
}
.official-deerflow *,
.official-deerflow *::before,
.official-deerflow *::after { box-sizing: border-box; }
.official-deerflow button,
.official-deerflow input,
.official-deerflow textarea { font: inherit; }
.official-sidebar {
background: var(--official-sidebar);
border-right: 1px solid var(--official-border);
display: flex;
flex: 0 0 288px;
flex-direction: column;
min-width: 0;
padding: 16px 10px 12px;
transition: flex-basis 180ms ease;
}
.sidebar-collapsed .official-sidebar { flex-basis: 64px; }
.sidebar-collapsed .official-sidebar span,
.sidebar-collapsed .official-sidebar p,
.sidebar-collapsed .official-sidebar small,
.sidebar-collapsed .official-brand { display: none; }
.sidebar-collapsed .official-brand-row { justify-content: center; }
.sidebar-collapsed .official-collapse-button { transform: rotate(180deg); }
.sidebar-collapsed .official-nav-item,
.sidebar-collapsed .official-recent-chat,
.sidebar-collapsed .official-sidebar-footer > button,
.sidebar-collapsed .official-user-row { justify-content: center; }
.official-brand-row { align-items: center; display: flex; height: 34px; justify-content: space-between; padding: 0 4px 0 1px; }
.official-brand { font-family: Georgia, "Times New Roman", serif; font-size: 20px; font-weight: 700; letter-spacing: -0.5px; }
.official-collapse-button,
.official-icon-button { align-items: center; background: transparent; border: 0; border-radius: 7px; color: #787878; cursor: pointer; display: inline-flex; justify-content: center; padding: 6px; }
.official-collapse-button:hover,
.official-icon-button:hover { background: #f0f0ee; color: var(--official-ink); }
.official-nav { display: grid; gap: 3px; margin-top: 31px; }
.official-nav-item,
.official-recent-chat,
.official-sidebar-footer > button { align-items: center; background: transparent; border: 0; border-radius: 7px; color: #6e6e6e; cursor: pointer; display: flex; gap: 10px; min-width: 0; padding: 9px 8px; text-align: left; width: 100%; }
.official-nav-item { font-size: 16px; }
.official-nav-item:hover,
.official-recent-chat:hover,
.official-sidebar-footer > button:hover { background: #f0f0ee; color: #222; }
.official-nav-item.is-active { background: #efefed; color: #252525; font-weight: 600; }
.official-nav-item span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.official-recent-section { border-top: 1px solid var(--official-border); margin-top: 25px; padding-top: 18px; }
.official-recent-section > p { color: #90908e; font-size: 12px; margin: 0 8px 9px; }
.official-recent-chat { display: grid; font-size: 13px; gap: 8px; grid-template-columns: 15px minmax(0, 1fr) auto; padding: 8px; }
.official-recent-chat span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.official-recent-chat small { color: #acaca9; font-size: 11px; }
.official-recent-chat.is-active { background: #efefed; color: #252525; }
.official-sidebar-loading,
.official-sidebar-error,
.official-sidebar-empty { align-items: center; color: #989895; display: flex; font-size: 12px; gap: 6px; margin: 0; padding: 8px; }
.official-sidebar-error { color: #b04747; }
.official-sidebar-footer { border-top: 1px solid var(--official-border); margin-top: auto; padding-top: 9px; }
.official-sidebar-footer > button { font-size: 14px; }
.official-sidebar-footer > button svg:last-child { margin-left: auto; }
.official-user-row { align-items: center; color: #777; display: flex; font-size: 13px; gap: 8px; margin: 10px 8px 1px; }
.official-user-avatar { align-items: center; background: #ecece9; border-radius: 50%; color: #71716f; display: inline-flex; height: 25px; justify-content: center; width: 25px; }
.official-main-panel { background: #fff; flex: 1; min-width: 0; overflow: auto; }
.official-page-header { align-items: flex-start; border-bottom: 1px solid var(--official-border); display: flex; justify-content: space-between; min-height: 104px; padding: 27px max(32px, 5vw); }
.official-page-header h1 { font-size: 24px; letter-spacing: -0.5px; line-height: 1.2; margin: 3px 0 7px; }
.official-page-header p { color: var(--official-muted); font-size: 14px; margin: 0; }
.official-eyebrow { color: #9a9a98 !important; font-size: 12px !important; }
.official-primary-button,
.official-secondary-button,
.official-outline-button { align-items: center; border-radius: 8px; cursor: pointer; display: inline-flex; gap: 7px; font-size: 14px; font-weight: 550; justify-content: center; white-space: nowrap; }
.official-primary-button { background: var(--official-accent); border: 1px solid var(--official-accent); color: #fff; padding: 9px 13px; }
.official-primary-button:hover { background: #303030; }
.official-secondary-button,
.official-outline-button { background: #fff; border: 1px solid #dededb; color: #3d3d3d; padding: 8px 12px; }
.official-secondary-button:hover,
.official-outline-button:hover { background: #f7f7f5; }
.official-secondary-button:disabled { cursor: not-allowed; opacity: .55; }
.official-chat-page { display: flex; flex-direction: column; height: 100%; min-height: 660px; }
.official-chat-header { align-items: center; min-height: 62px; padding-bottom: 12px; padding-top: 12px; }
.official-thread-title { align-items: center; display: flex; font-size: 14px; font-weight: 600; gap: 3px; }
.official-header-actions { align-items: center; display: flex; gap: 10px; }
.official-chat-canvas { align-items: center; display: flex; flex: 1; justify-content: center; min-height: 0; padding: 32px; }
.official-chat-canvas.has-history { align-items: stretch; justify-content: center; overflow: auto; }
.official-chat-welcome { margin: auto; max-width: 650px; text-align: center; transform: translateY(-35px); }
.official-logo-orb { align-items: center; background: linear-gradient(145deg, #202020, #626260); border-radius: 18px; color: white; display: inline-flex; height: 55px; justify-content: center; width: 55px; }
.official-chat-welcome h1 { font-size: clamp(25px, 3vw, 35px); letter-spacing: -1.15px; line-height: 1.2; margin: 21px 0 9px; }
.official-chat-welcome > p { color: var(--official-muted); font-size: 15px; margin: 0; }
.official-suggestion-grid { display: grid; gap: 10px; grid-template-columns: repeat(3, 1fr); margin-top: 31px; text-align: left; }
.official-suggestion-grid button { background: #fff; border: 1px solid #e7e7e4; border-radius: 11px; color: #363636; cursor: pointer; min-height: 94px; padding: 14px; text-align: left; transition: border-color 120ms ease, box-shadow 120ms ease; }
.official-suggestion-grid button:hover { border-color: #bdbdba; box-shadow: 0 4px 18px #0000000a; }
.official-suggestion-grid strong,
.official-suggestion-grid span { display: block; }
.official-suggestion-grid strong { font-size: 14px; margin-bottom: 6px; }
.official-suggestion-grid span { color: #8a8a87; font-size: 12px; line-height: 1.45; }
.official-composer-shell { margin: 0 auto; max-width: 850px; padding: 0 24px 19px; width: 100%; }
.official-composer { align-items: flex-end; background: #fff; border: 1px solid #dcdcd8; border-radius: 13px; box-shadow: 0 7px 20px #00000008; display: flex; gap: 8px; padding: 11px 11px 10px 15px; }
.official-composer:focus-within { border-color: #adadaa; box-shadow: 0 0 0 3px #00000009; }
.official-composer textarea { background: transparent; border: 0; color: #333; flex: 1; font-size: 15px; line-height: 24px; max-height: 144px; outline: 0; resize: none; }
.official-composer textarea::placeholder { color: #a2a2a0; }
.official-composer button { align-items: center; background: #222; border: 0; border-radius: 8px; color: #fff; cursor: pointer; display: inline-flex; height: 31px; justify-content: center; transition: opacity 100ms ease; width: 31px; }
.official-composer button:disabled { cursor: not-allowed; opacity: .3; }
.official-composer-shell > p { color: #acaca8; font-size: 11px; margin: 9px 0 0; text-align: center; }
.official-message-list { display: grid; gap: 18px; margin: 0 auto; max-width: 760px; padding: 12px 0 22px; width: 100%; }
.official-message { border-radius: 12px; font-size: 15px; line-height: 1.65; padding: 14px 16px; }
.official-message-user { background: #f4f4f2; justify-self: end; max-width: 76%; }
.official-message-assistant { border: 1px solid #eeeeeb; justify-self: start; max-width: 88%; }
.official-message p { margin: 0; white-space: pre-wrap; }
.official-message-assistant p { margin: 10px 0 13px; }
.official-assistant-mark { align-items: center; color: #5c5c5a; display: flex; font-size: 12px; font-weight: 600; gap: 6px; }
.official-insight-card { background: #f7f7f5; border-radius: 8px; display: grid; gap: 3px; padding: 10px 11px; }
.official-insight-card strong { font-size: 13px; }
.official-insight-card span { color: #858582; font-size: 12px; }
.official-streaming-indicator { align-items: center; color: #7d7d7a; display: flex; font-size: 13px; gap: 7px; padding: 6px 1px; }
.official-streaming-indicator svg,
.official-loading-state svg,
.official-sidebar-loading svg { animation: official-spin 900ms linear infinite; }
.official-inline-error { color: #ae3e3e; font-size: 12px; margin: 0 auto 8px; max-width: 850px; padding: 0 24px; width: 100%; }
.official-content-page { margin: 0 auto; max-width: 1180px; min-height: 100%; }
.official-content-page .official-page-header { padding-left: max(32px, 5vw); padding-right: max(32px, 5vw); }
.official-toolbar { padding: 22px max(32px, 5vw) 0; }
.official-search { align-items: center; background: #f7f7f5; border: 1px solid transparent; border-radius: 8px; color: #8a8a87; display: flex; gap: 8px; max-width: 330px; padding: 8px 10px; }
.official-search:focus-within { background: #fff; border-color: #d1d1ce; }
.official-search input { background: transparent; border: 0; color: #333; font-size: 14px; min-width: 0; outline: 0; width: 100%; }
.official-agent-grid { display: grid; gap: 15px; grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 24px max(32px, 5vw) 40px; }
.official-agent-card { border: 1px solid #e8e8e5; border-radius: 12px; min-height: 244px; padding: 19px; position: relative; }
.official-agent-card:hover { border-color: #cfcfcb; box-shadow: 0 8px 22px #00000008; }
.official-agent-avatar { align-items: center; border-radius: 9px; display: inline-flex; font-size: 13px; font-weight: 700; height: 38px; justify-content: center; width: 38px; }
.official-agent-avatar.violet { background: #eeeafe; color: #6d56b7; }
.official-agent-avatar.sky { background: #e6f4ff; color: #2786bc; }
.official-agent-avatar.amber { background: #fff1d7; color: #b46d0d; }
.official-agent-card h2 { font-size: 16px; letter-spacing: -.2px; margin: 17px 0 7px; }
.official-agent-card p { color: #80807e; font-size: 13px; line-height: 1.55; margin: 0; }
.official-agent-card button { background: transparent; border: 0; bottom: 17px; color: #3c3c3a; cursor: pointer; font-size: 13px; font-weight: 600; left: 14px; padding: 5px; position: absolute; }
.official-agent-card button:hover { text-decoration: underline; }
.official-query-error { color: #ae3e3e; font-size: 14px; margin: 26px max(32px, 5vw); }
.official-loading-state { align-items: center; color: #7e7e7b; display: flex; font-size: 14px; gap: 8px; justify-content: center; min-height: 300px; }
.official-empty-panel { align-items: center; display: flex; flex-direction: column; justify-content: center; min-height: 430px; padding: 32px; text-align: center; }
.official-empty-icon { align-items: center; background: #f3f3f1; border-radius: 13px; color: #666; display: flex; height: 52px; justify-content: center; width: 52px; }
.official-empty-panel h2 { font-size: 18px; margin: 17px 0 7px; }
.official-empty-panel p { color: #858582; font-size: 14px; margin: 0 0 18px; }
.official-settings-list { display: grid; gap: 8px; padding: 25px max(32px, 5vw); }
.official-setting-row { background: #fff; border: 1px solid #e8e8e5; border-radius: 10px; color: #3b3b39; display: block; padding: 15px 17px; text-align: left; }
.official-settings-list strong,
.official-settings-list small { display: block; }
.official-settings-list strong { font-size: 14px; }
.official-settings-list small { color: #888885; font-size: 12px; margin-top: 3px; }
.official-task-list { display: grid; gap: 9px; padding: 24px max(32px, 5vw) 40px; }
.official-task-card { align-items: center; background: #fff; border: 1px solid #e8e8e5; border-radius: 10px; color: #393937; display: flex; min-width: 0; padding: 5px 6px 5px 15px; transition: border-color 120ms ease, box-shadow 120ms ease; }
.official-task-card:hover { background: #fafaf8; border-color: #d2d2ce; }
.official-task-card.is-selected { border-color: #969692; box-shadow: 0 0 0 3px #00000008; }
.official-task-card-main { align-items: center; background: transparent; border: 0; color: inherit; cursor: pointer; display: grid; flex: 1; gap: 12px; grid-template-columns: 38px minmax(0, 1fr) auto auto; min-width: 0; padding: 8px 0; text-align: left; }
.official-task-icon { align-items: center; background: #f0f0ed; border-radius: 8px; color: #686865; display: flex; height: 34px; justify-content: center; width: 34px; }
.official-task-copy { min-width: 0; }
.official-task-copy strong,
.official-task-copy small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.official-task-copy strong { font-size: 14px; }
.official-task-copy small { color: #888885; font-size: 12px; margin-top: 3px; }
.official-task-status { background: #eef5ef; border-radius: 999px; color: #46804e; font-size: 11px; padding: 3px 8px; white-space: nowrap; }
.official-task-status.is-running,
.official-task-status.is-pending { background: #fff4de; color: #a97018; }
.official-task-status.is-paused { background: #f0f0ee; color: #7e7e7b; }
.official-task-status.is-failed { background: #fff0f0; color: #b34e4e; }
.official-task-card time { color: #9a9a97; font-size: 12px; white-space: nowrap; }
.official-task-card-actions { display: flex; gap: 3px; margin-left: 11px; }
.official-task-card-actions button { align-items: center; background: transparent; border: 0; border-radius: 7px; color: #686865; cursor: pointer; display: inline-flex; height: 30px; justify-content: center; width: 30px; }
.official-task-card-actions button:hover { background: #ededeb; color: #242422; }
.official-task-card-actions button:disabled { cursor: not-allowed; opacity: .45; }
.official-task-card-actions button:disabled svg { animation: official-spin 900ms linear infinite; }
.official-task-detail { border-bottom: 1px solid var(--official-border); padding: 24px max(32px, 5vw); }
.official-task-detail > header { align-items: flex-start; display: flex; gap: 16px; justify-content: space-between; }
.official-task-detail h2 { font-size: 19px; margin: 3px 0 7px; }
.official-task-detail header p:not(.official-eyebrow) { color: #777774; font-size: 13px; line-height: 1.5; margin: 0; max-width: 720px; }
.official-task-meta { color: #787875; display: flex; flex-wrap: wrap; font-size: 12px; gap: 7px 16px; margin-top: 16px; }
.official-task-detail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 17px; }
.official-task-detail-actions button:disabled svg { animation: official-spin 900ms linear infinite; }
.official-run-list { margin-top: 24px; }
.official-run-list h3 { font-size: 14px; margin: 0 0 10px; }
.official-run-row { align-items: center; background: transparent; border: 0; border-top: 1px solid #eeeeeb; color: #4c4c49; cursor: pointer; display: grid; font-size: 12px; gap: 13px; grid-template-columns: 76px 92px minmax(0, 1fr); padding: 11px 2px; text-align: left; width: 100%; }
.official-run-row:hover { color: #111; }
.official-run-row > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.official-run-status { background: #f0f0ee; border-radius: 999px; color: #777774; justify-self: start; padding: 3px 7px; }
.official-run-status.is-running,
.official-run-status.is-pending { background: #fff4de; color: #a97018; }
.official-run-status.is-succeeded { background: #eef5ef; color: #46804e; }
.official-run-status.is-failed { background: #fff0f0; color: #b34e4e; }
.official-run-empty { align-items: center; color: #858582; display: flex; font-size: 12px; gap: 6px; margin: 10px 0 0; }
.official-run-empty svg { animation: official-spin 900ms linear infinite; }
@keyframes official-spin { to { transform: rotate(360deg); } }
@media (max-width: 760px) {
.official-sidebar { flex-basis: 62px; padding-left: 7px; padding-right: 7px; }
.official-sidebar span,
.official-sidebar p,
.official-sidebar small,
.official-brand { display: none; }
.official-brand-row { justify-content: center; }
.official-collapse-button { display: none; }
.official-nav-item,
.official-recent-chat,
.official-sidebar-footer > button,
.official-user-row { justify-content: center; }
.official-page-header { padding-left: 22px; padding-right: 22px; }
.official-page-header h1 { font-size: 21px; }
.official-agent-grid { grid-template-columns: 1fr; padding-left: 22px; padding-right: 22px; }
.official-toolbar { padding-left: 22px; padding-right: 22px; }
.official-content-page .official-page-header { padding-left: 22px; padding-right: 22px; }
.official-suggestion-grid { grid-template-columns: 1fr; }
.official-chat-welcome { transform: none; }
.official-message-list { padding-left: 0; padding-right: 0; }
.official-outline-button { font-size: 0; gap: 0; padding: 8px; }
.official-composer-shell { padding-left: 14px; padding-right: 14px; }
.official-task-card-main { gap: 8px; grid-template-columns: 38px minmax(0, 1fr) auto; }
.official-task-card time { display: none; }
.official-task-detail { padding-left: 22px; padding-right: 22px; }
.official-run-row { grid-template-columns: 76px minmax(0, 1fr); }
.official-run-row > span:nth-child(2) { display: none; }
}