ready for app-registry refactor
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Widget } from 'widgets/Widget';
|
||||
import { useSettings } from '@/state/useSettings';
|
||||
import { useVisiblePiModels } from '@/state/useModels';
|
||||
import { ChatLauncher as ChatLauncherComponent } from 'apps/Chat';
|
||||
|
||||
export const ChatLauncher = () => {
|
||||
const navigate = useNavigate();
|
||||
const { settings } = useSettings();
|
||||
const piModels = useVisiblePiModels();
|
||||
const [model, setModel] = useState<string | null>(settings.chat.defaultModel);
|
||||
|
||||
useEffect(() => {
|
||||
setModel(settings.chat.defaultModel);
|
||||
}, [settings.chat.defaultModel]);
|
||||
|
||||
const handleSubmit = (data: {
|
||||
prompt: string;
|
||||
model: string | null;
|
||||
attachmentIds?: string[];
|
||||
images?: { filename: string; dataUrl: string }[];
|
||||
}) => {
|
||||
navigate('/chat/new', {
|
||||
state: {
|
||||
initialMessage: data.prompt,
|
||||
model: data.model,
|
||||
attachmentIds: data.attachmentIds,
|
||||
images: data.images,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Widget title="Start Chat">
|
||||
<ChatLauncherComponent
|
||||
availableModels={piModels}
|
||||
selectedModel={model}
|
||||
onModelChange={setModel}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { WorkspaceView } from '@/components/Workspace';
|
||||
import { useWorkspacesState } from '@/state/useWorkspacesState';
|
||||
import { appRegistry } from '../Workspaces/app-registry';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
export const HomeScreen = () => {
|
||||
const workspace = useWorkspacesState<LayoutNode>('screens/home', defaultLayout);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView workspace={workspace} registry={appRegistry} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { LayoutNode } from '@/components/Workspace';
|
||||
|
||||
export const defaultLayout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'home-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 },
|
||||
{
|
||||
node: {
|
||||
type: 'group',
|
||||
id: 'home-center',
|
||||
direction: 'vertical',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 },
|
||||
{ node: { type: 'panel', id: 'home-chat', appType: 'chat-launcher' }, size: 40 },
|
||||
],
|
||||
},
|
||||
size: 60,
|
||||
},
|
||||
{ node: { type: 'panel', id: 'home-right', appType: 'project-list' }, size: 20 },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,36 +1 @@
|
||||
import { useState } from 'react';
|
||||
import type { LayoutNode } from '@/components/Workspace';
|
||||
import { WorkspaceView } from '@/components/Workspace';
|
||||
import { appRegistry } from '../Workspaces/app-registry';
|
||||
|
||||
const initialLayout: LayoutNode = {
|
||||
type: 'group',
|
||||
id: 'home-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'home-left', appType: 'workspace-list' }, size: 20 },
|
||||
{
|
||||
node: {
|
||||
type: 'group',
|
||||
id: 'home-center',
|
||||
direction: 'vertical',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'homepage-widget-panel', appType: 'widget-panel' }, size: 60 },
|
||||
{ node: { type: 'panel', id: 'home-chat', appType: 'chat-launcher' }, size: 40 },
|
||||
],
|
||||
},
|
||||
size: 60,
|
||||
},
|
||||
{ node: { type: 'panel', id: 'home-right', appType: 'project-list' }, size: 20 },
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeScreen = () => {
|
||||
const [layout, setLayout] = useState<LayoutNode>(initialLayout);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView layout={layout} onLayoutChange={setLayout} registry={appRegistry} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export * from './HomeScreen';
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Navigate } from 'react-router';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import type { LayoutNode } from '@/components/Workspace';
|
||||
import { WorkspaceView } from '@/components/Workspace';
|
||||
import { useWorkspacesState } from '@/state/useWorkspacesState';
|
||||
import { appRegistry } from '../Workspaces/app-registry';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
export const TerminalScreen = () => {
|
||||
const { user } = useAuth();
|
||||
const workspace = useWorkspacesState<LayoutNode>('screens/terminal', defaultLayout);
|
||||
|
||||
if (user?.role !== 'Super Admin') return <Navigate to="/" replace />;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView workspace={workspace} registry={appRegistry} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { LayoutNode } from '@/components/Workspace';
|
||||
|
||||
export const defaultLayout: LayoutNode = {
|
||||
type: 'panel',
|
||||
id: 'terminal-screen',
|
||||
appType: 'terminal-host',
|
||||
};
|
||||
@@ -1,30 +1 @@
|
||||
import { useState } from 'react';
|
||||
import { Navigate } from 'react-router';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
import { WorkspaceView, createDefaultLayout } from '@/components/Workspace';
|
||||
import type { LayoutNode } from '@/components/Workspace';
|
||||
import { appRegistry } from '../Workspaces/app-registry';
|
||||
|
||||
|
||||
export const TerminalScreen = () => {
|
||||
const { user } = useAuth();
|
||||
const [layout, setLayout] = useState<LayoutNode>(defaultLayout);
|
||||
|
||||
if (user?.role !== 'Super Admin') return <Navigate to="/" replace />;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView
|
||||
workspace={null}
|
||||
layout={layout}
|
||||
onLayoutChange={setLayout}
|
||||
registry={appRegistry} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultLayout: LayoutNode = {
|
||||
type: 'panel',
|
||||
id: 'terminal-screen',
|
||||
appType: 'terminal-host'
|
||||
};
|
||||
export * from './TerminalScreen';
|
||||
|
||||
@@ -32,7 +32,7 @@ export const WorkspaceListApp = () => {
|
||||
const navigate = useNavigate();
|
||||
const client = useClient();
|
||||
const queryClient = useQueryClient();
|
||||
const [workspaces, setWorkspaces] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const { value: workspaces, setValue: setWorkspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const [selected, setSelected] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
||||
const [, setCreating] = useGlobal<boolean>(CREATING_WORKSPACE_KEY, false);
|
||||
const [, setEditing] = useGlobal<string | null>(EDITING_WORKSPACE_KEY, null);
|
||||
@@ -77,7 +77,7 @@ export const WorkspaceListApp = () => {
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||
const { [layoutKey]: _, ...rest } = currentState;
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], rest);
|
||||
client.patch('/user/workspaces-state', { [layoutKey]: null }).catch(() => {});
|
||||
client.patch('/workspaces', { [layoutKey]: null }).catch(() => {});
|
||||
setDeleting(null);
|
||||
};
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ const CreatePanel = () => {
|
||||
const client = useClient();
|
||||
const [name, setName] = useGlobal<string>(NEW_WS_NAME_KEY, '');
|
||||
const [filePath] = useUserState<string>('files/currentPath', '/');
|
||||
const [workspaces, setWorkspaces] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const { value: workspaces, setValue: setWorkspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
|
||||
const cwd = filePath === '/' ? '~' : `~/${filePath.replace(/^\//, '')}`;
|
||||
|
||||
@@ -325,10 +325,10 @@ const CreatePanel = () => {
|
||||
// Delete old files, write new layout
|
||||
const patch: Record<string, unknown> = { [newLayoutKey]: wsLayout };
|
||||
for (const k of oldKeys) patch[k] = null;
|
||||
client.patch('/user/workspaces-state', patch).catch(() => {});
|
||||
client.patch('/workspaces', patch).catch(() => {});
|
||||
} else {
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [newLayoutKey]: wsLayout });
|
||||
client.patch('/user/workspaces-state', { [newLayoutKey]: wsLayout }).catch(() => {});
|
||||
client.patch('/workspaces', { [newLayoutKey]: wsLayout }).catch(() => {});
|
||||
}
|
||||
|
||||
setEditingId(null);
|
||||
@@ -351,7 +351,7 @@ const CreatePanel = () => {
|
||||
setWorkspaces((prev) => [...prev, ws]);
|
||||
const currentState = queryClient.getQueryData<Record<string, unknown>>(['WORKSPACES_STATE']) ?? {};
|
||||
queryClient.setQueryData(['WORKSPACES_STATE'], { ...currentState, [layoutKey]: wsLayout });
|
||||
client.patch('/user/workspaces-state', { [layoutKey]: wsLayout }).catch(() => {});
|
||||
client.patch('/workspaces', { [layoutKey]: wsLayout }).catch(() => {});
|
||||
|
||||
setName('');
|
||||
setDescription('');
|
||||
@@ -478,14 +478,14 @@ const WorkspacePreviewEmpty = () => {
|
||||
};
|
||||
|
||||
const WorkspacePreviewInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
||||
const [layout, setLayout] = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||
const ws = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||
|
||||
return <WorkspaceView workspace={workspace} layout={layout} onLayoutChange={setLayout} registry={appRegistry} />;
|
||||
return <WorkspaceView workspace={ws} registry={appRegistry} cwd={workspace.cwd} />;
|
||||
};
|
||||
|
||||
const WorkspacePreview = () => {
|
||||
const [selectedId] = useGlobal<string | null>(SELECTED_WORKSPACE_KEY, null);
|
||||
const [workspaces] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const { value: workspaces } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const workspace = selectedId ? workspaces.find((ws) => ws.id === selectedId) : null;
|
||||
|
||||
if (!workspace) return <WorkspacePreviewEmpty />;
|
||||
@@ -506,13 +506,11 @@ const WorkspacePreview = () => {
|
||||
const registry = { ...appRegistry, 'workspace-preview': { name: 'Workspace Preview', icon: LayoutGrid, component: WorkspacePreview } };
|
||||
|
||||
export const WorkspaceListScreen = () => {
|
||||
const [layout, setLayout, isLoaded] = useWorkspacesState<LayoutNode>('ws-layout-ws-homepage', defaultLayout);
|
||||
|
||||
if (!isLoaded) return null;
|
||||
const workspace = useWorkspacesState<LayoutNode>('screens/homepage', defaultLayout);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceLayout layout={layout} onLayoutChange={setLayout} registry={registry} />
|
||||
<WorkspaceLayout layout={workspace.value} onLayoutChange={workspace.setValue} registry={registry} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { appRegistry } from './app-registry';
|
||||
|
||||
export const WorkspaceScreen = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [workspaces, , isLoaded] = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const { value: workspaces, isLoaded } = useWorkspacesState<WorkspaceDefinition[]>('workspaces', []);
|
||||
const workspace = workspaces.find((ws) => ws.id === id);
|
||||
|
||||
if (!isLoaded) return null;
|
||||
@@ -16,16 +16,11 @@ export const WorkspaceScreen = () => {
|
||||
};
|
||||
|
||||
const WorkspaceScreenInner = ({ workspace }: { workspace: WorkspaceDefinition }) => {
|
||||
const [layout, setLayout] = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||
const ws = useWorkspacesState<LayoutNode>(`ws-layout-${workspace.id}`, createDefaultLayout());
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<WorkspaceView
|
||||
workspace={workspace}
|
||||
layout={layout}
|
||||
onLayoutChange={setLayout}
|
||||
registry={appRegistry}
|
||||
/>
|
||||
<WorkspaceView workspace={ws} registry={appRegistry} cwd={workspace.cwd} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,22 +9,30 @@ import { TerminalView } from 'apps/Terminal';
|
||||
import { useWorkspacesState } from '@/state/useWorkspacesState';
|
||||
import { usePanelChannel } from 'hooks/usePanelChannel';
|
||||
import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'apps/FileViewer';
|
||||
import { usePi } from 'apps/Chat';
|
||||
import { ChatPanel } from 'apps/Chat';
|
||||
import { EmbeddableChat, ChatLauncher, usePi } from 'apps/Chat';
|
||||
import { useVisiblePiModels } from '@/state/useModels';
|
||||
import { ChatHistoryApp as ChatHistory } from '../ChatHistory';
|
||||
import { Files } from '../Files';
|
||||
import { Catalog } from 'sounds';
|
||||
import { WorkspaceListApp } from './WorkspaceListApp';
|
||||
import { ProjectListApp } from '../Projects/ProjectListApp';
|
||||
import { ChatLauncher } from '../Home/ChatLauncher';
|
||||
import { widgetRegistry } from 'widgets/widget-registry';
|
||||
import { WidgetPanel } from 'widgets/WidgetPanel';
|
||||
|
||||
const ChatWidget = () => {
|
||||
const ChatWidget = () => <EmbeddableChat className="h-full" />;
|
||||
|
||||
const ChatLauncherWidget = () => {
|
||||
const pi = usePi();
|
||||
const models = useVisiblePiModels();
|
||||
return <ChatPanel chat={pi} availableModels={models} />;
|
||||
console.log(models)
|
||||
return (
|
||||
<ChatLauncher
|
||||
availableModels={models}
|
||||
selectedModel={pi.selectedModel}
|
||||
onModelChange={pi.setSelectedModel}
|
||||
onSubmit={({ prompt, model, attachmentIds, images }) => pi.sendPrompt(prompt, attachmentIds, images)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1));
|
||||
@@ -75,7 +83,7 @@ const EMPTY_TERMINALS: Record<string, string> = {};
|
||||
const TerminalWrapper = ({ panelId }: { panelId: string }) => {
|
||||
const { workspaceId, cwd } = useWorkspace();
|
||||
const stateKey = workspaceId ? `ws-terminals-${workspaceId}` : 'ws-terminals-default';
|
||||
const [terminals, setTerminals] = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const setTerminalsRef = useRef(setTerminals);
|
||||
setTerminalsRef.current = setTerminals;
|
||||
|
||||
@@ -105,7 +113,7 @@ const HostTerminalWrapper = ({ panelId }: { panelId: string }) => {
|
||||
const { user } = useAuth();
|
||||
const { workspaceId, cwd } = useWorkspace();
|
||||
const stateKey = workspaceId ? `ws-host-terminals-${workspaceId}` : 'ws-host-terminals-default';
|
||||
const [terminals, setTerminals] = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const setTerminalsRef = useRef(setTerminals);
|
||||
setTerminalsRef.current = setTerminals;
|
||||
|
||||
@@ -142,7 +150,7 @@ const HostTerminalWrapper = ({ panelId }: { panelId: string }) => {
|
||||
const CommandTerminalWrapper = ({ panelId, command, statePrefix }: { panelId: string; command: string; statePrefix: string }) => {
|
||||
const { workspaceId, cwd } = useWorkspace();
|
||||
const stateKey = workspaceId ? `ws-${statePrefix}-${workspaceId}` : `ws-${statePrefix}-default`;
|
||||
const [terminals, setTerminals] = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const { value: terminals, setValue: setTerminals } = useWorkspacesState<Record<string, string>>(stateKey, EMPTY_TERMINALS);
|
||||
const setTerminalsRef = useRef(setTerminals);
|
||||
setTerminalsRef.current = setTerminals;
|
||||
|
||||
@@ -239,7 +247,7 @@ export const appRegistry: AppRegistry = {
|
||||
'workspace-list': { name: 'Workspaces', icon: LayoutGrid, component: () => <WorkspaceListApp />, availableOnPanel: false },
|
||||
'project-list': { name: 'Projects', icon: FolderKanban, component: () => <ProjectListApp />, availableOnPanel: false },
|
||||
'file-viewer': { name: 'File Viewer', icon: Eye, component: FileViewerBody, header: FileViewerHeader, provider: FileViewerWorkspaceProvider, availableOnPanel: false },
|
||||
'chat-launcher': { name: 'Chat Launcher', icon: Sparkles, component: () => <ChatLauncher />, fixedHeight: 180, availableOnPanel: false },
|
||||
'chat-launcher': { name: 'Chat Launcher', icon: Sparkles, component: ChatLauncherWidget, fixedHeight: 180, availableOnPanel: false },
|
||||
'widget-panel': { name: 'Widget Panel', icon: LayoutDashboard, component: WidgetPanel, transparent: true },
|
||||
...widgetRegistry,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useAuth } from 'hooks/useAuth';
|
||||
@@ -6,7 +6,7 @@ import type { UserState } from './types/user-settings';
|
||||
|
||||
const QUERY_KEY = ['WORKSPACES_STATE'];
|
||||
|
||||
export function useWorkspacesState<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void, boolean] {
|
||||
export function useWorkspacesState<T>(key: string, defaultValue: T) {
|
||||
const client = useClient();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -16,10 +16,23 @@ export function useWorkspacesState<T>(key: string, defaultValue: T): [T, (value:
|
||||
const { data: state = {}, isSuccess } = useQuery<UserState>({
|
||||
queryKey: QUERY_KEY,
|
||||
enabled: isAuthenticated,
|
||||
queryFn: () => client.get<UserState>('/user/workspaces-state'),
|
||||
queryFn: () => client.get<UserState>('/workspaces'),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
// Seed default to backend when key is missing after initial fetch
|
||||
const seededKeyRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isSuccess || seededKeyRef.current === key) return;
|
||||
seededKeyRef.current = key;
|
||||
|
||||
const currentState = queryClient.getQueryData<UserState>(QUERY_KEY) ?? {};
|
||||
if (!(key in currentState)) {
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: defaultValue });
|
||||
clientRef.current.patch('/workspaces', { [key]: defaultValue }).catch(() => { });
|
||||
}
|
||||
}, [isSuccess, key, defaultValue, queryClient]);
|
||||
|
||||
const value = key in state ? (state[key] as T) : defaultValue;
|
||||
|
||||
const setValue = useCallback(
|
||||
@@ -30,10 +43,10 @@ export function useWorkspacesState<T>(key: string, defaultValue: T): [T, (value:
|
||||
|
||||
queryClient.setQueryData(QUERY_KEY, { ...currentState, [key]: newValue });
|
||||
|
||||
clientRef.current.patch('/user/workspaces-state', { [key]: newValue }).catch(() => {});
|
||||
clientRef.current.patch('/workspaces', { [key]: newValue }).catch(() => { });
|
||||
},
|
||||
[key, defaultValue, queryClient],
|
||||
);
|
||||
|
||||
return [value, setValue, isSuccess];
|
||||
return { key, value, setValue, isLoaded: isSuccess };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user