(".long-message-content");
+ expect(content).not.toBeNull();
+ expect(content?.style.maxHeight).toBe(`${lineHeight * 8}px`);
+
+ naturalContentHeight = lineHeight * 12;
+ rerender();
+
+ expect(screen.getByRole("button", { name: "Expand" })).toBeTruthy();
+ expect(content?.style.maxHeight).toBe(`${lineHeight * 8}px`);
+
+ await user.click(screen.getByRole("button", { name: "Expand" }));
+
+ expect(screen.getByRole("button", { name: "Collapse" })).toBeTruthy();
+ expect(content?.style.maxHeight).toBe(`${lineHeight * 12}px`);
+ });
});
diff --git a/web/app/src/components/business/MessageContent/LongMessageCollapse.tsx b/web/app/src/components/business/MessageContent/LongMessageCollapse.tsx
index 9d4665c7..68a3da58 100644
--- a/web/app/src/components/business/MessageContent/LongMessageCollapse.tsx
+++ b/web/app/src/components/business/MessageContent/LongMessageCollapse.tsx
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useLayoutEffect, useId, useRef, useState } from "react";
import { Button } from "@/components/ui";
+import { LongMessageCollapseIcon, LongMessageExpandIcon } from "@/components/ui/Icons";
import { classNames } from "@/shared/lib/classNames";
import type { TranslateFn } from "@/models/conversations";
import { prepareMermaidBlocks, renderMermaidBlocks } from "./mermaid";
@@ -73,6 +74,14 @@ export function LongMessageCollapse({ expanded, html, onExpandedChange, t }: Lon
const measure = () => {
const nextMetrics = calculateMetrics(element);
setMetrics((current) => {
+ if (
+ current?.shouldCollapse &&
+ nextMetrics.shouldCollapse &&
+ !isExpanded &&
+ current.collapseHeight === nextMetrics.collapseHeight
+ ) {
+ return current;
+ }
if (nextMetrics.shouldCollapse) {
return metricsEqual(current, nextMetrics) ? current : nextMetrics;
}
@@ -145,6 +154,7 @@ export function LongMessageCollapse({ expanded, html, onExpandedChange, t }: Lon
transitionDuration: "0ms",
} as const;
const toggleLabel = isExpanded ? t("messageLongCollapse") : t("messageLongExpand");
+ const ToggleIcon = isExpanded ? LongMessageCollapseIcon : LongMessageExpandIcon;
return (
@@ -165,7 +175,8 @@ export function LongMessageCollapse({ expanded, html, onExpandedChange, t }: Lon
className="long-message-toggle"
onClick={toggleExpanded}
>
- {toggleLabel}
+
+ {toggleLabel}
@@ -192,29 +203,13 @@ function calculateMetrics(element: HTMLDivElement): LongMessageMetrics {
function metricsEqual(current: LongMessageMetrics | null, next: LongMessageMetrics): boolean {
return (
current?.collapseHeight === next.collapseHeight &&
- current.expandedHeight === next.expandedHeight &&
+ Math.abs(current.expandedHeight - next.expandedHeight) <= COLLAPSE_HEIGHT_TOLERANCE_PX &&
current.shouldCollapse === next.shouldCollapse
);
}
function measureNaturalExpandedHeight(element: HTMLDivElement): number {
- const previousMaxHeight = element.style.maxHeight;
- const previousOverflow = element.style.overflow;
- const previousPaddingBottom = element.style.paddingBottom;
- const previousTransitionDuration = element.style.transitionDuration;
-
- element.style.maxHeight = "none";
- element.style.overflow = "visible";
- element.style.paddingBottom = "0px";
- element.style.transitionDuration = "0ms";
- const expandedHeight = Math.ceil(element.scrollHeight);
-
- element.style.maxHeight = previousMaxHeight;
- element.style.overflow = previousOverflow;
- element.style.paddingBottom = previousPaddingBottom;
- element.style.transitionDuration = previousTransitionDuration;
-
- return expandedHeight;
+ return Math.ceil(element.scrollHeight);
}
function hasImageLikeContent(element: HTMLDivElement): boolean {
diff --git a/web/app/src/components/business/MessageContent/MessageContent.css b/web/app/src/components/business/MessageContent/MessageContent.css
index da3231eb..9dfc6764 100644
--- a/web/app/src/components/business/MessageContent/MessageContent.css
+++ b/web/app/src/components/business/MessageContent/MessageContent.css
@@ -221,9 +221,16 @@
.long-message-toggle {
position: relative;
z-index: 4;
+ color: #2f384c;
box-shadow: var(--shadow-soft);
}
+.long-message-toggle-icon {
+ width: 24px;
+ height: 24px;
+ flex: 0 0 24px;
+}
+
:root[data-theme="dark"] .message-bubble:has(.long-message-collapse.is-collapsed)::after {
background: linear-gradient(180deg, rgba(15, 23, 42, 0), rgba(15, 23, 42, 0.96));
}
diff --git a/web/app/src/components/business/MessageContent/MessageContent.tsx b/web/app/src/components/business/MessageContent/MessageContent.tsx
index 2bd222a8..7386f6bd 100644
--- a/web/app/src/components/business/MessageContent/MessageContent.tsx
+++ b/web/app/src/components/business/MessageContent/MessageContent.tsx
@@ -42,7 +42,7 @@ export function MessageContent({
);
useEffect(() => {
- if (enableLongMessageCollapse && t) {
+ if (enableLongMessageCollapse) {
return undefined;
}
@@ -62,7 +62,7 @@ export function MessageContent({
return () => {
cancelled = true;
};
- }, [enableLongMessageCollapse, markup, t]);
+ }, [enableLongMessageCollapse, markup]);
if (blankTurnPlaceholder) {
return (
diff --git a/web/app/src/components/ui/Icons/LongMessageIcons.tsx b/web/app/src/components/ui/Icons/LongMessageIcons.tsx
new file mode 100644
index 00000000..401121ce
--- /dev/null
+++ b/web/app/src/components/ui/Icons/LongMessageIcons.tsx
@@ -0,0 +1,39 @@
+import type { SVGProps } from "react";
+
+type LongMessageIconProps = SVGProps & {
+ size?: number | string;
+};
+
+function iconSize(size: number | string | undefined) {
+ return size ?? 24;
+}
+
+export function LongMessageExpandIcon({ size, ...props }: LongMessageIconProps) {
+ const resolvedSize = iconSize(size);
+ return (
+
+ );
+}
+
+export function LongMessageCollapseIcon({ size, ...props }: LongMessageIconProps) {
+ const resolvedSize = iconSize(size);
+ return (
+
+ );
+}
diff --git a/web/app/src/components/ui/Icons/index.ts b/web/app/src/components/ui/Icons/index.ts
index d9cbc81f..e1aa3623 100644
--- a/web/app/src/components/ui/Icons/index.ts
+++ b/web/app/src/components/ui/Icons/index.ts
@@ -1,2 +1,3 @@
export * from "./IconImage";
+export * from "./LongMessageIcons";
export * from "./SidebarNavigationIcons";
diff --git a/web/app/src/shared/i18n/messages.ts b/web/app/src/shared/i18n/messages.ts
index 45d19787..02aaf3fa 100644
--- a/web/app/src/shared/i18n/messages.ts
+++ b/web/app/src/shared/i18n/messages.ts
@@ -640,6 +640,8 @@ export const messages = {
inputPlaceholder: "输入消息,使用 @ 选择成员",
send: "发送",
composerTip: "Enter 发送,Shift + Enter 换行。支持房间、私信和 @ 提及。",
+ messageLongExpand: "展开",
+ messageLongCollapse: "收起",
profileSetupTitle: "配置 Manager Profile",
profileSetupSubtitle: "自动检测没有找到可用模型。请完成 Manager 的运行配置后再开始对话。",
profileProvider: "模型提供方",