Skip to content

Commit 72407ee

Browse files
Sg312Vikhyath Mondreti
authored andcommitted
Add private knowledge graph selection in settings
1 parent 614e343 commit 72407ee

32 files changed

Lines changed: 1138 additions & 233 deletions

File tree

‎apps/sim/app/api/mothership/chats/route.ts‎

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { db } from '@sim/db'
2-
import { copilotChats } from '@sim/db/schema'
31
import { createLogger } from '@sim/logger'
42
import { type NextRequest, NextResponse } from 'next/server'
53
import {
@@ -9,6 +7,7 @@ import {
97
import { parseRequest } from '@/lib/api/server'
108
import { asOrchestrationError } from '@/lib/core/orchestration/types'
119
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
10+
import { createWorkspaceChat } from '@/lib/mothership/chat/application/create-workspace-chat'
1211
import {
1312
ChatWorkspaceAccessError,
1413
listWorkspaceChats,
@@ -17,19 +16,14 @@ import {
1716
createOrganizationChat,
1817
listOrganizationChats,
1918
} from '@/lib/mothership/chat/organization-chats'
20-
import { chatPubSub } from '@/lib/mothership/chat-status'
21-
import { MOTHERSHIP_CHAT_DEFAULT_MODEL } from '@/lib/mothership/constants'
2219
import {
2320
authenticateCopilotRequestSessionOnly,
2421
createForbiddenResponse,
2522
createInternalServerErrorResponse,
2623
createUnauthorizedResponse,
2724
} from '@/lib/mothership/request/http'
2825
import { captureServerEvent } from '@/lib/posthog/server'
29-
import {
30-
assertActiveWorkspaceAccess,
31-
isWorkspaceAccessDeniedError,
32-
} from '@/lib/workspaces/permissions/utils'
26+
import { isWorkspaceAccessDeniedError } from '@/lib/workspaces/permissions/utils'
3327

3428
const logger = createLogger('MothershipChatsAPI')
3529

@@ -101,23 +95,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
10195
}
10296

10397
if (!workspaceId) throw new Error('Conversation owner is required')
104-
await assertActiveWorkspaceAccess(workspaceId, userId)
105-
106-
const now = new Date()
107-
const [chat] = await db
108-
.insert(copilotChats)
109-
.values({
110-
userId,
111-
workspaceId,
112-
type: 'mothership',
113-
title: null,
114-
model: MOTHERSHIP_CHAT_DEFAULT_MODEL,
115-
updatedAt: now,
116-
lastSeenAt: now,
117-
})
118-
.returning({ id: copilotChats.id })
119-
120-
chatPubSub?.publishStatusChanged({ workspaceId, chatId: chat.id, type: 'created' })
98+
if (!principal) return createUnauthorizedResponse()
99+
const chat = await createWorkspaceChat.execute({ principal, input: { workspaceId, mode } })
121100

122101
captureServerEvent(
123102
userId,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { selectMemorySpaceContract } from '@/lib/api/contracts/memory-spaces'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import {
9+
memorySpaceOperations,
10+
selectMemorySpace,
11+
} from '@/lib/mothership/memory/application/spaces'
12+
13+
export const PUT = defineInternalJsonRoute({
14+
contract: selectMemorySpaceContract,
15+
auth: internalSessionAuth,
16+
operation: memorySpaceOperations.select,
17+
rateLimit: internalRateLimits.user({ bucketName: 'memory-spaces' }),
18+
errorPolicy: internalOrchestrationErrorPolicy,
19+
mapInput: ({ params, body }) => ({ organizationId: params.id, ...body }),
20+
useCase: selectMemorySpace,
21+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {
2+
createMemorySpaceContract,
3+
listMemorySpacesContract,
4+
} from '@/lib/api/contracts/memory-spaces'
5+
import {
6+
defineInternalJsonRoute,
7+
internalOrchestrationErrorPolicy,
8+
internalRateLimits,
9+
internalSessionAuth,
10+
} from '@/lib/api/server/routes'
11+
import {
12+
createMemorySpace,
13+
listMemorySpaces,
14+
memorySpaceOperations,
15+
} from '@/lib/mothership/memory/application/spaces'
16+
export const GET = defineInternalJsonRoute({
17+
contract: listMemorySpacesContract,
18+
auth: internalSessionAuth,
19+
operation: memorySpaceOperations.list,
20+
rateLimit: internalRateLimits.user({ bucketName: 'memory-spaces' }),
21+
errorPolicy: internalOrchestrationErrorPolicy,
22+
mapInput: ({ params }) => ({ organizationId: params.id }),
23+
useCase: listMemorySpaces,
24+
})
25+
26+
export const POST = defineInternalJsonRoute({
27+
contract: createMemorySpaceContract,
28+
auth: internalSessionAuth,
29+
operation: memorySpaceOperations.create,
30+
rateLimit: internalRateLimits.user({ bucketName: 'memory-spaces' }),
31+
errorPolicy: internalOrchestrationErrorPolicy,
32+
mapInput: ({ params, body }) => ({ organizationId: params.id, ...body }),
33+
useCase: createMemorySpace,
34+
})

‎apps/sim/app/o/[organizationId]/layout.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import { getSession } from '@/lib/auth'
66
import { getActiveOrganizationId } from '@/lib/auth/session-response'
77
import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag'
88
import { isBenchmarkEnabled } from '@/lib/benchmarks/config'
9-
import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags'
9+
import {
10+
isMemorySpacesEnabled,
11+
isMothershipModelSelectorEnabled,
12+
isPlanModeEnabled,
13+
} from '@/lib/mothership/feature-flags'
1014
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
1115
import { getOrganizationSurfaceContext } from '@/lib/organizations/surface'
1216
import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability'
@@ -57,7 +61,7 @@ export default async function OrganizationLayout({
5761
if (!context.mothershipAvailable && !context.searchAccess.memberScoped)
5862
redirect(WORKSPACE_SETTINGS_PATH)
5963

60-
const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled, dashboardsEnabled] =
64+
const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled, dashboardsEnabled, memorySpacesEnabled] =
6165
await Promise.all([
6266
prefetchOrganizationSidebar(
6367
queryClient,
@@ -69,6 +73,7 @@ export default async function OrganizationLayout({
6973
isMothershipModelSelectorEnabled(),
7074
isPlanModeEnabled(),
7175
isDashboardsEnabled(organizationId),
76+
isMemorySpacesEnabled(),
7277
])
7378
const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1'
7479

@@ -81,6 +86,7 @@ export default async function OrganizationLayout({
8186
'mothership-model-selector': modelSelectorEnabled,
8287
'mothership-plan-mode': planModeEnabled,
8388
'mothership-desktop-background-executor': false,
89+
'mothership-memory-spaces': memorySpacesEnabled,
8490
}}
8591
>
8692
<OrganizationProvider context={context}>

‎apps/sim/app/o/[organizationId]/settings/[section]/settings.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
import { SettingsSectionProvider } from '@/components/settings/settings-panel'
1010
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
1111

12+
const KnowledgeGraphs = dynamic(() =>
13+
import('@/app/o/[organizationId]/settings/components/knowledge-graphs').then(
14+
(m) => m.KnowledgeGraphs
15+
)
16+
)
17+
1218
const OrganizationRecentlyDeleted = dynamic(() =>
1319
import('@/app/o/[organizationId]/settings/components/organization-recently-deleted').then(
1420
(m) => m.OrganizationRecentlyDeleted
@@ -90,6 +96,7 @@ export function OrganizationSettings({ section }: OrganizationSettingsProps) {
9096
{section === 'recently-deleted' && (
9197
<OrganizationRecentlyDeleted key={organizationId} organizationId={organizationId} />
9298
)}
99+
{section === 'knowledge-graphs' && <KnowledgeGraphs organizationId={organizationId} />}
93100
{section === 'integrations' && <OrganizationIntegrationsSettings />}
94101
{section === 'connected-accounts' && (
95102
<OrganizationConnectedAccounts organizationId={organizationId} />
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import {
5+
Chip,
6+
ChipInput,
7+
ChipModal,
8+
ChipModalBody,
9+
ChipModalError,
10+
ChipModalField,
11+
ChipModalFooter,
12+
ChipModalHeader,
13+
ChipTag,
14+
toast,
15+
} from '@sim/emcn'
16+
import { Database, Plus } from '@sim/emcn/icons'
17+
import { SettingsPanel } from '@/components/settings/settings-panel'
18+
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
19+
import {
20+
RESOURCE_LIST_STACK,
21+
SettingsResourceRow,
22+
} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
23+
import {
24+
useCreateMemorySpace,
25+
useMemorySpaces,
26+
useSelectMemorySpace,
27+
} from '@/hooks/queries/memory-spaces'
28+
29+
interface KnowledgeGraphsProps {
30+
organizationId: string
31+
}
32+
33+
export function KnowledgeGraphs({ organizationId }: KnowledgeGraphsProps) {
34+
const { data, isLoading, error } = useMemorySpaces(organizationId)
35+
const select = useSelectMemorySpace(organizationId)
36+
const [creating, setCreating] = useState(false)
37+
return (
38+
<SettingsPanel
39+
actions={[
40+
{
41+
text: 'New graph',
42+
icon: Plus,
43+
onSelect: () => setCreating(true),
44+
disabled: isLoading || !!error || select.isPending,
45+
},
46+
]}
47+
>
48+
{error ? (
49+
<SettingsEmptyState tone='error'>{error.message}</SettingsEmptyState>
50+
) : isLoading ? (
51+
<SettingsEmptyState>Loading knowledge graphs...</SettingsEmptyState>
52+
) : (
53+
<div className={RESOURCE_LIST_STACK}>
54+
{data?.spaces.map((space) => (
55+
<SettingsResourceRow
56+
key={space.id ?? 'default'}
57+
icon={<Database />}
58+
title={space.name}
59+
description={
60+
space.id === null
61+
? 'Your original knowledge graph, preserved.'
62+
: 'Private to you in this organization.'
63+
}
64+
trailing={
65+
space.id === data.activeSpaceId ? (
66+
<ChipTag>Active</ChipTag>
67+
) : (
68+
<Chip
69+
disabled={select.isPending || creating}
70+
onClick={() =>
71+
select.mutate(
72+
{ spaceId: space.id },
73+
{ onError: (failure) => toast.error(failure.message) }
74+
)
75+
}
76+
>
77+
Make active
78+
</Chip>
79+
)
80+
}
81+
/>
82+
))}
83+
</div>
84+
)}
85+
{creating && (
86+
<CreateKnowledgeGraph organizationId={organizationId} onClose={() => setCreating(false)} />
87+
)}
88+
</SettingsPanel>
89+
)
90+
}
91+
92+
interface CreateKnowledgeGraphProps extends KnowledgeGraphsProps {
93+
onClose: () => void
94+
}
95+
function CreateKnowledgeGraph({ organizationId, onClose }: CreateKnowledgeGraphProps) {
96+
const create = useCreateMemorySpace(organizationId)
97+
const [name, setName] = useState('')
98+
const submit = () => create.mutate({ name: name.trim() }, { onSuccess: onClose })
99+
return (
100+
<ChipModal
101+
open
102+
onOpenChange={(open) => !open && onClose()}
103+
dismissDisabled={create.isPending}
104+
srTitle='New knowledge graph'
105+
>
106+
<ChipModalHeader onClose={onClose}>New knowledge graph</ChipModalHeader>
107+
<ChipModalBody>
108+
<ChipModalField
109+
type='custom'
110+
title='Name'
111+
hint='Creates an empty graph and makes it active for new chats. Your other graphs are kept.'
112+
>
113+
<ChipInput
114+
aria-label='Graph name'
115+
placeholder='e.g. Fresh exploration'
116+
value={name}
117+
maxLength={100}
118+
disabled={create.isPending}
119+
onChange={(event) => setName(event.target.value)}
120+
/>
121+
</ChipModalField>
122+
<ChipModalError>{create.error?.message}</ChipModalError>
123+
</ChipModalBody>
124+
<ChipModalFooter
125+
onCancel={onClose}
126+
primaryAction={{
127+
label: create.isPending ? 'Creating...' : 'Create and activate',
128+
onClick: submit,
129+
disabled: create.isPending || !name.trim(),
130+
}}
131+
/>
132+
</ChipModal>
133+
)
134+
}

‎apps/sim/app/o/[organizationId]/settings/navigation.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ export function resolveOrganizationSurfaceHeaderMeta(path: string): SettingsHead
8282
export function organizationSettingsNavigation(
8383
isAdmin: boolean,
8484
features: OrganizationSettingsFeatures,
85-
availability: { connectedAccounts: boolean; search: boolean }
85+
availability: { connectedAccounts: boolean; search: boolean; knowledgeGraphs?: boolean }
8686
) {
8787
return ORGANIZATION_SETTINGS_ITEMS.filter(
8888
(item) =>
89+
(item.id !== 'knowledge-graphs' || availability.knowledgeGraphs === true) &&
8990
(item.id !== 'connected-accounts' || availability.connectedAccounts) &&
9091
((item.id !== 'search-mcp' && item.id !== 'search-slack' && item.id !== 'integrations') ||
9192
availability.search) &&
@@ -107,7 +108,7 @@ export function organizationSettingsNavigation(
107108
export function organizationSurfaceSettingsNavigation(
108109
isAdmin: boolean,
109110
features: OrganizationSettingsFeatures,
110-
availability: { connectedAccounts: boolean; search: boolean }
111+
availability: { connectedAccounts: boolean; search: boolean; knowledgeGraphs?: boolean }
111112
): SettingsNavigationItem<AccountSettingsSection | OrganizationSettingsSection>[] {
112113
return [
113114
...ORGANIZATION_SURFACE_ACCOUNT_ITEMS,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
resolveOrganizationSurfaceSection,
1818
} from '@/app/o/[organizationId]/settings/navigation'
1919
import { warmOrganizationSettingsSectionQuery } from '@/app/o/[organizationId]/settings/settings-query-warmers'
20+
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
2021
import { useOrganizationBillingSummary } from '@/hooks/queries/organization-billing-summary'
2122

2223
interface OrganizationSettingsSidebarProps {
@@ -27,6 +28,7 @@ interface OrganizationSettingsSidebarProps {
2728
export function OrganizationSettingsSidebar(props: OrganizationSettingsSidebarProps) {
2829
const { organization, viewer, connectedAccountsAvailable, searchAccess, settingsFeatures } =
2930
useOrganizationContext()
31+
const knowledgeGraphs = useFeatureFlag('mothership-memory-spaces')
3032
const pathname = usePathname()
3133
const queryClient = useQueryClient()
3234
const refreshPlan = viewer.isAdmin && settingsFeatures.hosted && settingsFeatures.billingEnabled
@@ -68,6 +70,7 @@ export function OrganizationSettingsSidebar(props: OrganizationSettingsSidebarPr
6870
items={organizationSurfaceSettingsNavigation(isAdmin, features, {
6971
connectedAccounts: connectedAccountsAvailable,
7072
search: searchAccess.memberScoped,
73+
knowledgeGraphs,
7174
})}
7275
hrefForSection={(section) => routes.settingsSection(section)}
7376
onSectionIntent={(section) =>

‎apps/sim/app/workspace/[workspaceId]/providers/feature-flags-provider.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface WorkspaceFeatureFlags {
88
'mothership-model-selector': boolean
99
'mothership-plan-mode': boolean
1010
'mothership-desktop-background-executor': boolean
11+
'mothership-memory-spaces'?: boolean
1112
}
1213

1314
const FeatureFlagsContext = createContext<WorkspaceFeatureFlags | null>(null)
@@ -26,5 +27,5 @@ export function FeatureFlagsProvider({ children, flags }: FeatureFlagsProviderPr
2627
export function useFeatureFlag(name: keyof WorkspaceFeatureFlags): boolean {
2728
const flags = useContext(FeatureFlagsContext)
2829
if (!flags) throw new Error('useFeatureFlag must be used within FeatureFlagsProvider')
29-
return flags[name]
30+
return flags[name] ?? false
3031
}

0 commit comments

Comments
 (0)