- {chat && (
+ {chatId && (
void setParams({ chat: null }, { history: 'replace' })}
+ key={panelKey}
+ project={project}
+ chatId={chatId}
+ initialMessage={q || undefined}
+ onChatCreated={onChatCreated}
+ onInitialMessageSent={onInitialMessageSent}
+ onClose={() => void setParams({ chat: null, q: null }, { history: 'replace' })}
/>
)}
@@ -49,44 +66,72 @@ export function ProjectShell({ workspace, children }: ProjectShellProps) {
}
interface SideChatProps {
- chat: Chat
- workspace: Workspace
+ project: Project
+ chatId: string
+ initialMessage?: string
+ onChatCreated: (chatId: string) => void
+ onInitialMessageSent: () => void
onClose: () => void
}
-function SideChat({ chat, workspace, onClose }: SideChatProps) {
- const runs =
- DRAFTS.find((draft) => draft.workspaceId === workspace.id)?.running.filter(
- (work) => work.chat.id === chat.id
- ) ?? []
- const running = runs.length > 0
+function SideChat({
+ project,
+ chatId,
+ initialMessage,
+ onChatCreated,
+ onInitialMessageSent,
+ onClose,
+}: SideChatProps) {
+ const sources = useProjectSources()
+ const { data: chats } = useMothershipChats(project.isMock ? undefined : project.id)
+ const realChat = chats?.find((chat) => chat.id === chatId)
+ /** Chats from the project source (changelog and issue stories) keep their canned threads. */
+ const mockChat = sources.chatsFor(project).find((chat) => chat.id === chatId)
+ const runs = mockChat
+ ? (sources
+ .changelogFor(project)
+ .draft?.running.filter((work) => work.chat.id === mockChat.id) ?? [])
+ : []
+ const running = runs.length > 0 || Boolean(realChat?.isActive)
+ const title =
+ chatId === NEW_CHAT && !realChat
+ ? `New chat in ${project.name}`
+ : (realChat?.name ?? mockChat?.title ?? 'Chat')
return (
{running && }
-
- {chat.title}
-
- {chat.owner && }
+ {title}
+ {mockChat?.owner && }
- {running ? (
+ {runs.length > 0 ? (
{runs.map((work) => (
-
+
))}
- ) : chat.id === 'new' ? (
+ ) : mockChat ? (
+
+ ) : project.isMock ? (
-
+
) : (
-
+
)}
)
diff --git a/apps/sim/app/playground/org/components/proto-dashboard.tsx b/apps/sim/app/playground/org/components/proto-dashboard.tsx
index e730431939a..931f95e6ca8 100644
--- a/apps/sim/app/playground/org/components/proto-dashboard.tsx
+++ b/apps/sim/app/playground/org/components/proto-dashboard.tsx
@@ -1,5 +1,6 @@
'use client'
+import type { ReactNode } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -10,25 +11,56 @@ import {
import { Check, ChevronDown, Plus } from '@sim/emcn/icons'
import { useQueryStates } from 'nuqs'
import { DashboardPreview } from '@/components/dashboards/dashboard-preview'
+import { DashboardFile } from '@/app/playground/org/components/dashboard-file'
+import { RunsDashboard } from '@/app/playground/org/components/runs-dashboard'
+import { mockTableAnalytics } from '@/app/playground/org/fixtures'
import { MOCK_DASHBOARDS } from '@/app/playground/org/lib/dashboards'
-import { mockTableAnalytics } from '@/app/playground/org/lib/mock-analytics'
-import type { Workspace } from '@/app/playground/org/lib/mock-data'
+import { type Project, realWorkspaceId } from '@/app/playground/org/lib/project'
import { protoParsers } from '@/app/playground/org/lib/search-params'
+import { useProjectResources } from '@/app/playground/org/lib/use-project-resources'
import { TableAnalyticsSourceContext } from '@/hooks/queries/table-analytics'
-/** The real dashboard renderer over seeded mock rows, with a switcher on the title. */
-export function ProtoDashboard({ workspace }: { workspace: Workspace }) {
+const RUNS_ID = 'runs'
+
+interface DashboardOption {
+ id: string
+ title: string
+ kind: 'file' | 'runs' | 'sample'
+}
+
+/**
+ * The project's dashboards: its real dashboard files rendered live, a built-in Runs dashboard
+ * from the workspace's execution stats, and the pack's sample dashboards only when the
+ * workspace has no dashboard file of its own.
+ */
+export function ProtoDashboard({ project }: { project: Project }) {
const [{ dashboard: selected }, setParams] = useQueryStates(protoParsers)
- if (!workspace.dashboards.length)
+ const { dashboardFiles, filesPending } = useProjectResources(realWorkspaceId(project))
+
+ if (filesPending)
+ return
Loading…
+
+ const samples = (suffix: string): DashboardOption[] =>
+ project.mock.dashboards.map((id) => ({
+ id,
+ title: `${MOCK_DASHBOARDS[id]?.title ?? id}${suffix}`,
+ kind: 'sample' as const,
+ }))
+ /** A mock project has only its pack's dashboards; a real one leads with its own files and runs. */
+ const options: DashboardOption[] = project.isMock
+ ? samples('')
+ : [
+ ...dashboardFiles.map((file) => ({ id: file.id, title: file.name, kind: 'file' as const })),
+ { id: RUNS_ID, title: 'Runs', kind: 'runs' },
+ ...(dashboardFiles.length ? [] : samples(' (sample)')),
+ ]
+ if (!options.length)
return (
- No dashboards yet. Ask Sim to build one from this workspace’s tables.
+ No dashboards yet. Ask Sim to build one from this project’s tables.
)
- const id =
- selected && workspace.dashboards.includes(selected) ? selected : workspace.dashboards[0]
- const dashboard = MOCK_DASHBOARDS[id]
- if (!dashboard) throw new Error(`Unknown mock dashboard ${id}`)
+ const current = options.find((option) => option.id === selected) ?? options[0]
const title = (
@@ -37,18 +69,18 @@ export function ProtoDashboard({ workspace }: { workspace: Workspace }) {
type='button'
className='flex items-center gap-2 self-start rounded-lg text-left @min-[1000px]/dashboard:text-[32px] text-[28px] text-[var(--text-primary)] leading-tight tracking-[-0.02em]'
>
- {dashboard.title}
+ {current.title}
- {workspace.dashboards.map((option) => (
+ {options.map((option) => (
void setParams({ dashboard: option as typeof selected })}
+ key={option.id}
+ onSelect={() => void setParams({ dashboard: option.id }, { history: 'replace' })}
>
- {MOCK_DASHBOARDS[option]?.title}
- {option === id && }
+ {option.title}
+ {option.id === current.id && }
))}
@@ -60,13 +92,32 @@ export function ProtoDashboard({ workspace }: { workspace: Workspace }) {
)
+ return
+}
+
+interface DashboardBodyProps {
+ project: Project
+ option: DashboardOption
+ title: ReactNode
+}
+
+function DashboardBody({ project, option, title }: DashboardBodyProps) {
+ const { dashboardFiles } = useProjectResources(realWorkspaceId(project))
+ if (option.kind === 'runs') return
+ if (option.kind === 'file') {
+ const file = dashboardFiles.find((candidate) => candidate.id === option.id)
+ if (!file) return null
+ return
+ }
+ const sample = MOCK_DASHBOARDS[option.id]
+ if (!sample) return null
return (
diff --git a/apps/sim/app/playground/org/components/proto-page.tsx b/apps/sim/app/playground/org/components/proto-page.tsx
index 8edfc0fe4fa..783e88e1363 100644
--- a/apps/sim/app/playground/org/components/proto-page.tsx
+++ b/apps/sim/app/playground/org/components/proto-page.tsx
@@ -1,65 +1,131 @@
'use client'
+import type { ReactNode } from 'react'
import { notFound } from 'next/navigation'
-import { ChatThread } from '@/app/playground/org/components/chat-thread'
+import type { OrganizationSurfaceContext } from '@/lib/organizations/surface'
import { IssuePage } from '@/app/playground/org/components/issue-page'
+import { LiveChat } from '@/app/playground/org/components/live-chat'
import { NewChatHome } from '@/app/playground/org/components/new-chat-home'
import { ProjectShell } from '@/app/playground/org/components/project-shell'
import { WorkspaceView } from '@/app/playground/org/components/workspace-view'
-import { CHATS, issueByKey, WORKSPACES } from '@/app/playground/org/lib/mock-data'
+import { type Project, useProject, useProjects } from '@/app/playground/org/lib/project'
+import { useProjectSources } from '@/app/playground/org/lib/project-sources'
import { parseProtoRoute } from '@/app/playground/org/lib/routes'
+import { useOrganizationMothershipChats } from '@/hooks/queries/mothership-chats'
-/** Client-side router for the prototype's catch-all route. */
-export function ProtoPage({ slug }: { slug?: string[] }) {
+/** Client-side router for the prototype's catch-all and project routes. */
+interface ProtoPageProps {
+ slug?: string[]
+ /** The organization the home page chats in; resolved on the server for the home route only. */
+ organization?: OrganizationSurfaceContext | null
+}
+
+export function ProtoPage({ slug, organization = null }: ProtoPageProps) {
const route = parseProtoRoute(slug)
if (!route) notFound()
switch (route.kind) {
case 'home':
- return
+ return
case 'search':
return
case 'connectors':
return
- case 'chat': {
- const chat = CHATS.find((c) => c.id === route.chatId)
- if (!chat) notFound()
- return (
-
-
-
-
- )
- }
- case 'workspace': {
- const workspace = WORKSPACES.find((w) => w.id === route.workspaceId)
- if (!workspace) notFound()
+ case 'chat':
+ return
+ case 'workspace':
return (
-
-
-
+
+ {(project) => (
+
+
+
+ )}
+
)
- }
- case 'issue': {
- const workspace = WORKSPACES.find((w) => w.id === route.workspaceId)
- const issue = issueByKey(route.issueKey)
- if (!workspace || !issue || issue.workspaceId !== workspace.id) notFound()
+ case 'issue':
return (
-
-
-
+
+ {(project) => }
+
)
- }
}
}
+interface ProjectRouteProps {
+ workspaceId: string
+ children: (project: Project) => ReactNode
+}
+
+/** Resolves the real workspace behind a project route before rendering it. */
+function ProjectRoute({ workspaceId, children }: ProjectRouteProps) {
+ const { project, isPending } = useProject(workspaceId)
+ if (project) return <>{children(project)}>
+ if (isPending) return
+ notFound()
+}
+
+interface IssueRouteProps {
+ project: Project
+ issueKey: string
+}
+
+/** One of the project's issues; the source may still be settling while the workflow lists load. */
+function IssueRoute({ project, issueKey }: IssueRouteProps) {
+ const issue = useProjectSources()
+ .issuesFor(project)
+ .find((candidate) => candidate.key === issueKey)
+ if (!issue) {
+ if (project.overlayPending) return
+ notFound()
+ }
+ return (
+
+
+
+ )
+}
+
+/** An org-wide chat: Sim over the organization instead of one project. */
+function OrgChat({ chatId }: { chatId: string }) {
+ const { projects, isPending } = useProjects()
+ const organizationId = projects.find((project) => project.organizationId)?.organizationId
+ if (!organizationId)
+ return isPending ? (
+
+ ) : (
+
+ )
+ return
+}
+
+function OrgChatView({ organizationId, chatId }: { organizationId: string; chatId: string }) {
+ const { data: chats } = useOrganizationMothershipChats(organizationId)
+ const title = chats?.find((chat) => chat.id === chatId)?.name ?? 'Chat'
+ return (
+
+
+
+
+ )
+}
+
+function Loading({ label }: { label: string }) {
+ return
{label}
+}
+
function Placeholder({ title, body }: { title: string; body: string }) {
return (
diff --git a/apps/sim/app/playground/org/components/proto-shell.tsx b/apps/sim/app/playground/org/components/proto-shell.tsx
index ce894e12233..10bfdf74780 100644
--- a/apps/sim/app/playground/org/components/proto-shell.tsx
+++ b/apps/sim/app/playground/org/components/proto-shell.tsx
@@ -2,12 +2,17 @@
import type { ReactNode } from 'react'
import { ProtoSidebar } from '@/app/playground/org/components/proto-sidebar'
+import { fixtureProjectSources } from '@/app/playground/org/fixtures'
+import { ProjectSourcesProvider } from '@/app/playground/org/lib/project-sources'
import { WorkspaceChrome } from '@/app/workspace/[workspaceId]/components/workspace-chrome'
+/** The playground shell: the org sidebar, with the fixture packs as the project sources. */
export function ProtoShell({ children }: { children: ReactNode }) {
return (
-
- }>{children}
-
+
+
+ }>{children}
+
+
)
}
diff --git a/apps/sim/app/playground/org/components/proto-sidebar.tsx b/apps/sim/app/playground/org/components/proto-sidebar.tsx
index 330be70ac51..15aefa692f0 100644
--- a/apps/sim/app/playground/org/components/proto-sidebar.tsx
+++ b/apps/sim/app/playground/org/components/proto-sidebar.tsx
@@ -2,10 +2,8 @@
import { useRef, useState } from 'react'
import {
- Avatar,
Chip,
ChipLink,
- ChipTag,
chipVariants,
cn,
OverflowText,
@@ -16,28 +14,26 @@ import {
import {
ChevronDown,
ChevronLeft,
- Home,
Integration,
PanelLeft,
Plus,
Search,
- Settings,
+ SquarePen,
} from '@sim/emcn/icons'
+import { formatRelativeTime } from '@sim/utils/formatting'
import Link from 'next/link'
-import { usePathname } from 'next/navigation'
+import { usePathname, useRouter } from 'next/navigation'
import { useQueryStates } from 'nuqs'
import { IdentityTile } from '@/components/identity-tile/identity-tile'
+import { useActiveOrganization, useSession } from '@/lib/auth/auth-client'
+import { DOCS_URL, SLACK_COMMUNITY_URL } from '@/lib/help-links'
+import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
import { RunningDot } from '@/app/playground/org/components/glyphs'
-import { DRAFTS } from '@/app/playground/org/lib/changelog-data'
-import {
- CHATS,
- type Chat,
- ORGANIZATION,
- PEOPLE,
- WORKSPACES,
- type Workspace,
-} from '@/app/playground/org/lib/mock-data'
+import { ProjectRow } from '@/app/playground/org/components/project-row'
+import { type Project, useProjects } from '@/app/playground/org/lib/project'
+import { type ProjectChat, useProjectSources } from '@/app/playground/org/lib/project-sources'
import {
+ BUILD_SECTION_IDS,
fullViewProject,
MAIN_SECTION_IDS,
PROTO_BASE,
@@ -52,9 +48,12 @@ import {
SETTINGS_GROUPS,
SETTINGS_NAV,
} from '@/app/playground/org/lib/settings-nav'
+import { type ProjectDragProps, useProjectOrder } from '@/app/playground/org/lib/use-project-order'
import { useSidebarChrome } from '@/app/workspace/[workspaceId]/components/workspace-chrome'
import {
+ HelpModal,
isNavItemActive,
+ SidebarFooter,
SidebarNavChip,
type SidebarNavItemData,
SidebarSection,
@@ -67,26 +66,77 @@ import {
SIDEBAR_RAIL_CHIP_CLASS,
SIDEBAR_SECTION_GAP_CLASS,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
+import {
+ type MothershipChatMetadata,
+ useMothershipChats,
+ useOrganizationMothershipChats,
+} from '@/hooks/queries/mothership-chats'
import { useSidebarStore } from '@/stores/sidebar/store'
const CHAT_PREVIEW = 3
-const GENERAL_CHATS = CHATS.filter((chat) => !chat.workspaceId)
-const RUNNING_CHAT_IDS = new Set(DRAFTS.flatMap((draft) => draft.running.map((w) => w.chat.id)))
const NAV_ITEMS: SidebarNavItemData[] = [
- { id: 'home', label: 'New chat', icon: Home, href: PROTO_BASE },
+ { id: 'home', label: 'New chat', icon: SquarePen, href: PROTO_BASE },
{ id: 'search', label: 'Search', icon: Search, href: protoRoutes.search },
{ id: 'connectors', label: 'Connectors', icon: Integration, href: protoRoutes.connectors },
]
-/** Mock org rail: Home / Search / Connectors, then projects (each with its chats) and recent chats. */
+/** How the sidebar shows a chat's age: "5m", "2h", "3d". */
+function compactAge(date: Date): string {
+ const relative = formatRelativeTime(date.toISOString())
+ return relative === 'just now' ? 'now' : relative.replace(' ago', '')
+}
+
+/** One shape for a row, whether the chat is a real Sim chat or one from a pack. */
+interface ChatItem {
+ id: string
+ name: string
+ age: string
+ running: boolean
+}
+
+function toChatItems(
+ project: Project,
+ real: MothershipChatMetadata[] | undefined,
+ fromSource: ProjectChat[]
+): ChatItem[] {
+ if (project.isMock)
+ return fromSource.map((chat) => ({
+ id: chat.id,
+ name: chat.title,
+ age: chat.age,
+ running: chat.running,
+ }))
+ return (real ?? []).map((chat) => ({
+ id: chat.id,
+ name: chat.name,
+ age: compactAge(chat.updatedAt),
+ running: chat.isActive,
+ }))
+}
+
+/** Org rail: Home / Search / Connectors, then the real workspaces as projects (each with its chats). */
export function ProtoSidebar() {
const { isCollapsed: railCollapsed, isPeeking } = useSidebarChrome()
const isCollapsed = railCollapsed && !isPeeking
const pathname = usePathname()
const [{ chat: openChat }] = useQueryStates(protoParsers)
- const fullProject = WORKSPACES.find((w) => w.id === fullViewProject(pathname))
+ const { projects, roots } = useProjects()
+ const { projects: orderedProjects, dragProps, isAnyDragActive } = useProjectOrder(roots)
+ const router = useRouter()
+ const [helpOpen, setHelpOpen] = useState(false)
+ const { data: organization } = useActiveOrganization()
+ const { data: session } = useSession()
+ /** Only a mock project has a full view here; a real project's Build is its workspace pages. */
+ const fullProject = projects.find(
+ (project) => project.isMock && project.id === fullViewProject(pathname)
+ )
const inSettings = Boolean(fullProject && settingsProject(pathname))
+ const organizationId = projects.find((project) => project.organizationId)?.organizationId
+ /** The organization's settings; without an organization, the viewer's workspace settings. */
+ const accountSettingsHref = organizationId
+ ? organizationRoutes(organizationId).settingsSection('general')
+ : WORKSPACE_SETTINGS_PATH
const toggleCollapsed = useSidebarStore((state) => state.toggleCollapsed)
const scrollContainerRef = useRef
(null)
const scrollContentRef = useRef(null)
@@ -94,6 +144,8 @@ export function ProtoSidebar() {
contentRef: scrollContentRef,
enabled: !isCollapsed,
})
+ const organizationName = organization?.name ?? 'Organization'
+ const userName = session?.user?.name ?? 'You'
return (
}
+ leftAdornment={}
rightIcon={isCollapsed ? undefined : ChevronDown}
>
- {ORGANIZATION.name}
+ {organizationName}
)}
@@ -182,13 +234,13 @@ export function ProtoSidebar() {
{fullProject && inSettings ? (
) : fullProject ? (
- {WORKSPACES.map((workspace) => (
+ {orderedProjects.map((project) => (
))}
- {GENERAL_CHATS.length > 0 && (
-
-
- {GENERAL_CHATS.map((chat) => (
-
- ))}
-
-
+ />
)}
>
)}
-