From ab03b175e7a22da1a0df225645dafac18bc5611c Mon Sep 17 00:00:00 2001 From: Andre Padez Date: Sun, 22 Feb 2026 01:50:15 +0000 Subject: [PATCH] Projects --- src/apps/officer-web/App.tsx | 3 +- .../Dashboard/ChatHistory/defaultLayout.ts | 11 + .../Screens/Dashboard/ChatHistory/index.tsx | 34 +- .../Dashboard/ChatScreen/ChatScreen.tsx | 81 --- .../Screens/Dashboard/ChatScreen/index.tsx | 1 - .../Screens/Dashboard/CodeEditor/index.tsx | 10 - .../Dashboard/Projects/ProjectListScreen.tsx | 610 +----------------- .../Dashboard/Projects/ProjectScreen.tsx | 15 +- .../Dashboard/Projects/defaultLayout.ts | 11 + .../Dashboard/Workspaces/WorkspaceScreen.tsx | 21 +- .../Dashboard/Workspaces/WorkspacesScreen.tsx | 14 + .../Screens/Dashboard/Workspaces/index.tsx | 1 + .../officer-web/Screens/Dashboard/index.tsx | 2 - src/servers/api/settings/projects.ts | 128 ---- src/servers/api/terminal/websocket.ts | 1 + src/servers/api/workspaces/types.ts | 2 +- src/servers/api/workspaces/utils.ts | 66 +- src/servers/api/workspaces/workspaces.ts | 33 +- src/servers/data-path.ts | 2 +- src/servers/hono.ts | 2 - .../src/AppRegistry/AppRegistry.tsx | 4 +- .../src/apps}/ChatHistory/ChatDetailPanel.tsx | 13 +- .../apps}/ChatHistory/CreateGroupDialog.tsx | 2 +- .../apps}/ChatHistory/GroupContextMenu.tsx | 2 +- .../apps}/ChatHistory/SessionContextMenu.tsx | 2 +- .../src/apps/ChatHistory/SessionList.tsx} | 2 +- .../officerdev/src/apps/ChatHistory/index.ts | 25 + .../FileBrowserApp/FileBrowserApp.tsx | 5 +- .../FileBrowserPanelWrapper.tsx | 3 +- .../FileBrowserApp/useFileBrowserApp.ts | 10 +- .../src/apps}/Projects/ProjectListApp.tsx | 24 +- .../src/apps/Projects/ProjectPreview.tsx | 549 ++++++++++++++++ .../src/apps}/Projects/constants.ts | 0 .../officerdev/src/apps/Projects/index.ts | 36 ++ .../apps/Terminal/CommandTerminalWrapper.tsx | 3 +- .../officerdev/src/apps/Terminal/Headers.tsx | 5 +- .../src/apps/Terminal/TerminalWrapper.tsx | 3 +- src/workspaces/officerdev/src/index.ts | 4 +- src/workspaces/state/src/index.ts | 1 - src/workspaces/state/src/useProjectsState.ts | 44 -- 40 files changed, 829 insertions(+), 956 deletions(-) create mode 100644 src/apps/officer-web/Screens/Dashboard/ChatHistory/defaultLayout.ts delete mode 100644 src/apps/officer-web/Screens/Dashboard/ChatScreen/ChatScreen.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/ChatScreen/index.tsx delete mode 100644 src/apps/officer-web/Screens/Dashboard/CodeEditor/index.tsx create mode 100644 src/apps/officer-web/Screens/Dashboard/Projects/defaultLayout.ts create mode 100644 src/apps/officer-web/Screens/Dashboard/Workspaces/WorkspacesScreen.tsx delete mode 100644 src/servers/api/settings/projects.ts rename src/{apps/officer-web/Screens/Dashboard => workspaces/officerdev/src/apps}/ChatHistory/ChatDetailPanel.tsx (92%) rename src/{apps/officer-web/Screens/Dashboard => workspaces/officerdev/src/apps}/ChatHistory/CreateGroupDialog.tsx (99%) rename src/{apps/officer-web/Screens/Dashboard => workspaces/officerdev/src/apps}/ChatHistory/GroupContextMenu.tsx (98%) rename src/{apps/officer-web/Screens/Dashboard => workspaces/officerdev/src/apps}/ChatHistory/SessionContextMenu.tsx (99%) rename src/{apps/officer-web/Screens/Dashboard/ChatHistory/Screen.tsx => workspaces/officerdev/src/apps/ChatHistory/SessionList.tsx} (99%) rename src/{apps/officer-web/Screens/Dashboard => workspaces/officerdev/src/apps}/Projects/ProjectListApp.tsx (89%) create mode 100644 src/workspaces/officerdev/src/apps/Projects/ProjectPreview.tsx rename src/{apps/officer-web/Screens/Dashboard => workspaces/officerdev/src/apps}/Projects/constants.ts (100%) create mode 100644 src/workspaces/officerdev/src/apps/Projects/index.ts delete mode 100644 src/workspaces/state/src/useProjectsState.ts diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index 78fc74e6..991113ea 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -51,7 +51,8 @@ export function App() { } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/apps/officer-web/Screens/Dashboard/ChatHistory/defaultLayout.ts b/src/apps/officer-web/Screens/Dashboard/ChatHistory/defaultLayout.ts new file mode 100644 index 00000000..70c8d2df --- /dev/null +++ b/src/apps/officer-web/Screens/Dashboard/ChatHistory/defaultLayout.ts @@ -0,0 +1,11 @@ +import type { LayoutNode } from '@/components/Workspace'; + +export const defaultLayout: LayoutNode = { + type: 'group', + id: 'chat-history-root', + direction: 'horizontal', + children: [ + { node: { type: 'panel', id: 'chat-list', appType: 'chat-session-list' }, size: 35 }, + { node: { type: 'panel', id: 'chat-detail', appType: 'chat-detail' }, size: 65 }, + ], +}; diff --git a/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx b/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx index e2222060..852f0912 100644 --- a/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx +++ b/src/apps/officer-web/Screens/Dashboard/ChatHistory/index.tsx @@ -1,25 +1,14 @@ -import { useMemo, useEffect } from 'react'; +import { useEffect } from 'react'; import { useParams } from 'react-router'; -import type { LayoutNode, PanelComponents } from '@/components/Workspace'; -import { WorkspaceLayout } from '@/components/Workspace'; +import { WorkspaceView } from '@/components/Workspace'; +import type { LayoutNode } from '@/components/Workspace'; +import { useWorkspacesState } from 'state/useWorkspacesState'; import { usePanelChannel } from 'hooks/usePanelChannel'; import { useChatSessions } from 'state/useChatSessions'; - -import { SessionList } from './Screen'; -import { ChatDetailPanel, type SelectedSession } from './ChatDetailPanel'; +import type { SelectedSession } from 'officerdev'; +import { defaultLayout } from './defaultLayout'; export { ChatHistory as ChatHistoryApp } from './Widget'; -export { SessionList }; - -const layout: LayoutNode = { - type: 'group', - id: 'chat-history-root', - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: 'chat-list', appType: null }, size: 35 }, - { node: { type: 'panel', id: 'chat-detail', appType: null }, size: 65 }, - ], -}; type SessionListPageProps = { isNew?: boolean; @@ -29,6 +18,7 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => { const { sessionId } = useParams<{ sessionId: string }>(); const { sessions } = useChatSessions(); const [, setSelected] = usePanelChannel('chat:selected-session', null); + const workspace = useWorkspacesState('screens/chat', defaultLayout); useEffect(() => { if (isNew) { @@ -40,17 +30,9 @@ export const SessionListPage = ({ isNew }: SessionListPageProps) => { setSelected({ id: sessionId, model: session?.model ?? null }); }, [sessionId, isNew]); - const panelComponents: PanelComponents = useMemo( - () => ({ - 'chat-list': SessionList, - 'chat-detail': ChatDetailPanel, - }), - [], - ); - return (
- {}} components={panelComponents} /> +
); }; diff --git a/src/apps/officer-web/Screens/Dashboard/ChatScreen/ChatScreen.tsx b/src/apps/officer-web/Screens/Dashboard/ChatScreen/ChatScreen.tsx deleted file mode 100644 index 5ebf205e..00000000 --- a/src/apps/officer-web/Screens/Dashboard/ChatScreen/ChatScreen.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useRef, useEffect, useState } from 'react'; -import { useNavigate, useLocation } from 'react-router'; -import { useChatSessions } from 'state/useChatSessions'; -import { SessionBar, EmbeddableChat, type UsePiChatType, type Attachment } from 'officerdev'; -import { Card } from '@/components/Card'; - -export type { Attachment }; - -type ChatPanelProps = { - chat: UsePiChatType; -}; - -export const ChatScreen = ({ chat }: ChatPanelProps) => { - const { isConnected, isGenerating, sessionId, setSelectedModel, sendPrompt } = chat; - - const location = useLocation(); - const navigate = useNavigate(); - const [fullscreen, setFullscreen] = useState(false); - - const { sessions, deleteSession } = useChatSessions(); - const sessionTitle = sessionId ? sessions.find((s) => s.id === sessionId)?.title : undefined; - const listPath = '/chat'; - - // Capture initial state from navigation - const locationState = location.state as { - initialMessage?: string; - prefillInput?: string; - model?: string; - cwd?: { root?: string; path: string }; - attachmentIds?: string[]; - images?: { filename: string; dataUrl: string }[]; - } | null; - - const initialMessage = locationState?.initialMessage - ? { - text: locationState.initialMessage, - attachmentIds: locationState.attachmentIds, - images: locationState.images, - cwd: locationState.cwd, - } - : undefined; - - const defaultInput = locationState?.prefillInput ?? ''; - const initialModel = locationState?.model ?? null; - - // Clear location state after capturing - useEffect(() => { - if (locationState) { - window.history.replaceState({}, '', location.pathname); - } - }, []); - - return ( - - { - if (!sessionId) return; - await deleteSession(sessionId); - navigate(listPath); - }} - onToggleFullscreen={() => setFullscreen((f) => !f)} - /> - - - - ); -}; diff --git a/src/apps/officer-web/Screens/Dashboard/ChatScreen/index.tsx b/src/apps/officer-web/Screens/Dashboard/ChatScreen/index.tsx deleted file mode 100644 index 18188300..00000000 --- a/src/apps/officer-web/Screens/Dashboard/ChatScreen/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './ChatScreen'; diff --git a/src/apps/officer-web/Screens/Dashboard/CodeEditor/index.tsx b/src/apps/officer-web/Screens/Dashboard/CodeEditor/index.tsx deleted file mode 100644 index 7cb4c18e..00000000 --- a/src/apps/officer-web/Screens/Dashboard/CodeEditor/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { CodeEditorView } from 'officerdev'; -import { Widget } from 'widgets/Widget'; - -export const CodeEditor = () => ( -
- - - -
-); diff --git a/src/apps/officer-web/Screens/Dashboard/Projects/ProjectListScreen.tsx b/src/apps/officer-web/Screens/Dashboard/Projects/ProjectListScreen.tsx index a7aeb758..74dbf363 100644 --- a/src/apps/officer-web/Screens/Dashboard/Projects/ProjectListScreen.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Projects/ProjectListScreen.tsx @@ -1,15 +1,10 @@ -import { useEffect, useState } from 'react'; -import { Link, useNavigate, useSearchParams } from 'react-router'; -import { FolderKanban, Plus, ArrowRight, Type, Rocket, FileText, Layout } from 'lucide-react'; -import { useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router'; import { useGlobal } from 'hooks/useGlobal'; -import { useClient } from 'hooks/useClient'; -import { useProjectsState } from 'state/useProjectsState'; -import { WorkspaceLayout, WorkspaceView, createDefaultLayout } from '@/components/Workspace'; -import type { LayoutNode, ProjectDefinition, ProjectType } from '@/components/Workspace'; -import { Button } from '@/components/ui/button'; -import { generateSlug, slugify } from 'helpers/slug'; -import { useAppRegistry } from 'officerdev'; +import { useWorkspacesState } from 'state/useWorkspacesState'; +import { WorkspaceView } from '@/components/Workspace'; +import type { LayoutNode, ProjectType } from '@/components/Workspace'; +import { generateSlug } from 'helpers/slug'; import { SELECTED_PROJECT, CREATING_PROJECT, @@ -20,600 +15,15 @@ import { NEW_PROJ_TYPE, NEW_PROJ_HAS_BACKEND, NEW_PROJ_HAS_AUTH, - NEW_PROJ_PREVIEW_LAYOUT, -} from './constants'; -import { ProjectListApp } from './ProjectListApp'; - -const defaultLayout: LayoutNode = { - type: 'group', - id: 'proj-home-root', - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: 'proj-home-list', appType: 'project-list' }, size: 25 }, - { node: { type: 'panel', id: 'proj-home-right', appType: 'project-preview' }, size: 75 }, - ], -}; - -// --- Layout Templates --- - -let tplCounter = 0; -const tplUid = () => `ptpl-${++tplCounter}`; - -type LayoutTemplate = { - name: string; - layout: () => LayoutNode; -}; - -const templates: LayoutTemplate[] = [ - { - name: 'Single', - layout: () => ({ type: 'panel', id: tplUid(), appType: null }), - }, - { - name: '2 Columns', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }), - }, - { - name: 'Main + Side', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { - node: { - type: 'group', - id: tplUid(), - direction: 'vertical', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 50, - }, - ], - }), - }, - { - name: 'Sidebar', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 25 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 75 }, - ], - }), - }, - { - name: '2x2 Grid', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'vertical', - children: [ - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 50, - }, - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 50, - }, - ], - }), - }, - { - name: 'Cols + Bottom', - layout: () => ({ - type: 'group', - id: tplUid(), - direction: 'vertical', - children: [ - { - node: { - type: 'group', - id: tplUid(), - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 50 }, - ], - }, - size: 70, - }, - { node: { type: 'panel', id: tplUid(), appType: null }, size: 30 }, - ], - }), - }, -]; - -// --- Template Thumbnails --- - -const ThumbnailNode = ({ node }: { node: LayoutNode }) => { - if (node.type === 'panel') { - return
; - } - const isH = node.direction === 'horizontal'; - return ( -
- {node.children.map((child) => ( -
- -
- ))} -
- ); -}; - -const TemplateCell = ({ index }: { index: number }) => { - const tpl = templates[index]!; - const [selected, setSelected] = useGlobal(NEW_PROJ_TEMPLATE, 0); - const node = tpl.layout(); - const isSelected = selected === index; - - return ( -
- -
- ); -}; - -// --- Panel: Template Picker (3x2 workspace) --- - -const tplPanelLayout: LayoutNode = { - type: 'group', - id: 'ptpl-root', - direction: 'vertical', - children: [ - { - node: { - type: 'group', - id: 'ptpl-row-0', - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: 'ptpl-0', appType: 'ptpl-0' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-1', appType: 'ptpl-1' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-2', appType: 'ptpl-2' }, size: 33.34 }, - ], - }, - size: 50, - }, - { - node: { - type: 'group', - id: 'ptpl-row-1', - direction: 'horizontal', - children: [ - { node: { type: 'panel', id: 'ptpl-3', appType: 'ptpl-3' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-4', appType: 'ptpl-4' }, size: 33.33 }, - { node: { type: 'panel', id: 'ptpl-5', appType: 'ptpl-5' }, size: 33.34 }, - ], - }, - size: 50, - }, - ], -}; - -const tplRegistry = Object.fromEntries( - templates.map((tpl, i) => [`ptpl-${i}`, { name: tpl.name, icon: Layout, component: () => }]), -); - -const TemplatePanel = () => ( - {}} registry={tplRegistry} /> -); - -// --- Panel: Details (Name + Description + Project Type + Backend/Auth toggles) --- - -const PROJECT_TYPE_OPTIONS: { value: ProjectType; label: string }[] = [ - { value: 'landing-page', label: 'Landing Page' }, - { value: 'website', label: 'Website' }, - { value: 'app', label: 'App' }, -]; - -const DetailsPanel = () => { - const [name, setName] = useGlobal(NEW_PROJ_NAME, ''); - const [description, setDescription] = useGlobal(NEW_PROJ_DESC, ''); - const [projectType, setProjectType] = useGlobal(NEW_PROJ_TYPE, 'app'); - const [hasBackend, setHasBackend] = useGlobal(NEW_PROJ_HAS_BACKEND, false); - const [hasAuth, setHasAuth] = useGlobal(NEW_PROJ_HAS_AUTH, false); - - return ( -
-
-
- - setName(ev.target.value)} - placeholder="My Project" - autoFocus - className="rounded-lg border border-duck-dark/20 bg-background px-3 py-2 text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-2 focus:ring-emerald-500/30 focus:border-emerald-500/50" - /> -
-
- -