代码提交
This commit is contained in:
1 parent
75f288b241
commit
56bb4f141d
11 files changed
+1066
-3
No files matched your search
@@ -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后端接口
|
||||
|
||||
@@ -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
|
||||
@@ -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后端接口
|
||||
|
||||
@@ -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后端接口
|
||||
|
||||
@@ -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后端接口
|
||||
|
||||
@@ -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";
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in new issue
Block a user