feat: 导航/侧边栏菜单脱敏 + 圆桌会商各步输入框自适应高度
- 菜单标签展示层脱敏:useSidebarMenuItems 直接用 /api/sensitive-words 结果构造脱敏器,覆盖导航栏/三套侧边栏皮肤/三级子导航;菜单管理页用原始树不受影响 - 新增可复用 AutoGrowTextarea(自适应高度多行框),替换圆桌 Step1/2/3 输入框: 多行时自动扩展、到上限框内滚动;Step2 两个单行 input 改多行;统一 Enter 发送/Shift+Enter 换行 - 各步输入框最大展开高度翻倍(Step1 320 / Step2 280 / Step3 224) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
25e4e3dbc9
commit
6746fe4a78
9 files changed
+141
-29
No files matched your search
@@ -81,7 +81,7 @@ The frontend (`frontend-web/`) is a React 19 + Vite SPA:
|
||||
- **Pages**: `ChatPage` (thread chat), `AgentChatPage` (custom agent chat), `AgentsPage`/`NewAgentPage` (agent management), `ScheduledTasksPage`/`ScheduledTaskRunDetailPage` (scheduled tasks), `LightAppManagementPage`/`LightAppListPage`/`LightAppIframeView` (轻应用中心 — register/list/embed mini-apps)
|
||||
- **轻应用中心 (Light App Center)**: a sibling of 任务管理 in the sidebar with two pages — 应用管理 (admin registers `window_open` or `iframe` mini-apps with a route-param builder: login params resolved from `localStorage.userInfo`, theme params literal) and 应用列表 (card grid of `window_open` apps, `window.open` launch). `iframe` apps are mounted under a chosen parent menu (XK/PG/TY/综合管理) and **dynamically injected into the sidebar** by `core/page-layout/use-sidebar-menu.ts` (`useSidebarMenuItems`), opening in the `LightAppIframeView` host. Client + React Query hooks in `strategy-components/api/light-apps.ts`; URL builder/constants in `strategy-components/lib/light-app.ts`. Backend: `/api/light-apps` (global/shared, admin-gated writes).
|
||||
- **菜单管理 (Menu Management, admin-only)**: an **overlay** over the static sidebar menu — `MenuManagementPage` (`pages/MenuManagementPage.tsx`, route `/page/strategy/admin/menu-management`, opened from the admin-only 「菜单管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`). Admins rename / 停用·启用 / reorder (↑↓ or @dnd-kit drag) / re-parent (horizontal drag **or** the edit dialog's 「移动到」parent selector) any node across 一级/二级/三级, **including injected iframe light-app nodes** (`light-app-<appId>`). The static tree (`core/page-layout/sidebar-menu.ts`) stays the source of icons/routes; only per-node deltas (`label`/`disabled`/`parentId`/`sortOrder`) persist to the backend. Merge logic in `core/page-layout/menu-overrides.ts` (`applyMenuOverrides`, applied in `use-sidebar-menu.ts` after light-app injection, before role filter; disabling a node hides its whole subtree) — strict **3-level cap** (`wouldExceedDepth`). Client `strategy-components/api/menu-overrides.ts`. Backend: `/api/menu-overrides` (global/shared; open read, admin atomic replace-all).
|
||||
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape.
|
||||
- **敏感词管理 (Sensitive Word Management)**: configures the **展示层脱敏映射** (原始词 → 替换词). **Gated on the username `lqq`** (not the admin role) — `getAccountDisplayName().toLowerCase() === "lqq"` (username = email `@` prefix, else `user_id`, same as the profile page); the dropdown entry, the route guard, and the page-level guard all use this check. `src/lib/desensitize.ts` ships a frozen seed map `SEED_DESENSITIZE_MAP` (the deployment seed) plus a live mutable `DESENSITIZE_MAP` (initialized from the seed so `desensitize()` works before any fetch). `DesensitizeWordsLoader` (mounted in `App.tsx` `AppProviders`) fetches `/api/sensitive-words` on startup and, if non-empty, calls `replaceDesensitizeMap(...)` to make the backend config the source of truth (empty/failed → keep seed). Page `SensitiveWordsPage.tsx` (route `/page/strategy/admin/sensitive-words`, opened from the 「敏感词管理」 item in the **bottom-left settings dropdown** of `components/page-sidebar.tsx`, present in **both** full + collapsed sidebar modes) lists/adds/edits/启停/deletes words and saves the **whole set atomically** (replace-all); a 「导入种子词」 button merges any missing `SEED_DESENSITIZE_MAP` entries into the editor. Client + hooks in `strategy-components/api/sensitive-words.ts` (`wordsToMap` folds enabled rows into the runtime map). Backend: `/api/sensitive-words` (global/shared; open read, write allowed for admin **or** the `lqq` account), mirroring 菜单管理's store shape. **侧边栏 + 导航栏 menu labels are desensitized** at the data source: `useSidebarMenuItems`/`useActiveThirdLevel` (`core/page-layout/use-sidebar-menu.ts`) map every node's `label` through a desensitizer built **directly from the `/api/sensitive-words` query** (`makeDesensitizer(wordsToMap(words))`, falling back to the global seed `desensitize` while empty) — this sidesteps the `replaceDesensitizeMap` effect-timing race. Only `label` is rewritten; `path`/`id`/`icon` are untouched so routing/highlight are unaffected. All nav surfaces (`page-sidebar.tsx`, `page-sidebar-dark.tsx`, `page-sidebar-v1.tsx`, `strategy-components/Header.tsx`, `PageRoutes.tsx`) consume this hook and are covered; `MenuManagementPage` uses the raw tree so admins still edit real labels.
|
||||
- **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/`
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from "react";
|
||||
|
||||
export interface AutoGrowTextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
/** 内容增长高度上限(px),超过后框内滚动。默认 200。 */
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自适应高度的多行输入框。
|
||||
*
|
||||
* 内容增多时自动扩展高度(输入框处于底部布局时即「向上扩展」),到 `maxHeight` 后框内滚动。
|
||||
* 体验对齐通用问答页(`/page/strategy/qa/general`)的 tdesign `autosize` 输入框,但保留原生
|
||||
* `<textarea>`,从而沿用各调用处自带的 Tailwind 样式 / 边框 / 占位色,无需 TDesign 包裹层。
|
||||
*
|
||||
* 用法:当成普通受控 `<textarea>` 用(传 `value` + `onChange`)。高度在布局阶段随 `value` 重算,
|
||||
* 也在 `onChange` 时即时重算(兼顾非受控/输入法场景)。最小高度交给调用方的 `min-h-*` className
|
||||
* 或 `rows` 控制——本组件不强加最小高度。换行/发送(Enter/Shift+Enter)逻辑由调用方的
|
||||
* `onKeyDown` 决定。
|
||||
*/
|
||||
export const AutoGrowTextarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
AutoGrowTextareaProps
|
||||
>(function AutoGrowTextarea(
|
||||
{ maxHeight = 200, value, onChange, onInput, style, rows = 1, ...rest },
|
||||
ref,
|
||||
) {
|
||||
const innerRef = React.useRef<HTMLTextAreaElement | null>(null);
|
||||
React.useImperativeHandle(ref, () => innerRef.current as HTMLTextAreaElement);
|
||||
|
||||
const resize = React.useCallback(() => {
|
||||
const el = innerRef.current;
|
||||
if (!el) return;
|
||||
el.style.height = "auto";
|
||||
const next = Math.min(el.scrollHeight, maxHeight);
|
||||
el.style.height = `${next}px`;
|
||||
el.style.overflowY = el.scrollHeight > maxHeight ? "auto" : "hidden";
|
||||
}, [maxHeight]);
|
||||
|
||||
// 受控值变化(含外部清空、粘贴大段文本)后在布局阶段重算,避免闪烁。
|
||||
React.useLayoutEffect(() => {
|
||||
resize();
|
||||
}, [resize, value]);
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={innerRef}
|
||||
rows={rows}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onInput={(e) => {
|
||||
onInput?.(e);
|
||||
resize();
|
||||
}}
|
||||
style={{ resize: "none", ...style }}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -12,8 +12,10 @@ import { useLocation } from "react-router-dom";
|
||||
import { Layout } from "lucide-react";
|
||||
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { desensitize, makeDesensitizer } from "@/lib/desensitize";
|
||||
import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps";
|
||||
import { useMenuOverrides } from "@/strategy-components/api/menu-overrides";
|
||||
import { useSensitiveWords, wordsToMap } from "@/strategy-components/api/sensitive-words";
|
||||
|
||||
import { applyMenuOverrides } from "./menu-overrides";
|
||||
import {
|
||||
@@ -58,23 +60,43 @@ export function injectLightApps(items: MenuItem[], apps: LightApp[]): MenuItem[]
|
||||
return walk(items);
|
||||
}
|
||||
|
||||
/** 递归对菜单标签做脱敏(仅改 `label`,path/id/icon 不动,保证高亮与路由不受影响)。 */
|
||||
function desensitizeLabels(items: MenuItem[], fn: (text: string) => string): MenuItem[] {
|
||||
return items.map((node) => ({
|
||||
...node,
|
||||
label: fn(node.label),
|
||||
children: node.children ? desensitizeLabels(node.children, fn) : node.children,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回合并了 iframe 轻应用、套用了后端「菜单覆盖层」(改名/停用/排序/层级)、
|
||||
* 并按 admin 角色过滤后的菜单。
|
||||
* 按 admin 角色过滤、并对标签做**展示层脱敏**后的菜单。
|
||||
*
|
||||
* 管线顺序:静态菜单 → 注入 iframe 轻应用 → 套覆盖层(停用项被剔除)→ 角色过滤。
|
||||
* 管线顺序:静态菜单 → 注入 iframe 轻应用 → 套覆盖层(停用项被剔除)→ 角色过滤 → 标签脱敏。
|
||||
* 覆盖层作用在合并后的整棵树上,所以 iframe 轻应用节点同样可被改名/停用/移层级。
|
||||
*
|
||||
* 脱敏直接用 `/api/sensitive-words` 查询结果构造(后台非空时用其映射,否则回退到内置种子的
|
||||
* 全局 {@link desensitize}),避免依赖 {@link replaceDesensitizeMap} 的 effect 回灌时序。
|
||||
*/
|
||||
export function useSidebarMenuItems(isAdmin: boolean): MenuItem[] {
|
||||
const { data: iframeApps = [] } = useLightApps({ appType: "iframe", enabledOnly: true });
|
||||
const { data: overrides = [] } = useMenuOverrides();
|
||||
const { data: words = [] } = useSensitiveWords();
|
||||
const desensitizeLabel = useMemo(
|
||||
() => (words.length > 0 ? makeDesensitizer(wordsToMap(words)) : desensitize),
|
||||
[words],
|
||||
);
|
||||
return useMemo(
|
||||
() =>
|
||||
filterMenuItemsByRole(
|
||||
applyMenuOverrides(injectLightApps(sidebarMenuItems, iframeApps), overrides),
|
||||
isAdmin,
|
||||
desensitizeLabels(
|
||||
filterMenuItemsByRole(
|
||||
applyMenuOverrides(injectLightApps(sidebarMenuItems, iframeApps), overrides),
|
||||
isAdmin,
|
||||
),
|
||||
desensitizeLabel,
|
||||
),
|
||||
[iframeApps, overrides, isAdmin],
|
||||
[iframeApps, overrides, isAdmin, desensitizeLabel],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,22 @@ export function desensitize(text: string): string {
|
||||
return text.replace(_regex, (matched) => DESENSITIZE_MAP[matched] ?? matched);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用**指定映射**(而非全局 {@link DESENSITIZE_MAP})构造一个脱敏函数。
|
||||
*
|
||||
* 适用于「数据加载完就要立即用其结果脱敏、不能依赖全局 map 的异步回灌时序」的场景
|
||||
* (如侧边栏/导航栏菜单:直接用 `/api/sensitive-words` 查询结果计算标签,避免与
|
||||
* {@link replaceDesensitizeMap} 的 effect 时序竞态)。正则在构造时编译一次,可缓存复用。
|
||||
*
|
||||
* @param map 原始词 → 替换词
|
||||
* @returns `(text) => 脱敏后文本`;map 为空时返回恒等函数
|
||||
*/
|
||||
export function makeDesensitizer(map: Record<string, string>): (text: string) => string {
|
||||
const regex = buildRegex(map);
|
||||
if (!regex) return (text) => text;
|
||||
return (text) => text.replace(regex, (matched) => map[matched] ?? matched);
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度脱敏:递归遍历对象 / 数组,对其中所有字符串值做 {@link desensitize},
|
||||
* 其余类型原样保留。返回**新值**(不修改入参),用于对结构化数据(如大屏 report-json
|
||||
|
||||
@@ -34,6 +34,7 @@ function formatFileSize(bytes: number): string {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import type { MessageMetric } from "@/components/workspace/messages/use-message-metrics";
|
||||
@@ -221,7 +222,7 @@ export function Step1Panel(props: Step1PanelProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
<AutoGrowTextarea
|
||||
id="input-text-area"
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
@@ -233,10 +234,11 @@ export function Step1Panel(props: Step1PanelProps) {
|
||||
}}
|
||||
placeholder={
|
||||
pendingIntentClarificationMsgId
|
||||
? "也可在此输入自定义回答 (Enter 快速发送)"
|
||||
: "请输入补充信息 / 回答问题 / 继续澄清 (Enter 快速发送)"
|
||||
? "也可在此输入自定义回答 (Enter 发送 / Shift+Enter 换行)"
|
||||
: "请输入补充信息 / 回答问题 / 继续澄清 (Enter 发送 / Shift+Enter 换行)"
|
||||
}
|
||||
className="w-full text-xs text-slate-700 placeholder:text-slate-400 resize-none h-[48px] focus:outline-none leading-relaxed font-normal"
|
||||
className="w-full text-xs text-slate-700 placeholder:text-slate-400 min-h-[48px] max-h-80 focus:outline-none leading-relaxed font-normal"
|
||||
maxHeight={320}
|
||||
maxLength={500}
|
||||
/>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ function formatFileSize(bytes: number): string {
|
||||
/** 后台挂起入口 —— 后端 _run_dag 已支持业务链分层并行,放出该入口。 */
|
||||
const SHOW_BACKGROUND_SUSPEND = true;
|
||||
|
||||
import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { MessageMetric } from "@/components/workspace/messages/use-message-metrics";
|
||||
@@ -696,16 +697,19 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
</div>
|
||||
)}
|
||||
{pendingClarification.allowCustom !== false && (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<input
|
||||
type="text"
|
||||
<div className="flex items-end gap-2 pt-1">
|
||||
<AutoGrowTextarea
|
||||
value={clarificationDraft}
|
||||
onChange={(e) => setClarificationDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") onSubmitClarification(clarificationDraft);
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
onSubmitClarification(clarificationDraft);
|
||||
}
|
||||
}}
|
||||
placeholder="输入你的回答…"
|
||||
className="flex-1 bg-white border border-amber-300 rounded-xl px-3 py-2 text-xs focus:outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-200 font-medium"
|
||||
placeholder="输入你的回答…(Enter 发送 / Shift+Enter 换行)"
|
||||
className="flex-1 bg-white border border-amber-300 rounded-xl px-3 py-2 text-xs leading-relaxed focus:outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-200 font-medium"
|
||||
maxHeight={280}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -751,16 +755,19 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
<div className="flex items-end gap-3">
|
||||
<AutoGrowTextarea
|
||||
value={customAgentInput}
|
||||
onChange={(e) => setCustomAgentInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleInject();
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleInject();
|
||||
}
|
||||
}}
|
||||
placeholder="作为指挥官,您可以随时插话输入指令,对智能体博弈朝向实施人工干预..."
|
||||
className="flex-1 bg-white border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 font-medium"
|
||||
placeholder="作为指挥官,您可以随时插话输入指令,对智能体博弈朝向实施人工干预...(Enter 发送 / Shift+Enter 换行)"
|
||||
className="flex-1 bg-white border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs leading-relaxed focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 font-medium"
|
||||
maxHeight={280}
|
||||
/>
|
||||
|
||||
<input
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea";
|
||||
import { desensitizeDeep } from "@/lib/desensitize";
|
||||
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
|
||||
import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp";
|
||||
@@ -545,7 +546,8 @@ function DashboardChatPanel({
|
||||
{/* 问答修改输入框 */}
|
||||
<form onSubmit={handleSubmit} className="shrink-0 border-t border-slate-100 bg-white px-3 py-3">
|
||||
<div className="flex items-end gap-2 rounded-2xl border border-slate-200 bg-slate-50/70 px-3 py-2 focus-within:border-primary/60 focus-within:bg-white focus-within:ring-2 focus-within:ring-[var(--brand-200)]/40">
|
||||
<textarea
|
||||
<AutoGrowTextarea
|
||||
maxHeight={224}
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -561,7 +563,7 @@ function DashboardChatPanel({
|
||||
? "随时输入修改要求——发送会打断当前绘制并按新要求重出数据"
|
||||
: "输入修改要求,例如:把风险分布换成柱状图,新增一个执行路线席位"
|
||||
}
|
||||
className="min-h-8 max-h-28 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea";
|
||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -670,7 +671,8 @@ function AgentProgressPanel({
|
||||
className="shrink-0 border-t border-slate-100 bg-white px-3 py-3"
|
||||
>
|
||||
<div className="flex items-end gap-2 rounded-2xl border border-slate-200 bg-slate-50/70 px-3 py-2 focus-within:border-primary/60 focus-within:bg-white focus-within:ring-2 focus-within:ring-[var(--brand-200)]/40">
|
||||
<textarea
|
||||
<AutoGrowTextarea
|
||||
maxHeight={224}
|
||||
value={revisionText}
|
||||
onChange={(e) => setRevisionText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -686,7 +688,7 @@ function AgentProgressPanel({
|
||||
? "随时输入修改要求——发送会打断当前绘制并按新要求继续"
|
||||
: "输入修改要求,例如:增加风险矩阵,执行路线改成时间轴"
|
||||
}
|
||||
className="min-h-8 max-h-28 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea";
|
||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||
import { useLocalSettings } from "@/core/settings";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -729,7 +730,8 @@ function AgentProgressPanel({
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-end gap-2 rounded-2xl border border-slate-200 bg-slate-50/70 px-3 py-2 focus-within:border-primary/60 focus-within:bg-white focus-within:ring-2 focus-within:ring-[var(--brand-200)]/40">
|
||||
<textarea
|
||||
<AutoGrowTextarea
|
||||
maxHeight={224}
|
||||
value={askText}
|
||||
onChange={(e) => setAskText(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
@@ -744,7 +746,7 @@ function AgentProgressPanel({
|
||||
? "生成中也可随时插话干预:发送将打断当前轮并据此继续…"
|
||||
: "继续对话:改报告(如「执行路线写细些」)/ 改流程图(如「总结成结构化输出」「流程图加上验收环节」)/ 提问"
|
||||
}
|
||||
className="min-h-8 max-h-28 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
|
||||
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
|
||||
/>
|
||||
|
||||
{/* 上传文件(参考第一/二步:本地选取,发送时上传到总结线程) */}
|
||||
|
||||
Reference in new issue
Block a user