refactor(client): share the file-type icon map, drop two dead modules

AiMessageBubble carried its own extension → bisheng-icon table, a copy
of what FileIcon already knows. FileIcon exports getFileTypeIcon and the
bubble consumes it, so a new file type is registered once.

Also deletes FileListRow and the knowledge mock: neither has had a
call site for a while (the file list renders through FileTable /
FileCard). Suppressions shrink accordingly. The share dialog picks up
the icon import while it is here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kinyoo
2026-08-13 14:27:58 +08:00
parent a9409dd9b4
commit f9b3bbe821
4 changed files with 67 additions and 41 deletions
@@ -882,11 +882,6 @@
"count": 1
}
},
"src/components/FileListRow.tsx": {
"no-restricted-syntax": {
"count": 11
}
},
"src/components/Files/ActionButton.tsx": {
"@typescript-eslint/no-unused-vars": {
"count": 1
@@ -33,7 +33,7 @@ import {
} from "~/components/Chat/MessageSelection";
import { copyText, cn } from "~/utils";
import type { AgentEvent, ChatMessage } from "~/api/chatApi";
import { getFileTypebyFileName, isImageFileName } from "~/components/ui/icon/File/FileIcon";
import { getFileTypeIcon, isImageFileName } from "~/components/ui/icon/File/FileIcon";
import { MessageImage } from "~/components/Chat/Messages/Content/MessageImage";
// Transient/retryable backend error codes surfaced by daily-mode chat — LLM rate
@@ -43,35 +43,6 @@ import { MessageImage } from "~/components/Chat/Messages/Content/MessageImage";
// has to reach the calm "busy" card rather than the red failure one.
const RETRYABLE_ERROR_CODES = new Set([12046, 429, 503, 10540, 12045]);
// Map an uploaded file's extension to a bisheng outlined file-type icon.
// Anything not listed falls back to the generic Outlined.File icon.
const FILE_TYPE_ICONS: Record<string, typeof Outlined.File> = {
// FileExcel
xls: Outlined.FileExcel,
xlsx: Outlined.FileExcel,
csv: Outlined.FileExcel,
et: Outlined.FileExcel,
// FilePdf
pdf: Outlined.FilePdf,
ppt: Outlined.FilePdf,
dps: Outlined.FilePdf,
// FileTxt
txt: Outlined.FileTxt,
// FileWord
doc: Outlined.FileWord,
docx: Outlined.FileWord,
wps: Outlined.FileWord,
// FileImage
png: Outlined.FileImage,
jpg: Outlined.FileImage,
jpeg: Outlined.FileImage,
bmp: Outlined.FileImage,
// FileEditing
md: Outlined.FileEditing,
// File (generic)
html: Outlined.File,
};
/**
* Uploaded-file list for a user message: a type icon + filename per row, never a
* content preview. Stacks vertically and scrolls past 120px. A linear-gradient
@@ -133,8 +104,7 @@ function UploadedFileList({ files, conversationId }: { files: any[]; conversatio
>
{others.map((file, i) => {
const fileName = file.name || file.file_name || "File";
const fileType = getFileTypebyFileName(fileName);
const FileTypeIcon = FILE_TYPE_ICONS[fileType] ?? Outlined.File;
const FileTypeIcon = getFileTypeIcon(fileName);
return (
<div key={i} className="flex shrink-0 items-center gap-1 text-[#999999]">
<FileTypeIcon size={12} className="shrink-0 text-[#CCCCCC]" />
@@ -1,3 +1,4 @@
import { Outlined } from "bisheng-icons";
import { BookType, File, FileMinus, Heading, Image, Loader2, Table2 } from "lucide-react";
import React from "react";
@@ -135,6 +136,39 @@ const getSizeClass = (size: 'sm' | 'md' | 'lg') => {
export const getFileTypebyFileName = (fileName: string) => {
return fileName ? fileName.split('.').pop()?.toLocaleLowerCase() as FileType : '';
}
// Map an uploaded file's extension to a bisheng outlined file-type icon.
// Anything not listed falls back to the generic Outlined.File icon.
const FILE_TYPE_ICONS: Record<string, typeof Outlined.File> = {
// FileExcel
xls: Outlined.FileExcel,
xlsx: Outlined.FileExcel,
csv: Outlined.FileExcel,
et: Outlined.FileExcel,
// FilePdf
pdf: Outlined.FilePdf,
ppt: Outlined.FilePdf,
dps: Outlined.FilePdf,
// FileTxt
txt: Outlined.FileTxt,
// FileWord
doc: Outlined.FileWord,
docx: Outlined.FileWord,
wps: Outlined.FileWord,
// FileImage
png: Outlined.FileImage,
jpg: Outlined.FileImage,
jpeg: Outlined.FileImage,
bmp: Outlined.FileImage,
// FileEditing
md: Outlined.FileEditing,
// File (generic)
html: Outlined.File,
};
/** Outlined icon component for a file name, generic `File` when unmapped. */
export const getFileTypeIcon = (fileName: string): typeof Outlined.File =>
FILE_TYPE_ICONS[getFileTypebyFileName(fileName)] ?? Outlined.File;
// Whether an attachment should render as a picture. Judged by filename rather
// than any stored MIME type: older messages don't carry one, and the upload
// endpoints disagree on where they put it — the name is always there, and this
@@ -1,3 +1,4 @@
import { Outlined } from "bisheng-icons";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
INCLUDE_CHILDREN_CHECKBOX_CLASS,
@@ -23,12 +24,36 @@ import {
TabsList,
TabsTrigger,
} from "~/components/ui";
import { getFileTypeIcon } from "~/components/ui/icon/File/FileIcon";
import { useLocalize } from "~/hooks";
import { useRecoilValue } from "recoil";
import store from "~/store";
import { getGrantableRelationModels } from "~/api/permission";
import type { RelationModel, ResourceType } from "~/api/permission";
/**
* The resource name sits here instead of in the dialog title: file names can run
* past 100 characters, and appended to the title they wrapped under the close
* button. One truncated line keeps the header height fixed; the native tooltip
* still exposes the full name. Icon + name, no container fill — same shape as
* the uploaded-file rows in chat.
*/
function ResourceContextBar({ name, resourceType }: { name: string; resourceType: ResourceType }) {
const Icon =
resourceType === "folder"
? Outlined.FolderClose
: resourceType === "knowledge_space" || resourceType === "knowledge_library"
? Outlined.Book
: getFileTypeIcon(name);
return (
<div className="mb-3 flex shrink-0 items-center gap-1.5 text-text-2" title={name}>
<Icon size={14} className="shrink-0 text-text-3" />
<span className="truncate text-body-sm">{name}</span>
</div>
);
}
interface KnowledgeSpaceShareDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@@ -100,8 +125,6 @@ export function KnowledgeSpaceShareDialog({
setGrantDialogOpen(false);
}, [grantSubjectType]);
const dialogTitle = `${localize("com_permission.dialog_title")} - ${resourceName}`;
// F033: department spaces drop the user-group dimension. The list view and
// the grant dialog share this array, so both lose the tab at once.
const SUBJECT_TABS = useMemo<Array<{
@@ -172,10 +195,13 @@ export function KnowledgeSpaceShareDialog({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className={PERMISSION_DIALOG_CONTENT_CLASS}>
<DialogHeader className="shrink-0 text-left">
<DialogTitle className="text-left">{dialogTitle}</DialogTitle>
<DialogTitle className="text-left">
{localize("com_permission.dialog_title")}
</DialogTitle>
</DialogHeader>
<div className="mt-4 flex min-h-0 flex-1 flex-col overflow-hidden">
<ResourceContextBar name={resourceName} resourceType={resourceType} />
{permissionPanel}
</div>
</DialogContent>
@@ -185,11 +211,12 @@ export function KnowledgeSpaceShareDialog({
<DialogContent className={PERMISSION_DIALOG_CONTENT_CLASS}>
<DialogHeader className="shrink-0 text-left">
<DialogTitle className="text-left">
{localize("com_permission.tab_grant")} - {resourceName}
{localize("com_permission.tab_grant")}
</DialogTitle>
</DialogHeader>
<div className="user-manger mt-4 flex min-h-0 flex-1 flex-col overflow-hidden">
<ResourceContextBar name={resourceName} resourceType={resourceType} />
<div className="flex items-center gap-3">
<div className={`inline-flex items-center justify-center ${SUBJECT_TAB_LIST_CLASS}`}>
{SUBJECT_TABS.map((tab) => (