diff --git a/frontend/src/components/ai-elements/artifact.tsx b/frontend/src/components/ai-elements/artifact.tsx index 9d273e2d..96db33d8 100644 --- a/frontend/src/components/ai-elements/artifact.tsx +++ b/frontend/src/components/ai-elements/artifact.tsx @@ -63,7 +63,7 @@ export type ArtifactTitleProps = HTMLAttributes; export const ArtifactTitle = ({ className, ...props }: ArtifactTitleProps) => (
); diff --git a/frontend/src/components/ui/dropdown-menu.tsx b/frontend/src/components/ui/dropdown-menu.tsx index 3fe3d08d..1270e58f 100644 --- a/frontend/src/components/ui/dropdown-menu.tsx +++ b/frontend/src/components/ui/dropdown-menu.tsx @@ -130,7 +130,7 @@ function DropdownMenuRadioItem({ { value: T; @@ -76,26 +77,30 @@ export function DropdownSelector({ - - {selectedOption?.label ?? value} + + {truncateMiddle(selectedOption?.label ?? value, 50)} {isOpen ? : } - + onChange(v as T)} > {options.map((option) => ( - - {option.label} + + {truncateMiddle(option.label)} ))} ); -} +} \ No newline at end of file diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index 8017ed8c..76860325 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -34,7 +34,7 @@ import { installSkill } from "@/core/skills/api"; import { streamdownPlugins } from "@/core/streamdown"; import { checkCodeFile, getFileName } from "@/core/utils/files"; import { useMarkdownDownload } from "@/core/utils/markdown-download"; -import { cn, copyToClipboard } from "@/lib/utils"; +import { cn, copyToClipboard, truncateMiddle } from "@/lib/utils"; import { CitationLink } from "../citations/citation-link"; @@ -236,7 +236,7 @@ export function ArtifactFileDetail({
{isWriteFile ? ( -
{getFileName(filepath)}
+
{truncateMiddle(getFileName(filepath), 50)}
) : ( handleClick(file)} > - -
{getFileName(file)}
-
- {getFileIcon(file, "size-6 stroke-[1.5px] stroke-[#333333]")} + +
+ {truncateMiddle(getFileName(file), 50)}
+
+ {getFileIcon(file, "size-6 stroke-[1.5px] stroke-[#333333]")} +
{getFileExtensionDisplayName(file)} file diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 36369100..db85669d 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -33,3 +33,49 @@ export async function copyToClipboard(text: string): Promise { console.log("[copyToClipboard] direct mode", message); await navigator.clipboard.writeText(text); } + +/** + * 计算字符串的视觉宽度(中文算2,英文算1) + */ +export function getVisualWidth(text: string): number { + let width = 0; + for (const char of text) { + // 中文字符范围:\u4e00-\u9fff(基本汉字) + width += /[\u4e00-\u9fff]/.test(char) ? 2 : 1; + } + return width; +} + +/** + * 截断字符串中间部分,保留开头和结尾 + * 例如: "very-long-file-name.txt" -> "very-lon...me.txt" + * 中文按视觉宽度计算(中文算2,英文算1) + */ +export function truncateMiddle(text: string, maxVisualWidth: number = 30): string { + const visualWidth = getVisualWidth(text); + if (visualWidth <= maxVisualWidth) return text; + + const startWidth = Math.ceil(maxVisualWidth * 0.6); + const endWidth = Math.floor(maxVisualWidth * 0.4) - 3; // -3 for "..." + + let startPart = ""; + let currentWidth = 0; + for (const char of text) { + const charWidth = /[\u4e00-\u9fff]/.test(char) ? 2 : 1; + if (currentWidth + charWidth > startWidth) break; + startPart += char; + currentWidth += charWidth; + } + + let endPart = ""; + currentWidth = 0; + for (let i = text.length - 1; i >= 0; i--) { + const char = text[i]!; + const charWidth = /[\u4e00-\u9fff]/.test(char) ? 2 : 1; + if (currentWidth + charWidth > endWidth) break; + endPart = char + endPart; + currentWidth += charWidth; + } + + return `${startPart}...${endPart}`; +}