Skip to content

Commit d00e347

Browse files
mzxchandraclaude
andcommitted
feat(playground): New chat is the product's org Home plus a project picker; Search matches the org surface
New chat now renders the organization Home from the product, with its Build, Plan and Search switch, suggestions and org chats. The only addition is the project picker: with a project picked, a Build message opens that project's chat. The org Home gains three optional props for this (a per-mode heading and accessory, a before-send hook, and the existing flag that keeps chat URLs in place); its default behavior is unchanged. Search renders the organization Search page, as the org surface does. The separate Assistant page and nav item are removed: the product has no such destination, search is a mode of Home. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f51110a commit d00e347

5 files changed

Lines changed: 166 additions & 17 deletions

File tree

‎apps/sim/app/o/[organizationId]/home/organization-home.tsx‎

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
3+
import { type ReactNode, useCallback, useEffect, useState, useSyncExternalStore } from 'react'
44
import { toast } from '@sim/emcn'
55
import { useQueryClient } from '@tanstack/react-query'
66
import Link from 'next/link'
@@ -52,10 +52,26 @@ import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
5252
import { useOrganizationChatModeStore } from '@/stores/organization-chat-mode/store'
5353
import type { ChatContext } from '@/stores/panel'
5454

55+
/** What a host surface adds to the empty Home for one mode. */
56+
interface OrganizationHomeLanding {
57+
heading?: string
58+
/** Rendered above the composer, such as a picker that scopes the next message. */
59+
accessory?: ReactNode
60+
}
61+
5562
interface OrganizationHomeProps {
5663
userName?: string
5764
chatId?: string
5865
requestMode?: ChatRequestMode
66+
/** Lets a host surface change the empty Home's heading and add a control, per mode. */
67+
landing?: (requestMode: ChatRequestMode) => OrganizationHomeLanding | undefined
68+
/**
69+
* Called with the first message of a new chat before it is sent here; returning true means
70+
* the host sent it elsewhere, so this Home clears the draft and sends nothing.
71+
*/
72+
onBeforeSend?: (message: string, requestMode: ChatRequestMode) => boolean
73+
/** False keeps the address bar where it is when the chat gets its id. */
74+
syncChatUrl?: boolean
5975
}
6076

6177
const subscribeToClient = () => () => {}
@@ -82,6 +98,9 @@ function OrganizationHomeContent({
8298
userName,
8399
chatId,
84100
requestMode: savedMode,
101+
landing,
102+
onBeforeSend,
103+
syncChatUrl,
85104
}: OrganizationHomeProps) {
86105
const { organization, searchAccess, canBuild, mothershipAvailable } = useOrganizationContext()
87106
const { data: session } = useSession()
@@ -114,6 +133,7 @@ function OrganizationHomeContent({
114133
requestMode,
115134
onResourceEvent: controller.onResourceEvent,
116135
activeResourceState: controller.activeResourceState,
136+
...(syncChatUrl === false ? { syncChatUrl } : {}),
117137
})
118138
)
119139
const initialDraftKey = `${userId}:organization:${organization.id}:${chatId ?? 'new'}`
@@ -296,6 +316,11 @@ function OrganizationHomeContent({
296316
path: file.path,
297317
}))
298318
if (!message && !attachments.length) return
319+
if (!hasChat && message && onBeforeSend?.(message, requestMode)) {
320+
useMothershipDraftsStore.getState().clearDraft(draftKey)
321+
files.clearAttachedFiles()
322+
return
323+
}
299324
useMothershipDraftsStore.getState().clearDraft(draftKey)
300325
send(message, attachments.length ? attachments : undefined, contexts)
301326
setRestoredContexts([])
@@ -334,6 +359,7 @@ function OrganizationHomeContent({
334359
/>
335360
)
336361

362+
const hostLanding = hasChat ? undefined : landing?.(requestMode)
337363
const content = (
338364
<div className='flex h-full min-h-0 min-w-[min(480px,100%)] flex-1 flex-col bg-[var(--bg)]'>
339365
{hasChat ? (
@@ -399,13 +425,15 @@ function OrganizationHomeContent({
399425
) : (
400426
<OrganizationLanding
401427
heading={
402-
requestMode === 'assistant'
428+
hostLanding?.heading ??
429+
(requestMode === 'assistant'
403430
? `Search ${organization.name}`
404431
: requestMode === 'plan'
405432
? `What should we understand and plan${firstName ? `, ${firstName}` : ''}?`
406-
: `What should we get done${firstName ? `, ${firstName}` : ''}?`
433+
: `What should we get done${firstName ? `, ${firstName}` : ''}?`)
407434
}
408435
>
436+
{hostLanding?.accessory}
409437
{composer}
410438
<div className='absolute inset-x-0 top-full'>
411439
{requestMode === 'agent' ? (
Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
11
import { Suspense } from 'react'
2+
import { getSession } from '@/lib/auth'
3+
import { getActiveOrganizationId } from '@/lib/auth/session-response'
4+
import {
5+
getOrganizationSurfaceContext,
6+
type OrganizationSurfaceContext,
7+
resolveOrganizationLanding,
8+
} from '@/lib/organizations/surface'
29
import { ProtoPage } from '@/app/playground/org/components/proto-page'
310

11+
/** The organization the home page chats in, resolved the way the app entry picks one. */
12+
async function resolveHomeOrganization(): Promise<OrganizationSurfaceContext | null> {
13+
const session = await getSession()
14+
if (!session?.user) return null
15+
const organizationId = await resolveOrganizationLanding(
16+
session.user.id,
17+
getActiveOrganizationId(session)
18+
)
19+
return organizationId ? getOrganizationSurfaceContext(organizationId, session.user.id) : null
20+
}
21+
422
export default async function OrgPrototypePage({
523
params,
624
}: {
725
params: Promise<{ slug?: string[] }>
826
}) {
927
const { slug } = await params
28+
const organization = slug?.length ? null : await resolveHomeOrganization()
1029
return (
1130
<Suspense
1231
fallback={<p className='p-6 text-[var(--text-muted)] text-caption'>Loading prototype…</p>}
1332
>
14-
<ProtoPage slug={slug} />
33+
<ProtoPage slug={slug} organization={organization} />
1534
</Suspense>
1635
)
1736
}

‎apps/sim/app/playground/org/components/new-chat-home.tsx‎

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,27 @@ import { Check, ChevronDown, Folder, Plus, X } from '@sim/emcn/icons'
1616
import { useRouter } from 'next/navigation'
1717
import { useQueryStates } from 'nuqs'
1818
import { useSession } from '@/lib/auth/auth-client'
19+
import type { OrganizationSurfaceContext } from '@/lib/organizations/surface'
20+
import { OrganizationHome } from '@/app/o/[organizationId]/home/organization-home'
21+
import { OrganizationProvider } from '@/app/o/[organizationId]/providers/organization-provider'
1922
import { type Project, useProjects } from '@/app/playground/org/lib/project'
2023
import { protoRoutes } from '@/app/playground/org/lib/routes'
2124
import { protoParsers } from '@/app/playground/org/lib/search-params'
2225
import { ChatSurfaceProvider, UserInput } from '@/app/workspace/[workspaceId]/home/components'
2326

2427
const NO_PROJECT = 'none'
2528

26-
/** New chat, Codex-style: pick the project in the heading or on the composer, then ask. */
27-
export function NewChatHome() {
29+
interface NewChatHomeProps {
30+
/** The organization to chat in; without one, only project chats can start here. */
31+
organization: OrganizationSurfaceContext | null
32+
}
33+
34+
/**
35+
* New chat: the organization Home from the product (Build, Plan and Search), plus a project
36+
* picker. Build with a project picked opens that project's chat; everything else is the
37+
* organization chat exactly as the product runs it.
38+
*/
39+
export function NewChatHome({ organization }: NewChatHomeProps) {
2840
const router = useRouter()
2941
const { data: session } = useSession()
3042
const { roots: projects } = useProjects()
@@ -35,6 +47,49 @@ export function NewChatHome() {
3547
const select = (id: string) => void setParams({ project: id })
3648
const firstName = session?.user?.name?.split(' ')[0]
3749

50+
/** Hands a build message to the picked project's chat; false leaves it to the org Home. */
51+
const openProjectChat = (message: string): boolean => {
52+
if (!selected) return false
53+
router.push(
54+
selected.isMock
55+
? `${protoRoutes.workspace(selected.id)}?chat=new`
56+
: `${protoRoutes.workspace(selected.id)}?chat=new&q=${encodeURIComponent(message)}`
57+
)
58+
return true
59+
}
60+
61+
if (organization?.mothershipAvailable) {
62+
return (
63+
<OrganizationProvider context={organization}>
64+
<OrganizationHome
65+
userName={session?.user?.name ?? undefined}
66+
syncChatUrl={false}
67+
landing={(mode) =>
68+
mode === 'agent'
69+
? {
70+
heading: selected ? `What should we build in ${selected.name}?` : undefined,
71+
accessory: (
72+
<div className='mb-2 flex items-center gap-1'>
73+
<ProjectMenu
74+
projects={projects}
75+
value={selected?.id ?? NO_PROJECT}
76+
onSelect={select}
77+
>
78+
<Chip leftIcon={selected ? Folder : X} rightIcon={ChevronDown}>
79+
{selected ? selected.name : 'No project'}
80+
</Chip>
81+
</ProjectMenu>
82+
</div>
83+
),
84+
}
85+
: undefined
86+
}
87+
onBeforeSend={(message, mode) => mode === 'agent' && openProjectChat(message)}
88+
/>
89+
</OrganizationProvider>
90+
)
91+
}
92+
3893
return (
3994
<div className='flex h-full flex-col items-center justify-center px-6 pt-[2vh] pb-[18vh]'>
4095
<h1 className='mb-8 max-w-chat text-balance text-center font-season text-[26px] text-[var(--text-primary)] leading-[1.15] tracking-[-0.01em] sm:text-[28px]'>
@@ -72,15 +127,8 @@ export function NewChatHome() {
72127
onSubmit={(text) => {
73128
const message = text.trim()
74129
if (!message) return
75-
if (selected?.isMock) {
76-
router.push(`${protoRoutes.workspace(selected.id)}?chat=new`)
77-
} else if (selected) {
78-
router.push(
79-
`${protoRoutes.workspace(selected.id)}?chat=new&q=${encodeURIComponent(message)}`
80-
)
81-
} else {
82-
toast.success('Org-wide chats open from the sidebar for now')
83-
}
130+
if (!openProjectChat(message))
131+
toast.success('Org-wide chats need an organization with Sim Chat')
84132
}}
85133
/>
86134
</ChatSurfaceProvider>

‎apps/sim/app/playground/org/components/proto-page.tsx‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import type { ReactNode } from 'react'
44
import { notFound } from 'next/navigation'
5+
import type { OrganizationSurfaceContext } from '@/lib/organizations/surface'
56
import { IssuePage } from '@/app/playground/org/components/issue-page'
67
import { LiveChat } from '@/app/playground/org/components/live-chat'
78
import { NewChatHome } from '@/app/playground/org/components/new-chat-home'
@@ -13,13 +14,19 @@ import { parseProtoRoute } from '@/app/playground/org/lib/routes'
1314
import { useOrganizationMothershipChats } from '@/hooks/queries/mothership-chats'
1415

1516
/** Client-side router for the prototype's catch-all and project routes. */
16-
export function ProtoPage({ slug }: { slug?: string[] }) {
17+
interface ProtoPageProps {
18+
slug?: string[]
19+
/** The organization the home page chats in; resolved on the server for the home route only. */
20+
organization?: OrganizationSurfaceContext | null
21+
}
22+
23+
export function ProtoPage({ slug, organization = null }: ProtoPageProps) {
1724
const route = parseProtoRoute(slug)
1825
if (!route) notFound()
1926

2027
switch (route.kind) {
2128
case 'home':
22-
return <NewChatHome />
29+
return <NewChatHome organization={organization} />
2330
case 'search':
2431
return <Placeholder title='Search' body='Org-wide search stays as it is today.' />
2532
case 'connectors':
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Suspense } from 'react'
2+
import { redirect } from 'next/navigation'
3+
import { getSession } from '@/lib/auth'
4+
import { getActiveOrganizationId } from '@/lib/auth/session-response'
5+
import {
6+
getOrganizationSurfaceContext,
7+
resolveOrganizationLanding,
8+
} from '@/lib/organizations/surface'
9+
import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect'
10+
import { OrganizationProvider } from '@/app/o/[organizationId]/providers/organization-provider'
11+
import { OrganizationSearch } from '@/app/o/[organizationId]/search/search'
12+
import { protoRoutes } from '@/app/playground/org/lib/routes'
13+
14+
/** Renders the authorized organization search surface inside the playground rail. */
15+
export default async function SearchPage() {
16+
const session = await getSession()
17+
if (!session?.user)
18+
redirect(buildAuthCrossLink('/login', { callbackUrl: protoRoutes.search, isInviteFlow: false }))
19+
/** The session's active organization, else the first the viewer belongs to, as the app entry does. */
20+
const organizationId = await resolveOrganizationLanding(
21+
session.user.id,
22+
getActiveOrganizationId(session)
23+
)
24+
const context = organizationId
25+
? await getOrganizationSurfaceContext(organizationId, session.user.id)
26+
: null
27+
if (!context?.searchAccess.memberScoped)
28+
return (
29+
<div className='flex h-full flex-col items-center justify-center gap-2'>
30+
<h1 className='text-[20px] text-[var(--text-primary)]'>Search</h1>
31+
<p className='text-[var(--text-muted)] text-small'>
32+
{organizationId
33+
? 'Sim Search is not enabled for this organization.'
34+
: 'You are not a member of an organization.'}
35+
</p>
36+
</div>
37+
)
38+
return (
39+
<OrganizationProvider context={context}>
40+
<Suspense
41+
fallback={<p className='p-6 text-[var(--text-muted)] text-caption'>Loading search…</p>}
42+
>
43+
<OrganizationSearch userId={session.user.id} />
44+
</Suspense>
45+
</OrganizationProvider>
46+
)
47+
}

0 commit comments

Comments
 (0)