mirror of
https://github.com/dataelement/bisheng.git
synced 2026-09-19 01:27:18 +08:00
Merge branch 'feat/2.6.0-beta4' of github.com:dataelement/bisheng into feat/2.6.0-beta4
This commit is contained in:
@@ -330,7 +330,8 @@ export default function AiChatMessages({
|
||||
hideShare={hideShare}
|
||||
conversation={{ title: headerTitleText, flowId: "", conversationId, flowType: 15 }}
|
||||
onOpenWorkspace={onOpenWorkspace}
|
||||
hasWorkspaceFiles={hasWorkspaceFiles && !workspaceOpen}
|
||||
hasWorkspaceFiles={hasWorkspaceFiles}
|
||||
workspaceOpen={workspaceOpen}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
|
||||
@@ -364,6 +364,33 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
|
||||
// auto-expand (the entry icon appearing is enough).
|
||||
const { getLinsight, updateLinsight } = useLinsightManager();
|
||||
const taskArtifacts = useWorkspacePanel();
|
||||
|
||||
// F035: enter/exit animation for the fullscreen workspace overlay. The overlay
|
||||
// is a separate instance from the docked panel (PreviewBody isn't cached, so we
|
||||
// never mount both at once). `fsMounted` keeps it in the DOM across the collapse
|
||||
// transition; `fsExpanded` drives the scale/opacity. Entering: mount collapsed,
|
||||
// then expand on the next frame so the transition runs. Exiting: collapse, then
|
||||
// unmount on transitionEnd.
|
||||
const [fsMounted, setFsMounted] = useState(false);
|
||||
const [fsExpanded, setFsExpanded] = useState(false);
|
||||
useEffect(() => {
|
||||
if (taskArtifacts.fullscreen) {
|
||||
setFsMounted(true);
|
||||
// Double rAF: let the collapsed state paint before flipping to expanded,
|
||||
// otherwise React can batch both and the enter transition is skipped.
|
||||
let r2 = 0;
|
||||
const r1 = requestAnimationFrame(() => {
|
||||
r2 = requestAnimationFrame(() => setFsExpanded(true));
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(r1);
|
||||
cancelAnimationFrame(r2);
|
||||
};
|
||||
}
|
||||
setFsExpanded(false);
|
||||
return undefined;
|
||||
}, [taskArtifacts.fullscreen]);
|
||||
|
||||
const taskLinsight = latestTaskVersionId ? getLinsight(latestTaskVersionId) : null;
|
||||
const taskWorkspaceFiles = useMemo(() => {
|
||||
const uploaded = toUploadedArtifacts(taskLinsight?.files as any[]);
|
||||
@@ -454,7 +481,7 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
|
||||
isStreaming={isStreaming}
|
||||
shareToken={shareToken}
|
||||
knowledgeChatLayout
|
||||
contentWidthClassName="w-full max-w-[800px] mx-auto px-4 touch-mobile:max-w-full"
|
||||
contentWidthClassName="w-full max-w-[800px] mx-auto px-3 touch-mobile:max-w-full"
|
||||
onRegenerate={regenerate}
|
||||
onOpenCitationPanel={onOpenCitationPanel}
|
||||
activeCitationMessageId={activeCitationMessageId}
|
||||
@@ -531,19 +558,36 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
|
||||
{/* F035: inline workspace panel docked to the right of the chat
|
||||
main for the latest task turn. Opens from the header entry
|
||||
button; the file preview renders in place. Fullscreen is a
|
||||
separate overlay (below) covering the whole route viewport. */}
|
||||
{latestTaskVersionId && taskArtifacts.open && !taskArtifacts.fullscreen && (
|
||||
<div className="min-h-0 shrink-0 p-1 w-[46%] min-w-[440px] max-w-[720px]">
|
||||
<WorkspacePanel
|
||||
files={taskWorkspaceFiles}
|
||||
versionId={latestTaskVersionId}
|
||||
previewFile={taskArtifacts.previewFile}
|
||||
fullscreen={false}
|
||||
onPreview={taskArtifacts.openPreview}
|
||||
onBack={taskArtifacts.backToList}
|
||||
onClose={taskArtifacts.closeWorkspace}
|
||||
onToggleFullscreen={taskArtifacts.toggleFullscreen}
|
||||
/>
|
||||
separate overlay (below) covering the whole route viewport.
|
||||
|
||||
Kept mounted whenever a task turn exists (gated only on
|
||||
!fullscreen) so open/close animates the wrapper's width +
|
||||
opacity instead of hard mount/unmount. Width uses an inline
|
||||
clamp() (not min/max-w classes) so it interpolates smoothly
|
||||
0px → 46% — min-width doesn't transition and would otherwise
|
||||
snap to 440px on the first frame. The inner box keeps a
|
||||
min-width so the panel content slides/clips rather than
|
||||
reflowing while it collapses. */}
|
||||
{latestTaskVersionId && !taskArtifacts.fullscreen && (
|
||||
<div
|
||||
className={cn(
|
||||
'min-h-0 shrink-0 overflow-hidden transition-[width,opacity,padding] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]',
|
||||
taskArtifacts.open ? 'p-1 opacity-100' : 'pointer-events-none p-0 opacity-0',
|
||||
)}
|
||||
style={{ width: taskArtifacts.open ? 'clamp(440px, 46%, 720px)' : '0px' }}
|
||||
>
|
||||
<div className="h-full min-w-[420px]">
|
||||
<WorkspacePanel
|
||||
files={taskWorkspaceFiles}
|
||||
versionId={latestTaskVersionId}
|
||||
previewFile={taskArtifacts.previewFile}
|
||||
fullscreen={false}
|
||||
onPreview={taskArtifacts.openPreview}
|
||||
onBack={taskArtifacts.backToList}
|
||||
onClose={taskArtifacts.closeWorkspace}
|
||||
onToggleFullscreen={taskArtifacts.toggleFullscreen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -628,9 +672,25 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
|
||||
viewport (chatContainerRef is relative + un-clipped), flush to the
|
||||
edges with no padding. Covers the chat (incl. its header) but not the
|
||||
browser, so the global nav stays. Separate instance from the inline
|
||||
panel above (toggling fullscreen remounts the preview). */}
|
||||
{latestTaskVersionId && taskArtifacts.open && taskArtifacts.fullscreen && (
|
||||
<div className="absolute inset-0 z-50 bg-white">
|
||||
panel above (only one is ever mounted, so the preview never double
|
||||
fetches). Scale + opacity transition from the right (where the docked
|
||||
panel sits) so expanding/collapsing fullscreen animates smoothly
|
||||
instead of hard mount/unmount; unmounts on transitionEnd after the
|
||||
collapse so the exit plays too. */}
|
||||
{latestTaskVersionId && fsMounted && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-50 origin-right bg-white transition-[transform,opacity] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]',
|
||||
fsExpanded ? 'scale-100 opacity-100' : 'scale-[0.96] opacity-0',
|
||||
)}
|
||||
onTransitionEnd={(e) => {
|
||||
// Unmount only after the collapse finishes (ignore the expand end
|
||||
// and bubbled child transitions).
|
||||
if (e.target === e.currentTarget && e.propertyName === 'transform' && !taskArtifacts.fullscreen) {
|
||||
setFsMounted(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<WorkspacePanel
|
||||
files={taskWorkspaceFiles}
|
||||
versionId={latestTaskVersionId}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PanelRight } from 'lucide-react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocalize, useMediaQuery, usePrefersMobileLayout } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import ShareChat from '../Share/ShareChat';
|
||||
|
||||
const types = {
|
||||
@@ -10,12 +11,15 @@ const types = {
|
||||
15: 'workbench_chat'
|
||||
} as const;
|
||||
|
||||
export default function HeaderTitle({ conversation, readOnly, hideShare = false, onOpenWorkspace, hasWorkspaceFiles = false }: {
|
||||
export default function HeaderTitle({ conversation, readOnly, hideShare = false, onOpenWorkspace, hasWorkspaceFiles = false, workspaceOpen = false }: {
|
||||
conversation?: any;
|
||||
readOnly?: boolean;
|
||||
hideShare?: boolean;
|
||||
onOpenWorkspace?: () => void;
|
||||
hasWorkspaceFiles?: boolean;
|
||||
/** Workspace panel open state — drives the entry button's fade so it stays
|
||||
in sync with the panel's open/close animation instead of hard-toggling. */
|
||||
workspaceOpen?: boolean;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { pathname } = useLocation();
|
||||
@@ -32,7 +36,11 @@ export default function HeaderTitle({ conversation, readOnly, hideShare = false,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-10 flex h-[56px] w-full items-center justify-between bg-white px-4 text-[#212121]">
|
||||
<div
|
||||
className={cn(
|
||||
'sticky top-0 z-10 flex h-[56px] w-full items-center justify-between bg-white pl-4 pr-4 text-[#212121]',
|
||||
)}
|
||||
>
|
||||
{/* Left placeholder to balance the center layout */}
|
||||
<div className="flex-1"></div>
|
||||
|
||||
@@ -53,16 +61,26 @@ export default function HeaderTitle({ conversation, readOnly, hideShare = false,
|
||||
/>
|
||||
)}
|
||||
{/* F035: task-mode workspace entry — opens the drawer of uploaded sources +
|
||||
generated deliverables for the latest task turn (ChatView owns the drawer). */}
|
||||
generated deliverables for the latest task turn (ChatView owns the drawer).
|
||||
Kept mounted and faded by `workspaceOpen` (not mount/unmounted) so it
|
||||
tracks the panel's open/close animation. On open it fades out instantly
|
||||
(the panel covers it); on close it waits `delay-150` so it reappears only
|
||||
after the panel has mostly collapsed — avoids the button flashing in
|
||||
mid-animation. */}
|
||||
{hasWorkspaceFiles && onOpenWorkspace && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenWorkspace}
|
||||
title={localize('com_linsight_workspace')}
|
||||
aria-label="workspace"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-lg text-gray-600 hover:bg-gray-100"
|
||||
className={cn(
|
||||
'flex h-7 shrink-0 items-center justify-center overflow-hidden rounded-lg text-gray-600 transition-[width,opacity] duration-200 hover:bg-gray-100',
|
||||
// Collapse width to 0 (not just opacity) when open so it reserves no
|
||||
// slot in the header's right cell; expand back with a short delay on close.
|
||||
workspaceOpen ? 'pointer-events-none w-0 opacity-0' : 'w-7 opacity-100 delay-150',
|
||||
)}
|
||||
>
|
||||
<PanelRight size={16} />
|
||||
<PanelRight size={16} className="shrink-0" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ResultSection({ answer, files, versionId, onPreview }: ResultSec
|
||||
|
||||
{/* answer summary, markdown rendered */}
|
||||
{answer && (
|
||||
<div className="bs-mkdown rounded-2xl border border-gray-100 bg-white p-4 text-sm leading-6 text-gray-800">
|
||||
<div className="bs-mkdown rounded-2xl border border-gray-100 bg-white p-4 text-sm leading-6 text-gray-800 [&_p:last-child]:mb-0">
|
||||
<Markdown content={answer} isLatestMessage={true} webContent={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,7 @@ interface WorkspacePanelProps {
|
||||
}
|
||||
|
||||
const iconBtn =
|
||||
'flex h-7 w-7 items-center justify-center rounded-md text-[#8C8C8C] transition-colors hover:bg-gray-100 hover:text-[#335CFF]';
|
||||
'flex h-7 w-7 items-center justify-center rounded-lg text-[#8C8C8C] transition-colors hover:bg-gray-100';
|
||||
|
||||
export function WorkspacePanel({
|
||||
files,
|
||||
@@ -47,7 +47,7 @@ export function WorkspacePanel({
|
||||
key={file.file_id || file.file_url}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="group flex cursor-pointer items-center gap-2.5 rounded-lg py-2 hover:bg-[#F7F7F7]"
|
||||
className="group flex cursor-pointer items-center gap-2.5 rounded-lg px-1 py-2 hover:bg-[#F7F7F7]"
|
||||
onClick={() => onPreview(file)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && onPreview(file)}
|
||||
>
|
||||
@@ -81,7 +81,7 @@ export function WorkspacePanel({
|
||||
{previewFile ? (
|
||||
<>
|
||||
{/* preview toolbar */}
|
||||
<div className="flex h-[52px] shrink-0 items-center gap-2 px-3">
|
||||
<div className="flex h-12 shrink-0 items-center gap-2 px-3">
|
||||
<button type="button" aria-label={localize('com_linsight_back_to_workspace')} className={iconBtn} onClick={onBack}>
|
||||
<Outlined.ArrowLeft className="size-4" />
|
||||
</button>
|
||||
@@ -115,18 +115,18 @@ export function WorkspacePanel({
|
||||
) : (
|
||||
<>
|
||||
{/* list header */}
|
||||
<div className="flex h-[52px] shrink-0 items-center justify-between px-4">
|
||||
<div className="flex h-12 shrink-0 items-center justify-between px-4">
|
||||
<span className="text-sm font-medium text-[#212121]">{localize('com_linsight_workspace')}</span>
|
||||
<button type="button" aria-label={localize('com_ui_close')} className={iconBtn} onClick={onClose}>
|
||||
<Outlined.Close className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/* list body — overall padding 16px, 16px between rows */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-os p-4">
|
||||
{/* list body — 4px top, 16px bottom, 12px left/right; 16px between rows */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-os px-3 pt-1 pb-4">
|
||||
{files.length ? (
|
||||
<div className="flex flex-col gap-4">{files.map(renderRow)}</div>
|
||||
) : (
|
||||
<div className="py-10 text-center text-sm text-gray-400">
|
||||
<div className="flex h-full items-center justify-center text-center text-sm font-normal text-gray-400">
|
||||
{localize('com_linsight_workspace_empty')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -131,7 +131,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
|
||||
return (
|
||||
<div
|
||||
className="my-3 w-full rounded-2xl border border-[#EEF2F6] bg-white p-6 shadow-[0_4px_20px_rgba(0,0,0,0.03)]"
|
||||
className="my-3 w-full rounded-2xl border border-[#EEF2F6] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.03)]"
|
||||
style={{
|
||||
backgroundImage: 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'5\' height=\'5\'%3E%3Ccircle cx=\'0.5\' cy=\'0.5\' r=\'0.5\' fill=\'%23EAEEFF\'/%3E%3C/svg%3E")',
|
||||
backgroundSize: '5px 5px',
|
||||
@@ -145,7 +145,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="shrink-0 rounded-full p-1 text-[#8C8C8C] hover:bg-gray-100 transition-colors"
|
||||
className="shrink-0 rounded-md p-1 text-[#8C8C8C] hover:bg-gray-100 transition-colors"
|
||||
aria-label="close"
|
||||
>
|
||||
<X size={16} />
|
||||
@@ -154,7 +154,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
|
||||
{/* Body: Current question */}
|
||||
{q ? (
|
||||
<div className="mt-4">
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[14px] font-bold text-[#1A1A1A]">{q.question}</span>
|
||||
@@ -174,7 +174,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
type="button"
|
||||
disabled={page === 0}
|
||||
onClick={() => setPage(page - 1)}
|
||||
className="rounded-full p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
|
||||
className="rounded-md p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
@@ -185,14 +185,14 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
type="button"
|
||||
disabled={page === questions.length - 1}
|
||||
onClick={() => setPage(page + 1)}
|
||||
className="rounded-full p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
|
||||
className="rounded-md p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ul className="mt-4 space-y-3">
|
||||
<ul className="mt-4 space-y-2">
|
||||
{q.options.map((option, i) => {
|
||||
const active = selected.includes(option);
|
||||
const { title: optTitle, desc: optDesc } = parseOption(option);
|
||||
@@ -203,9 +203,9 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
disabled={disabled || submitted}
|
||||
onClick={() => handleSelect(q, option)}
|
||||
className={cn(
|
||||
'flex w-full items-start gap-2 rounded-xl px-4 py-2.5 text-left text-sm transition-all duration-200 select-none border-0',
|
||||
'flex h-9 w-full items-center gap-2 rounded-lg px-4 text-left text-sm transition-all duration-200 select-none border-0',
|
||||
active
|
||||
? 'bg-[#EDF2FF] text-[#335CFF] font-medium shadow-[0_2px_8px_rgba(51,92,255,0.08)]'
|
||||
? 'bg-[#EEE] text-[#212121] font-medium'
|
||||
: 'text-[#1A1A1A] hover:bg-gray-50/80',
|
||||
)}
|
||||
>
|
||||
@@ -213,12 +213,12 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className={cn(active ? 'text-[#335CFF]' : 'text-[#1A1A1A]')}>{optTitle}</span>
|
||||
{optDesc && (
|
||||
<span className={cn('ml-1 text-[13px] font-normal', active ? 'text-[#335CFF]/80' : 'text-[#8C8C8C]')}>
|
||||
<span className="ml-1 text-[13px] font-normal text-[#8C8C8C]">
|
||||
{optDesc}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{active && <Check size={16} className="shrink-0 text-[#335CFF] self-center" />}
|
||||
{active && <Check size={16} className="shrink-0 text-[#212121] self-center" />}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
@@ -226,7 +226,12 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
|
||||
{/* Trailing "type your own" entry: inline input */}
|
||||
<li>
|
||||
<div
|
||||
className="flex items-center gap-2 rounded-xl bg-[#F5F7FA] px-4 py-2.5 transition-all duration-200"
|
||||
className={cn(
|
||||
'flex h-9 items-center gap-2 rounded-lg px-4 transition-all duration-200',
|
||||
// No box by default (matches the other options); the
|
||||
// input-box background only appears once it's active.
|
||||
customSelected ? 'bg-[#EEE]' : 'hover:bg-gray-50/80',
|
||||
)}
|
||||
>
|
||||
<span className="shrink-0 text-sm font-medium text-[#8C8C8C]">
|
||||
{q.options.length + 1}.
|
||||
|
||||
@@ -227,14 +227,8 @@ export function ExecutionFlow({ versionId, conversationId, isSharePage = false,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── footer: waiting hint + task panel + unified input ─────────── */}
|
||||
{/* ── footer: task panel + unified input ────────────────────────── */}
|
||||
<div className="mx-auto w-full max-w-[800px] shrink-0 px-4 pb-4">
|
||||
{pendingInput && (
|
||||
<div className="mb-2 flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-gray-800">
|
||||
<span className="size-1.5 animate-pulse-scale rounded-full bg-gray-700" />
|
||||
{localize('com_linsight_waiting_your_input')}
|
||||
</div>
|
||||
)}
|
||||
{/* Design (Figma 12221-40080/40081): card inset 24px each side
|
||||
relative to the input, 12px gap above it. */}
|
||||
<div className="px-6 pb-3">
|
||||
|
||||
@@ -14,16 +14,16 @@ export function RunningSpinner() {
|
||||
}
|
||||
|
||||
/** Map a step/tool name to its leading icon (completed state). */
|
||||
export function stepTypeIcon(name?: string) {
|
||||
export function stepTypeIcon(name?: string, size = 16) {
|
||||
const n = (name || '').toLowerCase();
|
||||
const cls = 'text-[#333]';
|
||||
if (/agent|subagent/.test(n)) return <Outlined.PeopleRound size={16} className={cls} />;
|
||||
if (/knowledge|knowledge_base|space|retrieval|recall|检索|知识/.test(n)) return <Outlined.BookOpenText size={16} className={cls} />;
|
||||
if (/think|reason|思考/.test(n)) return <Outlined.Bulb size={16} className={cls} />;
|
||||
if (/research|调研/.test(n)) return <Outlined.Dashboard size={16} className={cls} />;
|
||||
if (/web[_\s-]?search|websearch|联网|网页|网络搜索/.test(n)) return <Outlined.Earth size={16} className={cls} />;
|
||||
if (/write|edit|撰写|编写|写入/.test(n)) return <Outlined.Write size={16} className={cls} />;
|
||||
return <Wrench size={16} className={cls} />; // default
|
||||
if (/agent|subagent/.test(n)) return <Outlined.PeopleRound size={size} className={cls} />;
|
||||
if (/knowledge|knowledge_base|space|retrieval|recall|检索|知识/.test(n)) return <Outlined.BookOpenText size={size} className={cls} />;
|
||||
if (/think|reason|思考/.test(n)) return <Outlined.Bulb size={size} className={cls} />;
|
||||
if (/research|调研/.test(n)) return <Outlined.Dashboard size={size} className={cls} />;
|
||||
if (/web[_\s-]?search|websearch|联网|网页|网络搜索/.test(n)) return <Outlined.Earth size={size} className={cls} />;
|
||||
if (/write|edit|撰写|编写|写入/.test(n)) return <Outlined.Write size={size} className={cls} />;
|
||||
return <Wrench size={size} className={cls} />; // default
|
||||
}
|
||||
|
||||
/** Shared style for expanded detail text blocks. */
|
||||
|
||||
@@ -5,11 +5,28 @@
|
||||
* called inside that agent's namespace; a finished card shows "called N tools".
|
||||
*/
|
||||
import { Outlined } from 'bisheng-icons';
|
||||
import { Check, Recycle } from 'lucide-react';
|
||||
import { Check } from 'lucide-react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
import { StepRow } from './StepRow';
|
||||
import type { MergedStep, SubagentGroup } from './stepUtils';
|
||||
import { StepRow, stepTypeIcon } from './StepRow';
|
||||
import type { SubagentGroup } from './stepUtils';
|
||||
|
||||
/** Dotted base texture, reused verbatim from the clarification card
|
||||
* (ClarifyCard): a 5px-tiled SVG with a faint #EAEEFF dot. */
|
||||
const DOT_BG: CSSProperties = {
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundImage:
|
||||
'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'5\' height=\'5\'%3E%3Ccircle cx=\'0.5\' cy=\'0.5\' r=\'0.5\' fill=\'%23EAEEFF\'/%3E%3C/svg%3E")',
|
||||
backgroundSize: '5px 5px',
|
||||
};
|
||||
/** Diagonal glint overlaid on the dotted base (Figma 12221:40064). A soft grey
|
||||
* streak on its own layer; the `animate-sheen-sweep` keyframe slides it across
|
||||
* the card (clipped by the card's overflow-hidden) so the gradient flows. A
|
||||
* plain white streak is invisible on the near-white card, so it carries a light
|
||||
* grey cast to read as motion. */
|
||||
const SHEEN =
|
||||
'linear-gradient(120deg, transparent 0%, rgba(140,140,140,0.025) 28%, rgba(140,140,140,0.09) 50%, rgba(140,140,140,0.025) 72%, transparent 100%)';
|
||||
|
||||
function SubagentCard({ agent }: { agent: SubagentGroup['agents'][number] }) {
|
||||
const localize = useLocalize();
|
||||
@@ -18,16 +35,41 @@ function SubagentCard({ agent }: { agent: SubagentGroup['agents'][number] }) {
|
||||
const calledCount = children.length;
|
||||
const currentTool = [...children].reverse().find((c) => c.running) || children[children.length - 1];
|
||||
|
||||
// the dotted texture stays in both states; the sweeping sheen is the
|
||||
// "in-progress" treatment only and stops once the agent finishes.
|
||||
const running = step.running;
|
||||
|
||||
return (
|
||||
<div className="min-w-40 max-w-56 rounded-xl border border-gray-200 bg-white p-2.5 shadow-sm">
|
||||
<div className="flex items-center gap-1.5 text-xs font-medium text-gray-700">
|
||||
<Recycle size={12} className="shrink-0 text-blue-500" />
|
||||
<span className="truncate">{step.name}</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center gap-1.5 text-xs text-gray-500">
|
||||
<div
|
||||
className="relative flex min-w-40 max-w-56 flex-col items-start gap-1 overflow-hidden rounded-lg border-[0.5px] border-[#ECECEC] px-4 py-2 shadow-[0px_4px_6px_0px_rgba(167,186,224,0.05)]"
|
||||
style={DOT_BG}
|
||||
>
|
||||
{/* diagonal glint sweeping above the dots; semi-transparent so the
|
||||
dot texture still reads through. The static mask fades the streak
|
||||
near the card's L/R edges, so the overflow-hidden clip lands in an
|
||||
already-transparent zone instead of cutting the band into a hard
|
||||
vertical line. Only while the agent is running. */}
|
||||
{running && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0"
|
||||
style={{
|
||||
WebkitMaskImage: 'linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%)',
|
||||
maskImage: 'linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%)',
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 animate-sheen-sweep" style={{ backgroundImage: SHEEN }} />
|
||||
</div>
|
||||
)}
|
||||
{/* circular white badge carrying the agent's type icon */}
|
||||
<span className="relative flex items-center rounded-full bg-white p-[5px]">
|
||||
{stepTypeIcon(step.name, 14)}
|
||||
</span>
|
||||
<span className="relative max-w-full truncate text-xs leading-5 text-[#1D2129]">{step.name}</span>
|
||||
<div className="relative flex max-w-full items-center gap-2 text-xs leading-5 text-[#999]">
|
||||
{step.running ? (
|
||||
<>
|
||||
<span className="size-1.5 shrink-0 animate-pulse rounded-full bg-blue-500" />
|
||||
<span className="size-2 shrink-0 animate-pulse rounded-full bg-[#212121]" />
|
||||
<span className="truncate">{currentTool?.name || localize('com_linsight_subagent_running')}</span>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -204,13 +204,6 @@ export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = fa
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* waiting-for-input hint */}
|
||||
{pendingInput && (
|
||||
<div className="mb-2 mt-2 flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-gray-800">
|
||||
<span className="size-1.5 animate-pulse-scale rounded-full bg-gray-700" />
|
||||
{localize('com_linsight_waiting_your_input')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* task checklist progress is rendered by <PinnedTaskPanel> pinned
|
||||
above the input (ChatView) — not inline in the message stream. */}
|
||||
@@ -221,8 +214,8 @@ export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = fa
|
||||
synthesizes get_final_result_file / the report), so the user does
|
||||
not mistake an in-progress task for a finished one. */}
|
||||
{running && !queueing && !planning && !pendingInput && (
|
||||
<div className="mb-2 mt-2 flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-gray-800">
|
||||
<span className="size-1.5 animate-pulse-scale rounded-full bg-gray-700" />
|
||||
<div className="mb-2 mt-2 flex items-center gap-2 rounded-lg py-1.5 text-xs text-gray-800">
|
||||
<span className="inline-block size-3 animate-pulse-scale rounded-full bg-black" />
|
||||
{localize('com_linsight_generating')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -200,9 +200,6 @@ export function useFileManager({ activeSpace, initialFolderId, enabled = true }:
|
||||
setTotal(files.length + (hasMore ? 1 : 0));
|
||||
}, [files.length, hasMore]);
|
||||
|
||||
// Track which initialFolderId has been consumed (value, not boolean)
|
||||
// so re-navigation to a different folder deep link works correctly.
|
||||
const consumedFolderIdRef = useRef<string | undefined>(undefined);
|
||||
// Bumped on space switch to guarantee the filter effect fires even when
|
||||
// search state was already empty (no dep change otherwise).
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
@@ -215,9 +212,12 @@ export function useFileManager({ activeSpace, initialFolderId, enabled = true }:
|
||||
setSearchTagIds([]);
|
||||
setStatusFilter([]);
|
||||
|
||||
// If there's an unconsumed initial folder from URL, navigate there
|
||||
if (initialFolderId && consumedFolderIdRef.current !== initialFolderId) {
|
||||
consumedFolderIdRef.current = initialFolderId;
|
||||
// The URL folder id is the single source of truth — sync the content
|
||||
// pane to it on every change. A previous "consumed" guard skipped
|
||||
// re-entering an already-visited folder (its id was still marked
|
||||
// consumed), which left the URL on /folder/<id> while the pane reset
|
||||
// to the space root — folders became un-enterable from the sidebar tree.
|
||||
if (initialFolderId) {
|
||||
setCurrentFolderId(initialFolderId);
|
||||
// Fetch the breadcrumb path for the URL folder. The API now
|
||||
// includes the folder itself as the leaf (with its real name),
|
||||
|
||||
@@ -40,10 +40,16 @@ module.exports = {
|
||||
'0%': { transform: 'translateX(-100%)' },
|
||||
'100%': { transform: 'translateX(400%)' },
|
||||
},
|
||||
// diagonal glint sweeping continuously across the subagent card
|
||||
'sheen-sweep': {
|
||||
'0%': { transform: 'translateX(-130%)' },
|
||||
'100%': { transform: 'translateX(130%)' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-out forwards',
|
||||
'crawl-slide': 'crawl-slide 1.4s linear infinite',
|
||||
'sheen-sweep': 'sheen-sweep 2s linear infinite',
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
'pulse-scale': 'pulse-scale 1s ease-in-out infinite',
|
||||
|
||||
Reference in New Issue
Block a user