Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8ad8fda94 | ||
|
|
e602d88695 | ||
|
|
5d6627af50 | ||
|
|
a26fd32eba | ||
|
|
67d12929fc | ||
|
|
5fd2e8302c |
No files matched your search
@@ -69,10 +69,10 @@ offline-backend-20260512/wheelhouse/
|
||||
offline-backend-20260512/backend/.deer-flow/data/*.db
|
||||
offline-backend-20260512/backend/.deer-flow/data/*.db-*
|
||||
offline-backend-20260512/backend/.deer-flow/*
|
||||
offline-backend-20260512/backend/.env
|
||||
offline-backend-20260512/backend/.env.local
|
||||
offline-backend-20260512/backend/.extensions_config.json.lock
|
||||
offline-backend-20260512/start_backend.sh
|
||||
offline-backend-20260512/backend/.env
|
||||
offline-backend-20260512/backend/.env.local
|
||||
offline-backend-20260512/backend/.extensions_config.json.lock
|
||||
offline-backend-20260512/start_backend.sh
|
||||
offline-backend-20260512/stop_backend.sh
|
||||
offline-backend-20260512/check_backend.sh
|
||||
|
||||
@@ -88,34 +88,18 @@ offline-backend-20260512/backend/skill_evolution.json
|
||||
skills/
|
||||
hermes-agent-main/
|
||||
.gitignore
|
||||
offline-backend-20260512/backend/tests/*
|
||||
!offline-backend-20260512/backend/tests/test_large_file_safety.py
|
||||
# Seeder-owned Agent Skills are product source; keep them out of the broad
|
||||
# runtime `skills/` ignore above.
|
||||
!offline-backend-20260512/backend/app/gateway/routers/_*_seed_assets/skills/
|
||||
!offline-backend-20260512/backend/app/gateway/routers/_*_seed_assets/skills/**
|
||||
# Keep startup seed completeness and task-report conversion regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_seed_assets.py
|
||||
!offline-backend-20260512/backend/tests/test_task_report_convert.py
|
||||
!offline-backend-20260512/backend/tests/test_migration_compatibility.py
|
||||
# Keep project-scoped cross-conversation memory regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory.py
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory_integration.py
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory_runtime.py
|
||||
!offline-backend-20260512/backend/tests/test_analysis_project_memory_store.py
|
||||
# Keep answer-level feedback persistence, scoring, resolver, and migration regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_answer_feedback.py
|
||||
# Keep wx common-agent category configuration regressions versioned.
|
||||
offline-backend-20260512/backend/tests/*
|
||||
!offline-backend-20260512/backend/tests/test_large_file_safety.py
|
||||
# Keep answer-level feedback persistence, scoring, resolver, and migration regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_answer_feedback.py
|
||||
# Keep wx common-agent category configuration regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_wx_common_agents.py
|
||||
# Keep the built-in public-document seed regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_gongwen_agent_seed.py
|
||||
!offline-backend-20260512/backend/tests/test_gongwen_documents.py
|
||||
# Keep the openGauss URL/dialect/schema compatibility regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_gauss_database.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_weknora.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_advanced_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_knowledge_live_embedding.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_knowledge_management.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_advanced_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_knowledge_live_embedding.py
|
||||
!offline-backend-20260512/backend/tests/test_health_auth_bypass.py
|
||||
# Keep formal Word OOXML and embedded-font regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_word_export.py
|
||||
@@ -137,28 +121,28 @@ offline-backend-20260512/backend/tests/*
|
||||
# Keep the local LLMWiki vector-index and external API regression coverage versioned.
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_local_index_repository.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_vector_search.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_contract.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_encoder_runtime.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_sync.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_contract.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_encoder_runtime.py
|
||||
!offline-backend-20260512/backend/tests/test_external_llmwiki_search.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_wiki_retrieval.py
|
||||
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
|
||||
# Keep assistant-source snapshot publication atomic across re-import jobs.
|
||||
!offline-backend-20260512/backend/tests/test_assistant_snapshot_pointer_contract.py
|
||||
!offline-backend-20260512/backend/tests/test_public_llmwiki_embed.py
|
||||
# Keep assistant-source snapshot publication atomic across re-import jobs.
|
||||
!offline-backend-20260512/backend/tests/test_assistant_snapshot_pointer_contract.py
|
||||
# Deep-research reliability regression coverage.
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_persistence.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_event_sink.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_executor_reliability.py
|
||||
!offline-backend-20260512/backend/tests/test_deep_research_start_job.py
|
||||
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_title_ranking.py
|
||||
!offline-backend-20260512/backend/tests/test_menu_overrides.py
|
||||
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
|
||||
# Keep database-backed upload-policy, enforcement and bounded-preview regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_upload_policy.py
|
||||
!offline-backend-20260512/backend/tests/test_uploads_policy_enforcement.py
|
||||
!offline-backend-20260512/backend/tests/test_artifact_preview.py
|
||||
# Independent enterprise-research persistence, HTTP and race regressions.
|
||||
!offline-backend-20260512/backend/tests/test_summarization_human_message.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_title_ranking.py
|
||||
!offline-backend-20260512/backend/tests/test_menu_overrides.py
|
||||
!offline-backend-20260512/backend/tests/test_compact_menu_overrides.py
|
||||
# Keep database-backed upload-policy, enforcement and bounded-preview regressions versioned.
|
||||
!offline-backend-20260512/backend/tests/test_upload_policy.py
|
||||
!offline-backend-20260512/backend/tests/test_uploads_policy_enforcement.py
|
||||
!offline-backend-20260512/backend/tests/test_artifact_preview.py
|
||||
# Independent enterprise-research persistence, HTTP and race regressions.
|
||||
!offline-backend-20260512/backend/tests/test_enterprise_research_tasks.py
|
||||
# Position-roundtable per-turn write/present cap (weak-model duplicate artifacts).
|
||||
!offline-backend-20260512/backend/tests/test_position_artifact_cap.py
|
||||
@@ -215,22 +199,22 @@ codegraph.mdc
|
||||
|
||||
docs/aidev-docs
|
||||
docs/backups
|
||||
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||
!frontend-web/src/components/workspace/skills/
|
||||
|
||||
# Built-in analysis mind-map skill is product source, not a per-user runtime skill.
|
||||
!offline-backend-20260512/backend/skills/
|
||||
offline-backend-20260512/backend/skills/*
|
||||
!offline-backend-20260512/backend/skills/public/
|
||||
offline-backend-20260512/backend/skills/public/*
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/SKILL.md
|
||||
# Frontend skills components must be tracked (excluded from the broad skills/ ignore above)
|
||||
!frontend-web/src/components/workspace/skills/
|
||||
|
||||
# Built-in analysis mind-map skill is product source, not a per-user runtime skill.
|
||||
!offline-backend-20260512/backend/skills/
|
||||
offline-backend-20260512/backend/skills/*
|
||||
!offline-backend-20260512/backend/skills/public/
|
||||
offline-backend-20260512/backend/skills/public/*
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/
|
||||
!offline-backend-20260512/backend/skills/public/analysis-mindmap/SKILL.md
|
||||
|
||||
# uv lockfile (not tracked per project preference)
|
||||
offline-backend-20260512/backend/uv.lock
|
||||
|
||||
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_scheduler.py
|
||||
!offline-backend-20260512/backend/tests/test_wiki_package_roundtrip.py
|
||||
!offline-backend-20260512/backend/tests/test_llmwiki_incremental_sync_scheduler.py
|
||||
|
||||
# Local-only scratch directory (not committed)
|
||||
0622/
|
||||
|
||||
@@ -100,8 +100,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route, `appendTaskId`/`idParam` become numeric `queryParams` (task id, or action id when `idKind:"action"` — same selector as url buttons, xdfx only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url **or host** button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. The old hardcoded jump buttons were migrated out: `TaskFxWorkspace` no longer hardcodes 切换到三情分析, `TaskActionsPanel` keeps only the non-jump 清除记录, and `TaskPurposePanel` dropped its 3q详情 button (now configurable) **and defaults the 目的树 table to collapsed** (chevron toggle). Helpers `core/auth/task-buttons.ts` (`TaskButton`, `defaultTaskButtons`, `resolveTaskButtons`, `buttonsForBusiness`, `runTaskButton`), client+hooks `strategy-components/api/task-buttons.ts`. Backend: `/api/task-buttons` (global/shared; open read, admin replace-all), store `deerflow.persistence.task_buttons` (table `task_buttons`, auto-created by `create_all`), wired `app.state.task_button_store`. Tests: `tests/test_task_buttons.py`.
|
||||
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
|
||||
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
|
||||
- **UI**: Radix UI primitives + TailwindCSS v3 + shadcn-style components in `src/components/ui/`
|
||||
- **麒麟 Chromium ~80 兼容 (Kylin legacy Chrome)**: Tailwind 自 v4 降级到 v3(v4 的 `@layer` 需要 Chrome 99+,麒麟系统自带 Chromium 80 多整站裸奔)。要点:① `tailwind.config.ts` —— 语义色走六套主题(`:root/.dark/.lightblue/.lightgreen/.iceblue/.lightyellow`)维护的 `--legacy-*-rgb` 通道变量,透明度修饰符(`bg-primary/10`)用 `rgb(var()/<alpha-value>)` 原生支持;border/input/sidebar-border/ring 用 `--*-alpha` 变量还原 v4 color-mix 叠乘语义;`xs/sm` 刻度(shadow/blur/rounded)与裸 `ring`(1px+currentColor)按 v4 值覆盖,类名零改动;streamdown 运行时拼类名的白名单在 `safelist`;v4 独有工具类(outline-hidden/text-shadow-sm/field-sizing-*)以插件手写兜底。② `darkMode: "class"` 输出 `:is(.dark *)`,由 `scripts/postcss-dark-descendant.mjs` 在 postcss 阶段改写为 `.dark X` 后代形式(v3.4 的 `["selector", ...]` 堆叠变体会生成非法选择器、无 darkMode 配置会退化成媒体查询,均不可用)。③ 手写 CSS 禁用 `:is()`(复杂参数 lightningcss 无法安全降级,globals/roundtable/intent 已全部展开为逗号选择器);`:has()`(105+)按渐进增强放行,唯一功能性依赖(input-group 焦点/校验环)在 globals.css 有 `@supports not selector(:has(*))` 回退。④ vite `build.target` + lightningcss targets = chrome80(转译 `:is()`/逻辑属性/媒体 range 语法、厂商前缀);`src/polyfills.ts`(core-js 精选 API)在 main.tsx 首位引入。⑤ 构建后跑 `pnpm check:legacy-css` 防回归(产物不得含 @layer/@property/:is()/未守护 color-mix)。
|
||||
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||
- **Shims**: `src/shims/` provides `next/link` and `next/navigation` polyfills (original code was Next.js)
|
||||
- **Login URL params** (`LoginPage.tsx`, frontend-only — the backend never sees these): besides the auth params (`ticket`/`userId`/`env`/`theme`/`yUserId`/`token`/`access_token`/`authToken`/`password`), the page supports a **task deep-link** via `?taskId=&goPath=`. After login it calls the consumer API `GET {VITE_AIPI_CONSUMER_URL}/taskAnalyseSearch/cop-task-detail?taskId=` (header `Authorization: Bearer admin`, auto-injected by `utils/request.ts`), stashes the full `data` (`id/overview/taskDirection/taskName/taskContent`) into `sessionStorage["login.copTaskDetail"]`, then routes by `goPath`: base codes `3qfx`/`rwfx`/`xdfx` → that business's configured **single agent** Q&A inside a **任务工作区** (resolved via `/api/business-mapping`; `resolveTaskDeeplinkTarget` calls `persistTaskContext({taskId, agentId, openingText})` and navigates to the `…/chats` **index**, which opens the latest task conversation or 新建 — a 1:N task↔对话 workspace, NOT `pendingAgentMessage`); the `-A` suffix (`3qfx-A`/…) → **multi-agent roundtable** with `roundtablePrefill.businessCode` so the page auto-selects the configured 业务链条. **xdfx's deep-link `?taskId=` is actually an 行动id, not a 任务id** — `resolveDeeplinkTaskId(goPath, rawId)` (called in `LoginPage` before everything) detects `xdfx`/`xdfx-A` and resolves it via `GET {action_detail_url}/{actionId}` → `data.taskId` (`fetchTaskIdFromAction`; `action_test`+`mock.action_detail` for offline testing); the resolved 任务id then drives `cop-task-detail`, the purpose tree, and the 1:N binding (other businesses pass the id through unchanged). **xdfx additionally partitions its chat records by 行动id (action_id), NOT by task_id**: `resolveTaskDeeplinkTarget` sets `partitionByAction:true` + keeps the raw action_id as `TaskContext.actionId`, so new threads are tagged `metadata.{taskId,actionId}` (taskId kept only to preserve the backend「任务深链对话全局共享」semantics, which keys off `metadata.taskId`) and the conversation list filters by `actionId` (+`agent_id`) — different actions of the same task get separate lists. xdfx's first-question opening text also appends `行动id(action_id)为…` (see `buildOpeningText` / `taskThreadMetadata`). goPath→businessCode: `3qfx`→`3Q`, `rwfx`→`6BF`, `xdfx`→`7BF`. The roundtable target also threads the URL `taskId` into `roundtablePrefill.taskId` (`resolveTaskDeeplinkTarget(goPath, detail, taskId)`), which turns on two taskId-scoped behaviors: (1) **separate, unpartitioned per-task storage** — taskId 会商聊天记录 live in their **own** store/table (`roundtable_task_drafts`), keyed **only by `task_id`** (one taskId = many records, shared across users, **no user 分权** at all; `created_by` is audit-only), kept entirely out of the per-user personal store (`roundtable_drafts`, now strictly per-user) so the two never mix. Backend router `/api/roundtable-task-drafts` (`GET /by-task/{id}` + `/by-task/{id}/latest`, `POST`, `GET/PUT/DELETE /{id}` — all user-agnostic); frontend client `roundtable-planning/api/task-drafts.ts`, and `useDraftPersistence`/`useRoundtableDrafts` route **all** CRUD to it when a `taskId` is present (else the personal `drafts.ts`). The matching **background jobs** are shared by task too: `roundtable_jobs.task_id` marks a task job; reads/dedup/stream/resume/cancel pass `?task_id=` so any user opening the task sees the same running 研讨 (progress/flowchart), and the executor writes the final report back to the task-draft store. Existing rows are moved over by `scripts/migrate_roundtable_task_drafts.py` (migration `20260618_01`). The history dropdown filters to the task and 新建 keeps the binding; (2) **Step-3 auto structured output** — after the `roundtable-summary` agent writes the Markdown report it automatically runs a second round emitting a `flow-json` flowchart (`useStep3Summary` `autoFlow`+`chainLevels`), whose hierarchy follows the business chain's stage goals (任务 → 目的 → 行为体 → …; coordinatorPrompt-specified hierarchy wins) and whose nodes never carry 阶段编号/阶段名 (the prior bug). **Both targets render embedded under the 任务管理 layout** — nested routes under `task-management/<business-path>/{roundtable,agent/:agent_id/chats}` in `StrategyRoutes.tsx` (`<business-path>` = `plan-task-sentiment` / `plan-task/plan-task-fx` / `plan-task/plan-task-action`, matching the COH中任务 menu paths in `sidebar-menu.ts`) so the global sidebar keeps 任务管理 → COH中任务 → the matching business highlighted. **All three single-agent businesses share `TaskFxWorkspace`** (`task-components/TaskFxWorkspace.tsx`): left = a「当前任务」card (`readCopTaskDetail()` 任务名称 + 简介, desensitized, 留存 above 新建对话) + per-task 1:N conversation list (`useTaskThreads(taskId, agentId, actionId?)` — filtered by **both** `metadata.taskId` AND `metadata.agent_id` so the three businesses' conversations staLine truncated
|
||||
- **agentfx 任务深链(新系统 iframe 嵌入的智能体问答,3qfx 单智能体 lite 模式的独立复制)**: goPath=`agentfx`(`${loginBase}?password=…&taskId=…&goPath=agentfx&embed=1&hideJump=1&theme=dark-blue`)。与 3qfx 的差异:①任务详情不走外部 consumer,改调**本系统 Gateway** `GET {backend}/taskAnalyseSearch/cop-task-detail?taskId=`(`apiFetch` 带刚登录的 DeerFlow 态;任务数据来自 TaskCOP 任务表,与任务列表/报告导入同一份);②**固定使用内置智能体** `agentfx-analyst`(不走业务映射);③路由 `task-management/plan-task-agentfx/agent/:agent_id/chats`,页面为独立复制的 `AgentFxWorkspace.tsx`(`AgentFxWorkspace`/`AgentFxIndexResolver`/`AgentFxChat` —— 左「当前任务」卡 + 1:N 对话列表 + 新建对话,`AgentChatPage taskBind` 自动发开场白;无 TaskJumpBar/清除记录/闸门/xdfx 行动卡)。模块 `core/auth/agentfx-deeplink.ts`(`isAgentfxGoPath`/`fetchAgentfxTaskDetail`/`resolveAgentfxDeeplinkTarget`);任务详情整包仍存共享键 `login.copTaskDetail`(按标签页隔离),`AgentChatPage` 的 taskBind 自恢复对 agentfx 生效。`LoginPage` 在既有深链分支前用 `isAgentfxGoPath` 分流。**内置「任务研判报告助手」**:后端 seeder `app/gateway/routers/_agentfx_seed.py` + `_agentfx_seed_assets/`(config.yaml + SOUL.md,目录存在不覆盖;app.py lifespan 在 `_sync_legacy_agents` 之前调用,自动进 agents 表 user_id IS NULL)——SOUL 规定工作流「搜集信息(`web_search`,**首步不可跳过**)→ 每次检索 JSON 原样落盘 `/mnt/user-data/workspace/hits/qN.json` → **一条命令** `task-report-build`(group `all`)技能函数转换成 14 类 `task-reports.json`(模型禁止手写该 JSON;分页技能 `task-report-{enemy,our,env,judge}` 仅用于事后补跑单页;五者都是共享引擎 `task-report-lib/convert_lib.py` 的瘦 CLI,`resolve_hit_files` 对虚拟绝对路径读空的情况自动回退到相对写法并在 `warnings` 里列出尝试过的路径,免得智能体自己写脚本排查)→ `write_file` 生成 `/mnt/user-data/outputs/report.md` → 聊天总结 → 引导入库」,硬性约束:categoryType 逐字用 14 个全称(禁缩写自造如「DQ关键事件」)、JSON 直接写汉字禁 `\uXXXX` 转义、每类必须有实质内容不许空数组/纯占位、文件一律用 `/mnt/user-data/...` 虚拟路径(防宿主路径泄漏进 tool args)。**检索依赖 `config.yaml tools.web_search`(DDG,enabled 已开;内网需换 configurable_search 时改回并填 endpoint)**。任务工作区输入框在 `taskCtxOn` 时隐藏「写作/记忆/参考文献」(InputBox `hideContextTools` prop)与「知识空间」(不传 knowledgeSelector);消息流里 write_file 文件卡片只显示文件名(`artifactDisplayName` 收敛宿主/虚拟路径)。**入库两条路径**:(a) **前端协助卡** —— `AgentChatPage`(taskBind 且非 taskGate)检测线程 artifacts 出现 `task-reports.json`(按文件名后缀匹配,兼容本地沙箱记录的宿主路径)且流式结束时,向 displayThread 尾部注入 `additional_kwargs.task_report_import_approval` 虚拟 ai 消息(`core/messages/utils.ts` 分组 `assistant:task-report-import` + `MessageList` 的 `taskReportImportSlot` 插槽渲染 `TaskReportImportCard`),点「开始入库」前端校验(`strategy-components/lib/agentfx-report-schema.ts`,14 类 + contentJson 合法 JSON + **\u 转义归一化还原为汉字**,与技能脚本同规则)后 `importTaskReport`(`strategy-components/api/task-reports.ts`)POST 导入接口整体替换;任务已有详情**不抑制**卡片(入库即整体替换,卡片文案说明覆盖语义),导入成功按 thread 记 sessionStorage(刷新不重复询问)、「暂不入库」为会话内记忆;(b) **入库技能** `task-report-import`(seeder 一并复制到 `skills/public/`,public 类技能 extensions_config 缺省即启用)—— 用户聊天里说「入库」时智能体按 SKILL.md 用 bash 跑 `scripts/task_report_import.py`(纯标准库:同样的校验规则 + 转义归一化;**默认关闭 SSL 证书校验** `ssl._create_unverified_context` + 忽略代理,适配内网自签证书;鉴权 `--token`/env `AGENTFX_IMPORT_TOKEN`→`DEERFLOW_AUTH_TOKEN`→`TOOLS_TOKEN` 优先,缺省经公共端点 `POST /api/parallel-agents/auth/token` 以 `agentfx-import` 用户现签 token——内网既有鉴权旁路;gateway 地址 env `AGENTFX_GATEWAY_URL` 或 `http://127.0.0.1:{DEER_FLOW_GATEWAY_PORT}`);`--strict` 可要求 14 类齐全,默认允许部分导入并在 stdout JSON 的 `missing_categories` 里列缺。入库成功后任务 TaskCOP 状态自动置 25(已完成分析)。**入库成功通知宿主**(iframe 嵌入场景):两条路径成功后都会 `window.top.postMessage({source: "magent-web", type: "Line truncated
|
||||
|
||||
@@ -82,7 +82,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **Routing**: React Router v7; top-level routes in `src/pages/PageRoutes.tsx`, workspace sub-routes in `src/pages/WorkspaceRoutes.tsx`
|
||||
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
|
||||
- **普通问答地图可视化**: shared `MarkdownContent` recognizes only dedicated `map-json` fences and otherwise preserves the legacy Markdown/Streamdown path byte-for-byte. `components/workspace/visualizations/` validates the bounded map contract with Zod, lazy-loads MapLibre, and provides loading/error/text fallbacks so malformed or unreachable maps never blank the message. The lead-agent prompt emits `map-json` only when the user explicitly asks for a map; flowcharts and other graphics continue through the existing Markdown/Mermaid path. The raster tile template comes from the single post-build setting `runtime-config.js` → `VITE_MAP_TILE_URL` (Google by default); replace it with the intranet `{z}/{x}/{y}` endpoint for disconnected deployments.
|
||||
- **HTML 成品离线绘图运行时**: 新生成的 `.html` 源码直接引用 Gateway `/api/public/html-runtime/` 下的浏览器版 `g6.min.js`、`echarts.min.js`、`leaflet.js/css` 和 `deerflow-runtime.js`;文件随后台源码存放于 `vendor/html-runtime/`,不再依赖前端 Vite/node_modules 产物或公网 CDN。`config.yaml -> html_runtime.base_url` 是写入新 HTML 的浏览器可访问绝对地址,`html_runtime.map_tile_url` 是初始化脚本读取的瓦片模板。用户上传的栅格图若要作为页面背景,使用同一 Gateway 源下的签名地址 `/api/public/html-images/{token}` 写入 `background-image`。流程图使用 `data-df-flow` + G6(不用 Mermaid),图表使用 `data-df-chart`,地图使用 Leaflet `data-df-map`。普通问答保持独立:消息列表 `map-json` 继续由前端原逻辑渲染,普通流程图/图表仍走 Markdown/Mermaid。
|
||||
- **HTML 成品离线绘图运行时**: 新生成的 `.html` 源码直接引用 Gateway `/api/public/html-runtime/` 下的浏览器版 `g6.min.js`、`echarts.min.js`、`leaflet.js/css` 和 `deerflow-runtime.js`;文件随后台源码存放于 `vendor/html-runtime/`,不再依赖前端 Vite/node_modules 产物或公网 CDN。`config.yaml -> html_runtime.base_url` 是写入新 HTML 的浏览器可访问绝对地址,`html_runtime.map_tile_url` 是初始化脚本读取的瓦片模板。流程图使用 `data-df-flow` + G6(不用 Mermaid),图表使用 `data-df-chart`,地图使用 Leaflet `data-df-map`。普通问答保持独立:消息列表 `map-json` 继续由前端原逻辑渲染,普通流程图/图表仍走 Markdown/Mermaid。
|
||||
- **定时任务「JSON → HTML 模板填充」(模板数据转换器)**: a third scheduled-task execution mode. In the create/edit dialog (`ScheduledTasksPage.tsx`), selecting the built-in agent **「模板数据转换器」** (`execution_agent_name === "template-json-builder"`, `TEMPLATE_BUILDER_AGENT_ID`) reveals a 「HTML模板」 select (`useHtmlTemplates` → `GET /api/scheduled-tasks/html-templates`) + a **.json 上传** button (reads the file's text into the prompt box) and **hides** the 必须生成 HTML/Markdown switches (this mode always outputs HTML). The chosen template persists as `execution_context.template_config.template_id` (`TemplateFillConfig` in `core/scheduled-tasks/types.ts`). At run time the backend converts the source JSON (the task prompt) into that template's `DATA` structure, validates (retry-on-fail), fills the template, and stores an HTML page previewable via the existing run-detail HTML preview. The source can be **pure JSON, plain text, or text-with-embedded-JSON** — the backend pre-extracts any embedded JSON and the agent's `SOUL.md` maps it (or the plain text's semantics) to the template's schema skeleton. **Chat-page debug entry (only this agent's `AgentChatPage`)**: when `agent_id === "template-json-builder"`, the chat header shows a 「页面模板」 select + a 「渲染预览」 button — after a normal Q&A run, the button takes the **latest AI reply**, POSTs it to `POST /api/scheduled-tasks/template-fill/render` (`renderTemplateFill`), and pops a dialog rendering the filled HTML in a `sandbox="allow-scripts"` iframe plus the structure-validation status/errors/gaps — for debugging the agent's conversion effect without creating a scheduled task. Gated strictly on the agent id, so other agents' chat pages are untouched. See the backend CLAUDE.md「Template-fill mode」for the engine (`deerflow/runtime/scheduler/template_fill.py`), agent seed, dispatch, and the render API.
|
||||
- **AI 写作·对话版(对话驱动写作台)**: a **parallel new entry** beside the classic AI-writing page (classic page/routes untouched) — route `/page/canvas/ai-writing-chat` (`OpenCanvasRoutes.tsx`), sidebar 「问答管理 → 课题研究(对话版)」. Page `open-canvas/pages/AIWritingChatPage.tsx` fully **reuses the engine** (`AIWritingContext` state machine + LangGraph `useStream`, `AIWritingHistoryProvider`, right-side `AIWritingDraftPanel`) but replaces the presentation: left = `components/ai-writing-chat/WritingChatPanel.tsx` (timeline + **button-only intervention cards** `ChatInterventionCards.tsx` — the five pause cards copied from `InterventionCard.tsx` with all inline textareas removed, confirm/action buttons kept — plus a bottom **unified smart composer**). Free text goes through two-level intent resolution (`open-canvas/api/ai-writing-intent.ts`): deterministic short-phrase fast path (确认/继续/重新搜索/重写大纲…; high-cost actions never fast-pathed) then `POST /api/ai-writing/sessions/{id}/intent` — the backend reads the graph checkpoint (authoritative pause point from the pending interrupt) and routes via `deerflow/agents/ai_writing/intent_router.py` (LLM + strict per-pause action whitelist aligned to the graph's real resume actions, confidence gates: finalize/force_finalize/覆盖式 re_search need ≥0.85 else clarify, payload key-whitelist cleaning). Results map to `submitIntervention`/control (暂停/继续/后台挂起)/QA bypass (`answer` rendered as a chat bubble, never touches graph state)/clarify (amber follow-up bubble with multi-turn context). **Two-state layout**: idle (未开始写作) renders the **classic setup experience unchanged** — `WritingSetupChat` + collapsible `AIWritingPanel` form under `WritingFormProvider` (same as `AIWritingPage`'s idle branch); once writing starts (or history view opens) it switches to the chat workbench. **Unified chronological feed** (`ChatFlowTimeline.tsx`): intent-layer chat bubbles are interleaved INTO the progress-event timeline by anchor (= `progressEvents.length` at message creation), so events triggered by a user message render **below** it (Google-AI-Studio-style single stream, not timeline-above/chat-below); composer-driven interventions suppress the duplicate 「我的要求」card (`suppressedRequirementIndices`). **素材不足自动续跑** only at `material_confirm` (the only pause the graph supports re-search from): needResearch caches the payload as a pending intent, submits `re_search + userQuery`, and auto-submits the pending intent when the new material-confirm pause arrives (cancellable pill). Stage chips + per-stage placeholder + 「当前交互对象」status badge. Backend tests `tests/test_ai_writing_intent.py`. Plan doc: `frontend-web/docs/ai-writing-对话驱动写作台-实现方案.md`.
|
||||
- **策略优选**: route `/page/strategy/qa/strategy-selection`, sidebar 「问答管理 → 策略优选」. The standalone `src/strategy-selection/` workbench has two horizontal split stages: Step 1 keeps intent-understanding conversation cards on the left and an editable task draft plus angle selection on the right; Step 2 keeps multiplexed parallel strategy cards and automatic judging on the left while the right stays dedicated to the selected-strategy basket and final Markdown synthesis. It consumes the existing `POST /api/parallel-agents/run/stream` protocol, isolates output by stable agent id, supports expanded per-agent dialogs, user selection, Markdown editing/copy/download, and shared server-side DOCX export. A clearly marked neutral Taiwan-Strait risk-management demo is loaded by default for offline UI acceptance. Administrators can edit the SOUL and skills of the nine built-in functional agents through the page dialog; the backend seeds `strategy-selection-{intent,angle-*,judge,solution}` only when their directories are missing, preserving later admin changes. Pure parsing/SSE tests live in `src/strategy-selection/lib.test.ts`; backend seed tests live in `tests/test_strategy_selection_seed.py`.
|
||||
@@ -93,9 +93,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **技能地址管理 (Skill Address Management, admin-only)**: 扫描运行中的技能(`skills/{public,custom}` 下所有 `.md`/`.py`)里的 **http(s) URL 与 IP/IP:端口**,按唯一地址聚合全部出现位置(跨 .md/.py),支持**批量替换**(如把 UAT 地址一次性改成生产)。主体抽成可复用组件 `SkillAddressesPanel`(`pages/SkillAddressesPage.tsx` 导出,默认导出 `SkillAddressesPage` 仍保留旧路由 `/page/strategy/admin/skill-addresses` 作直链回退),**作为「技能地址管理」标签页嵌入管理员「技能管理」页 `pages/AdminSkillsPage.tsx`**(该页加 Tabs:「技能列表」+「技能地址管理」;入口在「设置和更多」下拉的「技能管理」`workspace-nav-menu.tsx` → `/page/workspace/admin/skills`,不再单列于 `components/page-sidebar.tsx` 左下角设置下拉):打开自动扫描并带**进度条**(扫描逐技能 / 应用逐文件 X/N,应对 150+ 技能),URL/IP 类型过滤 + 搜索 + 「片段批量套用」(如所有 `uat.4.cn` → `prod.4.cn`)、每地址「替换为」输入框(可展开看 `file:line` 片段)、内联**预览**、**应用**、**多关键词检索**(空格分隔全部命中),以及**修改记录**面板——每次应用生成一条记录(显示时间/改了哪些地址 from→to/文件数/处数),可**针对单次记录回退**(二次确认)。client + NDJSON 流式解析在 `strategy-components/api/skill-addresses.ts`。后端 `/api/skill-addresses`(**仅管理员**):所有文件 IO 走 `asyncio.to_thread` 不阻塞事件循环;`GET /scan`(+`/scan/stream`)、`POST /preview`、`POST /apply`(+`/apply/stream`)、`GET /backups`、`POST /rollback/{id}`;**子串安全替换**(基于 token span,不会误伤子串)、`expected_total` 乐观锁(409)、写前自动磁盘备份到 `{skills_root}/.address-edits/`(同时写一份 `{id}.manifest.json` 记录本次替换明细,供修改记录展示)、应用后刷新技能系统提示缓存。纯引擎 `deerflow/skills/address_scan.py`,router `app/gateway/routers/skill_addresses.py`,tests `tests/test_skill_addresses.py`。范围仅运行中技能,不动 `deploy/minimal/skills/`。
|
||||
- **按钮管理 (Task Button Management, admin-only)**: configures the **可配置跳转按钮** rendered in the left task panel of the task deep-link workspaces (rwfx / 3qfx / xdfx, see `TaskFxWorkspace`). Each button = `{business, label, linkType, target, appendTaskId, idParam, idKind, openMode, loginParams, appendAuth, authTokenParam, authNameParam, enabled, sortOrder}`; `linkType` is `url` (外链 `window.open`, optional append of `?{idParam}={id}` — **`idParam` is the editable query-param name**, e.g. `id`/`task_id`), `business` (内部切换到另一业务, reuses `resolveTaskDeeplinkTarget`, always task id), or `purpose` (弹出当前任务的「任务目的」表格 via the shared `TaskPurposeDialog`, no target — configurable for any page incl. xdfx). **`host`(宿主路由)** asks the embedding parent to navigate: `runTaskButton` sends `window.parent.postMessage({ type:'HOST_NAVIGATE', path, queryParams:{ [idParam]: Number(taskId) } }, hostOrigin)` — `target` is the host route (e.g. `/task/item-aiceh/step-analysis-audience`), `appendTaskId`/`idParam` become numeric `queryParams` (task id only), origin from `runtime-config.js` `VITE_HOST_NAVIGATE_ORIGIN` (else ancestorOrigins/referrer; never `*`). Independent-tab clicks toast and do nothing. **`url` buttons support two independent, coexisting login-append mechanisms**: (1) **`loginParams` (追加登录参数, like 应用管理/Light App Center)** — a list of `{key, source, customValue}` resolved at click time: `runTaskButton` reads `userInfo` (via `readUserInfo` from `strategy-components/lib/light-app`, embed-isolated through `authStorage`) and appends `?{key}={value}` for each (`source` = a userInfo field such as `accessToken`/`yUserId`, or `other` → literal `customValue`); empty list = nothing appended. Reuses `LOGIN_SOURCE_OPTIONS` from light-app; stored as JSON column `task_buttons.login_params_json` (`PortableJSON`, migration `20260625_01`). (2) **`appendAuth` (追加登录态)** — appends `?{authTokenParam}={token1}&{authNameParam}={username}` (default param names `token`/`username`) so the跳转目标外部系统 can identify the current user — e.g. `…/action/sentiment?token=<JWT>&username=lizhaoxing`. **Gated on token login**: `token1` = the `?authToken=` used by token-exchange login; `username` = the raw login name the backend resolved from `getTokenInfo`. Both are stashed at login by `persistLoginCredentials` into `login.token1`/`login.username` (`core/auth/login-credentials.ts`, scoped via [[scoped-storage]] so嵌入/独立会话隔离 + 随独立会话迁移). Only token login writes them → a non-token-login session has no `token1`, so `runTaskButton` silently skips the auth append (the jump still works, just without those two params). The frontend reads `username` straight from the new `UsernameLoginResponse.username` field (the stored email local-part is lossy — lowercased/normalized — so not used), **not** by decoding the JWT. Stored as columns `task_buttons.{append_auth, auth_token_param, auth_name_param}` (migration `20260625_02`). The admin page renders both editors for `url` buttons (loginParams 虚线框 + 追加登录态 switch). **`idKind` (`task`/`action`) only matters for xdfx**: its deep-link `?taskId=` is an 行动id and the 任务id is resolved at login (`fetchTaskIdFromAction`) — `TaskContext` now also stashes the raw `actionId`, so a url button can append either (default `task`). Page `TaskButtonsPage.tsx` (route `/page/strategy/admin/task-buttons`, **admin-gated**, entry「按钮管理」in the bottom-left settings dropdown of `components/page-sidebar.tsx`, both full + collapsed modes) has per-business tabs, edits name/type/target/启停/排序/增删, saves the **whole set atomically** (replace-all), plus 「导入默认」 to merge code-seed defaults. **Seed/fallback** (like 敏感词管理): only when the whole table is empty (fresh deploy, never saved) do the workspace + page fall back to `defaultTaskButtons(cfg)` — rwfx's url buttons (编辑/上一步/下一步/3q详情) built from the **live** `/api/public/config/task-deeplink` plus cross-business switch buttons for **all three** businesses (each switches to the other two — rwfx→三情/行动, 3qfx→任务/行动, xdfx→任务/三情). Once saved, the store is the sole source of truth. **Bottom 快捷跳转 bar groups by prev/next**: `TaskJumpBar` renders only「上一步/下一步」-type buttons in the bottom bar — a `prev` group (id 含 `prev` 或文案「上一步」, 描边次要样式 + ←图标) rendered **左侧**, then a `next` group (id 含 `next` 或文案「下一步」, 实心主色) on the **右侧** (container `justify-end`); everything else goes to the right-edge floating menu. The default 上一步 button (`rwfx-prev`, `url`, target = `DASHBOARD_AGENT_ROUTE` `/page/strategy/dashboard-agent`, `appendTaskId` + `idParam=taskId` → `?taskId=任务id`, `openMode=self`) 默认指向**应用内**「大屏绘制」Line truncated
|
||||
- **实时并发监控 (Concurrency / System-Pressure Monitor, admin-only)**: a live panel mounted at the **top of the admin 排行榜 page** (`components/workspace/admin/concurrency-monitor.tsx` `ConcurrencyMonitorPanel`, rendered in `pages/AdminUserLeaderboardPage.tsx`) showing **当前并发的大模型调用数 + 列表(用户/智能体/类型/已运行时长)**, each row with a 「停止」button (and a 「全部停止」) that **truly aborts** that run's in-flight model stream via `RunManager.cancel` (`POST /api/admin/active-runs/{run_id}/cancel`) — to relieve pressure when 使用人数过多. Two line charts (minute/hour toggle): **并发量折线图** (peak/avg, `GET /api/admin/active-runs/concurrency`, fed by a lightweight background sampler into `concurrency_samples`) and **大模型出字速度折线图** (tokens/sec, `GET /api/admin/active-runs/token-speed`, aggregated from `llm_call_metrics`) so a dip pinpoints where the bottleneck is. A **「后台采样」Switch** (`system_settings.json → concurrency_monitor.enabled`, **default off / opt-in**, `GET|PUT /api/admin/active-runs/settings`) turns the background sampling on/off **at runtime, no restart** (the live concurrency count + 「停止」 work even while sampling is off — only the 并发量 history chart needs it on); everything is designed to **never impact production** (one tiny INSERT/15s, retention-purged, best-effort). Client/hooks: `core/admin/active-runs.ts` + `core/admin/hooks.ts` (`useActiveRuns`/`useConcurrencySeries`/`useTokenSpeedSeries`/`useCancel*`/`useMonitorSettings`). See the backend CLAUDE.md「Admin Concurrency Monitor」.
|
||||
- **会话冲突与模型工具流恢复**: when a user submits a new question while the previous turn is still streaming, the backend's `RunManager.create_or_reject` rejects with HTTP 409 (`services.py` returns the Chinese detail「当前对话还在生成回答…」). `core/threads/hooks.ts` detects the conflict and routes it through `confirmOrRejoin`. Run-creation SSE responses expose `Content-Location` plus the SDK `Location` reconnect endpoint; `core/api/api-client.ts` records the last consumed event id, so an in-page rejoin resumes after that cursor instead of replaying tool arguments from `-1`. `write_file` argument updates carried by `messages-tuple` count as business activity. A known active run is probed only after 90 seconds without a business frame; the watchdog hides `lg:stream`, stops only the old browser reader, and rejoins without cancelling the backend run. Clean EOF, transport errors, 409 and watchdog recovery share one single-flight Promise to prevent duplicate joins/finalization. The backend `PatchedChatOpenAI` separately normalizes confirmed cumulative argument snapshots and bounded compatible response shapes while preserving standard deltas; its content-free diagnostics never log prompts, arguments or file bodies.
|
||||
- **React #185 与流式刷新保护**: every direct LangGraph `useStream` surface (main/agent chat, AI writing, Canvas and Studio notebook) uses shared `STREAM_RENDER_BATCHING = true`. The installed SDK implements numeric `throttle: 16` as a trailing debounce, which can indefinitely postpone UI updates during a continuous fast stream; `true` retains asynchronous next-task batching without requiring 16ms silence. `stream-error-classification.ts` detects `Maximum update depth exceeded` / production `#185` through bounded, cycle-safe wrappers. Main chat filters the SDK's stored error and duplicate submit rejection; `react-ui-error.ts` gives each send at most one automatic UI recovery, then pauses rejoin without cancelling the server run. Token activity must not reset that budget. The top-level `ReactUiErrorBoundary` catches only this render/commit error, shows a static friendly reload panel, and never automatically remounts the failing tree. Other errors are not swallowed. Shared toasts are deduplicated for 30 seconds; diagnostics retain error type, recovery phase, thread/run identifiers and component stack but never message/tool content. Real SDK SSE replay covers partial reasoning and write_file Markdown before generation ends; the browser boundary check triggers a real production React layout-effect loop and requires manual reload to recover. These safeguards do not prove the unknown production component loop is fixed.
|
||||
- **流式游标作用域**: resumable cursors belong to the current SDK message accumulator, not a module-global map. `createStreamAPIClient` isolates same-reader recovery; newly mounted chats and real thread switches must replay the retained prefix instead of borrowing another reader's consumed position. Clearing replaces the cursor map so late old-reader events cannot pollute the new scope. The shared `getAPIClient` does not rewrite replay-from-start cursors. `stream-cursor-replay.test.ts` uses the real SDK to protect against both duplicate tool arguments on reconnect and missing prefixes on reopening.
|
||||
- **会话冲突 409 中文化**: when a user submits a new question while the previous turn is still streaming, the backend's `RunManager.create_or_reject` rejects with HTTP 409 (`services.py` now returns a **Chinese** detail「当前对话还在生成回答…」). `core/threads/hooks.ts` `onError` detects the conflict (`isActiveRunConflictError`: 409 / "already has an active run" / the Chinese text). A premature SSE close (SDK treats it as success and restores the send button while the backend run is still occupying the thread) is recovered by `confirmOrRejoin`: hold the stop button, `joinStream` the live run, and on 409 auto-reconnect instead of a fake AI bubble. `sse_consumer` no longer ends a resumable (`on_disconnect=continue`) stream on a false-positive `is_disconnected()`.
|
||||
- **State**: TanStack Query for server state, context providers (`SubtasksProvider`, `ArtifactsProvider`, `PromptInputProvider`) scoped to `WorkspaceRoutes`
|
||||
- **LLM streaming**: `@langchain/langgraph-sdk` streams messages from the backend via SSE
|
||||
- **UI**: Radix UI primitives + TailwindCSS v4 + shadcn-style components in `src/components/ui/`
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
# 技能并发与流式修复:配置、前端包与验收说明
|
||||
|
||||
日期:2026-09-27。
|
||||
|
||||
本说明对应本次提交的修改,包含模型流式兼容与恢复、React #185 错误处理和已完成的最小技能示例,
|
||||
以及可选的技能视图复用、版本缓存、模型计时日志队列和 Gateway 线程池扩容。
|
||||
部署前通过 `git log -1` 核对代码提交;仅修改配置或替换旧包不能确认已部署本次修复。
|
||||
|
||||
## 1. 先确认需要更新什么
|
||||
|
||||
- **后台代码也要更新**:四个新开关需要新版后台读取。只换前端 dist 或只把字段写进旧版 config.yaml 不会实现后台优化。
|
||||
- **前端**:使用本说明末尾的 ZIP,里面是完整 `dist/`,不是源码或 node_modules。
|
||||
- **默认不启用优化**:仓库两份 config.yaml 的四个新开关均为 `false`。只有主动改为 `true` 才启用对应项。
|
||||
- 开启线程池开关后可将上限调整为 100,但不提高模型服务商配额、不启用多 worker。
|
||||
|
||||
## 2. .env 需要修改哪些项
|
||||
|
||||
后台启动所使用的 `.env` 保持或修改为:
|
||||
|
||||
```dotenv
|
||||
# Gateway 进程数,不是线程池大小;当前内存流/运行管理器必须单进程。
|
||||
GATEWAY_WORKERS=1
|
||||
|
||||
# 默认就是 0;显式写 0 可防止误用不安全的多进程覆盖开关。
|
||||
DEERFLOW_ALLOW_UNSAFE_MULTIWORKER=0
|
||||
|
||||
# Gateway asyncio 线程池上限;还必须打开下一节的 asyncio_thread_pool_enabled。
|
||||
DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE=100
|
||||
```
|
||||
|
||||
适用于已更新的 `scripts/start_backend-online.sh` 和 `offline-backend-20260512/start_backend.sh`。
|
||||
若通过自定义服务直接运行 Uvicorn,也应确认实际命令是单 worker;仅修改 .env 不会改变硬编码的启动命令。
|
||||
启动环境中已导出的同名变量可能覆盖 .env,需一并核对。
|
||||
|
||||
前面三个技能/日志优化没有独立环境变量,它们配置在下一节的 `config.yaml`。
|
||||
线程池上限变量现已真正接入;修改 .env 后必须重新启动后台,Docker 应通过现有启动脚本重新创建容器,
|
||||
单纯 `docker restart` 不会更新容器创建时注入的环境变量。
|
||||
其余模型地址、密钥、数据库连接等保留服务器原值,不用本机 .env 覆盖生产文件。
|
||||
|
||||
### 100 个线程与环境变量优先级
|
||||
|
||||
`GATEWAY_WORKERS=100` **不是**把工具线程池调成 100,而是启动 100 个进程,当前部署不要这样设置。
|
||||
|
||||
`DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE=100` **已经实现**,兼容别名 `DEERFLOW_IO_THREADS=100`,
|
||||
两者任选其一即可。开启线程池开关后,按以下顺序取第一个非空值:
|
||||
|
||||
1. `DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE`(正式变量)
|
||||
2. `DEERFLOW_IO_THREADS`(兼容别名)
|
||||
3. `config.yaml` 的 `runtime_performance.asyncio_thread_pool_size`(默认 100)
|
||||
|
||||
有效范围为整数 **1..1024**。选中环境变量的值非法时启动失败并提示,不会静默退回默认小线程池。
|
||||
开关关闭时不读取环境变量来改变线程池,保留 Python 原默认。已有正式变量时,修改别名不会覆盖它。
|
||||
线程按需创建,100 是上限,不会预先常驻启动 100 个线程。
|
||||
|
||||
## 3. config.yaml 新增配置
|
||||
|
||||
在实际加载的 `config.yaml` **顶层**新增一段,不能缩进到 `models`、`sandbox` 或 `skills` 内,
|
||||
也不要重复定义两个 `runtime_performance`:
|
||||
|
||||
```yaml
|
||||
runtime_performance:
|
||||
# 一次工具调用内只获取一次已验证技能视图,默认 false。
|
||||
skill_view_reuse_enabled: false
|
||||
|
||||
# 版本未变时复用技能视图,默认 false。
|
||||
skill_view_cache_enabled: false
|
||||
|
||||
# 模型计时日志使用独立队列,不争抢耗时工具线程,默认 false。
|
||||
model_timing_nonblocking_enabled: false
|
||||
|
||||
# 是否覆盖 Gateway asyncio 默认线程池,默认 false。
|
||||
asyncio_thread_pool_enabled: false
|
||||
asyncio_thread_pool_size: 100
|
||||
```
|
||||
|
||||
如决定启用本次四项优化并将线程池上限设为 100,改为:
|
||||
|
||||
```yaml
|
||||
runtime_performance:
|
||||
skill_view_reuse_enabled: true
|
||||
skill_view_cache_enabled: true
|
||||
model_timing_nonblocking_enabled: true
|
||||
asyncio_thread_pool_enabled: true
|
||||
asyncio_thread_pool_size: 100
|
||||
```
|
||||
|
||||
四个开关独立生效,可以单独开关。关闭/省略对应项时,对应的性能优化走原实现。
|
||||
这四项不控制模型协议兼容修复或前端错误处理;全部关闭不等于回退所有代码。
|
||||
为了让已有模型实例和所有后台进程配置一致,修改后重启后台;回退也是改回 `false` 后重启。
|
||||
|
||||
### 确认没有改错配置文件
|
||||
|
||||
- 已设置 `DEER_FLOW_CONFIG_PATH` 时,以它指向的文件为准;Docker 内该值必须是容器内可访问路径。
|
||||
- 本仓库包含 `offline-backend-20260512/config.yaml` 和 `offline-backend-20260512/backend/config.yaml`;
|
||||
两份都已补齐默认关闭的示例,但服务器要修改的是启动时实际加载/挂载的那一份。
|
||||
- 启动日志 `Loaded AppConfig from ...` 可确认路径,`RUNTIME_PERFORMANCE` 可确认实际开关值。
|
||||
|
||||
## 4. 开启后做了什么,没做什么
|
||||
|
||||
### 单次调用复用
|
||||
|
||||
同一次 `read_file`、`ls`、`glob`、`grep`、`bash` 等工具执行期间,技能检查、沙箱初始化、
|
||||
路径替换和输出脱敏共用一份已验证视图;退出调用后清理作用域,下一次调用重新验证。
|
||||
不会把一个问答最初的技能启停状态一直沿用到结束。
|
||||
|
||||
### 技能版本缓存
|
||||
|
||||
每次核对启停 JSON 内容、已知源/目标文件和目录的元数据及标记。
|
||||
版本一致时不重新发现/解析全部技能、不复制文件、不持有全局刷新锁。
|
||||
新增、编辑、删除、启停及支持文件变化触发重建,并保留跨进程锁合并并发刷新。
|
||||
|
||||
没有采用会延迟下线的 TTL 缓存;下一次工具调用能发现下线或无效配置并拒绝访问。
|
||||
已经开始执行的工具不因此被强行中断。
|
||||
缓存仅用于标准 `LocalSkillStorage`,自定义存储实现/子类保守回到原刷新路径。
|
||||
|
||||
热路径仍有 O(文件数) 元数据校验,慢磁盘/网络挂载仍可能耗时;这不是“所有文件操作零开销”,
|
||||
也没有把受锁保护的重建变成无锁写入。
|
||||
|
||||
### 模型计时日志
|
||||
|
||||
仅轻量计时/入队在回调中执行,日志 I/O 由独立线程消费,不占工具共享线程池或事件循环。
|
||||
队列上限 4096 条;日志端长期阻塞时会丢弃诊断并在恢复后汇总告警,不无限积压或阻塞模型输出。
|
||||
退出时最多等待 0.5 秒尽力刷新,异常退出可能丢失尾部日志。
|
||||
|
||||
这是 `ModelTimingCallback` 的优化,不会改变其他回调(如 RunJournal、可选追踪插件)、
|
||||
子智能体调度池、数据库池或模型上游的连接/速率限制。不能据此保证 100 个会话同时生成。
|
||||
|
||||
### Gateway 工具线程池
|
||||
|
||||
开启 `asyncio_thread_pool_enabled` 后,在后台启动时、LangGraph 初始化之前设置当前
|
||||
Gateway 事件循环的默认执行器。同步工具、该循环的 `asyncio.to_thread` 等可使用最多 100 个槽位。
|
||||
关闭时完全不替换默认执行器;退出时由拥有该事件循环的服务运行器清理。
|
||||
|
||||
这不是 AnyIO 的同步 HTTP 路由线程池,也不改变子智能体专用池或独立事件循环的默认池。
|
||||
线程池扩大只能减少可并行 I/O 的排队,不能让同一把技能锁同时被多人持有,也不会增加模型端的生成配额。
|
||||
|
||||
## 5. 日志检查
|
||||
|
||||
确认加载日志类似:
|
||||
|
||||
```text
|
||||
RUNTIME_PERFORMANCE skill_view_reuse_enabled=True skill_view_cache_enabled=True model_timing_nonblocking_enabled=True asyncio_thread_pool_enabled=True asyncio_thread_pool_size=100
|
||||
```
|
||||
|
||||
线程池最终是否生效,以另一条启动日志为准(进程 id 随实际启动变化):
|
||||
|
||||
```text
|
||||
GATEWAY_ASYNCIO_EXECUTOR enabled=true max_workers=100 source=DEER_FLOW_ASYNCIO_THREAD_POOL_SIZE pid=12345
|
||||
```
|
||||
|
||||
如果仍显示 `enabled=false max_workers=python_default`,说明线程池开关没打开或改错了配置文件,
|
||||
不是已经调整到 100。改环境变量后若日志仍取旧值,检查是否重建了 Docker 容器及实际 env-file。
|
||||
|
||||
查看以下日志:
|
||||
|
||||
| 日志 | 用途 |
|
||||
| --- | --- |
|
||||
| `GATEWAY_ASYNCIO_EXECUTOR` | 实际线程池开关、最终上限、配置来源和进程 id;关闭时显示使用 Python 默认,不虚报 100 |
|
||||
| `SKILL_VIEW_CACHE status=refreshed` | 发生重建;包含 `lock_wait_ms`、`refresh_ms`、`total_ms` 和条目数 |
|
||||
| `SKILL_VIEW_CACHE status=joined` | 等待期间其他线程/进程已完成重建,本次直接复用 |
|
||||
| `SKILL_VIEW_CACHE status=hit` | 缓存命中;正常命中为 DEBUG,超过 500ms 的慢命中为 INFO |
|
||||
| `SKILL_VIEW_CACHE status=failed` | 刷新失败,旧能力不可继续使用,需要排查配置/磁盘 |
|
||||
| `LLM_CONNECT_START/OK`、`LLM_REQUEST_DONE/FAILED` | 模型调用生命周期、首块和总耗时 |
|
||||
| `LLM_CALL_RETRY/FAILED`、`OPENAI_TOOL_STREAM_SUMMARY` | 模型重试/兼容处理诊断 |
|
||||
| `LLM_TIMING_LOG_DROPPED` | 日志队列曾满;检查日志磁盘/采集端速度 |
|
||||
|
||||
如需查看正常缓存命中,可短时设置顶层 `log_level: debug`;排查后恢复 `info`。
|
||||
首块耗时不等于纯网络连接等待,仍包括上游排队、模型处理等阶段。单次 curl 成功也不能证明并发容量足够。
|
||||
本次日志不记录技能正文、模型正文或 API 密钥。
|
||||
|
||||
## 6. 前端替换与回退
|
||||
|
||||
1. 先备份服务器当前前端目录和 `runtime-config.js`,并备份实际后台 .env/config.yaml。
|
||||
2. 校验下载 ZIP 的 SHA-256 与本说明记录一致,再解压到一个新的发布目录。
|
||||
3. ZIP 包含 `dist/`;Web 根目录应指向其内部,确保根下直接能找到 `index.html`,不要误放成 `dist/dist/`。
|
||||
4. **保留/恢复服务器自己的 `runtime-config.js`**。核对生产后台地址、认证和嵌入参数;不要把构建机默认值当生产配置。
|
||||
也不要覆盖生产 .env 的密钥与数据库配置。恢复 runtime-config 后其单文件哈希自然与原包不同。
|
||||
5. 更新后台代码并按需设置四个开关,使用现有部署方式重启后台,再切换前端发布目录。
|
||||
6. 刷新浏览器缓存后检查普通问答、智能体问答、技能示例、读取技能、生成文件以及离开后重新打开正在生成的会话。
|
||||
7. 如需回退性能优化,后台关闭相应开关并重启,无需删除技能文件或技能缓存目录。
|
||||
如需回退模型协议或前端修复,须恢复对应的已备份代码/前端发布目录,单独关闭性能开关不够。
|
||||
|
||||
正式 Linux AMD64 离线镜像/发版仍通过独立的 `knowledge-base-bj/deerflow-offline-deployment`
|
||||
仓库流程执行;本次前端 ZIP 不是完整后台离线镜像。
|
||||
|
||||
## 7. 本地验证结果与限制
|
||||
|
||||
### 后台
|
||||
|
||||
- 提交前最终专项:技能/线程池/日志/启动配置及技能老回归 **166 项通过**;模型适配、错误分类、
|
||||
诊断失败保护、SSE 响应头、会话计时与真实 transport **123 项通过**(两组不重叠,共 289 项)。
|
||||
对应修改模块与测试 Ruff 通过,harness 未增加对 app 的反向依赖。后续没有执行生产模型并发压测。
|
||||
- 本次技能复用/缓存/配置、日志回调与相关技能目录、大文件安全专项:**79 项通过**;
|
||||
又补充 1 项开关关闭/重新开启的回退测试,通过缓存文件全部 13 项回归,合计 **80 项**。
|
||||
- 模型工具参数流/推理流、错误恢复、SSE 传输和启动 worker 校验另 **76 项通过**;相关专项共 **156 项通过**。
|
||||
- 后续新增线程池接入与配置专项 **45 项通过**(与前述配置测试有重叠,不相加充当独立总数):
|
||||
覆盖默认关闭、两个环境变量优先级/别名、非法值拒启、启动顺序,以及实际 **24 / 100 个本地阻塞任务同时执行**、
|
||||
退出后线程全部清理;新模块和相关测试 Ruff 检查通过。另一次联合技能/日志/启动配置回归 **88 项通过**。
|
||||
该测试只验证本地线程池,不是 100 路模型压测。
|
||||
- 线程池接入后再次尝试后台全量测试(首次失败即停止):**604 通过、2 跳过、1 失败**,
|
||||
仍停在修改前已复现的 `test_llm_metrics.py::test_middleware_records_success_with_speed`,不是全量通过。
|
||||
- 冷启动 8 线程并发只构建一次;真实第二进程复用持久化清单、不再发现技能。
|
||||
- 下线/无效配置、源文件变更、同长保留 mtime 编辑、目标篡改、Windows junction 保护均有回归覆盖。
|
||||
- 真实 LangChain 异步模型测试证明:默认工具线程池占满且日志处理器阻塞时,启用计时日志优化仍可开始、输出 token 并结束;未调用真实模型服务。
|
||||
- 修改后尝试全量测试,设置失败 5 项停止:**871 通过、2 跳过、5 失败**,并非全量通过。
|
||||
失败包括 `test_llm_metrics.py` 的 3 项记录时序断言、既有 Wiki `auto_sync` 配置与测试预期不一致、
|
||||
`test_model_management.py` 的 thinking 参数预期不一致。首项在本轮修改前即已复现;
|
||||
本轮没有修改指标中间件、模型工厂或 Wiki auto_sync 设置,不为过测试擅改这些业务配置。
|
||||
- 本次针对 #185/MD 的追加回归:模型真实 SDK 回放、工具参数、思考字段、日志、配置、线程池、
|
||||
错误中间件和传输联合 **145 项通过**。其中新增 SDK 回放 19 项,覆盖 8 种写文件形态,
|
||||
确认 JSON 尚未闭合时部分 MD 已传出、最终工具名称和参数正确;没有连接线上模型。
|
||||
|
||||
### 合成性能对比
|
||||
|
||||
Windows 11 / Python 3.12.13,本机临时目录:150 个技能、450 个文件,各模式预热一次后串行 10 次。
|
||||
旧刷新平均约 **3133ms**,缓存命中平均约 **806ms**(本次约 3.89 倍);
|
||||
`load_skills` 从 10 次降为 0 次。临时数据已清理。
|
||||
这不是生产 Linux 性能结论,也不是多人访问模型的承载压测。
|
||||
|
||||
### 前端与交付包
|
||||
|
||||
- 提交前最终前端专项 **60 项通过**(原 57 项,加同 reader 续接/新页面重播与迟到事件隔离 2 项、
|
||||
最小技能示例 1 项);真实生产 React 浏览器错误边界测试再次通过。
|
||||
- 最终全量类型检查仍有既有 **75 个诊断**,本次变更文件无新增诊断;记录在
|
||||
`.runtime/frontend-final-typecheck-20260927.log`。不要把构建成功等同全量类型检查通过。
|
||||
- 前端本次流式恢复、React #185 分类、技能示例、流式预览及相关权限等 **32 项专项通过**;
|
||||
修正新增测试文件一处 `.ts` 导入导致的 TS5097 后,相关 16 项再次通过。
|
||||
- 最新追加前端专项 **57 项通过**,含真实 SDK SSE → 文件参数 → MD 预览;旧 16ms 设置在连续流中
|
||||
没有通知刷新,修复后在 JSON 尚未闭合时持续更新。真实 Chrome + 生产 React 中用循环 layoutEffect
|
||||
触发 #185,验证静态友好兜底、无自动重挂、点击重新载入后恢复。
|
||||
- 最新 `pnpm build` **通过**:Vite 13,853 个模块,8 分 49 秒;存在大于 500KB 的 chunk 提醒,不是构建失败。
|
||||
构建日志 `.runtime/frontend-final-build-20260927.log`;本次修复重新打开会话的游标边界后已重新打包。
|
||||
- 前一版临时 localhost 静态 HTTP 烟测 **通过**:入口 HTML 及引用的主 JS/CSS/runtime 等 5 个资源均 200,
|
||||
响应字节 SHA-256 与磁盘一致,没有缺失资源;烟测服务已关闭。
|
||||
- 最新 ZIP 中 **886 个文件**均逐文件比对大小和 SHA-256,与本次 dist 一致;入口/CSS 的 99 个本地资源引用全部存在。包内无 `.env`,
|
||||
入口为 `dist/index.html`,包含 `dist/runtime-config.js`。
|
||||
- 全量 `pnpm typecheck` **未通过**:复测还有 **75 个诊断、涉及 29 个本次未修改文件**,
|
||||
包括现有 nextra / `@codemirror/state` 依赖、旧测试及 legacy QA 类型问题;
|
||||
本次新增测试文件的 TS5097 已修正并复测,本次前端变更文件无类型诊断。
|
||||
完整本地日志为 `.runtime/qa/frontend-delivery-typecheck-20260927.log`;
|
||||
不能把生产构建通过表述成整个项目类型检查通过。
|
||||
追加修复后本次产品变更文件无全量类型诊断,新增测试及依赖专项 TypeScript 检查为 0 诊断。
|
||||
- 附加 legacy Chrome CSS 检查 **未通过**,报三个本轮未修改文件
|
||||
`legacy-html-preview.test.ts`、`legacy-html-preview.ts`、`SolutionProjectLanding.tsx` 中的现代颜色函数字样;
|
||||
没有报本次 dist CSS 缺失回退,也不能据此宣称完成旧浏览器端到端兼容验收。
|
||||
|
||||
交付 ZIP(仓库根目录):
|
||||
|
||||
```text
|
||||
deerflow-frontend-dist-20260927-master-final.zip
|
||||
```
|
||||
|
||||
- ZIP 大小:35,869,210 字节(约 34.2 MiB)。
|
||||
- 此包替代 `deerflow-frontend-dist-20260927-191207-185-stream-fix.zip` 及更早版本;旧包保留,但不包含最终的游标作用域修复。
|
||||
- 解压后文件总大小:66,347,693 字节。
|
||||
- ZIP SHA-256:
|
||||
|
||||
```text
|
||||
7E4C1525BDEB686161FFF5AED12D36087407BBF4F472B87E78CA04DADD1DF371
|
||||
```
|
||||
|
||||
Linux 可用 `sha256sum deerflow-frontend-dist-20260927-master-final.zip`;
|
||||
Windows 可用 `Get-FileHash -Algorithm SHA256 .\deerflow-frontend-dist-20260927-master-final.zip`。
|
||||
|
||||
未连接生产环境进行真实多人模型问答端到端验收,不能宣称所有线上卡顿已被实测消除。
|
||||
|
||||
## 8. 关于 20 人同时使用
|
||||
|
||||
当前不能承诺 20 人同时问答一定流畅。本次验证了本地代码路径和构建产物,并没有验证你的两台服务器、
|
||||
上游模型地址/API key 配额、数据库/磁盘及实际技能耗时在 20 路负载下的容量。
|
||||
|
||||
部署新版后台、明确开启所需开关后,先在测试环境按 5 → 10 → 20 个独立会话逐级测试:
|
||||
|
||||
1. 纯聊天和包含技能读取/写文件的问答分开测试,保持提示长度和输出规模接近真实业务。
|
||||
2. 每级记录成功/失败/超时数、首字和总耗时的分位数,以及技能 `lock_wait_ms/total_ms`。
|
||||
3. 同步查看模型端 429/503、排队数量/首 token 延迟,以及应用 CPU、内存、磁盘和数据库等待。
|
||||
4. 若上游只能并行处理少量请求,应用扩线程无法增加上游容量;应控制并发或扩容模型服务。
|
||||
|
||||
20 个在线用户、20 条运行中的问答和 20 个正在模型端生成的请求不是同一指标。
|
||||
|
||||
## 9. #185 与 MD 流式追加修复
|
||||
|
||||
用户已确认思考模式正常;本节针对仍出现的 #185 和 MD 预览卡住。
|
||||
|
||||
- 四个直接流入口统一使用 `STREAM_RENDER_BATCHING=true`,避免安装版本 SDK 的数字 16ms
|
||||
尾部防抖不断推迟 UI 刷新;保留异步批处理,不改成每个片段同步强制渲染。
|
||||
- #185 流错误不会再按模型异常直接显示原始错误串;同一轮最多一次自动恢复,重复失败暂停重连,
|
||||
不主动取消后台任务,不循环重新发送问题。友好提示 30 秒去重。
|
||||
- 真正渲染/commit 阶段的 #185 由页面错误边界接住,进入静态提示,只能手动重新载入;
|
||||
不吞其他错误。控制台 `react_ui_render_error` 保留组件堆栈,帮助定位未知的线上循环组件。
|
||||
- 恢复游标与当前页面的消息累积器绑定:同一页面断线续接不重复拼接工具参数,离开后重新打开或
|
||||
切换会话则完整重播,不能复用全局旧游标跳过正文前缀;旧 reader 的迟到事件也不污染新页面。
|
||||
- 后端兼容重复工具名称,避免 `write_filewrite_file` 使工具路由和预览识别失效;
|
||||
`OPENAI_TOOL_STREAM_SUMMARY repeated_tool_names` 可确认是否命中。也修复了空 `error:{}` 被误判失败。
|
||||
- 诊断日志写入失败不会覆盖真实模型异常、取消或成功结果。错误脱敏后仍在内存中识别上下文超限,
|
||||
并识别 `invalid_api_key` 等结构化认证错误,防止本应修剪上下文或停止重试的请求反复排队。
|
||||
- **实时预览和物理落盘不同**:模型还在生成参数时展示虚拟 MD 内容;参数完整、工具校验通过后,
|
||||
`write_file` 才正式写沙箱文件。此次不把未完成的 JSON 直接写进最终文件。
|
||||
|
||||
本地已经复现并修复上述具体缺陷,但没有线上原始流/组件堆栈,不能保证它们解释全部生产故障。
|
||||
|
||||
## 10. `threads` 创建慢:数据库路由与阶段耗时
|
||||
|
||||
`POST /api/threads` 会先查/写应用数据库的 `threads_meta`,再调用检查点存储写入空 checkpoint;
|
||||
它不需要请求模型,也不会在创建时遍历所有旧问答。
|
||||
|
||||
当前仓库部署模板是 `database.backend: mysql`,同时 `checkpointer.type: postgres`、
|
||||
`checkpointer.use_postgres: false`。如果服务器保持这些值且没有 `DEER_FLOW_USE_PG_CHECKPOINTER`
|
||||
覆盖,实际是 **MySQL 元数据 + SQLite 检查点**,清理 PostgreSQL 不会加速这条链路。
|
||||
本地开发配置则是 SQLite。线上以实际加载的配置和环境覆盖为准,不凭数据库连接串是否存在判断。
|
||||
|
||||
新建/搜索接口新增 `GATEWAY_OPERATION_TIMING` 日志和 `Server-Timing` 响应头,
|
||||
浏览器 Network 的响应头也能看到,不必先拿服务器日志:
|
||||
|
||||
- `metadata_lookup` / `metadata_create`:应用元数据查询/写入。
|
||||
- `checkpoint_create`:检查点创建,包含 saver 锁/连接池等待和数据库写入,不能等同为纯 SQL 时间。
|
||||
- `metadata_search`:会话列表的元数据查询。
|
||||
- `handler_total`:进入处理函数后的耗时;**不含进入函数前的事件循环/鉴权/代理排队**。
|
||||
|
||||
模型图构建仍保持原来的任务上下文。新增 `AGENT_BUILD_TIMING`:超过 500ms 以 WARNING 记录,
|
||||
正常为 DEBUG。已在本地复现同步慢工厂阻塞同事件循环的其他请求,但不能直接裸搬到线程:
|
||||
延迟工具注册上下文需回传,MCP 初始化存在跨事件循环约束。此处目前是定位日志,不是已经消除阻塞。
|
||||
|
||||
本机安装的 SQLite saver 和 PostgreSQL saver 都存在实例级串行锁;后者使用连接池也不代表
|
||||
同一个 saver 自动并行处理所有检查点。扩大 asyncio 线程池不解除此锁。
|
||||
尚未取得线上分阶段耗时,因此不能承诺新建 10 秒已经根治,也不能承诺归档即可解决。
|
||||
新增计时与真实 SQLite 锁测试、已有分析项目/浏览器内部创建入口联合 **43 项通过**,
|
||||
其中计时专项 14 项;这是本地回归,不是线上延迟验收。
|
||||
|
||||
## 11. 30 天已结束会话的 PostgreSQL 归档 SQL
|
||||
|
||||
归档脚本单独交付,不进入本源码仓库,也不在启动时执行。使用 `pg_checkpoint_archive.sql` 搬移,
|
||||
`pg_checkpoint_restore.sql` 按提交批次恢复;两者需与 `pg_checkpoint_common.sql` 放在同一目录。
|
||||
应用数据库在 MySQL / PostgreSQL 的候选只读导出分别为
|
||||
`mysql_checkpoint_candidates.sql` / `pg_checkpoint_candidates.sql`。
|
||||
|
||||
按用户确认,候选仅包括 30 天未更新、所有运行已结束的会话,排除正在运行、待处理和等待用户输入的会话。
|
||||
PG 检查点没有应用会话终态字段,因此不能只按 `checkpoint.ts` 猜测,必须先从应用元数据导出候选 ID。
|
||||
脚本仅在确认实际启用 PG、全部写入服务已停、备份可恢复及双读回退关闭后允许提交。
|
||||
默认是预览/回滚;提交时三张原始表的全部 namespace 一起搬入新归档 schema,逐行校验后才删源行,
|
||||
恢复时不覆盖同 ID 的新数据,归档副本保留。不删除沙箱文件。
|
||||
|
||||
**范围与风险**:这是 PG 检查点冷归档,不是完整应用归档。它不搬 MySQL `threads_meta/runs`,
|
||||
因此旧会话列表入口可能仍在,但归档正文需要恢复后才能读取/续聊。归档到同一库的新表也不保证减少总磁盘占用。
|
||||
SQL 的验证记录与具体执行命令见单独交付包的 README;部署前仍须在生产备份恢复的隔离副本演练。
|
||||
不要跳过预检直接在生产提交。如果线上仍为 SQLite,这组 PG SQL 不适用。
|
||||
|
||||
独立包 `deerflow-db-archive-20260927.zip` 含 SQL、README 和复验测试,不含数据库数据、连接凭据或数据库二进制。
|
||||
已在 PostgreSQL 16.15 + 安装的 LangGraph checkpoint 3.1.2 原始建表结构上通过 13 项真实执行和 14 项静态检查;
|
||||
未执行真实 MySQL 候选导出,详见独立包 README。ZIP 共 9 个文件、20,983 字节,SHA-256:
|
||||
|
||||
```text
|
||||
E90A189DA7D7032527EED7CEC9B51DE1A30FB6D48B4382CE06AF1C287B670F59
|
||||
```
|
||||
@@ -13,22 +13,12 @@
|
||||
|
||||
模式在创建后不可修改,前后端都不提供修改入口。项目、对话归属和智能体访问权限在服务端校验;方案结构及每条对话所选智能体也以服务端配置为准。
|
||||
|
||||
## 普通问答跨会话记忆
|
||||
|
||||
- 仅普通问答项目显示「跨会话项目记忆」。它只受项目自身开关控制,与系统「问答记忆」总开关相互独立;成功完成的会话会在后台整理,状态包括等待沉淀、整理中、已就绪和异常。
|
||||
- 新会话提问时会召回同一账号、同一项目中其他会话的事实、决策、约束、偏好、术语与未决事项。不会读取其他项目或其他账号,也不会混用普通聊天/智能体的通用记忆。
|
||||
- 项目设置和问答页头部均可打开「项目记忆」面板,查看精简快照、条目可信度/重要度和来源对话;支持手工新增、编辑、固定、删除、清空以及从当前项目历史对话重建。项目暂停后仍可清空已有数据,但不能重建。
|
||||
- 编辑、删除或清空原对话时,服务端先让对应记忆失效,再修改消息;重建期间不注入旧快照。固定或手工条目在重建时保留,自动条目按来源 checkpoint 时间判断新旧,较早会话不会因后台任务晚完成而覆盖新的纠正。
|
||||
- 交互问答在首包前使用有界关键词召回,避免等待远程向量编码;后台整理仍可写入向量供显式或离线检索使用,编码不可用不影响跨会话记忆。输入提示块有条数与 token 上限,历史内容只作为非指令资料,当前消息优先。
|
||||
|
||||
## 代码与运行
|
||||
|
||||
- `src/pages/AnalysisProjectsPage.tsx`:普通问答与方案项目的新建、设置、历史和工作台布局。
|
||||
- `src/strategy-selection/StrategyProjectsPage.tsx`:「我的项目」三类项目列表及创建入口。
|
||||
- `src/pages/analysis-projects/ProjectConversation.tsx`:现有聊天、研究进度、方案画布的项目级组合。
|
||||
- `src/core/analysis-projects/api.ts`:Gateway 项目 API 与按账号隔离的缓存。
|
||||
- `src/pages/analysis-projects/ProjectMemoryPanel.tsx`:项目记忆状态、快照、来源和管理操作。
|
||||
- `src/core/analysis-projects/memory-status.ts`:总开关/项目开关、状态和按钮可用性映射。
|
||||
- 后端:`/api/analysis-projects`,对话仍通过现有 `/api/threads/{thread_id}/runs/*` 流式接口发送;服务端从不可变归属读取项目配置。
|
||||
|
||||
启动新版 Gateway 后自动创建项目表。生产持久化依赖已配置的数据库;memory 模式中的项目随服务重启丢失。前端沿用仓库的 Vite 开发与构建命令。
|
||||
@@ -43,14 +33,6 @@ python -m pytest tests/test_analysis_projects.py tests/test_deep_research_report
|
||||
|
||||
涵盖内存与 SQLite 存储、跨用户隔离、禁止修改模式、运行时覆盖伪造模式与知识库、更新配置、对话归属与失败清理、固定方案模板及知识库资料进入研究管线。
|
||||
|
||||
跨会话记忆专项:
|
||||
|
||||
```shell
|
||||
python -m pytest tests/test_analysis_project_memory.py tests/test_analysis_project_memory_store.py tests/test_analysis_project_memory_runtime.py tests/test_analysis_project_memory_integration.py tests/test_analysis_projects.py -q
|
||||
```
|
||||
|
||||
前端专项:`node --experimental-strip-types --test src/core/analysis-projects/memory-status.test.ts`,并执行 `pnpm build`。
|
||||
|
||||
部署前还需使用实际知识库和模型验证三类问答、流式断线续接及方案后续修改。接口模拟与单元测试不能替代真实模型联调。
|
||||
|
||||
## 2026-09-14 我的项目与知识库验收修复
|
||||
@@ -61,7 +43,7 @@ python -m pytest tests/test_analysis_project_memory.py tests/test_analysis_proje
|
||||
- 普通问答先创建具名项目,再在项目内创建多个对话。列表读取 `/api/analysis-projects` 的 `qa` 项目,不再读取、标记或自动归入全局普通聊天。
|
||||
- 项目设置更新保留既有知识库绑定。旧「项目空间」入口已移除并跳转「我的项目」;普通问答路由为 `qa/projects/groups/*`,方案生成路由为 `qa/projects/solutions/*`。
|
||||
- 策略生成卡片统一先显示思考/执行过程,再显示正文;评判、方案及展开视图沿用相同顺序。
|
||||
- 策略优选工作台拆为三步:意图与任务定义、策略生成与智能评判、综合方案生成。第二步不重复展示问答记录,突出当前任务并扩大策略回答、评判正文;第三步将生成过程默认收起并为方案正文保留固定可视空间,集中完成方案生成、Markdown 编辑、项目保存以及 Markdown/Word 直接下载。保存后的项目卡也提供 Markdown/Word 下载;`?module=solution` 直接进入第三步,历史方案即使任务已修改仍可查看下载。
|
||||
- 策略优选工作台拆为三步:意图与任务定义、策略生成与智能评判、综合方案生成。第二步只保留候选策略和评判选择,第三步集中完成方案生成、Markdown 编辑、项目保存以及 Markdown/Word 直接下载;`?module=solution` 直接进入第三步,历史方案即使任务已修改仍可查看下载。
|
||||
- 资料库把已完成的策略优选综合方案作为独立「策略生成」来源展示,并标注具体策略项目名称;条目可回到策略项目第三步,也可直接下载 Markdown/Word。普通策略项目线程生成的文件沿用原文件下载链路并显示同类来源标识。
|
||||
- 方案生成使用独立项目和固定报告结构,通过 `deep_research_report` 直接生成并打开方案画布;策略优选项目不再出现在方案生成分类中。
|
||||
- 知识库管理页读取上游最新目录,避免远端删除后命中旧缓存;检索调用仍共享短期目录缓存。
|
||||
|
||||
@@ -1,977 +0,0 @@
|
||||
# 多智能体会商功能与接口梳理
|
||||
|
||||
## 1. 文档范围与入口
|
||||
|
||||
本文梳理的是常规「多智能体会商 / 圆桌规划」页面 `RoundtablePlanningPage` 的完整三步流程:
|
||||
|
||||
1. 任务意图理解
|
||||
2. 多智能体研讨
|
||||
3. 方案结果展示
|
||||
|
||||
主入口为 `/page/workspace/qa/multi-agent-roundtable`。它也可由任务深链 `3qfx-A`、`rwfx-A`、`xdfx-A` 打开,分别挂载在任务管理下的三情、任务、行动业务路由中;另有 `#/embed/roundtable` 嵌入入口。
|
||||
|
||||
任务深链会预填任务名称、任务目标、业务编码和任务 ID,并自动选择对应业务链:
|
||||
|
||||
| 深链 | 业务编码 | 嵌入路由 |
|
||||
| --- | --- | --- |
|
||||
| `3qfx-A` | `3Q` | `/page/strategy/task-management/plan-task-sentiment/roundtable` |
|
||||
| `rwfx-A` | `6BF` | `/page/strategy/task-management/plan-task/plan-task-fx/roundtable` |
|
||||
| `xdfx-A` | `7BF` | `/page/strategy/task-management/plan-task/plan-task-action/roundtable` |
|
||||
|
||||
> 本文不展开独立的阵位会商,以及仅展示报告的 8BF 变体。8BF 复用了部分组件,但其第三步为 `report-only`,不属于常规三步会商主路径。
|
||||
|
||||
## 2. 公共机制
|
||||
|
||||
### 2.1 会商记录与保存
|
||||
|
||||
功能:
|
||||
|
||||
- 保存任务意图、研讨过程、会商结论、第三步报告及当前已完成步骤。
|
||||
- 支持新建会商、历史记录切换、重命名、删除和从已有记录继续。
|
||||
- 普通入口按当前用户隔离;任务深链按任务共享,进入同一任务的用户看到同一批会商和后台任务。
|
||||
- 更新使用乐观锁;版本不一致时后端返回 `409`,前端应以服务端最新记录为准后再处理。
|
||||
|
||||
接口:
|
||||
|
||||
| 场景 | 接口 | 调用方式与参数 |
|
||||
| --- | --- | --- |
|
||||
| 普通会商列表 | `GET /api/roundtable-drafts` | 无请求体,按当前用户返回。 |
|
||||
| 新建普通会商 | `POST /api/roundtable-drafts` | JSON:`{ id?, title, furthest_step, step1, step2, step3 }`。 |
|
||||
| 读取/更新/删除普通会商 | `GET/PUT/DELETE /api/roundtable-drafts/{draftId}` | `PUT` 传需更新字段及 `expected_version`。 |
|
||||
| 任务会商列表 | `GET /api/roundtable-task-drafts/by-task/{taskId}` | `taskId` 为任务 ID。 |
|
||||
| 读取最新任务会商 | `GET /api/roundtable-task-drafts/by-task/{taskId}/latest` | 用于任务进入后恢复最近一次会商。 |
|
||||
| 新建任务会商 | `POST /api/roundtable-task-drafts` | JSON:`{ id?, task_id, title, furthest_step, step1, step2, step3 }`。 |
|
||||
| 读取/更新/删除任务会商 | `GET/PUT/DELETE /api/roundtable-task-drafts/{draftId}` | `PUT` 为局部更新并携带 `expected_version`。 |
|
||||
|
||||
参数说明:
|
||||
|
||||
- `step1`、`step2`、`step3` 保存的是各步骤可恢复的完整页面状态,而不是单一文本。
|
||||
- `furthest_step` 记录用户历史上到达的最远步骤,用于恢复导航可用性。
|
||||
- `xdfx-A` 的页面记录和后台任务分区使用 URL 原始行动 ID;任务正文及任务详情仍使用解析后的实际任务 ID。
|
||||
|
||||
### 2.2 流式调用与附件
|
||||
|
||||
功能:
|
||||
|
||||
- 大模型回复、意图澄清、推荐、研讨、报告和流程图均通过 SSE 增量返回。
|
||||
- 用户可停止当前流;前端按事件逐段追加正文、思考、工具调用、澄清卡片和错误信息。
|
||||
- 有文件时先上传到对应线程,再将后端返回的虚拟路径传给后续流式接口。
|
||||
|
||||
附件接口:
|
||||
|
||||
```http
|
||||
POST /api/threads/{threadId}/uploads
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
files: <file> # 可重复,支持多文件
|
||||
```
|
||||
|
||||
响应中的 `{ filename, size, virtual_path }` 应转换后传入流式请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"filename": "任务材料.docx",
|
||||
"size": 10240,
|
||||
"path": "/mnt/user-data/uploads/任务材料.docx",
|
||||
"status": "uploaded"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
所有 JSON 接口经前端 `apiFetch` 调用,自动携带当前登录态;流式接口使用 `ReadableStream` 解析 SSE。
|
||||
|
||||
---
|
||||
|
||||
# 第一步:任务意图理解
|
||||
|
||||
## 1. 功能
|
||||
|
||||
第一步将用户的自然语言任务转成可供多个智能体研讨的结构化意图,并确定后续参会智能体来源。
|
||||
|
||||
- 创建内置「任务意图理解」智能体会话,接收任务描述、补充材料和多轮追问。
|
||||
- 流式展示智能体回复、思考过程和工具型澄清问题。
|
||||
- 澄清卡支持单选、多选、自定义填写、跳过和继续追问;同一轮可返回多张澄清卡。
|
||||
- 意图完成后生成可编辑的结构化摘要:目标(`objective`)、约束(`constraints`)、可选假设(`assumptions`)。
|
||||
- 支持手工修改意图摘要后再进入下一步。
|
||||
- 智能体选择有两条路径:
|
||||
- **智能体推荐**:依据意图,在可用智能体候选池中推荐参会人及理由。
|
||||
- **业务链条**:选择已配置的业务链条,带入席位、阶段、阶段目标、协调人提示词和席位模型策略。
|
||||
- 任务深链模式自动加载任务卡片、预填任务上下文并自动选择业务链;普通模式在“选择参会智能体”弹窗中手工选择“我的”或“公共”业务链。
|
||||
|
||||
步骤流转:
|
||||
|
||||
```text
|
||||
输入任务 / 上传材料
|
||||
↓
|
||||
意图智能体澄清(可多轮)
|
||||
↓
|
||||
结构化意图完成并可编辑
|
||||
↓
|
||||
智能体推荐 或 选择业务链
|
||||
↓
|
||||
进入第二步
|
||||
```
|
||||
|
||||
### 1.1 业务链功能与两个弹窗
|
||||
|
||||
业务链是可命名、可复用、可保存的“智能体编排模板”,不是单纯的智能体名单。一条链定义了谁参会、谁先后执行、哪些席位并行、每阶段应产出什么,以及协调人如何派发和汇总。
|
||||
|
||||
第一步中有两个职责不同的业务链入口:
|
||||
|
||||
| 入口 | 弹窗/页面 | 作用 | 是否直接开始研讨 |
|
||||
| --- | --- | --- | --- |
|
||||
| 页面顶栏“业务链条” | BusinessChainListDialog | 浏览、筛选、启停、发布、进入配置页编辑业务链。 | 否,它是管理入口。 |
|
||||
| 意图完成后点击“选择参会智能体 / 进入研讨” | RecommendAgentsDialog 的“业务链条”页签,内部为 BusinessChainPicker | 从可用链中选择一条,校验席位后把编排结果带入第二步。 | 是,确认“进入研讨”后开始第二步。 |
|
||||
|
||||
#### A. 选择参会智能体弹窗中的“业务链条”页签
|
||||
|
||||
功能:
|
||||
|
||||
- 弹窗有“智能体推荐”和“业务链条”两个页签;切换到业务链条后不会调用推荐模型,而是读取已保存的链条。
|
||||
- 可在“我的 / 公共”之间筛选:
|
||||
- **我的**:当前用户创建的业务链。
|
||||
- **公共**:已发布、所有账号都可查看和选用的业务链。
|
||||
- 每张链条卡展示链条名称、描述、席位数和席位编排流:
|
||||
- 线性链用“智能体 A → 智能体 B → 智能体 C”表示顺序。
|
||||
- 同一阶段有多个席位时显示“并行 N”分组;阶段之间仍用箭头表示串行依赖。
|
||||
- 实时智能体名称优先于链条保存时的名称快照;已删除智能体会置灰并提示。
|
||||
- 仅启用的链条可进入选择列表;选中链条后,至少保留 **2 个仍存在的可用席位** 才能点击“进入研讨”。
|
||||
- 如果链条配置了默认执行模式,弹窗顶部的执行模式会自动带入该值,用户仍可临时改为:
|
||||
- flash:快速,不开启思考。
|
||||
- thinking:思考,低推理强度。
|
||||
- pro:专业问答,思考 + 中等推理强度。
|
||||
- ultra:多智能体问答,思考 + 高推理强度,并允许席位调用子代理。
|
||||
- 如果链条含有有效分层、阶段目标或协调人提示词,选择后按 dag 编排;否则按 chain 线性编排。
|
||||
- DAG 链在弹窗底部额外选择执行方式:
|
||||
- **快速 · 双列禁写**:同阶段席位并行,主区双列展示,并行席位不写文件。
|
||||
- **沙箱 · 单列可写**:同阶段仍并行,但主区单列展示,允许文件产出,右侧沙箱随当前席位切换。
|
||||
- “配置链条”按钮跳转到业务链编辑页;没有链条时也会显示“去配置业务链条”引导。
|
||||
|
||||
确认后传给第二步的运行结果可概括为:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"agents": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势专家",
|
||||
"model": "该席位模型覆盖,可为空",
|
||||
"seat_skill_directive": true,
|
||||
"human_validation": false,
|
||||
"seatMode": "该席位推理深度覆盖,可为空"
|
||||
}
|
||||
],
|
||||
"mode": "chain | dag",
|
||||
"chain": {
|
||||
"id": "chain-id",
|
||||
"title": "任务综合研判链"
|
||||
},
|
||||
"plan": {
|
||||
"stages": "仅 dag 时携带的分层执行计划"
|
||||
},
|
||||
"dagExecMode": "fast | sandbox",
|
||||
"seatMode": "本次会商的链条级执行模式",
|
||||
"gatherFirst": true
|
||||
}
|
||||
~~~
|
||||
|
||||
其中,席位数组的顺序就是线性链执行顺序;DAG 时按阶段展平。单席位的 seatMode、模型覆盖、技能识别强化和人工校验会一并继承到第二步。
|
||||
|
||||
#### B. 顶部“业务链条”管理弹窗
|
||||
|
||||
功能:
|
||||
|
||||
- 顶栏点击“业务链条”打开,定位为“浏览和管理”,不直接选择参会人。
|
||||
- 支持按 **我的 / 公共 / 全部(仅管理员)** 筛选,再按 **全部状态 / 启用 / 停用** 过滤。
|
||||
- 每张卡片展示:
|
||||
- 名称、描述、席位数量;
|
||||
- 公共、待审核、已停用等状态;
|
||||
- 配置人和最后修改时间;
|
||||
- 智能体编排流:同 Stage 的席位用“并行 N”分组,Stage 之间以箭头连接。
|
||||
- 本人链条或管理员可在卡片上:
|
||||
- 启用 / 停用;
|
||||
- 发布为公共、取消发布;
|
||||
- 草稿链提交审核,审核中可撤回;
|
||||
- 点击“设置”跳转到该链条的编辑页。
|
||||
- 新建和编辑页面包含:
|
||||
- **基本信息**:标题、描述、默认执行模式、研讨前并行取数开关、总控提示词。
|
||||
- **链条编排**:从候选智能体池拖拽到 Stage;可新建、删除、上下移动 Stage,移动/拆分/合并席位。
|
||||
- **阶段目标**:每个 Stage 可填写目标,并可使用 AI 优化;阶段目标会指导协调人该阶段产出什么、如何承接前一阶段结果。
|
||||
- **总控提示词**:说明协调人如何派发、汇总、收口,支持 Markdown 和 AI 优化。
|
||||
- **席位级配置**:模型覆盖、推理深度覆盖、会商技能识别强化、人工校验;岗位协同专用入口还可配置岗位归属和产物位置。
|
||||
- **编排预览**:以流程图检查“同层并行、层间串行”的最终结构。
|
||||
|
||||
两个业务链弹窗的页面结构:
|
||||
|
||||
~~~text
|
||||
顶部“业务链条”管理弹窗
|
||||
┌ 标题和说明 ───────────────────────────────────────────────────────────┐
|
||||
│ 筛选:我的 / 公共 / 全部(管理员) | 全部状态 / 启用 / 停用 │
|
||||
├ 业务链卡片列表 ───────────────────────────────────────────────────────┤
|
||||
│ 名称、席位数、公共/待审核/停用状态、配置人、更新时间、说明 │
|
||||
│ 智能体 A → [并行:智能体 B、智能体 C] → 智能体 D │
|
||||
│ 操作:启停 / 发布或撤回 / 设置 │
|
||||
├───────────────────────────────────────────────────────────────────────┤
|
||||
│ 底部:配置(新建或跳转到业务链编辑页) │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
“选择参会智能体”弹窗的业务链页签
|
||||
┌ 顶部:智能体推荐 | 业务链条 | 本次执行模式(快速/思考/专业/多智能体)┐
|
||||
│ 筛选:我的 / 公共 │
|
||||
├ 链条选择卡片 ─────────────────────────────────────────────────────────┤
|
||||
│ 名称、描述、席位数、线性/分层编排、已删除席位告警 │
|
||||
├───────────────────────────────────────────────────────────────────────┤
|
||||
│ DAG 链:快速·双列禁写 | 沙箱·单列可写 │
|
||||
│ 底部:配置链条 | 取消 | 进入研讨 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
~~~
|
||||
|
||||
## 2. 接口、调用时机与传参
|
||||
|
||||
### 2.1 初始化意图会话
|
||||
|
||||
```http
|
||||
POST /api/intent/init
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "模型标识,可选,使用页面当前模型",
|
||||
"task_context": {
|
||||
"task_id": "任务深链时的任务 ID",
|
||||
"task_name": "任务名称",
|
||||
"task_goal": "任务目标",
|
||||
"business_code": "3Q | 6BF | 7BF"
|
||||
},
|
||||
"intent_agent_id": "roundtable-intent"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 普通会商一般省略 `task_context`,`intent_agent_id` 也可省略并由后端使用内置 `roundtable-intent`。
|
||||
- 返回 `{ status, agent_id, thread_id }`;后续上传与流式对话都使用此 `thread_id`。
|
||||
|
||||
### 2.2 发送任务或回答澄清问题
|
||||
|
||||
```http
|
||||
POST /api/intent/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"thread_id": "意图会话线程 ID",
|
||||
"message": "用户任务描述,或对澄清问题的答案",
|
||||
"model": "页面当前模型,可选",
|
||||
"intent_agent_id": "roundtable-intent",
|
||||
"task_context": {
|
||||
"task_id": "任务 ID",
|
||||
"task_name": "任务名称",
|
||||
"task_goal": "任务目标",
|
||||
"business_code": "业务编码"
|
||||
},
|
||||
"client_turn_id": "前端生成的幂等 ID",
|
||||
"files": [
|
||||
{
|
||||
"filename": "材料.pdf",
|
||||
"size": 10240,
|
||||
"path": "/mnt/user-data/uploads/材料.pdf",
|
||||
"status": "uploaded"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
主要 SSE 完成帧字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "asking | clarification | done | error",
|
||||
"content": "本轮可见回答",
|
||||
"summary": {
|
||||
"objective": "任务目标",
|
||||
"constraints": ["约束 1"],
|
||||
"assumptions": ["可选假设"]
|
||||
},
|
||||
"question": "单个澄清问题",
|
||||
"clarifications": ["多个澄清问题"],
|
||||
"clarification_type": "single | multiple | text",
|
||||
"clarification_context": {},
|
||||
"options": ["选项 A", "选项 B"],
|
||||
"allow_custom": true,
|
||||
"allow_multiple": false,
|
||||
"error_detail": "失败原因"
|
||||
}
|
||||
```
|
||||
|
||||
调用要点:
|
||||
|
||||
- `client_turn_id` 用于避免重试或网络抖动造成同一用户轮次重复执行。
|
||||
- 用户选项、填写的自定义答案、点击“继续/跳过”后,仍通过本接口的 `message` 继续同一 `thread_id`。
|
||||
- 上传接口必须先于本接口调用;流式接口只传上传成功后的虚拟路径。
|
||||
|
||||
### 2.3 获取候选智能体和执行推荐
|
||||
|
||||
```http
|
||||
GET /api/agents
|
||||
```
|
||||
|
||||
前端从返回列表中排除内置工具型智能体,将可参会的候选人整理成:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势研判专家",
|
||||
"description": "负责态势信息研判"
|
||||
}
|
||||
```
|
||||
|
||||
```http
|
||||
POST /api/recommend/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"intent": {
|
||||
"objective": "形成处置建议",
|
||||
"constraints": ["时限 24 小时"],
|
||||
"assumptions": ["数据可访问"]
|
||||
},
|
||||
"candidates": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势研判专家",
|
||||
"description": "负责态势信息研判"
|
||||
}
|
||||
],
|
||||
"model": "页面当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
推荐完成帧可得到:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "done",
|
||||
"content": "推荐过程说明",
|
||||
"picks": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"reason": "与任务目标的匹配理由"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
普通会商会额外保存推荐记录,便于恢复和审计:
|
||||
|
||||
| 接口 | 参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable-drafts/{draftId}/recommendations` | 无请求体,读取该草稿的推荐历史。 |
|
||||
| `POST /api/roundtable-drafts/{draftId}/recommendations` | `{ objective, status, model?, rationale?, picks, candidates }`。 |
|
||||
|
||||
### 2.4 读取、选择和管理业务链
|
||||
|
||||
| 接口 | 调用方式与参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable-chains?scope=public\|mine\|pending\|all` | 获取业务链列表;入口页通常读取公开链。 |
|
||||
| `GET /api/roundtable-chains/{id}` | 获取某条业务链完整定义。 |
|
||||
| `POST /api/roundtable-chains` | 新建业务链。 |
|
||||
| `PUT /api/roundtable-chains/{id}` | 更新业务链。 |
|
||||
| `DELETE /api/roundtable-chains/{id}` | 删除业务链。 |
|
||||
| `POST /api/roundtable-chains/optimize-prompt` | 使用模型优化总控提示词或阶段目标。 |
|
||||
|
||||
读取与弹窗行为:
|
||||
|
||||
- “选择参会智能体”弹窗调用 GET /api/roundtable-chains(我的)或 GET /api/roundtable-chains?scope=public(公共),并同时调用 GET /api/agents 校验每个席位是否仍存在。
|
||||
- 顶部管理弹窗在管理员身份下还可调用 scope=all;scope=pending 用于待审核链的管理场景。非管理员请求 all/pending 时后端会退化为当前用户范围。
|
||||
- 启用状态为 false 的链条仍可在管理弹窗查看,但不会出现在“选择参会智能体”弹窗中。
|
||||
|
||||
创建或更新业务链的核心参数示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "任务综合研判链",
|
||||
"description": "先收集信息,再由专家研讨",
|
||||
"seats": [
|
||||
{
|
||||
"agent_id": "agent-001",
|
||||
"name": "态势专家",
|
||||
"description": "分析外部态势",
|
||||
"model": "模型标识",
|
||||
"seat_skill_directive": true,
|
||||
"human_validation": true,
|
||||
"seat_mode": "flash"
|
||||
}
|
||||
],
|
||||
"stages": [["agent-001", "agent-002"], ["agent-003"]],
|
||||
"stage_goals": ["平行收集信息", "汇总形成判断"],
|
||||
"coordinator_prompt": "协调人需要按风险、机会、建议汇总",
|
||||
"seat_mode": "flash",
|
||||
"gather_first": true
|
||||
}
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- 链条创建时前端使用驼峰字段 stageGoals、coordinatorPrompt、seatMode、gatherFirst;请求发送前映射为接口字段 stage_goals、coordinator_prompt、seat_mode、gather_first。更新时传 null 可清除分层、阶段目标、总控提示词或默认执行模式。
|
||||
- `stages` 表示 DAG 分层:同层可并行,不同层按顺序执行。
|
||||
- `seat_mode` / 席位 `seat_mode` 可取 `flash`、`thinking`、`pro`、`ultra`。
|
||||
- model 为空时,该席位跟随第二步页面的全局模型;有值时仅覆盖该席位。
|
||||
- seat_skill_directive: true 会在该席位运行时强化其技能识别和使用指令;它与智能体自身同名开关任一开启即可生效。
|
||||
- `human_validation: true` 表示该席位输出后需要人工确认才可继续。
|
||||
- `gather_first: true` 表示先让全部席位平行收集信息,再进入正式研讨。
|
||||
|
||||
发布、审核和启停均通过更新接口发送局部字段:
|
||||
|
||||
| 操作 | PUT /api/roundtable-chains/{id} 请求体 |
|
||||
| --- | --- |
|
||||
| 启用 / 停用 | { "enabled": true } / { "enabled": false } |
|
||||
| 直接发布 / 取消发布 | { "is_public": true } / { "is_public": false } |
|
||||
| 提交审核 / 撤回审核 | { "pending_review": true } / { "pending_review": false } |
|
||||
| 管理员审核发布 | { "is_public": true, "pending_review": false } |
|
||||
|
||||
提示词优化接口的请求示例:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"prompt_text": "当前总控提示词或阶段目标原文",
|
||||
"kind": "coordinator | stage_goal",
|
||||
"context": "链条标题、各阶段席位及已有阶段目标组成的上下文",
|
||||
"model_name": "可选模型标识"
|
||||
}
|
||||
~~~
|
||||
|
||||
返回:
|
||||
|
||||
~~~json
|
||||
{
|
||||
"optimized": "可直接回填到总控提示词或阶段目标编辑框的文本"
|
||||
}
|
||||
~~~
|
||||
|
||||
## 3. 页面结构
|
||||
|
||||
```text
|
||||
┌──────────────────────────────── 页面顶部 ────────────────────────────────┐
|
||||
│ 三步导航:①任务意图理解 ②多智能体研讨 ③方案结果展示 │
|
||||
│ 业务链 / 新建或重新分析 / 保存草稿 / 历史记录 / 模型选择 / 设置 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────── 左侧入口栏 ───────────┬──────────── 中间工作区 ────────────┬──── 右侧摘要栏 ────┐
|
||||
│ 任务卡(仅任务深链) │ Step1 对话面板 │ 当前模型选择 │
|
||||
│ 业务链列表与展开关系 │ - 意图智能体消息 │ 结构化意图摘要 │
|
||||
│ 会商/分析历史记录 │ - 思考、工具、澄清卡 │ - 目标 │
|
||||
│ │ - 输入框、附件、发送/停止/继续 │ - 约束 │
|
||||
│ │ │ - 假设,均可编辑 │
|
||||
└───────────────────────────────────┴────────────────────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
任务深链还会显示任务目的入口和底部 `TaskJumpBar` 快捷跳转栏;任务模式隐藏手工选择业务链的入口,以深链业务编码自动选择链条。
|
||||
|
||||
---
|
||||
|
||||
# 第二步:多智能体研讨
|
||||
|
||||
## 1. 功能
|
||||
|
||||
第二步以第一步意图和选定智能体/业务链为输入,创建协调人与多个席位智能体,进行可观察、可干预、可恢复的会商。
|
||||
|
||||
- 为每位参会智能体和协调人创建独立线程。
|
||||
- 支持三种编排模式:
|
||||
- **推荐模式(recommend)**:协调人自由决定调度哪些推荐智能体。
|
||||
- **链条模式(chain)**:按业务链指定的席位顺序执行。
|
||||
- **DAG 模式(dag)**:同阶段并行、跨阶段串行;可传入阶段目标和协调人提示词。
|
||||
- DAG 支持 `fast` 与 `sandbox` 执行视图。快速模式会限制并行席位写文件;沙箱模式可产出和预览文件。
|
||||
- 可选“先收集后研讨”:全部席位先并行收集资料,再将有效交付共享给正式研讨。
|
||||
- 协调人可继续派发、发起人工澄清,或在不再派发时形成共识。
|
||||
- 用户可暂停/继续本地编排,回答协调人的澄清问题,向协调人追加文字或附件。
|
||||
- 席位可配置人工验证点;达到验证点后需人工确认。
|
||||
- 支持打开角色画像抽屉,在下一次运行前调整单席位模型和运行策略。
|
||||
- 实时展示角色状态、对话、思考、工具调用、阶段流图、并行席位、文件产物和人工参与记录。
|
||||
- 每次重新推荐或选择链条都会创建独立“研讨运行快照”;历史运行互不覆盖,可在左侧切换查看。
|
||||
- 支持将长时间会商转入后台任务。后台任务可被同一任务的其他用户看到、跟踪、继续回答或接管。
|
||||
|
||||
步骤门槛:
|
||||
|
||||
- 只有第一步的 `intentReady` 为真时,第二步可进入。
|
||||
- 协调人形成共识(`hasConsensus`)后,第三步才可进入。
|
||||
|
||||
## 2. 接口、调用时机与传参
|
||||
|
||||
### 2.1 初始化所有参会线程
|
||||
|
||||
```http
|
||||
POST /api/multi-agent/init
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_names": ["agent-001", "agent-002"],
|
||||
"main_agent_name": "coordinator",
|
||||
"model": "页面当前模型,可选"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `agent_names` 为选中的席位智能体标识。
|
||||
- `main_agent_name` 为协调人标识。
|
||||
- SSE 依次返回席位就绪、协调人就绪和初始化完成事件,最终得到线程映射:
|
||||
|
||||
```json
|
||||
{
|
||||
"thread_ids": {
|
||||
"agent-001": "席位线程 ID",
|
||||
"agent-002": "席位线程 ID",
|
||||
"coordinator": "协调人线程 ID"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 驱动协调人和席位研讨
|
||||
|
||||
```http
|
||||
POST /api/multi-agent/run/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
基础请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "leader | special",
|
||||
"agent_name": "coordinator 或席位 agent ID",
|
||||
"display_name": "页面展示名称",
|
||||
"d_agent_thread_id": {
|
||||
"coordinator": "协调人线程 ID",
|
||||
"agent-001": "席位线程 ID"
|
||||
},
|
||||
"new_message": "给协调人或席位的本轮提示词",
|
||||
"model": "模型标识",
|
||||
"business_code": "3Q | 6BF | 7BF",
|
||||
"files": []
|
||||
}
|
||||
```
|
||||
|
||||
关键参数:
|
||||
|
||||
| 参数 | 适用对象 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `agent_type: "leader"` | 协调人 | 由协调人分析现状、派发席位或形成共识。 |
|
||||
| `agent_type: "special"` | 席位、总结/报告等专用智能体 | 执行专业分析、总结或文件生成。 |
|
||||
| `d_agent_thread_id` | 全部 | 是“智能体 ID → 线程 ID”的映射,不是单一线程 ID。 |
|
||||
| `thinking_enabled?` | 席位 | 是否启用思考。 |
|
||||
| `reasoning_effort?` | 席位 | 推理强度。 |
|
||||
| `subagent_enabled?` | 席位 | 是否允许席位再调用子智能体。 |
|
||||
| `seat_skill_directive?` | 席位 | 席位特定的技能使用指令。 |
|
||||
| `parallel_no_file?` | 并行席位 | 快速并行时限制文件写入。 |
|
||||
| `peer_deliveries?` | 后续席位/协调人 | 传递前序席位可见交付物。 |
|
||||
| `synthesis_mode?` | 协调人 | 指示汇总类执行。 |
|
||||
| `suppress_pre_broadcast?` | 协调人 | 控制汇总前广播。 |
|
||||
| `skill_stop_names?` | 全部 | 指定需要拦截/停止的技能名称。 |
|
||||
|
||||
流式完成后前端重点读取:
|
||||
|
||||
```json
|
||||
{
|
||||
"dispatched": ["agent-001"],
|
||||
"content": "可见回答或结论",
|
||||
"agentName": "本次完成者",
|
||||
"modelUsed": "实际使用模型",
|
||||
"clarification": {
|
||||
"question": "需要用户回答的问题",
|
||||
"options": ["选项 A"]
|
||||
},
|
||||
"invalidDelivery": false
|
||||
}
|
||||
```
|
||||
|
||||
编排规则:
|
||||
|
||||
- 协调人先运行并决定需调度的席位。
|
||||
- 每个席位完成后,将其“可见回答”作为交付物回传,思考内容不作为席位交付。
|
||||
- 链条模式按链定义的顺序运行;DAG 模式同层并行、阶段间串行。
|
||||
- 协调人返回不再派发且无待答澄清时,前端将结果标记为共识。
|
||||
|
||||
### 2.3 产物查看与下载
|
||||
|
||||
| 接口 | 调用方式 |
|
||||
| --- | --- |
|
||||
| `GET /api/threads/{threadId}/artifacts` | 查询指定协调人或席位线程的文件产物。 |
|
||||
| `GET /api/threads/{threadId}/artifacts/{path}` | 预览或下载文件;下载时前端附加 `?download=true`。 |
|
||||
|
||||
### 2.4 后台会商任务
|
||||
|
||||
开始后台任务:
|
||||
|
||||
```http
|
||||
POST /api/roundtable-jobs
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"requestId": "前端请求 ID",
|
||||
"draftId": "会商草稿 ID",
|
||||
"taskId": "任务深链时的任务 ID",
|
||||
"intent": {
|
||||
"objective": "任务目标",
|
||||
"constraints": ["约束"],
|
||||
"assumptions": ["假设"]
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "agent-001",
|
||||
"name": "态势专家"
|
||||
}
|
||||
],
|
||||
"model": "当前模型",
|
||||
"orchestrationMode": "recommend | chain | dag",
|
||||
"chain": {
|
||||
"id": "chain-id",
|
||||
"title": "业务链名称"
|
||||
},
|
||||
"orchestrationPlan": {},
|
||||
"seedMessage": "首次给协调人的提示",
|
||||
"threadIds": {
|
||||
"coordinator": "线程 ID",
|
||||
"agent-001": "线程 ID"
|
||||
},
|
||||
"coordinatorName": "协调人名称",
|
||||
"seat_skill_directives": {
|
||||
"agent-001": true
|
||||
},
|
||||
"seat_modes": {
|
||||
"agent-001": "thinking"
|
||||
},
|
||||
"gather_first": true,
|
||||
"business_code": "3Q",
|
||||
"thinking_enabled": true,
|
||||
"reasoning_effort": "medium",
|
||||
"subagent_enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
后台任务接口:
|
||||
|
||||
| 接口 | 调用方式与参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable-jobs?draft_id={draftId}&task_id={taskId}` | 获取草稿/任务关联的后台任务列表。任务模式需带 `task_id`。 |
|
||||
| `GET /api/roundtable-jobs/{jobId}?task_id={taskId}` | 获取任务快照。 |
|
||||
| `GET /api/roundtable-jobs/{jobId}/stream?task_id={taskId}` | SSE 跟踪后台任务完整进度。 |
|
||||
| `POST /api/roundtable-jobs/{jobId}/resume?task_id={taskId}` | JSON:`{ answer }`,用于回答后台任务等待的人工问题。 |
|
||||
| `POST /api/roundtable-jobs/{jobId}/cancel?task_id={taskId}` | 取消后台任务;前台接管时也会先结束后台执行。 |
|
||||
|
||||
状态和阶段:
|
||||
|
||||
| 分类 | 枚举 |
|
||||
| --- | --- |
|
||||
| 任务状态 | `queued`、`running`、`awaiting_input`、`done`、`error`、`cancelled` |
|
||||
| 执行阶段 | `initializing`、`leader_thinking`、`dispatching`、`consensus`、`summary_writing`、`report_drawing`、`report_done` |
|
||||
|
||||
## 3. 页面结构
|
||||
|
||||
```text
|
||||
┌──────────────────────────────── 页面顶部 ────────────────────────────────┐
|
||||
│ 三步导航 / 会商记录 / 模型选择 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────── 可收起左栏 ───────────┬──────────── 中间研讨区 ─────────────┬──── 右侧沙箱区 ────┐
|
||||
│ 意图摘要 │ 顶部:模型、暂停/继续、转后台 │ Step2SandboxLayout │
|
||||
│ 角色列表与席位筛选 │ DAG 流程缩略图、并行席位切换 │ 文件/网页预览 │
|
||||
│ 人工参与/验证状态 │ 对话时间线:协调人、席位、工具、思考 │ 产物查看和下载 │
|
||||
│ 多次研讨运行快照切换 │ 澄清问题卡、人工确认卡 │ 打开时左栏自动收起 │
|
||||
│ 任务概览(任务深链) │ 底部:追加消息、附件、发送 │ │
|
||||
└───────────────────────────────────┴─────────────────────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
页面展示规则:
|
||||
|
||||
- 角色卡显示席位状态、当前轮次、模型和是否等待人工确认。
|
||||
- 对话区可按角色筛选,思考内容默认折叠,工具调用和交付物以步骤卡展示。
|
||||
- DAG 研讨可打开流程面板观察阶段、依赖和并行关系。
|
||||
- 打开沙箱/产物预览后,优先保证产物可读区域,左侧栏自动收起。
|
||||
|
||||
---
|
||||
|
||||
# 第三步:方案结果展示
|
||||
|
||||
## 1. 功能
|
||||
|
||||
第三步基于第一步意图、第二步席位交付和协调人共识,生成可阅读、可修改、可下载、可继续加工的最终方案。
|
||||
|
||||
- 首次进入常规会商第三步时,启动三个相互独立的产出流:
|
||||
- **方案总结报告**:生成 Markdown 方案,是默认主界面。
|
||||
- **经典可视化报告**:生成 HTML 风格报告/仪表盘。
|
||||
- **结构化仪表盘**:生成 `report-json`,由前端 `DashboardApp` 渲染。
|
||||
- 三条流以约 0 / 300 / 600 ms 错峰启动,减少瞬间请求峰值,但互不等待。
|
||||
- 已保存草稿再次打开时优先恢复既有结果,不重复生成;切换到新会商运行时会终止旧运行的活跃流。
|
||||
- Markdown 报告完成后,常规模式自动调用流程图提取,生成 `flow-json` 结构化流程图。
|
||||
- 用户可对报告提问、要求重写、修改流程图、重新生成流程图或报告。
|
||||
- 任务深链可根据业务链层次和业务编码生成任务 → 目的 → 行为体等结构;行动业务会使用行动名称作为根节点。
|
||||
- 若任务和技能条件满足,可选择“现在导入 / 稍后导入 / 不再导入”,把报告和流程结构交给 `roundtable-structure` 进行任务结构化入库。
|
||||
- 左侧汇集协调人共识、各席位交付和文件;右侧可预览 Markdown、流程图及沙箱产物。
|
||||
- 支持下载会商文档和智能体产物。任务模式还可将选定产物提交到任务交付接口。
|
||||
|
||||
## 2. 接口、调用时机与传参
|
||||
|
||||
### 2.1 初始化第三步专用智能体
|
||||
|
||||
| 目标 | 接口 | 返回 |
|
||||
| --- | --- | --- |
|
||||
| Markdown 方案总结 | `POST /api/multi-agent/summary/init` | `{ agent_name, thread_id }` |
|
||||
| 经典可视化报告 | `POST /api/multi-agent/report/init` | `{ agent_name, thread_id }` |
|
||||
| 结构化仪表盘 | `POST /api/multi-agent/dashboard/init` | `{ agent_name, thread_id }` |
|
||||
| 任务结构化入库 | `POST /api/multi-agent/structure/init` | `{ agent_name, thread_id }` |
|
||||
|
||||
初始化请求体均为:
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
各专用智能体初始化后,均继续通过统一接口 `POST /api/multi-agent/run/stream` 运行。
|
||||
|
||||
### 2.2 生成或修改 Markdown 方案总结
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-summary",
|
||||
"display_name": "方案总结报告",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-summary": "总结线程 ID"
|
||||
},
|
||||
"new_message": "包含结构化意图、席位交付、协调人共识、链条提示词的生成或修改指令",
|
||||
"model": "当前模型",
|
||||
"business_code": "3Q",
|
||||
"files": []
|
||||
}
|
||||
```
|
||||
|
||||
功能覆盖:
|
||||
|
||||
- 首次生成 Markdown 报告。
|
||||
- 在报告下方继续问答或下达修改指令。
|
||||
- 支持暂停、继续、重新生成和生成后下载 Docx。
|
||||
- 上述问答与修改沿用同一总结线程,保证上下文连续。
|
||||
|
||||
### 2.3 生成经典报告和仪表盘
|
||||
|
||||
经典报告请求使用 `roundtable-report`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-report",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-report": "报告线程 ID"
|
||||
},
|
||||
"new_message": "包含意图、席位交付、共识、主题模式、修订要求和已有 HTML 的提示词",
|
||||
"model": "当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
结构化仪表盘请求使用 `roundtable-dashboard`:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-dashboard",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-dashboard": "仪表盘线程 ID"
|
||||
},
|
||||
"new_message": "包含意图、席位交付、共识,或对已有仪表盘的修订要求",
|
||||
"model": "当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
前端依据返回的 HTML 或 `report-json` 分别渲染经典报告和 `DashboardApp`,并提供全屏、在新窗口打开、继续优化以及切回经典报告的操作。
|
||||
|
||||
### 2.4 自动提取和修改流程图
|
||||
|
||||
```http
|
||||
POST /api/flow-extract/stream
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"report_md": "Markdown 完整报告",
|
||||
"seat_deliveries": [
|
||||
{
|
||||
"sender": "态势专家",
|
||||
"content": "席位可见交付"
|
||||
}
|
||||
],
|
||||
"chain_levels": [["信息收集"], ["研判决策"]],
|
||||
"coordinator_prompt": "协调人对结构层次的要求",
|
||||
"business_code": "3Q",
|
||||
"root_label": "任务根节点名称",
|
||||
"model": "当前模型",
|
||||
"current_graph": {
|
||||
"nodes": [],
|
||||
"edges": []
|
||||
},
|
||||
"instruction": "用户对当前流程图的修改要求"
|
||||
}
|
||||
```
|
||||
|
||||
参数使用规则:
|
||||
|
||||
- 首次自动提取可不传 `current_graph` 和 `instruction`。
|
||||
- 修改现有图时传当前图和用户要求。
|
||||
- 任务模式的 `chain_levels` 由业务链阶段目标推导;若 `coordinator_prompt` 明确规定层次,则优先使用该要求。
|
||||
|
||||
流中包含提取增量、校验过程、模型切换和最终图数据,最终形成前端 `FlowGraphPanel` 使用的 `flow-json`。
|
||||
|
||||
### 2.5 判断用户在第三步的意图
|
||||
|
||||
| 接口 | 请求体 | 返回用途 |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/flow-extract/intent` | `{ message, has_report, has_flow, can_ingest, model? }` | 分类为 `regenerate_report`、`modify_flow`、`regenerate_flow`、`ingest`、`qa`、`unclear`,决定继续调用哪一条流。 |
|
||||
| `POST /api/flow-extract/ingest-intent` | `{ message, model? }` | 在已提示导入或稍后导入时,判断 `start_ingest`、`add_data`、`other`、`unclear`。 |
|
||||
|
||||
### 2.6 任务结构化入库
|
||||
|
||||
先检查是否可入库:
|
||||
|
||||
```http
|
||||
POST /api/flow-extract/ingest-eligibility
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "任务 ID",
|
||||
"business_code": "3Q",
|
||||
"chain_name": "业务链名称",
|
||||
"coordinator_prompt": "协调人提示词",
|
||||
"report_title": "报告标题",
|
||||
"model": "当前模型"
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"can_ingest": true,
|
||||
"skill": "命中的入库技能",
|
||||
"reason": "是否可入库的原因"
|
||||
}
|
||||
```
|
||||
|
||||
符合条件且用户确认后,初始化 `roundtable-structure` 线程,并调用统一研讨流:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_type": "special",
|
||||
"agent_name": "roundtable-structure",
|
||||
"d_agent_thread_id": {
|
||||
"roundtable-structure": "结构化入库线程 ID"
|
||||
},
|
||||
"new_message": "包含 taskId、业务编码/链条/技能提示、最新 Markdown 报告、flow-json 和用户补充说明的指令",
|
||||
"model": "当前模型",
|
||||
"business_code": "3Q"
|
||||
}
|
||||
```
|
||||
|
||||
注意:前端不会直接调用某个入库技能,而是交给 `roundtable-structure` 智能体在具备完整报告和流程图上下文后执行。
|
||||
|
||||
### 2.7 任务交付物提交
|
||||
|
||||
任务深链启用交付配置时,第三步可以查询和更新任务产物提交状态。
|
||||
|
||||
| 接口 | 调用方式与参数 |
|
||||
| --- | --- |
|
||||
| `GET /api/roundtable/tasks/{taskId}/artifacts?submitted=true\|false` | 获取任务已提交或待提交产物。 |
|
||||
| `PUT /api/roundtable/tasks/{taskId}/artifacts/submission` | 更新某个产物的提交状态。 |
|
||||
|
||||
```json
|
||||
{
|
||||
"taskId": "任务 ID",
|
||||
"submitted": true,
|
||||
"registerOnly": false,
|
||||
"submissionId": "已有提交记录 ID,可选",
|
||||
"taskDeliveryId": "外部交付 ID,可选",
|
||||
"expectedVersion": 3,
|
||||
"artifact": {
|
||||
"sessionId": "会商会话 ID",
|
||||
"threadId": "产物所属线程 ID",
|
||||
"nodeKey": "流程节点标识,可选",
|
||||
"sourceKey": "来源标识",
|
||||
"path": "/mnt/user-data/outputs/report.md",
|
||||
"name": "会商报告.md",
|
||||
"mimeType": "text/markdown",
|
||||
"revision": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 页面结构
|
||||
|
||||
默认的“方案总结”视图:
|
||||
|
||||
```text
|
||||
┌──────────────────────────────── 页面顶部 ────────────────────────────────┐
|
||||
│ 步骤导航 / 报告标题 / 模型 / 总结报告、流程图、最终方案等视图切换 │
|
||||
└───────────────────────────────────────────────────────────────────────┘
|
||||
┌──────────── 可收起左栏 ───────────┬──────────── 中间主内容 ─────────────┬──── 右侧结果区 ────┐
|
||||
│ 协调人共识 │ Markdown 总结报告对话流 │ Markdown 预览 │
|
||||
│ 各席位交付 │ 报告问答、修改、重生成入口 │ 或 FlowGraphPanel │
|
||||
│ 各线程文件/产物 │ 输入框与附件 │ 文件/沙箱预览 │
|
||||
│ │ │ │
|
||||
└───────────────────────────────────┴─────────────────────────────────────┴────────────────────┘
|
||||
```
|
||||
|
||||
其他结果视图:
|
||||
|
||||
| 视图 | 中央区域 | 右侧/扩展区域 |
|
||||
| --- | --- | --- |
|
||||
| 最终方案 | 汇总后的静态方案和文件清单 | 可查看来源与下载产物。 |
|
||||
| 经典可视化报告 | 报告生成对话、修订指令 | HTML/沙箱预览,支持全屏和下载。 |
|
||||
| 结构化仪表盘 | 仪表盘优化对话 | `DashboardApp` 或 HTML 视图,可新窗口打开。 |
|
||||
| 流程图 | 报告问答和流程图修改指令 | `FlowGraphPanel` 显示节点、关系和层级。 |
|
||||
|
||||
常规第三步会隐藏外层研讨左导航,以扩大结果阅读区域;任务深链保留任务环境相关的交付与跳转能力。
|
||||
|
||||
## 4. 三步数据衔接
|
||||
|
||||
```text
|
||||
第一步
|
||||
结构化意图 + 已选智能体/业务链 + 意图线程
|
||||
│
|
||||
▼
|
||||
第二步
|
||||
席位线程 + 协调人线程 + 对话/交付 + 共识 + 研讨运行快照
|
||||
│
|
||||
▼
|
||||
第三步
|
||||
Markdown 报告 + HTML 报告 + report-json 仪表盘 + flow-json 流程图 + 任务入库/交付
|
||||
```
|
||||
|
||||
关键约束:
|
||||
|
||||
- 第一步未形成有效意图,不能开始第二步。
|
||||
- 第二步未形成协调人共识,不能进入第三步。
|
||||
- 第三步的所有产出都应绑定对应的第二步研讨运行快照,避免新一轮会商覆盖历史结果。
|
||||
- 后端发生模型降级/切换时,前端应保留已收到的流内容并显示实际使用模型或切换提示。
|
||||
|
||||
## 5. 主要前端模块索引
|
||||
|
||||
| 范围 | 主要文件/目录 |
|
||||
| --- | --- |
|
||||
| 页面编排 | `src/roundtable-planning/pages/RoundtablePlanningPage.tsx` |
|
||||
| 第一步 | `components/Step1Panel.tsx`、`hooks/useStep1Intent.ts`、`api/intent.ts` |
|
||||
| 推荐与业务链 | `api/recommend.ts`、`components/RecommendAgentsDialog.tsx`、`api/chains.ts` |
|
||||
| 第二步 | `components/Step2Panel.tsx`、`components/Step2SandboxLayout.tsx`、`hooks/useStep2Orchestration.ts` |
|
||||
| 多智能体与后台任务 API | `api/multi-agent.ts`、`api/roundtable-jobs.ts` |
|
||||
| 第三步 | `components/Step3Panel.tsx`、`components/HighFidelityReport.tsx`、`hooks/useStep3Summary.ts`、`hooks/useStep3Report.ts`、`hooks/useStep3Dashboard.ts` |
|
||||
| 流程图与任务入库 | `api/flow-extract.ts`、`api/ingest.ts` |
|
||||
| 草稿持久化 | `api/drafts.ts`、`api/task-drafts.ts`、相关草稿 hooks |
|
||||
@@ -1,49 +0,0 @@
|
||||
# 麒麟系统 Chromium ~80 真机回归清单
|
||||
|
||||
> 背景:Tailwind v4 依赖 `@layer`(Chrome 99+)/`@property`(85+)/`color-mix`、`oklch`(111+),麒麟系统自带的 Chromium 80 多浏览器上整站样式失效。本分支(`feat/tailwind-v3-legacy-chrome`)完成 v4→v3 降级迁移,产物结构性指标已全部达标(详见提交 1607c7fe / 40b02520 / e54abcc6)。本文档供真机验收使用。
|
||||
|
||||
## 一、测试准备
|
||||
|
||||
1. 在麒麟机器的浏览器地址栏输入 `chrome://version`,记录完整版本号(重点关注是否 ≥84 —— 低于 84 时 flex `gap` 不支持,各处间距会塌陷,需单独反馈)。
|
||||
2. 部署本分支 `pnpm build` 产物(或开发机 `pnpm preview` 后用麒麟机器访问)。
|
||||
3. 先跑一次 `pnpm check:legacy-css`(开发机),确认绿色再发版。
|
||||
|
||||
## 二、页面巡检(明/暗两套主题各过一遍)
|
||||
|
||||
| 页面 | 路径 | 重点检查点 |
|
||||
|---|---|---|
|
||||
| 登录/落地页 | `/` | 布局完整、文字阴影(hero 标题)、无裸奔样式 |
|
||||
| 问答首页 | `/chats/new` | 整体配色、输入框焦点环(回退为内层 outline)、常用提示词栏 |
|
||||
| 对话流 | 任意会话 | Markdown 渲染(标题/列表/表格/代码块/引用——streamdown 白名单类)、气泡圆角、流式输出 |
|
||||
| 智能体 | `/page/workspace/agents` 及详情 | 卡片、Tabs、新建页表单、技能穿梭框 |
|
||||
| 圆桌会商 | roundtable 页 | 左侧步骤条、消息气泡、流程图面板、暗色兜底配色 |
|
||||
| 任务深链 | `?taskId=&goPath=3qfx/rwfx/xdfx` | 左侧任务卡、目的树、快捷跳转栏(按钮管理配置项) |
|
||||
| 轻应用/菜单管理/敏感词 | 各管理页 | Tabs、表格、弹窗 |
|
||||
| 简洁模式 | 外观设置切换 | 无全局导航栏布局、右侧工具箱、引导高亮框 |
|
||||
| 换肤 | 浅红/暗色/浅蓝/绿/冰蓝/浅黄 | 六套主题各自的 primary/ring 颜色、半透明档(primary/10 等) |
|
||||
| 大屏(dashboard-agent) | 战情大屏页 | 时间线、图表、暗色文字覆盖 |
|
||||
| iframe 嵌入 | `?embed=1&theme=dark-blue` | 去全局 chrome、强制主题生效 |
|
||||
|
||||
## 三、已知降级点(预期行为,非 bug)
|
||||
|
||||
1. **`:has()` 渐进增强**(Chrome <105 不生效):按钮内含图标时内边距略宽、alert 带图标时首列宽度、item 带描述时对齐微差 —— 视觉细节,不影响功能。
|
||||
2. **input-group 焦点环回退**:聊天输入框的焦点指示从「整框 3px 环」回退为「内层输入控件 2px outline」;校验态从整框环回退为内层红色阴影 —— 仍有明确指示。
|
||||
3. **`:where()` 圆桌字号缩放**(Chrome <88 不生效):设置页调字号时圆桌页面小字号文本不跟随缩放,基础字号正常。
|
||||
4. **textarea 自增高**:`field-sizing` 需要 Chrome 123+,旧浏览器走原生 `rows` 行为(已有多行默认值)。
|
||||
5. **prefers-color-scheme 单块**:产物中 1 处 lightningcss 内部标记(`--lightningcss-light/dark`),无影响。
|
||||
|
||||
## 四、重点风险项(出现问题按此排查)
|
||||
|
||||
- **整页无样式** → 确认产物是本分支构建(旧 v4 产物在 80 上必然裸奔);查 `@layer`。
|
||||
- **暗色主题失效** → 查产物 `.dark .dark\:` 前缀选择器是否存在(`pnpm check:legacy-css` 会拦)。
|
||||
- **弹层错位(下拉/气泡/选择器)** → radix 弹层依赖 `origin-[var(--radix-...)]`,确认类已生成。
|
||||
- **控制台语法错误/白屏** → JS 侧:确认构建 `target: chrome80` 且 `polyfills.ts` 在 main chunk 首位;记录报错栈反馈。
|
||||
- **mermaid 流程图 / 地图(maplibre)**:重型库在 80 上的运行时差异,出现异常截图 + 控制台报错反馈,单独处理。
|
||||
|
||||
## 五、发版前自检(开发机)
|
||||
|
||||
```bash
|
||||
pnpm build # 构建绿
|
||||
pnpm check:legacy-css # 产物守卫:@layer/@property/:is() 为 0,color-mix 全部守护
|
||||
pnpm typecheck # 58 个存量错误 = master 基线(迁移零新增)
|
||||
```
|
||||
@@ -1,139 +1,135 @@
|
||||
{
|
||||
"name": "deer-flow-frontend-vite",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
|
||||
"build:sit": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode sit",
|
||||
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
|
||||
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:input-mode": "node --experimental-strip-types --test src/components/workspace/input-mode.test.ts",
|
||||
"test:answer-feedback": "node --experimental-strip-types --test src/components/workspace/agents/agent-feedback-visibility.test.ts src/components/workspace/messages/answer-feedback-eligibility.test.ts src/strategy-components/api/answer-feedback-batching.test.ts",
|
||||
"test:retrieval-debug": "node --experimental-strip-types --test src/core/llmwiki/retrieval-debug.test.ts",
|
||||
"test:html-preview": "pnpm exec esbuild src/core/utils/legacy-html-preview.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/legacy-html-preview.test.mjs && node --test .runtime/unit-tests/legacy-html-preview.test.mjs",
|
||||
"test:visualizations": "node --experimental-strip-types --test src/components/workspace/visualizations/visualization-spec.test.ts",
|
||||
"test:report-collaboration": "npx esbuild src/report-collaboration/tests/event-reducer.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/event-reducer.test.mjs && npx esbuild src/report-collaboration/tests/message-adapter.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/message-adapter.test.mjs && node --test .runtime/unit-tests/event-reducer.test.mjs .runtime/unit-tests/message-adapter.test.mjs",
|
||||
"check:legacy-css": "node scripts/check-legacy-css.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/noto-sans-sc": "^5.2.9",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph-sdk": "^1.5.3",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.18",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "1.3.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"@tiptap/core": "^3.23.4",
|
||||
"@tiptap/extension-bubble-menu": "^3.23.4",
|
||||
"@tiptap/extension-placeholder": "^3.23.4",
|
||||
"@tiptap/extension-underline": "^3.23.4",
|
||||
"@tiptap/pm": "^3.23.4",
|
||||
"@tiptap/react": "^3.23.4",
|
||||
"@tiptap/starter-kit": "^3.23.4",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@uiw/codemirror-theme-basic": "^4.25.4",
|
||||
"@uiw/codemirror-theme-monokai": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "^6.0.33",
|
||||
"axios": "1.16.1",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"core-js": "3.45.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dnd-kit": "^0.0.2",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"katex": "^0.16.28",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^6.9.0",
|
||||
"marked": "16.4.2",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shiki": "3.15.0",
|
||||
"sonner": "^2.0.7",
|
||||
"streamdown": "1.4.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tdesign-react": "^1.12.3",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/gsap": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"lightningcss": "^1.30.2",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-source-identifier": "^1.1.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@radix-ui/react-slot": "1.3.3"
|
||||
},
|
||||
"browserslist": [
|
||||
"Chrome >= 80"
|
||||
],
|
||||
"packageManager": "pnpm@10.26.2"
|
||||
}
|
||||
{
|
||||
"name": "deer-flow-frontend-vite",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build",
|
||||
"build:sit": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode sit",
|
||||
"build:test": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode test",
|
||||
"build:network": "node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --mode network",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:input-mode": "node --experimental-strip-types --test src/components/workspace/input-mode.test.ts",
|
||||
"test:answer-feedback": "node --experimental-strip-types --test src/components/workspace/agents/agent-feedback-visibility.test.ts src/components/workspace/messages/answer-feedback-eligibility.test.ts src/strategy-components/api/answer-feedback-batching.test.ts",
|
||||
"test:retrieval-debug": "node --experimental-strip-types --test src/core/llmwiki/retrieval-debug.test.ts",
|
||||
"test:html-preview": "pnpm exec esbuild src/core/utils/legacy-html-preview.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/legacy-html-preview.test.mjs && node --test .runtime/unit-tests/legacy-html-preview.test.mjs",
|
||||
"test:visualizations": "node --experimental-strip-types --test src/components/workspace/visualizations/visualization-spec.test.ts",
|
||||
"test:report-collaboration": "npx esbuild src/report-collaboration/tests/event-reducer.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/event-reducer.test.mjs && npx esbuild src/report-collaboration/tests/message-adapter.test.ts --bundle --platform=node --format=esm --outfile=.runtime/unit-tests/message-adapter.test.mjs && node --test .runtime/unit-tests/event-reducer.test.mjs .runtime/unit-tests/message-adapter.test.mjs",
|
||||
"check:chrome109-css": "node scripts/check-chrome109-css.mjs",
|
||||
"check:legacy-chrome-css": "node scripts/check-chrome109-css.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.11",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.0",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/language-data": "^6.5.2",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fontsource/noto-sans-sc": "^5.2.9",
|
||||
"@langchain/core": "^1.1.15",
|
||||
"@langchain/langgraph-sdk": "^1.5.3",
|
||||
"@radix-ui/react-avatar": "^1.1.11",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-scroll-area": "1.2.18",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "1.3.3",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toggle": "^1.1.10",
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
||||
"@tanstack/react-query": "^5.90.17",
|
||||
"@tiptap/core": "^3.23.4",
|
||||
"@tiptap/extension-bubble-menu": "^3.23.4",
|
||||
"@tiptap/extension-placeholder": "^3.23.4",
|
||||
"@tiptap/extension-underline": "^3.23.4",
|
||||
"@tiptap/pm": "^3.23.4",
|
||||
"@tiptap/react": "^3.23.4",
|
||||
"@tiptap/starter-kit": "^3.23.4",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@uiw/codemirror-theme-basic": "^4.25.4",
|
||||
"@uiw/codemirror-theme-monokai": "^4.25.4",
|
||||
"@uiw/react-codemirror": "^4.25.4",
|
||||
"@xyflow/react": "^12.10.0",
|
||||
"ai": "^6.0.33",
|
||||
"axios": "1.16.1",
|
||||
"best-effort-json-parser": "^1.2.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.21",
|
||||
"dnd-kit": "^0.0.2",
|
||||
"docx": "^9.5.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"katex": "^0.16.28",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"maplibre-gl": "^6.9.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "^12.26.2",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"ogl": "^1.0.11",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable-panels": "^4.4.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"shiki": "3.15.0",
|
||||
"sonner": "^2.0.7",
|
||||
"streamdown": "1.4.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tdesign-react": "^1.12.3",
|
||||
"tiptap-markdown": "^0.9.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-stick-to-bottom": "^1.1.1",
|
||||
"uuid": "^14.0.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/gsap": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lightningcss": "^1.30.2",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.0.15",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-source-identifier": "^1.1.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@radix-ui/react-slot": "1.3.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.26.2"
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import tailwindcss from "tailwindcss";
|
||||
import darkDescendant from "./scripts/postcss-dark-descendant.mjs";
|
||||
import autoprefixer from "autoprefixer";
|
||||
import tailwindcss from "@tailwindcss/postcss";
|
||||
import legacyColorFallback from "./scripts/postcss-legacy-color-fallback.mjs";
|
||||
import lightningCssLegacyChrome from "./scripts/postcss-lightningcss-chrome109.mjs";
|
||||
|
||||
// Tailwind v3:透明度修饰符(bg-primary/10 等)由 tailwind.config.ts 里的
|
||||
// --legacy-*-rgb 通道变量直接实现,不再需要 v4 时代的 color-mix 回退插件;
|
||||
// dark-descendant 把暗色变体的 :is(.dark *) 改写为 Chrome 80 可读的后代形式;
|
||||
// dev 环境面向开发者的现代 Chrome,产物侧的旧浏览器降级(:is()/逻辑属性/
|
||||
// 媒体查询 range 语法/厂商前缀)由 vite 的 lightningcss targets 负责。
|
||||
export default {
|
||||
plugins: [tailwindcss, darkDescendant, autoprefixer],
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
legacyColorFallback(),
|
||||
lightningCssLegacyChrome(),
|
||||
],
|
||||
};
|
||||
@@ -98,10 +98,10 @@ window.__CM_RUNTIME_CONFIG__ = {
|
||||
VITE_AIPI_MOPECHAT_URL: "https://ch1.b.uat.4.cn/likun_b1_mope",
|
||||
|
||||
// 工作室/空间后端接口:空间列表、空间详情、素材上传等使用。
|
||||
VITE_AIPI_BACKENP_URL: "https://knowledgetoolserver.b.uat.4.cn",
|
||||
VITE_AIPI_BACKENP_URL: "http://47.88.25.99:8000",
|
||||
|
||||
// 工作室/空间后端别名:兼容旧配置命名,通常与 VITE_AIPI_BACKENP_URL 保持一致。
|
||||
VITE_NOTEBOOK_API_BASE_URL: "https://knowledgetoolserver.b.uat.4.cn",
|
||||
VITE_NOTEBOOK_API_BASE_URL: "http://47.88.25.99:8000",
|
||||
|
||||
// 工作室/空间前端地址:用于 auth/callback、新建对话等跳转。
|
||||
VITE_AIPI_FRONTEND_URL: "http://47.88.25.99:3000",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import postcss from "postcss";
|
||||
|
||||
const unsupportedColor = /(?:color-mix|oklch|oklab|lab|lch)\(/i;
|
||||
const assetsDir = path.resolve("dist/assets");
|
||||
|
||||
if (!fs.existsSync(assetsDir)) {
|
||||
throw new Error("dist/assets 不存在,请先运行 pnpm build");
|
||||
}
|
||||
|
||||
const cssFiles = fs
|
||||
.readdirSync(assetsDir)
|
||||
.filter((file) => file.endsWith(".css"));
|
||||
|
||||
if (cssFiles.length === 0) {
|
||||
throw new Error("dist/assets 中没有 CSS 构建产物");
|
||||
}
|
||||
|
||||
const failures = [];
|
||||
let modernDeclarations = 0;
|
||||
|
||||
const requiredLegacyFallbacks = [
|
||||
{
|
||||
file: "src/components/ui/textarea.tsx",
|
||||
pattern: /rows=\{rows\s*\?\?\s*3\}/,
|
||||
description: "Textarea needs a native rows fallback",
|
||||
},
|
||||
{
|
||||
file: "src/components/ai-elements/prompt-input.tsx",
|
||||
pattern: /<InputGroup className="h-auto min-h-16">/,
|
||||
description: "Prompt textarea must not rely on :has() for its parent height",
|
||||
},
|
||||
{
|
||||
file: "src/core/utils/mermaid.ts",
|
||||
pattern: /htmlLabels:\s*false[\s\S]*secure:\s*\[[\s\S]*"htmlLabels"/,
|
||||
description: "Sandbox flowcharts must use native SVG labels in legacy browsers",
|
||||
},
|
||||
{
|
||||
file: "src/core/utils/legacy-html-preview.ts",
|
||||
pattern: /export function downlevelModernColors[\s\S]*function injectLegacyChartShim/,
|
||||
description: "Generated HTML charts must downlevel modern CSS colors for Chrome 109",
|
||||
},
|
||||
];
|
||||
|
||||
for (const file of cssFiles) {
|
||||
const root = postcss.parse(
|
||||
fs.readFileSync(path.join(assetsDir, file), "utf8"),
|
||||
{ from: file },
|
||||
);
|
||||
|
||||
root.walkDecls((declaration) => {
|
||||
if (!unsupportedColor.test(declaration.value)) return;
|
||||
modernDeclarations += 1;
|
||||
|
||||
let ancestor = declaration.parent;
|
||||
while (ancestor) {
|
||||
if (
|
||||
ancestor.type === "atrule" &&
|
||||
ancestor.name === "supports" &&
|
||||
unsupportedColor.test(ancestor.params)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
|
||||
let previous = declaration.prev();
|
||||
while (previous?.type === "comment") previous = previous.prev();
|
||||
const hasSafeFallback =
|
||||
!declaration.prop.startsWith("--") &&
|
||||
previous?.type === "decl" &&
|
||||
previous.prop === declaration.prop &&
|
||||
!unsupportedColor.test(previous.value);
|
||||
|
||||
if (!hasSafeFallback) {
|
||||
failures.push(
|
||||
`${file}:${declaration.source?.start?.line ?? "?"} ${declaration.prop}: ${declaration.value}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function walk(directory) {
|
||||
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const fullPath = path.join(directory, entry.name);
|
||||
return entry.isDirectory() ? walk(fullPath) : [fullPath];
|
||||
});
|
||||
}
|
||||
|
||||
for (const file of walk(path.resolve("src"))) {
|
||||
if (!/\.[jt]sx?$/.test(file)) continue;
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
if (unsupportedColor.test(source)) {
|
||||
failures.push(
|
||||
`${path.relative(process.cwd(), file)} contains a runtime-only modern color function`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const fallback of requiredLegacyFallbacks) {
|
||||
const source = fs.readFileSync(path.resolve(fallback.file), "utf8");
|
||||
if (!fallback.pattern.test(source)) {
|
||||
failures.push(`${fallback.file}: ${fallback.description}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("Legacy Chrome CSS compatibility check failed:\n");
|
||||
console.error(failures.slice(0, 50).join("\n"));
|
||||
if (failures.length > 50) {
|
||||
console.error(`... and ${failures.length - 50} more`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Legacy Chrome (100+) CSS compatibility check passed (${modernDeclarations} guarded modern declarations).`,
|
||||
);
|
||||
@@ -1,72 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import postcss from "postcss";
|
||||
|
||||
/**
|
||||
* 旧浏览器(麒麟 Chromium ~80)CSS 产物守卫:构建后运行,防止回归。
|
||||
* 迁移后的结构性保证(见 tailwind.config.ts / postcss.config.js):
|
||||
* - Tailwind v3 输出无 @layer(Chrome 99+)与 @property(Chrome 85+);
|
||||
* - dark: 变体经 postcss-dark-descendant 改写为 .dark 后代形式,全库 :is( 为 0;
|
||||
* - 媒体查询 range 语法(Chrome 104+)由 lightningcss 按 target 转译;
|
||||
* - 手写 CSS 不使用 :is()(复杂参数无法安全降级,源头展开为逗号选择器);
|
||||
* - color-mix()/oklch() 仅允许出现在 @supports 守护内或带前置回退声明。
|
||||
* :has()(Chrome 105+)与 :where()(Chrome 88+)按渐进增强放行,不在检查范围。
|
||||
*/
|
||||
const unsupportedColor = /(?:color-mix|oklch|oklab|lab|lch)\(/i;
|
||||
const assetsDir = path.resolve("dist/assets");
|
||||
|
||||
if (!fs.existsSync(assetsDir)) {
|
||||
throw new Error("dist/assets 不存在,请先运行 pnpm build");
|
||||
}
|
||||
|
||||
const cssFiles = fs.readdirSync(assetsDir).filter((f) => f.endsWith(".css"));
|
||||
if (cssFiles.length === 0) throw new Error("dist/assets 中没有 CSS 构建产物");
|
||||
|
||||
const failures = [];
|
||||
const hardTokens = [
|
||||
[/@layer\b/, "@layer(Chrome 99+)"],
|
||||
[/@property\b/, "@property(Chrome 85+)"],
|
||||
[/:is\(/, ":is()(Chrome 88+)"],
|
||||
[/\(width\s*>=|\(width\s*<=|\(height\s*>=/, "媒体查询 range 语法(Chrome 104+)"],
|
||||
];
|
||||
let guardedColors = 0;
|
||||
|
||||
for (const file of cssFiles) {
|
||||
const raw = fs.readFileSync(path.join(assetsDir, file), "utf8");
|
||||
for (const [re, label] of hardTokens) {
|
||||
const hits = raw.match(new RegExp(re.source, "g")) || [];
|
||||
if (hits.length > 0) failures.push(`${file}: ${hits.length} 处 ${label}`);
|
||||
}
|
||||
|
||||
const root = postcss.parse(raw, { from: file });
|
||||
root.walkDecls((decl) => {
|
||||
if (!unsupportedColor.test(decl.value)) return;
|
||||
guardedColors += 1;
|
||||
let anc = decl.parent;
|
||||
while (anc) {
|
||||
if (anc.type === "atrule" && anc.name === "supports" && unsupportedColor.test(anc.params)) return;
|
||||
anc = anc.parent;
|
||||
}
|
||||
let prev = decl.prev();
|
||||
while (prev?.type === "comment") prev = prev.prev();
|
||||
const hasFallback =
|
||||
!decl.prop.startsWith("--") &&
|
||||
prev?.type === "decl" &&
|
||||
prev.prop === decl.prop &&
|
||||
!unsupportedColor.test(prev.value);
|
||||
if (!hasFallback) {
|
||||
failures.push(`${file}: 未守护的现代颜色 ${decl.prop}: ${decl.value.slice(0, 60)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("旧浏览器 CSS 兼容检查失败:\n");
|
||||
console.error(failures.slice(0, 50).join("\n"));
|
||||
if (failures.length > 50) console.error(`... 及另外 ${failures.length - 50} 处`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`旧浏览器(麒麟 Chromium ~80)CSS 兼容检查通过(${guardedColors} 处守护的现代颜色声明)。`,
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const { build } = await import(pathToFileURL(require.resolve("esbuild", {
|
||||
paths: [dirname(require.resolve("vite"))],
|
||||
})).href);
|
||||
const output = join(root, ".runtime", "ui-boundary-check");
|
||||
mkdirSync(output, { recursive: true });
|
||||
await build({
|
||||
absWorkingDir: root,
|
||||
entryPoints: ["src/components/react-ui-error-boundary.browser-check.tsx"],
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
format: "iife",
|
||||
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
||||
outfile: join(output, "check.js"),
|
||||
});
|
||||
const browser = process.env.CHROME_PATH || (process.platform === "win32"
|
||||
? "C:/Program Files/Google/Chrome/Application/chrome.exe"
|
||||
: "/usr/bin/chromium");
|
||||
const response = spawnSync(browser, [
|
||||
"--headless=new", "--disable-gpu", "--disable-background-networking", "--no-first-run",
|
||||
`--user-data-dir=${mkdtempSync(join(output, "browser-profile-"))}`,
|
||||
"--virtual-time-budget=3000", "--dump-dom",
|
||||
pathToFileURL(join(root, "scripts", "react-ui-error-boundary-check.html")).href,
|
||||
], { encoding: "utf8", windowsHide: true, timeout: 30_000 });
|
||||
if (response.error) throw response.error;
|
||||
const payload = response.stdout?.match(/<pre id="boundary-check-result">([^<]+)<\/pre>/)?.[1];
|
||||
if (!payload || response.status !== 0) {
|
||||
throw new Error(`Browser check did not finish: exit=${response.status}; ${response.stderr?.slice(-500)}`);
|
||||
}
|
||||
const result = JSON.parse(payload);
|
||||
if (result.passed !== true) throw new Error(`Browser check failed: ${payload}`);
|
||||
console.log("React production boundary browser check:", result);
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* 把 Tailwind v3.4 class 模式暗色变体输出的 `X:is(.dark *)` 机械改写为
|
||||
* `.dark X` 后代选择器,供麒麟 Chromium ~80 使用(无 :is())。
|
||||
*
|
||||
* - 特异性不变:`:is(.dark *)` 取最高参数 = (0,1,0),剥离后前置 `.dark `
|
||||
* 同样 +(0,1,0),两种形式均为 (0,2,0),级联行为一致。
|
||||
* - `.dark` 挂在 <html> 上、是全局祖先,前置到组合器最左侧语义等价
|
||||
* (group-hover:dark:* 等组合下同样成立)。
|
||||
* - 堆叠变体(dark:marker:* 等)生成的 `X *:is(.dark *)::marker` 会被
|
||||
* 改写为 `.dark X *::marker`,保持合法。
|
||||
*/
|
||||
const TOKEN = ":is(.dark *)";
|
||||
|
||||
export default function darkDescendant() {
|
||||
return {
|
||||
postcssPlugin: "postcss-dark-descendant",
|
||||
Rule(rule) {
|
||||
if (!rule.selector.includes(TOKEN)) return;
|
||||
rule.selector = rule.selector
|
||||
.split(",")
|
||||
.map((sel) => {
|
||||
const trimmed = sel.trim();
|
||||
if (!trimmed.includes(TOKEN)) return trimmed;
|
||||
return ".dark " + trimmed.split(TOKEN).join("");
|
||||
})
|
||||
.join(", ");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
darkDescendant.postcss = true;
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Tailwind 4 gracefully falls back from color-mix() to the base semantic
|
||||
* color, but that turns utilities such as bg-primary/5 into an opaque fill on
|
||||
* Chrome 109. Replace that fallback with an sRGB-channel based alpha color.
|
||||
* The modern color-mix() declaration remains in its @supports block.
|
||||
*/
|
||||
|
||||
const LEGACY_RGB_TOKENS = new Set([
|
||||
"accent",
|
||||
"background",
|
||||
"border",
|
||||
"brand-200",
|
||||
"card",
|
||||
"destructive",
|
||||
"foreground",
|
||||
"input",
|
||||
"muted",
|
||||
"muted-foreground",
|
||||
"primary",
|
||||
"primary-foreground",
|
||||
"ring",
|
||||
"secondary",
|
||||
"sidebar-accent",
|
||||
"sidebar-border",
|
||||
"sidebar-foreground",
|
||||
"workspace-brand",
|
||||
]);
|
||||
|
||||
const TRANSPARENT_MIX_RE =
|
||||
/color-mix\(\s*in\s+[\w-]+\s*,\s*var\(--([\w-]+)\)\s+([\d.]+)%\s*,\s*transparent\s*\)/i;
|
||||
|
||||
function legacyAlphaColor(value) {
|
||||
const match = value.match(TRANSPARENT_MIX_RE);
|
||||
if (!match || !LEGACY_RGB_TOKENS.has(match[1])) return null;
|
||||
|
||||
const [, token, percentage] = match;
|
||||
return `rgb(var(--legacy-${token}-rgb) / ${percentage}%)`;
|
||||
}
|
||||
|
||||
export default function legacyColorFallback() {
|
||||
return {
|
||||
postcssPlugin: "postcss-legacy-color-fallback",
|
||||
OnceExit(root) {
|
||||
root.walkAtRules("supports", (atRule) => {
|
||||
if (!atRule.params.includes("color-mix")) return;
|
||||
|
||||
atRule.walkDecls((modernDeclaration) => {
|
||||
const fallbackValue = legacyAlphaColor(modernDeclaration.value);
|
||||
if (!fallbackValue) return;
|
||||
|
||||
// Tailwind emits the old-browser declaration immediately before
|
||||
// the nested @supports block, in the same utility/variant rule.
|
||||
let fallbackDeclaration = atRule.prev();
|
||||
while (fallbackDeclaration?.type === "comment") {
|
||||
fallbackDeclaration = fallbackDeclaration.prev();
|
||||
}
|
||||
|
||||
if (
|
||||
fallbackDeclaration?.type === "decl" &&
|
||||
fallbackDeclaration.prop === modernDeclaration.prop
|
||||
) {
|
||||
fallbackDeclaration.value = fallbackValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
legacyColorFallback.postcss = true;
|
||||
@@ -0,0 +1,33 @@
|
||||
import postcss from "postcss";
|
||||
import { transform } from "lightningcss";
|
||||
|
||||
const LEGACY_CHROME = 100 << 16;
|
||||
|
||||
/**
|
||||
* Keep Vite's PostCSS pipeline (required by Tailwind 4), then compile the
|
||||
* resulting CSS to Chrome 100-compatible syntax for both dev and production.
|
||||
*/
|
||||
export default function lightningCssLegacyChrome() {
|
||||
return {
|
||||
postcssPlugin: "postcss-lightningcss-legacy-chrome",
|
||||
OnceExit(root, { result }) {
|
||||
const filename = root.source?.input.file ?? result.opts.from ?? "style.css";
|
||||
const output = transform({
|
||||
filename,
|
||||
code: Buffer.from(root.toString()),
|
||||
targets: { chrome: LEGACY_CHROME },
|
||||
minify: false,
|
||||
});
|
||||
const compiled = postcss.parse(output.code.toString(), { from: filename });
|
||||
|
||||
root.removeAll();
|
||||
root.append(compiled.nodes);
|
||||
|
||||
for (const warning of output.warnings) {
|
||||
result.warn(warning.message, { plugin: "lightningcss" });
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
lightningCssLegacyChrome.postcss = true;
|
||||
@@ -1,8 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head><meta charset="utf-8"><title>Offline React UI boundary check</title></head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="../.runtime/ui-boundary-check/check.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:#fddadc;stroke:#f86c73;stroke-dasharray:4;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f5222d"/><stop offset="1" stop-color="#a8071a"/></linearGradient></defs><g transform="translate(0.5 -109.5)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 110)"/><g transform="translate(6.5 116.5)"><rect class="b" width="39" height="39"/><path class="c" d="M20.72,28.846a8.938,8.938,0,0,1-7.044-14.437c.01-.014.02-.025.029-.037s.026-.035.04-.051l.02-.025.017-.02.012-.014c.01-.012.021-.023.03-.037l.015-.018a.179.179,0,0,0,.021-.025l.011-.012a.139.139,0,0,1,.011-.012l.064-.078.026-.031A.221.221,0,0,0,14,14.021a8.936,8.936,0,0,1,12.315-1.082c.093.072.181.146.279.234,0,0,0,0,.008.006.023.021.048.041.07.063a.393.393,0,0,1,.037.033c.015.012.028.025.041.037l.016.016a.055.055,0,0,1,.013.01l.014.012.014.012a.043.043,0,0,0,.008.01l0,0a8.964,8.964,0,0,1,2.719,5.055l0,.014c0,.031.01.061.015.09l0,.016,0,.016c0,.021.007.045.011.066l0,.018v0a.04.04,0,0,1,0,.01l0,.016,0,.018,0,.016c0,.018,0,.035.006.053a.171.171,0,0,1,0,.031c.011.078.02.158.026.238l.008.07v.018c0,.016,0,.029,0,.045a.106.106,0,0,0,0,.018.108.108,0,0,1,0,.018.28.28,0,0,0,0,.035l0,.051v.006c0,.021,0,.045,0,.064s0,.02,0,.029a.064.064,0,0,1,0,.014v.014c0,.021,0,.041,0,.063v.014c0,.01,0,.02,0,.029v.018c0,.025,0,.049,0,.074s0,.033,0,.049a.131.131,0,0,0,0,.02V19.7c0,.016,0,.027,0,.043v.041c0,.041,0,.082,0,.123A8.947,8.947,0,0,1,20.72,28.846Zm-3.574-9.953a1.016,1.016,0,0,0,0,2.031H19.7v2.563a1.016,1.016,0,0,0,2.031,0V20.924h2.563a1.016,1.016,0,0,0,0-2.031H21.73V16.334a1.016,1.016,0,0,0-2.031,0v2.559Zm-6.872,9.547H2.726A2.731,2.731,0,0,1,0,25.709V18.166a2.728,2.728,0,0,1,2.726-2.725h7.548a2.736,2.736,0,0,1,1.336.35A10.015,10.015,0,0,0,12.954,26.2,2.727,2.727,0,0,1,10.273,28.439ZM2.726,17.877a.292.292,0,0,0-.289.289v7.543A.293.293,0,0,0,2.726,26h7.548a.292.292,0,0,0,.289-.295V18.166a.292.292,0,0,0-.289-.289ZM10.273,13H2.726A2.731,2.731,0,0,1,0,10.273V2.73A2.731,2.731,0,0,1,2.726,0h7.548A2.731,2.731,0,0,1,13,2.73v7.543A2.731,2.731,0,0,1,10.273,13Zm17.09-.559v0a9.929,9.929,0,0,0-3.091-1.881H25.71A.288.288,0,0,0,26,10.273V2.73a.288.288,0,0,0-.288-.289H18.166a.286.286,0,0,0-.289.289v7.543c0,.014,0,.027,0,.049a9.841,9.841,0,0,0-2.246.979,2.737,2.737,0,0,1-.2-1.027V2.73A2.734,2.734,0,0,1,18.166,0H25.71a2.733,2.733,0,0,1,2.729,2.73v7.543a2.709,2.709,0,0,1-1.075,2.17Z" transform="translate(5.281 5.278)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#ffcfd1;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f5222d"/><stop offset="1" stop-color="#a8071a"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18558 4829)"><rect class="b" width="42" height="42" transform="translate(-18553 -4708)"/><path class="c" d="M-9520.252-26774.373h-17.5a8.261,8.261,0,0,1-8.248-8.254v-8.746a8.261,8.261,0,0,1,8.248-8.256h7.75v-2.582a2.5,2.5,0,0,1-1.5-2.287,2.506,2.506,0,0,1,2.5-2.5,2.5,2.5,0,0,1,2.5,2.5,2.515,2.515,0,0,1-1.5,2.293v2.576h2.752a1.254,1.254,0,0,1,1.251,1.252,1.254,1.254,0,0,1-1.251,1.252h-12.5a5.758,5.758,0,0,0-5.752,5.752v8.746a5.758,5.758,0,0,0,5.752,5.752h17.5a5.758,5.758,0,0,0,5.752-5.752v-5.746a1.252,1.252,0,0,1,1.251-1.25,1.252,1.252,0,0,1,1.252,1.25v5.746A8.264,8.264,0,0,1-9520.252-26774.373Zm-2.624-6.25h-12.251a5.506,5.506,0,0,1-5.5-5.5,5.5,5.5,0,0,1,5.5-5.5h12.251a5.507,5.507,0,0,1,5.5,5.5A5.509,5.509,0,0,1-9522.876-26780.623Zm-.989-7.812a.992.992,0,0,0-.706.291l-1.312,1.313a1,1,0,0,0-.289.707.985.985,0,0,0,.289.705l1.313,1.313a1,1,0,0,0,.706.3,1,1,0,0,0,.706-.3.988.988,0,0,0,.293-.707.987.987,0,0,0-.293-.705l-.605-.605.605-.605a.988.988,0,0,0,.293-.707.988.988,0,0,0-.293-.707A.988.988,0,0,0-9523.865-26788.436Zm-10.388.561a1.751,1.751,0,0,0-1.749,1.75,1.75,1.75,0,0,0,1.749,1.748,1.75,1.75,0,0,0,1.749-1.748A1.751,1.751,0,0,0-9534.253-26787.875Zm19.262-3.531a.587.587,0,0,1-.552-.4l-1.5-4.568-4.555-1.447a.593.593,0,0,1-.4-.551.577.577,0,0,1,.4-.553l4.5-1.514,1.419-4.5a.59.59,0,0,1,.557-.412.609.609,0,0,1,.17.027.569.569,0,0,1,.377.377l1.5,4.568,4.7,1.668a.579.579,0,0,1,.384.559.59.59,0,0,1-.417.545l-4.6,1.293-1.426,4.5A.572.572,0,0,1-9514.991-26791.406Z" transform="translate(-9002.999 22102.623)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:#d4e9ff;stroke:#55a6fe;stroke-dasharray:4;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0.5 -109.5)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 110)"/><g transform="translate(6.5 116.5)"><rect class="b" width="39" height="39"/><path class="c" d="M20.72,28.846a8.938,8.938,0,0,1-7.044-14.437c.01-.014.02-.025.029-.037s.026-.035.04-.051l.02-.025.017-.02.012-.014c.01-.012.021-.023.03-.037l.015-.018a.179.179,0,0,0,.021-.025l.011-.012a.139.139,0,0,1,.011-.012l.064-.078.026-.031A.221.221,0,0,0,14,14.021a8.936,8.936,0,0,1,12.315-1.082c.093.072.181.146.279.234,0,0,0,0,.008.006.023.021.048.041.07.063a.393.393,0,0,1,.037.033c.015.012.028.025.041.037l.016.016a.055.055,0,0,1,.013.01l.014.012.014.012a.043.043,0,0,0,.008.01l0,0a8.964,8.964,0,0,1,2.719,5.055l0,.014c0,.031.01.061.015.09l0,.016,0,.016c0,.021.007.045.011.066l0,.018v0a.04.04,0,0,1,0,.01l0,.016,0,.018,0,.016c0,.018,0,.035.006.053a.171.171,0,0,1,0,.031c.011.078.02.158.026.238l.008.07v.018c0,.016,0,.029,0,.045a.106.106,0,0,0,0,.018.108.108,0,0,1,0,.018.28.28,0,0,0,0,.035l0,.051v.006c0,.021,0,.045,0,.064s0,.02,0,.029a.064.064,0,0,1,0,.014v.014c0,.021,0,.041,0,.063v.014c0,.01,0,.02,0,.029v.018c0,.025,0,.049,0,.074s0,.033,0,.049a.131.131,0,0,0,0,.02V19.7c0,.016,0,.027,0,.043v.041c0,.041,0,.082,0,.123A8.947,8.947,0,0,1,20.72,28.846Zm-3.574-9.953a1.016,1.016,0,0,0,0,2.031H19.7v2.563a1.016,1.016,0,0,0,2.031,0V20.924h2.563a1.016,1.016,0,0,0,0-2.031H21.73V16.334a1.016,1.016,0,0,0-2.031,0v2.559Zm-6.872,9.547H2.726A2.731,2.731,0,0,1,0,25.709V18.166a2.728,2.728,0,0,1,2.726-2.725h7.548a2.736,2.736,0,0,1,1.336.35A10.015,10.015,0,0,0,12.954,26.2,2.727,2.727,0,0,1,10.273,28.439ZM2.726,17.877a.292.292,0,0,0-.289.289v7.543A.293.293,0,0,0,2.726,26h7.548a.292.292,0,0,0,.289-.295V18.166a.292.292,0,0,0-.289-.289ZM10.273,13H2.726A2.731,2.731,0,0,1,0,10.273V2.73A2.731,2.731,0,0,1,2.726,0h7.548A2.731,2.731,0,0,1,13,2.73v7.543A2.731,2.731,0,0,1,10.273,13Zm17.09-.559v0a9.929,9.929,0,0,0-3.091-1.881H25.71A.288.288,0,0,0,26,10.273V2.73a.288.288,0,0,0-.288-.289H18.166a.286.286,0,0,0-.289.289v7.543c0,.014,0,.027,0,.049a9.841,9.841,0,0,0-2.246.979,2.737,2.737,0,0,1-.2-1.027V2.73A2.734,2.734,0,0,1,18.166,0H25.71a2.733,2.733,0,0,1,2.729,2.73v7.543a2.709,2.709,0,0,1-1.075,2.17Z" transform="translate(5.281 5.278)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#aad2ff;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(2.236 118.236)"><path class="b" d="M9204.336-22218.762a6.4,6.4,0,0,1-2.489-.691,6.242,6.242,0,0,1-2-1.615,6.363,6.363,0,0,1-1.206-2.283,6.357,6.357,0,0,1-.246-1.863c-.047-.033-.094-.066-.139-.1a6.4,6.4,0,0,1-2.272-3.4,6.418,6.418,0,0,1,.268-4.076,6.336,6.336,0,0,1,1.233-1.912,6.383,6.383,0,0,1-.4-1.973,6.41,6.41,0,0,1,.53-2.809,6.441,6.441,0,0,1,1.7-2.289,6.424,6.424,0,0,1,1.751-1.057,4.969,4.969,0,0,1,.343-1.289,4.965,4.965,0,0,1,1.334-1.859,5.038,5.038,0,0,1,2.016-1.082,5.037,5.037,0,0,1,2.289-.074,5.024,5.024,0,0,1,2.089.943,4.947,4.947,0,0,1,.871.861,5.147,5.147,0,0,1,.876-.861,5,5,0,0,1,2.083-.943,5.035,5.035,0,0,1,2.289.074,5.06,5.06,0,0,1,2.021,1.082,5.031,5.031,0,0,1,1.334,1.859,5.008,5.008,0,0,1,.343,1.289,6.424,6.424,0,0,1,1.751,1.057,6.441,6.441,0,0,1,1.7,2.289,6.405,6.405,0,0,1,.524,2.809,6.434,6.434,0,0,1-.4,1.973,6.315,6.315,0,0,1,1.234,1.912,6.418,6.418,0,0,1,.268,4.076,6.4,6.4,0,0,1-2.272,3.4l-.143.105a6.214,6.214,0,0,1-.248,1.859,6.26,6.26,0,0,1-1.2,2.283,6.271,6.271,0,0,1-2.01,1.615,6.354,6.354,0,0,1-2.485.691c-.14.006-.278.012-.418.012a6.213,6.213,0,0,1-2.139-.375,6.394,6.394,0,0,1-2.2-1.34,6.4,6.4,0,0,1-.915-1.066,6.415,6.415,0,0,1-.91,1.066,6.427,6.427,0,0,1-2.206,1.34,6.283,6.283,0,0,1-2.143.377C9204.609-22218.748,9204.473-22218.752,9204.336-22218.762Zm6.768-6.342a4.232,4.232,0,0,0,.346,1.652,4.123,4.123,0,0,0,.972,1.379,4.116,4.116,0,0,0,1.44.877,4.062,4.062,0,0,0,1.67.234,4.159,4.159,0,0,0,1.63-.447,4.159,4.159,0,0,0,1.313-1.061,4.227,4.227,0,0,0,.787-1.49c.02-.07.037-.139.054-.209a6.32,6.32,0,0,1-1.422.164,1.1,1.1,0,0,1-1.1-1.1,1.1,1.1,0,0,1,1.1-1.1,4.178,4.178,0,0,0,2.529-.859,4.138,4.138,0,0,0,1.485-2.221,4.216,4.216,0,0,0-.173-2.67,4.2,4.2,0,0,0-1.765-2.01,1.1,1.1,0,0,1-.4-1.5,1.1,1.1,0,0,1,1.063-.543,4.16,4.16,0,0,0,.1-.758,4.249,4.249,0,0,0-.346-1.838,4.156,4.156,0,0,0-1.117-1.5,4.078,4.078,0,0,0-.569-.408,4.928,4.928,0,0,1-.385.838,1.1,1.1,0,0,1-.955.553,1.08,1.08,0,0,1-.547-.15,1.093,1.093,0,0,1-.4-1.5,2.906,2.906,0,0,0,.374-1.229,1.022,1.022,0,0,1,.006-.145,2.841,2.841,0,0,0-.218-1.133,2.836,2.836,0,0,0-.754-1.051,2.794,2.794,0,0,0-1.139-.607,2.8,2.8,0,0,0-1.29-.045,2.782,2.782,0,0,0-1.173.535,2.84,2.84,0,0,0-.82.994,2.912,2.912,0,0,0-.3,1.262v5.254a4.426,4.426,0,0,0,.877,2.625,4.4,4.4,0,0,0,2.277,1.58,1.1,1.1,0,0,1,.748,1.361,1.1,1.1,0,0,1-1.367.748,6.6,6.6,0,0,1-2.535-1.41v6.928Zm-10.352,1.145a4.119,4.119,0,0,0,.787,1.49,4.148,4.148,0,0,0,1.313,1.061,4.105,4.105,0,0,0,1.624.447,4.076,4.076,0,0,0,1.676-.234,4.125,4.125,0,0,0,1.44-.877,4.208,4.208,0,0,0,.972-1.379,4.226,4.226,0,0,0,.34-1.652v-6.924a6.628,6.628,0,0,1-2.534,1.406,1.117,1.117,0,0,1-.308.045,1.1,1.1,0,0,1-1.055-.793,1.093,1.093,0,0,1,.748-1.361,4.393,4.393,0,0,0,2.272-1.58,4.354,4.354,0,0,0,.876-2.625v-5.254a2.906,2.906,0,0,0-.29-1.262,2.84,2.84,0,0,0-.82-.994,2.778,2.778,0,0,0-1.179-.535,2.8,2.8,0,0,0-1.29.045,2.832,2.832,0,0,0-1.139.607,2.821,2.821,0,0,0-.748,1.051,2.819,2.819,0,0,0-.212,1.271,2.821,2.821,0,0,0,.374,1.234,1.1,1.1,0,0,1-.4,1.5,1.1,1.1,0,0,1-1.5-.4,5.009,5.009,0,0,1-.386-.838,4.127,4.127,0,0,0-.568.408,4.156,4.156,0,0,0-1.117,1.5,4.249,4.249,0,0,0-.346,1.838,4.246,4.246,0,0,0,.1.758,1.106,1.106,0,0,1,1.064.543,1.1,1.1,0,0,1-.4,1.5,4.2,4.2,0,0,0-1.765,2.01,4.216,4.216,0,0,0-.173,2.67,4.138,4.138,0,0,0,1.485,2.221,4.178,4.178,0,0,0,2.529.859,1.1,1.1,0,0,1,1.1,1.1,1.1,1.1,0,0,1-1.1,1.1,6.32,6.32,0,0,1-1.422-.164C9200.714-22224.1,9200.732-22224.029,9200.752-22223.959Z" transform="translate(-9186.242 22256.754)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#aad2ff;}.b{opacity:0;}.b,.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18559 4825)"><rect class="b" width="42" height="42" transform="translate(-18554 -4704)"/><path class="c" d="M-9736.837-26950.139a1.008,1.008,0,0,1-.542-.889v-3.371h-.752a4.506,4.506,0,0,1-4.5-4.5v-2.629c-1.89-.08-3.366-2-3.366-4.367s1.477-4.287,3.366-4.369v-2.633a4.5,4.5,0,0,1,4.5-4.5h15.658v.129a3.676,3.676,0,0,0,.494,1.869h-16.152a2.5,2.5,0,0,0-2.5,2.5v14a2.5,2.5,0,0,0,2.5,2.5h1.741a1.006,1.006,0,0,1,1.009,1v2.432l4.544-3.244a.97.97,0,0,1,.576-.189h13.124a2.5,2.5,0,0,0,2.506-2.5v-9.441a3.524,3.524,0,0,0,1.118.189h.033a3.634,3.634,0,0,0,2.764-1.293,4.793,4.793,0,0,1,1.456,3.549,5.014,5.014,0,0,1-.982,3.041,3.23,3.23,0,0,1-2.391,1.334v2.621a4.507,4.507,0,0,1-4.5,4.5h-12.806l-5.858,4.184a1.021,1.021,0,0,1-.582.191A1.007,1.007,0,0,1-9736.837-26950.139Zm1.334-10.389a1.876,1.876,0,0,1-1.876-1.877v-7.869a1.876,1.876,0,0,1,1.876-1.877h15.739a1.876,1.876,0,0,1,1.876,1.877v7.869a1.876,1.876,0,0,1-1.876,1.877Zm10.931-5.811a1.752,1.752,0,0,0,1.754,1.746,1.751,1.751,0,0,0,1.747-1.746,1.754,1.754,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9724.572-26966.338Zm-9.617,0a1.752,1.752,0,0,0,1.748,1.746,1.751,1.751,0,0,0,1.747-1.746,1.755,1.755,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9734.189-26966.338Zm20.1-5.277-.453-1.551a4.433,4.433,0,0,0-2.946-3.006l-1.537-.467a.649.649,0,0,1-.447-.611.524.524,0,0,1,.021-.148.616.616,0,0,1,.217-.346.624.624,0,0,1,.217-.115l.284-.088,1.232-.365a4.417,4.417,0,0,0,2.96-3.02l.46-1.551a.625.625,0,0,1,.42-.434v.006a.538.538,0,0,1,.183-.033.574.574,0,0,1,.292.08.581.581,0,0,1,.3.381l.46,1.557a4.422,4.422,0,0,0,2.96,3.014l1.171.346.346.107a.647.647,0,0,1,.454.609.641.641,0,0,1-.447.611l-1.537.467a4.434,4.434,0,0,0-2.946,3.006l-.453,1.551a.631.631,0,0,1-.6.461A.636.636,0,0,1-9714.089-26971.615Zm-4.571-7.34-.183-.623a2.7,2.7,0,0,0-1.829-1.83l-.623-.189a.29.29,0,0,1-.2-.277.284.284,0,0,1,.2-.277l.61-.184a2.7,2.7,0,0,0,1.842-1.842l.183-.615a.32.32,0,0,1,.2-.2.327.327,0,0,1,.224.027.275.275,0,0,1,.143.17l.183.615a2.713,2.713,0,0,0,1.842,1.842l.609.184a.289.289,0,0,1,.21.277.3.3,0,0,1-.21.277l-.257.076-.366.113a2.725,2.725,0,0,0-1.828,1.83l-.183.623a.293.293,0,0,1-.278.2A.3.3,0,0,1-9718.66-26978.955Z" transform="translate(-8805.37 22282.029)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="52" height="52" viewBox="0 0 52 52"><defs><style>.a{fill:#aad2ff;}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#0077fb"/><stop offset="1" stop-color="#004bb5"/></linearGradient></defs><g transform="translate(0 -116.002)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18558 4829)"><rect class="b" width="42" height="42" transform="translate(-18553 -4708)"/><path class="c" d="M-9520.252-26774.373h-17.5a8.261,8.261,0,0,1-8.248-8.254v-8.746a8.261,8.261,0,0,1,8.248-8.256h7.75v-2.582a2.5,2.5,0,0,1-1.5-2.287,2.506,2.506,0,0,1,2.5-2.5,2.5,2.5,0,0,1,2.5,2.5,2.515,2.515,0,0,1-1.5,2.293v2.576h2.752a1.254,1.254,0,0,1,1.251,1.252,1.254,1.254,0,0,1-1.251,1.252h-12.5a5.758,5.758,0,0,0-5.752,5.752v8.746a5.758,5.758,0,0,0,5.752,5.752h17.5a5.758,5.758,0,0,0,5.752-5.752v-5.746a1.252,1.252,0,0,1,1.251-1.25,1.252,1.252,0,0,1,1.252,1.25v5.746A8.264,8.264,0,0,1-9520.252-26774.373Zm-2.624-6.25h-12.251a5.506,5.506,0,0,1-5.5-5.5,5.5,5.5,0,0,1,5.5-5.5h12.251a5.507,5.507,0,0,1,5.5,5.5A5.509,5.509,0,0,1-9522.876-26780.623Zm-.989-7.812a.992.992,0,0,0-.706.291l-1.312,1.313a1,1,0,0,0-.289.707.985.985,0,0,0,.289.705l1.313,1.313a1,1,0,0,0,.706.3,1,1,0,0,0,.706-.3.988.988,0,0,0,.293-.707.987.987,0,0,0-.293-.705l-.605-.605.605-.605a.988.988,0,0,0,.293-.707.988.988,0,0,0-.293-.707A.988.988,0,0,0-9523.865-26788.436Zm-10.388.561a1.751,1.751,0,0,0-1.749,1.75,1.75,1.75,0,0,0,1.749,1.748,1.75,1.75,0,0,0,1.749-1.748A1.751,1.751,0,0,0-9534.253-26787.875Zm19.262-3.531a.587.587,0,0,1-.552-.4l-1.5-4.568-4.555-1.447a.593.593,0,0,1-.4-.551.577.577,0,0,1,.4-.553l4.5-1.514,1.419-4.5a.59.59,0,0,1,.557-.412.609.609,0,0,1,.17.027.569.569,0,0,1,.377.377l1.5,4.568,4.7,1.668a.579.579,0,0,1,.384.559.59.59,0,0,1-.417.545l-4.6,1.293-1.426,4.5A.572.572,0,0,1-9514.991-26791.406Z" transform="translate(-9002.999 22102.623)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><image width="1920" height="1080" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="582" height="667" viewBox="0 0 582 667"><image width="582" height="667" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABIwAAAU2CAYAAAD3cC0IAAAgAElEQVR4nOy9B5fkynWteSKQWb6qze2+XuSltxIpihLlpTdv3F+eWWtmzfN6eiKfSFG0IkWK5LXty1cmImadiBPAQWQgE1VdWVXZtXcvNNIiAaQpxId99jGnU08QdNNlzrt+537CtS725qzfNW3gTd+vy9aN2/5X/A257Z+3gTIyWXW5b7LZ49eJaIOItoloU66vyVxPG4Xb1tXjeaqIaEREY7k+ktcYyfWxPKaSy2O5byS35W+3V+s6Us+3PbvFEVFNRBMimsrzfc9ya3nMVB4/kdvS80/lvrTMM7XcdP1MHleaTgq3pccfE9GheoxT66ov9036MdAcvfI76IZt4G3/QF7b9r/kC9/0921p63fOBd/2zze0GhrhfYIgCIIgSGQUeNHwJZ8qBXPyaU1A0aaCQuMMBK0V4NBadr0PBlkFe/J1yW+bB4z6IFGf3ABgVCtgNM1uO8uAUQkqaWh0VoBI+W0JRJ0INDpWr5NPdQa18mmiJoxjIAiCIAgCMIIgCIKgWy4jcGdHXEE83yKiXZmnaVsAUAJB6XK6f1OBHg2RrHLx6HnfZT3XcKfKrtsrNowtAkzzjqkSUNIOn7rgBKoLc31b6X5XAD9nAo+OZDoWqKTB0lE27cv1A3ErHcjjAI8gCIIg6JYKwAiCIAiCXh1VCtqU5qPMQTRWpWMJGKXpPMAoOYqgssw1HHNpMKSB0ZGa62lfQNFhBowOM/dRPp3R7LzG5wCCIAiCVl/IMIJWQsgwGiZkGK2mkGF0tXpFN49hzR0i2pNpV+Y7cvuO3LatwNCuAj+6nGusMoL6ytF02Rd086XL0frK0s6ycro0T6BpXyBSciAlwPRcbtPzFzIdv2qfDWQYXa1u+ygFGUbLETKMIGi44DCCIAiCoJupPNOn7/KWAkPnBUa3ndfeFlXKfXYR+QsAo+dy/1FPNtNJls80ve1vEgRBEATdNAEYQRAEQdDNUoI892V+V+DPPTW/o8DQtgJKfaVouiQNriDovDIKRtYDStI0IErlbgkiPVXzZ+r6k1fVlQRBEARBqyoAIwiCIAi6GlUCdTZ7WsmnPKA7A4DRnnIOrYJm2rn7GNbM15ysf7o9f2x+X7qlc79Xj1PP6ZXvPn+RjDEL3VhGzY3Jb1PPN214d+c58rzufSaGbZtuCHj+/KtUcittnOM1tfNoHjB6LtOxch6dKkfSqZojJwmCIAiClixkGEErIWQYDRMyjFZTyDC6Wl3D5lkBPA+I6DWZ35PLd+XyPQFE26oV/XpWiqbb04+vdScuVuoCNnWplbyPHb28XPfpum+6h9Wd21VXMYFLroFMERfVGjx53zzGy3JoDhBKXck0oCpJA5x5XdmMECIrj7cmBz2mbfMvt1kNgYx0hjOm0ynOpOvNPHWek+u2my1107+9EwWATrOStHQ9BW4/U0DpmQAlvvxYpkcCoNyA1700IcPoanXbRynIMFqOkGEEQcMFhxEEQRAEvZwS3NkoTJvKDbQIGN27IQN+pyavQIzrgBbftoT3LXzRYCgAowwQ1T4DQ9njvcxr9XppuU63oU+v79v18ep+t8hBpCAULQBGpOBOn5IDKQEgK9dNgkICkFr4ZGaAUYBEJoEiAUg0C4yCwycDSCP9+OY19fLb19f3JbCl75u3nS+r8UDYORkIjJ4JNDqZM50tcXsgCIIg6JUWHEbQSggOo2GCw2g1BYfR1eoSN29TIM8DcQfp+WsypfKxzQJM0uVoyxykDxEDmtAVy/vOvPY+QJ9Jut0loONjxy2eO309OYME4OQlZhoEqcfpcjN9P/WWm8Xnkde3qcctOLqRZy8sX9MlZTTv46NK0HSZWSo/S0uYX7bWQpu2bK19XgdAZSVsllpAlYDSyMqc1NxKlztjaGyIxgKexnJ/mjOAGt+AvKu6pyQtTcdSwvYsA0lPZM7XPxHQdHoZKwSH0dXqto9S4DBajuAwgqDhAjCCVkIARsMEYLSaAjC6Wp1j84zKFUpuoTRtCxzKQVEOjO5ew6C7Jl3C5Rt3UJ1dD/MEelKIsYvXNTCKt0v7dedngZHX89kSod4yrnl3Dnj+ZT/nMnSRg6r5WUuLH9e53YgLicGPKQGjCIhGUso2A4qsXJeJwVKVldAlV1K83TT3VVfgUCppKtBoHjBK7qQjgUyladB7B2B0tbrtoxQAo+UIwAiChgvACFoJARgNE4DRagrA6Go1cPMYEr0u0xtE9FBdf111KttQIdYbKrx644pBEf8xPxMn0KkAn7MAeRyd+ugMCnNy4XYGQ5OsRMx1SrrS5W5JWusYykvC2sfnynd5yatTeltmbjML7h+yjCVpyMGUn7nQc3/xttKl0vNk786UpZnmutHwR5xJ7fVuBlNTAmcNjS1DJENrDJWMoXUTgdK6gKY1AU9rpu3Kd5W/JrVyHZWcSClw+2OZPhKo9KFc/0SA0lwBGF2tbvsoBcBoOQIwgqDhAjCCVkIARsMEYLSaAjC6WqnN21JTchKtSwnZ3QXA6M4VrHTK5IlQxzc5P7WfDY2eeGlr7nycszNIAFIASc3tLriF0v1T3z1mLcKdgaDmtn+Vb5oWOpLmuJea6yZCplDCZiMQGhsbytnWEkBiUGTbeXAnGWoAUpPB1MlpMi2MWhAofpl6vgAY6Uyk5EI6VM4kAKMr1m0fpQAYLUcARhA0XABG0EoIwGiYAIxWUwBGVysT4RCXi71FRG/K/HW5/JqUle1mJWh6WrZzyAnwYUfQGc8dwyAX5z5Op45vd6EcZyJAKZWDdR1Bbai0I1WSprKGtNp3v20Db0r3mzn3QTdC+p3NM538zGVfuK19V0M2Uh6UbXQod+tQam4XIDS2NjiQ2JG0LqBpjSytW74eHUlrJgKntSsoaasF/hwJGEowiC/vq9K2jwQkfaCmxz4+7tUVgNGNEoDRcgRgBEHDBWAErYQAjIYJwGg1BWC0FNnMQbQluUPbJkKhh3OA0c4S1ys5hqa+zQJquoT5NjtoQhEUnSpwdOYEIPF1BkbsFspygzoAZ4Ez6LZ/9aDzqehC6nEphctNnpINzqN1AUMMitZMe50B0ro4ldYkjHvUuJFM40gaZY6kZWl/DjB65ON9h8p5pKcrbfG/FAEY3SgBGC1HAEYQNFwARtBKCMBomACMVlMARpeuHQFAb0k52dty+W3VuSwHSclBdOGt73ui/JVlqMO5KgyATpynE+/opAFCPjqGFDDS5Wd15hRKoMmpdvP5apS6epW6ckHQUJW61rUf876uc7H0TAdlNyVqRJ0ytQSIAjAKgInhUgRM62RpU24LWWEmlo9WC753F5VXziMNhA6lbI1L2d6Xcrb3BS59IIDpYKU/UQBGN0oARssRgBEEDReAEbQSAjAaJgCj1RSA0YW1KXBoW6YdKSV7IA6itwvA6N7LlJSVdo1kAOVt5YNzSLqHnUnZWHIKnSRoxMCIXLicSs+mqu28Lv/qK/267V8faDVULHfzs7eHkG4bM5PWAiCytCGla5tGgFFwJkVglBxJlQJQleoKN+r5zr6Maum6lgOj9yUH6RNxKSUX0oHMj1funboBuu2jFACj5QjACIKGC8AIWgkBGA0TgNFqCsDo3BoLFHqHiN4lot8TGPSuamW/qZxD2yrQ+lJk2gM9BkHH3tOxi46hExIQxNeljCx2J2sdQlMNlMgryBSdGukPc/IImdwVZOASglZLqXxNO5Dy7KQ4N00nt6oDglogNKI2I2kUYJINTqQNKW0Lc8uAydKmZJaNzOUPzk6V6+hYzZ9KSdvvZPqNmj8J5aY3WQBGN0oARssRgBEEDReAEbQSAjAaJgCj1RSAUa8q5RzSLqLXpOSsBIzuXuSF5pS1OIqun6mAnalv52fSyv5YlZgF95DvAqMzAUKmAHwoh0EQBKWvX2c+A5wYKIU8JAFGkovEJWx8edPa4EbapOhQSk6kUZaJlPKSSt//i8hL2dpvxXWkgdEjmQ4LDqT6RrztAEY3SgBGyxGAEQQNF4ARtBICMBomAKPVFIBRUdsCgRIUSvPU4n43g0jb0mHppZQ2XbqInTbuoVhGdiylZMlNlJxD08wpNPU6zNp3BoImyxdCrhAEzaqUl6Rzktq2/8qB1JSjtUDIynwcXEdc3taWtm0JZApQSTq4Wbq8QdxZBoQOZEot/XMH0vviWLpeARjdKAEYLUcARhA0XABG0EoIwGiYAIxWU7ccGBmBPztqfldKzkrA6HUpLzv3i+QSt8JUQR92DfGcW9qzKyg5ho4kmDrAo7p1E010dzK4hiDo2tTnRmq6tYkTaaNKkCiCo+hEogCNNqTF/1hcSCNK8+hCmvkuX/AI+lCgUQkYsfvouYClfTW/uoN1AKMbJQCj5QjACIKGC8AIWgkBGA0TgNFq6hYDoy1xDH2aiD4l8wSH7mcOoi2Zv9TamdaewLDnSGAQO4aOHF93dFLzbTGceqodQ7okjeJct7KHawiCrkdlNxI1nqQYpm06bfpHKQNJZSUxFFqrDG1J0PaWtbQlYGnLUJjGl5SF5AUGHSnn0ZFkHCWI9G8y/VrCta8mNBvA6EYJwGg5AjCCoOECMIJWQgBGwwRgtJq6JcBIdzFLE8OiNwvA6E3qyRQ5zyo3sEe5hzgnxKVOZTF76Ig8HblYdnbknJSfRUjUdCorhEzf9o8tBK2iOk4knYlkIugdhfDs2MKfS9Y2Kc63QtlaDNReoxYwjaQb27iUg3SBI2wn7fl/mwGj1LJ/P5suv4QNwOhGCcBoOQIwgqDhAjCCVkILine truncated
|
||||
|
Before Width: | Height: | Size: 608 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" width="1" height="1" patternTransform="matrix(-1, 0, 0, 1, 278, 0)" viewBox="0 0 139 765"><image preserveAspectRatio="none" width="139" height="765" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 50 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><image width="139" height="765" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="727" height="727" viewBox="0 0 727 727"><defs><style>.a{fill:url(#a);}.b{fill:url(#d);}.c,.d,.f{fill:none;}.c{stroke:rgba(32,194,255,0);stroke-width:8px;}.d{stroke:rgba(32,194,255,0.15);stroke-width:5px;stroke-dasharray:7 5;}.e{stroke:none;}.g{filter:url(#e);}.h{filter:url(#b);}</style><linearGradient id="a" x1="0.351" y1="0.016" x2="0.065" y2="0.243" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#1ec6fe"/><stop offset="1" stop-color="#3279ff"/></linearGradient><filter id="b" x="88.068" y="60.324" width="217.004" height="181.59" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="c"/><feFlood flood-color="#1a9cff" flood-opacity="0.8"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><linearGradient id="d" x1="0.025" y1="0.971" x2="0.971" y2="0.034" xlink:href="#a"/><filter id="e" x="424.041" y="484.359" width="215.309" height="181.689" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="7.5" result="f"/><feFlood flood-color="#1a9cff" flood-opacity="0.8"/><feComposite operator="in" in2="f"/><feComposite in="SourceGraphic"/></filter></defs><g transform="translate(4 4)"><g transform="translate(-126.998 -127.002)"><g class="h" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="a" d="M18115.566-21172.494q4.919-8.443,10.389-16.543a294.531,294.531,0,0,1,35.713-43.289,294.545,294.545,0,0,1,43.293-35.717,292.5,292.5,0,0,1,49.682-26.969,290.614,290.614,0,0,1,31.271-11.162l1.656,5.506a288.092,288.092,0,0,0-166.973,131.084Z" transform="translate(-18005 21389)"/></g><g class="g" transform="matrix(1, 0, 0, 1, 123, 123)"><path class="b" d="M18451.539-20750.967a288.209,288.209,0,0,0,165.279-131.172l5.029,2.92q-5.1,8.815-10.8,17.258a294.659,294.659,0,0,1-35.717,43.287,294.647,294.647,0,0,1-43.293,35.717,292.2,292.2,0,0,1-49.686,26.971,291.1,291.1,0,0,1-29.178,10.537Z" transform="translate(-18005 21389)"/></g><circle class="c" cx="359.5" cy="359.5" r="359.5" transform="translate(126.998 127.002)"/></g><g class="d" transform="translate(98 98)"><circle class="e" cx="261.5" cy="261.5" r="261.5"/><circle class="f" cx="261.5" cy="261.5" r="259"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="458" height="113.588" viewBox="0 0 458 113.588"><defs><style>.a{fill:url(#a);}.b{fill:#fff;}.c{filter:url(#d);}.d{filter:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f0feff"/><stop offset="1" stop-color="#c1e6ff"/></linearGradient><filter id="b" x="111.721" y="26.943" width="211.559" height="46.289" filterUnits="userSpaceOnUse"><feOffset dy="1" input="SourceAlpha"/><feGaussianBlur stdDeviation="3" result="c"/><feFlood flood-color="#00ffa2" flood-opacity="0.322"/><feComposite operator="in" in2="c"/><feComposite in="SourceGraphic"/></filter><filter id="d" x="111.721" y="26.943" width="211.559" height="46.289" filterUnits="userSpaceOnUse"><feOffset input="SourceAlpha"/><feGaussianBlur stdDeviation="2" result="e"/><feFlood flood-color="#fff" result="f"/><feComposite operator="out" in="SourceGraphic" in2="e"/><feComposite operator="in" in="f"/><feComposite operator="in" in2="SourceGraphic"/></filter></defs><g transform="translate(-731 -564.412)"><image width="458" height="113.588" transform="translate(731 564.412)" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 813 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M110.023,105.37H98.2a1.721,1.721,0,0,1-1.716-1.716v-1.918h10.189a1.721,1.721,0,0,0,1.716-1.716V72.8a1.721,1.721,0,0,0-1.716-1.716H98.2V67.437A3.444,3.444,0,0,0,94.76,64H87.83a3.444,3.444,0,0,0-3.437,3.437v3.652H75.625A1.721,1.721,0,0,0,73.91,72.8v27.21a1.721,1.721,0,0,0,1.716,1.716h10.49v1.918a1.721,1.721,0,0,1-1.716,1.716H72.3a1.721,1.721,0,0,0-1.716,1.716v10.1A1.721,1.721,0,0,0,72.3,118.9h37.724a1.721,1.721,0,0,0,1.716-1.716v-10.1A1.726,1.726,0,0,0,110.023,105.37Zm-8.406-19.1a3.437,3.437,0,1,1-3.437-3.437A3.436,3.436,0,0,1,101.616,86.266ZM87.83,67.437h6.924v3.646H87.83ZM81.121,86.266A3.437,3.437,0,1,1,84.558,89.7,3.436,3.436,0,0,1,81.121,86.266Zm32.48-7.089V93.355a1.716,1.716,0,0,1-3.431,0V79.177a1.716,1.716,0,0,1,3.431,0Zm5.122,3.548v6.881a1.716,1.716,0,0,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0ZM70.644,95.07a1.717,1.717,0,0,1-1.716-1.716V79.177a1.716,1.716,0,0,1,3.431,0V93.355A1.713,1.713,0,0,1,70.644,95.07ZM67.231,82.724v6.881a1.716,1.716,0,1,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0Zm0,0" transform="translate(-14540.154 22987.455)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,48 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="188" height="241" viewBox="0 0 188 241" fill="none">
|
||||
<defs>
|
||||
<radialGradient id="surface" cx="0" cy="0" r="1" gradientTransform="translate(63 27) rotate(61) scale(148 156)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset=".2" stop-color="#FAFDFF"/>
|
||||
<stop offset=".48" stop-color="#E4F2FF"/>
|
||||
<stop offset=".72" stop-color="#A7D4FF"/>
|
||||
<stop offset="1" stop-color="#3C9AF4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="inner-blue" cx="0" cy="0" r="1" gradientTransform="translate(94 101) rotate(90) scale(68 84)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#68B6FF" stop-opacity=".58"/>
|
||||
<stop offset="1" stop-color="#1688F7" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="lower-blue" x1="94" y1="103" x2="94" y2="165" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4EA7FA" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#188AF4" stop-opacity=".72"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="38" y1="10" x2="151" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF" stop-opacity=".98"/>
|
||||
<stop offset=".52" stop-color="#FFFFFF" stop-opacity=".86"/>
|
||||
<stop offset="1" stop-color="#C6E8FF" stop-opacity=".9"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-13" y="-13" width="214" height="214" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="4.8"/>
|
||||
</filter>
|
||||
<filter id="shadow" x="18" y="162" width="152" height="49" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="7"/>
|
||||
</filter>
|
||||
<clipPath id="clip">
|
||||
<circle cx="94" cy="82" r="81"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="94" cy="188" rx="65" ry="9" fill="#147BD2" fill-opacity=".23" filter="url(#shadow)"/>
|
||||
<circle cx="94" cy="82" r="82" fill="#5EBAFF" fill-opacity=".2" filter="url(#glow)"/>
|
||||
<circle cx="94" cy="82" r="81" fill="url(#surface)"/>
|
||||
<g clip-path="url(#clip)">
|
||||
<ellipse cx="94" cy="106" rx="80" ry="61" fill="url(#inner-blue)"/>
|
||||
<ellipse cx="94" cy="143" rx="91" ry="38" fill="url(#lower-blue)"/>
|
||||
<ellipse cx="94" cy="151" rx="76" ry="14" fill="#FFFFFF" fill-opacity=".11"/>
|
||||
<path d="M20 145C47 157 141 157 168 145" stroke="#FFFFFF" stroke-opacity=".28" stroke-width="1.2"/>
|
||||
<path d="M31 160C55 168 133 168 157 160" stroke="#7CC4FF" stroke-opacity=".35" stroke-width="1.1"/>
|
||||
<path d="M22 64C27 37 49 16 77 9" stroke="#FFFFFF" stroke-opacity=".42" stroke-width="8" stroke-linecap="round" filter="url(#glow)"/>
|
||||
</g>
|
||||
<circle cx="94" cy="82" r="81" stroke="url(#edge)" stroke-width="2"/>
|
||||
<circle cx="94" cy="82" r="76" stroke="#FFFFFF" stroke-opacity=".24"/>
|
||||
<path d="M19 70C24 39 49 13 81 5" stroke="#FFFFFF" stroke-opacity=".88" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.337" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(-204.426 -230.797)"><rect class="a" width="68" height="68" transform="translate(204.426 230.797)"/><g transform="translate(209.658 232.191)"><path class="b" d="M25.52,64.045c-1.725-.852-8.6-4.486-10.846-5.656-2.309-1.207-3.3-1.01-5.09.146L6.346,60.67,0,46.383s6.887-3.254,10.033-4.508C15,39.885,17.273,41.6,20.115,43.4c1.209.762,5.131,3.664,5.131,3.664a17.886,17.886,0,0,0,7.783,2.316c5.928,0,6.975.344,6.975,2.266,0,2.113-3.859,2.266-3.883,2.266H26.605c-3.236,0-7.766-1.893-7.766-1.893s3.846,4.156,7.766,4.156h9.547c2.105-.047,6.1-1.047,6.1-4.537,0-4.529-4.066-4.529-9.219-4.529a5.2,5.2,0,0,1-1.127-.145c.2-.1.4-.207.639-.35.9-.523,2.145-1.27,3.535-2.158,3.67-2.3,5.555-2.592,7.387-.152a5.718,5.718,0,0,1,4.248-1.678c1.551,0,2.865,1.039,2.494,2.719,3.975-2.377,5.936-1.42,6.809-.266,1.156,1.527.373,3.109-2.143,4.926C53.244,51.17,37.809,61.543,35.635,62.82c-1.732,1-3.326,2.395-5.8,2.395A9.825,9.825,0,0,1,25.52,64.045ZM22.51,39.5l-.229-.865a.706.706,0,0,1,.129-.607.688.688,0,0,1,.557-.275H35.734a.689.689,0,0,1,.547.275.7.7,0,0,1,.121.607l-.219.865a7.065,7.065,0,0,1-13.674,0Zm6.84,2.486a4.265,4.265,0,0,0,3.178-1.424H26.174A4.239,4.239,0,0,0,29.35,41.982Zm-6.285-5.07a.885.885,0,1,1,0-1.77H35.658a.885.885,0,0,1-.008,1.77Zm-.189-2.637a.7.7,0,0,1-.707-.7V31.021a.565.565,0,0,0-.244-.461,13.168,13.168,0,1,1,14.844,0,.563.563,0,0,0-.242.461v2.547a.7.7,0,0,1-.7.707Zm8.672-8.785a6.207,6.207,0,0,0,2.455-.3c.547-.258.721-.676.721-1.572V14.719H33.059v8.887c0,.219-.1.281-.334.3-.234,0-1.033,0-1.732-.039A6.217,6.217,0,0,1,31.547,25.49Zm-6.18-2.166c0,1.535.463,1.967,1.969,1.967h1.7c1.285,0,1.717-.516,1.877-2.174a4.544,4.544,0,0,1-1.443-.607c-.062,1.141-.145,1.361-.586,1.361H27.557c-.494,0-.578-.055-.578-.562V20.117h1.475a3.131,3.131,0,0,1-.213,1.223.374.374,0,0,1-.334.131,6.154,6.154,0,0,1-.836-.047,3.419,3.419,0,0,1,.41,1.4,8.144,8.144,0,0,0,1.3-.045,1.284,1.284,0,0,0,.844-.455c.281-.35.395-1.223.465-3.107l.016-.2.744-.707v4.154h1.619V15.793H30.848V18.17a28.18,28.18,0,0,0-2.326-2.758c.084-.158.152-.328.221-.486l-1.535-.479a9.09,9.09,0,0,1-3.215,3.9,7.919,7.919,0,0,1,1.2,1.27l.174-.129Zm.906-4.643a11.211,11.211,0,0,0,1.465-1.855c.518.609,1.049,1.27,1.469,1.855Zm17.676-2.123a1.606,1.606,0,0,1,1.186-1.943l2.363-.578a1.608,1.608,0,0,1,.76,3.125l-2.369.576a1.629,1.629,0,0,1-.383.047A1.611,1.611,0,0,1,43.949,16.559ZM12.631,17.631,10.389,16.7a1.608,1.608,0,0,1,1.23-2.971l2.242.941a1.6,1.6,0,1,1-1.23,2.963Zm4.088-9.514L15.328,6.125a1.605,1.605,0,0,1,2.637-1.832l1.391,1.99a1.6,1.6,0,0,1-.4,2.234,1.58,1.58,0,0,1-.916.289A1.6,1.6,0,0,1,16.719,8.117Zm22.709.32a1.614,1.614,0,0,1-.273-2.252l1.488-1.914A1.6,1.6,0,1,1,43.174,6.24L41.686,8.16a1.626,1.626,0,0,1-2.258.277ZM27.746,4.043V1.6a1.607,1.607,0,0,1,3.215,0V4.043a1.607,1.607,0,0,1-3.215,0Z" transform="translate(0 0)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.414" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#2bbaff"/><stop offset="0.498" stop-color="#1a8cfd"/><stop offset="1" stop-color="#006aef"/></linearGradient></defs><g transform="translate(-2.29 3.525)"><g class="a" transform="translate(2.29 -3.525)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M36.066,58.469a15.431,15.431,0,0,1,0-21.866,15.011,15.011,0,0,1,3.577-2.662,2.018,2.018,0,0,1,1.878,3.571,11.53,11.53,0,0,0-2.6,1.947A11.446,11.446,0,0,0,55.132,55.622a11.938,11.938,0,0,0,1.878-2.462A2.02,2.02,0,1,1,60.5,55.2,15.711,15.711,0,0,1,58,58.469a15.533,15.533,0,0,1-21.929,0Zm-5.87-.994H5.225A5.225,5.225,0,0,1,0,52.263V13.925A5.226,5.226,0,0,1,5.225,8.712H53.769A5.226,5.226,0,0,1,59,13.925V32.117A19.526,19.526,0,0,0,30.2,57.474v0ZM21.753,18.77l-8.5,7.4a2.531,2.531,0,0,0-.239,3.565,2.536,2.536,0,0,0,3.568.239l6.835-5.958L28.446,28.4a2.52,2.52,0,0,0,3.351-.015L42.282,19a2.508,2.508,0,0,0,.191-3.557,2.529,2.529,0,0,0-3.567-.192L30.085,23.14l-5-4.369a2.544,2.544,0,0,0-3.335,0ZM47.03,49.554a2.017,2.017,0,0,1-2.022-2.017V34.091a2.021,2.021,0,0,1,2.022-2.017A15.485,15.485,0,0,1,62.537,47.537a2.018,2.018,0,0,1-2.024,2.017Zm2.025-4.034h9.258a11.471,11.471,0,0,0-9.258-9.229ZM7.53,5.051a2.521,2.521,0,1,1,0-5.043H50.681a2.521,2.521,0,1,1,0,5.043Z" transform="translate(6.296 0.013)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:rgba(0,121,254,0.3);stroke:rgba(0,121,254,0.5);}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><g transform="translate(0.5 -115.502)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(2.236 118.236)"><path class="b" d="M9204.336-22218.762a6.4,6.4,0,0,1-2.489-.691,6.242,6.242,0,0,1-2-1.615,6.363,6.363,0,0,1-1.206-2.283,6.357,6.357,0,0,1-.246-1.863c-.047-.033-.094-.066-.139-.1a6.4,6.4,0,0,1-2.272-3.4,6.418,6.418,0,0,1,.268-4.076,6.336,6.336,0,0,1,1.233-1.912,6.383,6.383,0,0,1-.4-1.973,6.41,6.41,0,0,1,.53-2.809,6.441,6.441,0,0,1,1.7-2.289,6.424,6.424,0,0,1,1.751-1.057,4.969,4.969,0,0,1,.343-1.289,4.965,4.965,0,0,1,1.334-1.859,5.038,5.038,0,0,1,2.016-1.082,5.037,5.037,0,0,1,2.289-.074,5.024,5.024,0,0,1,2.089.943,4.947,4.947,0,0,1,.871.861,5.147,5.147,0,0,1,.876-.861,5,5,0,0,1,2.083-.943,5.035,5.035,0,0,1,2.289.074,5.06,5.06,0,0,1,2.021,1.082,5.031,5.031,0,0,1,1.334,1.859,5.008,5.008,0,0,1,.343,1.289,6.424,6.424,0,0,1,1.751,1.057,6.441,6.441,0,0,1,1.7,2.289,6.405,6.405,0,0,1,.524,2.809,6.434,6.434,0,0,1-.4,1.973,6.315,6.315,0,0,1,1.234,1.912,6.418,6.418,0,0,1,.268,4.076,6.4,6.4,0,0,1-2.272,3.4l-.143.105a6.214,6.214,0,0,1-.248,1.859,6.26,6.26,0,0,1-1.2,2.283,6.271,6.271,0,0,1-2.01,1.615,6.354,6.354,0,0,1-2.485.691c-.14.006-.278.012-.418.012a6.213,6.213,0,0,1-2.139-.375,6.394,6.394,0,0,1-2.2-1.34,6.4,6.4,0,0,1-.915-1.066,6.415,6.415,0,0,1-.91,1.066,6.427,6.427,0,0,1-2.206,1.34,6.283,6.283,0,0,1-2.143.377C9204.609-22218.748,9204.473-22218.752,9204.336-22218.762Zm6.768-6.342a4.232,4.232,0,0,0,.346,1.652,4.123,4.123,0,0,0,.972,1.379,4.116,4.116,0,0,0,1.44.877,4.062,4.062,0,0,0,1.67.234,4.159,4.159,0,0,0,1.63-.447,4.159,4.159,0,0,0,1.313-1.061,4.227,4.227,0,0,0,.787-1.49c.02-.07.037-.139.054-.209a6.32,6.32,0,0,1-1.422.164,1.1,1.1,0,0,1-1.1-1.1,1.1,1.1,0,0,1,1.1-1.1,4.178,4.178,0,0,0,2.529-.859,4.138,4.138,0,0,0,1.485-2.221,4.216,4.216,0,0,0-.173-2.67,4.2,4.2,0,0,0-1.765-2.01,1.1,1.1,0,0,1-.4-1.5,1.1,1.1,0,0,1,1.063-.543,4.16,4.16,0,0,0,.1-.758,4.249,4.249,0,0,0-.346-1.838,4.156,4.156,0,0,0-1.117-1.5,4.078,4.078,0,0,0-.569-.408,4.928,4.928,0,0,1-.385.838,1.1,1.1,0,0,1-.955.553,1.08,1.08,0,0,1-.547-.15,1.093,1.093,0,0,1-.4-1.5,2.906,2.906,0,0,0,.374-1.229,1.022,1.022,0,0,1,.006-.145,2.841,2.841,0,0,0-.218-1.133,2.836,2.836,0,0,0-.754-1.051,2.794,2.794,0,0,0-1.139-.607,2.8,2.8,0,0,0-1.29-.045,2.782,2.782,0,0,0-1.173.535,2.84,2.84,0,0,0-.82.994,2.912,2.912,0,0,0-.3,1.262v5.254a4.426,4.426,0,0,0,.877,2.625,4.4,4.4,0,0,0,2.277,1.58,1.1,1.1,0,0,1,.748,1.361,1.1,1.1,0,0,1-1.367.748,6.6,6.6,0,0,1-2.535-1.41v6.928Zm-10.352,1.145a4.119,4.119,0,0,0,.787,1.49,4.148,4.148,0,0,0,1.313,1.061,4.105,4.105,0,0,0,1.624.447,4.076,4.076,0,0,0,1.676-.234,4.125,4.125,0,0,0,1.44-.877,4.208,4.208,0,0,0,.972-1.379,4.226,4.226,0,0,0,.34-1.652v-6.924a6.628,6.628,0,0,1-2.534,1.406,1.117,1.117,0,0,1-.308.045,1.1,1.1,0,0,1-1.055-.793,1.093,1.093,0,0,1,.748-1.361,4.393,4.393,0,0,0,2.272-1.58,4.354,4.354,0,0,0,.876-2.625v-5.254a2.906,2.906,0,0,0-.29-1.262,2.84,2.84,0,0,0-.82-.994,2.778,2.778,0,0,0-1.179-.535,2.8,2.8,0,0,0-1.29.045,2.832,2.832,0,0,0-1.139.607,2.821,2.821,0,0,0-.748,1.051,2.819,2.819,0,0,0-.212,1.271,2.821,2.821,0,0,0,.374,1.234,1.1,1.1,0,0,1-.4,1.5,1.1,1.1,0,0,1-1.5-.4,5.009,5.009,0,0,1-.386-.838,4.127,4.127,0,0,0-.568.408,4.156,4.156,0,0,0-1.117,1.5,4.249,4.249,0,0,0-.346,1.838,4.246,4.246,0,0,0,.1.758,1.106,1.106,0,0,1,1.064.543,1.1,1.1,0,0,1-.4,1.5,4.2,4.2,0,0,0-1.765,2.01,4.216,4.216,0,0,0-.173,2.67,4.138,4.138,0,0,0,1.485,2.221,4.178,4.178,0,0,0,2.529.859,1.1,1.1,0,0,1,1.1,1.1,1.1,1.1,0,0,1-1.1,1.1,6.32,6.32,0,0,1-1.422-.164C9200.714-22224.1,9200.732-22224.029,9200.752-22223.959Z" transform="translate(-9186.242 22256.754)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:rgba(0,121,254,0.3);stroke:rgba(0,121,254,0.5);}.b{opacity:0;}.b,.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><g transform="translate(0.5 -115.502)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18559 4825)"><rect class="b" width="42" height="42" transform="translate(-18554 -4704)"/><path class="c" d="M-9736.837-26950.139a1.008,1.008,0,0,1-.542-.889v-3.371h-.752a4.506,4.506,0,0,1-4.5-4.5v-2.629c-1.89-.08-3.366-2-3.366-4.367s1.477-4.287,3.366-4.369v-2.633a4.5,4.5,0,0,1,4.5-4.5h15.658v.129a3.676,3.676,0,0,0,.494,1.869h-16.152a2.5,2.5,0,0,0-2.5,2.5v14a2.5,2.5,0,0,0,2.5,2.5h1.741a1.006,1.006,0,0,1,1.009,1v2.432l4.544-3.244a.97.97,0,0,1,.576-.189h13.124a2.5,2.5,0,0,0,2.506-2.5v-9.441a3.524,3.524,0,0,0,1.118.189h.033a3.634,3.634,0,0,0,2.764-1.293,4.793,4.793,0,0,1,1.456,3.549,5.014,5.014,0,0,1-.982,3.041,3.23,3.23,0,0,1-2.391,1.334v2.621a4.507,4.507,0,0,1-4.5,4.5h-12.806l-5.858,4.184a1.021,1.021,0,0,1-.582.191A1.007,1.007,0,0,1-9736.837-26950.139Zm1.334-10.389a1.876,1.876,0,0,1-1.876-1.877v-7.869a1.876,1.876,0,0,1,1.876-1.877h15.739a1.876,1.876,0,0,1,1.876,1.877v7.869a1.876,1.876,0,0,1-1.876,1.877Zm10.931-5.811a1.752,1.752,0,0,0,1.754,1.746,1.751,1.751,0,0,0,1.747-1.746,1.754,1.754,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9724.572-26966.338Zm-9.617,0a1.752,1.752,0,0,0,1.748,1.746,1.751,1.751,0,0,0,1.747-1.746,1.755,1.755,0,0,0-1.747-1.756A1.755,1.755,0,0,0-9734.189-26966.338Zm20.1-5.277-.453-1.551a4.433,4.433,0,0,0-2.946-3.006l-1.537-.467a.649.649,0,0,1-.447-.611.524.524,0,0,1,.021-.148.616.616,0,0,1,.217-.346.624.624,0,0,1,.217-.115l.284-.088,1.232-.365a4.417,4.417,0,0,0,2.96-3.02l.46-1.551a.625.625,0,0,1,.42-.434v.006a.538.538,0,0,1,.183-.033.574.574,0,0,1,.292.08.581.581,0,0,1,.3.381l.46,1.557a4.422,4.422,0,0,0,2.96,3.014l1.171.346.346.107a.647.647,0,0,1,.454.609.641.641,0,0,1-.447.611l-1.537.467a4.434,4.434,0,0,0-2.946,3.006l-.453,1.551a.631.631,0,0,1-.6.461A.636.636,0,0,1-9714.089-26971.615Zm-4.571-7.34-.183-.623a2.7,2.7,0,0,0-1.829-1.83l-.623-.189a.29.29,0,0,1-.2-.277.284.284,0,0,1,.2-.277l.61-.184a2.7,2.7,0,0,0,1.842-1.842l.183-.615a.32.32,0,0,1,.2-.2.327.327,0,0,1,.224.027.275.275,0,0,1,.143.17l.183.615a2.713,2.713,0,0,0,1.842,1.842l.609.184a.289.289,0,0,1,.21.277.3.3,0,0,1-.21.277l-.257.076-.366.113a2.725,2.725,0,0,0-1.828,1.83l-.183.623a.293.293,0,0,1-.278.2A.3.3,0,0,1-9718.66-26978.955Z" transform="translate(-8805.37 22282.029)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="53" height="53" viewBox="0 0 53 53"><defs><style>.a{fill:rgba(0,121,254,0.3);stroke:rgba(0,121,254,0.5);}.b{fill:#fff;opacity:0;}.c{fill:url(#a);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#92eeff"/><stop offset="1" stop-color="#00c7ff"/></linearGradient></defs><g transform="translate(0.5 -115.502)"><rect class="a" width="52" height="52" rx="8" transform="translate(0 116.002)"/><g transform="translate(18558 4829)"><rect class="b" width="42" height="42" transform="translate(-18553 -4708)"/><path class="c" d="M-9520.252-26774.373h-17.5a8.261,8.261,0,0,1-8.248-8.254v-8.746a8.261,8.261,0,0,1,8.248-8.256h7.75v-2.582a2.5,2.5,0,0,1-1.5-2.287,2.506,2.506,0,0,1,2.5-2.5,2.5,2.5,0,0,1,2.5,2.5,2.515,2.515,0,0,1-1.5,2.293v2.576h2.752a1.254,1.254,0,0,1,1.251,1.252,1.254,1.254,0,0,1-1.251,1.252h-12.5a5.758,5.758,0,0,0-5.752,5.752v8.746a5.758,5.758,0,0,0,5.752,5.752h17.5a5.758,5.758,0,0,0,5.752-5.752v-5.746a1.252,1.252,0,0,1,1.251-1.25,1.252,1.252,0,0,1,1.252,1.25v5.746A8.264,8.264,0,0,1-9520.252-26774.373Zm-2.624-6.25h-12.251a5.506,5.506,0,0,1-5.5-5.5,5.5,5.5,0,0,1,5.5-5.5h12.251a5.507,5.507,0,0,1,5.5,5.5A5.509,5.509,0,0,1-9522.876-26780.623Zm-.989-7.812a.992.992,0,0,0-.706.291l-1.312,1.313a1,1,0,0,0-.289.707.985.985,0,0,0,.289.705l1.313,1.313a1,1,0,0,0,.706.3,1,1,0,0,0,.706-.3.988.988,0,0,0,.293-.707.987.987,0,0,0-.293-.705l-.605-.605.605-.605a.988.988,0,0,0,.293-.707.988.988,0,0,0-.293-.707A.988.988,0,0,0-9523.865-26788.436Zm-10.388.561a1.751,1.751,0,0,0-1.749,1.75,1.75,1.75,0,0,0,1.749,1.748,1.75,1.75,0,0,0,1.749-1.748A1.751,1.751,0,0,0-9534.253-26787.875Zm19.262-3.531a.587.587,0,0,1-.552-.4l-1.5-4.568-4.555-1.447a.593.593,0,0,1-.4-.551.577.577,0,0,1,.4-.553l4.5-1.514,1.419-4.5a.59.59,0,0,1,.557-.412.609.609,0,0,1,.17.027.569.569,0,0,1,.377.377l1.5,4.568,4.7,1.668a.579.579,0,0,1,.384.559.59.59,0,0,1-.417.545l-4.6,1.293-1.426,4.5A.572.572,0,0,1-9514.991-26791.406Z" transform="translate(-9002.999 22102.623)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1920" height="1080" viewBox="0 0 1920 1080"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 3840 2160"><image width="3840" height="2160" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 4.3 MiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="582" height="667" viewBox="0 0 582 667"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 1164 1312"><image width="1164" height="1312" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 968 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" width="1" height="1" patternTransform="matrix(-1, 0, 0, 1, 278, 0)" viewBox="0 0 139 765"><image preserveAspectRatio="xMidYMid slice" width="139" height="765" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAAX6CAYAAAA2hEelAAAgAElEQVR4nOy9aZfsSnaet4FEDWe6t+/U3ZfiJImDJEvysklZ1rIsLemjf4F/pv1ZXl4iKZHiZFEkm4NEdjd5SXXfoe9wphoygfACEJEZGRkRCCAnIPE83XlPnazKrDpViV1v7OHd2f/5fytJJc9ElBJ5KEVeLUXuVyKVEskykWz3Odq7lKR/ggEc9cltAp/oZJ8fYELkfb5UcxHVgaTI21sdPepgo7jCAEBT9PpGqDa4XOUiz4pWwbxVIo9le79HtUj4bn7ZA1wqvQKLiQR1QLle6PuK9h3LSh+LtKIBgPnST7FYAaaOHVcLkac6kLx9FHmsvMqlb5hByQBMnGGBRR+J1spFJ3VlJbIsk5VLV8AhwABMlEGBxbBWLrnI0yutXNSWcskGKJbYpwOACbBfYDmMcnFxP3pdjCLAAEwDO7AMPpoElItylEtfzGO6AgqBBmBk7KVYDDvKRTbKZVVZPS4DI8zm0Vt/+gIKQQZgBBQ9ciCdH+dUi1SjXJbrPpcsP86VHwo0BBmAM3EQxWJwlUslIg+r9mikVBsA9IccIqPre4qMAANwfgpPcvRQNAqlbvu/UpJVOvDUfx7pUqd8DTASDqpYDM0VrM9FtXLJc1F1hais2lJ01cqWdSDIth+7bwDw5WJQMQAnxA4sB1MuJlm7yERuFqKu2wNKUyXKlvX/JVObj83EOsNUcpSBRid2tcLJ+jsAHJCjKBb7am4moLPWPCHT0aOoNgGk3CgYE2S8SsZEngGKxg2UVJUAjowvsLgX12FyL5lIXYm+KURdt59B1aXoZSlZPcBYVpI1/9sMG+18XqXvOYCise0ilPUnSgbgABxVsYislciaItMKRh+VGpOoUqTM2w+rVUv9xkp5cjHt85jws09CJlRREpQMwP6kBJbDK5hs88dVLmphRgEyqRoLBtWUqbNazSiRXG0rmfrOrBkf0F/fngrG/ve4Ssa+AUAiR1csLq6CqXteFiZg6FxMri/nRSaqPptUSrL6qFSZSCI60LTBxjeKpHbeSMN+qswKKplzZBJUDECYIYHloApmXRKy7mhyMXU1KW8+WVW2LnXrXIzKJK+DjfbazZyvYK1t1FrS7PMV7jTd2SqmijwOYLacXLG4rHtenKvX+OnWd5e6opSbXEzrsdsomFI1QabNzbSqJ7Pjla/T1/PpQpiHuUekylEyqBgAi0MEluNUkaxnykO5mEqyh5Wo+s9Sxx67GUcZReN5WvdI1vOrMoHGzcFUzt8BZsnZFUuIpFxM3qqGvM6/1MFFNeomr9qEbqbVkCleZybfq5TnCNYv0NjdvT614gYcAg3MimMEltBFdJxcjO6LUarti3msGgUjOtnb5GPcPImOMCH/qb5VJjvIuGMEyjk6AcyC0SoWl2AuxuqLafIylZSLNhdTB5a8Djh1gFkpHWh0L4wxoLIC1tBKko07pxTLxRBo4GI5ZWA5SS6mriQV2TppWzXNdpVk96umqlRPWTe9Mesj0uYrWysYM3pwgCvfzsWIR8FQVYKLZDKKJYSbi1m4uRhpkryNalhUbT+MyLqq1CqZqkn0mk2x67hygKlrt6pkAkuug0rGMQkukXMGlkMrmMzzVvN5minrovGFUbqqVNaKpe7svVtJ9qi0irHdM63uGKXPTge4+rNATobgAhfF5BWLiycX01y8dXApdC5FSxJVV5rqo1GerStJudXhu1Y2cvir3pSs7dwLgQUuhjEFlq0LS6neCiZYjXIrSrZfzG0h6mahczFKyoelZI9Vk/TNVKtb8qaCdBjFsvO16QBDcIGL4uIUiw+jYnxX7VXWeGjWfTFq2V7adV9MU0lqTkBtb0xWJ4BXusM323T42vQJCnYfnx1YykP/2wHOwZgDy6H6YeyLOIzVF3Ol+2K0CXi1bKtKuSqbJrxGxdgzSQO/LrHyLHYyF9UCk2cWiiWGVVVqAkOxSamuczGLtqZT52TysnW9a8YIqla9ZNnGmGqIijGBJUe1wKUwxcAytJqUplzcj8hELUSy20KqeuK6Tuw+lFK9afcl1X0xi3w7Gdv3azMfo1e9USGC6TN7xeJiVZUy++qulUyuXe7yrMm7NCNHy7IJNnWBqVY4ldWz4lMwIexcS45qgalzCYGlby4mXbkEHni1EPW8rSqp10qqej6pPiFl28plYQWXrs/l9raQa4FJg2JJxO7wzTPJrhftUECtXKS2bihbj966NSbLttTHokcQI9cCF8ElB5auXEyqctkx2TYDjFe5yPOrtrP3tWqXselRgErnShbO0Sjl8yz0JyHXApMFxTIAd0e1Vi4iq2adSa1clO51MQEmd447oYBh97SQa4HJMqfAMji34jzOVS6ilYtsKZf2WGQmmCvZzr/EPofdI0OuBSYJimUPdpSLtMol08pF2qhQZZtjUQpu0xyBBSYHgeUASkbLC3W1EHmuI8gbEXlcacWSNUeaPjucTNKXGSKYJDk/tv1Ral0RapTLzaJVMbIZUao86iPlSNR1dAIYJXNULH2rQaHH7agIsxnAsxrAlI/dvpbY12AUy6rj6wQYHSiWA6Is9aJ3HdnRx1UtXYoF1QKThcASZnBuY+3/slEuRrGUPZ8zs3phCC4wGQgsB2Sdec3aPIuda5FwriWGrVoAJsOcq0KhnEko/xH6uM3f26NPUx16pv1d3qya6lD7AVl7UrLUUNdxSKxcS1+1A3A2+E14QLYUS9FWiBobfrXe82GrFkkYUMydG8AkoI8lTCjJGqwKuR8g2fa6RbVRHosExSLOUWiBaoGpwG/BI1BplZKbvpZis+tIDcuzkGuBSYFi2Z9grqXQuZZ8O9eidCeuPUMUwu3EXdDmD1OA34BHwFz1C6s6ZCkWsYJK6orVzAou/Mxg9KBYuqtDIZJyLZFOXNtSISVYLBIUDsAo4LffEenoxC17VIfoxIVJgWLZn86ZooRO3D6euAsrKJFrgVFCYDkibq6lqkRW1dZqabtClKJC7CQudgowWggsG0K5leF+LfroE6gONZ24lvqIHUvtr6EOKoVS2FbCeCHHckRMNCisTtxcNjkXS7GkHonsyhA/OxgtKJYwfZVKdL2qNtfe/gC1lWtJWRNil53xxIXRwm+9E7DTibvwduIaun4mGfNDMHZGp1jU5f3+3erEfZ5pN/9l64mr2gVnpRNcQsols45MiwE2DAAngd94JyDWietUh1KDRK5/KdDTAqOEwLLLocq4O01vJs+ShT1xy47czk51iMACY4TAckKUzrWU+lZtOnErbZq9GmCnwM8QRsfZcywTyKmEdkD3dfHf6sTNtr1ajBu/yZt0/Vwyp1nOzdEAnBXKzWfA14lrbUzs64dr+lroxIXRcPLAcoFVH0PntLOyOnGd6lBLtpXEjSVm7epQodUO1SEYDZzPT0hPn5bU6hBJXBgdHIXCNBe2Cl+wXZ250U5cZ9rZ/hhTHbITs26QcatDKV27ACfjJIqFzebb2NUhx6dFDawOsdQMRsVRFYuS3Wgy4eCinNxGiK5cS6pPS+V8fOi57OBiHgtwVk5yFEKpbGMCbsCnxc6x9PFpKTCAgrFwlMBiLpwLfHWnqhab3VyLqQ7lIs8Kv0+LNZwYy5+4s0ML6xgFcDaoCp2BLcVSBKtDfZbIG8VS8DOFMUBVqJv2wla9E6OduZbMCDsrqKjtjYmlEyyoDsEkOOhvN6UrHAjxNMy3qkqbHepK4tqzQwQXOCsHUSxuN62vGgRhArNDvmnnWK7FVi05SVw4J3sFlp2Awsu4FzEXf2t2qO/8UP0zXTKUCOdkUGCZeUDp6k9JvX9THTKzQyLyuq4Obay1K1FJLv7m67Jb/FMTvwAHZ68cyzqnAsO+f/rPndkhf64lZb1qRnUIxgBVoXT67nQ2dIXeTHfgqi1nObX+r+3iH/v8GYEFxkKvFx8K5ThsOctVba7FMzskPapDzA7BWUlSLCRpD4J9kXu/g9lu5DCBpcsL132KnOoQnJNoYCGgnIatXEvRqhdnx7M9A9RndshUiPjJwUnxBhYCShL2d8WnRlxv3PDHdzjLZVkbaxxDp9AWRNtZbmEdowBORhELGgSU02ArloXuZ7nT4UBHCbs6tEjIjbmKBSsFOClJigU6CamRPmxVhyQ8O5TiXGcrFhK4cHKiigVOi9JB3d47pMPCUGc5e1siP2k4GfSx9Ce1n8Wl6+PXF35k71BfZzl7dojjEJwMmqhGRIeLvxqwBD6jpwXOAYFlfw7nFa4bEOvq0Ivr9la/bVlRVD37WuwkLoEFTgaBZUR0KBbx+LR0QYs/nAVebIcl9WL3NNlu3t+kV3SOJbJ3qPI8l/uc9THoSt/4WcPJIHk7QiLVocqZHQpZVor1/kVi7wvAwSCwDCeU43Av8sHT0B3VoVRnuYXHWQ7gqBBYRkjMWc6z3zl1dsgEFlyJ4eggj89HOM8SqQ7phsbSMn+KBQnzPqpDcFIILCOkZ3Uo5WjD3iE4KRyFDsc+80LeCeid2SG1NTtkAktKsMit6tDDqb4hMF8ILCNmqzpUBatDygosIfWCYoGTQmDZH191KJZUjW1I9LrM9XCWC31OE0zsiWcSuHA0CCwjxsLine truncated
|
||||
|
Before Width: | Height: | Size: 53 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139" height="765" viewBox="0 0 139 765"><defs><style>.a{fill:url(#a);}</style><pattern id="a" preserveAspectRatio="xMidYMid slice" width="100%" height="100%" viewBox="0 0 278 1530"><image width="278" height="1530" xlink:href="data:image/png;base64,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 truncated
|
||||
|
Before Width: | Height: | Size: 53 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M110.023,105.37H98.2a1.721,1.721,0,0,1-1.716-1.716v-1.918h10.189a1.721,1.721,0,0,0,1.716-1.716V72.8a1.721,1.721,0,0,0-1.716-1.716H98.2V67.437A3.444,3.444,0,0,0,94.76,64H87.83a3.444,3.444,0,0,0-3.437,3.437v3.652H75.625A1.721,1.721,0,0,0,73.91,72.8v27.21a1.721,1.721,0,0,0,1.716,1.716h10.49v1.918a1.721,1.721,0,0,1-1.716,1.716H72.3a1.721,1.721,0,0,0-1.716,1.716v10.1A1.721,1.721,0,0,0,72.3,118.9h37.724a1.721,1.721,0,0,0,1.716-1.716v-10.1A1.726,1.726,0,0,0,110.023,105.37Zm-8.406-19.1a3.437,3.437,0,1,1-3.437-3.437A3.436,3.436,0,0,1,101.616,86.266ZM87.83,67.437h6.924v3.646H87.83ZM81.121,86.266A3.437,3.437,0,1,1,84.558,89.7,3.436,3.436,0,0,1,81.121,86.266Zm32.48-7.089V93.355a1.716,1.716,0,0,1-3.431,0V79.177a1.716,1.716,0,0,1,3.431,0Zm5.122,3.548v6.881a1.716,1.716,0,0,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0ZM70.644,95.07a1.717,1.717,0,0,1-1.716-1.716V79.177a1.716,1.716,0,0,1,3.431,0V93.355A1.713,1.713,0,0,1,70.644,95.07ZM67.231,82.724v6.881a1.716,1.716,0,1,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0Zm0,0" transform="translate(-14540.154 22987.455)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,47 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="188" height="241" viewBox="0 0 188 241" fill="none">
|
||||
<defs>
|
||||
<radialGradient id="surface" cx="0" cy="0" r="1" gradientTransform="translate(60 30) rotate(60) scale(150 158)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#46B6FF"/>
|
||||
<stop offset=".24" stop-color="#1A83DE"/>
|
||||
<stop offset=".6" stop-color="#075FBC"/>
|
||||
<stop offset="1" stop-color="#003675"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="inner-light" cx="0" cy="0" r="1" gradientTransform="translate(94 82) rotate(90) scale(79 84)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0876DC" stop-opacity=".42"/>
|
||||
<stop offset="1" stop-color="#001F55" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="lower-depth" x1="94" y1="118" x2="94" y2="165" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#005DBB" stop-opacity="0"/>
|
||||
<stop offset="1" stop-color="#002A68" stop-opacity=".58"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="edge" x1="36" y1="12" x2="152" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF" stop-opacity=".98"/>
|
||||
<stop offset=".48" stop-color="#B6EAFF" stop-opacity=".78"/>
|
||||
<stop offset="1" stop-color="#48BBFF" stop-opacity=".72"/>
|
||||
</linearGradient>
|
||||
<filter id="glow" x="-12" y="-12" width="212" height="212" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="4.5"/>
|
||||
</filter>
|
||||
<filter id="shadow" x="22" y="163" width="144" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feGaussianBlur stdDeviation="6.5"/>
|
||||
</filter>
|
||||
<clipPath id="clip">
|
||||
<circle cx="94" cy="82" r="81"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<ellipse cx="94" cy="188" rx="61" ry="9" fill="#00112F" fill-opacity=".58" filter="url(#shadow)"/>
|
||||
<circle cx="94" cy="82" r="82" fill="#139DFF" fill-opacity=".22" filter="url(#glow)"/>
|
||||
<circle cx="94" cy="82" r="81" fill="url(#surface)"/>
|
||||
<g clip-path="url(#clip)">
|
||||
<circle cx="94" cy="82" r="79" fill="url(#inner-light)"/>
|
||||
<ellipse cx="94" cy="142" rx="89" ry="38" fill="url(#lower-depth)"/>
|
||||
<ellipse cx="94" cy="150" rx="78" ry="15" fill="#55B9FF" fill-opacity=".13"/>
|
||||
<path d="M19 145C46 157 142 157 169 145" stroke="#BCEAFF" stroke-opacity=".24" stroke-width="1.2"/>
|
||||
<path d="M30 160C54 169 134 169 158 160" stroke="#1599F7" stroke-opacity=".28" stroke-width="1.2"/>
|
||||
<path d="M24 62C29 36 49 17 76 11" stroke="#D8F4FF" stroke-opacity=".3" stroke-width="9" stroke-linecap="round" filter="url(#glow)"/>
|
||||
</g>
|
||||
<circle cx="94" cy="82" r="81" stroke="url(#edge)" stroke-width="2"/>
|
||||
<circle cx="94" cy="82" r="76" stroke="#27B2FF" stroke-opacity=".28"/>
|
||||
<path d="M19 69C24 38 49 12 82 4" stroke="#FFFFFF" stroke-opacity=".88" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.337" x2="1" y2="1.007" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(-204.426 -230.797)"><rect class="a" width="68" height="68" transform="translate(204.426 230.797)"/><g transform="translate(209.658 232.191)"><path class="b" d="M25.52,64.045c-1.725-.852-8.6-4.486-10.846-5.656-2.309-1.207-3.3-1.01-5.09.146L6.346,60.67,0,46.383s6.887-3.254,10.033-4.508C15,39.885,17.273,41.6,20.115,43.4c1.209.762,5.131,3.664,5.131,3.664a17.886,17.886,0,0,0,7.783,2.316c5.928,0,6.975.344,6.975,2.266,0,2.113-3.859,2.266-3.883,2.266H26.605c-3.236,0-7.766-1.893-7.766-1.893s3.846,4.156,7.766,4.156h9.547c2.105-.047,6.1-1.047,6.1-4.537,0-4.529-4.066-4.529-9.219-4.529a5.2,5.2,0,0,1-1.127-.145c.2-.1.4-.207.639-.35.9-.523,2.145-1.27,3.535-2.158,3.67-2.3,5.555-2.592,7.387-.152a5.718,5.718,0,0,1,4.248-1.678c1.551,0,2.865,1.039,2.494,2.719,3.975-2.377,5.936-1.42,6.809-.266,1.156,1.527.373,3.109-2.143,4.926C53.244,51.17,37.809,61.543,35.635,62.82c-1.732,1-3.326,2.395-5.8,2.395A9.825,9.825,0,0,1,25.52,64.045ZM22.51,39.5l-.229-.865a.706.706,0,0,1,.129-.607.688.688,0,0,1,.557-.275H35.734a.689.689,0,0,1,.547.275.7.7,0,0,1,.121.607l-.219.865a7.065,7.065,0,0,1-13.674,0Zm6.84,2.486a4.265,4.265,0,0,0,3.178-1.424H26.174A4.239,4.239,0,0,0,29.35,41.982Zm-6.285-5.07a.885.885,0,1,1,0-1.77H35.658a.885.885,0,0,1-.008,1.77Zm-.189-2.637a.7.7,0,0,1-.707-.7V31.021a.565.565,0,0,0-.244-.461,13.168,13.168,0,1,1,14.844,0,.563.563,0,0,0-.242.461v2.547a.7.7,0,0,1-.7.707Zm8.672-8.785a6.207,6.207,0,0,0,2.455-.3c.547-.258.721-.676.721-1.572V14.719H33.059v8.887c0,.219-.1.281-.334.3-.234,0-1.033,0-1.732-.039A6.217,6.217,0,0,1,31.547,25.49Zm-6.18-2.166c0,1.535.463,1.967,1.969,1.967h1.7c1.285,0,1.717-.516,1.877-2.174a4.544,4.544,0,0,1-1.443-.607c-.062,1.141-.145,1.361-.586,1.361H27.557c-.494,0-.578-.055-.578-.562V20.117h1.475a3.131,3.131,0,0,1-.213,1.223.374.374,0,0,1-.334.131,6.154,6.154,0,0,1-.836-.047,3.419,3.419,0,0,1,.41,1.4,8.144,8.144,0,0,0,1.3-.045,1.284,1.284,0,0,0,.844-.455c.281-.35.395-1.223.465-3.107l.016-.2.744-.707v4.154h1.619V15.793H30.848V18.17a28.18,28.18,0,0,0-2.326-2.758c.084-.158.152-.328.221-.486l-1.535-.479a9.09,9.09,0,0,1-3.215,3.9,7.919,7.919,0,0,1,1.2,1.27l.174-.129Zm.906-4.643a11.211,11.211,0,0,0,1.465-1.855c.518.609,1.049,1.27,1.469,1.855Zm17.676-2.123a1.606,1.606,0,0,1,1.186-1.943l2.363-.578a1.608,1.608,0,0,1,.76,3.125l-2.369.576a1.629,1.629,0,0,1-.383.047A1.611,1.611,0,0,1,43.949,16.559ZM12.631,17.631,10.389,16.7a1.608,1.608,0,0,1,1.23-2.971l2.242.941a1.6,1.6,0,1,1-1.23,2.963Zm4.088-9.514L15.328,6.125a1.605,1.605,0,0,1,2.637-1.832l1.391,1.99a1.6,1.6,0,0,1-.4,2.234,1.58,1.58,0,0,1-.916.289A1.6,1.6,0,0,1,16.719,8.117Zm22.709.32a1.614,1.614,0,0,1-.273-2.252l1.488-1.914A1.6,1.6,0,1,1,43.174,6.24L41.686,8.16a1.626,1.626,0,0,1-2.258.277ZM27.746,4.043V1.6a1.607,1.607,0,0,1,3.215,0V4.043a1.607,1.607,0,0,1-3.215,0Z" transform="translate(0 0)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.207" x2="0.793" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#a6cfff"/></linearGradient></defs><g transform="translate(-2.29 3.525)"><g class="a" transform="translate(2.29 -3.525)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M36.066,58.469a15.431,15.431,0,0,1,0-21.866,15.011,15.011,0,0,1,3.577-2.662,2.018,2.018,0,0,1,1.878,3.571,11.53,11.53,0,0,0-2.6,1.947A11.446,11.446,0,0,0,55.132,55.622a11.938,11.938,0,0,0,1.878-2.462A2.02,2.02,0,1,1,60.5,55.2,15.711,15.711,0,0,1,58,58.469a15.533,15.533,0,0,1-21.929,0Zm-5.87-.994H5.225A5.225,5.225,0,0,1,0,52.263V13.925A5.226,5.226,0,0,1,5.225,8.712H53.769A5.226,5.226,0,0,1,59,13.925V32.117A19.526,19.526,0,0,0,30.2,57.474v0ZM21.753,18.77l-8.5,7.4a2.531,2.531,0,0,0-.239,3.565,2.536,2.536,0,0,0,3.568.239l6.835-5.958L28.446,28.4a2.52,2.52,0,0,0,3.351-.015L42.282,19a2.508,2.508,0,0,0,.191-3.557,2.529,2.529,0,0,0-3.567-.192L30.085,23.14l-5-4.369a2.544,2.544,0,0,0-3.335,0ZM47.03,49.554a2.017,2.017,0,0,1-2.022-2.017V34.091a2.021,2.021,0,0,1,2.022-2.017A15.485,15.485,0,0,1,62.537,47.537a2.018,2.018,0,0,1-2.024,2.017Zm2.025-4.034h9.258a11.471,11.471,0,0,0-9.258-9.229ZM7.53,5.051a2.521,2.521,0,1,1,0-5.043H50.681a2.521,2.521,0,1,1,0,5.043Z" transform="translate(6.296 0.013)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="54.923" height="54.898" viewBox="0 0 54.923 54.898"><defs><style>.a{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><path class="a" d="M110.023,105.37H98.2a1.721,1.721,0,0,1-1.716-1.716v-1.918h10.189a1.721,1.721,0,0,0,1.716-1.716V72.8a1.721,1.721,0,0,0-1.716-1.716H98.2V67.437A3.444,3.444,0,0,0,94.76,64H87.83a3.444,3.444,0,0,0-3.437,3.437v3.652H75.625A1.721,1.721,0,0,0,73.91,72.8v27.21a1.721,1.721,0,0,0,1.716,1.716h10.49v1.918a1.721,1.721,0,0,1-1.716,1.716H72.3a1.721,1.721,0,0,0-1.716,1.716v10.1A1.721,1.721,0,0,0,72.3,118.9h37.724a1.721,1.721,0,0,0,1.716-1.716v-10.1A1.726,1.726,0,0,0,110.023,105.37Zm-8.406-19.1a3.437,3.437,0,1,1-3.437-3.437A3.436,3.436,0,0,1,101.616,86.266ZM87.83,67.437h6.924v3.646H87.83ZM81.121,86.266A3.437,3.437,0,1,1,84.558,89.7,3.436,3.436,0,0,1,81.121,86.266Zm32.48-7.089V93.355a1.716,1.716,0,0,1-3.431,0V79.177a1.716,1.716,0,0,1,3.431,0Zm5.122,3.548v6.881a1.716,1.716,0,0,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0ZM70.644,95.07a1.717,1.717,0,0,1-1.716-1.716V79.177a1.716,1.716,0,0,1,3.431,0V93.355A1.713,1.713,0,0,1,70.644,95.07ZM67.231,82.724v6.881a1.716,1.716,0,1,1-3.431,0V82.724a1.716,1.716,0,0,1,3.431,0Zm0,0" transform="translate(-63.8 -64)"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;stroke:#707070;stroke-width:3.75px;opacity:0;}.b{fill:url(#a);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.431" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(-71.077 -67.746)"><g class="a" transform="translate(71.077 67.746)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M114.406,106.791a7.707,7.707,0,0,0-4.354,1.353L98.7,99.937a13.3,13.3,0,0,0,1.742-6.583c0-.421-.03-.842-.06-1.263L112,85.207a4.687,4.687,0,1,0-1.952-2.345L99.691,88.995a13.678,13.678,0,0,0-6.1-7.365l2.072-10.341c.15,0,.3.03.451.03a4.66,4.66,0,1,0-4.655-4.659,4.727,4.727,0,0,0,1.412,3.367l-2.1,10.462A13.479,13.479,0,0,0,80.05,81.87l-6.4-5.712A6.129,6.129,0,1,0,71.7,78.443L77.677,83.7a13.456,13.456,0,0,0,.691,19.991l-5.316,5.622a5.948,5.948,0,0,0-3.394-1.022,6.163,6.163,0,1,0,6.156,6.162,6.343,6.343,0,0,0-.721-2.916l5.826-6.193a13.464,13.464,0,0,0,16.1-2.946l11.022,7.966a7.77,7.77,0,0,0-1.2,4.118,7.658,7.658,0,1,0,15.316,0A7.817,7.817,0,0,0,114.406,106.791Zm0,0" transform="translate(13 9.671)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{opacity:0;fill:url(#a);}.b{fill:url(#b);}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient><linearGradient id="b" x1="0.337" x2="1" xlink:href="#a"/></defs><g transform="translate(-204.426 -230.797)"><rect class="a" width="68" height="68" transform="translate(204.426 230.797)"/><g transform="translate(209.658 232.191)"><path class="b" d="M25.52,64.045c-1.725-.852-8.6-4.486-10.846-5.656-2.309-1.207-3.3-1.01-5.09.146L6.346,60.67,0,46.383s6.887-3.254,10.033-4.508C15,39.885,17.273,41.6,20.115,43.4c1.209.762,5.131,3.664,5.131,3.664a17.886,17.886,0,0,0,7.783,2.316c5.928,0,6.975.344,6.975,2.266,0,2.113-3.859,2.266-3.883,2.266H26.605c-3.236,0-7.766-1.893-7.766-1.893s3.846,4.156,7.766,4.156h9.547c2.105-.047,6.1-1.047,6.1-4.537,0-4.529-4.066-4.529-9.219-4.529a5.2,5.2,0,0,1-1.127-.145c.2-.1.4-.207.639-.35.9-.523,2.145-1.27,3.535-2.158,3.67-2.3,5.555-2.592,7.387-.152a5.718,5.718,0,0,1,4.248-1.678c1.551,0,2.865,1.039,2.494,2.719,3.975-2.377,5.936-1.42,6.809-.266,1.156,1.527.373,3.109-2.143,4.926C53.244,51.17,37.809,61.543,35.635,62.82c-1.732,1-3.326,2.395-5.8,2.395A9.825,9.825,0,0,1,25.52,64.045ZM22.51,39.5l-.229-.865a.706.706,0,0,1,.129-.607.688.688,0,0,1,.557-.275H35.734a.689.689,0,0,1,.547.275.7.7,0,0,1,.121.607l-.219.865a7.065,7.065,0,0,1-13.674,0Zm6.84,2.486a4.265,4.265,0,0,0,3.178-1.424H26.174A4.239,4.239,0,0,0,29.35,41.982Zm-6.285-5.07a.885.885,0,1,1,0-1.77H35.658a.885.885,0,0,1-.008,1.77Zm-.189-2.637a.7.7,0,0,1-.707-.7V31.021a.565.565,0,0,0-.244-.461,13.168,13.168,0,1,1,14.844,0,.563.563,0,0,0-.242.461v2.547a.7.7,0,0,1-.7.707Zm8.672-8.785a6.207,6.207,0,0,0,2.455-.3c.547-.258.721-.676.721-1.572V14.719H33.059v8.887c0,.219-.1.281-.334.3-.234,0-1.033,0-1.732-.039A6.217,6.217,0,0,1,31.547,25.49Zm-6.18-2.166c0,1.535.463,1.967,1.969,1.967h1.7c1.285,0,1.717-.516,1.877-2.174a4.544,4.544,0,0,1-1.443-.607c-.062,1.141-.145,1.361-.586,1.361H27.557c-.494,0-.578-.055-.578-.562V20.117h1.475a3.131,3.131,0,0,1-.213,1.223.374.374,0,0,1-.334.131,6.154,6.154,0,0,1-.836-.047,3.419,3.419,0,0,1,.41,1.4,8.144,8.144,0,0,0,1.3-.045,1.284,1.284,0,0,0,.844-.455c.281-.35.395-1.223.465-3.107l.016-.2.744-.707v4.154h1.619V15.793H30.848V18.17a28.18,28.18,0,0,0-2.326-2.758c.084-.158.152-.328.221-.486l-1.535-.479a9.09,9.09,0,0,1-3.215,3.9,7.919,7.919,0,0,1,1.2,1.27l.174-.129Zm.906-4.643a11.211,11.211,0,0,0,1.465-1.855c.518.609,1.049,1.27,1.469,1.855Zm17.676-2.123a1.606,1.606,0,0,1,1.186-1.943l2.363-.578a1.608,1.608,0,0,1,.76,3.125l-2.369.576a1.629,1.629,0,0,1-.383.047A1.611,1.611,0,0,1,43.949,16.559ZM12.631,17.631,10.389,16.7a1.608,1.608,0,0,1,1.23-2.971l2.242.941a1.6,1.6,0,1,1-1.23,2.963Zm4.088-9.514L15.328,6.125a1.605,1.605,0,0,1,2.637-1.832l1.391,1.99a1.6,1.6,0,0,1-.4,2.234,1.58,1.58,0,0,1-.916.289A1.6,1.6,0,0,1,16.719,8.117Zm22.709.32a1.614,1.614,0,0,1-.273-2.252l1.488-1.914A1.6,1.6,0,1,1,43.174,6.24L41.686,8.16a1.626,1.626,0,0,1-2.258.277ZM27.746,4.043V1.6a1.607,1.607,0,0,1,3.215,0V4.043a1.607,1.607,0,0,1-3.215,0Z" transform="translate(0 0)"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:url(#a);}</style><linearGradient id="a" x1="0.405" x2="1" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient></defs><g transform="translate(14482.893 -23044.904)"><rect class="a" width="68" height="68" transform="translate(-14482.893 23044.904)"/><path class="b" d="M43.013,57.336h-.255l-.074,0a14.064,14.064,0,0,1-8.911-3.467,6.216,6.216,0,0,1,6.1-5.029h6.278a6.218,6.218,0,0,1,6.1,5.03A14.073,14.073,0,0,1,43.6,57.324h-.009l-.067,0H43.5l-.063,0h-.424ZM30.159,54.9H8.5a1.717,1.717,0,0,1-1.715-1.716v-10.1A1.717,1.717,0,0,1,8.5,41.363H20.6a1.717,1.717,0,0,0,1.716-1.715V37.73H11.824a1.717,1.717,0,0,1-1.716-1.715V8.8a1.718,1.718,0,0,1,1.716-1.716h8.768V3.437A3.44,3.44,0,0,1,24.029,0h6.93A3.441,3.441,0,0,1,34.4,3.437V7.089H42.87A1.718,1.718,0,0,1,44.586,8.8V26.022c-.515-.046-1.044-.07-1.574-.07A17.327,17.327,0,0,0,30.157,54.9Zm4.219-36.07a3.437,3.437,0,1,0,3.438,3.437A3.441,3.441,0,0,0,34.378,18.828Zm-13.619,0A3.437,3.437,0,1,0,24.2,22.265,3.441,3.441,0,0,0,20.758,18.828ZM24.029,3.437V7.083h6.924V3.437Zm7.256,47.59v0a14.059,14.059,0,1,1,23.456,0,9.508,9.508,0,0,0-8.589-5.455H44.686a7.193,7.193,0,1,0-3.347,0H39.873a9.5,9.5,0,0,0-8.587,5.457ZM43.013,42.5a3.923,3.923,0,1,1,3.923-3.924A3.928,3.928,0,0,1,43.013,42.5ZM6.844,31.07a1.717,1.717,0,0,1-1.716-1.715V15.176a1.715,1.715,0,1,1,3.431,0V29.355A1.717,1.717,0,0,1,6.844,31.07ZM49.8,27.327h0a17.187,17.187,0,0,0-3.429-1.051v-11.1a1.715,1.715,0,1,1,3.431,0v12.15Zm3.406-.007a1.717,1.717,0,0,1-1.715-1.715V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,53.206,27.32Zm-51.491,0A1.717,1.717,0,0,1,0,25.605V18.724a1.715,1.715,0,0,1,3.431,0v6.881A1.717,1.717,0,0,1,1.715,27.32Z" transform="translate(-14477.664 23050.15)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="68" viewBox="0 0 68 68"><defs><style>.a{stroke:#707070;stroke-width:3.75px;opacity:0;fill:url(#a);}.b{fill:url(#b);}.c{stroke:none;}.d{fill:none;}</style><linearGradient id="a" x1="0.5" x2="0.5" y2="1" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#f3212c"/><stop offset="1" stop-color="#aa071a"/></linearGradient><linearGradient id="b" x1="0.414" x2="1" xlink:href="#a"/></defs><g transform="translate(-2.29 3.525)"><g class="a" transform="translate(2.29 -3.525)"><rect class="c" width="68" height="68"/><rect class="d" x="1.875" y="1.875" width="64.25" height="64.25"/></g><path class="b" d="M36.066,58.469a15.431,15.431,0,0,1,0-21.866,15.011,15.011,0,0,1,3.577-2.662,2.018,2.018,0,0,1,1.878,3.571,11.53,11.53,0,0,0-2.6,1.947A11.446,11.446,0,0,0,55.132,55.622a11.938,11.938,0,0,0,1.878-2.462A2.02,2.02,0,1,1,60.5,55.2,15.711,15.711,0,0,1,58,58.469a15.533,15.533,0,0,1-21.929,0Zm-5.87-.994H5.225A5.225,5.225,0,0,1,0,52.263V13.925A5.226,5.226,0,0,1,5.225,8.712H53.769A5.226,5.226,0,0,1,59,13.925V32.117A19.526,19.526,0,0,0,30.2,57.474v0ZM21.753,18.77l-8.5,7.4a2.531,2.531,0,0,0-.239,3.565,2.536,2.536,0,0,0,3.568.239l6.835-5.958L28.446,28.4a2.52,2.52,0,0,0,3.351-.015L42.282,19a2.508,2.508,0,0,0,.191-3.557,2.529,2.529,0,0,0-3.567-.192L30.085,23.14l-5-4.369a2.544,2.544,0,0,0-3.335,0ZM47.03,49.554a2.017,2.017,0,0,1-2.022-2.017V34.091a2.021,2.021,0,0,1,2.022-2.017A15.485,15.485,0,0,1,62.537,47.537a2.018,2.018,0,0,1-2.024,2.017Zm2.025-4.034h9.258a11.471,11.471,0,0,0-9.258-9.229ZM7.53,5.051a2.521,2.521,0,1,1,0-5.043H50.681a2.521,2.521,0,1,1,0,5.043Z" transform="translate(6.296 0.013)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
@@ -18,9 +18,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import type { StreamdownProps } from "streamdown";
|
||||
|
||||
import { SafeStreamdown } from "@/core/streamdown";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage["role"];
|
||||
@@ -304,11 +302,11 @@ export const MessageBranchPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageResponseProps = StreamdownProps;
|
||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||
|
||||
export const MessageResponse = memo(
|
||||
({ className, ...props }: MessageResponseProps) => (
|
||||
<SafeStreamdown
|
||||
<Streamdown
|
||||
className={cn(
|
||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
||||
className,
|
||||
|
||||
@@ -912,9 +912,7 @@ export const PromptInput = ({
|
||||
{/* Do not rely on InputGroup's :has(textarea) selector here: Chrome
|
||||
versions before 105 ignore :has(), retain h-9, and clip the whole
|
||||
multi-line composer to a single row. */}
|
||||
<InputGroup className="prompt-input-group h-auto min-h-16 flex-col items-stretch">
|
||||
{children}
|
||||
</InputGroup>
|
||||
<InputGroup className="h-auto min-h-16">{children}</InputGroup>
|
||||
</form>
|
||||
</PromptInputValidationContext.Provider>
|
||||
);
|
||||
@@ -934,11 +932,7 @@ export const PromptInputBody = ({
|
||||
className,
|
||||
...props
|
||||
}: PromptInputBodyProps) => (
|
||||
<div
|
||||
data-slot="prompt-input-body"
|
||||
className={cn("contents", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div className={cn("contents", className)} {...props} />
|
||||
);
|
||||
|
||||
export type PromptInputTextareaProps = ComponentProps<
|
||||
@@ -1214,9 +1208,9 @@ export const PromptInputSubmit = ({
|
||||
<InputGroupButton
|
||||
aria-label="Submit"
|
||||
className={cn(
|
||||
"transition-all duration-300",
|
||||
status === "streaming" &&
|
||||
"streaming-stop-button bg-primary/10 text-primary border-primary/25 hover:bg-primary/14",
|
||||
"transition-all duration-300",
|
||||
status === "streaming" &&
|
||||
"animate-breathe-stop bg-primary/10 text-primary border-primary/25 shadow-[0_0_0_0_rgba(217,33,33,0.25)] hover:bg-primary/14",
|
||||
className,
|
||||
)}
|
||||
size={size}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils";
|
||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import { SafeStreamdown } from "@/core/streamdown";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { reasoningPlugins } from "@/core/streamdown/plugins";
|
||||
import { Shimmer } from "./shimmer";
|
||||
|
||||
@@ -179,7 +179,7 @@ export const ReasoningContent = memo(
|
||||
{...props}
|
||||
>
|
||||
{typeof children === "string" ? (
|
||||
<SafeStreamdown {...reasoningPlugins}>{children}</SafeStreamdown>
|
||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import { ReactUiErrorBoundary } from "./react-ui-error-boundary";
|
||||
|
||||
// Offline browser QA: real React production render/commit recursion, not a
|
||||
// fabricated #185 thrown by a unit-test stub. A manual button click reloads
|
||||
// this same page without submitting any conversation or model request.
|
||||
const key = "deerflow.boundary-browser-check";
|
||||
let mounts = 0;
|
||||
function InfiniteCommitLoop() {
|
||||
const [, update] = useState(0);
|
||||
useState(() => { mounts += 1; return 0; });
|
||||
useLayoutEffect(() => { update((value) => value + 1); });
|
||||
return <div>Loop fixture</div>;
|
||||
}
|
||||
|
||||
function result(passed: boolean, details: Record<string, unknown>) {
|
||||
const output = document.createElement("pre");
|
||||
output.id = "boundary-check-result";
|
||||
output.textContent = JSON.stringify({ passed, ...details });
|
||||
document.body.append(output);
|
||||
}
|
||||
|
||||
const resumed = sessionStorage.getItem(key) === "manual-reload";
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ReactUiErrorBoundary>
|
||||
{resumed ? <div id="healthy">Existing page restored</div> : <InfiniteCommitLoop />}
|
||||
</ReactUiErrorBoundary>,
|
||||
);
|
||||
|
||||
if (resumed) {
|
||||
sessionStorage.removeItem(key);
|
||||
window.setTimeout(() => result(Boolean(document.getElementById("healthy")), {
|
||||
phase: "manual-reload-restored", renderLoopCaught: true, noAutomaticRemount: true,
|
||||
}), 100);
|
||||
} else {
|
||||
window.setTimeout(() => {
|
||||
const alert = document.querySelector('[role="alert"]');
|
||||
const before = mounts;
|
||||
if (!alert || /#185|react\.dev|Maximum update depth/.test(alert.textContent ?? "")) {
|
||||
result(false, { phase: "friendly-fallback", hasAlert: Boolean(alert) });
|
||||
return;
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
if (mounts !== before || before !== 1) {
|
||||
result(false, { phase: "unexpected-automatic-remount", mounts, before });
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(key, "manual-reload");
|
||||
(alert.querySelector("button") as HTMLButtonElement).click();
|
||||
}, 250);
|
||||
}, 100);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Component, type ErrorInfo, type ReactNode } from "react";
|
||||
|
||||
import { isReactMaximumUpdateDepthError } from "@/core/threads/stream-error-classification";
|
||||
import { REACT_UI_ERROR_MESSAGE } from "@/core/threads/react-ui-error";
|
||||
|
||||
/** Last-resort render/commit guard. It deliberately never auto-remounts a broken
|
||||
* tree, which could otherwise repeatedly resume the same run and crash again. */
|
||||
export class ReactUiErrorBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ failed: boolean }
|
||||
> {
|
||||
state = { failed: false };
|
||||
|
||||
static getDerivedStateFromError(error: unknown) {
|
||||
if (!isReactMaximumUpdateDepthError(error)) throw error;
|
||||
return { failed: true };
|
||||
}
|
||||
|
||||
componentDidCatch(_error: unknown, info: ErrorInfo) {
|
||||
console.warn("[deer-flow] react_ui_render_error", {
|
||||
errorType: "react_maximum_update_depth",
|
||||
componentStack: info.componentStack,
|
||||
recovery: "manual_reload",
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.failed) return this.props.children;
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center p-6">
|
||||
<section role="alert" className="max-w-lg rounded-lg border p-6 text-center">
|
||||
<h1 className="mb-3 text-lg font-medium">页面暂时无法显示</h1>
|
||||
<p className="mb-5 text-sm text-muted-foreground">{REACT_UI_ERROR_MESSAGE}</p>
|
||||
<button
|
||||
className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
|
||||
onClick={() => window.location.reload()}
|
||||
type="button"
|
||||
>
|
||||
重新载入页面
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const DropdownMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger>
|
||||
@@ -34,7 +34,7 @@ const DropdownMenuTrigger = React.forwardRef<
|
||||
});
|
||||
|
||||
DropdownMenuTrigger.displayName = DropdownMenuPrimitive.Trigger.displayName;
|
||||
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
@@ -46,7 +46,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -56,15 +56,15 @@ const DropdownMenuContent = React.forwardRef<
|
||||
});
|
||||
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
@@ -88,123 +88,123 @@ const DropdownMenuItem = React.forwardRef<
|
||||
});
|
||||
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
@@ -230,7 +230,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
@@ -240,7 +240,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
ref={ref}
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -250,21 +250,21 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
}
|
||||
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
|
||||
}
|
||||
|
||||
const HoverCardTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Trigger>
|
||||
@@ -38,7 +38,7 @@ const HoverCardContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-[var(--radix-hover-card-content-transform-origin)] rounded-md border p-4 shadow-md outline-hidden",
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -48,5 +48,5 @@ const HoverCardContent = React.forwardRef<
|
||||
});
|
||||
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
||||
@@ -1,198 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default";
|
||||
}
|
||||
>(({ className, size = "default", children, ...props }, ref) => {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEE2] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#022248] dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
});
|
||||
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-[var(--radix-select-content-available-height)] min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border shadow-md dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn(
|
||||
"text-muted-foreground px-2 py-1.5 text-xs dark:text-[#566B7F]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"dark:text-[#AACEFF] dark:focus:bg-[#0062D9] dark:focus:text-white dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus:[&_svg:not([class*='text-'])]:text-white dark:data-[highlighted]:[&_svg:not([class*='text-'])]:text-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default";
|
||||
}
|
||||
>(({ className, size = "default", children, ...props }, ref) => {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEE2] dark:data-[placeholder]:text-[#566B7F] dark:hover:bg-[#022248] dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
});
|
||||
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "item-aligned", align = "center", ...props }, ref) => {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn(
|
||||
"text-muted-foreground px-2 py-1.5 text-xs dark:text-[#566B7F]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"dark:text-[#AACEFF] dark:focus:bg-[#0062D9] dark:focus:text-white dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white dark:[&_svg:not([class*='text-'])]:text-[#AACEFF] dark:focus:[&_svg:not([class*='text-'])]:text-white dark:data-[highlighted]:[&_svg:not([class*='text-'])]:text-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1 dark:text-[#AACEFF]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -36,7 +36,7 @@ function ToggleGroup({
|
||||
data-spacing={spacing}
|
||||
style={{ "--gap": spacing } as React.CSSProperties}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center gap-[calc(var(--gap)*0.25rem)] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const TooltipTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
|
||||
@@ -54,7 +54,7 @@ const TooltipContent = React.forwardRef<
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset ?? 4}
|
||||
className={cn(
|
||||
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-foreground text-background dark:text-foreground z-50 w-fit origin-[var(--radix-tooltip-content-transform-origin)] rounded-md border px-3 py-1.5 text-xs text-balance shadow-xs dark:border-white/18 dark:bg-[#050504]",
|
||||
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-foreground text-background dark:text-foreground z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md border px-3 py-1.5 text-xs text-balance shadow-xs dark:border-white/18 dark:bg-[#050504]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -66,5 +66,5 @@ const TooltipContent = React.forwardRef<
|
||||
});
|
||||
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -172,7 +172,7 @@ export function AgentChatSidebar({ activeAgentId }: { activeAgentId?: string })
|
||||
|
||||
return (
|
||||
// 配色与 AgentModuleSidebar 一致:浅色白底,深蓝主题半透明深蓝玻璃面板。
|
||||
<aside className="flex h-full w-[var(--sidebar-width)] shrink-0 flex-col overflow-hidden border-r border-border/70 bg-white text-foreground backdrop-blur-md dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80">
|
||||
<aside className="flex h-full w-(--sidebar-width) shrink-0 flex-col overflow-hidden border-r border-border/70 bg-white text-foreground backdrop-blur-md dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80">
|
||||
{/* 智能体(上半,可收起)。展开时:标题栏 + 来源下拉/名称搜索 + 列表;
|
||||
收起时只留标题栏,下方「最近的对话」占满剩余空间。 */}
|
||||
<div
|
||||
|
||||
@@ -460,14 +460,14 @@ export function AgentGallery({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="打开智能体广场视图"
|
||||
onClick={() => router.push(`${routeBase}/agents/plaza`)}
|
||||
>
|
||||
广场视图
|
||||
</Button>
|
||||
</Button> */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
|
||||
@@ -291,7 +291,7 @@ export function AgentModuleSidebar({
|
||||
// 浅色保持白底。其余颜色已走语义 token 自适配。
|
||||
<aside
|
||||
className={cn(
|
||||
"flex h-full w-[var(--sidebar-width)] shrink-0 flex-col overflow-hidden border-r border-border/70 bg-white text-foreground backdrop-blur-md dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80",
|
||||
"flex h-full w-(--sidebar-width) shrink-0 flex-col overflow-hidden border-r border-border/70 bg-white text-foreground backdrop-blur-md dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { AnchorHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { defaultUrlTransform } from "react-markdown";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import {
|
||||
Artifact,
|
||||
@@ -57,9 +58,9 @@ import {
|
||||
} from "@/open-canvas/api/rewrite";
|
||||
import { publishArtifactToKnowledge } from "@/core/agents/api";
|
||||
import { useAgent } from "@/core/agents/hooks";
|
||||
import { apiFetch } from "@/core/api";
|
||||
import { copyTextToClipboard } from "@/core/clipboard";
|
||||
import { siteStudioRoute, useWebAppCapability } from "@/core/sites/api";
|
||||
import { apiFetch } from "@/core/api";
|
||||
import { copyTextToClipboard } from "@/core/clipboard";
|
||||
import { siteStudioRoute, useWebAppCapability } from "@/core/sites/api";
|
||||
import {
|
||||
DOCUMENT_REWRITE_LOCAL_UNDO_GUARD_EVENT,
|
||||
DOCUMENT_REWRITE_STARTED_EVENT,
|
||||
@@ -87,11 +88,7 @@ import {
|
||||
import { isArtifactApiPath } from "@/core/artifacts/streaming-write-preview";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||
import {
|
||||
SafeStreamdown,
|
||||
streamdownPlugins,
|
||||
useStreamdownMermaidPngDownload,
|
||||
} from "@/core/streamdown";
|
||||
import { streamdownPlugins, useStreamdownMermaidPngDownload } from "@/core/streamdown";
|
||||
import {
|
||||
checkCodeFile,
|
||||
getFileName,
|
||||
@@ -1457,9 +1454,9 @@ export function ArtifactFileDetail({
|
||||
toast.error("文件内容为空,暂时无法在新窗口打开");
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([
|
||||
prepareGeneratedHtml(source),
|
||||
], {
|
||||
const blob = new Blob([
|
||||
prepareGeneratedHtml(source),
|
||||
], {
|
||||
type: "text/html;charset=utf-8",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -1486,11 +1483,11 @@ export function ArtifactFileDetail({
|
||||
: language === "markdown"
|
||||
? "text/markdown;charset=utf-8"
|
||||
: "text/plain;charset=utf-8";
|
||||
const downloadSource =
|
||||
language === "html"
|
||||
? prepareGeneratedHtml(source)
|
||||
: source;
|
||||
const blob = new Blob([downloadSource], { type: blobType });
|
||||
const downloadSource =
|
||||
language === "html"
|
||||
? prepareGeneratedHtml(source)
|
||||
: source;
|
||||
const blob = new Blob([downloadSource], { type: blobType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
@@ -2140,9 +2137,9 @@ export function ArtifactFilePreview({
|
||||
// <meta charset> 就会中文乱码。与下方下载/新窗口打开的 blobType 保持一致。
|
||||
// Intercept in-page anchor clicks so they scroll within the iframe instead
|
||||
// of navigating the sandboxed blob: document (which blanks the frame).
|
||||
const blob = new Blob([
|
||||
prepareGeneratedHtml(content ?? ""),
|
||||
], {
|
||||
const blob = new Blob([
|
||||
prepareGeneratedHtml(content ?? ""),
|
||||
], {
|
||||
type: "text/html;charset=utf-8",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -2195,7 +2192,7 @@ export function ArtifactFilePreview({
|
||||
animated && "animate-[sandbox-preview-enter_320ms_ease-out]",
|
||||
)}
|
||||
>
|
||||
<SafeStreamdown
|
||||
<Streamdown
|
||||
className="size-full"
|
||||
{...streamdownPlugins}
|
||||
// react-markdown's safe default strips unknown protocols before the
|
||||
@@ -2220,7 +2217,7 @@ export function ArtifactFilePreview({
|
||||
}}
|
||||
>
|
||||
{previewContent}
|
||||
</SafeStreamdown>
|
||||
</Streamdown>
|
||||
{activeCursor && (
|
||||
<span className="mt-2 inline-block h-5 w-0.5 animate-pulse rounded-full bg-[#9b7a2f]" />
|
||||
)}
|
||||
|
||||
@@ -20,17 +20,12 @@ import {
|
||||
mergeArtifactPaths,
|
||||
} from "@/core/artifacts/listing";
|
||||
import {
|
||||
latestCompletedArtifactMutationKey,
|
||||
latestInFlightWriteFilePreview,
|
||||
latestPreparedHtmlArtifact,
|
||||
} from "@/core/artifacts/write-file-in-flight";
|
||||
import {
|
||||
isAutoPreviewableArtifactPath,
|
||||
languageOfStreamingArtifact,
|
||||
} from "@/core/artifacts/streaming-write-preview";
|
||||
import { shouldAutoOpenWritingArtifact } from "@/core/threads/writing-mode";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { resolveRagModeRequested, useLocalSettings } from "@/core/settings";
|
||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { filterKnowledgeWikiSources } from "@/core/llmwiki/knowledge-reference";
|
||||
@@ -152,11 +147,11 @@ const ChatBox: React.FC<{
|
||||
const { data: citationDisplay } = useCitationDisplaySettings();
|
||||
const referenceModeEntryEnabled =
|
||||
citationDisplay?.reference_mode_enabled ?? false;
|
||||
const ragModeEnabled = resolveRagModeRequested({
|
||||
referenceModeEntryEnabled,
|
||||
ragModeEnabled: settings.display.ragModeEnabled,
|
||||
knowledgeSelected: knowledgeReferencesEnabled || !!knowledgeReferences,
|
||||
});
|
||||
const ragModeEnabled =
|
||||
(referenceModeEntryEnabled && settings.display.ragModeEnabled) ||
|
||||
isThreadRagModeLocked(threadId) ||
|
||||
knowledgeReferencesEnabled ||
|
||||
!!knowledgeReferences;
|
||||
// Frontend RAG modes write their own hidden source tool messages. They use
|
||||
// the existing reference panel, but must not request or write reference
|
||||
// batches through DeerFlow's backend APIs.
|
||||
@@ -534,7 +529,7 @@ const ChatBox: React.FC<{
|
||||
!disableArtifacts &&
|
||||
nextArtifacts.length > 0 &&
|
||||
((artifactDirectoryModeEnabled && threadChanged) ||
|
||||
(hasNewArtifact || !hadArtifacts))
|
||||
((hasNewArtifact || !hadArtifacts) && !selectedArtifact))
|
||||
) {
|
||||
// The preferred deliverable wins over the generic "newest artifact"
|
||||
// heuristic. Both effects can run in the same commit with a stale null
|
||||
@@ -542,42 +537,16 @@ const ChatBox: React.FC<{
|
||||
// file such as research-plan.json from overwriting report.md.
|
||||
const automaticTarget =
|
||||
availablePreferredArtifact ?? nextArtifacts[nextArtifacts.length - 1]!;
|
||||
const isGeneratedHtml =
|
||||
languageOfStreamingArtifact(automaticTarget) === "html" &&
|
||||
isAutoPreviewableArtifactPath(automaticTarget);
|
||||
if (
|
||||
shouldAutoOpenWritingArtifact(
|
||||
automaticTarget,
|
||||
thread.messages,
|
||||
writingModeEnabled,
|
||||
) &&
|
||||
(isGeneratedHtml || !selectedArtifact)
|
||||
)
|
||||
) {
|
||||
if (isGeneratedHtml) {
|
||||
// HTML is the requested end product, so a later webpage should
|
||||
// replace a previously selected document automatically. Preserve a
|
||||
// matching in-flight write key so a user-closed live preview stays
|
||||
// closed after the tool call completes; otherwise a completed
|
||||
// tool-call id makes a later write to the same path a new preview.
|
||||
const revision = latestCompletedArtifactMutationKey(
|
||||
{ messages: thread.messages ?? [] },
|
||||
automaticTarget,
|
||||
);
|
||||
const previewKey =
|
||||
selectedArtifact?.startsWith("write-file:") &&
|
||||
artifactPathname(selectedArtifact) === artifactPathname(automaticTarget) &&
|
||||
selectedArtifactKey
|
||||
? selectedArtifactKey
|
||||
: revision ?? `artifact:${threadId}:${artifactPathname(automaticTarget)}`;
|
||||
openStreamedArtifact(
|
||||
automaticTarget,
|
||||
previewKey,
|
||||
);
|
||||
} else {
|
||||
selectArtifact(automaticTarget);
|
||||
setArtifactsOpen(true);
|
||||
setSideTab("document");
|
||||
}
|
||||
selectArtifact(automaticTarget);
|
||||
setArtifactsOpen(true);
|
||||
setSideTab("document");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,12 +579,9 @@ const ChatBox: React.FC<{
|
||||
autoSelectFirstArtifact,
|
||||
deselect,
|
||||
selectArtifact,
|
||||
openStreamedArtifact,
|
||||
selectedArtifact,
|
||||
selectedArtifactKey,
|
||||
artifacts,
|
||||
setArtifacts,
|
||||
setArtifactsOpen,
|
||||
availableArtifacts,
|
||||
availablePreferredArtifact,
|
||||
disableArtifacts,
|
||||
@@ -802,13 +768,13 @@ const ChatBox: React.FC<{
|
||||
description="Select an artifact to view its details"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex size-full max-w-[var(--container-width-sm)] flex-col justify-center p-4 pt-8">
|
||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
||||
<header className="shrink-0">
|
||||
<h2 className="text-lg font-medium">Artifacts</h2>
|
||||
</header>
|
||||
<main className="min-h-0 grow">
|
||||
<ArtifactFileList
|
||||
className="max-w-[var(--container-width-sm)] p-4 pt-12"
|
||||
className="max-w-(--container-width-sm) p-4 pt-12"
|
||||
files={artifacts}
|
||||
threadId={threadId}
|
||||
/>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
// Node's type-stripping test runner requires the explicit TypeScript suffix.
|
||||
// @ts-expect-error The application compiler intentionally uses extensionless imports.
|
||||
import { hasPromptInputPayload } from "./input-box-submit.ts";
|
||||
|
||||
test("allows attachment-only prompt submissions", () => {
|
||||
assert.equal(hasPromptInputPayload({ text: "", files: [{}] }), true);
|
||||
});
|
||||
|
||||
test("rejects submissions with neither text nor attachments", () => {
|
||||
assert.equal(hasPromptInputPayload({ text: "", files: [] }), false);
|
||||
assert.equal(hasPromptInputPayload({ text: " \n", files: [] }), false);
|
||||
});
|
||||
|
||||
test("allows text-only prompt submissions", () => {
|
||||
assert.equal(hasPromptInputPayload({ text: "总结一下", files: [] }), true);
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
export type PromptInputPayload = {
|
||||
text: string;
|
||||
files: readonly unknown[];
|
||||
};
|
||||
|
||||
/** A prompt may contain text, attachments, or both. */
|
||||
export function hasPromptInputPayload(message: PromptInputPayload): boolean {
|
||||
return message.text.trim().length > 0 || message.files.length > 0;
|
||||
}
|
||||
@@ -92,12 +92,15 @@ import {
|
||||
|
||||
import { useRecommendedQuestions } from "@/core/recommended-questions/hooks";
|
||||
import { useUserPreferences } from "@/core/user-preferences";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import {
|
||||
isThreadRagModeLocked,
|
||||
lockThreadRagMode,
|
||||
useLocalSettings,
|
||||
} from "@/core/settings";
|
||||
import { useCitationDisplaySettings } from "@/core/system-settings/hooks";
|
||||
|
||||
import { CustomPromptBar } from "./custom-prompt-menu";
|
||||
import { resolveInputMode, type InputMode } from "./input-mode";
|
||||
import { hasPromptInputPayload } from "./input-box-submit";
|
||||
import { useThread } from "./messages/context";
|
||||
import { ModeHoverGuide } from "./mode-hover-guide";
|
||||
import { Tooltip } from "./tooltip";
|
||||
@@ -382,13 +385,14 @@ export function InputBox({
|
||||
});
|
||||
}, [artifactDirectoryModeEnabled, setLocalSettings]);
|
||||
|
||||
const ragModeEnabled =
|
||||
referenceModeEntryEnabled && localSettings.display.ragModeEnabled;
|
||||
const ragModeLocked = isThreadRagModeLocked(threadId);
|
||||
const ragModeEnabled = localSettings.display.ragModeEnabled || ragModeLocked;
|
||||
const handleRagModeToggle = useCallback(() => {
|
||||
setLocalSettings("display", {
|
||||
ragModeEnabled: !localSettings.display.ragModeEnabled,
|
||||
});
|
||||
}, [localSettings.display.ragModeEnabled, setLocalSettings]);
|
||||
if (ragModeLocked) {
|
||||
return;
|
||||
}
|
||||
setLocalSettings("display", { ragModeEnabled: !ragModeEnabled });
|
||||
}, [ragModeEnabled, ragModeLocked, setLocalSettings]);
|
||||
|
||||
const contextCompressionEnabled =
|
||||
localSettings.display.contextCompressionEnabled ?? false;
|
||||
@@ -404,12 +408,16 @@ export function InputBox({
|
||||
onStop?.();
|
||||
return;
|
||||
}
|
||||
if (!hasPromptInputPayload(message)) {
|
||||
return;
|
||||
}
|
||||
if (!message.text) {
|
||||
return;
|
||||
}
|
||||
setFollowups([]);
|
||||
setFollowupsHidden(false);
|
||||
setFollowupsLoading(false);
|
||||
if (ragModeEnabled) {
|
||||
lockThreadRagMode(threadId);
|
||||
}
|
||||
|
||||
// Guard against submitting before the initial model auto-selection
|
||||
// effect has flushed thread settings to storage/state.
|
||||
if (resolvedModelName && context.model_name !== resolvedModelName) {
|
||||
@@ -434,9 +442,11 @@ export function InputBox({
|
||||
onSubmit,
|
||||
onStop,
|
||||
resolvedModelName,
|
||||
ragModeEnabled,
|
||||
selectedModel?.supports_thinking,
|
||||
selectedModel?.execution_profile,
|
||||
status,
|
||||
threadId,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -697,7 +707,7 @@ export function InputBox({
|
||||
// 对话态:常规白卡 + 中性边框 + 轻阴影
|
||||
isNewThread
|
||||
? "rounded-3xl bg-transparent border-0 shadow-none ring-0"
|
||||
: "rounded-3xl bg-background border border-border shadow-sm transition-shadow hover:shadow-md focus-within:shadow-md dark:bg-slate-800 dark:border-slate-700",
|
||||
: "rounded-3xl bg-background border border-border shadow-sm transition-shadow hover:shadow-md focus-within:border-ring/40 focus-within:shadow-md focus-within:ring-1 focus-within:ring-ring/20 dark:bg-slate-800 dark:border-slate-700",
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
@@ -715,8 +725,8 @@ export function InputBox({
|
||||
<PromptInputTextarea
|
||||
className={cn(
|
||||
"size-full px-2 py-1 text-[15px] leading-7 placeholder:text-muted-foreground/60",
|
||||
isNewThread && "!min-h-[110px]",
|
||||
compact && "!min-h-[84px] px-3 pt-3 pb-1 text-[14px]",
|
||||
isNewThread && "min-h-[110px]!",
|
||||
compact && "min-h-[84px]! px-3 pt-3 pb-1 text-[14px]",
|
||||
)}
|
||||
disabled={disabled}
|
||||
placeholder={t.inputBox.placeholder}
|
||||
@@ -745,9 +755,9 @@ export function InputBox({
|
||||
{!frontendRagActive && (
|
||||
<AddAttachmentsButton
|
||||
className={cn(
|
||||
"rounded-full border-0 bg-transparent !px-2.5 shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
||||
"rounded-full border-0 bg-transparent px-2.5! shadow-none text-muted-foreground hover:bg-muted/70 hover:text-foreground dark:text-slate-400 dark:hover:bg-slate-700/70 dark:hover:text-slate-100",
|
||||
compact &&
|
||||
"!gap-1.5 rounded-lg !px-2.5 !py-1 text-[11px] font-medium",
|
||||
"gap-1.5! rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -816,10 +826,11 @@ export function InputBox({
|
||||
<PromptInputButton
|
||||
type="button"
|
||||
aria-pressed={ragModeEnabled}
|
||||
disabled={disabled}
|
||||
disabled={ragModeLocked || disabled}
|
||||
className={cn(
|
||||
"gap-1.5! rounded-full px-3! shadow-none",
|
||||
compact && "rounded-lg px-2.5! py-1! text-[11px] font-medium",
|
||||
ragModeLocked && "cursor-not-allowed opacity-80",
|
||||
ragModeEnabled
|
||||
? "border border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-200"
|
||||
: compact
|
||||
|
||||
@@ -6,18 +6,17 @@ import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { canAccessAssistantKnowledge } from "@/core/llmwiki/assistant-knowledge-access-policy";
|
||||
import {
|
||||
useAssistantKnowledgeVisibility,
|
||||
useUpdateAssistantKnowledgeVisibility,
|
||||
} from "@/core/system-settings/hooks";
|
||||
import { useLlmWikiRuntime } from "@/strategy-components/api/llmwiki";
|
||||
import {
|
||||
loadKnowledgeBaseProductVersion,
|
||||
loadOrdinaryKnowledgeLanding,
|
||||
saveOrdinaryKnowledgeLanding,
|
||||
type OrdinaryKnowledgeLanding,
|
||||
type KnowledgeBaseProductVersion,
|
||||
updateKnowledgeBaseProductVersion,
|
||||
} from "@/core/system-settings/api";
|
||||
|
||||
export const KNOWLEDGE_PRODUCT_KEY = ["system-settings", "knowledge-base-product-version"] as const;
|
||||
const LANDING_KEY = ["system-settings", "ordinary-knowledge-landing"] as const;
|
||||
|
||||
export function useOrdinaryKnowledgeLanding() {
|
||||
@@ -26,11 +25,15 @@ export function useOrdinaryKnowledgeLanding() {
|
||||
export const KNOWLEDGE_NORMAL_VIEW_VALUE = "normal";
|
||||
export const KNOWLEDGE_NORMAL_VIEW_SEARCH = `?view=${KNOWLEDGE_NORMAL_VIEW_VALUE}`;
|
||||
|
||||
export function useKnowledgeBaseProductVersion() {
|
||||
return useQuery({ queryKey: KNOWLEDGE_PRODUCT_KEY, queryFn: loadKnowledgeBaseProductVersion, staleTime: 60_000 });
|
||||
}
|
||||
|
||||
export function KnowledgeProductNav() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const client = useQueryClient();
|
||||
const assistantVisibility = useAssistantKnowledgeVisibility();
|
||||
const version = useKnowledgeBaseProductVersion();
|
||||
const landing = useOrdinaryKnowledgeLanding();
|
||||
const saveLanding = useMutation({
|
||||
mutationFn: saveOrdinaryKnowledgeLanding,
|
||||
@@ -40,11 +43,13 @@ export function KnowledgeProductNav() {
|
||||
const runtime = useLlmWikiRuntime();
|
||||
const normalAvailable = runtime.data?.provider === "weknora" && Boolean(runtime.data.api_base_url?.trim());
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const assistantAllowed = canAccessAssistantKnowledge(
|
||||
isAdmin,
|
||||
assistantVisibility.isError ? undefined : assistantVisibility.data,
|
||||
);
|
||||
const saveAssistantVisibility = useUpdateAssistantKnowledgeVisibility();
|
||||
const save = useMutation({
|
||||
mutationFn: updateKnowledgeBaseProductVersion,
|
||||
onSuccess: (value) => {
|
||||
client.setQueryData(KNOWLEDGE_PRODUCT_KEY, value);
|
||||
toast.success(`默认知识库体验已切换为 ${value}`);
|
||||
},
|
||||
});
|
||||
const retrievalDebug = location.pathname.includes("/knowledge/retrieval-debug");
|
||||
const assistant = location.pathname.includes("/knowledge/assistant");
|
||||
const ordinary = !assistant && !retrievalDebug;
|
||||
@@ -54,16 +59,12 @@ export function KnowledgeProductNav() {
|
||||
<Button size="sm" disabled={!normalAvailable} title={normalAvailable ? "普通知识库" : "管理员配置知识服务地址后可用"} variant={ordinary ? "secondary" : "ghost"} onClick={() => navigate(`/page/workspace/knowledge${KNOWLEDGE_NORMAL_VIEW_SEARCH}`)}>
|
||||
<BookOpen />普通知识库
|
||||
</Button>
|
||||
{assistantAllowed && (
|
||||
<Button size="sm" variant={assistant ? "secondary" : "ghost"} onClick={() => navigate("/page/workspace/knowledge/assistant")}>
|
||||
<Database />助手知识库
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Button size="sm" variant={retrievalDebug ? "secondary" : "ghost"} onClick={() => navigate("/page/workspace/knowledge/retrieval-debug")}>
|
||||
<SearchCheck />检索调试
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" variant={assistant ? "secondary" : "ghost"} onClick={() => navigate("/page/workspace/knowledge/assistant")}>
|
||||
<Database />助手知识库
|
||||
</Button>
|
||||
<Button size="sm" variant={retrievalDebug ? "secondary" : "ghost"} onClick={() => navigate("/page/workspace/knowledge/retrieval-debug")}>
|
||||
<SearchCheck />检索调试
|
||||
</Button>
|
||||
{isAdmin && (
|
||||
<div className="ml-auto flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
普通知识库默认页面
|
||||
@@ -71,19 +72,10 @@ export function KnowledgeProductNav() {
|
||||
<SelectTrigger className="h-8 w-40" aria-label="普通知识库默认页面"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="wiki">本系统 Wiki 索引</SelectItem><SelectItem value="weknora">知识库管理页面</SelectItem></SelectContent>
|
||||
</Select>
|
||||
助手知识库日常展示
|
||||
<Select
|
||||
disabled={assistantVisibility.isLoading || saveAssistantVisibility.isPending}
|
||||
value={assistantVisibility.data === true ? "visible" : "hidden"}
|
||||
onValueChange={(value) =>
|
||||
saveAssistantVisibility.mutate(value === "visible", {
|
||||
onSuccess: (visible) => toast.success(visible ? "已向普通用户开放助手知识库" : "已隐藏普通用户的助手知识库"),
|
||||
onError: (error) => toast.error(error.message),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-52" aria-label="助手知识库日常展示"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="hidden">隐藏(仅管理员可用)</SelectItem><SelectItem value="visible">开放给普通用户</SelectItem></SelectContent>
|
||||
默认产品版本
|
||||
<Select value={version.data ?? "v1"} onValueChange={(value) => save.mutate(value as KnowledgeBaseProductVersion)}>
|
||||
<SelectTrigger className="h-8 w-40"><SelectValue /></SelectTrigger>
|
||||
<SelectContent><SelectItem value="v1">v1 普通知识库</SelectItem><SelectItem value="v2">v2 助手知识库</SelectItem></SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
classifyKnowledgeWikiSource,
|
||||
filterKnowledgeWikiSources,
|
||||
} from "@/core/llmwiki/knowledge-reference";
|
||||
import { resolveDisplaySourceChunk } from "@/core/llmwiki/source-excerpt";
|
||||
import {
|
||||
formatRetrievalScore,
|
||||
retrievalModeLabel,
|
||||
@@ -173,7 +172,6 @@ export function KnowledgeWikiPanel({
|
||||
})
|
||||
: null;
|
||||
const openContent = openSource ? sourceContent(openSource) : "";
|
||||
const openChunk = openSource ? resolveDisplaySourceChunk(openSource) : "";
|
||||
const selectedSpaceCount =
|
||||
selection.knowledgeBaseIds.length +
|
||||
selection.assistantKnowledgeBaseIds.length;
|
||||
@@ -240,10 +238,7 @@ export function KnowledgeWikiPanel({
|
||||
open
|
||||
target={previewTarget}
|
||||
title={openSource.title}
|
||||
quote={openChunk}
|
||||
collapseRetrievedExcerptByDefault={
|
||||
classifyKnowledgeWikiSource(openSource) === "wiki"
|
||||
}
|
||||
quote={openContent}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setOpenSourceKey(null);
|
||||
}}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useAssistantKnowledgeAccess } from "@/core/llmwiki/assistant-knowledge-access";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
type AssistantKnowledgeBase,
|
||||
@@ -37,7 +36,6 @@ import {
|
||||
type SelectorSource = "all" | "agent";
|
||||
type SelectorVariant = "popover" | "panel";
|
||||
const EMPTY_IDS: string[] = [];
|
||||
const IGNORE_ASSISTANT_CHANGE = () => undefined;
|
||||
|
||||
export function buildKnowledgeSpaceQuestions(names: string[]): string[] {
|
||||
const cleanNames = names.map((name) => name.trim()).filter(Boolean);
|
||||
@@ -393,7 +391,7 @@ export function LlmWikiKnowledgeSelector({
|
||||
value,
|
||||
assistantValue = EMPTY_IDS,
|
||||
onChange,
|
||||
onAssistantChange,
|
||||
onAssistantChange = () => undefined,
|
||||
onSuggestedQuestionsChange,
|
||||
source = "all",
|
||||
agentId,
|
||||
@@ -425,11 +423,6 @@ export function LlmWikiKnowledgeSelector({
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
const assistantAccess = useAssistantKnowledgeAccess({
|
||||
enabled: onAssistantChange !== undefined,
|
||||
});
|
||||
const assistantAllowed =
|
||||
onAssistantChange !== undefined && assistantAccess.canAccess;
|
||||
const runtime = useLlmWikiRuntime();
|
||||
const weknoraEnabled = runtime.data?.provider === "weknora";
|
||||
const shouldLoadOptions =
|
||||
@@ -447,28 +440,23 @@ export function LlmWikiKnowledgeSelector({
|
||||
weknoraEnabled && shouldLoadOptions && source === "agent" && !!agentId,
|
||||
);
|
||||
const assistantBases = useAssistantKnowledgeBases(
|
||||
assistantAllowed &&
|
||||
(shouldLoadOptions ||
|
||||
runtime.isError ||
|
||||
(!runtime.isLoading && !weknoraEnabled)),
|
||||
shouldLoadOptions ||
|
||||
runtime.isError ||
|
||||
(!runtime.isLoading && !weknoraEnabled),
|
||||
);
|
||||
// Stable empty results are essential: suggested questions are reported to
|
||||
// the parent, so a fresh [] on every render creates an effect/update loop
|
||||
// before lazy knowledge queries have loaded (or when they are disabled).
|
||||
const rows = useMemo(() => weknoraEnabled ? (all.data ?? []) : [], [all.data, weknoraEnabled]);
|
||||
const assistantRows = useMemo(
|
||||
() => assistantAllowed ? (assistantBases.data ?? []) : [],
|
||||
[assistantAllowed, assistantBases.data],
|
||||
);
|
||||
const effectiveAssistantValue = assistantAllowed ? assistantValue : EMPTY_IDS;
|
||||
const assistantRows = useMemo(() => assistantBases.data ?? [], [assistantBases.data]);
|
||||
const defaultIds = useMemo(() => {
|
||||
if (source !== "agent") {
|
||||
return rows.map((item) => item.id);
|
||||
}
|
||||
return (agent.data ?? []).map((item) => item.id);
|
||||
}, [agent.data, rows, source]);
|
||||
const loading = runtime.isLoading || (weknoraEnabled && all.isLoading) || (assistantAllowed && assistantBases.isLoading) || (source === "agent" && !!agentId && agent.isLoading);
|
||||
const error = (weknoraEnabled ? all.error ?? (source === "agent" ? agent.error : null) : null) ?? (assistantAllowed ? assistantBases.error : null);
|
||||
const loading = runtime.isLoading || (weknoraEnabled && all.isLoading) || assistantBases.isLoading || (source === "agent" && !!agentId && agent.isLoading);
|
||||
const error = (weknoraEnabled ? all.error ?? (source === "agent" ? agent.error : null) : null) ?? assistantBases.error;
|
||||
const availableIds = useMemo(() => new Set(rows.map((item) => item.id)), [rows]);
|
||||
const assistantAvailableIds = useMemo(
|
||||
() => new Set(assistantRows.map((item) => item.id)),
|
||||
@@ -481,8 +469,8 @@ export function LlmWikiKnowledgeSelector({
|
||||
[rows, value],
|
||||
);
|
||||
const selectedAssistantNames = useMemo(
|
||||
() => effectiveAssistantValue.map((id) => assistantRows.find((item) => item.id === id)?.name).filter((name): name is string => !!name),
|
||||
[assistantRows, effectiveAssistantValue],
|
||||
() => assistantValue.map((id) => assistantRows.find((item) => item.id === id)?.name).filter((name): name is string => !!name),
|
||||
[assistantRows, assistantValue],
|
||||
);
|
||||
const allSelectedNames = useMemo(
|
||||
() => [...selectedNames, ...selectedAssistantNames],
|
||||
@@ -494,13 +482,13 @@ export function LlmWikiKnowledgeSelector({
|
||||
);
|
||||
const suggestedQuestions = useMemo(
|
||||
() => {
|
||||
if (value.length === 1 && effectiveAssistantValue.length === 0 && selectedConfiguredQuestions.length > 0) {
|
||||
if (value.length === 1 && assistantValue.length === 0 && selectedConfiguredQuestions.length > 0) {
|
||||
return selectedConfiguredQuestions.slice(0, 12);
|
||||
}
|
||||
const general = buildKnowledgeSpaceQuestions(allSelectedNames);
|
||||
return Array.from(new Set([...selectedConfiguredQuestions, ...general])).slice(0, 12);
|
||||
},
|
||||
[allSelectedNames, effectiveAssistantValue.length, selectedConfiguredQuestions, value.length],
|
||||
[allSelectedNames, assistantValue.length, selectedConfiguredQuestions, value.length],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -524,12 +512,12 @@ export function LlmWikiKnowledgeSelector({
|
||||
}, [agent.data, agentId, all.data, autoSelectAll, availableIds, defaultIds, initializationKey, onChange, runtime.isLoading, source, value, weknoraEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!assistantAllowed || !assistantBases.data || !onAssistantChange) return;
|
||||
const valid = effectiveAssistantValue.filter((id) => assistantAvailableIds.has(id));
|
||||
if (valid.length !== effectiveAssistantValue.length || valid.some((id, index) => id !== effectiveAssistantValue[index])) {
|
||||
if (!assistantBases.data) return;
|
||||
const valid = assistantValue.filter((id) => assistantAvailableIds.has(id));
|
||||
if (valid.length !== assistantValue.length || valid.some((id, index) => id !== assistantValue[index])) {
|
||||
onAssistantChange(valid);
|
||||
}
|
||||
}, [assistantAllowed, assistantAvailableIds, assistantBases.data, effectiveAssistantValue, onAssistantChange]);
|
||||
}, [assistantAvailableIds, assistantBases.data, assistantValue, onAssistantChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setSearch("");
|
||||
@@ -538,7 +526,7 @@ export function LlmWikiKnowledgeSelector({
|
||||
if (
|
||||
!runtime.isLoading &&
|
||||
(runtime.isError || !weknoraEnabled) &&
|
||||
(!assistantAllowed || !assistantBases.isLoading) &&
|
||||
!assistantBases.isLoading &&
|
||||
assistantRows.length === 0
|
||||
) return null;
|
||||
|
||||
@@ -547,9 +535,9 @@ export function LlmWikiKnowledgeSelector({
|
||||
rows={rows}
|
||||
assistantRows={assistantRows}
|
||||
value={value}
|
||||
assistantValue={effectiveAssistantValue}
|
||||
assistantValue={assistantValue}
|
||||
onChange={onChange}
|
||||
onAssistantChange={onAssistantChange ?? IGNORE_ASSISTANT_CHANGE}
|
||||
onAssistantChange={onAssistantChange}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
loading={loading}
|
||||
@@ -572,14 +560,14 @@ export function LlmWikiKnowledgeSelector({
|
||||
{panelDescription}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="shrink-0">{value.length + effectiveAssistantValue.length} 个</Badge>
|
||||
<Badge variant="secondary" className="shrink-0">{value.length + assistantValue.length} 个</Badge>
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedCount = value.length + effectiveAssistantValue.length;
|
||||
const selectedCount = value.length + assistantValue.length;
|
||||
const selectedLabel = allSelectedNames.length > 0
|
||||
? allSelectedNames.join("、")
|
||||
: selectedCount > 0
|
||||
|
||||
@@ -12,13 +12,7 @@ import {
|
||||
MessageResponse,
|
||||
type MessageResponseProps,
|
||||
} from "@/components/ai-elements/message";
|
||||
import {
|
||||
looksLikeHtmlDocument,
|
||||
MarkdownFallback,
|
||||
MarkdownRenderBoundary,
|
||||
streamdownPlugins,
|
||||
useStreamdownMermaidPngDownload,
|
||||
} from "@/core/streamdown";
|
||||
import { streamdownPlugins, useStreamdownMermaidPngDownload } from "@/core/streamdown";
|
||||
import {
|
||||
createMarkdownAnchorResolver,
|
||||
isInPageAnchor,
|
||||
@@ -150,14 +144,6 @@ export function MarkdownContent({
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
// Full HTML pages belong in the artifact iframe. Feeding them to Streamdown
|
||||
// mid-stream can recurse forever in marked and white-screen the chat.
|
||||
if (looksLikeHtmlDocument(content)) {
|
||||
return (
|
||||
<MarkdownFallback message="正在生成 HTML 页面,请查看右侧预览。" />
|
||||
);
|
||||
}
|
||||
|
||||
// Preserve the exact legacy rendering path for ordinary Markdown. Heavy
|
||||
// visualization renderers are lazy-loaded only when a dedicated fence exists.
|
||||
if (
|
||||
@@ -166,17 +152,15 @@ export function MarkdownContent({
|
||||
) {
|
||||
return (
|
||||
<div ref={rootRef} data-markdown-anchor-root className="contents">
|
||||
<MarkdownRenderBoundary resetKey={`${content.length}:${content.slice(-32)}`}>
|
||||
<MessageResponse
|
||||
className={className}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
mermaidConfig={streamdownPlugins.mermaidConfig}
|
||||
components={components}
|
||||
>
|
||||
{content}
|
||||
</MessageResponse>
|
||||
</MarkdownRenderBoundary>
|
||||
<MessageResponse
|
||||
className={className}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
mermaidConfig={streamdownPlugins.mermaidConfig}
|
||||
components={components}
|
||||
>
|
||||
{content}
|
||||
</MessageResponse>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -193,27 +177,16 @@ export function MarkdownContent({
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : segment.content ? (
|
||||
looksLikeHtmlDocument(segment.content) ? (
|
||||
<MarkdownFallback
|
||||
key={`markdown-${index}`}
|
||||
message="正在生成 HTML 页面,请查看右侧预览。"
|
||||
/>
|
||||
) : (
|
||||
<MarkdownRenderBoundary
|
||||
key={`markdown-${index}`}
|
||||
resetKey={`${segment.content.length}:${segment.content.slice(-32)}`}
|
||||
>
|
||||
<MessageResponse
|
||||
className={className}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
mermaidConfig={streamdownPlugins.mermaidConfig}
|
||||
components={components}
|
||||
>
|
||||
{segment.content}
|
||||
</MessageResponse>
|
||||
</MarkdownRenderBoundary>
|
||||
)
|
||||
<MessageResponse
|
||||
key={`markdown-${index}`}
|
||||
className={className}
|
||||
remarkPlugins={remarkPlugins}
|
||||
rehypePlugins={rehypePlugins}
|
||||
mermaidConfig={streamdownPlugins.mermaidConfig}
|
||||
components={components}
|
||||
>
|
||||
{segment.content}
|
||||
</MessageResponse>
|
||||
) : null,
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Loader } from '@/components/ai-elements/loader';
|
||||
import {
|
||||
Message as AIElementMessage,
|
||||
MessageContent as AIElementMessageContent,
|
||||
MessageResponse as AIElementMessageResponse,
|
||||
MessageToolbar,
|
||||
} from '@/components/ai-elements/message';
|
||||
import {
|
||||
@@ -67,21 +68,22 @@ import {
|
||||
applyInlineCitations,
|
||||
type RuntimeDisplaySource,
|
||||
} from '@/core/skill-display/runtime';
|
||||
import { humanMessagePlugins } from '@/core/streamdown';
|
||||
import {
|
||||
getMultiRoundRagTrace,
|
||||
isMultiRoundRagMessage,
|
||||
} from '@/core/threads/use-multi-round-rag';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AnswerFeedbackContext } from '@/strategy-components/api/answer-feedback';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AnswerFeedbackContext } from '@/strategy-components/api/answer-feedback';
|
||||
|
||||
import { CitationLink } from '../citations/citation-link';
|
||||
import { CopyButton } from '../copy-button';
|
||||
import { Tooltip } from '../tooltip';
|
||||
import { SkillDisplayBlock } from '../skill-display-block';
|
||||
import { ThreadContext } from './context';
|
||||
import { AnswerFeedbackControl } from './answer-feedback-control';
|
||||
import { isEligibleAnswerFeedbackMessage } from './answer-feedback-eligibility';
|
||||
import { DeleteMessageButton } from './delete-message-button';
|
||||
import { ThreadContext } from './context';
|
||||
import { AnswerFeedbackControl } from './answer-feedback-control';
|
||||
import { isEligibleAnswerFeedbackMessage } from './answer-feedback-eligibility';
|
||||
import { DeleteMessageButton } from './delete-message-button';
|
||||
import { DownloadWordButton } from './download-word-button';
|
||||
import { MarkdownContent, type MarkdownContentProps } from './markdown-content';
|
||||
import { MessageTokenUsage } from './message-token-usage';
|
||||
@@ -164,11 +166,11 @@ export function MessageListItem({
|
||||
extraRehypePlugins,
|
||||
onEditResubmit,
|
||||
onEditSave,
|
||||
onMessageDeleted,
|
||||
turnTime,
|
||||
answerFeedbackEnabled = false,
|
||||
answerFeedbackContext,
|
||||
}: {
|
||||
onMessageDeleted,
|
||||
turnTime,
|
||||
answerFeedbackEnabled = false,
|
||||
answerFeedbackContext,
|
||||
}: {
|
||||
className?: string;
|
||||
fitRowToContent?: boolean;
|
||||
/**
|
||||
@@ -193,12 +195,12 @@ export function MessageListItem({
|
||||
* timestamp of its own — without it the answer would fall back to the current
|
||||
* wall clock at render time and appear to "drift" to now on every reload.
|
||||
*/
|
||||
turnTime?: string;
|
||||
/** Opt-in feedback affordance for completed, persisted assistant answers. */
|
||||
answerFeedbackEnabled?: boolean;
|
||||
/** Authoritative run/agent mapping hydrated in one conversation batch. */
|
||||
answerFeedbackContext?: AnswerFeedbackContext;
|
||||
}) {
|
||||
turnTime?: string;
|
||||
/** Opt-in feedback affordance for completed, persisted assistant answers. */
|
||||
answerFeedbackEnabled?: boolean;
|
||||
/** Authoritative run/agent mapping hydrated in one conversation batch. */
|
||||
answerFeedbackContext?: AnswerFeedbackContext;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const isHuman = message.type === 'human';
|
||||
const messageMarkdown =
|
||||
@@ -215,16 +217,16 @@ export function MessageListItem({
|
||||
}
|
||||
}, [assistantEditing, messageMarkdown]);
|
||||
|
||||
const canSaveAssistantEdit =
|
||||
const canSaveAssistantEdit =
|
||||
!isLoading &&
|
||||
!isHuman &&
|
||||
!!onEditSave &&
|
||||
!!message.id &&
|
||||
!message.id.startsWith('opt-') &&
|
||||
!!messageMarkdown;
|
||||
const canEvaluateAnswer =
|
||||
answerFeedbackEnabled &&
|
||||
isEligibleAnswerFeedbackMessage(message, Boolean(isLoading));
|
||||
!!messageMarkdown;
|
||||
const canEvaluateAnswer =
|
||||
answerFeedbackEnabled &&
|
||||
isEligibleAnswerFeedbackMessage(message, Boolean(isLoading));
|
||||
const confirmAssistantEdit = useCallback(async () => {
|
||||
const value = assistantDraft.trim();
|
||||
if (!value || !message.id || !onEditSave || assistantSaving) {
|
||||
@@ -320,28 +322,28 @@ export function MessageListItem({
|
||||
onMessageDeleted={onMessageDeleted}
|
||||
turnTime={turnTime}
|
||||
/>
|
||||
{/*
|
||||
Human messages carry their own inline copy/edit toolbar inside
|
||||
MessageContent (above the timestamp). Only assistant answers use the
|
||||
always-visible floating bottom toolbar here.
|
||||
*/}
|
||||
{/*
|
||||
Human messages carry their own inline copy/edit toolbar inside
|
||||
MessageContent (above the timestamp). Only assistant answers use the
|
||||
floating bottom toolbar here.
|
||||
*/}
|
||||
{!isLoading && !isHuman && (
|
||||
<MessageToolbar
|
||||
className={cn(
|
||||
'-bottom-8',
|
||||
'absolute right-0 left-0 z-20',
|
||||
)}
|
||||
className={cn(
|
||||
'-bottom-8',
|
||||
'absolute right-0 left-0 z-20 opacity-0 transition-opacity delay-200 duration-300 group-hover/conversation-message:opacity-100',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
<CopyButton clipboardData={messageMarkdown} />
|
||||
{canEvaluateAnswer && message.id && answerFeedbackContext && (
|
||||
<AnswerFeedbackControl
|
||||
threadId={threadId}
|
||||
messageId={message.id}
|
||||
context={answerFeedbackContext}
|
||||
/>
|
||||
)}
|
||||
{messageMarkdown && (
|
||||
<div className="flex gap-1">
|
||||
<CopyButton clipboardData={messageMarkdown} />
|
||||
{canEvaluateAnswer && message.id && answerFeedbackContext && (
|
||||
<AnswerFeedbackControl
|
||||
threadId={threadId}
|
||||
messageId={message.id}
|
||||
context={answerFeedbackContext}
|
||||
/>
|
||||
)}
|
||||
{messageMarkdown && (
|
||||
<DownloadWordButton
|
||||
markdown={messageMarkdown}
|
||||
fileName="message"
|
||||
@@ -372,7 +374,7 @@ export function MessageListItem({
|
||||
/**
|
||||
* Inline copy/edit affordances + editor for the user's own question bubble.
|
||||
*
|
||||
* Layout: bubble → toolbar (copy + edit) → timestamp. The toolbar sits
|
||||
* Layout: bubble → hover toolbar (copy + edit) → timestamp. The toolbar sits
|
||||
* in normal flow above the timestamp (not floating) so it never covers the
|
||||
* following message. Clicking edit turns the bubble into a textarea seeded with
|
||||
* the question; confirming sends the edited text as a brand-new turn (the
|
||||
@@ -384,6 +386,7 @@ function HumanMessageBlock({
|
||||
pageContext,
|
||||
filesList,
|
||||
messageTime,
|
||||
components,
|
||||
onEditResubmit,
|
||||
onEditSave,
|
||||
deleteButton,
|
||||
@@ -393,7 +396,7 @@ function HumanMessageBlock({
|
||||
pageContext?: EmbeddedPageContext | null;
|
||||
filesList: ReactNode;
|
||||
messageTime: { label: string; full: string } | null;
|
||||
components?: MarkdownContentProps["components"];
|
||||
components: MarkdownContentProps["components"];
|
||||
onEditResubmit?: (text: string) => void;
|
||||
onEditSave?: (messageId: string, text: string) => void | Promise<void>;
|
||||
deleteButton?: ReactNode;
|
||||
@@ -473,9 +476,14 @@ function HumanMessageBlock({
|
||||
}
|
||||
|
||||
const messageResponse = text ? (
|
||||
<div className="whitespace-pre-wrap break-words text-[15px] leading-7">
|
||||
<AIElementMessageResponse
|
||||
remarkPlugins={humanMessagePlugins.remarkPlugins}
|
||||
rehypePlugins={humanMessagePlugins.rehypePlugins}
|
||||
components={components}
|
||||
parseIncompleteMarkdown={false}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
</AIElementMessageResponse>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
@@ -487,9 +495,9 @@ function HumanMessageBlock({
|
||||
</AIElementMessageContent>
|
||||
)}
|
||||
<MessageTimeStamp time={messageTime} align="right" />
|
||||
{pageContext && <EmbeddedPageContextCard pageContext={pageContext} />}
|
||||
{(text || deleteButton) && (
|
||||
<div className="flex items-center gap-0.5">
|
||||
{pageContext && <EmbeddedPageContextCard pageContext={pageContext} />}
|
||||
{(text || deleteButton) && (
|
||||
<div className="flex items-center gap-0.5 opacity-0 transition-opacity duration-200 group-hover/conversation-message:opacity-100">
|
||||
{text && <CopyButton clipboardData={text} className="size-6" />}
|
||||
{text && (onEditResubmit || onEditSave) && (
|
||||
<Tooltip content={onEditSave ? "编辑消息" : t.inputBox.editQuestion}>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function MultiRoundRagMessageList({
|
||||
return (
|
||||
<Conversation className={cn("flex size-full flex-col justify-center", className)}>
|
||||
<ConversationContent
|
||||
className="mx-auto w-full max-w-[var(--container-width-lg)] gap-8 pt-10"
|
||||
className="mx-auto w-full max-w-(--container-width-lg) gap-8 pt-10"
|
||||
>
|
||||
{messages.map((message, index) => {
|
||||
const isLastAssistant =
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const STAGGER_MS = 60;
|
||||
|
||||
function SkeletonBar({
|
||||
className,
|
||||
style,
|
||||
originRight,
|
||||
}: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
originRight?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`animate-skeleton-entrance fill-mode-[forwards] overflow-hidden rounded-md ${originRight ? "origin-[right]" : "origin-[left]"} ${className ?? ""}`}
|
||||
style={{ opacity: 0, ...style }}
|
||||
>
|
||||
<Skeleton className="h-full w-full rounded-md" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageListSkeleton() {
|
||||
let index = 0;
|
||||
return (
|
||||
<div className="flex w-full max-w-[var(--container-width-lg)] flex-col gap-12 p-8 pt-16">
|
||||
<div
|
||||
role="human-message"
|
||||
className="flex w-[50%] flex-col items-end gap-2 self-end"
|
||||
>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
originRight
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[80%]"
|
||||
originRight
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
</div>
|
||||
<div role="assistant-message" className="flex flex-col gap-2">
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[70%]"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[60%]"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[40%]"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const STAGGER_MS = 60;
|
||||
|
||||
function SkeletonBar({
|
||||
className,
|
||||
style,
|
||||
originRight,
|
||||
}: {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
originRight?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`animate-skeleton-entrance fill-mode-[forwards] overflow-hidden rounded-md ${originRight ? "origin-[right]" : "origin-[left]"} ${className ?? ""}`}
|
||||
style={{ opacity: 0, ...style }}
|
||||
>
|
||||
<Skeleton className="h-full w-full rounded-md" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MessageListSkeleton() {
|
||||
let index = 0;
|
||||
return (
|
||||
<div className="flex w-full max-w-(--container-width-lg) flex-col gap-12 p-8 pt-16">
|
||||
<div
|
||||
role="human-message"
|
||||
className="flex w-[50%] flex-col items-end gap-2 self-end"
|
||||
>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
originRight
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[80%]"
|
||||
originRight
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
</div>
|
||||
<div role="assistant-message" className="flex flex-col gap-2">
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[70%]"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-full"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[60%]"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
<SkeletonBar
|
||||
className="h-6 w-[40%]"
|
||||
style={{ animationDelay: `${index++ * STAGGER_MS}ms` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
XCircleIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import {
|
||||
ChainOfThought,
|
||||
ChainOfThoughtContent,
|
||||
@@ -17,10 +19,7 @@ import { ShineBorder } from "@/components/ui/shine-border";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { hasToolCalls } from "@/core/messages/utils";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import {
|
||||
SafeStreamdown,
|
||||
streamdownPluginsWithWordAnimation,
|
||||
} from "@/core/streamdown";
|
||||
import { streamdownPluginsWithWordAnimation } from "@/core/streamdown";
|
||||
import { useSubtask } from "@/core/tasks/context";
|
||||
import { explainLastToolCall } from "@/core/tools/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -127,12 +126,12 @@ export function SubtaskCard({
|
||||
{task.prompt && (
|
||||
<ChainOfThoughtStep
|
||||
label={
|
||||
<SafeStreamdown
|
||||
<Streamdown
|
||||
{...streamdownPluginsWithWordAnimation}
|
||||
components={{ a: CitationLink }}
|
||||
>
|
||||
{task.prompt}
|
||||
</SafeStreamdown>
|
||||
</Streamdown>
|
||||
}
|
||||
></ChainOfThoughtStep>
|
||||
)}
|
||||
|
||||
@@ -72,7 +72,6 @@ import { ViewModeToggle } from "@/components/workspace/view-mode-toggle";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { MINIMAL_SKILL_MD_EXAMPLE } from "@/core/skill-example";
|
||||
import type { ValidateSkillUploadResponse } from "@/core/skills/api";
|
||||
import {
|
||||
useDeleteBuiltinSkill,
|
||||
@@ -421,6 +420,7 @@ function SkillSettingsList({
|
||||
theme === "dark" ? rescanErrorToastDarkOptions : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
@@ -1693,16 +1693,6 @@ function InstallSkillDialog({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border bg-muted/30 p-3">
|
||||
<div className="text-sm font-medium">{dialog.exampleTitle}</div>
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
{dialog.exampleHint}
|
||||
</p>
|
||||
<pre className="mt-2 overflow-x-auto rounded bg-background px-3 py-2 text-xs leading-5">
|
||||
<code>{MINIMAL_SKILL_MD_EXAMPLE}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="text-muted-foreground shrink-0">{dialog.targetCategoryLabel}</span>
|
||||
|
||||
@@ -8,31 +8,21 @@ export interface TagColorDef {
|
||||
chip: string;
|
||||
/** A solid swatch used in the colour picker. */
|
||||
swatch: string;
|
||||
/** Real border hex — Tailwind `border-*-200` is often not generated, so `* { border-color: var(--border) }` greys every chip. */
|
||||
border: string;
|
||||
borderDark: string;
|
||||
}
|
||||
|
||||
export const TAG_COLORS: TagColorDef[] = [
|
||||
{ name: "gray", chip: "bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-700", swatch: "bg-gray-400", border: "#d1d5db", borderDark: "#4b5563" },
|
||||
{ name: "red", chip: "bg-red-100 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-900", swatch: "bg-red-500", border: "#fca5a5", borderDark: "#7f1d1d" },
|
||||
{ name: "orange", chip: "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-900", swatch: "bg-orange-500", border: "#fdba74", borderDark: "#7c2d12" },
|
||||
{ name: "amber", chip: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-900", swatch: "bg-amber-500", border: "#fcd34d", borderDark: "#78350f" },
|
||||
{ name: "green", chip: "bg-green-100 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-900", swatch: "bg-green-500", border: "#86efac", borderDark: "#14532d" },
|
||||
{ name: "teal", chip: "bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-950 dark:text-teal-300 dark:border-teal-900", swatch: "bg-teal-500", border: "#5eead4", borderDark: "#134e4a" },
|
||||
{ name: "blue", chip: "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-900", swatch: "bg-blue-500", border: "#93c5fd", borderDark: "#1e3a8a" },
|
||||
{ name: "indigo", chip: "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-900", swatch: "bg-indigo-500", border: "#a5b4fc", borderDark: "#312e81" },
|
||||
{ name: "purple", chip: "bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-900", swatch: "bg-purple-500", border: "#d8b4fe", borderDark: "#581c87" },
|
||||
{ name: "pink", chip: "bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-950 dark:text-pink-300 dark:border-pink-900", swatch: "bg-pink-500", border: "#f9a8d4", borderDark: "#831843" },
|
||||
{ name: "gray", chip: "bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-700", swatch: "bg-gray-400" },
|
||||
{ name: "red", chip: "bg-red-100 text-red-700 border-red-200 dark:bg-red-950 dark:text-red-300 dark:border-red-900", swatch: "bg-red-500" },
|
||||
{ name: "orange", chip: "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-950 dark:text-orange-300 dark:border-orange-900", swatch: "bg-orange-500" },
|
||||
{ name: "amber", chip: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-900", swatch: "bg-amber-500" },
|
||||
{ name: "green", chip: "bg-green-100 text-green-700 border-green-200 dark:bg-green-950 dark:text-green-300 dark:border-green-900", swatch: "bg-green-500" },
|
||||
{ name: "teal", chip: "bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-950 dark:text-teal-300 dark:border-teal-900", swatch: "bg-teal-500" },
|
||||
{ name: "blue", chip: "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-950 dark:text-blue-300 dark:border-blue-900", swatch: "bg-blue-500" },
|
||||
{ name: "indigo", chip: "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-950 dark:text-indigo-300 dark:border-indigo-900", swatch: "bg-indigo-500" },
|
||||
{ name: "purple", chip: "bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-950 dark:text-purple-300 dark:border-purple-900", swatch: "bg-purple-500" },
|
||||
{ name: "pink", chip: "bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-950 dark:text-pink-300 dark:border-pink-900", swatch: "bg-pink-500" },
|
||||
];
|
||||
|
||||
export function tagBorderStyle(color: TagColorDef) {
|
||||
return {
|
||||
"--lam-tag-border": color.border,
|
||||
"--lam-tag-border-dark": color.borderDark,
|
||||
} as Record<string, string>;
|
||||
}
|
||||
|
||||
const BY_NAME = new Map(TAG_COLORS.map((c) => [c.name, c]));
|
||||
|
||||
export function resolveTagColor(name: string | undefined | null): TagColorDef {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { BookOpen, BotIcon, BrainIcon, FileSearch, FileText, FolderKanban, Landmark, Library, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react";
|
||||
import { BookOpen, BotIcon, BrainIcon, FileSearch, FileText, FolderKanban, Library, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, Plus, SparklesIcon, Telescope, Users } from "lucide-react";
|
||||
|
||||
import { KnowledgeRagToggle } from "./knowledge-rag-toggle";
|
||||
import { SidebarCollapseButton } from "./workspace-header";
|
||||
@@ -112,13 +112,6 @@ export const WORKSPACE_COMPACT_NAV_ITEMS: WorkspaceCompactNavItem[] = [
|
||||
path: "/page/workspace/deep-research",
|
||||
match: (pathname) => pathname.startsWith("/page/workspace/deep-research"),
|
||||
},
|
||||
{
|
||||
id: "workspace-gongwen",
|
||||
label: "公文助手",
|
||||
icon: Landmark,
|
||||
path: "/page/workspace/gongwen",
|
||||
match: (pathname) => pathname.startsWith("/page/workspace/gongwen"),
|
||||
},
|
||||
// {
|
||||
// id: "workspace-report-collaboration",
|
||||
// label: "报告协作",
|
||||
|
||||
@@ -150,7 +150,7 @@ export function WorkspaceNavMenu() {
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[var(--radix-dropdown-menu-trigger-width)] min-w-56 rounded-lg"
|
||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarRail,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { GeneralQaSidebar } from "./general-qa-sidebar";
|
||||
import { RecentChatList } from "./recent-chat-list";
|
||||
import { WorkspaceHeader } from "./workspace-header";
|
||||
import { WorkspaceNavChatList } from "./workspace-nav-chat-list";
|
||||
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarHeader,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarRail,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { GeneralQaSidebar } from "./general-qa-sidebar";
|
||||
import { RecentChatList } from "./recent-chat-list";
|
||||
import { WorkspaceHeader } from "./workspace-header";
|
||||
import { WorkspaceNavChatList } from "./workspace-nav-chat-list";
|
||||
import { WorkspaceNavMenu } from "./workspace-nav-menu";
|
||||
|
||||
export function WorkspaceSidebar({
|
||||
compactChrome = false,
|
||||
forceCollapsed = false,
|
||||
@@ -30,115 +30,115 @@ export function WorkspaceSidebar({
|
||||
/** Enables wx's shared Agent Gallery category selection. */
|
||||
wxCommonAgents?: boolean;
|
||||
}) {
|
||||
const { open: isSidebarOpen } = useSidebar();
|
||||
const { theme } = useTheme();
|
||||
const pathname = usePathname();
|
||||
const useWhiteSidebar = compactChrome && theme === "light";
|
||||
// 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。
|
||||
// 仅在通用问答(/page/workspace/chats*)下启用三块式布局;点击常用智能体后是
|
||||
// 「原地嵌入」(停留在 /chats/new?agent=...,仅右侧问答区换成智能体对话),左侧栏不变。
|
||||
const { open: isSidebarOpen } = useSidebar();
|
||||
const { theme } = useTheme();
|
||||
const pathname = usePathname();
|
||||
const useWhiteSidebar = compactChrome && theme === "light";
|
||||
// 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。
|
||||
// 仅在通用问答(/page/workspace/chats*)下启用三块式布局;点击常用智能体后是
|
||||
// 「原地嵌入」(停留在 /chats/new?agent=...,仅右侧问答区换成智能体对话),左侧栏不变。
|
||||
const isGeneralQa =
|
||||
wxCommonAgents ||
|
||||
pathname.startsWith("/page/workspace/chats") ||
|
||||
pathname.startsWith("/page/strategy/chats");
|
||||
|
||||
if (forceCollapsed) {
|
||||
return (
|
||||
<Sidebar
|
||||
variant="sidebar"
|
||||
collapsible="icon"
|
||||
className={cn(
|
||||
"bg-sidebar [&_[data-slot=sidebar-inner]]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent>
|
||||
<WorkspaceNavChatList />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
// 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。
|
||||
// collapsible="none" 的 Sidebar 是普通 in-flow 定宽块(非 absolute,故不受本布局对
|
||||
// sidebar-container 的 absolute/left-0 覆盖影响),外层 overflow-hidden 把它从
|
||||
// --sidebar-width 裁到 0、聊天区随宽度平滑回流;收起后整列不接收指针。
|
||||
if (compactChrome) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden={!isSidebarOpen}
|
||||
className={cn(
|
||||
"h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out",
|
||||
isSidebarOpen ? "w-[var(--sidebar-width)]" : "w-0 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<Sidebar
|
||||
variant="sidebar"
|
||||
collapsible="none"
|
||||
className={cn(
|
||||
"h-full w-[var(--sidebar-width)] border-r border-border",
|
||||
useWhiteSidebar &&
|
||||
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent>
|
||||
{isGeneralQa ? (
|
||||
// 全量版通用问答(/page/workspace/chats*):新建对话侧栏独立成
|
||||
// GeneralQaSidebar(按 0616 样式参考图重画),与其余 workspace 页解耦。
|
||||
|
||||
if (forceCollapsed) {
|
||||
return (
|
||||
<Sidebar
|
||||
variant="sidebar"
|
||||
collapsible="icon"
|
||||
className={cn(
|
||||
"bg-sidebar [&_[data-slot=sidebar-inner]]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent>
|
||||
<WorkspaceNavChatList />
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
// 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。
|
||||
// collapsible="none" 的 Sidebar 是普通 in-flow 定宽块(非 absolute,故不受本布局对
|
||||
// sidebar-container 的 absolute/left-0 覆盖影响),外层 overflow-hidden 把它从
|
||||
// --sidebar-width 裁到 0、聊天区随宽度平滑回流;收起后整列不接收指针。
|
||||
if (compactChrome) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden={!isSidebarOpen}
|
||||
className={cn(
|
||||
"h-full shrink-0 overflow-hidden transition-[width] duration-200 ease-out",
|
||||
isSidebarOpen ? "w-(--sidebar-width)" : "w-0 pointer-events-none",
|
||||
)}
|
||||
>
|
||||
<Sidebar
|
||||
variant="sidebar"
|
||||
collapsible="none"
|
||||
className={cn(
|
||||
"h-full w-(--sidebar-width) border-r border-border",
|
||||
useWhiteSidebar &&
|
||||
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SidebarContent>
|
||||
{isGeneralQa ? (
|
||||
// 全量版通用问答(/page/workspace/chats*):新建对话侧栏独立成
|
||||
// GeneralQaSidebar(按 0616 样式参考图重画),与其余 workspace 页解耦。
|
||||
<GeneralQaSidebar wxCommonAgents={wxCommonAgents} />
|
||||
) : (
|
||||
<>
|
||||
<WorkspaceNavChatList showCollapseTrigger compact />
|
||||
<RecentChatList compact />
|
||||
</>
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
variant="sidebar"
|
||||
// 紧凑模式:展开时为固定宽度整列(none,无内置折叠),收起由上面的条件渲染处理;
|
||||
// 非紧凑沿用 icon。
|
||||
collapsible={compactChrome ? "none" : "icon"}
|
||||
className={cn(
|
||||
useWhiteSidebar &&
|
||||
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!compactChrome && (
|
||||
<SidebarHeader className="py-0">
|
||||
<WorkspaceHeader />
|
||||
</SidebarHeader>
|
||||
)}
|
||||
<SidebarContent>
|
||||
<WorkspaceNavChatList
|
||||
showCollapseTrigger={compactChrome}
|
||||
compact={compactChrome}
|
||||
/>
|
||||
{isSidebarOpen && <RecentChatList compact={compactChrome} />}
|
||||
{/* 「多智能体会商」「最近写作」历史入口已移至「对话历史」页
|
||||
(/page/workspace/chats 的 Tabs,2026-08 需求):简洁模式(本分支,
|
||||
无全局导航栏/页面侧栏时的工作区侧栏)不再在左下角展示这两个分组,
|
||||
全量模式本就不展示。 */}
|
||||
</SidebarContent>
|
||||
{!compactChrome && (
|
||||
<SidebarFooter>
|
||||
<WorkspaceNavMenu />
|
||||
</SidebarFooter>
|
||||
)}
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
) : (
|
||||
<>
|
||||
<WorkspaceNavChatList showCollapseTrigger compact />
|
||||
<RecentChatList compact />
|
||||
</>
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar
|
||||
variant="sidebar"
|
||||
// 紧凑模式:展开时为固定宽度整列(none,无内置折叠),收起由上面的条件渲染处理;
|
||||
// 非紧凑沿用 icon。
|
||||
collapsible={compactChrome ? "none" : "icon"}
|
||||
className={cn(
|
||||
useWhiteSidebar &&
|
||||
"[&_[data-slot=sidebar-inner]]:bg-white [&_[data-slot=sidebar-container]]:bg-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!compactChrome && (
|
||||
<SidebarHeader className="py-0">
|
||||
<WorkspaceHeader />
|
||||
</SidebarHeader>
|
||||
)}
|
||||
<SidebarContent>
|
||||
<WorkspaceNavChatList
|
||||
showCollapseTrigger={compactChrome}
|
||||
compact={compactChrome}
|
||||
/>
|
||||
{isSidebarOpen && <RecentChatList compact={compactChrome} />}
|
||||
{/* 「多智能体会商」「最近写作」历史入口已移至「对话历史」页
|
||||
(/page/workspace/chats 的 Tabs,2026-08 需求):简洁模式(本分支,
|
||||
无全局导航栏/页面侧栏时的工作区侧栏)不再在左下角展示这两个分组,
|
||||
全量模式本就不展示。 */}
|
||||
</SidebarContent>
|
||||
{!compactChrome && (
|
||||
<SidebarFooter>
|
||||
<WorkspaceNavMenu />
|
||||
</SidebarFooter>
|
||||
)}
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiFetch } from "@/core/api";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
@@ -41,99 +41,6 @@ export type ProjectThread = {
|
||||
updated_at: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
export type ProjectMemoryKind =
|
||||
| "fact"
|
||||
| "decision"
|
||||
| "constraint"
|
||||
| "preference"
|
||||
| "open_item"
|
||||
| "term"
|
||||
| "episode"
|
||||
| string;
|
||||
export type ProjectMemorySourceRef = {
|
||||
kind?: string;
|
||||
thread_id?: string;
|
||||
message_id?: string;
|
||||
run_id?: string;
|
||||
title?: string;
|
||||
excerpt?: string;
|
||||
};
|
||||
export type ProjectMemoryItem = {
|
||||
id: string;
|
||||
kind: ProjectMemoryKind;
|
||||
logical_key?: string | null;
|
||||
content: string;
|
||||
status: string;
|
||||
confidence: number;
|
||||
importance: number;
|
||||
pinned: boolean;
|
||||
source_thread_id?: string | null;
|
||||
source_message_ids?: string[];
|
||||
source_run_id?: string | null;
|
||||
source_refs?: ProjectMemorySourceRef[];
|
||||
metadata?: Record<string, unknown>;
|
||||
superseded_by?: string | null;
|
||||
revision?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
export type ProjectMemorySnapshot = {
|
||||
summary?: string;
|
||||
goal?: string;
|
||||
goals?: string[];
|
||||
facts?: string[];
|
||||
confirmed_facts?: string[];
|
||||
decisions?: string[];
|
||||
constraints?: string[];
|
||||
preferences?: string[];
|
||||
terms?: string[];
|
||||
open_items?: string[];
|
||||
episodes?: string[];
|
||||
statistics?: {
|
||||
total?: number;
|
||||
by_kind?: Record<string, number>;
|
||||
truncated?: boolean;
|
||||
};
|
||||
generated_from_revision?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
export type ProjectMemory = {
|
||||
project_id?: string;
|
||||
enabled: boolean;
|
||||
/** Legacy compatibility flag; project memory is controlled independently. */
|
||||
available?: boolean;
|
||||
runtime_enabled?: boolean;
|
||||
global_enabled?: boolean;
|
||||
effective_enabled?: boolean;
|
||||
settings?: {
|
||||
item_limit?: number;
|
||||
episode_limit?: number;
|
||||
token_budget?: number;
|
||||
};
|
||||
status: string;
|
||||
revision: number;
|
||||
item_count: number;
|
||||
summary: string;
|
||||
snapshot?: string | ProjectMemorySnapshot | null;
|
||||
items: ProjectMemoryItem[];
|
||||
last_built_at?: string | null;
|
||||
rebuilt_at?: string | null;
|
||||
rebuild_requested_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
last_error?: string | null;
|
||||
};
|
||||
export type ProjectMemoryItemInput = {
|
||||
kind: ProjectMemoryKind;
|
||||
content: string;
|
||||
logical_key?: string;
|
||||
confidence?: number;
|
||||
importance?: number;
|
||||
pinned?: boolean;
|
||||
};
|
||||
export type ProjectMemoryItemPatch = Partial<ProjectMemoryItemInput>;
|
||||
export type ProjectMemoryRebuildResult = ProjectMemory & {
|
||||
queued_jobs: number;
|
||||
};
|
||||
export const PROJECT_MODES = {
|
||||
qa: { name: "问答模式", description: "围绕项目资料问答、解释与总结" },
|
||||
strategy: {
|
||||
@@ -174,7 +81,6 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
: `项目操作失败(${response.status})`,
|
||||
);
|
||||
}
|
||||
if (response.status === 204) return undefined as T;
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
export function useAnalysisProjects(
|
||||
@@ -215,137 +121,6 @@ export function useProjectThreadAccess(projectId: string, threadId?: string) {
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
export const projectMemoryKey = (projectId: string) =>
|
||||
[...projectKeys(), projectId, "memory"] as const;
|
||||
|
||||
export function useProjectMemory(
|
||||
projectId: string,
|
||||
options: { enabled?: boolean } = {},
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: projectMemoryKey(projectId),
|
||||
queryFn: () =>
|
||||
request<ProjectMemory>(`/${encodeURIComponent(projectId)}/memory`),
|
||||
enabled: !!projectId && (options.enabled ?? true),
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data?.status;
|
||||
return status &&
|
||||
["queued", "pending", "building", "rebuilding", "recovering"].includes(status)
|
||||
? 2000
|
||||
: false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const updateProjectMemorySettings = (
|
||||
projectId: string,
|
||||
settings: { enabled: boolean },
|
||||
) =>
|
||||
request<ProjectMemory>(
|
||||
`/${encodeURIComponent(projectId)}/memory/settings`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(settings),
|
||||
},
|
||||
);
|
||||
|
||||
export const rebuildProjectMemory = (projectId: string) =>
|
||||
request<ProjectMemoryRebuildResult>(
|
||||
`/${encodeURIComponent(projectId)}/memory/rebuild`,
|
||||
{ method: "POST" },
|
||||
);
|
||||
|
||||
export const clearProjectMemory = (projectId: string) =>
|
||||
request<ProjectMemory>(`/${encodeURIComponent(projectId)}/memory`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
export const createProjectMemoryItem = (
|
||||
projectId: string,
|
||||
input: ProjectMemoryItemInput,
|
||||
) =>
|
||||
request<ProjectMemoryItem>(
|
||||
`/${encodeURIComponent(projectId)}/memory/items`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
},
|
||||
);
|
||||
|
||||
export const updateProjectMemoryItem = (
|
||||
projectId: string,
|
||||
itemId: string,
|
||||
patch: ProjectMemoryItemPatch,
|
||||
) =>
|
||||
request<ProjectMemoryItem>(
|
||||
`/${encodeURIComponent(projectId)}/memory/items/${encodeURIComponent(itemId)}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(patch),
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteProjectMemoryItem = (
|
||||
projectId: string,
|
||||
itemId: string,
|
||||
) =>
|
||||
request<void>(
|
||||
`/${encodeURIComponent(projectId)}/memory/items/${encodeURIComponent(itemId)}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
|
||||
function useProjectMemoryMutation<TVariables>(
|
||||
projectId: string,
|
||||
mutationFn: (variables: TVariables) => Promise<unknown>,
|
||||
) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: projectMemoryKey(projectId),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateProjectMemorySettings(projectId: string) {
|
||||
return useProjectMemoryMutation(projectId, (settings: { enabled: boolean }) =>
|
||||
updateProjectMemorySettings(projectId, settings),
|
||||
);
|
||||
}
|
||||
|
||||
export function useRebuildProjectMemory(projectId: string) {
|
||||
return useProjectMemoryMutation(projectId, () =>
|
||||
rebuildProjectMemory(projectId),
|
||||
);
|
||||
}
|
||||
|
||||
export function useClearProjectMemory(projectId: string) {
|
||||
return useProjectMemoryMutation(projectId, () =>
|
||||
clearProjectMemory(projectId),
|
||||
);
|
||||
}
|
||||
|
||||
export function useCreateProjectMemoryItem(projectId: string) {
|
||||
return useProjectMemoryMutation(projectId, (input: ProjectMemoryItemInput) =>
|
||||
createProjectMemoryItem(projectId, input),
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdateProjectMemoryItem(projectId: string) {
|
||||
return useProjectMemoryMutation(
|
||||
projectId,
|
||||
({ itemId, patch }: { itemId: string; patch: ProjectMemoryItemPatch }) =>
|
||||
updateProjectMemoryItem(projectId, itemId, patch),
|
||||
);
|
||||
}
|
||||
|
||||
export function useDeleteProjectMemoryItem(projectId: string) {
|
||||
return useProjectMemoryMutation(projectId, (itemId: string) =>
|
||||
deleteProjectMemoryItem(projectId, itemId),
|
||||
);
|
||||
}
|
||||
export const createAnalysisProject = (
|
||||
fields: ProjectFields & { mode: ProjectMode },
|
||||
) =>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
// @ts-ignore Node's type-stripping runner requires the explicit TypeScript suffix.
|
||||
import {
|
||||
projectMemoryActionAvailability,
|
||||
projectMemorySnapshotSections,
|
||||
projectMemoryStatus,
|
||||
} from "./memory-status.ts";
|
||||
|
||||
const base = {
|
||||
enabled: true,
|
||||
global_enabled: true,
|
||||
status: "empty",
|
||||
item_count: 0,
|
||||
};
|
||||
|
||||
test("project memory ignores legacy system-memory flags", () => {
|
||||
assert.equal(
|
||||
projectMemoryStatus({ ...base, global_enabled: false }).label,
|
||||
"等待沉淀",
|
||||
);
|
||||
assert.equal(
|
||||
projectMemoryStatus({
|
||||
...base,
|
||||
available: false,
|
||||
runtime_enabled: false,
|
||||
}).label,
|
||||
"等待沉淀",
|
||||
);
|
||||
assert.equal(projectMemoryStatus({ ...base, enabled: false }).label, "已暂停");
|
||||
});
|
||||
|
||||
test("project memory exposes building, error, ready and empty states", () => {
|
||||
assert.equal(projectMemoryStatus({ ...base, status: "building" }).label, "整理中");
|
||||
assert.equal(projectMemoryStatus({ ...base, status: "recovering" }).label, "整理中");
|
||||
assert.equal(projectMemoryStatus({ ...base, status: "error" }).label, "异常");
|
||||
assert.equal(projectMemoryStatus({ ...base, item_count: 2 }).label, "已就绪");
|
||||
assert.equal(projectMemoryStatus(base).label, "等待沉淀");
|
||||
});
|
||||
|
||||
test("project memory renders only public snapshot sections", () => {
|
||||
assert.deepEqual(
|
||||
projectMemorySnapshotSections({
|
||||
summary: "已沉淀 2 条项目记忆。",
|
||||
confirmed_facts: ["代号是海棠", " "],
|
||||
decisions: "采用方案 B",
|
||||
statistics: { total: 2 },
|
||||
generated_from_revision: 3,
|
||||
unexpected_internal_field: "不得显示",
|
||||
}),
|
||||
[
|
||||
{ key: "confirmed_facts", label: "已确认事实", lines: ["代号是海棠"] },
|
||||
{ key: "decisions", label: "决策", lines: ["采用方案 B"] },
|
||||
],
|
||||
);
|
||||
assert.deepEqual(projectMemorySnapshotSections("legacy snapshot"), []);
|
||||
});
|
||||
|
||||
test("project memory actions honor only the project switch without trapping persisted data", () => {
|
||||
assert.deepEqual(
|
||||
projectMemoryActionAvailability({
|
||||
...base,
|
||||
global_enabled: false,
|
||||
item_count: 2,
|
||||
}),
|
||||
{ canRebuild: true, canClear: true },
|
||||
);
|
||||
assert.deepEqual(
|
||||
projectMemoryActionAvailability({ ...base, enabled: false, item_count: 1 }),
|
||||
{ canRebuild: false, canClear: true },
|
||||
);
|
||||
assert.deepEqual(projectMemoryActionAvailability(base), {
|
||||
canRebuild: true,
|
||||
canClear: false,
|
||||
});
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
export type ProjectMemoryStatusInput = {
|
||||
enabled: boolean;
|
||||
/** Legacy server fields; project memory intentionally does not depend on them. */
|
||||
available?: boolean;
|
||||
global_enabled?: boolean;
|
||||
runtime_enabled?: boolean;
|
||||
status: string;
|
||||
item_count: number;
|
||||
};
|
||||
|
||||
export type ProjectMemorySnapshotSection = {
|
||||
key: string;
|
||||
label: string;
|
||||
lines: string[];
|
||||
};
|
||||
|
||||
const SNAPSHOT_SECTIONS = [
|
||||
["goals", "项目目标"],
|
||||
["confirmed_facts", "已确认事实"],
|
||||
["decisions", "决策"],
|
||||
["constraints", "约束"],
|
||||
["preferences", "偏好"],
|
||||
["terms", "术语"],
|
||||
["open_items", "未决事项"],
|
||||
["episodes", "会话结论"],
|
||||
] as const;
|
||||
|
||||
export function projectMemoryStatus(
|
||||
memory?: ProjectMemoryStatusInput,
|
||||
loading = false,
|
||||
failed = false,
|
||||
) {
|
||||
if (loading) return { label: "读取中", tone: "muted" as const };
|
||||
if (failed) return { label: "读取失败", tone: "error" as const };
|
||||
if (!memory) return { label: "未初始化", tone: "muted" as const };
|
||||
if (!memory.enabled) return { label: "已暂停", tone: "muted" as const };
|
||||
if (["queued", "pending", "building", "rebuilding", "recovering"].includes(memory.status))
|
||||
return { label: "整理中", tone: "working" as const };
|
||||
if (["error", "failed"].includes(memory.status))
|
||||
return { label: "异常", tone: "error" as const };
|
||||
if (memory.item_count > 0 || memory.status === "ready")
|
||||
return { label: "已就绪", tone: "ready" as const };
|
||||
return { label: "等待沉淀", tone: "muted" as const };
|
||||
}
|
||||
|
||||
export function projectMemoryActionAvailability(
|
||||
memory?: ProjectMemoryStatusInput,
|
||||
renderedItemCount = 0,
|
||||
) {
|
||||
return {
|
||||
canRebuild: !!memory?.enabled,
|
||||
// Pausing project recall must never trap already-persisted project data.
|
||||
canClear: Math.max(memory?.item_count ?? 0, renderedItemCount) > 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the management UI aligned with the server's public snapshot schema.
|
||||
* Internal bookkeeping fields such as `statistics` and
|
||||
* `generated_from_revision` must not accidentally become user-facing cards.
|
||||
*/
|
||||
export function projectMemorySnapshotSections(
|
||||
snapshot: unknown,
|
||||
): ProjectMemorySnapshotSection[] {
|
||||
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
|
||||
return [];
|
||||
}
|
||||
const source = snapshot as Record<string, unknown>;
|
||||
return SNAPSHOT_SECTIONS.flatMap(([key, label]) => {
|
||||
const value = source[key];
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
const lines = values
|
||||
.filter(
|
||||
(item): item is string | number =>
|
||||
typeof item === "string" || typeof item === "number",
|
||||
)
|
||||
.map(String)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
return lines.length ? [{ key, label, lines }] : [];
|
||||
});
|
||||
}
|
||||
@@ -6,29 +6,9 @@ import { getAuthorizationHeaderValue } from "../auth";
|
||||
import { getLangGraphBaseURL } from "../config";
|
||||
|
||||
import { withCsrfHeaders } from "./csrf";
|
||||
import {
|
||||
rememberRunStreamCursor,
|
||||
resolveRunStreamResumeCursor,
|
||||
type RunStreamCursorStore,
|
||||
} from "./stream-cursor";
|
||||
import { sanitizeRunStreamOptions } from "./stream-mode";
|
||||
|
||||
function streamEventId(event: unknown): string | undefined {
|
||||
if (
|
||||
typeof event === "object" &&
|
||||
event !== null &&
|
||||
"id" in event &&
|
||||
typeof event.id === "string"
|
||||
) {
|
||||
return event.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createCompatibleClient(
|
||||
isMock?: boolean,
|
||||
getRunStreamCursors?: () => RunStreamCursorStore,
|
||||
): LangGraphClient {
|
||||
function createCompatibleClient(isMock?: boolean): LangGraphClient {
|
||||
const client = new LangGraphClient({
|
||||
apiUrl: getLangGraphBaseURL(isMock),
|
||||
onRequest(_url, init) {
|
||||
@@ -46,105 +26,24 @@ function createCompatibleClient(
|
||||
});
|
||||
|
||||
const originalRunStream = client.runs.stream.bind(client.runs);
|
||||
client.runs.stream = (async function* (threadId, assistantId, payload) {
|
||||
// Capture this reader's map. A thread switch replaces the owner's map,
|
||||
// so late events from an old reader cannot repopulate the new scope.
|
||||
const readerCursors = getRunStreamCursors?.();
|
||||
let runId: string | undefined;
|
||||
let effectiveThreadId = threadId ?? undefined;
|
||||
const sanitizedPayload = sanitizeRunStreamOptions(payload);
|
||||
const originalOnRunCreated = sanitizedPayload?.onRunCreated;
|
||||
const stream = originalRunStream(
|
||||
client.runs.stream = ((threadId, assistantId, payload) =>
|
||||
originalRunStream(
|
||||
threadId,
|
||||
assistantId,
|
||||
{
|
||||
...sanitizedPayload,
|
||||
onRunCreated(params) {
|
||||
runId = params.run_id;
|
||||
effectiveThreadId = params.thread_id ?? effectiveThreadId;
|
||||
originalOnRunCreated?.(params);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
for await (const event of stream) {
|
||||
if (readerCursors) {
|
||||
rememberRunStreamCursor(
|
||||
readerCursors,
|
||||
effectiveThreadId,
|
||||
runId,
|
||||
streamEventId(event),
|
||||
);
|
||||
}
|
||||
yield event;
|
||||
}
|
||||
}) as typeof client.runs.stream;
|
||||
sanitizeRunStreamOptions(payload),
|
||||
)) as typeof client.runs.stream;
|
||||
|
||||
const originalJoinStream = client.runs.joinStream.bind(client.runs);
|
||||
client.runs.joinStream = (async function* (threadId, runId, options) {
|
||||
const readerCursors = getRunStreamCursors?.();
|
||||
const sanitizedOptions = sanitizeRunStreamOptions(options);
|
||||
// The shared client preserves -1. Only a client owned by one message
|
||||
// accumulator may resume its SDK-initiated reconnectOnMount retry: that
|
||||
// callback also passes -1 after an error, while retaining previous chunks.
|
||||
const joinOptions =
|
||||
readerCursors && threadId != null && !(sanitizedOptions instanceof AbortSignal)
|
||||
? {
|
||||
...sanitizedOptions,
|
||||
lastEventId: resolveRunStreamResumeCursor(
|
||||
readerCursors,
|
||||
threadId,
|
||||
runId,
|
||||
sanitizedOptions?.lastEventId,
|
||||
),
|
||||
}
|
||||
: sanitizedOptions;
|
||||
if (threadId != null) {
|
||||
const lastEventId =
|
||||
joinOptions instanceof AbortSignal ? undefined : joinOptions?.lastEventId;
|
||||
console.info("[deer-flow] joining resumable run stream", {
|
||||
threadId,
|
||||
runId,
|
||||
hasLastEventId: Boolean(lastEventId && lastEventId !== "-1"),
|
||||
replayFromStart: lastEventId === "-1",
|
||||
});
|
||||
}
|
||||
const stream = originalJoinStream(threadId, runId, joinOptions);
|
||||
|
||||
for await (const event of stream) {
|
||||
if (readerCursors) {
|
||||
rememberRunStreamCursor(
|
||||
readerCursors,
|
||||
threadId,
|
||||
runId,
|
||||
streamEventId(event),
|
||||
);
|
||||
}
|
||||
yield event;
|
||||
}
|
||||
}) as typeof client.runs.joinStream;
|
||||
client.runs.joinStream = ((threadId, runId, options) =>
|
||||
originalJoinStream(
|
||||
threadId,
|
||||
runId,
|
||||
sanitizeRunStreamOptions(options),
|
||||
)) as typeof client.runs.joinStream;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Own this scope alongside exactly one SDK message accumulator, never globally.
|
||||
* Clear it whenever that accumulator clears (a real thread switch). A newly
|
||||
* mounted hook must create a new scope even when it opens the same live run.
|
||||
*/
|
||||
export function createStreamAPIClient(isMock?: boolean) {
|
||||
let cursors: RunStreamCursorStore = new Map();
|
||||
return {
|
||||
client: createCompatibleClient(isMock, () => cursors),
|
||||
getResumeCursor(threadId: string, runId: string): string | undefined {
|
||||
return resolveRunStreamResumeCursor(cursors, threadId, runId, undefined);
|
||||
},
|
||||
clearResumeCursors() {
|
||||
cursors = new Map();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const _clients = new Map<string, LangGraphClient>();
|
||||
export function getAPIClient(isMock?: boolean): LangGraphClient {
|
||||
const cacheKey = isMock ? "mock" : "default";
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { setImmediate as nextTurn } from "node:timers/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { MessageTupleManager, StreamManager, type EventStreamEvent } from "@langchain/langgraph-sdk/ui";
|
||||
|
||||
import { createStreamAPIClient } from "./api-client";
|
||||
|
||||
const threadId = "cursor-replay-thread";
|
||||
const runId = "cursor-replay-run";
|
||||
const parts = [
|
||||
'{"path":"/mnt/user-data/outputs/report.md","content":"# Report',
|
||||
" first half",
|
||||
' second half"}',
|
||||
];
|
||||
|
||||
function frame(index: number): string {
|
||||
return `id: ${index + 1}\nevent: messages\ndata: ${JSON.stringify([
|
||||
{
|
||||
id: "write-message",
|
||||
type: "AIMessageChunk",
|
||||
content: "",
|
||||
additional_kwargs: {},
|
||||
response_metadata: {},
|
||||
tool_call_chunks: [{
|
||||
index: 0,
|
||||
...(index === 0 ? { id: "write-call", name: "write_file" } : {}),
|
||||
args: parts[index],
|
||||
type: "tool_call_chunk",
|
||||
}],
|
||||
},
|
||||
{ langgraph_node: "model", langgraph_checkpoint_ns: "" },
|
||||
])}\n\n`;
|
||||
}
|
||||
|
||||
const responseHeaders = {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Content-Location": `/threads/${threadId}/runs/${runId}`,
|
||||
};
|
||||
type Values = { messages: Message[] };
|
||||
type Reader = StreamManager<Values, {}>;
|
||||
|
||||
function reader(): Reader {
|
||||
return new StreamManager<Values, {}>(new MessageTupleManager(), { throttle: true });
|
||||
}
|
||||
|
||||
function consume(
|
||||
manager: Reader,
|
||||
action: () => Promise<AsyncGenerator<{ event: string; data: unknown; id?: string }>>,
|
||||
) {
|
||||
// RunsClient.joinStream erases the event discriminated union; these fixture
|
||||
// responses contain the same messages tuples as the typed creation stream.
|
||||
return manager.start(async () => await action() as AsyncGenerator<EventStreamEvent<Values, Partial<Values>, unknown>>, {
|
||||
initialValues: { messages: [] },
|
||||
getMessages: (values) => values.messages,
|
||||
setMessages: (values, messages) => ({ ...values, messages }),
|
||||
callbacks: {},
|
||||
onSuccess() {},
|
||||
onError(error) { throw error; },
|
||||
});
|
||||
}
|
||||
|
||||
function assertCompleteToolArguments(manager: Reader) {
|
||||
assert.equal(manager.error, undefined);
|
||||
assert.equal(manager.values?.messages.length, 1);
|
||||
const message = manager.values!.messages[0]!;
|
||||
assert.equal(message.type, "ai");
|
||||
if (message.type !== "ai") throw new Error("Expected an AI tool message");
|
||||
assert.equal(message.tool_calls?.[0]?.name, "write_file");
|
||||
assert.deepEqual(message.tool_calls?.[0]?.args, {
|
||||
path: "/mnt/user-data/outputs/report.md",
|
||||
content: "# Report first half second half",
|
||||
});
|
||||
}
|
||||
|
||||
test("same-reader recovery preserves arguments while fresh readers replay the prefix", async (t) => {
|
||||
const requestedCursors: Array<string | null> = [];
|
||||
t.mock.method(globalThis, "fetch", async (_input, init) => {
|
||||
const cursor = new Headers(init?.headers).get("Last-Event-ID");
|
||||
const isCreate = init?.method === "POST";
|
||||
if (!isCreate) requestedCursors.push(cursor);
|
||||
const start = isCreate || cursor === "-1" || cursor === null ? 0 : Number(cursor);
|
||||
const end = isCreate ? 2 : parts.length;
|
||||
return new Response(
|
||||
Array.from({ length: end - start }, (_, i) => frame(start + i)).join(""),
|
||||
{ headers: responseHeaders },
|
||||
);
|
||||
});
|
||||
|
||||
const scope = createStreamAPIClient();
|
||||
const original = reader();
|
||||
await consume(original, async () => scope.client.runs.stream(threadId, "lead_agent", {
|
||||
streamMode: ["messages-tuple"],
|
||||
streamIdleReconnect: 0,
|
||||
}));
|
||||
assert.equal(scope.getResumeCursor(threadId, runId), "2");
|
||||
|
||||
await consume(original, async () => scope.client.runs.joinStream(threadId, runId, {
|
||||
lastEventId: scope.getResumeCursor(threadId, runId),
|
||||
streamIdleReconnect: 0,
|
||||
}));
|
||||
assertCompleteToolArguments(original);
|
||||
|
||||
// The SDK's own retry also passes -1 while keeping the same accumulator.
|
||||
await consume(original, async () => scope.client.runs.joinStream(threadId, runId, {
|
||||
lastEventId: "-1",
|
||||
streamIdleReconnect: 0,
|
||||
}));
|
||||
assertCompleteToolArguments(original);
|
||||
|
||||
// A newly mounted hook owns a new client scope even for the same live run.
|
||||
const independentScope = createStreamAPIClient();
|
||||
assert.equal(independentScope.getResumeCursor(threadId, runId), undefined);
|
||||
const remounted = reader();
|
||||
await consume(remounted, async () => independentScope.client.runs.joinStream(threadId, runId, {
|
||||
lastEventId: "-1",
|
||||
streamIdleReconnect: 0,
|
||||
}));
|
||||
assertCompleteToolArguments(remounted);
|
||||
|
||||
scope.clearResumeCursors();
|
||||
assert.equal(scope.getResumeCursor(threadId, runId), undefined);
|
||||
const afterThreadSwitch = reader();
|
||||
await consume(afterThreadSwitch, async () => scope.client.runs.joinStream(threadId, runId, {
|
||||
lastEventId: scope.getResumeCursor(threadId, runId) ?? "-1",
|
||||
streamIdleReconnect: 0,
|
||||
}));
|
||||
assertCompleteToolArguments(afterThreadSwitch);
|
||||
assert.deepEqual(requestedCursors, ["2", "3", "-1", "-1"]);
|
||||
});
|
||||
|
||||
test("a cleared scope ignores late cursor writes from its previous reader", async (t) => {
|
||||
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
||||
const body = new ReadableStream<Uint8Array>({ start(value) { controller = value; } });
|
||||
t.mock.method(globalThis, "fetch", async () => new Response(body, { headers: responseHeaders }));
|
||||
const scope = createStreamAPIClient();
|
||||
const running = consume(reader(), async () => scope.client.runs.stream(threadId, "lead_agent", {
|
||||
streamMode: ["messages-tuple"],
|
||||
streamIdleReconnect: 0,
|
||||
}));
|
||||
const encoder = new TextEncoder();
|
||||
controller.enqueue(encoder.encode(frame(0)));
|
||||
// Drain the Promise/SSE work without depending on a wall-clock delay.
|
||||
for (let i = 0; i < 10 && !scope.getResumeCursor(threadId, runId); i += 1) await nextTurn();
|
||||
assert.equal(scope.getResumeCursor(threadId, runId), "1");
|
||||
scope.clearResumeCursors();
|
||||
controller.enqueue(encoder.encode(frame(1)));
|
||||
controller.close();
|
||||
await running;
|
||||
assert.equal(scope.getResumeCursor(threadId, runId), undefined);
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
// @ts-ignore Node's type-stripping runner requires the explicit TypeScript suffix.
|
||||
import * as streamCursor from "./stream-cursor.ts";
|
||||
|
||||
const { rememberRunStreamCursor, resolveRunStreamResumeCursor } = streamCursor;
|
||||
|
||||
test("in-page rejoin resumes after the last consumed SSE event", () => {
|
||||
const cursors = new Map<string, string>();
|
||||
rememberRunStreamCursor(cursors, "thread-1", "run-1", "1720000000000-7");
|
||||
|
||||
assert.equal(
|
||||
resolveRunStreamResumeCursor(cursors, "thread-1", "run-1", "-1"),
|
||||
"1720000000000-7",
|
||||
);
|
||||
});
|
||||
|
||||
test("explicit resume cursor wins and a fresh page may replay from start", () => {
|
||||
const cursors = new Map<string, string>();
|
||||
rememberRunStreamCursor(cursors, "thread-1", "run-1", "event-7");
|
||||
|
||||
assert.equal(
|
||||
resolveRunStreamResumeCursor(cursors, "thread-1", "run-1", "event-3"),
|
||||
"event-3",
|
||||
);
|
||||
assert.equal(
|
||||
resolveRunStreamResumeCursor(cursors, "thread-2", "run-2", "-1"),
|
||||
"-1",
|
||||
);
|
||||
});
|
||||