Skip to content

Commit 6e1673b

Browse files
committed
Merge branch 'feat/2.6.0-beta4' of github.com:dataelement/bisheng into feat/2.6.0-beta4
2 parents 42c2571 + a93b0fc commit 6e1673b

8 files changed

Lines changed: 73 additions & 138 deletions

File tree

src/frontend/client/src/components/Chat/Input/AttachmentBar.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
useRef,
2121
useState,
2222
} from "react";
23-
import { Loader2, X } from "lucide-react";
23+
import { Loader2 } from "lucide-react";
2424
import { Outlined } from "bisheng-icons";
2525
import BookOpen from "~/components/ui/icon/BookOpen";
2626
import BooksIcon from "~/components/ui/icon/Books";
@@ -80,7 +80,7 @@ const CardShell = ({
8080
onRemove?: () => void;
8181
}) => (
8282
<div
83-
className="group flex h-[30px] shrink-0 items-center gap-1 rounded-md border border-[#ebebeb] bg-white px-2 text-xs text-[#212121]"
83+
className="group flex h-[30px] shrink-0 items-center gap-1 rounded-md bg-white px-2 text-xs text-[#212121]"
8484
style={{ width: CARD_WIDTH }}
8585
>
8686
<span className="flex size-4 shrink-0 items-center justify-center text-[#999]">{icon}</span>
@@ -95,7 +95,7 @@ const CardShell = ({
9595
className="hidden size-4 shrink-0 items-center justify-center rounded text-slate-400 transition-colors hover:text-slate-600 group-hover:flex touch-mobile:flex"
9696
aria-label="Remove"
9797
>
98-
<X className="size-3" />
98+
<Outlined.Close size={12} />
9999
</button>
100100
)}
101101
</div>
@@ -147,7 +147,12 @@ const ArrowButton = ({
147147
type="button"
148148
onClick={onClick}
149149
aria-label={direction === "left" ? "Scroll back" : "Scroll forward"}
150-
className="flex size-4 shrink-0 items-center justify-center text-[#666] transition-colors hover:text-[#212121]"
150+
// 8px gap only on the outer side (strip edge); the side facing the cards
151+
// stays flush at 0.
152+
className={cn(
153+
"flex size-4 shrink-0 items-center justify-center text-[#666] transition-colors hover:text-[#212121]",
154+
direction === "left" ? "ml-2" : "mr-2",
155+
)}
151156
>
152157
<Icon size={16} />
153158
</button>
@@ -250,7 +255,7 @@ export const AttachmentBar = ({
250255
}, []);
251256

252257
return (
253-
<div className="relative -mb-4 w-full overflow-hidden rounded-t-2xl bg-[rgba(244,244,244,0.5)] px-4 pb-6 pt-2">
258+
<div className="relative -mb-4 w-full overflow-hidden rounded-t-2xl bg-[rgba(244,244,244,0.55)] px-2 pb-6 pt-2">
254259
<div className="flex items-center">
255260
{canLeft && <ArrowButton direction="left" onClick={() => pageScroll("left")} />}
256261
<div className="relative min-w-0 flex-1">

src/frontend/client/src/components/Chat/Messages/Content/CitationReferencesDrawer.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function CitationReferenceCard({
121121
'text-[14px] font-normal leading-[22px] text-[#1D2129]';
122122

123123
return (
124-
<div className="flex min-h-[92px] flex-col gap-2 rounded-[8px] border border-[#ECECEC] bg-white p-2">
124+
<div className="flex min-h-[92px] flex-col gap-2 rounded-[6px] border border-[#ECECEC] bg-white p-2">
125125
<div className="flex items-center">
126126
<SourceTypeBadge preview={preview} type={item.data.type} />
127127
</div>
@@ -151,7 +151,10 @@ function CitationReferenceCard({
151151
target="_blank"
152152
rel="noreferrer"
153153
className={cn(
154-
'flex w-full min-w-0 items-center gap-1 rounded-[4px] hover:text-blue-500',
154+
// Web links are generic external links, not a brand action — pin the
155+
// hover to a fixed blue (matches the web hover arrow #1B61E6) so it
156+
// doesn't follow the blue⇄green brand theme.
157+
'flex w-full min-w-0 items-center gap-1 rounded-[4px] hover:text-[#1B61E6]',
155158
nameRowTextClass,
156159
)}
157160
title={title}
@@ -736,7 +739,7 @@ export default function CitationReferencesDrawer({
736739
<aside
737740
className={cn(
738741
'fixed inset-y-0 right-0 z-[130] flex min-h-0 w-[min(520px,calc(100vw-24px))] min-w-0 flex-col overflow-hidden bg-white shadow-[0_8px_24px_rgba(0,0,0,0.12)] animate-in slide-in-from-right duration-300',
739-
'rounded-tl-xl',
742+
'rounded-tl-[8px]',
740743
)}
741744
aria-label="参考资料"
742745
onClick={(event) => event.stopPropagation()}

src/frontend/client/src/components/Chat/Messages/Content/Markdown.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,9 @@ function CitationPreviewCard({
292292
const cardClassName = 'group relative cursor-pointer w-[320px] max-w-[calc(100vw-32px)] overflow-hidden rounded-lg bg-white text-[#1D2129] shadow-[0_4px_19px_rgba(34,34,34,0.07)]';
293293
const titleClassName = 'min-w-0 flex-1 truncate text-[14px] font-medium leading-5 text-[#1D2129]';
294294
const titleContainerClassName = 'bg-white p-3';
295-
const interactiveTitleClassName = `${titleClassName} transition-colors duration-200 group-hover:text-blue-600`;
295+
// Web link titles hover to a fixed blue (matches the web hover arrow #1B61E6),
296+
// not the blue⇄green brand theme — they're generic external links, not a brand action.
297+
const interactiveTitleClassName = `${titleClassName} transition-colors duration-200 group-hover:text-[#1B61E6]`;
296298
const ragTitleClassName = 'min-w-0 truncate text-[14px] font-medium leading-[22px] text-[#1D2129]';
297299
const formatSourceMeta = (value?: string) => {
298300
if (!value) {

src/frontend/client/src/components/Chat/Messages/Content/useCitationReferencePanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function useCitationReferencePanel({ hasMessages }: UseCitationReferenceP
105105
handleCloseCitationPanel();
106106
}
107107
}}
108-
panelClassName="h-full w-full overflow-hidden rounded-[12px] border border-[#ECECEC] bg-[#FBFBFB]"
108+
panelClassName="h-full w-full overflow-hidden rounded-[8px] border border-[#ECECEC] bg-[#FBFBFB]"
109109
messageId={citationPanelPayload.messageId}
110110
content={citationPanelPayload.content}
111111
webContent={citationPanelPayload.webContent}
@@ -147,7 +147,7 @@ export function useCitationReferencePanel({ hasMessages }: UseCitationReferenceP
147147
data-citation-popover-surface
148148
className={cn(
149149
'fixed inset-y-0 right-0 z-[130] flex min-h-0 flex-col overflow-hidden border-l border-[#ECECEC] bg-white shadow-[-8px_0_28px_rgba(0,0,0,0.08)] animate-in slide-in-from-right duration-300',
150-
'rounded-tl-xl',
150+
'rounded-tl-[8px]',
151151
'min-w-[260px] w-[min(520px,42vw)] max-[580px]:min-w-[240px] max-[580px]:w-[min(360px,calc(100vw-40px))]',
152152
)}
153153
onClick={(event) => event.stopPropagation()}
@@ -168,7 +168,7 @@ export function useCitationReferencePanel({ hasMessages }: UseCitationReferenceP
168168
data-citation-popover-surface
169169
className={cn(
170170
'fixed inset-y-0 right-0 z-[150] flex min-h-0 flex-col overflow-hidden border-l border-[#ECECEC] bg-white shadow-[-8px_0_28px_rgba(0,0,0,0.1)] animate-in slide-in-from-right duration-300',
171-
'rounded-tl-xl',
171+
'rounded-tl-[8px]',
172172
useExpandedCitationPanel ? 'w-[min(480px,100vw)]' : 'w-[min(360px,100vw)]',
173173
)}
174174
onClick={(event) => event.stopPropagation()}

src/frontend/client/src/components/Linsight/Artifacts/FilePreviewPanel.tsx

Lines changed: 10 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,18 @@
11
/**
22
* F035 Track H (P4): file preview panel (spec §5, fig 10) — wide right-side
3-
* sheet. Toolbar: back-to-workspace (when opened from the drawer) / file name
4-
* / save-as / close. Body: md → markdown render, txt → plain text, images →
5-
* inline image, everything else → file icon + "download to view".
3+
* sheet for the full-page /linsight view. Toolbar: back-to-workspace (when
4+
* opened from the drawer) / file name / save-as / close. The body delegates to
5+
* the shared PreviewBody so this surface and the inline WorkspacePanel render
6+
* identical content (md / txt / image / docx·pdf·xls·xlsx·csv viewer / download
7+
* fallback) from a single source of truth.
68
*/
79
import { Outlined } from 'bisheng-icons';
810
import { ChevronLeft } from 'lucide-react';
9-
import { useEffect, useState } from 'react';
10-
import { NotificationSeverity } from '~/common';
11-
import Markdown from '~/components/Chat/Messages/Content/Markdown';
12-
import { Button } from '~/components/ui';
1311
import FileIcon from '~/components/ui/icon/File';
1412
import { Sheet, SheetContent } from '~/components/ui/Sheet';
1513
import { useLocalize } from '~/hooks';
16-
import '~/markdown.css';
17-
import { useToastContext } from '~/Providers';
18-
import {
19-
type ArtifactFile,
20-
downloadArtifactFile,
21-
getArtifactPreviewKind,
22-
getFileExtension,
23-
resolveArtifactUrl,
24-
} from './artifactUtils';
14+
import { type ArtifactFile, getFileExtension } from './artifactUtils';
15+
import { PreviewBody } from './PreviewBody';
2516
import { SaveAsButton } from './SaveAsButton';
2617

2718
interface FilePreviewPanelProps {
@@ -33,112 +24,10 @@ interface FilePreviewPanelProps {
3324
onBack?: () => void;
3425
}
3526

36-
/** Resolve + load the preview source (text content or same-origin image url). */
37-
function usePreviewSource(file: ArtifactFile | null, versionId: string) {
38-
const [loading, setLoading] = useState(false);
39-
const [error, setError] = useState(false);
40-
const [text, setText] = useState('');
41-
const [imageUrl, setImageUrl] = useState('');
42-
43-
useEffect(() => {
44-
setText('');
45-
setImageUrl('');
46-
setError(false);
47-
if (!file) return undefined;
48-
const kind = getArtifactPreviewKind(file);
49-
if (kind === 'unsupported') return undefined;
50-
51-
let cancelled = false;
52-
setLoading(true);
53-
(async () => {
54-
try {
55-
const url = await resolveArtifactUrl(file.file_url, versionId);
56-
if (cancelled) return;
57-
if (kind === 'image') {
58-
setImageUrl(url);
59-
} else {
60-
const response = await fetch(url);
61-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
62-
const content = await response.text();
63-
if (!cancelled) setText(content);
64-
}
65-
} catch (e) {
66-
console.error('artifact preview failed:', e);
67-
if (!cancelled) setError(true);
68-
} finally {
69-
if (!cancelled) setLoading(false);
70-
}
71-
})();
72-
return () => {
73-
cancelled = true;
74-
};
75-
}, [file, versionId]);
76-
77-
return { loading, error, text, imageUrl };
78-
}
79-
8027
export function FilePreviewPanel({ open, onOpenChange, file, versionId, onBack }: FilePreviewPanelProps) {
8128
const localize = useLocalize();
82-
const { showToast } = useToastContext();
83-
const { loading, error, text, imageUrl } = usePreviewSource(open ? file : null, versionId);
8429

8530
if (!file) return null;
86-
const kind = getArtifactPreviewKind(file);
87-
88-
const handleDownloadToView = async () => {
89-
try {
90-
await downloadArtifactFile(file, versionId);
91-
} catch (e) {
92-
console.error('artifact download failed:', e);
93-
showToast?.({ message: localize('com_linsight_download_failed'), severity: NotificationSeverity.ERROR });
94-
}
95-
};
96-
97-
const renderBody = () => {
98-
if (kind === 'unsupported' || error) {
99-
// not inline-renderable (docx / pdf / xlsx …) or load failure → download hint
100-
return (
101-
<div className="flex h-full flex-col items-center justify-center gap-2 p-6">
102-
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- FileIcon accepts more types than its union */}
103-
<FileIcon type={getFileExtension(file.file_name) as any} className="h-20 w-20" />
104-
<div className="max-w-[80%] truncate text-base font-medium text-gray-900">
105-
{file.file_name}
106-
</div>
107-
<div className="mb-2 text-sm text-gray-500">
108-
{error
109-
? localize('com_linsight_preview_load_failed')
110-
: localize('com_linsight_preview_unsupported')}
111-
</div>
112-
<Button variant="outline" size="sm" onClick={handleDownloadToView}>
113-
{localize('com_linsight_download_to_view')}
114-
</Button>
115-
</div>
116-
);
117-
}
118-
if (loading) {
119-
return (
120-
<div className="flex h-full items-center justify-center">
121-
<img className="size-6 animate-spin" src={`${__APP_ENV__.BASE_URL}/assets/load.webp`} alt="" />
122-
</div>
123-
);
124-
}
125-
if (kind === 'image') {
126-
return (
127-
<div className="flex h-full items-center justify-center p-4">
128-
<img className="max-h-full max-w-full object-contain" src={imageUrl} alt={file.file_name} />
129-
</div>
130-
);
131-
}
132-
if (kind === 'markdown') {
133-
return (
134-
<div className="bs-mkdown p-8">
135-
<Markdown content={text} isLatestMessage={true} webContent={false} />
136-
</div>
137-
);
138-
}
139-
// plain text
140-
return <pre className="whitespace-pre-wrap break-words p-6 text-sm text-gray-800">{text}</pre>;
141-
};
14231

14332
return (
14433
<Sheet open={open} onOpenChange={onOpenChange}>
@@ -175,7 +64,9 @@ export function FilePreviewPanel({ open, onOpenChange, file, versionId, onBack }
17564
{/* content */}
17665
{/* scrollbar-os: opt out of the forced custom webkit scrollbar so the
17766
OS setting (auto-hide vs always-on) is respected (see style.css). */}
178-
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-os">{renderBody()}</div>
67+
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-os">
68+
<PreviewBody file={file} versionId={versionId} />
69+
</div>
17970
</SheetContent>
18071
</Sheet>
18172
);

src/frontend/client/src/components/Linsight/Artifacts/PreviewBody.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Colored, Outlined } from 'bisheng-icons';
88
import { useEffect, useState } from 'react';
99
import { NotificationSeverity } from '~/common';
1010
import Markdown from '~/components/Chat/Messages/Content/Markdown';
11+
import FilePreview from '~/pages/knowledge/FilePreview';
1112
import { Button } from '~/components/ui';
1213
import { LoadingIcon } from '~/components/ui/icon/Loading';
1314
import { useLocalize } from '~/hooks';
@@ -50,7 +51,7 @@ const OUTLINED_FILE_ICONS: Record<string, React.ComponentType<{ className?: stri
5051
// WorkspacePanel instance, which would otherwise re-fetch and flash a spinner every
5152
// time. Artifacts are immutable per version, so versionId+url is a safe key — a hit
5253
// lets the new instance render instantly with no spinner and no network round-trip.
53-
const previewCache = new Map<string, { text?: string; imageUrl?: string }>();
54+
const previewCache = new Map<string, { text?: string; imageUrl?: string; resolvedUrl?: string }>();
5455
const previewCacheKey = (file: ArtifactFile, versionId: string) => `${versionId}::${file.file_url}`;
5556

5657
/** Resolve + load the preview source (text content or same-origin image url). */
@@ -64,19 +65,24 @@ export function usePreviewSource(file: ArtifactFile | null, versionId: string) {
6465
const [error, setError] = useState(false);
6566
const [text, setText] = useState(cached?.text ?? '');
6667
const [imageUrl, setImageUrl] = useState(cached?.imageUrl ?? '');
68+
// For 'document' kind we only resolve the share url and hand it to FilePreview,
69+
// which fetches/parses the bytes itself (pdfjs / mammoth / xlsx).
70+
const [resolvedUrl, setResolvedUrl] = useState(cached?.resolvedUrl ?? '');
6771

6872
useEffect(() => {
6973
setError(false);
7074
if (!file) {
7175
setText('');
7276
setImageUrl('');
77+
setResolvedUrl('');
7378
setLoading(false);
7479
return undefined;
7580
}
7681
const kind = getArtifactPreviewKind(file);
7782
if (kind === 'unsupported') {
7883
setText('');
7984
setImageUrl('');
85+
setResolvedUrl('');
8086
setLoading(false);
8187
return undefined;
8288
}
@@ -87,13 +93,15 @@ export function usePreviewSource(file: ArtifactFile | null, versionId: string) {
8793
// Already fetched — render immediately, no spinner, no refetch.
8894
setText(hit.text ?? '');
8995
setImageUrl(hit.imageUrl ?? '');
96+
setResolvedUrl(hit.resolvedUrl ?? '');
9097
setLoading(false);
9198
return undefined;
9299
}
93100

94101
let cancelled = false;
95102
setText('');
96103
setImageUrl('');
104+
setResolvedUrl('');
97105
setLoading(true);
98106
(async () => {
99107
try {
@@ -102,6 +110,10 @@ export function usePreviewSource(file: ArtifactFile | null, versionId: string) {
102110
if (kind === 'image') {
103111
previewCache.set(key, { imageUrl: url });
104112
setImageUrl(url);
113+
} else if (kind === 'document') {
114+
// Resolve only — FilePreview's viewers fetch the bytes themselves.
115+
previewCache.set(key, { resolvedUrl: url });
116+
setResolvedUrl(url);
105117
} else {
106118
const response = await fetch(url);
107119
if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -122,7 +134,7 @@ export function usePreviewSource(file: ArtifactFile | null, versionId: string) {
122134
};
123135
}, [file, versionId]);
124136

125-
return { loading, error, text, imageUrl };
137+
return { loading, error, text, imageUrl, resolvedUrl };
126138
}
127139

128140
interface PreviewBodyProps {
@@ -133,7 +145,7 @@ interface PreviewBodyProps {
133145
export function PreviewBody({ file, versionId }: PreviewBodyProps) {
134146
const localize = useLocalize();
135147
const { showToast } = useToastContext();
136-
const { loading, error, text, imageUrl } = usePreviewSource(file, versionId);
148+
const { loading, error, text, imageUrl, resolvedUrl } = usePreviewSource(file, versionId);
137149
const kind = getArtifactPreviewKind(file);
138150

139151
const handleDownloadToView = async () => {
@@ -185,6 +197,19 @@ export function PreviewBody({ file, versionId }: PreviewBodyProps) {
185197
</div>
186198
);
187199
}
200+
if (kind === 'document') {
201+
// docx / pdf / xls / xlsx / csv → reuse the shared knowledge-space viewer.
202+
// compactMode = viewer only (no top bar / sidebar / zoom): Linsight's own
203+
// toolbar (back / download / fullscreen / close) provides the chrome.
204+
return (
205+
<FilePreview
206+
fileName={file.file_name}
207+
fileType={getFileExtension(file.file_name)}
208+
fileUrl={resolvedUrl}
209+
compactMode
210+
/>
211+
);
212+
}
188213
if (kind === 'markdown') {
189214
return (
190215
<div className="bs-mkdown p-8">

0 commit comments

Comments
 (0)