mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-22 11:15:47 +00:00
update workspace page structure with subpage content
This commit is contained in:
parent
913cebafd4
commit
2e712e013f
10 changed files with 1098 additions and 561 deletions
|
|
@ -263,7 +263,7 @@ export function UserMessageCard({
|
|||
<UserMessageRichContent content={content} variant="card" />
|
||||
{canClamp && !expanded && (
|
||||
<div
|
||||
className="inset-x-0 bottom-0 h-14 bg-ds-bg-neutral-default-default pointer-events-none absolute z-[1]"
|
||||
className="inset-x-0 bottom-0 h-14 bg-ds-bg-neutral-subtle-default pointer-events-none absolute z-[1]"
|
||||
style={USER_MESSAGE_FOLD_FADE_STYLE}
|
||||
aria-hidden
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,243 +0,0 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { usePageTabStore } from '@/store/pageTabStore';
|
||||
import { motion } from 'framer-motion';
|
||||
import { PenLine, ScrollText } from 'lucide-react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PROJECT_SIDEBAR_FOLD_SPRING } from './constants';
|
||||
import { WORKSPACE_TAB_LABEL_CLASS, workspaceTabButtonClass } from './NavTab';
|
||||
|
||||
const MEMORY_STORAGE_KEY = 'eigent-sidebar-instructions-memory-on';
|
||||
const INSTRUCTIONS_ACCORDION_STORAGE_KEY =
|
||||
'eigent-sidebar-instructions-accordion-open';
|
||||
|
||||
function readMemoryInitial(): boolean {
|
||||
if (typeof window === 'undefined') return true;
|
||||
const v = window.localStorage.getItem(MEMORY_STORAGE_KEY);
|
||||
if (v === null) return true;
|
||||
return v === 'true';
|
||||
}
|
||||
|
||||
function readInstructionsAccordionPreference(): string | undefined {
|
||||
if (typeof window === 'undefined') return 'instructions';
|
||||
const v = window.localStorage.getItem(INSTRUCTIONS_ACCORDION_STORAGE_KEY);
|
||||
if (v === null) return 'instructions';
|
||||
return v === 'true' ? 'instructions' : undefined;
|
||||
}
|
||||
|
||||
const accordionItemClass = cn(
|
||||
'border-none rounded-xl transition-colors',
|
||||
'data-[state=open]:bg-ds-bg-neutral-subtle-default'
|
||||
);
|
||||
|
||||
const accordionTriggerClass = cn(
|
||||
workspaceTabButtonClass(false),
|
||||
'hover:no-underline',
|
||||
'hover:bg-ds-bg-neutral-subtle-default',
|
||||
'py-0 min-h-8',
|
||||
'[&>svg:last-child]:text-ds-icon-neutral-muted-default [&>svg:last-child]:shrink-0'
|
||||
);
|
||||
|
||||
/** Project sidebar: Instructions accordion (NavTab-aligned). */
|
||||
export function HeaderAction() {
|
||||
const { t } = useTranslation();
|
||||
const setActiveWorkspaceTab = usePageTabStore((s) => s.setActiveWorkspaceTab);
|
||||
const requestWorkspaceChatFocus = usePageTabStore(
|
||||
(s) => s.requestWorkspaceChatFocus
|
||||
);
|
||||
const projectSidebarFolded = usePageTabStore((s) => s.projectSidebarFolded);
|
||||
const setProjectSidebarFolded = usePageTabStore(
|
||||
(s) => s.setProjectSidebarFolded
|
||||
);
|
||||
const openInstructionsAfterExpandRef = useRef(false);
|
||||
const [memoryOn, setMemoryOn] = useState(readMemoryInitial);
|
||||
const [instructionsOpen, setInstructionsOpen] = useState<string | undefined>(
|
||||
readInstructionsAccordionPreference
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (projectSidebarFolded) {
|
||||
setInstructionsOpen(undefined);
|
||||
} else if (openInstructionsAfterExpandRef.current) {
|
||||
openInstructionsAfterExpandRef.current = false;
|
||||
setInstructionsOpen('instructions');
|
||||
} else {
|
||||
setInstructionsOpen(readInstructionsAccordionPreference());
|
||||
}
|
||||
}, [projectSidebarFolded]);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(MEMORY_STORAGE_KEY, String(memoryOn));
|
||||
}, [memoryOn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectSidebarFolded) return;
|
||||
window.localStorage.setItem(
|
||||
INSTRUCTIONS_ACCORDION_STORAGE_KEY,
|
||||
instructionsOpen === 'instructions' ? 'true' : 'false'
|
||||
);
|
||||
}, [instructionsOpen, projectSidebarFolded]);
|
||||
|
||||
const handleImportWorkforceTemplate = useCallback(() => {
|
||||
setActiveWorkspaceTab('workforce');
|
||||
}, [setActiveWorkspaceTab]);
|
||||
|
||||
const handleEditInstructions = useCallback(() => {
|
||||
requestWorkspaceChatFocus();
|
||||
}, [requestWorkspaceChatFocus]);
|
||||
|
||||
const instructionsLabel = t('layout.instructions', {
|
||||
defaultValue: 'Instructions',
|
||||
});
|
||||
const instructionsHint = t('layout.instructions-rules-tone', {
|
||||
defaultValue: 'Rules & Tone',
|
||||
});
|
||||
const memoryLabel = t('layout.memory', { defaultValue: 'Memory' });
|
||||
const memoryOnLabel = t('layout.memory-on', { defaultValue: 'On' });
|
||||
const memoryOffLabel = t('layout.memory-off', { defaultValue: 'Off' });
|
||||
const workforceSettingLabel = t('layout.workforce-setting', {
|
||||
defaultValue: 'Workforce Setting',
|
||||
});
|
||||
const selectLabel = t('layout.select', { defaultValue: 'Select' });
|
||||
const editInstructionsLabel = t('layout.edit-instructions', {
|
||||
defaultValue: 'Edit instructions',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-w-0 flex items-stretch">
|
||||
<div className="no-drag min-w-0 w-full overflow-hidden">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
value={instructionsOpen ?? ''}
|
||||
onValueChange={(v) => {
|
||||
if (projectSidebarFolded) return;
|
||||
const next = v || undefined;
|
||||
setInstructionsOpen(next);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<AccordionItem value="instructions" className={accordionItemClass}>
|
||||
<AccordionTrigger
|
||||
className={cn(
|
||||
accordionTriggerClass,
|
||||
projectSidebarFolded && '[&>svg:last-child]:hidden'
|
||||
)}
|
||||
title={
|
||||
projectSidebarFolded ? String(instructionsLabel) : undefined
|
||||
}
|
||||
onClick={(e) => {
|
||||
if (!projectSidebarFolded) return;
|
||||
openInstructionsAfterExpandRef.current = true;
|
||||
setProjectSidebarFolded(false);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 flex flex-1 items-center overflow-hidden',
|
||||
projectSidebarFolded ? 'gap-0' : 'gap-3'
|
||||
)}
|
||||
>
|
||||
<ScrollText
|
||||
className="h-4 w-4 text-ds-icon-neutral-muted-default shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
<motion.span
|
||||
className={cn(WORKSPACE_TAB_LABEL_CLASS, 'min-w-0 flex-1')}
|
||||
initial={false}
|
||||
animate={{
|
||||
opacity: projectSidebarFolded ? 0 : 1,
|
||||
maxWidth: projectSidebarFolded ? 0 : 1600,
|
||||
}}
|
||||
transition={PROJECT_SIDEBAR_FOLD_SPRING}
|
||||
aria-hidden={projectSidebarFolded}
|
||||
>
|
||||
{instructionsLabel}
|
||||
</motion.span>
|
||||
</span>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-3 py-3">
|
||||
<div className="-mr-1 gap-3 pl-7 flex flex-col">
|
||||
<div className="min-w-0 gap-2 rounded-lg hover:bg-ds-bg-neutral-subtle-default hover:ring-ds-bg-neutral-subtle-default hover:ring-offset-ds-bg-neutral-subtle-default flex items-center justify-between hover:ring-2 hover:ring-offset-2">
|
||||
<span className="min-w-0 text-body-sm font-medium text-ds-text-neutral-muted-default flex-1">
|
||||
{instructionsHint}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
buttonContent="icon-only"
|
||||
className="no-drag shrink-0"
|
||||
aria-label={editInstructionsLabel}
|
||||
onClick={handleEditInstructions}
|
||||
>
|
||||
<PenLine className="h-4 w-4 shrink-0" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-w-0 gap-2 rounded-lg hover:bg-ds-bg-neutral-subtle-default hover:ring-ds-bg-neutral-subtle-default hover:ring-offset-ds-bg-neutral-subtle-default flex items-center justify-between hover:ring-2 hover:ring-offset-2">
|
||||
<span className="min-w-0 text-body-sm font-medium text-ds-text-neutral-muted-default">
|
||||
{memoryLabel}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
className="no-drag shrink-0"
|
||||
onClick={() => setMemoryOn((v) => !v)}
|
||||
aria-pressed={memoryOn}
|
||||
aria-label={`${memoryLabel}: ${memoryOn ? memoryOnLabel : memoryOffLabel}`}
|
||||
>
|
||||
{memoryOn ? memoryOnLabel : memoryOffLabel}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-w-0 gap-2 rounded-lg hover:bg-ds-bg-neutral-subtle-default hover:ring-ds-bg-neutral-subtle-default hover:ring-offset-ds-bg-neutral-subtle-default flex items-center justify-between hover:ring-2 hover:ring-offset-2">
|
||||
<span className="min-w-0 text-body-sm font-medium text-ds-text-neutral-muted-default">
|
||||
{workforceSettingLabel}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
className="no-drag shrink-0"
|
||||
onClick={handleImportWorkforceTemplate}
|
||||
aria-label={`${workforceSettingLabel}: ${selectLabel}`}
|
||||
>
|
||||
{selectLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -12,14 +12,11 @@
|
|||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { TooltipSimple } from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { LayoutGrid, Plus } from 'lucide-react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SIDEBAR_TOOLTIP_CONTENT_CLASS } from './constants';
|
||||
import { NavListSessionRows, type NavListSession } from './NavListSessionRows';
|
||||
import { NavTab, workspaceTabButtonClass } from './NavTab';
|
||||
import { NavTab } from './NavTab';
|
||||
|
||||
export {
|
||||
NAV_LIST_SESSIONS_RECENT_MAX,
|
||||
|
|
@ -32,33 +29,26 @@ export interface NavListProps {
|
|||
activeSessionId?: string | null;
|
||||
onSessionClick?: (sessionId: string) => void;
|
||||
onDeleteSession?: (sessionId: string) => void;
|
||||
/** Top row: workspace tab — switches to workforce view. */
|
||||
workspaceActive: boolean;
|
||||
onWorkspaceClick: () => void;
|
||||
/** Trailing + control (e.g. create task + focus session). */
|
||||
onNewSession: () => void;
|
||||
/** Icon-only rail: match other sidebar `NavTab`s. */
|
||||
folded: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** Workspace row (split: tab + new session) and a flat scrollable session column. */
|
||||
/** New Session row and a flat scrollable session column. */
|
||||
export function NavList({
|
||||
sessions,
|
||||
activeSessionId,
|
||||
onSessionClick,
|
||||
onDeleteSession,
|
||||
workspaceActive,
|
||||
onWorkspaceClick,
|
||||
onNewSession,
|
||||
folded,
|
||||
className,
|
||||
}: NavListProps) {
|
||||
const { t } = useTranslation();
|
||||
const workspaceLabel = t('triggers.workspace');
|
||||
|
||||
const newSessionLabel = t('layout.sessions-start-new', {
|
||||
defaultValue: 'Start new session',
|
||||
defaultValue: 'New Session',
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -70,62 +60,16 @@ export function NavList({
|
|||
>
|
||||
<div className="min-w-0 gap-2 flex w-full flex-col">
|
||||
<NavTab
|
||||
layout="split"
|
||||
active={workspaceActive}
|
||||
onClick={onWorkspaceClick}
|
||||
leading={<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />}
|
||||
label={workspaceLabel}
|
||||
endAction={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
buttonContent="icon-only"
|
||||
className={cn(
|
||||
'no-drag mr-1 rounded-xl hover:bg-ds-bg-neutral-strong-default shrink-0',
|
||||
'focus-visible:ring-ds-border-neutral-default-default focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none'
|
||||
)}
|
||||
aria-label={newSessionLabel}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onNewSession();
|
||||
}}
|
||||
>
|
||||
<Plus
|
||||
className="h-4 w-4 text-ds-icon-neutral-muted-default"
|
||||
aria-hidden
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
tooltip={workspaceLabel}
|
||||
active={false}
|
||||
onClick={onNewSession}
|
||||
leading={<Plus className="h-4 w-4 shrink-0" aria-hidden />}
|
||||
label={newSessionLabel}
|
||||
tooltip={newSessionLabel}
|
||||
tooltipEnabledWhenCollapsed={!folded}
|
||||
folded={folded}
|
||||
ariaLabel={workspaceLabel}
|
||||
ariaCurrentPage={workspaceActive}
|
||||
ariaLabel={newSessionLabel}
|
||||
ariaCurrentPage={false}
|
||||
/>
|
||||
|
||||
{folded ? (
|
||||
<TooltipSimple
|
||||
content={newSessionLabel}
|
||||
side="right"
|
||||
align="center"
|
||||
enabled
|
||||
className={SIDEBAR_TOOLTIP_CONTENT_CLASS}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onNewSession}
|
||||
className={cn(workspaceTabButtonClass(false), 'gap-0 w-full')}
|
||||
aria-label={newSessionLabel}
|
||||
>
|
||||
<Plus
|
||||
className="h-4 w-4 text-ds-icon-neutral-muted-default shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
</button>
|
||||
</TooltipSimple>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -25,12 +25,11 @@ import { usePageTabStore } from '@/store/pageTabStore';
|
|||
import { useProjectStore } from '@/store/projectStore';
|
||||
import { useTriggerStore } from '@/store/triggerStore';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import { Inbox, Plus, Zap, ZapOff } from 'lucide-react';
|
||||
import { Inbox, LayoutGrid, Plus, Zap, ZapOff } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { HeaderAction } from './HeaderAction';
|
||||
import { NavList } from './NavList';
|
||||
import {
|
||||
NavTab,
|
||||
|
|
@ -59,6 +58,9 @@ export default function ProjectPageSidebar({
|
|||
}: ProjectPageSidebarProps) {
|
||||
const activeWorkspaceTab = usePageTabStore((s) => s.activeWorkspaceTab);
|
||||
const setActiveWorkspaceTab = usePageTabStore((s) => s.setActiveWorkspaceTab);
|
||||
const requestWorkspaceChatFocus = usePageTabStore(
|
||||
(s) => s.requestWorkspaceChatFocus
|
||||
);
|
||||
const requestOpenTriggerAddDialog = usePageTabStore(
|
||||
(s) => s.requestOpenTriggerAddDialog
|
||||
);
|
||||
|
|
@ -257,8 +259,9 @@ export default function ProjectPageSidebar({
|
|||
|
||||
const handleNewSession = useCallback(() => {
|
||||
chatStore.create();
|
||||
setActiveWorkspaceTab('session');
|
||||
}, [chatStore, setActiveWorkspaceTab]);
|
||||
setActiveWorkspaceTab('workforce');
|
||||
requestWorkspaceChatFocus();
|
||||
}, [chatStore, setActiveWorkspaceTab, requestWorkspaceChatFocus]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -276,9 +279,20 @@ export default function ProjectPageSidebar({
|
|||
<div className="min-h-0 min-w-0 flex h-full w-full max-w-full flex-col overflow-x-hidden">
|
||||
<div className="min-h-0 min-w-0 flex flex-1 flex-col overflow-hidden">
|
||||
<div className="gap-2 flex w-full shrink-0 flex-col">
|
||||
<HeaderAction />
|
||||
|
||||
<div className="gap-2 min-w-0 flex w-full flex-col">
|
||||
<NavTab
|
||||
active={activeWorkspaceTab === 'workforce'}
|
||||
onClick={() => setActiveWorkspaceTab('workforce')}
|
||||
leading={
|
||||
<LayoutGrid className="h-4 w-4 shrink-0" aria-hidden />
|
||||
}
|
||||
label="Cowork"
|
||||
tooltip="Cowork"
|
||||
tooltipEnabledWhenCollapsed={!projectSidebarFolded}
|
||||
folded={projectSidebarFolded}
|
||||
ariaLabel="Cowork"
|
||||
ariaCurrentPage={activeWorkspaceTab === 'workforce'}
|
||||
/>
|
||||
<NavTab
|
||||
active={activeWorkspaceTab === 'inbox'}
|
||||
onClick={() => {
|
||||
|
|
@ -403,8 +417,6 @@ export default function ProjectPageSidebar({
|
|||
setActiveWorkspaceTab('session');
|
||||
}}
|
||||
onDeleteSession={handleDeleteSession}
|
||||
workspaceActive={activeWorkspaceTab === 'workforce'}
|
||||
onWorkspaceClick={() => setActiveWorkspaceTab('workforce')}
|
||||
onNewSession={handleNewSession}
|
||||
folded={projectSidebarFolded}
|
||||
/>
|
||||
|
|
|
|||
83
src/components/Workspace/WorkspaceAllSessions.tsx
Normal file
83
src/components/Workspace/WorkspaceAllSessions.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import {
|
||||
NavListSessionRows,
|
||||
type NavListSession,
|
||||
} from '@/components/ProjectPageSidebar/NavList';
|
||||
import { taskIdToCreatedMs } from '@/lib/chatTaskIdTime';
|
||||
import { getSessionNavLeadPresentation } from '@/lib/sessionNavLead';
|
||||
import type { ChatStore } from '@/store/chatStore';
|
||||
import { ChatTaskStatus } from '@/types/constants';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface WorkspaceAllSessionsProps {
|
||||
tasks: ChatStore['tasks'];
|
||||
activeTaskId?: string | null;
|
||||
onSelectSession: (sessionId: string) => void;
|
||||
onDeleteSession: (sessionId: string) => void;
|
||||
}
|
||||
|
||||
export function WorkspaceAllSessions({
|
||||
tasks,
|
||||
activeTaskId,
|
||||
onSelectSession,
|
||||
onDeleteSession,
|
||||
}: WorkspaceAllSessionsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sessions: NavListSession[] = useMemo(() => {
|
||||
const entries = Object.entries(tasks)
|
||||
.filter(([, task]) => {
|
||||
return (
|
||||
(task.messages?.length || 0) > 0 ||
|
||||
task.hasMessages ||
|
||||
task.status !== ChatTaskStatus.PENDING
|
||||
);
|
||||
})
|
||||
.map(([id, task]) => ({
|
||||
id,
|
||||
title:
|
||||
task.summaryTask?.trim() ||
|
||||
t('layout.sessions-untitled', { defaultValue: 'Untitled session' }),
|
||||
sessionLead: getSessionNavLeadPresentation(task),
|
||||
}));
|
||||
entries.sort((a, b) => taskIdToCreatedMs(b.id) - taskIdToCreatedMs(a.id));
|
||||
return entries;
|
||||
}, [tasks, t]);
|
||||
|
||||
return (
|
||||
<div className="min-h-0 min-w-0 flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="m-0 min-h-0 gap-0.5 p-2 max-w-3xl mx-auto flex w-full flex-1 flex-col overflow-y-auto">
|
||||
{sessions.length === 0 ? (
|
||||
<p className="text-ds-text-neutral-muted-default m-0 px-3 py-6 text-body-sm text-center">
|
||||
{t('layout.sessions-create-task-hint', {
|
||||
defaultValue: 'Create a task to start a session',
|
||||
})}
|
||||
</p>
|
||||
) : (
|
||||
<NavListSessionRows
|
||||
sessions={sessions}
|
||||
activeSessionId={activeTaskId}
|
||||
onSessionClick={onSelectSession}
|
||||
onDeleteSession={onDeleteSession}
|
||||
folded={false}
|
||||
panelListHover
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
289
src/components/Workspace/WorkspaceCoworkPanel.tsx
Normal file
289
src/components/Workspace/WorkspaceCoworkPanel.tsx
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Check, PenLine } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ONBOARDING_KEY = 'eigent-workspace-onboarding-checked';
|
||||
|
||||
const ONBOARDING_STEPS = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Connect your everyday tools',
|
||||
subtitle:
|
||||
'The more Eigent understands your setup, the more useful it becomes.',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Build your workforce team',
|
||||
subtitle: 'Add workers to shape a team of agents for your projects.',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Ask Eigent to create something',
|
||||
subtitle:
|
||||
'Ask Eigent to create a spreadsheet, document, presentation, dashboard, or anything else you need.',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Schedule a recurring task',
|
||||
subtitle:
|
||||
'Turn repeat work into automatic workflows. Great for reminders, reports, check-ins, and regular updates.',
|
||||
},
|
||||
] as const;
|
||||
|
||||
function readCheckedSteps(): Set<number> {
|
||||
try {
|
||||
const v = localStorage.getItem(ONBOARDING_KEY);
|
||||
return v ? new Set(JSON.parse(v) as number[]) : new Set();
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
interface StepCardProps {
|
||||
id: number;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
checked: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function StepCard({ id, title, subtitle, checked, onClick }: StepCardProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'group rounded-xl p-2 gap-2 flex w-full items-start text-left transition-colors',
|
||||
checked
|
||||
? 'cursor-default'
|
||||
: 'hover:bg-ds-bg-neutral-strong-default cursor-pointer'
|
||||
)}
|
||||
onClick={checked ? undefined : onClick}
|
||||
aria-pressed={checked}
|
||||
>
|
||||
{/* Circle */}
|
||||
<div
|
||||
className={cn(
|
||||
'mt-0.5 h-4 w-4 flex shrink-0 items-center justify-center rounded-full',
|
||||
checked
|
||||
? 'bg-ds-bg-status-completed-subtle-default'
|
||||
: 'bg-ds-bg-neutral-muted-default'
|
||||
)}
|
||||
>
|
||||
{checked ? (
|
||||
<Check
|
||||
className="h-2.5 w-2.5 !text-ds-text-status-completed-strong-default"
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
<span className="font-bold text-ds-text-neutral-muted-default text-[8px] leading-none">
|
||||
{id}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div className="min-w-0 flex flex-1 flex-col">
|
||||
<span
|
||||
className={cn(
|
||||
'!text-body-sm font-semibold',
|
||||
checked
|
||||
? 'text-ds-text-neutral-muted-default'
|
||||
: 'text-ds-text-neutral-default-default'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
<span className="mt-1 text-label-xs text-ds-text-neutral-muted-default">
|
||||
{subtitle}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export interface WorkspaceCoworkPanelProps {
|
||||
memoryOn: boolean;
|
||||
onMemoryToggle: () => void;
|
||||
onEditInstructions: () => void;
|
||||
onWorkforceSetting: () => void;
|
||||
}
|
||||
|
||||
export function WorkspaceCoworkPanel({
|
||||
memoryOn,
|
||||
onMemoryToggle,
|
||||
onEditInstructions,
|
||||
onWorkforceSetting,
|
||||
}: WorkspaceCoworkPanelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [checkedSteps, setCheckedSteps] =
|
||||
useState<Set<number>>(readCheckedSteps);
|
||||
const [accordionOpen, setAccordionOpen] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const allChecked = checkedSteps.size >= ONBOARDING_STEPS.length;
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(ONBOARDING_KEY, JSON.stringify([...checkedSteps]));
|
||||
}, [checkedSteps]);
|
||||
|
||||
const handleCheckStep = (id: number) => {
|
||||
setCheckedSteps((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const instructionsHint = t('layout.instructions-rules-tone', {
|
||||
defaultValue: 'Rules & Tone',
|
||||
});
|
||||
const memoryLabel = t('layout.memory', { defaultValue: 'Memory' });
|
||||
const memoryOnLabel = t('layout.memory-on', { defaultValue: 'On' });
|
||||
const memoryOffLabel = t('layout.memory-off', { defaultValue: 'Off' });
|
||||
const workforceSettingLabel = t('layout.workforce-setting', {
|
||||
defaultValue: 'Workforce Setting',
|
||||
});
|
||||
const selectLabel = t('layout.select', { defaultValue: 'Select' });
|
||||
const editInstructionsLabel = t('layout.edit-instructions', {
|
||||
defaultValue: 'Edit instructions',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
{/* Settings area */}
|
||||
<div className="px-2 py-1 gap-0.5 rounded-2xl bg-ds-bg-neutral-default-default flex shrink-0 flex-col">
|
||||
{/* Panel title */}
|
||||
<div className="px-2 py-1.5 shrink-0">
|
||||
<span className="text-body-sm font-semibold text-ds-text-neutral-default-default">
|
||||
Welcome to Eigent
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 gap-2 rounded-lg px-2 py-1.5 hover:bg-ds-bg-neutral-strong-default flex items-center justify-between">
|
||||
<span className="min-w-0 text-body-sm font-medium text-ds-text-neutral-muted-default flex-1">
|
||||
{instructionsHint}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
buttonContent="icon-only"
|
||||
className="no-drag shrink-0"
|
||||
aria-label={editInstructionsLabel}
|
||||
onClick={onEditInstructions}
|
||||
>
|
||||
<PenLine className="h-4 w-4 shrink-0" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-w-0 gap-2 rounded-lg px-2 py-1.5 hover:bg-ds-bg-neutral-strong-default flex items-center justify-between">
|
||||
<span className="min-w-0 text-body-sm font-medium text-ds-text-neutral-muted-default">
|
||||
{memoryLabel}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
className="no-drag shrink-0"
|
||||
onClick={onMemoryToggle}
|
||||
aria-pressed={memoryOn}
|
||||
aria-label={`${memoryLabel}: ${memoryOn ? memoryOnLabel : memoryOffLabel}`}
|
||||
>
|
||||
{memoryOn ? memoryOnLabel : memoryOffLabel}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="min-w-0 gap-2 rounded-lg px-2 py-1.5 hover:bg-ds-bg-neutral-strong-default flex items-center justify-between">
|
||||
<span className="min-w-0 text-body-sm font-medium text-ds-text-neutral-muted-default">
|
||||
{workforceSettingLabel}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
className="no-drag shrink-0"
|
||||
onClick={onWorkforceSetting}
|
||||
aria-label={`${workforceSettingLabel}: ${selectLabel}`}
|
||||
>
|
||||
{selectLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable onboarding area */}
|
||||
<div className="min-h-0 py-2 flex flex-col">
|
||||
{allChecked ? (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
value={accordionOpen ?? ''}
|
||||
onValueChange={(v) => setAccordionOpen(v || undefined)}
|
||||
>
|
||||
<AccordionItem
|
||||
value="onboarding"
|
||||
className="rounded-xl data-[state=open]:bg-ds-bg-neutral-default-default overflow-hidden border-none"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-2.5 text-body-sm font-medium hover:bg-ds-bg-neutral-strong-default rounded-xl [&>svg]:text-ds-icon-neutral-muted-default hover:no-underline data-[state=open]:rounded-b-none">
|
||||
<div className="gap-2 min-w-0 flex items-center">
|
||||
<span className="text-ds-text-neutral-default-default truncate">
|
||||
Getting started
|
||||
</span>
|
||||
<span className="text-body-xs text-ds-text-neutral-muted-default shrink-0">
|
||||
{checkedSteps.size}/{ONBOARDING_STEPS.length}
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-2">
|
||||
<div className="gap-1 pb-1 flex flex-col">
|
||||
{ONBOARDING_STEPS.map((step) => (
|
||||
<StepCard
|
||||
key={step.id}
|
||||
id={step.id}
|
||||
title={step.title}
|
||||
subtitle={step.subtitle}
|
||||
checked={true}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
) : (
|
||||
<div className="gap-1 flex flex-col">
|
||||
{ONBOARDING_STEPS.map((step) => (
|
||||
<StepCard
|
||||
key={step.id}
|
||||
id={step.id}
|
||||
title={step.title}
|
||||
subtitle={step.subtitle}
|
||||
checked={checkedSteps.has(step.id)}
|
||||
onClick={() => handleCheckStep(step.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
191
src/components/Workspace/WorkspaceDispatch.tsx
Normal file
191
src/components/Workspace/WorkspaceDispatch.tsx
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import larkIcon from '@/assets/icon/lark.png';
|
||||
import telegramIcon from '@/assets/icon/telegram.svg';
|
||||
import whatsappIcon from '@/assets/icon/whatsapp.svg';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { WebSocketConnectionStatus } from '@/store/triggerStore';
|
||||
import { useTriggerStore } from '@/store/triggerStore';
|
||||
import { Copy, MonitorSmartphone } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
function statusDotClass(status: WebSocketConnectionStatus): string {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return 'bg-green-500';
|
||||
case 'connecting':
|
||||
return 'bg-yellow-400 animate-pulse';
|
||||
case 'unhealthy':
|
||||
case 'disconnected':
|
||||
default:
|
||||
return 'bg-red-500';
|
||||
}
|
||||
}
|
||||
|
||||
interface DispatchChannelCardProps {
|
||||
name: string;
|
||||
icon?: string;
|
||||
leading?: ReactNode;
|
||||
disabled?: boolean;
|
||||
connectionStatus?: WebSocketConnectionStatus;
|
||||
badgeText?: string;
|
||||
action?: {
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
function DispatchChannelCard({
|
||||
name,
|
||||
icon,
|
||||
leading,
|
||||
disabled,
|
||||
connectionStatus,
|
||||
badgeText,
|
||||
action,
|
||||
}: DispatchChannelCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'gap-3 p-4 border-ds-border-neutral-subtle-default rounded-2xl bg-ds-bg-neutral-default-default border',
|
||||
'min-h-40 flex flex-col justify-between',
|
||||
disabled
|
||||
? 'pointer-events-none cursor-not-allowed opacity-50 select-none'
|
||||
: '',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="gap-2 min-w-0 flex w-full items-center justify-between">
|
||||
<div className="gap-2 flex items-center">
|
||||
{leading}
|
||||
{icon && (
|
||||
<img
|
||||
src={icon}
|
||||
alt=""
|
||||
className="h-5 w-5 rounded shrink-0 object-contain"
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<span className="text-body-sm font-semibold text-ds-text-neutral-default-default truncate">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
{connectionStatus && (
|
||||
<span
|
||||
className={`h-2.5 w-2.5 shrink-0 rounded-full ${statusDotClass(connectionStatus)}`}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="gap-2 flex items-center justify-between">
|
||||
{badgeText ? (
|
||||
<Badge variant="secondary" size="xs">
|
||||
{badgeText}
|
||||
</Badge>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
{action && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-1.5 shrink-0"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WorkspaceDispatch() {
|
||||
const { t } = useTranslation();
|
||||
const wsConnectionStatus = useTriggerStore((s) => s.wsConnectionStatus);
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
toast.success(
|
||||
t('layout.dispatch-link-copied', { defaultValue: 'Link copied' })
|
||||
);
|
||||
} catch {
|
||||
toast.error(
|
||||
t('layout.dispatch-copy-failed', {
|
||||
defaultValue: 'Failed to copy link',
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex h-full w-full flex-col overflow-y-auto">
|
||||
<div className="gap-3 p-4 max-w-3xl mx-auto grid h-full w-full grid-cols-2 grid-rows-2">
|
||||
<DispatchChannelCard
|
||||
name={t('layout.workspace-work-with-remote-control', {
|
||||
defaultValue: 'Remote control',
|
||||
})}
|
||||
leading={
|
||||
<MonitorSmartphone
|
||||
className="h-4 w-4 text-ds-text-neutral-muted-default shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
}
|
||||
connectionStatus={wsConnectionStatus}
|
||||
action={{
|
||||
label: t('layout.dispatch-copy-link', {
|
||||
defaultValue: 'Copy link',
|
||||
}),
|
||||
icon: <Copy className="h-3.5 w-3.5" aria-hidden />,
|
||||
onClick: handleCopyLink,
|
||||
}}
|
||||
/>
|
||||
<DispatchChannelCard
|
||||
icon={telegramIcon}
|
||||
name={t('layout.channels-telegram', { defaultValue: 'Telegram' })}
|
||||
disabled
|
||||
badgeText={t('layout.dispatch-coming-soon', {
|
||||
defaultValue: 'Coming soon',
|
||||
})}
|
||||
/>
|
||||
<DispatchChannelCard
|
||||
icon={larkIcon}
|
||||
name={t('layout.channels-lark', { defaultValue: 'Lark' })}
|
||||
disabled
|
||||
badgeText={t('layout.dispatch-coming-soon', {
|
||||
defaultValue: 'Coming soon',
|
||||
})}
|
||||
/>
|
||||
<DispatchChannelCard
|
||||
icon={whatsappIcon}
|
||||
name={t('layout.channels-whatsapp', { defaultValue: 'WhatsApp' })}
|
||||
disabled
|
||||
badgeText={t('layout.dispatch-coming-soon', {
|
||||
defaultValue: 'Coming soon',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/components/Workspace/WorkspaceInstructionMd.tsx
Normal file
66
src/components/Workspace/WorkspaceInstructionMd.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface WorkspaceInstructionMdProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
function storageKey(projectId: string) {
|
||||
return `eigent-instructions-md-${projectId}`;
|
||||
}
|
||||
|
||||
export function WorkspaceInstructionMd({
|
||||
projectId,
|
||||
}: WorkspaceInstructionMdProps) {
|
||||
const { t } = useTranslation();
|
||||
const [content, setContent] = useState<string>(
|
||||
() => localStorage.getItem(storageKey(projectId)) ?? ''
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(storageKey(projectId), content);
|
||||
}, [content, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
const onSave = (e: Event) => {
|
||||
const custom = e as CustomEvent<{ projectId?: string }>;
|
||||
if (!custom.detail?.projectId || custom.detail.projectId !== projectId)
|
||||
return;
|
||||
localStorage.setItem(storageKey(projectId), content);
|
||||
};
|
||||
|
||||
window.addEventListener('workspace-instruction-md-save', onSave);
|
||||
return () => {
|
||||
window.removeEventListener('workspace-instruction-md-save', onSave);
|
||||
};
|
||||
}, [content, projectId]);
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex h-full w-full flex-col items-center justify-center overflow-hidden">
|
||||
<textarea
|
||||
className="min-h-0 p-6 font-mono text-body-sm text-ds-text-neutral-default-default placeholder:text-ds-text-neutral-muted-default max-w-3xl w-full flex-1 resize-none border-none bg-transparent outline-none"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder={t('layout.instruction-md-placeholder', {
|
||||
defaultValue:
|
||||
'Write your rules & tone instructions here in Markdown...',
|
||||
})}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,17 +23,23 @@ import { BASE_WORKFLOW_AGENTS } from '@/components/WorkFlow/baseWorkers';
|
|||
import { isBaseWorkflowAgent } from '@/components/Workspace/FoldedAgentCard';
|
||||
import { SingleAgentList } from '@/components/Workspace/SingleAgentList';
|
||||
import { WorkforceAgentList } from '@/components/Workspace/WorkforceAgentList';
|
||||
import { WorkspaceAllSessions } from '@/components/Workspace/WorkspaceAllSessions';
|
||||
import { WorkspaceCoworkPanel } from '@/components/Workspace/WorkspaceCoworkPanel';
|
||||
import { WorkspaceDispatch } from '@/components/Workspace/WorkspaceDispatch';
|
||||
import { WorkspaceExamplePrompts } from '@/components/Workspace/WorkspaceExamplePrompts';
|
||||
import { WorkspaceInstructionMd } from '@/components/Workspace/WorkspaceInstructionMd';
|
||||
import { WorkspaceProjectPicker } from '@/components/Workspace/WorkspaceProjectPicker';
|
||||
import { WorkspaceRecentSessions } from '@/components/Workspace/WorkspaceRecentSessions';
|
||||
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
|
||||
import { useModelConfigCheck } from '@/hooks/useModelConfigCheck';
|
||||
import { useHost } from '@/host';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAuthStore, useWorkerList } from '@/store/authStore';
|
||||
import { usePageTabStore } from '@/store/pageTabStore';
|
||||
import { useProjectStore } from '@/store/projectStore';
|
||||
import { SessionMode } from '@/types/constants';
|
||||
import { Cast, MonitorSmartphone } from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { ArrowLeft, Cast, MonitorSmartphone, ScrollText } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
|
@ -41,6 +47,15 @@ import { toast } from 'sonner';
|
|||
|
||||
const EMPTY_TASK_ASSIGNING: Agent[] = [];
|
||||
|
||||
const MEMORY_STORAGE_KEY = 'eigent-sidebar-instructions-memory-on';
|
||||
|
||||
function readMemoryInitial(): boolean {
|
||||
if (typeof window === 'undefined') return true;
|
||||
const v = window.localStorage.getItem(MEMORY_STORAGE_KEY);
|
||||
if (v === null) return true;
|
||||
return v === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace tab: project landing with a centered task input.
|
||||
* After the user starts a task, it switches to the session tab.
|
||||
|
|
@ -69,6 +84,10 @@ export default function Workspace() {
|
|||
return true;
|
||||
}, [activeProject, customAgentFolderPath, isEmptyProject]);
|
||||
const setActiveWorkspaceTab = usePageTabStore((s) => s.setActiveWorkspaceTab);
|
||||
const activeWorkspaceTab = usePageTabStore((s) => s.activeWorkspaceTab);
|
||||
const workspaceChatFocusRequestId = usePageTabStore(
|
||||
(s) => s.workspaceChatFocusRequestId
|
||||
);
|
||||
const sessionSidePanelMode = usePageTabStore(
|
||||
(s) => s.sessionSidePanelMode ?? SessionMode.WORKFORCE
|
||||
);
|
||||
|
|
@ -85,18 +104,47 @@ export default function Workspace() {
|
|||
const [editingWorkerAgent, setEditingWorkerAgent] = useState<Agent | null>(
|
||||
null
|
||||
);
|
||||
const [workspaceWorkWithPanelOpen, setWorkspaceWorkWithPanelOpen] =
|
||||
useState(false);
|
||||
const [leftPanelTab, setLeftPanelTab] = useState<
|
||||
'instructions' | 'workWith' | null
|
||||
>(null);
|
||||
type WorkspaceSubPage = 'all-sessions' | 'instruction-md' | 'dispatch' | null;
|
||||
const [workspaceSubPage, setWorkspaceSubPage] =
|
||||
useState<WorkspaceSubPage>(null);
|
||||
const SUB_PAGE_TITLES: Record<NonNullable<WorkspaceSubPage>, string> = {
|
||||
'all-sessions': t('layout.sessions-full-title', {
|
||||
defaultValue: 'All sessions',
|
||||
}),
|
||||
'instruction-md': t('layout.instructions-rules-tone', {
|
||||
defaultValue: 'Rules & Tone',
|
||||
}),
|
||||
dispatch: t('layout.workspace-work-with-title', {
|
||||
defaultValue: 'Dispatch',
|
||||
}),
|
||||
};
|
||||
const [memoryOn, setMemoryOn] = useState(readMemoryInitial);
|
||||
const textareaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceWorkWithPanelOpen) return;
|
||||
window.localStorage.setItem(MEMORY_STORAGE_KEY, String(memoryOn));
|
||||
}, [memoryOn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceChatFocusRequestId === 0) return;
|
||||
if (activeWorkspaceTab !== 'workforce') return;
|
||||
const focusTimer = window.setTimeout(() => {
|
||||
textareaRef.current?.focus();
|
||||
}, 180);
|
||||
return () => window.clearTimeout(focusTimer);
|
||||
}, [workspaceChatFocusRequestId, activeWorkspaceTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!leftPanelTab) return;
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setWorkspaceWorkWithPanelOpen(false);
|
||||
if (e.key === 'Escape') setLeftPanelTab(null);
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [workspaceWorkWithPanelOpen]);
|
||||
}, [leftPanelTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
|
@ -264,257 +312,404 @@ export default function Workspace() {
|
|||
|
||||
const activeAgentId = chatStore.tasks[chatStore.activeTaskId]?.activeAgent;
|
||||
|
||||
const workWithPanelToggleLabel = workspaceWorkWithPanelOpen
|
||||
? t('layout.workspace-work-with-panel-hide', {
|
||||
defaultValue: 'Hide Work with panel',
|
||||
})
|
||||
: t('layout.workspace-work-with-panel-show', {
|
||||
defaultValue: 'Show Work with panel',
|
||||
});
|
||||
const instructionsLabel = t('layout.instructions', {
|
||||
defaultValue: 'Instructions',
|
||||
});
|
||||
const workWithLabel = t('layout.workspace-work-with-title', {
|
||||
defaultValue: 'Work with',
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-0 relative flex h-full w-full flex-col">
|
||||
<div className="px-3 relative z-50 flex h-[44px] w-full shrink-0 flex-row items-center justify-start">
|
||||
<TooltipSimple content={workWithPanelToggleLabel} delayDuration={300}>
|
||||
<div className="min-h-0 flex h-full w-full flex-col">
|
||||
{/* Header toolbar */}
|
||||
<div className="px-3 gap-1 relative flex h-[44px] w-full shrink-0 flex-row items-center justify-start">
|
||||
{workspaceSubPage !== null && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
onClick={() => setWorkspaceSubPage(null)}
|
||||
className="no-drag shrink-0"
|
||||
aria-label={t('layout.back-to-workspace-tooltip', {
|
||||
defaultValue: 'Back to workspace',
|
||||
})}
|
||||
>
|
||||
<ArrowLeft aria-hidden />
|
||||
Back
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{workspaceSubPage !== null && (
|
||||
<div className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<span className="!text-label-sm font-semibold text-ds-text-neutral-default-default block max-w-[60vw] truncate text-center">
|
||||
{SUB_PAGE_TITLES[workspaceSubPage]}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{workspaceSubPage === 'instruction-md' && activeProjectId && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
buttonContent="icon-only"
|
||||
onClick={() => setWorkspaceWorkWithPanelOpen((open) => !open)}
|
||||
aria-expanded={workspaceWorkWithPanelOpen}
|
||||
aria-controls="workspace-work-with-panel"
|
||||
className="no-drag text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-strong-default shrink-0"
|
||||
aria-label={workWithPanelToggleLabel}
|
||||
buttonContent="text"
|
||||
className="no-drag shrink-0"
|
||||
onClick={() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('workspace-instruction-md-save', {
|
||||
detail: { projectId: activeProjectId },
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Cast className="h-4 w-4" aria-hidden />
|
||||
Save
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
<div className="min-h-0 relative z-0 flex w-full flex-1 flex-col items-stretch overflow-hidden">
|
||||
<div className="min-h-0 px-3 flex w-full flex-1 flex-col">
|
||||
<div className="mx-auto flex w-full max-w-[600px] shrink-0 flex-col">
|
||||
<div className="min-w-0 flex min-h-[50vh] w-full flex-col justify-end">
|
||||
<div className="mb-8 flex w-full justify-center">
|
||||
<WorkspaceProjectPicker />
|
||||
</div>
|
||||
<span className="mb-8 text-heading-lg font-bold text-ds-text-neutral-default-default w-full text-center">
|
||||
{sessionSidePanelMode === SessionMode.SINGLE_AGENT
|
||||
? t('layout.workspace-cowork-single-agent', {
|
||||
defaultValue: 'Cowork with Single Agent',
|
||||
})
|
||||
: t('layout.workspace-cowork-workforce', {
|
||||
defaultValue: 'Cowork with Workforce',
|
||||
})}
|
||||
</span>
|
||||
<div className="mb-8 px-5 flex w-full justify-center">
|
||||
{sessionSidePanelMode === SessionMode.SINGLE_AGENT ? (
|
||||
<SingleAgentList />
|
||||
) : (
|
||||
<WorkforceAgentList
|
||||
sortedAgents={sortedAgents}
|
||||
activeAgentId={activeAgentId}
|
||||
onSelectAgent={onSelectAgent}
|
||||
onEditWorkerFromMenu={onEditWorkerFromMenu}
|
||||
onDuplicateUserAgent={onDuplicateUserAgent}
|
||||
onDeleteUserAgent={onDeleteUserAgent}
|
||||
onAddWorker={() => setAddWorkerDialogOpen(true)}
|
||||
/>
|
||||
)}
|
||||
{workspaceSubPage === null && (
|
||||
<>
|
||||
<TooltipSimple content={instructionsLabel} delayDuration={300}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
buttonContent="icon-only"
|
||||
onClick={() =>
|
||||
setLeftPanelTab((prev) =>
|
||||
prev === 'instructions' ? null : 'instructions'
|
||||
)
|
||||
}
|
||||
aria-expanded={leftPanelTab === 'instructions'}
|
||||
aria-controls="workspace-right-panel"
|
||||
className={cn(
|
||||
'no-drag shrink-0',
|
||||
leftPanelTab === 'instructions'
|
||||
? 'bg-ds-bg-neutral-strong-default'
|
||||
: 'hover:bg-ds-bg-neutral-strong-default'
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<BottomBox
|
||||
state="input"
|
||||
queuedMessages={[]}
|
||||
onRemoveQueuedMessage={() => {}}
|
||||
noModelOverlay={!hasModel}
|
||||
onSelectModel={() => navigate('/history?tab=agents')}
|
||||
inputProps={{
|
||||
value: message,
|
||||
onChange: setMessage,
|
||||
onSend: handleSend,
|
||||
files:
|
||||
chatStore.tasks[chatStore.activeTaskId]?.attaches?.map(
|
||||
(f) => ({
|
||||
fileName: f.fileName,
|
||||
filePath: f.filePath,
|
||||
})
|
||||
) || [],
|
||||
onFilesChange: (files) =>
|
||||
chatStore.setAttaches(
|
||||
chatStore.activeTaskId as string,
|
||||
files as any
|
||||
),
|
||||
onAddFile: handleFileSelect,
|
||||
disabled: !hasModel,
|
||||
textareaRef,
|
||||
allowDragDrop: true,
|
||||
useCloudModelInDev,
|
||||
placeholder: t('layout.project-task-placeholder', {
|
||||
defaultValue: 'Describe what you want to accomplish...',
|
||||
}),
|
||||
sessionMode: sessionSidePanelMode,
|
||||
onSessionModeChange: setSessionSidePanelMode,
|
||||
sessionModeSelectInteractive: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AddWorker
|
||||
isOpen={addWorkerDialogOpen}
|
||||
onOpenChange={setAddWorkerDialogOpen}
|
||||
/>
|
||||
{editingWorkerAgent && (
|
||||
<AddWorker
|
||||
edit
|
||||
workerInfo={editingWorkerAgent}
|
||||
isOpen={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setEditingWorkerAgent(null);
|
||||
}}
|
||||
/>
|
||||
aria-label={instructionsLabel}
|
||||
>
|
||||
<ScrollText aria-hidden />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
tone="default"
|
||||
textWeight="medium"
|
||||
onClick={() =>
|
||||
setLeftPanelTab((prev) =>
|
||||
prev === 'workWith' ? null : 'workWith'
|
||||
)
|
||||
}
|
||||
aria-expanded={leftPanelTab === 'workWith'}
|
||||
aria-controls="workspace-right-panel"
|
||||
className={cn(
|
||||
'no-drag !text-label-sm gap-2 shrink-0',
|
||||
leftPanelTab === 'workWith'
|
||||
? 'bg-ds-bg-neutral-subtle-default'
|
||||
: 'hover:bg-ds-bg-neutral-subtle-hover'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="min-h-0 pt-6 flex w-full flex-1 flex-col overflow-y-auto"
|
||||
id="workspace-bottom-group"
|
||||
>
|
||||
{showWorkspaceExamplePrompts ? (
|
||||
<WorkspaceExamplePrompts
|
||||
onSelectPrompt={setMessage}
|
||||
disabled={!hasModel}
|
||||
/>
|
||||
) : (
|
||||
<WorkspaceRecentSessions
|
||||
tasks={chatStore.tasks}
|
||||
activeTaskId={chatStore.activeTaskId}
|
||||
onSelectSession={(id) => {
|
||||
chatStore.setActiveTaskId(id);
|
||||
setActiveWorkspaceTab('session');
|
||||
}}
|
||||
onOpenAllSessions={() => setActiveWorkspaceTab('sessions')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
aria-label="Dispatch"
|
||||
>
|
||||
<Cast aria-hidden />
|
||||
Dispatch
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{workspaceWorkWithPanelOpen ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="inset-0 absolute z-40 cursor-default bg-transparent backdrop-blur-[1px]"
|
||||
aria-label={t('layout.workspace-work-with-dismiss-overlay', {
|
||||
defaultValue: 'Dismiss',
|
||||
})}
|
||||
onClick={() => setWorkspaceWorkWithPanelOpen(false)}
|
||||
{/* Body: main content + right panel */}
|
||||
<div className="min-h-0 flex flex-1 flex-row overflow-hidden">
|
||||
{/* Sub-pages */}
|
||||
{workspaceSubPage === 'all-sessions' && (
|
||||
<WorkspaceAllSessions
|
||||
tasks={chatStore.tasks}
|
||||
activeTaskId={chatStore.activeTaskId}
|
||||
onSelectSession={(id) => {
|
||||
chatStore.setActiveTaskId(id);
|
||||
setActiveWorkspaceTab('session');
|
||||
setWorkspaceSubPage(null);
|
||||
}}
|
||||
onDeleteSession={(id) => {
|
||||
if (!window.confirm(t('layout.delete-task-confirmation'))) return;
|
||||
const wasActive = chatStore.activeTaskId === id;
|
||||
chatStore.removeTask(id);
|
||||
if (wasActive) setActiveWorkspaceTab('workforce');
|
||||
setWorkspaceSubPage(null);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
id="workspace-work-with-panel"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="workspace-work-with-heading"
|
||||
className="left-0 top-8 ease-out animate-in fade-in-0 slide-in-from-left-2 absolute z-50 flex max-h-[calc(100%-2.75rem)] w-[300px] flex-col overflow-y-auto duration-200"
|
||||
>
|
||||
<div className="gap-3 p-3 flex flex-col">
|
||||
<div className="min-w-0 rounded-xl border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default p-3 flex flex-col border border-solid">
|
||||
<span
|
||||
id="workspace-work-with-heading"
|
||||
className="text-body-sm font-semibold text-ds-text-neutral-default-default"
|
||||
)}
|
||||
{workspaceSubPage === 'instruction-md' && activeProjectId && (
|
||||
<WorkspaceInstructionMd
|
||||
key={activeProjectId}
|
||||
projectId={activeProjectId}
|
||||
/>
|
||||
)}
|
||||
{workspaceSubPage === 'dispatch' && <WorkspaceDispatch />}
|
||||
|
||||
{/* Main content + right panel (hidden when sub-page is active) */}
|
||||
{workspaceSubPage === null && (
|
||||
<>
|
||||
<div className="min-h-0 min-w-0 relative z-0 flex flex-1 flex-col items-stretch overflow-hidden">
|
||||
<div className="min-h-0 px-3 flex w-full flex-1 flex-col">
|
||||
<div className="mx-auto flex w-full max-w-[600px] shrink-0 flex-col">
|
||||
<div className="min-w-0 flex min-h-[50vh] w-full flex-col justify-end">
|
||||
<div className="mb-8 flex w-full justify-center">
|
||||
<WorkspaceProjectPicker />
|
||||
</div>
|
||||
<span className="mb-8 text-heading-lg font-bold text-ds-text-neutral-default-default w-full text-center">
|
||||
{sessionSidePanelMode === SessionMode.SINGLE_AGENT
|
||||
? t('layout.workspace-cowork-single-agent', {
|
||||
defaultValue: 'Cowork with Single Agent',
|
||||
})
|
||||
: t('layout.workspace-cowork-workforce', {
|
||||
defaultValue: 'Cowork with Workforce',
|
||||
})}
|
||||
</span>
|
||||
<div className="mb-8 px-5 flex w-full justify-center">
|
||||
{sessionSidePanelMode === SessionMode.SINGLE_AGENT ? (
|
||||
<SingleAgentList />
|
||||
) : (
|
||||
<WorkforceAgentList
|
||||
sortedAgents={sortedAgents}
|
||||
activeAgentId={activeAgentId}
|
||||
onSelectAgent={onSelectAgent}
|
||||
onEditWorkerFromMenu={onEditWorkerFromMenu}
|
||||
onDuplicateUserAgent={onDuplicateUserAgent}
|
||||
onDeleteUserAgent={onDeleteUserAgent}
|
||||
onAddWorker={() => setAddWorkerDialogOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<BottomBox
|
||||
state="input"
|
||||
queuedMessages={[]}
|
||||
onRemoveQueuedMessage={() => {}}
|
||||
noModelOverlay={!hasModel}
|
||||
onSelectModel={() => navigate('/history?tab=agents')}
|
||||
inputProps={{
|
||||
value: message,
|
||||
onChange: setMessage,
|
||||
onSend: handleSend,
|
||||
files:
|
||||
chatStore.tasks[
|
||||
chatStore.activeTaskId
|
||||
]?.attaches?.map((f) => ({
|
||||
fileName: f.fileName,
|
||||
filePath: f.filePath,
|
||||
})) || [],
|
||||
onFilesChange: (files) =>
|
||||
chatStore.setAttaches(
|
||||
chatStore.activeTaskId as string,
|
||||
files as any
|
||||
),
|
||||
onAddFile: handleFileSelect,
|
||||
disabled: !hasModel,
|
||||
textareaRef,
|
||||
allowDragDrop: true,
|
||||
useCloudModelInDev,
|
||||
placeholder: t('layout.project-task-placeholder', {
|
||||
defaultValue:
|
||||
'Describe what you want to accomplish...',
|
||||
}),
|
||||
sessionMode: sessionSidePanelMode,
|
||||
onSessionModeChange: setSessionSidePanelMode,
|
||||
sessionModeSelectInteractive: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<AddWorker
|
||||
isOpen={addWorkerDialogOpen}
|
||||
onOpenChange={setAddWorkerDialogOpen}
|
||||
/>
|
||||
{editingWorkerAgent && (
|
||||
<AddWorker
|
||||
edit
|
||||
workerInfo={editingWorkerAgent}
|
||||
isOpen={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setEditingWorkerAgent(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="min-h-0 pt-6 flex w-full flex-1 flex-col overflow-y-auto"
|
||||
id="workspace-bottom-group"
|
||||
>
|
||||
{t('layout.workspace-work-with-title', {
|
||||
defaultValue: 'Work with',
|
||||
})}
|
||||
</span>
|
||||
<div className="mt-3 gap-1 flex flex-col">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
>
|
||||
<MonitorSmartphone
|
||||
className="h-4 w-4 text-ds-text-neutral-muted-default shrink-0"
|
||||
aria-hidden
|
||||
{showWorkspaceExamplePrompts ? (
|
||||
<WorkspaceExamplePrompts
|
||||
onSelectPrompt={setMessage}
|
||||
disabled={!hasModel}
|
||||
/>
|
||||
{t('layout.workspace-work-with-remote-control', {
|
||||
defaultValue: 'Remote control',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
aria-label={t('layout.channels-telegram', {
|
||||
defaultValue: 'Telegram',
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={telegramIcon}
|
||||
alt=""
|
||||
className="h-4 w-4 shrink-0 object-contain"
|
||||
aria-hidden
|
||||
) : (
|
||||
<WorkspaceRecentSessions
|
||||
tasks={chatStore.tasks}
|
||||
activeTaskId={chatStore.activeTaskId}
|
||||
onSelectSession={(id) => {
|
||||
chatStore.setActiveTaskId(id);
|
||||
setActiveWorkspaceTab('session');
|
||||
}}
|
||||
onOpenAllSessions={() =>
|
||||
setWorkspaceSubPage('all-sessions')
|
||||
}
|
||||
/>
|
||||
{t('layout.channels-telegram', {
|
||||
defaultValue: 'Telegram',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
aria-label={t('layout.channels-lark', {
|
||||
defaultValue: 'Lark',
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={larkIcon}
|
||||
alt=""
|
||||
className="h-4 w-4 rounded-lg shrink-0 object-contain"
|
||||
aria-hidden
|
||||
/>
|
||||
{t('layout.channels-lark', { defaultValue: 'Lark' })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
aria-label={t('layout.channels-whatsapp', {
|
||||
defaultValue: 'WhatsApp',
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={whatsappIcon}
|
||||
alt=""
|
||||
className="h-4 w-4 shrink-0 object-contain"
|
||||
aria-hidden
|
||||
/>
|
||||
{t('layout.channels-whatsapp', {
|
||||
defaultValue: 'WhatsApp',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Right slide-in panel */}
|
||||
<AnimatePresence initial={false}>
|
||||
{leftPanelTab !== null && (
|
||||
<motion.div
|
||||
key="workspace-right-panel"
|
||||
id="workspace-right-panel"
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: 288 }}
|
||||
exit={{ width: 0 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
className="pr-1 z-0 shrink-0 overflow-hidden"
|
||||
>
|
||||
<div className="flex h-full w-[280px] flex-col overflow-hidden">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={leftPanelTab}
|
||||
initial={{ x: 12, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -12, opacity: 0 }}
|
||||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
||||
className="flex h-full flex-col"
|
||||
>
|
||||
{leftPanelTab === 'instructions' ? (
|
||||
<WorkspaceCoworkPanel
|
||||
memoryOn={memoryOn}
|
||||
onMemoryToggle={() => setMemoryOn((v) => !v)}
|
||||
onEditInstructions={() => {
|
||||
setLeftPanelTab(null);
|
||||
setWorkspaceSubPage('instruction-md');
|
||||
}}
|
||||
onWorkforceSetting={() =>
|
||||
setActiveWorkspaceTab('workforce')
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="px-2 py-1 rounded-2xl bg-ds-bg-neutral-default-default flex shrink-0 flex-col">
|
||||
<span className="px-2 py-1.5 text-body-sm font-semibold text-ds-text-neutral-default-default">
|
||||
{workWithLabel}
|
||||
</span>
|
||||
<div className="gap-2 py-1.5 flex flex-col">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
onClick={() => {
|
||||
setLeftPanelTab(null);
|
||||
setWorkspaceSubPage('dispatch');
|
||||
}}
|
||||
>
|
||||
<MonitorSmartphone
|
||||
className="h-4 w-4 text-ds-text-neutral-muted-default shrink-0"
|
||||
aria-hidden
|
||||
/>
|
||||
{t(
|
||||
'layout.workspace-work-with-remote-control',
|
||||
{
|
||||
defaultValue: 'Remote control',
|
||||
}
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
aria-label={t('layout.channels-telegram', {
|
||||
defaultValue: 'Telegram',
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={telegramIcon}
|
||||
alt=""
|
||||
className="h-4 w-4 shrink-0 object-contain"
|
||||
aria-hidden
|
||||
/>
|
||||
{t('layout.channels-telegram', {
|
||||
defaultValue: 'Telegram',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
aria-label={t('layout.channels-lark', {
|
||||
defaultValue: 'Lark',
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={larkIcon}
|
||||
alt=""
|
||||
className="h-4 w-4 rounded-lg shrink-0 object-contain"
|
||||
aria-hidden
|
||||
/>
|
||||
{t('layout.channels-lark', {
|
||||
defaultValue: 'Lark',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
tone="default"
|
||||
emphasis="default"
|
||||
size="sm"
|
||||
buttonContent="text"
|
||||
className="no-drag gap-2 justify-start"
|
||||
aria-label={t('layout.channels-whatsapp', {
|
||||
defaultValue: 'WhatsApp',
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={whatsappIcon}
|
||||
alt=""
|
||||
className="h-4 w-4 shrink-0 object-contain"
|
||||
aria-hidden
|
||||
/>
|
||||
{t('layout.channels-whatsapp', {
|
||||
defaultValue: 'WhatsApp',
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ import type {
|
|||
const HOME_MAIN_LAYOUT_SPRING = PROJECT_SIDEBAR_FOLD_SPRING;
|
||||
|
||||
/** Sidebar width bounds (react-resizable-panels uses %; derived from shell width). */
|
||||
const SIDEBAR_MIN_PX = 240;
|
||||
const SIDEBAR_MAX_PX = 400;
|
||||
const SIDEBAR_MIN_PX = 230;
|
||||
const SIDEBAR_MAX_PX = 320;
|
||||
/** Default expanded sidebar width when nothing is stored (px). */
|
||||
const DEFAULT_SIDEBAR_WIDTH_PX = 288;
|
||||
const DEFAULT_SIDEBAR_WIDTH_PX = 230;
|
||||
const SIDEBAR_WIDTH_STORAGE_KEY = 'eigent-home-sidebar-width-px';
|
||||
|
||||
function clampPct(n: number): number {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue