From 2e712e013f4db71464781ffac307e9acccf04653 Mon Sep 17 00:00:00 2001 From: Douglas Date: Thu, 7 May 2026 16:22:16 +0100 Subject: [PATCH 1/2] update workspace page structure with subpage content --- .../ChatBox/MessageItem/UserMessageCard.tsx | 2 +- .../ProjectPageSidebar/HeaderAction.tsx | 243 ------- src/components/ProjectPageSidebar/NavList.tsx | 78 +- src/components/ProjectPageSidebar/index.tsx | 28 +- .../Workspace/WorkspaceAllSessions.tsx | 83 +++ .../Workspace/WorkspaceCoworkPanel.tsx | 289 ++++++++ .../Workspace/WorkspaceDispatch.tsx | 191 +++++ .../Workspace/WorkspaceInstructionMd.tsx | 66 ++ src/components/Workspace/index.tsx | 673 +++++++++++------- src/pages/Home.tsx | 6 +- 10 files changed, 1098 insertions(+), 561 deletions(-) delete mode 100644 src/components/ProjectPageSidebar/HeaderAction.tsx create mode 100644 src/components/Workspace/WorkspaceAllSessions.tsx create mode 100644 src/components/Workspace/WorkspaceCoworkPanel.tsx create mode 100644 src/components/Workspace/WorkspaceDispatch.tsx create mode 100644 src/components/Workspace/WorkspaceInstructionMd.tsx diff --git a/src/components/ChatBox/MessageItem/UserMessageCard.tsx b/src/components/ChatBox/MessageItem/UserMessageCard.tsx index 438e4d211..ca86b2349 100644 --- a/src/components/ChatBox/MessageItem/UserMessageCard.tsx +++ b/src/components/ChatBox/MessageItem/UserMessageCard.tsx @@ -263,7 +263,7 @@ export function UserMessageCard({ {canClamp && !expanded && (
diff --git a/src/components/ProjectPageSidebar/HeaderAction.tsx b/src/components/ProjectPageSidebar/HeaderAction.tsx deleted file mode 100644 index 964631d3b..000000000 --- a/src/components/ProjectPageSidebar/HeaderAction.tsx +++ /dev/null @@ -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( - 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 ( -
-
- { - if (projectSidebarFolded) return; - const next = v || undefined; - setInstructionsOpen(next); - }} - className="w-full" - > - - svg:last-child]:hidden' - )} - title={ - projectSidebarFolded ? String(instructionsLabel) : undefined - } - onClick={(e) => { - if (!projectSidebarFolded) return; - openInstructionsAfterExpandRef.current = true; - setProjectSidebarFolded(false); - e.preventDefault(); - }} - > - - - - {instructionsLabel} - - - - -
-
- - {instructionsHint} - - -
-
- - {memoryLabel} - - -
-
- - {workforceSettingLabel} - - -
-
-
-
-
-
-
- ); -} diff --git a/src/components/ProjectPageSidebar/NavList.tsx b/src/components/ProjectPageSidebar/NavList.tsx index 54f7ff852..56c1ea976 100644 --- a/src/components/ProjectPageSidebar/NavList.tsx +++ b/src/components/ProjectPageSidebar/NavList.tsx @@ -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({ >
} - label={workspaceLabel} - endAction={ - - } - tooltip={workspaceLabel} + active={false} + onClick={onNewSession} + leading={} + label={newSessionLabel} + tooltip={newSessionLabel} tooltipEnabledWhenCollapsed={!folded} folded={folded} - ariaLabel={workspaceLabel} - ariaCurrentPage={workspaceActive} + ariaLabel={newSessionLabel} + ariaCurrentPage={false} /> - - {folded ? ( - - - - ) : null}
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({
- -
+ setActiveWorkspaceTab('workforce')} + leading={ + + } + label="Cowork" + tooltip="Cowork" + tooltipEnabledWhenCollapsed={!projectSidebarFolded} + folded={projectSidebarFolded} + ariaLabel="Cowork" + ariaCurrentPage={activeWorkspaceTab === 'workforce'} + /> { @@ -403,8 +417,6 @@ export default function ProjectPageSidebar({ setActiveWorkspaceTab('session'); }} onDeleteSession={handleDeleteSession} - workspaceActive={activeWorkspaceTab === 'workforce'} - onWorkspaceClick={() => setActiveWorkspaceTab('workforce')} onNewSession={handleNewSession} folded={projectSidebarFolded} /> diff --git a/src/components/Workspace/WorkspaceAllSessions.tsx b/src/components/Workspace/WorkspaceAllSessions.tsx new file mode 100644 index 000000000..4b117aa8a --- /dev/null +++ b/src/components/Workspace/WorkspaceAllSessions.tsx @@ -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 ( +
+
+ {sessions.length === 0 ? ( +

+ {t('layout.sessions-create-task-hint', { + defaultValue: 'Create a task to start a session', + })} +

+ ) : ( + + )} +
+
+ ); +} diff --git a/src/components/Workspace/WorkspaceCoworkPanel.tsx b/src/components/Workspace/WorkspaceCoworkPanel.tsx new file mode 100644 index 000000000..de724d285 --- /dev/null +++ b/src/components/Workspace/WorkspaceCoworkPanel.tsx @@ -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 { + 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 ( + + ); +} + +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>(readCheckedSteps); + const [accordionOpen, setAccordionOpen] = useState( + 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 ( +
+ {/* Settings area */} +
+ {/* Panel title */} +
+ + Welcome to Eigent + +
+
+ + {instructionsHint} + + +
+
+ + {memoryLabel} + + +
+
+ + {workforceSettingLabel} + + +
+
+ + {/* Scrollable onboarding area */} +
+ {allChecked ? ( + setAccordionOpen(v || undefined)} + > + + +
+ + Getting started + + + {checkedSteps.size}/{ONBOARDING_STEPS.length} + +
+
+ +
+ {ONBOARDING_STEPS.map((step) => ( + {}} + /> + ))} +
+
+
+
+ ) : ( +
+ {ONBOARDING_STEPS.map((step) => ( + handleCheckStep(step.id)} + /> + ))} +
+ )} +
+
+ ); +} diff --git a/src/components/Workspace/WorkspaceDispatch.tsx b/src/components/Workspace/WorkspaceDispatch.tsx new file mode 100644 index 000000000..acf9c92f3 --- /dev/null +++ b/src/components/Workspace/WorkspaceDispatch.tsx @@ -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 ( +
+
+
+ {leading} + {icon && ( + + )} + + {name} + +
+ {connectionStatus && ( + + )} +
+ +
+ {badgeText ? ( + + {badgeText} + + ) : ( +
+ )} + + {action && ( + + )} +
+
+ ); +} + +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 ( +
+
+ + } + connectionStatus={wsConnectionStatus} + action={{ + label: t('layout.dispatch-copy-link', { + defaultValue: 'Copy link', + }), + icon: , + onClick: handleCopyLink, + }} + /> + + + +
+
+ ); +} diff --git a/src/components/Workspace/WorkspaceInstructionMd.tsx b/src/components/Workspace/WorkspaceInstructionMd.tsx new file mode 100644 index 000000000..ccafa1532 --- /dev/null +++ b/src/components/Workspace/WorkspaceInstructionMd.tsx @@ -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( + () => 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 ( +
+