Workspaces
This commit is contained in:
@@ -53,6 +53,8 @@ export function App() {
|
||||
<Route path="/tasks" element={<Dashboard.Tasks />} />
|
||||
<Route path="/processes" element={<Dashboard.Processes />} />
|
||||
<Route path="/task-logs" element={<Dashboard.TaskLogs />} />
|
||||
<Route path="/workspaces" element={<Dashboard.WorkspaceListScreen />} />
|
||||
<Route path="/workspaces/:id" element={<Dashboard.WorkspaceScreen />} />
|
||||
<Route path="/auth/signout" element={<Authentication.SignoutScreen />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -1,26 +1,52 @@
|
||||
import { Workspace, createWorkspaceDefaults } from '@/components/Workspace';
|
||||
import { useUserState } from '@/state/useUserState';
|
||||
import { ChatLauncher } from './ChatLauncher';
|
||||
import { FileBrowserWidget as FileBrowser } from '@/Screens/Dashboard/Files';
|
||||
import { ChatHistoryWidget as ChatHistory } from '@/Screens/Dashboard/ChatHistory';
|
||||
import { Catalog } from 'sounds';
|
||||
import { WorkspaceView } from '@/components/Workspace';
|
||||
import type { LayoutNode } from '@/components/Workspace';
|
||||
import { widgetRegistry } from '@/Screens/Dashboard/Workspaces/widget-registry';
|
||||
|
||||
const WIDGET_IDS = ['chat-launcher', 'file-browser', 'chat-history', 'sound-library'] as const;
|
||||
const DEFAULTS = createWorkspaceDefaults([...WIDGET_IDS]);
|
||||
const DEFAULT_HOME_LAYOUT: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'home-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{
|
||||
size: 50,
|
||||
node: {
|
||||
type: 'group',
|
||||
id: 'home-left',
|
||||
direction: 'vertical',
|
||||
children: [
|
||||
{ size: 50, node: { type: 'panel', id: 'home-tl', widgetType: 'chat-launcher' } },
|
||||
{ size: 50, node: { type: 'panel', id: 'home-bl', widgetType: 'file-browser' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
size: 50,
|
||||
node: {
|
||||
type: 'group',
|
||||
id: 'home-right',
|
||||
direction: 'vertical',
|
||||
children: [
|
||||
{ size: 50, node: { type: 'panel', id: 'home-tr', widgetType: 'chat-history' } },
|
||||
{ size: 50, node: { type: 'panel', id: 'home-br', widgetType: 'sound-library' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeScreen = () => {
|
||||
const [state, setState] = useUserState('home-layout', DEFAULTS);
|
||||
const [layout, setLayout] = useUserState<LayoutNode>('home-workspace-layout', DEFAULT_HOME_LAYOUT);
|
||||
|
||||
return (
|
||||
<Workspace
|
||||
widgets={{
|
||||
'chat-launcher': <ChatLauncher />,
|
||||
'file-browser': <FileBrowser />,
|
||||
'chat-history': <ChatHistory />,
|
||||
'sound-library': <Catalog />,
|
||||
}}
|
||||
state={state}
|
||||
onChange={setState}
|
||||
/>
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView
|
||||
workspace={null}
|
||||
name="Home"
|
||||
layout={layout}
|
||||
onLayoutChange={setLayout}
|
||||
registry={widgetRegistry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -91,7 +91,7 @@ export const Dock = ({ items, className }: DockProps) => {
|
||||
};
|
||||
|
||||
|
||||
import { Terminal, TerminalSquare, FileText, FolderOpen, Code } from 'lucide-react';
|
||||
import { Terminal, TerminalSquare, FileText, FolderOpen, Code, LayoutGrid } from 'lucide-react';
|
||||
import { Sparkles, ClipboardList, ScrollText, Workflow } from 'lucide-react';
|
||||
|
||||
export const dockItems: DockItem[] = [
|
||||
@@ -104,4 +104,5 @@ export const dockItems: DockItem[] = [
|
||||
{ label: 'Tasks', to: '/tasks', icon: ClipboardList, color: '#fb923c' },
|
||||
{ label: 'Processes', to: '/processes', icon: Workflow, color: '#2dd4bf' },
|
||||
{ label: 'Logs', to: '/task-logs', icon: ScrollText, color: '#94a3b8' },
|
||||
{ label: 'Workspaces', to: '/workspaces', icon: LayoutGrid, color: '#8b5cf6' },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Plus, Trash2, LayoutGrid, ArrowRight } from 'lucide-react';
|
||||
import { useUserState } from '@/state/useUserState';
|
||||
import type { WorkspaceDefinition } from '@/components/Workspace';
|
||||
import { Widget } from '@/components/Widget';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export const WorkspaceListScreen = () => {
|
||||
const [workspaces, setWorkspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const [newName, setNewName] = useState('');
|
||||
|
||||
const createWorkspace = () => {
|
||||
const name = newName.trim();
|
||||
if (!name) return;
|
||||
const ws: WorkspaceDefinition = {
|
||||
id: `ws-${Date.now()}`,
|
||||
name,
|
||||
cwd: '~',
|
||||
};
|
||||
setWorkspaces((prev) => [...prev, ws]);
|
||||
setNewName('');
|
||||
};
|
||||
|
||||
const deleteWorkspace = (id: string) => {
|
||||
setWorkspaces((prev) => prev.filter((ws) => ws.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-start justify-center p-8">
|
||||
<Widget title="Workspaces" className="w-full max-w-lg">
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newName}
|
||||
onChange={(ev) => setNewName(ev.target.value)}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
ev.preventDefault();
|
||||
createWorkspace();
|
||||
}
|
||||
}}
|
||||
placeholder="New workspace name..."
|
||||
className="flex-1 rounded-lg border border-duck-dark/20 bg-white px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50"
|
||||
/>
|
||||
<Button
|
||||
onClick={createWorkspace}
|
||||
disabled={!newName.trim()}
|
||||
className="bg-duck-teal hover:bg-duck-teal/90 cursor-pointer disabled:opacity-40"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{workspaces.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-2 py-8 text-duck-dark/40">
|
||||
<LayoutGrid className="h-8 w-8" />
|
||||
<p className="text-sm">No workspaces yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-1">
|
||||
{workspaces.map((ws) => (
|
||||
<li
|
||||
key={ws.id}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-duck-dark/5 group"
|
||||
>
|
||||
<LayoutGrid className="h-4 w-4 shrink-0 text-duck-teal/60" />
|
||||
<Link to={`/workspaces/${ws.id}`} className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-duck-dark truncate block">{ws.name}</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => deleteWorkspace(ws.id)}
|
||||
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-red-500 transition-opacity cursor-pointer"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<Link
|
||||
to={`/workspaces/${ws.id}`}
|
||||
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-duck-teal transition-opacity"
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Link } from 'react-router';
|
||||
import { LayoutGrid, ArrowRight } from 'lucide-react';
|
||||
import { useUserState } from '@/state/useUserState';
|
||||
import type { WorkspaceDefinition } from '@/components/Workspace';
|
||||
|
||||
export const WorkspaceListWidget = () => {
|
||||
const [workspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-3 max-h-72 overflow-y-auto">
|
||||
{workspaces.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-2 py-6 text-duck-dark/40">
|
||||
<LayoutGrid className="h-6 w-6" />
|
||||
<p className="text-xs">No workspaces</p>
|
||||
<Link to="/workspaces" className="text-xs text-duck-teal hover:underline">
|
||||
Create one
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{workspaces.map((ws) => (
|
||||
<li key={ws.id} className="flex items-center gap-2 px-2 py-1.5 rounded-md hover:bg-duck-dark/5 group">
|
||||
<Link to={`/workspaces/${ws.id}`} className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<LayoutGrid className="h-4 w-4 shrink-0 text-duck-teal/60" />
|
||||
<span className="text-sm text-duck-dark truncate">{ws.name}</span>
|
||||
</Link>
|
||||
<Link
|
||||
to={`/workspaces/${ws.id}`}
|
||||
className="shrink-0 p-1 rounded text-duck-dark/20 md:opacity-0 md:group-hover:opacity-100 hover:text-duck-teal transition-opacity"
|
||||
>
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useParams, Navigate } from 'react-router';
|
||||
import { useUserState } from '@/state/useUserState';
|
||||
import { WorkspaceView, createDefaultLayout } from '@/components/Workspace';
|
||||
import type { LayoutNode, WorkspaceDefinition } from '@/components/Workspace';
|
||||
import { widgetRegistry } from './widget-registry';
|
||||
|
||||
export const WorkspaceScreen = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [workspaces] = useUserState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const workspace = workspaces.find((ws) => ws.id === id);
|
||||
|
||||
if (!workspace) return <Navigate to="/workspaces" replace />;
|
||||
|
||||
return <WorkspaceScreenInner workspace={workspace} />;
|
||||
};
|
||||
|
||||
const WorkspaceScreenInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
||||
const [layout, setLayout] = useUserState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
layout={layout}
|
||||
onLayoutChange={setLayout}
|
||||
registry={widgetRegistry}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { WorkspaceListScreen } from './WorkspaceListScreen';
|
||||
export { WorkspaceScreen } from './WorkspaceScreen';
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MessageSquare, FolderOpen, History, Music, Code, TerminalSquare, LayoutGrid } from 'lucide-react';
|
||||
import type { WidgetRegistry } from '@/components/Workspace';
|
||||
import { CodeEditorView } from 'widgets/CodeEditor';
|
||||
import { TerminalView } from 'widgets/Terminal';
|
||||
import { ChatLauncher } from '../Home/ChatLauncher';
|
||||
import { ChatHistoryWidget as ChatHistory } from '../ChatHistory';
|
||||
import { FileBrowserWidget as FileBrowser } from '../Files';
|
||||
import { Catalog } from 'sounds';
|
||||
import { WorkspaceListWidget } from './WorkspaceListWidget';
|
||||
|
||||
const CodeEditorWrapper = () => <CodeEditorView className="h-full w-full" />;
|
||||
|
||||
const TerminalWrapper = () => <TerminalView className="h-full w-full p-2" />;
|
||||
|
||||
export const widgetRegistry: WidgetRegistry = {
|
||||
'chat-launcher': { name: 'Chat', icon: MessageSquare, component: () => <ChatLauncher /> },
|
||||
'file-browser': { name: 'File Browser', icon: FolderOpen, component: () => <FileBrowser /> },
|
||||
'chat-history': { name: 'Chat History', icon: History, component: () => <ChatHistory /> },
|
||||
'sound-library': { name: 'Sound Library', icon: Music, component: () => <Catalog /> },
|
||||
'code-editor': { name: 'Code Editor', icon: Code, component: CodeEditorWrapper },
|
||||
'terminal': { name: 'Terminal', icon: TerminalSquare, component: TerminalWrapper },
|
||||
'workspace-list': { name: 'Workspaces', icon: LayoutGrid, component: () => <WorkspaceListWidget /> },
|
||||
};
|
||||
@@ -14,3 +14,4 @@ export * from './Terminal';
|
||||
export * from './Files';
|
||||
export * from './ChatHistory';
|
||||
export * from './CodeEditor';
|
||||
export * from './Workspaces';
|
||||
|
||||
Reference in New Issue
Block a user