feat(rename): complete searchSpace to workspace transition across frontend and backend

- Introduced a comprehensive specification for renaming `searchSpace` to `workspace` in `surfsense_web` and `surfsense_desktop`, ensuring all TypeScript identifiers, React props, and local data structures are updated.
- Implemented migration shims for persisted local state to prevent data loss during the transition.
- Updated observability metrics and IPC channels to reflect the new naming convention.
- Removed legacy `active-search-space` module and replaced it with `active-workspace` to maintain consistency.
- Ensured no behavioral changes or data loss for users during the renaming process.
This commit is contained in:
Anish Sarkar 2026-07-06 15:12:40 +05:30
parent 2a020629c5
commit a8c1fb660d
259 changed files with 5480 additions and 2285 deletions

View file

@ -491,7 +491,7 @@ export const AssistantMessage: FC = () => {
const commentPanelRef = useRef<HTMLDivElement>(null);
const commentTriggerRef = useRef<HTMLButtonElement>(null);
const messageId = useAuiState(({ message }) => message?.id);
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const dbMessageId = parseMessageId(messageId);
const commentsEnabled = useAtomValue(commentsEnabledAtom);
@ -520,7 +520,7 @@ export const AssistantMessage: FC = () => {
const commentCount = commentsData?.total_count ?? 0;
const hasComments = commentCount > 0;
const showCommentTrigger = searchSpaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
const showCommentTrigger = workspaceId && commentsEnabled && !isMessageStreaming && dbMessageId;
// Close floating panel when clicking outside (but not on portaled popover/dropdown content)
useEffect(() => {

View file

@ -36,10 +36,10 @@ interface ConnectorIndicatorProps {
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
(_props, ref) => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
// Real-time document type counts via Zero (updates instantly as docs are indexed)
const documentTypeCounts = useZeroDocumentTypeCounts(searchSpaceId);
const documentTypeCounts = useZeroDocumentTypeCounts(workspaceId);
// Read status inbox items from shared atom (populated by LayoutDataProvider)
// instead of creating a duplicate useInbox("status") hook.
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
@ -124,7 +124,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
loading: connectorsLoading,
error: connectorsError,
refreshConnectors: refreshConnectorsSync,
} = useConnectorsSync(searchSpaceId);
} = useConnectorsSync(workspaceId);
const useSyncData = connectorsFromSync.length > 0 || (connectorsLoading && !connectorsError);
const connectors = useSyncData ? connectorsFromSync : allConnectors || [];
@ -142,7 +142,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
inboxItems
);
// Get document types that have documents in the search space
// Get document types that have documents in the workspace
const activeDocumentTypes = documentTypeCounts
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
: [];
@ -163,7 +163,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
open: () => handleOpenChange(true),
}));
if (!searchSpaceId) return null;
if (!workspaceId) return null;
return (
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
@ -191,8 +191,8 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
>
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
{isYouTubeView && workspaceId ? (
<YouTubeCrawlerView workspaceId={workspaceId} onBack={handleBackFromYouTube} />
) : viewingMCPList ? (
<ConnectorAccountsListView
connectorType="MCP_CONNECTOR"
@ -253,7 +253,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
isSaving={isSaving}
isDisconnecting={isDisconnecting}
isIndexing={indexingConnectorIds.has(editingConnector.id)}
searchSpaceId={searchSpaceId?.toString()}
workspaceId={workspaceId?.toString()}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
@ -346,7 +346,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
<TabsContent value="all" className="m-0">
<AllConnectorsTab
searchQuery={searchQuery}
searchSpaceId={searchSpaceId}
workspaceId={workspaceId}
connectedTypes={connectedTypes}
connectingId={connectingId}
allConnectors={connectors}

View file

@ -159,17 +159,17 @@ export const ObsidianConnectForm: FC<ConnectFormProps> = ({ onBack }) => {
<div className="h-px bg-border/60" />
{/* Step 4 — Pick search space */}
{/* Step 4 — Pick workspace */}
<article>
<header className="mb-3 flex items-center gap-2">
<div className="flex size-7 items-center justify-center rounded-md border border-slate-400/30 text-xs font-medium">
4
</div>
<h3 className="text-sm font-medium sm:text-base">Pick this search space</h3>
<h3 className="text-sm font-medium sm:text-base">Pick this workspace</h3>
</header>
<p className="text-[11px] text-muted-foreground sm:text-xs">
In the plugin's <span className="font-medium">Search space</span> setting, choose the
search space you want this vault to sync into. The connector will appear here
workspace you want this vault to sync into. The connector will appear here
automatically once the plugin makes its first sync.
</p>
</article>

View file

@ -7,7 +7,7 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
LINEAR_CONNECTOR: [
"Search through all your Linear issues and comments",
"Access issue titles, descriptions, and full discussion threads",
"Connect your team's project management directly to your search space",
"Connect your team's project management directly to your workspace",
"Keep your search results up-to-date with latest Linear content",
"Index your Linear issues for enhanced search capabilities",
],
@ -36,63 +36,63 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
SLACK_CONNECTOR: [
"Search through all your Slack messages and conversations",
"Access messages from public and private channels",
"Connect your team's communications directly to your search space",
"Connect your team's communications directly to your workspace",
"Keep your search results up-to-date with latest Slack content",
"Index your Slack conversations for enhanced search capabilities",
],
DISCORD_CONNECTOR: [
"Search through all your Discord messages and conversations",
"Access messages from all accessible channels",
"Connect your community's communications directly to your search space",
"Connect your community's communications directly to your workspace",
"Keep your search results up-to-date with latest Discord content",
"Index your Discord conversations for enhanced search capabilities",
],
NOTION_CONNECTOR: [
"Search through all your Notion pages and databases",
"Access page content, properties, and metadata",
"Connect your knowledge base directly to your search space",
"Connect your knowledge base directly to your workspace",
"Keep your search results up-to-date with latest Notion content",
"Index your Notion workspace for enhanced search capabilities",
],
CONFLUENCE_CONNECTOR: [
"Search through all your Confluence pages and spaces",
"Access page content, comments, and attachments",
"Connect your team's documentation directly to your search space",
"Connect your team's documentation directly to your workspace",
"Keep your search results up-to-date with latest Confluence content",
"Index your Confluence workspace for enhanced search capabilities",
],
BOOKSTACK_CONNECTOR: [
"Search through all your BookStack pages and books",
"Access page content, chapters, and documentation",
"Connect your documentation directly to your search space",
"Connect your documentation directly to your workspace",
"Keep your search results up-to-date with latest BookStack content",
"Index your BookStack instance for enhanced search capabilities",
],
GITHUB_CONNECTOR: [
"Search through code, issues, and documentation from GitHub repositories",
"Access repository content, pull requests, and discussions",
"Connect your codebase directly to your search space",
"Connect your codebase directly to your workspace",
"Keep your search results up-to-date with latest GitHub content",
"Index your GitHub repositories for enhanced search capabilities",
],
JIRA_CONNECTOR: [
"Search through all your Jira issues and tickets",
"Access issue descriptions, comments, and project data",
"Connect your project management directly to your search space",
"Connect your project management directly to your workspace",
"Keep your search results up-to-date with latest Jira content",
"Index your Jira projects for enhanced search capabilities",
],
CLICKUP_CONNECTOR: [
"Search through all your ClickUp tasks and projects",
"Access task descriptions, comments, and project data",
"Connect your task management directly to your search space",
"Connect your task management directly to your workspace",
"Keep your search results up-to-date with latest ClickUp content",
"Index your ClickUp workspace for enhanced search capabilities",
],
LUMA_CONNECTOR: [
"Search through all your Luma events",
"Access event details, descriptions, and attendee information",
"Connect your events directly to your search space",
"Connect your events directly to your workspace",
"Keep your search results up-to-date with latest Luma content",
"Index your Luma events for enhanced search capabilities",
],

View file

@ -165,7 +165,7 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({ connector, onNameC
<AlertDescription>
Configure this URL in Circleback Settings Automations Create automation Send
webhook request. The webhook will automatically send meeting notes, transcripts, and
action items to this search space.
action items to this workspace.
</AlertDescription>
</Alert>
)}

View file

@ -8,7 +8,7 @@ export interface ConnectorConfigProps {
connector: SearchSourceConnector;
onConfigChange?: (config: Record<string, unknown>) => void;
onNameChange?: (name: string) => void;
searchSpaceId?: string;
workspaceId?: string;
}
export type ConnectorConfigComponent = FC<ConnectorConfigProps>;

View file

@ -41,7 +41,7 @@ interface ConnectorEditViewProps {
isSaving: boolean;
isDisconnecting: boolean;
isIndexing?: boolean;
searchSpaceId?: string;
workspaceId?: string;
onStartDateChange: (date: Date | undefined) => void;
onEndDateChange: (date: Date | undefined) => void;
onPeriodicEnabledChange: (enabled: boolean) => void;
@ -65,7 +65,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
isSaving,
isDisconnecting,
isIndexing = false,
searchSpaceId,
workspaceId,
onStartDateChange,
onEndDateChange,
onPeriodicEnabledChange,
@ -78,7 +78,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
onConfigChange,
onNameChange,
}) => {
const searchSpaceIdAtom = useAtomValue(activeWorkspaceIdAtom);
const workspaceIdAtom = useAtomValue(activeWorkspaceIdAtom);
const isAuthExpired = connector.config?.auth_expired === true;
const reauthEndpoint = getReauthEndpoint(connector);
const [reauthing, setReauthing] = useState(false);
@ -91,7 +91,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
(connector.is_indexable || connector.connector_type === EnumConnectorName.OBSIDIAN_CONNECTOR);
const handleReauth = useCallback(async () => {
const spaceId = searchSpaceId ?? searchSpaceIdAtom;
const spaceId = workspaceId ?? workspaceIdAtom;
if (!spaceId || !reauthEndpoint) return;
setReauthing(true);
try {
@ -119,7 +119,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
} finally {
setReauthing(false);
}
}, [searchSpaceId, searchSpaceIdAtom, reauthEndpoint, connector.id]);
}, [workspaceId, workspaceIdAtom, reauthEndpoint, connector.id]);
// Get connector-specific config component (MCP-backed connectors use a generic view)
const ConnectorConfigComponent = useMemo(() => {
@ -273,7 +273,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
connector={connector}
onConfigChange={onConfigChange}
onNameChange={onNameChange}
searchSpaceId={searchSpaceId}
workspaceId={workspaceId}
/>
)}

View file

@ -58,7 +58,7 @@ function clearOAuthResultCookie(): void {
}
export const useConnectorDialog = () => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const { data: allConnectors, refetch: refetchAllConnectors } = useAtomValue(connectorsAtom);
const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom);
const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom);
@ -140,7 +140,7 @@ export const useConnectorDialog = () => {
const handleAutoIndex = useCallback(
async (connector: SearchSourceConnector, connectorTitle: string, connectorType: string) => {
if (!searchSpaceId || isAutoIndexingRef.current) return;
if (!workspaceId || isAutoIndexingRef.current) return;
isAutoIndexingRef.current = true;
const defaults = AUTO_INDEX_DEFAULTS[connectorType];
@ -165,13 +165,13 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: connector.id,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
start_date: format(startDate, "yyyy-MM-dd"),
end_date: format(endDate, "yyyy-MM-dd"),
},
});
trackIndexWithDateRangeStarted(Number(searchSpaceId), connectorType, connector.id, {
trackIndexWithDateRangeStarted(Number(workspaceId), connectorType, connector.id, {
hasStartDate: true,
hasEndDate: true,
});
@ -188,13 +188,13 @@ export const useConnectorDialog = () => {
});
} finally {
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
});
await refetchAllConnectors();
isAutoIndexingRef.current = false;
}
},
[searchSpaceId, indexConnector, updateConnector, refetchAllConnectors]
[workspaceId, indexConnector, updateConnector, refetchAllConnectors]
);
// YouTube view state
@ -206,7 +206,7 @@ export const useConnectorDialog = () => {
// Consume OAuth result from cookie (set by /connectors/callback route handler)
useEffect(() => {
const raw = readOAuthResultCookie();
if (!raw || !searchSpaceId) return;
if (!raw || !workspaceId) return;
clearOAuthResultCookie();
const result = parseOAuthCallbackResult(raw);
@ -221,7 +221,7 @@ export const useConnectorDialog = () => {
if (oauthConnector) {
trackConnectorSetupFailure(
Number(searchSpaceId),
Number(workspaceId),
oauthConnector.connectorType,
result.error,
"oauth_callback"
@ -292,7 +292,7 @@ export const useConnectorDialog = () => {
const connectorValidation = searchSourceConnector.safeParse(newConnector);
if (connectorValidation.success) {
trackConnectorConnected(
Number(searchSpaceId),
Number(workspaceId),
oauthConnector.connectorType,
newConnector.id
);
@ -338,20 +338,20 @@ export const useConnectorDialog = () => {
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchSpaceId, handleAutoIndex, refetchAllConnectors, setIsOpen]);
}, [workspaceId, handleAutoIndex, refetchAllConnectors, setIsOpen]);
// Handle OAuth connection
const handleConnectOAuth = useCallback(
async (connector: (typeof OAUTH_CONNECTORS)[number] | (typeof COMPOSIO_CONNECTORS)[number]) => {
if (!searchSpaceId || !connector.authEndpoint) return;
if (!workspaceId || !connector.authEndpoint) return;
// Set connecting state immediately to disable button and show spinner
setConnectingId(connector.id);
trackConnectorSetupStarted(Number(searchSpaceId), connector.connectorType, "oauth_click");
trackConnectorSetupStarted(Number(workspaceId), connector.connectorType, "oauth_click");
try {
const url = buildBackendUrl(connector.authEndpoint, { space_id: searchSpaceId });
const url = buildBackendUrl(connector.authEndpoint, { space_id: workspaceId });
const response = await authenticatedFetch(url, { method: "GET" });
@ -370,7 +370,7 @@ export const useConnectorDialog = () => {
} catch (error) {
console.error(`Error connecting to ${connector.title}:`, error);
trackConnectorSetupFailure(
Number(searchSpaceId),
Number(workspaceId),
connector.connectorType,
error instanceof Error ? error.message : "oauth_initiation_failed",
"oauth_init"
@ -384,22 +384,22 @@ export const useConnectorDialog = () => {
setConnectingId(null);
}
},
[searchSpaceId]
[workspaceId]
);
// Handle creating YouTube crawler (not a connector, shows view in popup)
const handleCreateYouTubeCrawler = useCallback(() => {
if (!searchSpaceId) return;
if (!workspaceId) return;
setIsYouTubeView(true);
}, [searchSpaceId]);
}, [workspaceId]);
// Handle creating webcrawler connector
const handleCreateWebcrawler = useCallback(async () => {
if (!searchSpaceId) return;
if (!workspaceId) return;
setConnectingId("webcrawler-connector");
trackConnectorSetupStarted(
Number(searchSpaceId),
Number(workspaceId),
EnumConnectorName.WEBCRAWLER_CONNECTOR,
"webcrawler_quick_add"
);
@ -418,7 +418,7 @@ export const useConnectorDialog = () => {
enable_vision_llm: false,
},
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
},
});
@ -433,7 +433,7 @@ export const useConnectorDialog = () => {
if (connectorValidation.success) {
// Track webcrawler connector connected
trackConnectorConnected(
Number(searchSpaceId),
Number(workspaceId),
EnumConnectorName.WEBCRAWLER_CONNECTOR,
connector.id
);
@ -453,7 +453,7 @@ export const useConnectorDialog = () => {
} catch (error) {
console.error("Error creating webcrawler connector:", error);
trackConnectorSetupFailure(
Number(searchSpaceId),
Number(workspaceId),
EnumConnectorName.WEBCRAWLER_CONNECTOR,
error instanceof Error ? error.message : "webcrawler_create_failed",
"webcrawler_quick_add"
@ -462,18 +462,18 @@ export const useConnectorDialog = () => {
} finally {
setConnectingId(null);
}
}, [searchSpaceId, createConnector, refetchAllConnectors, setIsOpen]);
}, [workspaceId, createConnector, refetchAllConnectors, setIsOpen]);
// Handle connecting non-OAuth connectors (like Tavily API, Obsidian plugin, etc.)
const handleConnectNonOAuth = useCallback(
(connectorType: string) => {
if (!searchSpaceId) return;
if (!workspaceId) return;
trackConnectorSetupStarted(Number(searchSpaceId), connectorType, "non_oauth_click");
trackConnectorSetupStarted(Number(workspaceId), connectorType, "non_oauth_click");
setConnectingConnectorType(connectorType);
},
[searchSpaceId]
[workspaceId]
);
// Handle submitting connect form
@ -495,7 +495,7 @@ export const useConnectorDialog = () => {
},
onIndexingStart?: (connectorId: number) => void
) => {
if (!searchSpaceId || !connectingConnectorType) {
if (!workspaceId || !connectingConnectorType) {
return;
}
@ -519,7 +519,7 @@ export const useConnectorDialog = () => {
enable_vision_llm: false,
},
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
},
});
// Refetch connectors to get the new one
@ -536,7 +536,7 @@ export const useConnectorDialog = () => {
const currentConnectorType = connectingConnectorType;
// Track connector connected event for non-OAuth connectors
trackConnectorConnected(Number(searchSpaceId), currentConnectorType, connector.id);
trackConnectorConnected(Number(workspaceId), currentConnectorType, connector.id);
// Find connector title from constants
const connectorInfo = OTHER_CONNECTORS.find(
@ -612,7 +612,7 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: connector.id,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
start_date: startDateStr,
end_date: endDateStr,
},
@ -631,7 +631,7 @@ export const useConnectorDialog = () => {
setIndexingConnectorConfig(null);
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
});
await refetchAllConnectors();
@ -684,7 +684,7 @@ export const useConnectorDialog = () => {
} catch (error) {
console.error("Error creating connector:", error);
trackConnectorSetupFailure(
Number(searchSpaceId),
Number(workspaceId),
connectingConnectorType ?? formData.connector_type,
error instanceof Error ? error.message : "connector_create_failed",
"non_oauth_form"
@ -698,7 +698,7 @@ export const useConnectorDialog = () => {
},
[
connectingConnectorType,
searchSpaceId,
workspaceId,
createConnector,
refetchAllConnectors,
updateConnector,
@ -724,7 +724,7 @@ export const useConnectorDialog = () => {
// Handle viewing accounts list for OAuth connector type
const handleViewAccountsList = useCallback(
(connectorType: string, _connectorTitle?: string) => {
if (!searchSpaceId) return;
if (!workspaceId) return;
const oauthConnector =
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
@ -736,7 +736,7 @@ export const useConnectorDialog = () => {
});
}
},
[searchSpaceId]
[workspaceId]
);
// Handle going back from accounts list view
@ -746,9 +746,9 @@ export const useConnectorDialog = () => {
// Handle viewing MCP list
const handleViewMCPList = useCallback(() => {
if (!searchSpaceId) return;
if (!workspaceId) return;
setViewingMCPList(true);
}, [searchSpaceId]);
}, [workspaceId]);
// Handle going back from MCP list view
const handleBackFromMCPList = useCallback(() => {
@ -765,7 +765,7 @@ export const useConnectorDialog = () => {
// Handle starting indexing
const handleStartIndexing = useCallback(
async (refreshConnectors: () => void) => {
if (!indexingConfig || !searchSpaceId) return;
if (!indexingConfig || !workspaceId) return;
// Validate date range (skip for Google Drive, Composio Drive, OneDrive, Dropbox, and Webcrawler)
if (
@ -847,7 +847,7 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: indexingConfig.connectorId,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
},
body: {
folders: selectedFolders || [],
@ -870,14 +870,14 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: indexingConfig.connectorId,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
},
});
} else {
await indexConnector({
connector_id: indexingConfig.connectorId,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
start_date: startDateStr,
end_date: endDateStr,
},
@ -886,7 +886,7 @@ export const useConnectorDialog = () => {
// Track index with date range started event
trackIndexWithDateRangeStarted(
Number(searchSpaceId),
Number(workspaceId),
indexingConfig.connectorType,
indexingConfig.connectorId,
{
@ -898,7 +898,7 @@ export const useConnectorDialog = () => {
// Track periodic indexing started if enabled
if (periodicEnabled) {
trackPeriodicIndexingStarted(
Number(searchSpaceId),
Number(workspaceId),
indexingConfig.connectorType,
indexingConfig.connectorId,
parseInt(frequencyMinutes, 10)
@ -915,7 +915,7 @@ export const useConnectorDialog = () => {
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
});
} catch (error) {
console.error("Error starting indexing:", error);
@ -926,7 +926,7 @@ export const useConnectorDialog = () => {
},
[
indexingConfig,
searchSpaceId,
workspaceId,
startDate,
endDate,
indexConnector,
@ -951,7 +951,7 @@ export const useConnectorDialog = () => {
// Handle starting edit mode
const handleStartEdit = useCallback(
(connector: SearchSourceConnector) => {
if (!searchSpaceId) return;
if (!workspaceId) return;
// For MCP connectors from "All Connectors" tab, show the list view instead of directly editing
// (unless we're already in the MCP list view or on the Active tab where individual MCPs are shown)
@ -986,11 +986,7 @@ export const useConnectorDialog = () => {
// Track index with date range opened event
if (connector.is_indexable) {
trackIndexWithDateRangeOpened(
Number(searchSpaceId),
connector.connector_type,
connector.id
);
trackIndexWithDateRangeOpened(Number(workspaceId), connector.connector_type, connector.id);
}
setEditingConnector(connector);
@ -1001,13 +997,13 @@ export const useConnectorDialog = () => {
setStartDate(undefined);
setEndDate(undefined);
},
[searchSpaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
[workspaceId, viewingAccountsType, viewingMCPList, handleViewMCPList, activeTab]
);
// Handle saving connector changes
const handleSaveConnector = useCallback(
async (refreshConnectors: () => void) => {
if (!editingConnector || !searchSpaceId || isSaving) return;
if (!editingConnector || !workspaceId || isSaving) return;
// Validate date range (skip for Google Drive/OneDrive/Dropbox which uses folder selection, Webcrawler which uses config, and non-indexable connectors)
if (
@ -1114,7 +1110,7 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: editingConnector.id,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
},
body: {
folders: selectedFolders || [],
@ -1134,7 +1130,7 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: editingConnector.id,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
},
});
indexingDescription = "Re-indexing started with updated configuration.";
@ -1143,7 +1139,7 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: editingConnector.id,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
start_date: startDateStr,
end_date: endDateStr,
},
@ -1157,7 +1153,7 @@ export const useConnectorDialog = () => {
(indexingDescription.includes("Re-indexing") || indexingDescription.includes("indexing"))
) {
trackIndexWithDateRangeStarted(
Number(searchSpaceId),
Number(workspaceId),
editingConnector.connector_type,
editingConnector.id,
{
@ -1170,7 +1166,7 @@ export const useConnectorDialog = () => {
// Track periodic indexing if enabled
if (periodicEnabled && editingConnector.is_indexable) {
trackPeriodicIndexingStarted(
Number(searchSpaceId),
Number(workspaceId),
editingConnector.connector_type,
editingConnector.id,
frequency || parseInt(frequencyMinutes, 10)
@ -1190,7 +1186,7 @@ export const useConnectorDialog = () => {
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
});
} catch (error) {
console.error("Error saving connector:", error);
@ -1201,7 +1197,7 @@ export const useConnectorDialog = () => {
},
[
editingConnector,
searchSpaceId,
workspaceId,
isSaving,
startDate,
endDate,
@ -1220,7 +1216,7 @@ export const useConnectorDialog = () => {
// Handle disconnecting connector
const handleDisconnectConnector = useCallback(
async (refreshConnectors: () => void) => {
if (!editingConnector || !searchSpaceId) return;
if (!editingConnector || !workspaceId) return;
setIsDisconnecting(true);
try {
@ -1230,7 +1226,7 @@ export const useConnectorDialog = () => {
// Track connector deleted event
trackConnectorDeleted(
Number(searchSpaceId),
Number(workspaceId),
editingConnector.connector_type,
editingConnector.id
);
@ -1255,7 +1251,7 @@ export const useConnectorDialog = () => {
refreshConnectors();
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
});
} catch (error) {
console.error("Error disconnecting connector:", error);
@ -1264,7 +1260,7 @@ export const useConnectorDialog = () => {
setIsDisconnecting(false);
}
},
[editingConnector, searchSpaceId, deleteConnector, cameFromMCPList, setIsOpen]
[editingConnector, workspaceId, deleteConnector, cameFromMCPList, setIsOpen]
);
// Handle quick index (index with selected date range, or backend defaults if none selected)
@ -1276,7 +1272,7 @@ export const useConnectorDialog = () => {
startDate?: Date,
endDate?: Date
) => {
if (!searchSpaceId) {
if (!workspaceId) {
if (stopIndexing) {
stopIndexing(connectorId);
}
@ -1285,7 +1281,7 @@ export const useConnectorDialog = () => {
// Track quick index clicked event
if (connectorType) {
trackQuickIndexClicked(Number(searchSpaceId), connectorType, connectorId);
trackQuickIndexClicked(Number(workspaceId), connectorType, connectorId);
}
try {
@ -1296,7 +1292,7 @@ export const useConnectorDialog = () => {
await indexConnector({
connector_id: connectorId,
queryParams: {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
start_date: startDateStr,
end_date: endDateStr,
},
@ -1305,7 +1301,7 @@ export const useConnectorDialog = () => {
// Invalidate queries to refresh data
queryClient.invalidateQueries({
queryKey: cacheKeys.logs.summary(Number(searchSpaceId)),
queryKey: cacheKeys.logs.summary(Number(workspaceId)),
});
// Note: Don't call stopIndexing here - let useIndexingConnectors hook
// detect when last_indexed_at changes via real-time sync
@ -1318,7 +1314,7 @@ export const useConnectorDialog = () => {
}
}
},
[searchSpaceId, indexConnector]
[workspaceId, indexConnector]
);
// Handle going back from edit view
@ -1406,7 +1402,7 @@ export const useConnectorDialog = () => {
periodicEnabled,
frequencyMinutes,
enableVisionLlm,
searchSpaceId,
workspaceId,
allConnectors,
viewingAccountsType,
viewingMCPList,

View file

@ -44,7 +44,7 @@ export function getConnectorDisplayName(fullName: string): string {
interface AllConnectorsTabProps {
searchQuery: string;
searchSpaceId: string;
workspaceId: string;
connectedTypes: Set<string>;
connectingId: string | null;
allConnectors: SearchSourceConnector[] | undefined;

View file

@ -44,7 +44,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
isConnecting = false,
addButtonText,
}) => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const [reauthingId, setReauthingId] = useState<number | null>(null);
const [confirmDisconnectId, setConfirmDisconnectId] = useState<number | null>(null);
const [disconnectingId, setDisconnectingId] = useState<number | null>(null);
@ -58,13 +58,13 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
const handleReauth = useCallback(
async (connector: SearchSourceConnector) => {
const endpoint = getReauthEndpoint(connector);
if (!searchSpaceId || !endpoint) return;
if (!workspaceId || !endpoint) return;
setReauthingId(connector.id);
try {
const response = await authenticatedFetch(
buildBackendUrl(endpoint, {
connector_id: connector.id,
space_id: searchSpaceId,
space_id: workspaceId,
return_url: window.location.pathname,
})
);
@ -86,7 +86,7 @@ export const ConnectorAccountsListView: FC<ConnectorAccountsListViewProps> = ({
setReauthingId(null);
}
},
[searchSpaceId]
[workspaceId]
);
// Filter connectors to only show those of this type

View file

@ -38,11 +38,11 @@ function extractYoutubeUrls(text: string): string[] {
}
interface YouTubeCrawlerViewProps {
searchSpaceId: string;
workspaceId: string;
onBack: () => void;
}
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId, onBack }) => {
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ workspaceId, onBack }) => {
const t = useTranslations("add_youtube");
const [videoTags, setVideoTags] = useState<TagType[]>([]);
const [activeTagIndex, setActiveTagIndex] = useState<number | null>(null);
@ -165,7 +165,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId,
{
document_type: "YOUTUBE_VIDEO",
content: videoUrls,
workspace_id: parseInt(searchSpaceId, 10),
workspace_id: parseInt(workspaceId, 10),
},
{
onSuccess: () => {

View file

@ -90,9 +90,9 @@ const DocumentUploadPopupContent: FC<{
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ isOpen, onOpenChange }) => {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
if (!searchSpaceId) return null;
if (!workspaceId) return null;
const handleSuccess = () => {
onOpenChange(false);
@ -112,12 +112,12 @@ const DocumentUploadPopupContent: FC<{
Upload Documents
</DialogTitle>
<DialogDescription className="text-xs sm:text-base text-muted-foreground/80 line-clamp-1">
Upload and sync your documents to your search space
Upload and sync your documents to your workspace
</DialogDescription>
</DialogHeader>
<div className="px-4 sm:px-6 pb-4 sm:pb-6">
<DocumentUploadTab searchSpaceId={searchSpaceId} onSuccess={handleSuccess} />
<DocumentUploadTab workspaceId={workspaceId} onSuccess={handleSuccess} />
</div>
</div>
</DialogContent>

View file

@ -189,7 +189,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const params = useParams();
const electronAPI = useElectronAPI();
const resolvedSearchSpaceId = getWorkspaceIdNumber(params);
const resolvedWorkspaceId = getWorkspaceIdNumber(params);
const { displayName, isFolder } = getVirtualPathDisplay(path);
const icon = isFolder ? <FolderIcon className="size-3.5" /> : <FileIcon className="size-3.5" />;
@ -204,7 +204,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
if (electronAPI.getAgentFilesystemMounts) {
try {
const mounts = (await electronAPI.getAgentFilesystemMounts(
resolvedSearchSpaceId
resolvedWorkspaceId
)) as AgentFilesystemMount[];
resolvedLocalPath = normalizeLocalVirtualPathForEditor(path, mounts);
} catch {
@ -215,21 +215,21 @@ function FilePathLink({ path, className }: { path: string; className?: string })
kind: "local_file",
localFilePath: resolvedLocalPath,
title: resolvedLocalPath.split("/").pop() || resolvedLocalPath,
searchSpaceId: resolvedSearchSpaceId,
workspaceId: resolvedWorkspaceId,
});
return;
}
if (!resolvedSearchSpaceId || !path.startsWith("/documents/")) return;
if (!resolvedWorkspaceId || !path.startsWith("/documents/")) return;
try {
const doc = await documentsApiService.getDocumentByVirtualPath({
workspace_id: resolvedSearchSpaceId,
workspace_id: resolvedWorkspaceId,
virtual_path: path,
});
openEditorPanel({
kind: "document",
documentId: doc.id,
searchSpaceId: resolvedSearchSpaceId,
workspaceId: resolvedWorkspaceId,
title: doc.title,
});
} catch {
@ -237,7 +237,7 @@ function FilePathLink({ path, className }: { path: string; className?: string })
}
})();
},
[electronAPI, openEditorPanel, path, resolvedSearchSpaceId]
[electronAPI, openEditorPanel, path, resolvedWorkspaceId]
);
// Folders cannot open in the editor panel — keep them as visual chips.

View file

@ -895,7 +895,7 @@ const Composer: FC = () => {
<ComposerSuggestionPopoverContent side="top">
<DocumentMentionPicker
ref={documentPickerRef}
searchSpaceId={workspaceId ?? 0}
workspaceId={workspaceId ?? 0}
enableChatMentions
currentChatId={threadId}
onSelectionChange={handleDocumentsMention}
@ -961,7 +961,7 @@ const Composer: FC = () => {
</div>
<ComposerAction
isBlockedByOtherUser={isBlockedByOtherUser}
searchSpaceId={workspaceId ?? 0}
workspaceId={workspaceId ?? 0}
onChatModelSelected={handleChatModelSelected}
/>
<ConnectorIndicator showTrigger={false} />
@ -982,13 +982,13 @@ const Composer: FC = () => {
interface ComposerActionProps {
isBlockedByOtherUser?: boolean;
searchSpaceId: number;
workspaceId: number;
onChatModelSelected?: () => void;
}
const ComposerAction: FC<ComposerActionProps> = ({
isBlockedByOtherUser = false,
searchSpaceId,
workspaceId,
onChatModelSelected,
}) => {
const mentionedDocuments = useAtomValue(mentionedDocumentsAtom);
@ -1564,7 +1564,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
)}
<div className="ml-auto flex min-w-0 shrink-0 items-center gap-2">
<ChatHeader
searchSpaceId={searchSpaceId}
workspaceId={workspaceId}
className="h-9 max-w-[44vw] px-2 sm:max-w-[220px] sm:px-3"
onChatModelSelected={onChatModelSelected}
/>

View file

@ -76,33 +76,33 @@ const UserTextPart: FC = () => {
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const router = useRouter();
const params = useParams();
const resolvedSearchSpaceId = getWorkspaceIdNumber(params);
const resolvedWorkspaceId = getWorkspaceIdNumber(params);
const handleOpenDoc = useCallback(
(docId: number, title: string) => {
if (!resolvedSearchSpaceId) {
toast.error("Cannot open document outside a search space.");
if (!resolvedWorkspaceId) {
toast.error("Cannot open document outside a workspace.");
return;
}
openEditorPanel({
kind: "document",
documentId: docId,
searchSpaceId: resolvedSearchSpaceId,
workspaceId: resolvedWorkspaceId,
title,
});
},
[openEditorPanel, resolvedSearchSpaceId]
[openEditorPanel, resolvedWorkspaceId]
);
const handleOpenThread = useCallback(
(threadId: number) => {
if (!resolvedSearchSpaceId) {
toast.error("Cannot open chat outside a search space.");
if (!resolvedWorkspaceId) {
toast.error("Cannot open chat outside a workspace.");
return;
}
router.push(`/dashboard/${resolvedSearchSpaceId}/new-chat/${threadId}`);
router.push(`/dashboard/${resolvedWorkspaceId}/new-chat/${threadId}`);
},
[resolvedSearchSpaceId, router]
[resolvedWorkspaceId, router]
);
const segments = parseMentionSegments(text, mentionedDocs);

View file

@ -77,7 +77,7 @@ export const CitationPanelContent: FC<CitationPanelContentProps> = ({
if (!data) return;
openEditorPanel({
documentId: data.id,
searchSpaceId: data.workspace_id,
workspaceId: data.workspace_id,
title: data.title,
});
};

View file

@ -49,7 +49,7 @@ export interface FolderDisplay {
name: string;
position: string;
parentId: number | null;
searchSpaceId: number;
workspaceId: number;
metadata?: Record<string, unknown> | null;
}

View file

@ -146,7 +146,7 @@ export function EditorPanelContent({
documentId,
localFilePath,
memoryScope,
searchSpaceId,
workspaceId,
title,
onClose,
}: {
@ -154,7 +154,7 @@ export function EditorPanelContent({
documentId?: number;
localFilePath?: string;
memoryScope?: "user" | "team";
searchSpaceId?: number;
workspaceId?: number;
title: string | null;
onClose?: () => void;
}) {
@ -186,14 +186,14 @@ export function EditorPanelContent({
}
try {
const mounts = (await electronAPI.getAgentFilesystemMounts(
searchSpaceId
workspaceId
)) as AgentFilesystemMount[];
return normalizeLocalVirtualPathForEditor(candidatePath, mounts);
} catch {
return candidatePath;
}
},
[electronAPI, searchSpaceId]
[electronAPI, workspaceId]
);
const plateMaxBytes = editorDoc?.editor_plate_max_bytes ?? LARGE_DOCUMENT_THRESHOLD;
@ -234,7 +234,7 @@ export function EditorPanelContent({
const resolvedLocalPath = await resolveLocalVirtualPath(localFilePath);
const readResult = await electronAPI.readAgentLocalFileText(
resolvedLocalPath,
searchSpaceId
workspaceId
);
if (!readResult.ok) {
throw new Error(readResult.error || "Failed to read local file");
@ -257,7 +257,7 @@ export function EditorPanelContent({
if (!memoryScope) throw new Error("Missing memory context");
const { document, limits } = await fetchMemoryEditorDocument({
scope: memoryScope,
searchSpaceId,
workspaceId,
title,
signal: controller.signal,
});
@ -271,12 +271,12 @@ export function EditorPanelContent({
return;
}
if (!documentId || !searchSpaceId) {
if (!documentId || !workspaceId) {
throw new Error("Missing document context");
}
const response = await authenticatedFetch(
buildBackendUrl(
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/editor-content`
`/api/v1/workspaces/${workspaceId}/documents/${documentId}/editor-content`
),
{ method: "GET" }
);
@ -323,7 +323,7 @@ export function EditorPanelContent({
localFilePath,
memoryScope,
resolveLocalVirtualPath,
searchSpaceId,
workspaceId,
title,
]);
@ -382,7 +382,7 @@ export function EditorPanelContent({
const writeResult = await electronAPI.writeAgentLocalFileText(
resolvedLocalPath,
contentToSave,
searchSpaceId
workspaceId
);
if (!writeResult.ok) {
throw new Error(writeResult.error || "Failed to save local file");
@ -395,7 +395,7 @@ export function EditorPanelContent({
if (!memoryScope) throw new Error("Missing memory context");
const { markdown: savedContent, limits } = await saveMemoryMarkdown({
scope: memoryScope,
searchSpaceId,
workspaceId,
markdown: markdownRef.current,
});
markdownRef.current = savedContent;
@ -408,11 +408,11 @@ export function EditorPanelContent({
return true;
}
if (!searchSpaceId || !documentId) {
if (!workspaceId || !documentId) {
throw new Error("Missing document context");
}
const response = await authenticatedFetch(
buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/save`),
buildBackendUrl(`/api/v1/workspaces/${workspaceId}/documents/${documentId}/save`),
{
method: "POST",
headers: { "Content-Type": "application/json" },
@ -460,7 +460,7 @@ export function EditorPanelContent({
plateMaxBytes,
plateMaxLines,
resolveLocalVirtualPath,
searchSpaceId,
workspaceId,
]
);
@ -515,12 +515,12 @@ export function EditorPanelContent({
}, [editorDoc?.source_markdown]);
const handleDownloadMarkdown = useCallback(async () => {
if (!searchSpaceId || !documentId) return;
if (!workspaceId || !documentId) return;
setDownloading(true);
try {
const response = await authenticatedFetch(
buildBackendUrl(
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/download-markdown`
`/api/v1/workspaces/${workspaceId}/documents/${documentId}/download-markdown`
),
{ method: "GET" }
);
@ -542,7 +542,7 @@ export function EditorPanelContent({
} finally {
setDownloading(false);
}
}, [documentId, editorDoc?.title, searchSpaceId]);
}, [documentId, editorDoc?.title, workspaceId]);
const largeDocAlert = viewerMode === "monaco" && !isLocalFileMode && editorDoc && (
<Alert className="m-4 shrink-0">
@ -890,7 +890,7 @@ function DesktopEditorPanel() {
const hasTarget =
panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId
? !!panelState.documentId && !!panelState.workspaceId
: panelState.kind === "local_file"
? !!panelState.localFilePath
: !!panelState.memoryScope;
@ -903,7 +903,7 @@ function DesktopEditorPanel() {
documentId={panelState.documentId ?? undefined}
localFilePath={panelState.localFilePath ?? undefined}
memoryScope={panelState.memoryScope ?? undefined}
searchSpaceId={panelState.searchSpaceId ?? undefined}
workspaceId={panelState.workspaceId ?? undefined}
title={panelState.title}
onClose={closePanel}
/>
@ -919,7 +919,7 @@ function MobileEditorDrawer() {
const hasTarget =
panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId
? !!panelState.documentId && !!panelState.workspaceId
: !!panelState.memoryScope;
if (!hasTarget) return null;
@ -943,7 +943,7 @@ function MobileEditorDrawer() {
documentId={panelState.documentId ?? undefined}
localFilePath={panelState.localFilePath ?? undefined}
memoryScope={panelState.memoryScope ?? undefined}
searchSpaceId={panelState.searchSpaceId ?? undefined}
workspaceId={panelState.workspaceId ?? undefined}
title={panelState.title}
/>
</div>
@ -957,7 +957,7 @@ export function EditorPanel() {
const isDesktop = useMediaQuery("(min-width: 1024px)");
const hasTarget =
panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId
? !!panelState.documentId && !!panelState.workspaceId
: panelState.kind === "local_file"
? !!panelState.localFilePath
: !!panelState.memoryScope;
@ -977,7 +977,7 @@ export function MobileEditorPanel() {
const isDesktop = useMediaQuery("(min-width: 1024px)");
const hasTarget =
panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId
? !!panelState.documentId && !!panelState.workspaceId
: panelState.kind === "local_file"
? !!panelState.localFilePath
: !!panelState.memoryScope;

View file

@ -24,10 +24,10 @@ interface MemoryReadResponse {
limits?: MemoryLimits;
}
function getMemoryPath(scope: MemoryScope, searchSpaceId?: number | null) {
function getMemoryPath(scope: MemoryScope, workspaceId?: number | null) {
if (scope === "user") return "/api/v1/users/me/memory";
if (!searchSpaceId) throw new Error("Missing search space context");
return `/api/v1/workspaces/${searchSpaceId}/memory`;
if (!workspaceId) throw new Error("Missing workspace context");
return `/api/v1/workspaces/${workspaceId}/memory`;
}
export function getMemoryLimitState(length: number, limits?: MemoryLimits | null) {
@ -53,16 +53,16 @@ export function getMemoryLimitState(length: number, limits?: MemoryLimits | null
export async function fetchMemoryEditorDocument({
scope,
searchSpaceId,
workspaceId,
title,
signal,
}: {
scope: MemoryScope;
searchSpaceId?: number | null;
workspaceId?: number | null;
title?: string | null;
signal?: AbortSignal;
}) {
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), {
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, workspaceId)), {
method: "GET",
signal,
});
@ -87,14 +87,14 @@ export async function fetchMemoryEditorDocument({
export async function saveMemoryMarkdown({
scope,
searchSpaceId,
workspaceId,
markdown,
}: {
scope: MemoryScope;
searchSpaceId?: number | null;
workspaceId?: number | null;
markdown: string;
}) {
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, searchSpaceId)), {
const response = await authenticatedFetch(buildBackendUrl(getMemoryPath(scope, workspaceId)), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ memory_md: markdown }),

View file

@ -5,13 +5,13 @@ export type {
IconRailProps,
NavItem,
PageUsage,
SearchSpace,
SidebarSectionProps,
User,
Workspace,
} from "./types/layout.types";
export {
ChatListItem,
CreateSearchSpaceDialog,
CreateWorkspaceDialog,
CreditBalanceDisplay,
Header,
IconRail,
@ -20,10 +20,10 @@ export {
MobileSidebarTrigger,
NavIcon,
NavSection,
SearchSpaceAvatar,
Sidebar,
SidebarCollapseButton,
SidebarHeader,
SidebarSection,
SidebarUserProfile,
WorkspaceAvatar,
} from "./ui";

View file

@ -9,14 +9,14 @@ import { useLoginGate } from "@/contexts/login-gate";
import { useAnnouncements } from "@/hooks/use-announcements";
import { useIsMobile } from "@/hooks/use-mobile";
import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service";
import type { ChatItem, NavItem, PageUsage, SearchSpace } from "../types/layout.types";
import type { ChatItem, NavItem, PageUsage, Workspace } from "../types/layout.types";
import { LayoutShell } from "../ui/shell";
interface FreeLayoutDataProviderProps {
children: ReactNode;
}
const GUEST_SPACE: SearchSpace = {
const GUEST_SPACE: Workspace = {
id: 0,
name: "SurfSense Free",
description: "Free AI chat without login",
@ -94,19 +94,16 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]);
const handleSearchSpaceSelect = useCallback(
(_id: number) => gate("switch search spaces"),
[gate]
);
const handleWorkspaceSelect = useCallback((_id: number) => gate("switch workspaces"), [gate]);
return (
<LayoutShell
searchSpaces={[GUEST_SPACE]}
activeSearchSpaceId={0}
onSearchSpaceSelect={handleSearchSpaceSelect}
onSearchSpaceSettings={gatedAction("search space settings")}
onAddSearchSpace={gatedAction("create search spaces")}
searchSpace={GUEST_SPACE}
workspaces={[GUEST_SPACE]}
activeWorkspaceId={0}
onWorkspaceSelect={handleWorkspaceSelect}
onWorkspaceSettings={gatedAction("workspace settings")}
onAddWorkspace={gatedAction("create workspaces")}
workspace={GUEST_SPACE}
navItems={navItems}
onNavItemClick={handleNavItemClick}
chats={[]}
@ -121,7 +118,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
email: "Guest",
name: "Guest",
}}
onSettings={gatedAction("search space settings")}
onSettings={gatedAction("workspace settings")}
onManageMembers={gatedAction("team management")}
onUserSettings={gatedAction("account settings")}
onAnnouncements={handleAnnouncements}

View file

@ -15,8 +15,8 @@ import { announcementsDialogAtom } from "@/atoms/layout/dialogs.atom";
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { deleteSearchSpaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
import { deleteWorkspaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { workspacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
import { ActionLogDialog } from "@/components/agent-action-log/action-log-dialog";
import { AnnouncementSpotlight } from "@/components/announcements/AnnouncementSpotlight";
import { AnnouncementsDialog } from "@/components/announcements/AnnouncementsDialog";
@ -47,17 +47,17 @@ import { useInbox } from "@/hooks/use-inbox";
import { useIsMobile } from "@/hooks/use-mobile";
import { useArchiveThread, useDeleteThread, useRenameThread } from "@/hooks/use-thread-mutations";
import { notificationsApiService } from "@/lib/apis/notifications-api.service";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { getLoginPath, logout } from "@/lib/auth-utils";
import { fetchThreads } from "@/lib/chat/thread-persistence";
import { resetUser, trackLogout } from "@/lib/posthog/events";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import type { ChatItem, NavItem, SearchSpace } from "../types/layout.types";
import { CreateSearchSpaceDialog } from "../ui/dialogs";
import type { ChatItem, NavItem, Workspace } from "../types/layout.types";
import { CreateWorkspaceDialog } from "../ui/dialogs";
import { LayoutShell } from "../ui/shell";
interface LayoutDataProviderProps {
searchSpaceId: string;
workspaceId: string;
children: React.ReactNode;
}
@ -72,7 +72,7 @@ function formatInboxCount(count: number): string {
return `${thousands}k+`;
}
export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProviderProps) {
export function LayoutDataProvider({ workspaceId, children }: LayoutDataProviderProps) {
const t = useTranslations("dashboard");
const tCommon = useTranslations("common");
const tSidebar = useTranslations("sidebar");
@ -88,19 +88,19 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
// Atoms
const { data: user } = useAtomValue(currentUserAtom);
const {
data: searchSpacesData,
refetch: refetchSearchSpaces,
isSuccess: searchSpacesLoaded,
} = useAtomValue(searchSpacesAtom);
const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom);
data: workspacesData,
refetch: refetchWorkspaces,
isSuccess: workspacesLoaded,
} = useAtomValue(workspacesAtom);
const { mutateAsync: deleteWorkspace } = useAtomValue(deleteWorkspaceMutationAtom);
const currentThreadState = useAtomValue(currentThreadAtom);
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
const syncChatTab = useSetAtom(syncChatTabAtom);
const removeChatTab = useSetAtom(removeChatTabAtom);
const { activateChatThread, prefetchChatThread } = useActivateChatThread();
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
const { mutateAsync: archiveThread } = useArchiveThread(workspaceId);
const { mutateAsync: deleteThread } = useDeleteThread(workspaceId);
const { mutateAsync: renameThread } = useRenameThread(workspaceId);
// Key used to force-remount the page component (e.g. after deleting the active chat
// when the router is out of sync due to replaceState)
@ -113,16 +113,16 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
// Fetch current workspace as a fallback for the selector while the full list catches up.
const { data: currentWorkspace } = useQuery({
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId),
queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }),
enabled: !!searchSpaceId,
queryKey: cacheKeys.workspaces.detail(workspaceId),
queryFn: () => workspacesApiService.getWorkspace({ id: Number(workspaceId) }),
enabled: !!workspaceId,
});
// Fetch threads (40 total to allow up to 20 per section - shared/private)
const { data: threadsData, isPending: isLoadingThreads } = useQuery({
queryKey: ["threads", searchSpaceId, { limit: 40 }],
queryFn: () => fetchThreads(Number(searchSpaceId), 40),
enabled: !!searchSpaceId,
queryKey: ["threads", workspaceId, { limit: 40 }],
queryFn: () => fetchThreads(Number(workspaceId), 40),
enabled: !!workspaceId,
});
// Unified slide-out panel state (only one can be open at a time)
@ -147,10 +147,10 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}, [setIsDocumentsSidebarOpen]);
// Search space dialog state
const [isCreateSearchSpaceDialogOpen, setIsCreateSearchSpaceDialogOpen] = useState(false);
const [isCreateWorkspaceDialogOpen, setIsCreateWorkspaceDialogOpen] = useState(false);
const userId = user?.id ? String(user.id) : null;
const numericSpaceId = Number(searchSpaceId) || null;
const numericSpaceId = Number(workspaceId) || null;
// Batch-fetch unread counts for all categories in a single request
// instead of 2 separate /unread-count calls.
@ -218,11 +218,11 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
icon: <AlertTriangle className="h-5 w-5 text-amber-500" />,
action: {
label: "Buy credits",
onClick: () => router.push(`/dashboard/${searchSpaceId}/buy-more`),
onClick: () => router.push(`/dashboard/${workspaceId}/buy-more`),
},
});
}
}, [statusInbox.inboxItems, statusInbox.loading, searchSpaceId, router]);
}, [statusInbox.inboxItems, statusInbox.loading, workspaceId, router]);
// Delete dialogs state
const [showDeleteChatDialog, setShowDeleteChatDialog] = useState(false);
@ -235,28 +235,28 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const [newChatTitle, setNewChatTitle] = useState("");
const [isRenamingChat, setIsRenamingChat] = useState(false);
// Delete/Leave search space dialog state
const [showDeleteSearchSpaceDialog, setShowDeleteSearchSpaceDialog] = useState(false);
const [showLeaveSearchSpaceDialog, setShowLeaveSearchSpaceDialog] = useState(false);
const [searchSpaceToDelete, setSearchSpaceToDelete] = useState<SearchSpace | null>(null);
const [searchSpaceToLeave, setSearchSpaceToLeave] = useState<SearchSpace | null>(null);
const [isDeletingSearchSpace, setIsDeletingSearchSpace] = useState(false);
const [isLeavingSearchSpace, setIsLeavingSearchSpace] = useState(false);
// Delete/Leave workspace dialog state
const [showDeleteWorkspaceDialog, setShowDeleteWorkspaceDialog] = useState(false);
const [showLeaveWorkspaceDialog, setShowLeaveWorkspaceDialog] = useState(false);
const [workspaceToDelete, setWorkspaceToDelete] = useState<Workspace | null>(null);
const [workspaceToLeave, setWorkspaceToLeave] = useState<Workspace | null>(null);
const [isDeletingWorkspace, setIsDeletingWorkspace] = useState(false);
const [isLeavingWorkspace, setIsLeavingWorkspace] = useState(false);
// Reset transient slide-out panels when switching search spaces.
// Reset transient slide-out panels when switching workspaces.
// Tabs intentionally persist across spaces — opening tabs from multiple
// search spaces is a supported flow (browser-tab semantics).
const prevSearchSpaceIdRef = useRef(searchSpaceId);
// workspaces is a supported flow (browser-tab semantics).
const prevWorkspaceIdRef = useRef(workspaceId);
useEffect(() => {
if (prevSearchSpaceIdRef.current !== searchSpaceId) {
prevSearchSpaceIdRef.current = searchSpaceId;
if (prevWorkspaceIdRef.current !== workspaceId) {
prevWorkspaceIdRef.current = workspaceId;
setActiveSlideoutPanel(null);
}
}, [searchSpaceId]);
}, [workspaceId]);
const searchSpaces: SearchSpace[] = useMemo(() => {
if (!searchSpacesData || !Array.isArray(searchSpacesData)) return [];
return searchSpacesData.map((space) => ({
const workspaces: Workspace[] = useMemo(() => {
if (!workspacesData || !Array.isArray(workspacesData)) return [];
return workspacesData.map((space) => ({
id: space.id,
name: space.name,
description: space.description,
@ -264,15 +264,15 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
memberCount: space.member_count || 0,
createdAt: space.created_at,
}));
}, [searchSpacesData]);
}, [workspacesData]);
// Find active workspace from list, falling back to the route-scoped detail query.
const activeSearchSpace: SearchSpace | null = useMemo(() => {
if (!searchSpaceId) return null;
const searchSpaceIdNumber = Number(searchSpaceId);
const listedSpace = searchSpaces.find((s) => s.id === searchSpaceIdNumber);
const activeWorkspace: Workspace | null = useMemo(() => {
if (!workspaceId) return null;
const workspaceIdNumber = Number(workspaceId);
const listedSpace = workspaces.find((s) => s.id === workspaceIdNumber);
if (listedSpace) return listedSpace;
if (!currentWorkspace || currentWorkspace.id !== searchSpaceIdNumber) return null;
if (!currentWorkspace || currentWorkspace.id !== workspaceIdNumber) return null;
return {
id: currentWorkspace.id,
name: currentWorkspace.name,
@ -281,25 +281,24 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
memberCount: 0,
createdAt: currentWorkspace.created_at,
};
}, [currentWorkspace, searchSpaceId, searchSpaces]);
}, [currentWorkspace, workspaceId, workspaces]);
// Safety redirect: if the current search space is no longer in the user's list
// Safety redirect: if the current workspace is no longer in the user's list
// (e.g. deleted by background task, membership revoked), redirect to a valid space.
useEffect(() => {
if (!searchSpacesLoaded || !searchSpaceId || isDeletingSearchSpace || isLeavingSearchSpace)
return;
if (searchSpaces.length > 0 && !activeSearchSpace) {
router.replace(`/dashboard/${searchSpaces[0].id}/new-chat`);
} else if (searchSpaces.length === 0 && searchSpacesLoaded && !activeSearchSpace) {
if (!workspacesLoaded || !workspaceId || isDeletingWorkspace || isLeavingWorkspace) return;
if (workspaces.length > 0 && !activeWorkspace) {
router.replace(`/dashboard/${workspaces[0].id}/new-chat`);
} else if (workspaces.length === 0 && workspacesLoaded && !activeWorkspace) {
router.replace("/dashboard");
}
}, [
searchSpacesLoaded,
searchSpaceId,
searchSpaces,
activeSearchSpace,
isDeletingSearchSpace,
isLeavingSearchSpace,
workspacesLoaded,
workspaceId,
workspaces,
activeWorkspace,
isDeletingWorkspace,
isLeavingWorkspace,
router,
]);
@ -307,18 +306,18 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
useEffect(() => {
const chatId = currentChatId ?? null;
const chatUrl = chatId
? `/dashboard/${searchSpaceId}/new-chat/${chatId}`
: `/dashboard/${searchSpaceId}/new-chat`;
? `/dashboard/${workspaceId}/new-chat/${chatId}`
: `/dashboard/${workspaceId}/new-chat`;
const thread = threadsData?.threads?.find((t) => t.id === chatId);
syncChatTab({
chatId,
// Avoid overwriting live SSE-updated tab titles with fallback values.
title: chatId ? (thread?.title ?? undefined) : "New Chat",
chatUrl,
searchSpaceId: Number(searchSpaceId),
workspaceId: Number(workspaceId),
...(thread?.visibility !== undefined ? { visibility: thread.visibility } : {}),
});
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
}, [currentChatId, workspaceId, threadsData?.threads, syncChatTab]);
const chats = useMemo(() => {
if (!threadsData?.threads) return [];
@ -326,12 +325,12 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
return threadsData.threads.map<ChatItem>((thread) => ({
id: thread.id,
name: thread.title || `Chat ${thread.id}`,
url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`,
url: `/dashboard/${workspaceId}/new-chat/${thread.id}`,
visibility: thread.visibility,
isOwnThread: thread.is_own_thread,
archived: thread.archived,
}));
}, [threadsData, searchSpaceId]);
}, [threadsData, workspaceId]);
// Navigation items
// Inbox, Automations, and Artifacts are rendered explicitly below "New chat"
@ -352,13 +351,13 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
},
{
title: "Automations",
url: `/dashboard/${searchSpaceId}/automations`,
url: `/dashboard/${workspaceId}/automations`,
icon: AlarmClock,
isActive: isAutomationsActive,
},
{
title: "Artifacts",
url: `/dashboard/${searchSpaceId}/artifacts`,
url: `/dashboard/${workspaceId}/artifacts`,
icon: Boxes,
isActive: isArtifactsActive,
},
@ -377,63 +376,63 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
isInboxSidebarOpen,
isDocumentsSidebarOpen,
totalUnreadCount,
searchSpaceId,
workspaceId,
isAutomationsActive,
isArtifactsActive,
]
);
// Handlers
const handleSearchSpaceSelect = useCallback(
const handleWorkspaceSelect = useCallback(
(id: number) => {
router.push(`/dashboard/${id}/new-chat`);
},
[router]
);
const handleAddSearchSpace = useCallback(() => {
setIsCreateSearchSpaceDialogOpen(true);
const handleAddWorkspace = useCallback(() => {
setIsCreateWorkspaceDialogOpen(true);
}, []);
const setAnnouncementsDialog = useSetAtom(announcementsDialogAtom);
const handleUserSettings = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/user-settings/profile`);
}, [router, searchSpaceId]);
router.push(`/dashboard/${workspaceId}/user-settings/profile`);
}, [router, workspaceId]);
const handleAnnouncements = useCallback(() => {
setAnnouncementsDialog(true);
}, [setAnnouncementsDialog]);
const handleSearchSpaceSettings = useCallback(
(space: SearchSpace) => {
const handleWorkspaceSettings = useCallback(
(space: Workspace) => {
router.push(`/dashboard/${space.id}/workspace-settings`);
},
[router]
);
const handleSearchSpaceDeleteClick = useCallback((space: SearchSpace) => {
const handleWorkspaceDeleteClick = useCallback((space: Workspace) => {
// If user is owner, show delete dialog; otherwise show leave dialog
if (space.isOwner) {
setSearchSpaceToDelete(space);
setShowDeleteSearchSpaceDialog(true);
setWorkspaceToDelete(space);
setShowDeleteWorkspaceDialog(true);
} else {
setSearchSpaceToLeave(space);
setShowLeaveSearchSpaceDialog(true);
setWorkspaceToLeave(space);
setShowLeaveWorkspaceDialog(true);
}
}, []);
const confirmDeleteSearchSpace = useCallback(async () => {
if (!searchSpaceToDelete) return;
setIsDeletingSearchSpace(true);
const confirmDeleteWorkspace = useCallback(async () => {
if (!workspaceToDelete) return;
setIsDeletingWorkspace(true);
try {
await deleteSearchSpace({ id: searchSpaceToDelete.id });
await deleteWorkspace({ id: workspaceToDelete.id });
const isCurrentSpace = Number(searchSpaceId) === searchSpaceToDelete.id;
const isCurrentSpace = Number(workspaceId) === workspaceToDelete.id;
// Await refetch so we have the freshest list (backend now hides [DELETING] spaces)
const result = await refetchSearchSpaces();
const updatedSpaces = (result.data ?? []).filter((s) => s.id !== searchSpaceToDelete.id);
const result = await refetchWorkspaces();
const updatedSpaces = (result.data ?? []).filter((s) => s.id !== workspaceToDelete.id);
if (isCurrentSpace) {
if (updatedSpaces.length > 0) {
@ -443,27 +442,27 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}
}
} catch (error) {
console.error("Error deleting search space:", error);
console.error("Error deleting workspace:", error);
toast.error(
t.has("delete_space_error") ? t("delete_space_error") : "Failed to delete search space"
t.has("delete_space_error") ? t("delete_space_error") : "Failed to delete workspace"
);
} finally {
setIsDeletingSearchSpace(false);
setShowDeleteSearchSpaceDialog(false);
setSearchSpaceToDelete(null);
setIsDeletingWorkspace(false);
setShowDeleteWorkspaceDialog(false);
setWorkspaceToDelete(null);
}
}, [searchSpaceToDelete, deleteSearchSpace, refetchSearchSpaces, searchSpaceId, router, t]);
}, [workspaceToDelete, deleteWorkspace, refetchWorkspaces, workspaceId, router, t]);
const confirmLeaveSearchSpace = useCallback(async () => {
if (!searchSpaceToLeave) return;
setIsLeavingSearchSpace(true);
const confirmLeaveWorkspace = useCallback(async () => {
if (!workspaceToLeave) return;
setIsLeavingWorkspace(true);
try {
await searchSpacesApiService.leaveSearchSpace(searchSpaceToLeave.id);
await workspacesApiService.leaveWorkspace(workspaceToLeave.id);
const isCurrentSpace = Number(searchSpaceId) === searchSpaceToLeave.id;
const isCurrentSpace = Number(workspaceId) === workspaceToLeave.id;
const result = await refetchSearchSpaces();
const updatedSpaces = (result.data ?? []).filter((s) => s.id !== searchSpaceToLeave.id);
const result = await refetchWorkspaces();
const updatedSpaces = (result.data ?? []).filter((s) => s.id !== workspaceToLeave.id);
if (isCurrentSpace) {
if (updatedSpaces.length > 0) {
@ -473,14 +472,14 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}
}
} catch (error) {
console.error("Error leaving search space:", error);
toast.error(t.has("leave_error") ? t("leave_error") : "Failed to leave search space");
console.error("Error leaving workspace:", error);
toast.error(t.has("leave_error") ? t("leave_error") : "Failed to leave workspace");
} finally {
setIsLeavingSearchSpace(false);
setShowLeaveSearchSpaceDialog(false);
setSearchSpaceToLeave(null);
setIsLeavingWorkspace(false);
setShowLeaveWorkspaceDialog(false);
setWorkspaceToLeave(null);
}
}, [searchSpaceToLeave, refetchSearchSpaces, searchSpaceId, router, t]);
}, [workspaceToLeave, refetchWorkspaces, workspaceId, router, t]);
const handleTabSwitch = useCallback(
(tab: Tab) => {
@ -489,14 +488,14 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
id: tab.chatId ?? null,
title: tab.title,
url: tab.chatUrl,
searchSpaceId: tab.searchSpaceId ?? searchSpaceId,
workspaceId: tab.workspaceId ?? workspaceId,
...(tab.visibility !== undefined ? { visibility: tab.visibility } : {}),
...(tab.hasComments !== undefined ? { hasComments: tab.hasComments } : {}),
});
}
// Document tabs are handled in-place by LayoutShell — no navigation needed
},
[activateChatThread, searchSpaceId]
[activateChatThread, workspaceId]
);
const handleTabPrefetch = useCallback(
@ -542,15 +541,15 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
if (isOutOfSync) {
resetCurrentThread();
// Immediately set the browser URL so the page remounts with a clean /new-chat path
window.history.replaceState(null, "", `/dashboard/${searchSpaceId}/new-chat`);
window.history.replaceState(null, "", `/dashboard/${workspaceId}/new-chat`);
// Force-remount the page component to reset all React state synchronously
setChatResetKey((k) => k + 1);
// Sync Next.js router internals so useParams/usePathname stay correct going forward
router.replace(`/dashboard/${searchSpaceId}/new-chat`);
router.replace(`/dashboard/${workspaceId}/new-chat`);
} else {
router.push(`/dashboard/${searchSpaceId}/new-chat`);
router.push(`/dashboard/${workspaceId}/new-chat`);
}
}, [router, searchSpaceId, currentThreadState.id, params?.chat_id, resetCurrentThread]);
}, [router, workspaceId, currentThreadState.id, params?.chat_id, resetCurrentThread]);
const handleChatSelect = useCallback(
(chat: ChatItem) => {
@ -558,11 +557,11 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
id: chat.id,
title: chat.name,
url: chat.url,
searchSpaceId,
workspaceId,
...(chat.visibility !== undefined ? { visibility: chat.visibility } : {}),
});
},
[activateChatThread, searchSpaceId]
[activateChatThread, workspaceId]
);
const handleChatDelete = useCallback((chat: ChatItem) => {
@ -595,12 +594,12 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
);
const handleSettings = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/workspace-settings`);
}, [router, searchSpaceId]);
router.push(`/dashboard/${workspaceId}/workspace-settings`);
}, [router, workspaceId]);
const handleManageMembers = useCallback(() => {
router.push(`/dashboard/${searchSpaceId}/team`);
}, [router, searchSpaceId]);
router.push(`/dashboard/${workspaceId}/team`);
}, [router, workspaceId]);
const handleLogout = useCallback(async () => {
try {
@ -634,7 +633,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
id: fallbackTab.chatId ?? null,
title: fallbackTab.title,
url: fallbackTab.chatUrl,
searchSpaceId: fallbackTab.searchSpaceId ?? searchSpaceId,
workspaceId: fallbackTab.workspaceId ?? workspaceId,
...(fallbackTab.visibility !== undefined ? { visibility: fallbackTab.visibility } : {}),
...(fallbackTab.hasComments !== undefined
? { hasComments: fallbackTab.hasComments }
@ -643,10 +642,10 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
} else {
const isOutOfSync = currentThreadState.id !== null && !params?.chat_id;
if (isOutOfSync) {
window.history.replaceState(null, "", `/dashboard/${searchSpaceId}/new-chat`);
window.history.replaceState(null, "", `/dashboard/${workspaceId}/new-chat`);
setChatResetKey((k) => k + 1);
} else {
router.push(`/dashboard/${searchSpaceId}/new-chat`);
router.push(`/dashboard/${workspaceId}/new-chat`);
}
}
}
@ -660,7 +659,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}, [
chatToDelete,
deleteThread,
searchSpaceId,
workspaceId,
resetCurrentThread,
currentChatId,
currentThreadState.id,
@ -695,7 +694,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
// Detect if we're on the chat page (needs overflow-hidden for chat's own scroll)
const isChatPage = pathname?.includes("/new-chat") ?? false;
const isUserSettingsPage = pathname?.includes("/user-settings") === true;
const isSearchSpaceSettingsPage = pathname?.includes("/workspace-settings") === true;
const isWorkspaceSettingsPage = pathname?.includes("/workspace-settings") === true;
const isTeamPage = pathname?.endsWith("/team") === true;
const isAutomationsPage = pathname?.includes("/automations") === true;
const isArtifactsPage = pathname?.endsWith("/artifacts") === true;
@ -703,15 +702,15 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const handleViewAllChats = useCallback(() => {
setActiveSlideoutPanel(null);
router.push(
isAllChatsPage ? `/dashboard/${searchSpaceId}/new-chat` : `/dashboard/${searchSpaceId}/chats`
isAllChatsPage ? `/dashboard/${workspaceId}/new-chat` : `/dashboard/${workspaceId}/chats`
);
}, [isAllChatsPage, router, searchSpaceId]);
}, [isAllChatsPage, router, workspaceId]);
const useWorkspacePanel =
pathname?.endsWith("/buy-more") === true ||
pathname?.endsWith("/earn-credits") === true ||
pathname?.endsWith("/more-pages") === true ||
isUserSettingsPage ||
isSearchSpaceSettingsPage ||
isWorkspaceSettingsPage ||
isTeamPage ||
isAutomationsPage ||
isArtifactsPage ||
@ -720,13 +719,13 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
return (
<>
<LayoutShell
searchSpaces={searchSpaces}
activeSearchSpaceId={Number(searchSpaceId)}
onSearchSpaceSelect={handleSearchSpaceSelect}
onSearchSpaceDelete={handleSearchSpaceDeleteClick}
onSearchSpaceSettings={handleSearchSpaceSettings}
onAddSearchSpace={handleAddSearchSpace}
searchSpace={activeSearchSpace}
workspaces={workspaces}
activeWorkspaceId={Number(workspaceId)}
onWorkspaceSelect={handleWorkspaceSelect}
onWorkspaceDelete={handleWorkspaceDeleteClick}
onWorkspaceSettings={handleWorkspaceSettings}
onAddWorkspace={handleAddWorkspace}
workspace={activeWorkspace}
navItems={navItems}
onNavItemClick={handleNavItemClick}
chats={chats}
@ -756,7 +755,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
useWorkspacePanel={useWorkspacePanel}
workspacePanelViewportClassName={
isUserSettingsPage ||
isSearchSpaceSettingsPage ||
isWorkspaceSettingsPage ||
isTeamPage ||
isAutomationsPage ||
isArtifactsPage ||
@ -769,7 +768,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
? "max-w-none select-none"
: isAllChatsPage
? "max-w-5xl"
: isUserSettingsPage || isSearchSpaceSettingsPage || isTeamPage || isArtifactsPage
: isUserSettingsPage || isWorkspaceSettingsPage || isTeamPage || isArtifactsPage
? "max-w-5xl"
: undefined
}
@ -888,66 +887,64 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
</DialogContent>
</Dialog>
{/* Delete Search Space Dialog */}
<AlertDialog open={showDeleteSearchSpaceDialog} onOpenChange={setShowDeleteSearchSpaceDialog}>
{/* Delete Workspace Dialog */}
<AlertDialog open={showDeleteWorkspaceDialog} onOpenChange={setShowDeleteWorkspaceDialog}>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>{t("delete_search_space")}</AlertDialogTitle>
<AlertDialogTitle>{t("delete_workspace")}</AlertDialogTitle>
<AlertDialogDescription>
{t("delete_space_confirm", { name: searchSpaceToDelete?.name || "" })}
{t("delete_space_confirm", { name: workspaceToDelete?.name || "" })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeletingSearchSpace}>
<AlertDialogCancel disabled={isDeletingWorkspace}>
{tCommon("cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
confirmDeleteSearchSpace();
confirmDeleteWorkspace();
}}
disabled={isDeletingSearchSpace}
disabled={isDeletingWorkspace}
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
<span className={isDeletingSearchSpace ? "opacity-0" : ""}>{tCommon("delete")}</span>
{isDeletingSearchSpace && <Spinner size="sm" className="absolute" />}
<span className={isDeletingWorkspace ? "opacity-0" : ""}>{tCommon("delete")}</span>
{isDeletingWorkspace && <Spinner size="sm" className="absolute" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Leave Search Space Dialog */}
<AlertDialog open={showLeaveSearchSpaceDialog} onOpenChange={setShowLeaveSearchSpaceDialog}>
{/* Leave Workspace Dialog */}
<AlertDialog open={showLeaveWorkspaceDialog} onOpenChange={setShowLeaveWorkspaceDialog}>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>{t("leave_title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("leave_confirm", { name: searchSpaceToLeave?.name || "" })}
{t("leave_confirm", { name: workspaceToLeave?.name || "" })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isLeavingSearchSpace}>
{tCommon("cancel")}
</AlertDialogCancel>
<AlertDialogCancel disabled={isLeavingWorkspace}>{tCommon("cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
confirmLeaveSearchSpace();
confirmLeaveWorkspace();
}}
disabled={isLeavingSearchSpace}
disabled={isLeavingWorkspace}
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
<span className={isLeavingSearchSpace ? "opacity-0" : ""}>{t("leave")}</span>
{isLeavingSearchSpace && <Spinner size="sm" className="absolute" />}
<span className={isLeavingWorkspace ? "opacity-0" : ""}>{t("leave")}</span>
{isLeavingWorkspace && <Spinner size="sm" className="absolute" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Create Search Space Dialog */}
<CreateSearchSpaceDialog
open={isCreateSearchSpaceDialogOpen}
onOpenChange={setIsCreateSearchSpaceDialogOpen}
{/* Create Workspace Dialog */}
<CreateWorkspaceDialog
open={isCreateWorkspaceDialogOpen}
onOpenChange={setIsCreateWorkspaceDialogOpen}
/>
<AnnouncementsDialog />

View file

@ -1,7 +1,7 @@
import type { LucideIcon } from "lucide-react";
import type { DocumentsProcessingStatus } from "@/hooks/use-documents-processing";
export interface SearchSpace {
export interface Workspace {
id: number;
name: string;
description?: string | null;
@ -41,15 +41,15 @@ export interface PageUsage {
}
export interface IconRailProps {
searchSpaces: SearchSpace[];
activeSearchSpaceId: number | null;
onSearchSpaceSelect: (id: number) => void;
onAddSearchSpace: () => void;
workspaces: Workspace[];
activeWorkspaceId: number | null;
onWorkspaceSelect: (id: number) => void;
onAddWorkspace: () => void;
className?: string;
}
export interface SidebarHeaderProps {
searchSpace: SearchSpace | null;
workspace: Workspace | null;
onSettings?: () => void;
}
@ -71,23 +71,23 @@ export interface ChatsSectionProps {
onChatSelect: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void;
onViewAllChats?: () => void;
searchSpaceId?: string;
workspaceId?: string;
}
export interface SidebarUserProfileProps {
user: User;
searchSpaceId?: string;
workspaceId?: string;
onSettings?: () => void;
onManageMembers?: () => void;
onSwitchSearchSpace?: () => void;
onSwitchWorkspace?: () => void;
onToggleTheme?: () => void;
onLogout?: () => void;
theme?: string;
}
export interface SidebarProps {
searchSpace: SearchSpace | null;
searchSpaceId?: string;
workspace: Workspace | null;
workspaceId?: string;
navItems: NavItem[];
chats: ChatItem[];
activeChatId?: number | null;
@ -106,10 +106,10 @@ export interface SidebarProps {
}
export interface LayoutShellProps {
searchSpaces: SearchSpace[];
activeSearchSpaceId: number | null;
onSearchSpaceSelect: (id: number) => void;
onAddSearchSpace: () => void;
workspaces: Workspace[];
activeWorkspaceId: number | null;
onWorkspaceSelect: (id: number) => void;
onAddWorkspace: () => void;
sidebarProps: Omit<SidebarProps, "className">;
children: React.ReactNode;
className?: string;

View file

@ -7,7 +7,7 @@ import { useTranslations } from "next-intl";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { createSearchSpaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { createWorkspaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { Button } from "@/components/ui/button";
import {
Dialog,
@ -27,7 +27,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { trackSearchSpaceCreated } from "@/lib/posthog/events";
import { trackWorkspaceCreated } from "@/lib/posthog/events";
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
@ -36,18 +36,18 @@ const formSchema = z.object({
type FormValues = z.infer<typeof formSchema>;
interface CreateSearchSpaceDialogProps {
interface CreateWorkspaceDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpaceDialogProps) {
const t = useTranslations("searchSpace");
export function CreateWorkspaceDialog({ open, onOpenChange }: CreateWorkspaceDialogProps) {
const t = useTranslations("workspace");
const tCommon = useTranslations("common");
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const { mutateAsync: createSearchSpace } = useAtomValue(createSearchSpaceMutationAtom);
const { mutateAsync: createWorkspace } = useAtomValue(createWorkspaceMutationAtom);
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
@ -60,16 +60,16 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
const handleSubmit = async (values: FormValues) => {
setIsSubmitting(true);
try {
const result = await createSearchSpace({
const result = await createWorkspace({
name: values.name,
description: values.description || "",
});
trackSearchSpaceCreated(result.id, values.name);
trackWorkspaceCreated(result.id, values.name);
router.push(`/dashboard/${result.id}/new-chat`);
} catch (error) {
console.error("Failed to create search space:", error);
console.error("Failed to create workspace:", error);
setIsSubmitting(false);
}
};

View file

@ -1 +1 @@
export { CreateSearchSpaceDialog } from "./CreateWorkspaceDialog";
export { CreateWorkspaceDialog } from "./CreateWorkspaceDialog";

View file

@ -16,7 +16,7 @@ interface HeaderProps {
export function Header({ mobileMenuTrigger }: HeaderProps) {
const pathname = usePathname();
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const activeTab = useAtomValue(activeTabAtom);
const isFreePage = pathname?.startsWith("/free") ?? false;
@ -26,14 +26,14 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
const currentThreadState = useAtomValue(currentThreadAtom);
const hasThread = isChatPage && !isDocumentTab && currentThreadState.id !== null;
const activeSearchSpaceId = searchSpaceId ? Number(searchSpaceId) : null;
const activeWorkspaceId = workspaceId ? Number(workspaceId) : null;
const canRenderShareButton =
hasThread &&
currentThreadState.id !== null &&
currentThreadState.visibility !== null &&
currentThreadState.searchSpaceId !== null &&
activeSearchSpaceId !== null &&
currentThreadState.searchSpaceId === activeSearchSpaceId;
currentThreadState.workspaceId !== null &&
activeWorkspaceId !== null &&
currentThreadState.workspaceId === activeWorkspaceId;
// Free chat pages have their own header with model selector; only render mobile trigger
if (isFreePage) {
@ -50,13 +50,13 @@ export function Header({ mobileMenuTrigger }: HeaderProps) {
canRenderShareButton &&
currentThreadState.id !== null &&
currentThreadState.visibility !== null &&
currentThreadState.searchSpaceId !== null
currentThreadState.workspaceId !== null
) {
threadForButton = {
id: currentThreadState.id,
visibility: currentThreadState.visibility,
created_by_id: null,
workspace_id: currentThreadState.searchSpaceId,
workspace_id: currentThreadState.workspaceId,
title: "",
archived: false,
created_at: "",

View file

@ -5,17 +5,17 @@ import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { NavItem, SearchSpace, User } from "../../types/layout.types";
import type { NavItem, User, Workspace } from "../../types/layout.types";
import { SidebarUserProfile } from "../sidebar/SidebarUserProfile";
import { SearchSpaceAvatar } from "./WorkspaceAvatar";
import { WorkspaceAvatar } from "./WorkspaceAvatar";
interface IconRailProps {
searchSpaces: SearchSpace[];
activeSearchSpaceId: number | null;
onSearchSpaceSelect: (id: number) => void;
onSearchSpaceDelete?: (searchSpace: SearchSpace) => void;
onSearchSpaceSettings?: (searchSpace: SearchSpace) => void;
onAddSearchSpace: () => void;
workspaces: Workspace[];
activeWorkspaceId: number | null;
onWorkspaceSelect: (id: number) => void;
onWorkspaceDelete?: (workspace: Workspace) => void;
onWorkspaceSettings?: (workspace: Workspace) => void;
onAddWorkspace: () => void;
isSingleRailMode?: boolean;
onNewChat?: () => void;
navItems?: NavItem[];
@ -31,12 +31,12 @@ interface IconRailProps {
}
export function IconRail({
searchSpaces,
activeSearchSpaceId,
onSearchSpaceSelect,
onSearchSpaceDelete,
onSearchSpaceSettings,
onAddSearchSpace,
workspaces,
activeWorkspaceId,
onWorkspaceSelect,
onWorkspaceDelete,
onWorkspaceSettings,
onAddWorkspace,
isSingleRailMode = false,
onNewChat,
navItems = [],
@ -77,18 +77,16 @@ export function IconRail({
<div className={cn("flex h-full w-14 min-h-0 flex-col items-center", className)}>
<ScrollArea className="w-full min-h-0 flex-1">
<div className="flex flex-col items-center gap-2 px-1.5 py-3">
{searchSpaces.map((searchSpace) => (
<SearchSpaceAvatar
key={searchSpace.id}
name={searchSpace.name}
isActive={searchSpace.id === activeSearchSpaceId}
isShared={searchSpace.memberCount > 1}
isOwner={searchSpace.isOwner}
onClick={() => onSearchSpaceSelect(searchSpace.id)}
onDelete={onSearchSpaceDelete ? () => onSearchSpaceDelete(searchSpace) : undefined}
onSettings={
onSearchSpaceSettings ? () => onSearchSpaceSettings(searchSpace) : undefined
}
{workspaces.map((workspace) => (
<WorkspaceAvatar
key={workspace.id}
name={workspace.name}
isActive={workspace.id === activeWorkspaceId}
isShared={workspace.memberCount > 1}
isOwner={workspace.isOwner}
onClick={() => onWorkspaceSelect(workspace.id)}
onDelete={onWorkspaceDelete ? () => onWorkspaceDelete(workspace) : undefined}
onSettings={onWorkspaceSettings ? () => onWorkspaceSettings(workspace) : undefined}
size="md"
/>
))}
@ -98,7 +96,7 @@ export function IconRail({
<Button
variant="ghost"
size="icon"
onClick={onAddSearchSpace}
onClick={onAddWorkspace}
className="h-10 w-10 rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-muted-foreground/50"
>
<Plus className="h-5 w-5 text-muted-foreground" />

View file

@ -14,7 +14,7 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
interface SearchSpaceAvatarProps {
interface WorkspaceAvatarProps {
name: string;
isActive?: boolean;
isShared?: boolean;
@ -27,7 +27,7 @@ interface SearchSpaceAvatarProps {
}
/**
* Generates a consistent color based on search space name
* Generates a consistent color based on workspace name
*/
function stringToColor(str: string): string {
let hash = 0;
@ -48,7 +48,7 @@ function stringToColor(str: string): string {
}
/**
* Gets initials from search space name (max 2 chars)
* Gets initials from workspace name (max 2 chars)
*/
function getInitials(name: string): string {
const words = name.trim().split(/\s+/);
@ -58,7 +58,7 @@ function getInitials(name: string): string {
return name.slice(0, 2).toUpperCase();
}
export function SearchSpaceAvatar({
export function WorkspaceAvatar({
name,
isActive,
isShared,
@ -68,8 +68,8 @@ export function SearchSpaceAvatar({
onSettings,
size = "md",
disableTooltip = false,
}: SearchSpaceAvatarProps) {
const t = useTranslations("searchSpace");
}: WorkspaceAvatarProps) {
const t = useTranslations("workspace");
const tCommon = useTranslations("common");
const bgColor = stringToColor(name);
const initials = getInitials(name);

View file

@ -1,3 +1,3 @@
export { IconRail } from "./IconRail";
export { NavIcon } from "./NavIcon";
export { SearchSpaceAvatar } from "./WorkspaceAvatar";
export { WorkspaceAvatar } from "./WorkspaceAvatar";

View file

@ -1,6 +1,6 @@
export { CreateSearchSpaceDialog } from "./dialogs";
export { CreateWorkspaceDialog } from "./dialogs";
export { Header } from "./header";
export { IconRail, NavIcon, SearchSpaceAvatar } from "./icon-rail";
export { IconRail, NavIcon, WorkspaceAvatar } from "./icon-rail";
export { LayoutShell } from "./shell";
export {
ChatListItem,

View file

@ -274,7 +274,7 @@ export function RightPanel({ showTopBorder = false }: RightPanelProps) {
documentId={editorState.documentId ?? undefined}
localFilePath={editorState.localFilePath ?? undefined}
memoryScope={editorState.memoryScope ?? undefined}
searchSpaceId={editorState.searchSpaceId ?? undefined}
workspaceId={editorState.workspaceId ?? undefined}
title={editorState.title}
onClose={closeEditor}
/>

View file

@ -18,7 +18,7 @@ import {
SIDEBAR_MIN_WIDTH,
useSidebarResize,
} from "../../hooks/useSidebarResize";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import type { ChatItem, NavItem, PageUsage, User, Workspace } from "../../types/layout.types";
import { Header } from "../header";
import { IconRail } from "../icon-rail";
import {
@ -102,13 +102,13 @@ interface InboxProps {
}
interface LayoutShellProps {
searchSpaces: SearchSpace[];
activeSearchSpaceId: number | null;
onSearchSpaceSelect: (id: number) => void;
onSearchSpaceDelete?: (searchSpace: SearchSpace) => void;
onSearchSpaceSettings?: (searchSpace: SearchSpace) => void;
onAddSearchSpace: () => void;
searchSpace: SearchSpace | null;
workspaces: Workspace[];
activeWorkspaceId: number | null;
onWorkspaceSelect: (id: number) => void;
onWorkspaceDelete?: (workspace: Workspace) => void;
onWorkspaceSettings?: (workspace: Workspace) => void;
onAddWorkspace: () => void;
workspace: Workspace | null;
navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[];
@ -186,12 +186,12 @@ function MainContentPanel({
<div className="relative flex flex-1 flex-col bg-panel overflow-hidden min-w-0">
<Header />
{isDocumentTab && activeTab.documentId && activeTab.searchSpaceId ? (
{isDocumentTab && activeTab.documentId && activeTab.workspaceId ? (
<div className="flex-1 overflow-hidden">
<DocumentTabContent
key={activeTab.documentId}
documentId={activeTab.documentId}
searchSpaceId={activeTab.searchSpaceId}
workspaceId={activeTab.workspaceId}
title={activeTab.title}
/>
</div>
@ -210,13 +210,13 @@ function DesktopWorkspaceRegion({ children }: { children: React.ReactNode }) {
}
export function LayoutShell({
searchSpaces,
activeSearchSpaceId,
onSearchSpaceSelect,
onSearchSpaceDelete,
onSearchSpaceSettings,
onAddSearchSpace,
searchSpace,
workspaces,
activeWorkspaceId,
onWorkspaceSelect,
onWorkspaceDelete,
onWorkspaceSettings,
onAddWorkspace,
workspace,
navItems,
onNavItemClick,
chats,
@ -295,11 +295,11 @@ export function LayoutShell({
<MobileSidebar
isOpen={mobileMenuOpen}
onOpenChange={setMobileMenuOpen}
searchSpaces={searchSpaces}
activeSearchSpaceId={activeSearchSpaceId}
onSearchSpaceSelect={onSearchSpaceSelect}
onAddSearchSpace={onAddSearchSpace}
searchSpace={searchSpace}
workspaces={workspaces}
activeWorkspaceId={activeWorkspaceId}
onWorkspaceSelect={onWorkspaceSelect}
onAddWorkspace={onAddWorkspace}
workspace={workspace}
navItems={navItems}
onNavItemClick={onNavItemClick}
chats={chats}
@ -392,12 +392,12 @@ export function LayoutShell({
)}
>
<IconRail
searchSpaces={searchSpaces}
activeSearchSpaceId={activeSearchSpaceId}
onSearchSpaceSelect={onSearchSpaceSelect}
onSearchSpaceDelete={onSearchSpaceDelete}
onSearchSpaceSettings={onSearchSpaceSettings}
onAddSearchSpace={onAddSearchSpace}
workspaces={workspaces}
activeWorkspaceId={activeWorkspaceId}
onWorkspaceSelect={onWorkspaceSelect}
onWorkspaceDelete={onWorkspaceDelete}
onWorkspaceSettings={onWorkspaceSettings}
onAddWorkspace={onAddWorkspace}
isSingleRailMode={false}
user={user}
onUserSettings={onUserSettings}
@ -417,7 +417,7 @@ export function LayoutShell({
)}
>
<Sidebar
searchSpace={searchSpace}
workspace={workspace}
isCollapsed={isCollapsed}
onToggleCollapse={toggleCollapsed}
navItems={navItems}

View file

@ -48,11 +48,11 @@ import { formatThreadTimestamp } from "@/lib/format-date";
import { cn } from "@/lib/utils";
interface AllChatsContentProps {
searchSpaceId: string;
workspaceId: string;
className?: string;
}
function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
function AllChatsContent({ workspaceId, className }: AllChatsContentProps) {
const t = useTranslations("sidebar");
const router = useRouter();
const params = useParams();
@ -60,9 +60,9 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
const isMobile = useIsMobile();
const removeChatTab = useSetAtom(removeChatTabAtom);
const { activateChatThread, prefetchChatThread } = useActivateChatThread();
const { mutateAsync: deleteThread } = useDeleteThread(searchSpaceId);
const { mutateAsync: archiveThread } = useArchiveThread(searchSpaceId);
const { mutateAsync: renameThread } = useRenameThread(searchSpaceId);
const { mutateAsync: deleteThread } = useDeleteThread(workspaceId);
const { mutateAsync: archiveThread } = useArchiveThread(workspaceId);
const { mutateAsync: renameThread } = useRenameThread(workspaceId);
const currentChatId = Array.isArray(params.chat_id)
? Number(params.chat_id[0])
@ -96,10 +96,10 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
error: threadsError,
isLoading: isLoadingThreads,
} = useQuery({
queryKey: ["all-threads", searchSpaceId],
queryFn: () => fetchThreads(Number(searchSpaceId)),
enabled: !!searchSpaceId && !isSearchMode,
placeholderData: () => queryClient.getQueryData(["threads", searchSpaceId, { limit: 40 }]),
queryKey: ["all-threads", workspaceId],
queryFn: () => fetchThreads(Number(workspaceId)),
enabled: !!workspaceId && !isSearchMode,
placeholderData: () => queryClient.getQueryData(["threads", workspaceId, { limit: 40 }]),
});
const {
@ -107,9 +107,9 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
error: searchError,
isLoading: isLoadingSearch,
} = useQuery({
queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery],
queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()),
enabled: !!searchSpaceId && isSearchMode,
queryKey: ["search-threads", workspaceId, debouncedSearchQuery],
queryFn: () => searchThreads(Number(workspaceId), debouncedSearchQuery.trim()),
enabled: !!workspaceId && isSearchMode,
});
const threads = useMemo(() => {
@ -127,11 +127,11 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
activateChatThread({
id: thread.id,
title: thread.title || "New Chat",
searchSpaceId,
workspaceId,
visibility: thread.visibility,
});
},
[activateChatThread, searchSpaceId]
[activateChatThread, workspaceId]
);
const handleDeleteThread = useCallback(
@ -153,7 +153,7 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
id: fallbackTab.chatId ?? null,
title: fallbackTab.title,
url: fallbackTab.chatUrl,
searchSpaceId: fallbackTab.searchSpaceId ?? searchSpaceId,
workspaceId: fallbackTab.workspaceId ?? workspaceId,
...(fallbackTab.visibility !== undefined
? { visibility: fallbackTab.visibility }
: {}),
@ -163,7 +163,7 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
});
return;
}
router.push(`/dashboard/${searchSpaceId}/new-chat`);
router.push(`/dashboard/${workspaceId}/new-chat`);
}, 250);
}
} catch (error) {
@ -173,7 +173,7 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
setDeletingThreadId(null);
}
},
[activateChatThread, deleteThread, t, currentChatId, router, removeChatTab, searchSpaceId]
[activateChatThread, deleteThread, t, currentChatId, router, removeChatTab, workspaceId]
);
const handleToggleArchive = useCallback(
@ -548,10 +548,10 @@ function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
);
}
export function AllChatsWorkspaceContent({ searchSpaceId }: { searchSpaceId: string }) {
export function AllChatsWorkspaceContent({ workspaceId }: { workspaceId: string }) {
return (
<div className="flex h-full min-h-0 w-full overflow-hidden text-sidebar-foreground">
<AllChatsContent searchSpaceId={searchSpaceId} />
<AllChatsContent workspaceId={workspaceId} />
</div>
);
}

View file

@ -25,7 +25,7 @@ interface DesktopLocalTabContentProps {
localRootPaths: string[];
canAddMoreLocalRoots: boolean;
maxLocalFilesystemRoots: number;
searchSpaceId: number;
workspaceId: number;
onPickFilesystemRoot: () => Promise<void> | void;
onRemoveFilesystemRoot: (rootPath: string) => Promise<void> | void;
onClearFilesystemRoots: () => Promise<void> | void;
@ -37,7 +37,7 @@ export function DesktopLocalTabContent({
localRootPaths,
canAddMoreLocalRoots,
maxLocalFilesystemRoots,
searchSpaceId,
workspaceId,
onPickFilesystemRoot,
onRemoveFilesystemRoot,
onClearFilesystemRoots,
@ -49,17 +49,17 @@ export function DesktopLocalTabContent({
const localSearchInputRef = useRef<HTMLInputElement>(null);
const [expandedFolderKeyMap, setExpandedFolderKeyMap] = useAtom(localExpandedFolderKeysAtom);
const expandedFolderKeys = useMemo(
() => new Set(expandedFolderKeyMap[searchSpaceId] ?? []),
[expandedFolderKeyMap, searchSpaceId]
() => new Set(expandedFolderKeyMap[workspaceId] ?? []),
[expandedFolderKeyMap, workspaceId]
);
const handleExpandedFolderKeysChange = useCallback(
(nextExpandedKeys: Set<string>) => {
setExpandedFolderKeyMap((prev) => ({
...prev,
[searchSpaceId]: Array.from(nextExpandedKeys),
[workspaceId]: Array.from(nextExpandedKeys),
}));
},
[searchSpaceId, setExpandedFolderKeyMap]
[workspaceId, setExpandedFolderKeyMap]
);
return (
@ -199,7 +199,7 @@ export function DesktopLocalTabContent({
</div>
<LocalFilesystemBrowser
rootPaths={localRootPaths}
searchSpaceId={searchSpaceId}
workspaceId={workspaceId}
active
searchQuery={debouncedLocalSearch.trim() || undefined}
onOpenFile={onOpenLocalFile}

View file

@ -17,7 +17,7 @@ import {
folderWatchDialogOpenAtom,
folderWatchInitialFolderAtom,
} from "@/atoms/folder-sync/folder-sync.atoms";
import { searchSpacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
import { workspacesAtom } from "@/atoms/workspaces/workspace-query.atoms";
import { CreateFolderDialog } from "@/components/documents/CreateFolderDialog";
import type { DocumentNodeDoc } from "@/components/documents/DocumentNode";
import { getDocumentTypeIcon } from "@/components/documents/DocumentTypeIcon";
@ -57,7 +57,7 @@ import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { useElectronAPI, usePlatform } from "@/hooks/use-platform";
import { documentsApiService } from "@/lib/apis/documents-api.service";
import { foldersApiService } from "@/lib/apis/folders-api.service";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { authenticatedFetch } from "@/lib/auth-fetch";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { getDocumentTypeLabel } from "@/lib/documents/document-type-labels";
@ -202,7 +202,7 @@ interface WatchedFolderEntry {
excludePatterns: string[];
fileExtensions: string[] | null;
rootFolderId: number | null;
searchSpaceId: number;
workspaceId: number;
active: boolean;
}
@ -264,7 +264,7 @@ function AuthenticatedDocumentsSidebarBase({
path: meta.folder_path as string,
name: bf.name,
rootFolderId: bf.id,
searchSpaceId: bf.workspace_id,
workspaceId: bf.workspace_id,
excludePatterns: (meta.exclude_patterns as string[]) ?? [],
fileExtensions: (meta.file_extensions as string[] | null) ?? null,
active: true,
@ -322,10 +322,10 @@ function AuthenticatedDocumentsSidebarBase({
// Zero queries for tree data
const [zeroFolders, zeroFoldersResult] = useQuery(
queries.folders.bySpace({ searchSpaceId: workspaceId })
queries.folders.bySpace({ workspaceId: workspaceId })
);
const [zeroAllDocs, zeroAllDocsResult] = useQuery(
queries.documents.bySpace({ searchSpaceId: workspaceId })
queries.documents.bySpace({ workspaceId: workspaceId })
);
const [agentCreatedDocs, setAgentCreatedDocs] = useAtom(agentCreatedDocumentsAtom);
@ -336,7 +336,7 @@ function AuthenticatedDocumentsSidebarBase({
name: f.name,
position: f.position,
parentId: f.parentId ?? null,
searchSpaceId: f.searchSpaceId,
workspaceId: f.workspaceId,
metadata: f.metadata as Record<string, unknown> | null | undefined,
})),
[zeroFolders]
@ -361,7 +361,7 @@ function AuthenticatedDocumentsSidebarBase({
const zeroIds = new Set(zeroDocs.map((d) => d.id));
const pendingAgentDocs = agentCreatedDocs
.filter((d) => d.searchSpaceId === workspaceId && !zeroIds.has(d.id))
.filter((d) => d.workspaceId === workspaceId && !zeroIds.has(d.id))
.map((d) => ({
id: d.id,
title: d.title,
@ -456,7 +456,7 @@ function AuthenticatedDocumentsSidebarBase({
await uploadFolderScan({
folderPath: matched.path,
folderName: matched.name,
searchSpaceId: workspaceId,
workspaceId: workspaceId,
excludePatterns: matched.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS,
fileExtensions:
matched.fileExtensions ?? Array.from(getSupportedExtensionsSet(undefined, etlService)),
@ -842,7 +842,7 @@ function AuthenticatedDocumentsSidebarBase({
openEditorPanel({
kind: "memory",
memoryScope: "user",
searchSpaceId: workspaceId,
workspaceId: workspaceId,
title: doc.title,
});
return true;
@ -851,7 +851,7 @@ function AuthenticatedDocumentsSidebarBase({
openEditorPanel({
kind: "memory",
memoryScope: "team",
searchSpaceId: workspaceId,
workspaceId: workspaceId,
title: doc.title,
});
return true;
@ -1020,7 +1020,7 @@ function AuthenticatedDocumentsSidebarBase({
if (openMemoryDocument(doc)) return;
openEditorPanel({
documentId: doc.id,
searchSpaceId: workspaceId,
workspaceId: workspaceId,
title: doc.title,
});
}}
@ -1028,7 +1028,7 @@ function AuthenticatedDocumentsSidebarBase({
if (openMemoryDocument(doc)) return;
openEditorPanel({
documentId: doc.id,
searchSpaceId: workspaceId,
workspaceId: workspaceId,
title: doc.title,
});
}}

View file

@ -166,7 +166,7 @@ export function InboxSidebarContent({
const router = useRouter();
const params = useParams();
const isMobile = !useMediaQuery("(min-width: 640px)");
const searchSpaceId = getWorkspaceIdNumber(params) ?? null;
const workspaceId = getWorkspaceIdNumber(params) ?? null;
const [, setTargetCommentId] = useAtom(setTargetCommentIdAtom);
@ -204,11 +204,11 @@ export function InboxSidebarContent({
// Server-side search query
const searchTypeFilter = activeTab === "comments" ? ("new_mention" as const) : undefined;
const { data: searchResponse, isLoading: isSearchLoading } = useQuery({
queryKey: cacheKeys.notifications.search(searchSpaceId, debouncedSearch.trim(), activeTab),
queryKey: cacheKeys.notifications.search(workspaceId, debouncedSearch.trim(), activeTab),
queryFn: () =>
notificationsApiService.getNotifications({
queryParams: {
workspace_id: searchSpaceId ?? undefined,
workspace_id: workspaceId ?? undefined,
type: searchTypeFilter,
search: debouncedSearch.trim(),
limit: 50,
@ -242,8 +242,8 @@ export function InboxSidebarContent({
// Fetch source types for the status tab filter
const { data: sourceTypesData } = useQuery({
queryKey: cacheKeys.notifications.sourceTypes(searchSpaceId),
queryFn: () => notificationsApiService.getSourceTypes(searchSpaceId ?? undefined),
queryKey: cacheKeys.notifications.sourceTypes(workspaceId),
queryFn: () => notificationsApiService.getSourceTypes(workspaceId ?? undefined),
staleTime: 60 * 1000,
enabled: activeTab === "status",
});
@ -364,17 +364,17 @@ export function InboxSidebarContent({
if (item.type === "new_mention") {
if (isNewMentionMetadata(item.metadata)) {
const searchSpaceId = item.workspace_id;
const workspaceId = item.workspace_id;
const threadId = item.metadata.thread_id;
const commentId = item.metadata.comment_id;
if (searchSpaceId && threadId) {
if (workspaceId && threadId) {
if (commentId) {
setTargetCommentId(commentId);
}
const url = commentId
? `/dashboard/${searchSpaceId}/new-chat/${threadId}?commentId=${commentId}`
: `/dashboard/${searchSpaceId}/new-chat/${threadId}`;
? `/dashboard/${workspaceId}/new-chat/${threadId}?commentId=${commentId}`
: `/dashboard/${workspaceId}/new-chat/${threadId}`;
onOpenChange(false);
onCloseMobileSidebar?.();
router.push(url);
@ -382,17 +382,17 @@ export function InboxSidebarContent({
}
} else if (item.type === "comment_reply") {
if (isCommentReplyMetadata(item.metadata)) {
const searchSpaceId = item.workspace_id;
const workspaceId = item.workspace_id;
const threadId = item.metadata.thread_id;
const replyId = item.metadata.reply_id;
if (searchSpaceId && threadId) {
if (workspaceId && threadId) {
if (replyId) {
setTargetCommentId(replyId);
}
const url = replyId
? `/dashboard/${searchSpaceId}/new-chat/${threadId}?commentId=${replyId}`
: `/dashboard/${searchSpaceId}/new-chat/${threadId}`;
? `/dashboard/${workspaceId}/new-chat/${threadId}?commentId=${replyId}`
: `/dashboard/${workspaceId}/new-chat/${threadId}`;
onOpenChange(false);
onCloseMobileSidebar?.();
router.push(url);

View file

@ -11,7 +11,7 @@ import { cn } from "@/lib/utils";
interface LocalFilesystemBrowserProps {
rootPaths: string[];
searchSpaceId: number;
workspaceId: number;
active?: boolean;
searchQuery?: string;
onOpenFile: (fullPath: string) => void;
@ -134,7 +134,7 @@ function getNormalizedExtension(pathValue: string): string {
export function LocalFilesystemBrowser({
rootPaths,
searchSpaceId,
workspaceId,
active = true,
searchQuery,
onOpenFile,
@ -184,7 +184,7 @@ export function LocalFilesystemBrowser({
}
const rootsToReload = rootEntries.filter(({ rootKey }) => {
const nonce = reloadNonceByRoot[rootKey] ?? 0;
const signature = `${searchSpaceId}:${rootKey}:${nonce}`;
const signature = `${workspaceId}:${rootKey}:${nonce}`;
return lastLoadedSignatureByRootRef.current.get(rootKey) !== signature;
});
if (rootsToReload.length === 0) {
@ -192,7 +192,7 @@ export function LocalFilesystemBrowser({
}
for (const { rootKey } of rootsToReload) {
const nonce = reloadNonceByRoot[rootKey] ?? 0;
lastLoadedSignatureByRootRef.current.set(rootKey, `${searchSpaceId}:${rootKey}:${nonce}`);
lastLoadedSignatureByRootRef.current.set(rootKey, `${workspaceId}:${rootKey}:${nonce}`);
}
let cancelled = false;
@ -212,7 +212,7 @@ export function LocalFilesystemBrowser({
try {
const files = (await electronAPI.listAgentFilesystemFiles({
rootPath,
searchSpaceId,
workspaceId,
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
})) as LocalFolderFileEntry[];
if (cancelled) return;
@ -241,7 +241,7 @@ export function LocalFilesystemBrowser({
return () => {
cancelled = true;
};
}, [active, electronAPI, isWindowsPlatform, reloadNonceByRoot, rootPaths, searchSpaceId]);
}, [active, electronAPI, isWindowsPlatform, reloadNonceByRoot, rootPaths, workspaceId]);
useEffect(() => {
if (active) return;
@ -254,19 +254,19 @@ export function LocalFilesystemBrowser({
if (!electronAPI?.onAgentFilesystemTreeDirty) return;
if (!active) return;
if (rootPaths.length === 0) {
void electronAPI.stopAgentFilesystemTreeWatch(searchSpaceId);
void electronAPI.stopAgentFilesystemTreeWatch(workspaceId);
return;
}
const unsubscribe = electronAPI.onAgentFilesystemTreeDirty(
(event: {
searchSpaceId: number | null;
workspaceId: number | null;
reason: "watcher_event" | "safety_poll";
rootPath: string;
changedPath: string | null;
timestamp: number;
}) => {
if ((event.searchSpaceId ?? null) !== (searchSpaceId ?? null)) {
if ((event.workspaceId ?? null) !== (workspaceId ?? null)) {
return;
}
const eventRootKey = normalizeRootPathForLookup(event.rootPath, isWindowsPlatform);
@ -290,16 +290,16 @@ export function LocalFilesystemBrowser({
}
);
void electronAPI.startAgentFilesystemTreeWatch({
searchSpaceId,
workspaceId,
rootPaths,
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
});
return () => {
unsubscribe();
void electronAPI.stopAgentFilesystemTreeWatch(searchSpaceId);
void electronAPI.stopAgentFilesystemTreeWatch(workspaceId);
};
}, [active, electronAPI, isWindowsPlatform, rootPaths, searchSpaceId]);
}, [active, electronAPI, isWindowsPlatform, rootPaths, workspaceId]);
useEffect(() => {
if (!electronAPI?.getAgentFilesystemMounts) {
@ -322,7 +322,7 @@ export function LocalFilesystemBrowser({
setMountRefreshInFlight(true);
}
void electronAPI
.getAgentFilesystemMounts(searchSpaceId)
.getAgentFilesystemMounts(workspaceId)
.then((mounts: LocalRootMount[]) => {
if (cancelled) return;
const next = new Map<string, string>();
@ -348,7 +348,7 @@ export function LocalFilesystemBrowser({
return () => {
cancelled = true;
};
}, [electronAPI, isWindowsPlatform, rootPaths, searchSpaceId]);
}, [electronAPI, isWindowsPlatform, rootPaths, workspaceId]);
const treeByRoot = useMemo(() => {
const query = searchQuery?.trim().toLowerCase() ?? "";

View file

@ -4,18 +4,18 @@ import { PanelLeft, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import { SearchSpaceAvatar } from "../icon-rail/WorkspaceAvatar";
import type { ChatItem, NavItem, PageUsage, User, Workspace } from "../../types/layout.types";
import { WorkspaceAvatar } from "../icon-rail/WorkspaceAvatar";
import { Sidebar } from "./Sidebar";
interface MobileSidebarProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
searchSpaces: SearchSpace[];
activeSearchSpaceId: number | null;
onSearchSpaceSelect: (id: number) => void;
onAddSearchSpace: () => void;
searchSpace: SearchSpace | null;
workspaces: Workspace[];
activeWorkspaceId: number | null;
onWorkspaceSelect: (id: number) => void;
onAddWorkspace: () => void;
workspace: Workspace | null;
navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[];
@ -62,12 +62,12 @@ export function MobileSidebarTrigger({ onClick }: { onClick: () => void }) {
export function MobileSidebar({
isOpen,
onOpenChange,
searchSpaces,
activeSearchSpaceId,
onSearchSpaceSelect,
workspaces,
activeWorkspaceId,
onWorkspaceSelect,
onAddSearchSpace,
searchSpace,
onAddWorkspace,
workspace,
navItems,
onNavItemClick,
chats,
@ -93,8 +93,8 @@ export function MobileSidebar({
setTheme,
isLoadingChats = false,
}: MobileSidebarProps) {
const handleSearchSpaceSelect = (id: number) => {
onSearchSpaceSelect(id);
const handleWorkspaceSelect = (id: number) => {
onWorkspaceSelect(id);
};
const handleNavItemClick = (item: NavItem) => {
@ -118,18 +118,18 @@ export function MobileSidebar({
>
<SheetTitle className="sr-only">Navigation</SheetTitle>
{/* Vertical Search Spaces Rail - left side */}
{/* Vertical Workspaces Rail - left side */}
<div className="flex h-full w-14 shrink-0 flex-col items-center border-r bg-rail">
<ScrollArea className="w-full flex-1">
<div className="flex flex-col items-center gap-2 px-1.5 py-3">
{searchSpaces.map((space) => (
<SearchSpaceAvatar
{workspaces.map((space) => (
<WorkspaceAvatar
key={space.id}
name={space.name}
isActive={space.id === activeSearchSpaceId}
isActive={space.id === activeWorkspaceId}
isShared={space.memberCount > 1}
isOwner={space.isOwner}
onClick={() => handleSearchSpaceSelect(space.id)}
onClick={() => handleWorkspaceSelect(space.id)}
size="md"
disableTooltip
/>
@ -137,7 +137,7 @@ export function MobileSidebar({
<Button
variant="ghost"
size="icon"
onClick={onAddSearchSpace}
onClick={onAddWorkspace}
className="h-10 w-10 shrink-0 rounded-lg border-2 border-dashed border-muted-foreground/30 hover:border-muted-foreground/50"
>
<Plus className="h-5 w-5 text-muted-foreground" />
@ -150,7 +150,7 @@ export function MobileSidebar({
{/* Sidebar Content - right side */}
<div className="flex-1 overflow-hidden flex flex-col [&>*]:!w-full">
<Sidebar
searchSpace={searchSpace}
workspace={workspace}
isCollapsed={false}
onToggleCollapse={() => onOpenChange(false)}
navItems={navItems}

View file

@ -13,7 +13,7 @@ import { useIsAnonymous } from "@/contexts/anonymous-mode";
import { getWorkspaceIdParam } from "@/lib/route-params";
import { cn } from "@/lib/utils";
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import type { ChatItem, NavItem, PageUsage, User, Workspace } from "../../types/layout.types";
import { ChatListItem } from "./ChatListItem";
import { CreditBalanceDisplay } from "./CreditBalanceDisplay";
import { DocumentsSidebar } from "./DocumentsSidebar";
@ -62,7 +62,7 @@ function CollapsedInboxIcon({ item }: { item: NavItem }) {
}
interface SidebarProps {
searchSpace: SearchSpace | null;
workspace: Workspace | null;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
navItems: NavItem[];
@ -103,7 +103,7 @@ interface SidebarProps {
}
export function Sidebar({
searchSpace,
workspace,
isCollapsed = false,
onToggleCollapse,
navItems,
@ -192,7 +192,7 @@ export function Sidebar({
aria-hidden={isCollapsed}
>
<SidebarHeader
searchSpace={searchSpace}
workspace={workspace}
isCollapsed={false}
onSettings={onSettings}
onManageMembers={onManageMembers}
@ -400,7 +400,7 @@ function SidebarUsageFooter({
onNavigate?: () => void;
}) {
const params = useParams();
const searchSpaceId = getWorkspaceIdParam(params) ?? "";
const workspaceId = getWorkspaceIdParam(params) ?? "";
const isAnonymous = useIsAnonymous();
if (isCollapsed) return null;
@ -446,7 +446,7 @@ function SidebarUsageFooter({
<CreditBalanceDisplay />
<div className="space-y-0.5">
<Link
href={`/dashboard/${searchSpaceId}/earn-credits`}
href={`/dashboard/${workspaceId}/earn-credits`}
onClick={onNavigate}
className="group flex w-full items-center justify-between rounded-md px-1.5 py-1 transition-colors hover:bg-accent"
>
@ -459,7 +459,7 @@ function SidebarUsageFooter({
</Badge>
</Link>
<Link
href={`/dashboard/${searchSpaceId}/buy-more`}
href={`/dashboard/${workspaceId}/buy-more`}
onClick={onNavigate}
className="group flex w-full items-center justify-between rounded-md px-1.5 py-1 transition-colors hover:bg-accent"
>

View file

@ -10,10 +10,10 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import type { SearchSpace } from "../../types/layout.types";
import type { Workspace } from "../../types/layout.types";
interface SidebarHeaderProps {
searchSpace: SearchSpace | null;
workspace: Workspace | null;
isCollapsed?: boolean;
onSettings?: () => void;
onManageMembers?: () => void;
@ -21,7 +21,7 @@ interface SidebarHeaderProps {
}
export function SidebarHeader({
searchSpace,
workspace,
isCollapsed,
onSettings,
onManageMembers,
@ -40,9 +40,7 @@ export function SidebarHeader({
isCollapsed && "w-10"
)}
>
<span className="truncate text-sm">
{searchSpace?.name ?? t("select_search_space")}
</span>
<span className="truncate text-sm">{workspace?.name ?? t("select_workspace")}</span>
<ChevronsUpDown className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
@ -53,7 +51,7 @@ export function SidebarHeader({
</DropdownMenuItem>
<DropdownMenuItem onClick={onSettings}>
<Settings className="h-4 w-4" />
{t("search_space_settings")}
{t("workspace_settings")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View file

@ -48,7 +48,7 @@ function DocumentSkeleton() {
interface DocumentTabContentProps {
documentId: number;
searchSpaceId: number;
workspaceId: number;
title?: string;
}
@ -69,7 +69,7 @@ function formatBytes(bytes: number): string {
return `${bytes}B`;
}
export function DocumentTabContent({ documentId, searchSpaceId, title }: DocumentTabContentProps) {
export function DocumentTabContent({ documentId, workspaceId, title }: DocumentTabContentProps) {
const [doc, setDoc] = useState<DocumentContent | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -104,7 +104,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
try {
const response = await authenticatedFetch(
buildBackendUrl(
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/editor-content`
`/api/v1/workspaces/${workspaceId}/documents/${documentId}/editor-content`
),
{ method: "GET" }
);
@ -140,7 +140,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
doFetch().catch(() => {});
return () => controller.abort();
}, [documentId, searchSpaceId]);
}, [documentId, workspaceId]);
const handleMarkdownChange = useCallback((md: string) => {
markdownRef.current = md;
@ -154,7 +154,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
setSaving(true);
try {
const response = await authenticatedFetch(
buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/save`),
buildBackendUrl(`/api/v1/workspaces/${workspaceId}/documents/${documentId}/save`),
{
method: "POST",
headers: { "Content-Type": "application/json" },
@ -183,7 +183,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
} finally {
setSaving(false);
}
}, [documentId, plateMaxBytes, searchSpaceId]);
}, [documentId, plateMaxBytes, workspaceId]);
if (isLoading) return <DocumentSkeleton />;
@ -313,7 +313,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen
try {
const response = await authenticatedFetch(
buildBackendUrl(
`/api/v1/workspaces/${searchSpaceId}/documents/${documentId}/download-markdown`
`/api/v1/workspaces/${workspaceId}/documents/${documentId}/download-markdown`
),
{ method: "GET" }
);

View file

@ -4,20 +4,20 @@ import { ImageModelSelector } from "./image-model-selector";
import { ModelSelector } from "./model-selector";
interface ChatHeaderProps {
searchSpaceId: number;
workspaceId: number;
className?: string;
onChatModelSelected?: () => void;
}
export function ChatHeader({ searchSpaceId, className, onChatModelSelected }: ChatHeaderProps) {
export function ChatHeader({ workspaceId, className, onChatModelSelected }: ChatHeaderProps) {
return (
<div className="flex min-w-0 shrink-0 items-center gap-2">
<ModelSelector
searchSpaceId={searchSpaceId}
workspaceId={workspaceId}
className={className}
onChatModelSelected={onChatModelSelected}
/>
<ImageModelSelector searchSpaceId={searchSpaceId} className={className} mobileIconOnly />
<ImageModelSelector workspaceId={workspaceId} className={className} mobileIconOnly />
</div>
);
}

View file

@ -38,8 +38,8 @@ const visibilityOptions: {
},
{
value: "SEARCH_SPACE",
label: "Search Space",
description: "All members of this search space can access",
label: "Workspace",
description: "All members of this workspace can access",
icon: Users,
},
];
@ -96,7 +96,7 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
onVisibilityChange?.(updatedThread.visibility);
toast.success(
newVisibility === "SEARCH_SPACE" ? "Chat shared with search space" : "Chat is now private"
newVisibility === "SEARCH_SPACE" ? "Chat shared with workspace" : "Chat is now private"
);
setOpen(false);
} catch (error) {

View file

@ -56,7 +56,7 @@ import { queries } from "@/zero/queries";
export type DocumentMentionPickerRef = ComposerSuggestionNavigatorRef;
interface DocumentMentionPickerProps {
searchSpaceId: number;
workspaceId: number;
onSelectionChange: (mentions: MentionedDocumentInfo[]) => void;
onDone: () => void;
initialSelectedDocuments?: MentionedDocumentInfo[];
@ -105,14 +105,14 @@ function isMentionedContextItem(value: unknown): value is MentionedDocumentInfo
return false;
}
function getRecentsStorageKey(searchSpaceId: number) {
return `${RECENTS_STORAGE_PREFIX}${searchSpaceId}`;
function getRecentsStorageKey(workspaceId: number) {
return `${RECENTS_STORAGE_PREFIX}${workspaceId}`;
}
function readRecentMentions(searchSpaceId: number): MentionedDocumentInfo[] {
function readRecentMentions(workspaceId: number): MentionedDocumentInfo[] {
if (typeof window === "undefined") return [];
try {
const raw = window.localStorage.getItem(getRecentsStorageKey(searchSpaceId));
const raw = window.localStorage.getItem(getRecentsStorageKey(workspaceId));
if (!raw) return [];
const parsed: unknown = JSON.parse(raw);
if (!Array.isArray(parsed)) return [];
@ -122,11 +122,11 @@ function readRecentMentions(searchSpaceId: number): MentionedDocumentInfo[] {
}
}
function writeRecentMentions(searchSpaceId: number, mentions: MentionedDocumentInfo[]) {
function writeRecentMentions(workspaceId: number, mentions: MentionedDocumentInfo[]) {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(
getRecentsStorageKey(searchSpaceId),
getRecentsStorageKey(workspaceId),
JSON.stringify(mentions.slice(0, RECENTS_LIMIT))
);
} catch {
@ -134,13 +134,13 @@ function writeRecentMentions(searchSpaceId: number, mentions: MentionedDocumentI
}
}
export function promoteRecentMention(searchSpaceId: number, mention: MentionedDocumentInfo) {
export function promoteRecentMention(workspaceId: number, mention: MentionedDocumentInfo) {
const mentionKey = getMentionDocKey(mention);
const next = [
mention,
...readRecentMentions(searchSpaceId).filter((item) => getMentionDocKey(item) !== mentionKey),
...readRecentMentions(workspaceId).filter((item) => getMentionDocKey(item) !== mentionKey),
].slice(0, RECENTS_LIMIT);
writeRecentMentions(searchSpaceId, next);
writeRecentMentions(workspaceId, next);
return next;
}
@ -261,7 +261,7 @@ export const DocumentMentionPicker = forwardRef<
DocumentMentionPickerProps
>(function DocumentMentionPicker(
{
searchSpaceId,
workspaceId,
onSelectionChange,
onDone,
initialSelectedDocuments = [],
@ -286,15 +286,15 @@ export const DocumentMentionPicker = forwardRef<
const [hasMore, setHasMore] = useState(false);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [recentMentions, setRecentMentions] = useState<MentionedDocumentInfo[]>(() =>
readRecentMentions(searchSpaceId)
readRecentMentions(workspaceId)
);
const [zeroFolders] = useZeroQuery(queries.folders.bySpace({ searchSpaceId }));
const [zeroFolders] = useZeroQuery(queries.folders.bySpace({ workspaceId }));
const { data: connectors = [], isLoading: isConnectorsLoading } = useAtomValue(connectorsAtom);
const activeConnectors = useMemo(() => connectors.filter(isConnectorActive), [connectors]);
const paginationScopeKey = useMemo(
() => `${searchSpaceId}:${debouncedSearch}`,
[searchSpaceId, debouncedSearch]
() => `${workspaceId}:${debouncedSearch}`,
[workspaceId, debouncedSearch]
);
const previousPaginationScopeKeyRef = useRef<string | null>(null);
@ -311,17 +311,17 @@ export const DocumentMentionPicker = forwardRef<
}, [hasSearch]);
useEffect(() => {
setRecentMentions(readRecentMentions(searchSpaceId));
}, [searchSpaceId]);
setRecentMentions(readRecentMentions(workspaceId));
}, [workspaceId]);
const titleSearchParams = useMemo(
() => ({
workspace_id: searchSpaceId,
workspace_id: workspaceId,
page: 0,
page_size: PAGE_SIZE,
...(isSearchValid ? { title: debouncedSearch.trim() } : {}),
}),
[searchSpaceId, debouncedSearch, isSearchValid]
[workspaceId, debouncedSearch, isSearchValid]
);
const { data: titleSearchResults, isLoading: isTitleSearchLoading } = useQuery({
@ -329,7 +329,7 @@ export const DocumentMentionPicker = forwardRef<
queryFn: ({ signal }) =>
documentsApiService.searchDocumentTitles({ queryParams: titleSearchParams }, signal),
staleTime: 60 * 1000,
enabled: !!searchSpaceId && currentPage === 0 && (!hasSearch || isSearchValid),
enabled: !!workspaceId && currentPage === 0 && (!hasSearch || isSearchValid),
placeholderData: keepPreviousData,
});
@ -362,7 +362,7 @@ export const DocumentMentionPicker = forwardRef<
try {
const queryParams = {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
page: nextPage,
page_size: PAGE_SIZE,
...(isSearchValid ? { title: debouncedSearch.trim() } : {}),
@ -381,7 +381,7 @@ export const DocumentMentionPicker = forwardRef<
} finally {
setIsLoadingMore(false);
}
}, [currentPage, hasMore, isLoadingMore, debouncedSearch, searchSpaceId, isSearchValid]);
}, [currentPage, hasMore, isLoadingMore, debouncedSearch, workspaceId, isSearchValid]);
const actualDocuments = useMemo(() => {
if (!isSingleCharSearch) return accumulatedDocuments;
@ -406,10 +406,10 @@ export const DocumentMentionPicker = forwardRef<
// or types a search. An empty title returns recent threads (the
// backend ``ilike '%%'`` matches all, newest first).
const { data: threadResults = [], isLoading: isThreadsLoading } = useQuery({
queryKey: ["composer-mention-threads", searchSpaceId, debouncedSearch],
queryFn: () => searchThreads(searchSpaceId, debouncedSearch.trim()),
queryKey: ["composer-mention-threads", workspaceId, debouncedSearch],
queryFn: () => searchThreads(workspaceId, debouncedSearch.trim()),
staleTime: 60 * 1000,
enabled: enableChatMentions && !!searchSpaceId && (view.kind === "chats" || hasSearch),
enabled: enableChatMentions && !!workspaceId && (view.kind === "chats" || hasSearch),
placeholderData: keepPreviousData,
});
const threadMentions = useMemo(
@ -425,7 +425,7 @@ export const DocumentMentionPicker = forwardRef<
[recentDocMentions]
);
const { data: hydratedRecentDocs = [], isFetched: hasHydratedRecentDocs } = useQuery({
queryKey: ["composer-mention-recent-docs", searchSpaceId, recentDocIdsKey],
queryKey: ["composer-mention-recent-docs", workspaceId, recentDocIdsKey],
queryFn: async () => {
const results = await Promise.allSettled(
recentDocMentions.map((mention) => documentsApiService.getDocument({ id: mention.id }))

View file

@ -31,7 +31,7 @@ import { cn } from "@/lib/utils";
import { providerDisplay } from "../settings/model-connections/provider-metadata";
interface ImageModelSelectorProps {
searchSpaceId: number;
workspaceId: number;
className?: string;
mobileIconOnly?: boolean;
}
@ -97,7 +97,7 @@ function groupedModels(models: ImageModel[]) {
}
export function ImageModelSelector({
searchSpaceId,
workspaceId,
className,
mobileIconOnly = false,
}: ImageModelSelectorProps) {
@ -146,7 +146,7 @@ export function ImageModelSelector({
function manageModelConnections() {
setOpen(false);
router.push(`/dashboard/${searchSpaceId}/workspace-settings/models`);
router.push(`/dashboard/${workspaceId}/workspace-settings/models`);
}
const handleScroll = useCallback((event: UIEvent<HTMLDivElement>) => {

View file

@ -31,7 +31,7 @@ import { cn } from "@/lib/utils";
import { providerDisplay } from "../settings/model-connections/provider-metadata";
interface ModelSelectorProps {
searchSpaceId: number;
workspaceId: number;
className?: string;
onChatModelSelected?: () => void;
}
@ -96,11 +96,7 @@ function groupedModels(models: ChatModel[]) {
}, {});
}
export function ModelSelector({
searchSpaceId,
className,
onChatModelSelected,
}: ModelSelectorProps) {
export function ModelSelector({ workspaceId, className, onChatModelSelected }: ModelSelectorProps) {
const router = useRouter();
const isMobile = useIsMobile();
const [open, setOpen] = useState(false);
@ -149,7 +145,7 @@ export function ModelSelector({
function manageModelConnections() {
setOpen(false);
router.push(`/dashboard/${searchSpaceId}/workspace-settings/models`);
router.push(`/dashboard/${workspaceId}/workspace-settings/models`);
}
const handleScroll = useCallback((event: UIEvent<HTMLDivElement>) => {

View file

@ -70,14 +70,14 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
const createPromptIndex = filtered.length;
const totalItems = filtered.length + 1;
const searchSpaceId = getWorkspaceIdParam(params);
const workspaceId = getWorkspaceIdParam(params);
const handleSelect = useCallback(
(index: number) => {
if (index === createPromptIndex) {
onDone();
if (searchSpaceId) {
router.push(`/dashboard/${searchSpaceId}/user-settings/prompts`);
if (workspaceId) {
router.push(`/dashboard/${workspaceId}/user-settings/prompts`);
}
return;
}
@ -85,7 +85,7 @@ export const PromptPicker = forwardRef<PromptPickerRef, PromptPickerProps>(funct
if (!action) return;
onSelect({ name: action.name, prompt: action.prompt, mode: action.mode });
},
[filtered, onSelect, createPromptIndex, onDone, router, searchSpaceId]
[filtered, onSelect, createPromptIndex, onDone, router, workspaceId]
);
useEffect(() => {

View file

@ -31,7 +31,7 @@ const TOUR_STEPS: TourStep[] = [
{
target: '[data-joyride="upload-button"]',
title: "Upload documents",
content: "Upload files to your search space.",
content: "Upload files to your workspace.",
placement: "left",
},
{
@ -391,17 +391,17 @@ export function OnboardingTour() {
// Get user data
const { data: user } = useAtomValue(currentUserAtom);
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
// Fetch threads data
const { data: threadsData } = useQuery({
queryKey: ["threads", searchSpaceId, { limit: 40 }], // Same key as layout
queryFn: () => fetchThreads(Number(searchSpaceId), 40),
enabled: !!searchSpaceId,
queryKey: ["threads", workspaceId, { limit: 40 }], // Same key as layout
queryFn: () => fetchThreads(Number(workspaceId), 40),
enabled: !!workspaceId,
});
// Real-time document type counts via Zero
const documentTypeCounts = useZeroDocumentTypeCounts(searchSpaceId);
const documentTypeCounts = useZeroDocumentTypeCounts(workspaceId);
// Get connectors
const { data: connectors = [] } = useAtomValue(connectorsAtom);
@ -448,7 +448,7 @@ export function OnboardingTour() {
// Check if tour should run: localStorage + data validation with user ID tracking
useEffect(() => {
// Don't check if not mounted or no user
if (!mounted || !user?.id || !searchSpaceId) return;
if (!mounted || !user?.id || !workspaceId) return;
// Check if on new-chat page
const isNewChatPage = pathname?.includes("/new-chat");
@ -459,7 +459,7 @@ export function OnboardingTour() {
// - threadsData is defined (query completed, even if empty)
// - documentTypeCounts is defined (query completed, even if empty object)
// - connectors is an array (always defined with default [])
// If searchSpaceId is not set, connectors query won't run, but that's okay
// If workspaceId is not set, connectors query won't run, but that's okay
const dataLoaded = threadsData !== undefined && documentTypeCounts !== undefined;
if (!dataLoaded) return;
@ -542,7 +542,7 @@ export function OnboardingTour() {
cancelled = true;
if (startCheckTimerRef.current) clearTimeout(startCheckTimerRef.current);
};
}, [mounted, user?.id, searchSpaceId, pathname, threadsData, documentTypeCounts, connectors]);
}, [mounted, user?.id, workspaceId, pathname, threadsData, documentTypeCounts, connectors]);
// Update position on resize/scroll
useEffect(() => {

View file

@ -125,7 +125,7 @@ export function PublicChatSnapshotsManager({
<Alert>
<Info />
<AlertDescription>
You don't have permission to view public chats in this search space.
You don't have permission to view public chats in this workspace.
</AlertDescription>
</Alert>
);

View file

@ -39,7 +39,7 @@ export function AutoReloadSettings() {
const pathname = usePathname();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const workspaceId = getWorkspaceIdNumber(params) ?? 0;
const [enabled, setEnabled] = useState(false);
const [thresholdInput, setThresholdInput] = useState("");
@ -79,8 +79,7 @@ export function AutoReloadSettings() {
}, [searchParams, router, pathname, queryClient]);
const setupMutation = useMutation({
mutationFn: () =>
stripeApiService.createAutoReloadSetupSession({ workspace_id: searchSpaceId }),
mutationFn: () => stripeApiService.createAutoReloadSetupSession({ workspace_id: workspaceId }),
onSuccess: (response) => {
window.location.assign(response.checkout_url);
},

View file

@ -38,7 +38,7 @@ const formatUsd = (micros: number) => {
export function BuyCreditsContent() {
const params = useParams();
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const workspaceId = getWorkspaceIdNumber(params) ?? 0;
const [quantity, setQuantity] = useState(1);
// Raw text of the amount field so the user can clear it while typing;
// committed back to a clamped integer on blur.
@ -177,7 +177,7 @@ export function BuyCreditsContent() {
<Button
className="w-full"
disabled={purchaseMutation.isPending}
onClick={() => purchaseMutation.mutate({ quantity, workspace_id: searchSpaceId })}
onClick={() => purchaseMutation.mutate({ quantity, workspace_id: workspaceId })}
>
{purchaseMutation.isPending ? (
<>

View file

@ -33,7 +33,7 @@ const formatRewardUsd = (micros: number) => {
export function EarnCreditsContent() {
const params = useParams();
const queryClient = useQueryClient();
const searchSpaceId = getWorkspaceIdParam(params) ?? "";
const workspaceId = getWorkspaceIdParam(params) ?? "";
useEffect(() => {
trackIncentivePageViewed();
@ -163,7 +163,7 @@ export function EarnCreditsContent() {
<p className="text-sm text-muted-foreground">Need more?</p>
{creditBuyingEnabled ? (
<Button asChild variant="link" className="text-emerald-600 dark:text-emerald-400">
<Link href={`/dashboard/${searchSpaceId}/buy-more`}>Buy credits at $1 per $1</Link>
<Link href={`/dashboard/${workspaceId}/buy-more`}>Buy credits at $1 per $1</Link>
</Button>
) : (
<p className="text-xs text-muted-foreground">

View file

@ -6,15 +6,15 @@ import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import {
updateSearchSpaceApiAccessMutationAtom,
updateSearchSpaceMutationAtom,
updateWorkspaceApiAccessMutationAtom,
updateWorkspaceMutationAtom,
} from "@/atoms/workspaces/workspace-mutation.atoms";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { authenticatedFetch } from "@/lib/auth-fetch";
import { buildBackendUrl } from "@/lib/env-config";
import { cacheKeys } from "@/lib/query-client/cache-keys";
@ -25,23 +25,22 @@ interface GeneralSettingsManagerProps {
}
export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerProps) {
const searchSpaceId = workspaceId;
const t = useTranslations("searchSpaceSettings");
const t = useTranslations("workspaceSettings");
const tCommon = useTranslations("common");
const {
data: searchSpace,
data: workspace,
isLoading: loading,
isError,
refetch: fetchSearchSpace,
refetch: fetchWorkspace,
} = useQuery({
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
enabled: !!searchSpaceId,
queryKey: cacheKeys.workspaces.detail(workspaceId.toString()),
queryFn: () => workspacesApiService.getWorkspace({ id: workspaceId }),
enabled: !!workspaceId,
});
const { mutateAsync: updateSearchSpace } = useAtomValue(updateSearchSpaceMutationAtom);
const { mutateAsync: updateSearchSpaceApiAccess } = useAtomValue(
updateSearchSpaceApiAccessMutationAtom
const { mutateAsync: updateWorkspace } = useAtomValue(updateWorkspaceMutationAtom);
const { mutateAsync: updateWorkspaceApiAccess } = useAtomValue(
updateWorkspaceApiAccessMutationAtom
);
const [name, setName] = useState("");
@ -49,16 +48,16 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
const [saving, setSaving] = useState(false);
const [savingApiAccess, setSavingApiAccess] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const hasSearchSpace = !!searchSpace;
const searchSpaceName = searchSpace?.name;
const searchSpaceDescription = searchSpace?.description;
const hasWorkspace = !!workspace;
const workspaceName = workspace?.name;
const workspaceDescription = workspace?.description;
const handleExportKB = useCallback(async () => {
if (isExporting) return;
setIsExporting(true);
try {
const response = await authenticatedFetch(
buildBackendUrl(`/api/v1/workspaces/${searchSpaceId}/export`),
buildBackendUrl(`/api/v1/workspaces/${workspaceId}/export`),
{ method: "GET" }
);
if (!response.ok) {
@ -81,37 +80,37 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
} finally {
setIsExporting(false);
}
}, [searchSpaceId, isExporting]);
}, [workspaceId, isExporting]);
// Initialize state from fetched search space
// Initialize state from fetched workspace
useEffect(() => {
if (hasSearchSpace) {
setName(searchSpaceName || "");
setDescription(searchSpaceDescription || "");
if (hasWorkspace) {
setName(workspaceName || "");
setDescription(workspaceDescription || "");
}
}, [hasSearchSpace, searchSpaceName, searchSpaceDescription]);
}, [hasWorkspace, workspaceName, workspaceDescription]);
// Derive hasChanges during render
const hasChanges =
!!searchSpace &&
((searchSpace.name || "") !== name || (searchSpace.description || "") !== description);
!!workspace &&
((workspace.name || "") !== name || (workspace.description || "") !== description);
const handleSave = async () => {
try {
setSaving(true);
await updateSearchSpace({
id: searchSpaceId,
await updateWorkspace({
id: workspaceId,
data: {
name: name.trim(),
description: description.trim() || undefined,
},
});
await fetchSearchSpace();
await fetchWorkspace();
} catch (error: unknown) {
console.error("Error saving search space details:", error);
toast.error(error instanceof Error ? error.message : "Failed to save search space details");
console.error("Error saving workspace details:", error);
toast.error(error instanceof Error ? error.message : "Failed to save workspace details");
} finally {
setSaving(false);
}
@ -126,11 +125,11 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
async (enabled: boolean) => {
try {
setSavingApiAccess(true);
await updateSearchSpaceApiAccess({
id: searchSpaceId,
await updateWorkspaceApiAccess({
id: workspaceId,
api_access_enabled: enabled,
});
await fetchSearchSpace();
await fetchWorkspace();
} catch (error) {
console.error("Error updating API access:", error);
toast.error(error instanceof Error ? error.message : "Failed to update API access");
@ -138,7 +137,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
setSavingApiAccess(false);
}
},
[fetchSearchSpace, searchSpaceId, updateSearchSpaceApiAccess]
[fetchWorkspace, workspaceId, updateWorkspaceApiAccess]
);
if (loading) {
@ -156,7 +155,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
return (
<div className="flex flex-col items-center justify-center gap-3 py-8 text-center">
<p className="text-sm text-destructive">Failed to load settings.</p>
<Button variant="outline" size="sm" onClick={() => fetchSearchSpace()}>
<Button variant="outline" size="sm" onClick={() => fetchWorkspace()}>
Retry
</Button>
</div>
@ -168,9 +167,9 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
<form onSubmit={onSubmit} className="space-y-6">
<div className="flex flex-col gap-6">
<div className="space-y-2">
<Label htmlFor="search-space-name">{t("general_name_label")}</Label>
<Label htmlFor="workspace-name">{t("general_name_label")}</Label>
<Input
id="search-space-name"
id="workspace-name"
maxLength={100}
placeholder={t("general_name_placeholder")}
value={name}
@ -180,12 +179,12 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
</div>
<div className="space-y-2">
<Label htmlFor="search-space-description">
<Label htmlFor="workspace-description">
{t("general_description_label")}{" "}
<span className="text-muted-foreground font-normal">({tCommon("optional")})</span>
</Label>
<Input
id="search-space-description"
id="workspace-description"
placeholder={t("general_description_placeholder")}
value={description}
onChange={(e) => setDescription(e.target.value)}
@ -210,13 +209,11 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
<div className="border-t pt-6 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<Label htmlFor="api-access-enabled">API key access</Label>
<p className="text-xs text-muted-foreground">
Allow API keys to access this search space.
</p>
<p className="text-xs text-muted-foreground">Allow API keys to access this workspace.</p>
</div>
<Switch
id="api-access-enabled"
checked={!!searchSpace?.api_access_enabled}
checked={!!workspace?.api_access_enabled}
disabled={savingApiAccess}
onCheckedChange={handleApiAccessToggle}
/>
@ -226,7 +223,7 @@ export function GeneralSettingsManager({ workspaceId }: GeneralSettingsManagerPr
<div className="space-y-1">
<Label>Export knowledge base</Label>
<p className="text-xs text-muted-foreground">
Download all documents in this search space as a ZIP of markdown files.
Download all documents in this workspace as a ZIP of markdown files.
</p>
</div>
<Button

View file

@ -51,7 +51,6 @@ function renderAutoModeOption() {
}
export function ModelConnectionsSettings({ workspaceId }: { workspaceId: number }) {
const searchSpaceId = workspaceId;
const [{ data: globalConnections = [] }] = useAtom(globalModelConnectionsAtom);
const [{ data: connections = [] }] = useAtom(modelConnectionsAtom);
const [{ data: roles }] = useAtom(modelRolesAtom);
@ -148,7 +147,7 @@ export function ModelConnectionsSettings({ workspaceId }: { workspaceId: number
<Separator />
<ModelProviderConnectionsPanel searchSpaceId={searchSpaceId} connections={connections} />
<ModelProviderConnectionsPanel workspaceId={workspaceId} connections={connections} />
</div>
);
}

View file

@ -66,7 +66,7 @@ export function ConnectionCard({ connection }: { connection: ConnectionRead }) {
<AlertDialogTitle>Delete this provider?</AlertDialogTitle>
<AlertDialogDescription>
<span className="font-medium text-foreground">{providerLabel}</span> and all of
its models will be removed from this search space. This cannot be undone.
its models will be removed from this workspace. This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View file

@ -23,7 +23,7 @@ import {
} from "./provider-metadata";
interface ModelProviderConnectionsPanelProps {
searchSpaceId: number;
workspaceId: number;
connections: ConnectionRead[];
className?: string;
addProviderTitle?: string;
@ -49,7 +49,7 @@ function toModelSelection(model: SelectableModel): ModelSelection {
}
export function ModelProviderConnectionsPanel({
searchSpaceId,
workspaceId,
connections,
className,
addProviderTitle = "Add Provider",
@ -126,7 +126,7 @@ export function ModelProviderConnectionsPanel({
// Each provider connect form builds its own credential payload; the backend
// resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM.
function handleCreate(draft: ConnectionDraft) {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
if (!Number.isFinite(workspaceId) || workspaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
@ -143,7 +143,7 @@ export function ModelProviderConnectionsPanel({
base_url: draft.base_url,
api_key: draft.api_key,
scope: "SEARCH_SPACE" as const,
workspace_id: searchSpaceId,
workspace_id: workspaceId,
extra: draft.extra,
enabled: true,
models,
@ -170,7 +170,7 @@ export function ModelProviderConnectionsPanel({
setProvider(providerId);
setIsAddProviderOpen(true);
if (providerId === "vertex_ai") {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
if (!Number.isFinite(workspaceId) || workspaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
@ -181,7 +181,7 @@ export function ModelProviderConnectionsPanel({
base_url: null,
api_key: null,
scope: "SEARCH_SPACE",
workspace_id: searchSpaceId,
workspace_id: workspaceId,
extra: {},
enabled: true,
models: [],
@ -194,7 +194,7 @@ export function ModelProviderConnectionsPanel({
}
function refreshConnectModels(draft: ConnectionDraft) {
if (!Number.isFinite(searchSpaceId) || searchSpaceId <= 0) {
if (!Number.isFinite(workspaceId) || workspaceId <= 0) {
toast.error("Workspace is still loading. Please try again.");
return;
}
@ -205,7 +205,7 @@ export function ModelProviderConnectionsPanel({
base_url: draft.base_url,
api_key: draft.api_key,
scope: "SEARCH_SPACE",
workspace_id: searchSpaceId,
workspace_id: workspaceId,
extra: draft.extra,
enabled: true,
models: [],

View file

@ -5,13 +5,13 @@ import { useAtomValue } from "jotai";
import { AlertTriangle, Info } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { updateSearchSpaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { updateWorkspaceMutationAtom } from "@/atoms/workspaces/workspace-mutation.atoms";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { searchSpacesApiService } from "@/lib/apis/workspaces-api.service";
import { workspacesApiService } from "@/lib/apis/workspaces-api.service";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { Spinner } from "../ui/spinner";
@ -20,36 +20,35 @@ interface PromptConfigManagerProps {
}
export function PromptConfigManager({ workspaceId }: PromptConfigManagerProps) {
const searchSpaceId = workspaceId;
const { data: searchSpace, isLoading: loading } = useQuery({
queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()),
queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }),
enabled: !!searchSpaceId,
const { data: workspace, isLoading: loading } = useQuery({
queryKey: cacheKeys.workspaces.detail(workspaceId.toString()),
queryFn: () => workspacesApiService.getWorkspace({ id: workspaceId }),
enabled: !!workspaceId,
});
const { mutateAsync: updateSearchSpace, isPending: isSaving } = useAtomValue(
updateSearchSpaceMutationAtom
const { mutateAsync: updateWorkspace, isPending: isSaving } = useAtomValue(
updateWorkspaceMutationAtom
);
const [customInstructions, setCustomInstructions] = useState("");
const hasSearchSpace = !!searchSpace;
const searchSpaceInstructions = searchSpace?.qna_custom_instructions;
const hasWorkspace = !!workspace;
const workspaceInstructions = workspace?.qna_custom_instructions;
// Initialize state from fetched search space
// Initialize state from fetched workspace
useEffect(() => {
if (hasSearchSpace) {
setCustomInstructions(searchSpaceInstructions || "");
if (hasWorkspace) {
setCustomInstructions(workspaceInstructions || "");
}
}, [hasSearchSpace, searchSpaceInstructions]);
}, [hasWorkspace, workspaceInstructions]);
// Derive hasChanges during render
const hasChanges =
!!searchSpace && (searchSpace.qna_custom_instructions || "") !== customInstructions;
!!workspace && (workspace.qna_custom_instructions || "") !== customInstructions;
const handleSave = async () => {
try {
await updateSearchSpace({
id: searchSpaceId,
await updateWorkspace({
id: workspaceId,
data: { qna_custom_instructions: customInstructions.trim() || "" },
});
toast.success("System instructions saved successfully");
@ -98,8 +97,8 @@ export function PromptConfigManager({ workspaceId }: PromptConfigManagerProps) {
<Alert>
<Info />
<AlertDescription>
System instructions apply to all AI interactions in this search space. They guide how the
AI responds, its tone, focus areas, and behavior patterns.
System instructions apply to all AI interactions in this workspace. They guide how the AI
responds, its tone, focus areas, and behavior patterns.
</AlertDescription>
</Alert>
@ -112,7 +111,7 @@ export function PromptConfigManager({ workspaceId }: PromptConfigManagerProps) {
</h3>
<p className="text-xs md:text-sm text-muted-foreground">
Provide specific guidelines for how you want the AI to respond. These instructions
will be applied to all answers in this search space.
will be applied to all answers in this workspace.
</p>
</div>
<div className="space-y-3 md:space-y-4">

View file

@ -160,7 +160,7 @@ const CATEGORY_CONFIG: Record<
settings: {
label: "Settings",
icon: Settings,
description: "Manage search space settings",
description: "Manage workspace settings",
order: 10,
},
public_sharing: {
@ -172,7 +172,7 @@ const CATEGORY_CONFIG: Record<
general: {
label: "General",
icon: SlidersHorizontal,
description: "General search space permissions",
description: "General workspace permissions",
order: 12,
},
};
@ -270,7 +270,6 @@ type PermissionWithDescription = PermissionInfo;
// ============ Roles Manager (for Settings page) ============
export function RolesManager({ workspaceId }: { workspaceId: number }) {
const searchSpaceId = workspaceId;
const { data: access = null } = useAtomValue(myAccessAtom);
const hasPermission = useCallback(
@ -279,9 +278,9 @@ export function RolesManager({ workspaceId }: { workspaceId: number }) {
);
const { data: roles = [], isLoading: rolesLoading } = useQuery({
queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
queryFn: () => rolesApiService.getRoles({ workspace_id: searchSpaceId }),
enabled: !!searchSpaceId,
queryKey: cacheKeys.roles.all(workspaceId.toString()),
queryFn: () => rolesApiService.getRoles({ workspace_id: workspaceId }),
enabled: !!workspaceId,
});
const { data: permissionsData } = useAtomValue(permissionsAtom);
@ -312,36 +311,36 @@ export function RolesManager({ workspaceId }: { workspaceId: number }) {
}
): Promise<Role> => {
const request: UpdateRoleRequest = {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
role_id: roleId,
data: data,
};
return await updateRole(request);
},
[updateRole, searchSpaceId]
[updateRole, workspaceId]
);
const handleDeleteRole = useCallback(
async (roleId: number): Promise<boolean> => {
const request: DeleteRoleRequest = {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
role_id: roleId,
};
await deleteRole(request);
return true;
},
[deleteRole, searchSpaceId]
[deleteRole, workspaceId]
);
const handleCreateRole = useCallback(
async (roleData: CreateRoleRequest["data"]): Promise<Role> => {
const request: CreateRoleRequest = {
workspace_id: searchSpaceId,
workspace_id: workspaceId,
data: roleData,
};
return await createRole(request);
},
[createRole, searchSpaceId]
[createRole, workspaceId]
);
return (
@ -885,7 +884,7 @@ function CreateRoleDialog({
<DialogHeader className="px-5 pt-5 pb-4 shrink-0">
<DialogTitle className="text-lg">Create Custom Role</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground">
Define permissions for a new role in this search space
Define permissions for a new role in this workspace
</DialogDescription>
</DialogHeader>
<div className="flex-1 min-h-0 overflow-y-auto">

View file

@ -41,7 +41,7 @@ import {
} from "@/lib/supported-extensions";
interface DocumentUploadTabProps {
searchSpaceId: string;
workspaceId: string;
onSuccess?: () => void;
onAccordionStateChange?: (isExpanded: boolean) => void;
}
@ -132,7 +132,7 @@ const toggleRowClass =
"flex items-center justify-between rounded-lg bg-slate-400/5 dark:bg-white/5 p-3";
export function DocumentUploadTab({
searchSpaceId,
workspaceId,
onSuccess,
onAccordionStateChange,
}: DocumentUploadTabProps) {
@ -319,7 +319,7 @@ export function DocumentUploadTab({
setUploadProgress(0);
setIsFolderUploading(true);
const total = folderUpload.entries.length;
trackDocumentUploadStarted(Number(searchSpaceId), total, totalFileSize);
trackDocumentUploadStarted(Number(workspaceId), total, totalFileSize);
try {
const batches: FolderEntry[][] = [];
@ -364,7 +364,7 @@ export function DocumentUploadTab({
batch.map((e) => e.file),
{
folder_name: folderUpload.folderName,
workspace_id: Number(searchSpaceId),
workspace_id: Number(workspaceId),
relative_paths: batch.map((e) => e.relativePath),
root_folder_id: rootFolderId,
use_vision_llm: useVisionLlm,
@ -380,13 +380,13 @@ export function DocumentUploadTab({
setUploadProgress(Math.round((uploaded / total) * 100));
}
trackDocumentUploadSuccess(Number(searchSpaceId), total);
trackDocumentUploadSuccess(Number(workspaceId), total);
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
setFolderUpload(null);
onSuccess?.();
} catch (error) {
const message = error instanceof Error ? error.message : "Upload failed";
trackDocumentUploadFailure(Number(searchSpaceId), message);
trackDocumentUploadFailure(Number(workspaceId), message);
toast(t("upload_error"), {
description: `${t("upload_error_desc")}: ${message}`,
});
@ -403,7 +403,7 @@ export function DocumentUploadTab({
}
setUploadProgress(0);
trackDocumentUploadStarted(Number(searchSpaceId), files.length, totalFileSize);
trackDocumentUploadStarted(Number(workspaceId), files.length, totalFileSize);
progressIntervalRef.current = setInterval(() => {
setUploadProgress((prev) => (prev >= 90 ? prev : prev + Math.random() * 10));
@ -413,7 +413,7 @@ export function DocumentUploadTab({
uploadDocuments(
{
files: rawFiles,
workspace_id: Number(searchSpaceId),
workspace_id: Number(workspaceId),
use_vision_llm: useVisionLlm,
processing_mode: processingMode,
},
@ -421,7 +421,7 @@ export function DocumentUploadTab({
onSuccess: () => {
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
setUploadProgress(100);
trackDocumentUploadSuccess(Number(searchSpaceId), files.length);
trackDocumentUploadSuccess(Number(workspaceId), files.length);
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
onSuccess?.();
},
@ -429,7 +429,7 @@ export function DocumentUploadTab({
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current);
setUploadProgress(0);
const message = error instanceof Error ? error.message : "Upload failed";
trackDocumentUploadFailure(Number(searchSpaceId), message);
trackDocumentUploadFailure(Number(workspaceId), message);
toast(t("upload_error"), {
description: `${t("upload_error_desc")}: ${message}`,
});

View file

@ -24,7 +24,7 @@ export interface SelectedFolder {
interface FolderWatchDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
searchSpaceId: number;
workspaceId: number;
onSuccess?: () => void;
initialFolder?: SelectedFolder | null;
}
@ -41,7 +41,7 @@ export const DEFAULT_EXCLUDE_PATTERNS = [
export function FolderWatchDialog({
open,
onOpenChange,
searchSpaceId,
workspaceId,
onSuccess,
initialFolder,
}: FolderWatchDialogProps) {
@ -91,7 +91,7 @@ export function FolderWatchDialog({
const rootFolderId = await uploadFolderScan({
folderPath: selectedFolder.path,
folderName: selectedFolder.name,
searchSpaceId,
workspaceId,
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
fileExtensions: supportedExtensions,
onProgress: setProgress,
@ -104,7 +104,7 @@ export function FolderWatchDialog({
excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
fileExtensions: supportedExtensions,
rootFolderId: rootFolderId ?? null,
searchSpaceId,
workspaceId,
active: true,
});
@ -124,7 +124,7 @@ export function FolderWatchDialog({
setSubmitting(false);
setProgress(null);
}
}, [selectedFolder, searchSpaceId, supportedExtensions, onOpenChange, onSuccess]);
}, [selectedFolder, workspaceId, supportedExtensions, onOpenChange, onSuccess]);
const handleOpenChange = useCallback(
(nextOpen: boolean) => {

View file

@ -108,9 +108,9 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
const draft = useMemo(() => extractDraft(effectiveArgs), [effectiveArgs]);
// Per-automation model selection. The card always supplies models (chosen
// here, not snapshotted from the search space), so Approve dispatches an
// here, not snapshotted from the workspace), so Approve dispatches an
// `edit` decision carrying `definition.models`.
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const eligibleModels = useAutomationEligibleModels();
const [modelSelection, setModelSelection] = useState<AutomationModelSelection>({
chatModelId: 0,
@ -156,7 +156,7 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
const plan = Array.isArray(baseDefinition.plan) ? baseDefinition.plan : [];
const triggers = Array.isArray(baseArgs.triggers) ? baseArgs.triggers : [];
trackAutomationChatApproved({
workspace_id: searchSpaceId ? Number(searchSpaceId) : undefined,
workspace_id: workspaceId ? Number(workspaceId) : undefined,
edited: pendingEdits !== null,
task_count: plan.length,
trigger_type:
@ -184,17 +184,17 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
args,
pendingEdits,
resolvedModels,
searchSpaceId,
workspaceId,
]);
const handleReject = useCallback(() => {
if (phase !== "pending" || !canReject || isEditing) return;
setRejected();
trackAutomationChatRejected({
workspace_id: searchSpaceId ? Number(searchSpaceId) : undefined,
workspace_id: workspaceId ? Number(workspaceId) : undefined,
});
onDecision({ type: "reject", message: "User rejected the automation draft." });
}, [phase, canReject, isEditing, setRejected, onDecision, searchSpaceId]);
}, [phase, canReject, isEditing, setRejected, onDecision, workspaceId]);
useEffect(() => {
if (isEditing) return;
@ -268,7 +268,7 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
setPendingEdits(parsed);
setIsEditing(false);
trackAutomationChatDraftEdited({
workspace_id: searchSpaceId ? Number(searchSpaceId) : undefined,
workspace_id: workspaceId ? Number(workspaceId) : undefined,
});
}}
onCancel={() => setIsEditing(false)}
@ -284,7 +284,7 @@ function ApprovalCard({ args, interruptData, onDecision }: ApprovalCardProps) {
<div className="px-5 py-4">
<p className="mb-3 text-xs font-medium text-foreground">Models</p>
<AutomationModelFields
workspaceId={Number(searchSpaceId)}
workspaceId={Number(workspaceId)}
value={resolvedModels}
onChange={(patch) => setModelSelection((prev) => ({ ...prev, ...patch }))}
/>
@ -385,7 +385,7 @@ function JsonEditor({ initialValue, onSave, onCancel }: JsonEditorProps) {
// ----------------------------------------------------------------------------
function SavedCard({ result }: { result: SavedResult }) {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const tracked = useRef(false);
useEffect(() => {
if (tracked.current) return;
@ -393,12 +393,12 @@ function SavedCard({ result }: { result: SavedResult }) {
trackAutomationChatCreateSucceeded({
automation_id: result.automation_id,
name: result.name,
workspace_id: searchSpaceId ? Number(searchSpaceId) : undefined,
workspace_id: workspaceId ? Number(workspaceId) : undefined,
});
}, [result.automation_id, result.name, searchSpaceId]);
}, [result.automation_id, result.name, workspaceId]);
const detailHref = searchSpaceId
? `/dashboard/${searchSpaceId}/automations/${result.automation_id}`
const detailHref = workspaceId
? `/dashboard/${workspaceId}/automations/${result.automation_id}`
: null;
return (
@ -429,7 +429,7 @@ function SavedCard({ result }: { result: SavedResult }) {
}
function InvalidCard({ result }: { result: InvalidResult }) {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const tracked = useRef(false);
useEffect(() => {
if (tracked.current) return;
@ -437,9 +437,9 @@ function InvalidCard({ result }: { result: InvalidResult }) {
trackAutomationChatCreateFailed({
reason: "invalid",
issue_count: result.issues.length,
workspace_id: searchSpaceId ? Number(searchSpaceId) : undefined,
workspace_id: workspaceId ? Number(workspaceId) : undefined,
});
}, [result.issues.length, searchSpaceId]);
}, [result.issues.length, workspaceId]);
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">
@ -464,7 +464,7 @@ function InvalidCard({ result }: { result: InvalidResult }) {
}
function ErrorCard({ result }: { result: ErrorResult }) {
const searchSpaceId = useAtomValue(activeWorkspaceIdAtom);
const workspaceId = useAtomValue(activeWorkspaceIdAtom);
const tracked = useRef(false);
useEffect(() => {
if (tracked.current) return;
@ -472,9 +472,9 @@ function ErrorCard({ result }: { result: ErrorResult }) {
trackAutomationChatCreateFailed({
reason: "error",
message: result.message,
workspace_id: searchSpaceId ? Number(searchSpaceId) : undefined,
workspace_id: workspaceId ? Number(workspaceId) : undefined,
});
}, [result.message, searchSpaceId]);
}, [result.message, workspaceId]);
return (
<div className="my-4 max-w-lg overflow-hidden rounded-2xl border bg-muted/30 select-none">

View file

@ -35,15 +35,15 @@ import { Tilt } from "@/components/ui/tilt";
import { cn } from "@/lib/utils";
// Define the form schema with Zod
const searchSpaceFormSchema = z.object({
const workspaceFormSchema = z.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
});
// Define the type for the form values
type SearchSpaceFormValues = z.infer<typeof searchSpaceFormSchema>;
type WorkspaceFormValues = z.infer<typeof workspaceFormSchema>;
interface SearchSpaceFormProps {
interface WorkspaceFormProps {
onSubmit?: (data: { name: string; description?: string }) => void;
onDelete?: () => void;
className?: string;
@ -51,19 +51,19 @@ interface SearchSpaceFormProps {
initialData?: { name: string; description?: string };
}
export function SearchSpaceForm({
export function WorkspaceForm({
onSubmit,
onDelete,
className,
isEditing = false,
initialData = { name: "", description: "" },
}: SearchSpaceFormProps) {
}: WorkspaceFormProps) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const router = useRouter();
// Initialize the form with React Hook Form and Zod validation
const form = useForm<SearchSpaceFormValues>({
resolver: zodResolver(searchSpaceFormSchema),
const form = useForm<WorkspaceFormValues>({
resolver: zodResolver(workspaceFormSchema),
defaultValues: {
name: initialData.name,
description: initialData.description,
@ -71,7 +71,7 @@ export function SearchSpaceForm({
});
// Handle form submission
const handleFormSubmit = (values: SearchSpaceFormValues) => {
const handleFormSubmit = (values: WorkspaceFormValues) => {
if (onSubmit) {
onSubmit(values);
}
@ -119,7 +119,7 @@ export function SearchSpaceForm({
<motion.div className="flex items-center justify-between" variants={itemVariants}>
<div className="flex flex-col space-y-2">
<h2 className="text-2xl md:text-3xl font-bold tracking-tight">
{isEditing ? "Edit Search Space" : "Create Search Space"}
{isEditing ? "Edit Workspace" : "Create Workspace"}
</h2>
</div>
<Button
@ -163,7 +163,7 @@ export function SearchSpaceForm({
<span className="p-3 rounded-full bg-blue-100 dark:bg-blue-950/50">
<Search className="size-6 text-blue-500" />
</span>
<h3 className="text-xl font-semibold">Search Space</h3>
<h3 className="text-xl font-semibold">Workspace</h3>
</div>
{isEditing && onDelete && (
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
@ -193,7 +193,7 @@ export function SearchSpaceForm({
)}
</div>
<p className="text-muted-foreground">
A search space is your personal workspace. Connect external sources, upload documents,
A workspace is your personal workspace. Connect external sources, upload documents,
take notes, and get work done with AI agents.
</p>
</div>
@ -211,9 +211,9 @@ export function SearchSpaceForm({
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Enter search space name" {...field} />
<Input placeholder="Enter workspace name" {...field} />
</FormControl>
<FormDescription>A unique name for your search space.</FormDescription>
<FormDescription>A unique name for your workspace.</FormDescription>
<FormMessage />
</FormItem>
)}
@ -228,10 +228,10 @@ export function SearchSpaceForm({
Description <span className="text-muted-foreground font-normal">(optional)</span>
</FormLabel>
<FormControl>
<Input placeholder="Enter search space description" {...field} />
<Input placeholder="Enter workspace description" {...field} />
</FormControl>
<FormDescription>
A brief description of what this search space will be used for.
A brief description of what this workspace will be used for.
</FormDescription>
<FormMessage />
</FormItem>
@ -250,4 +250,4 @@ export function SearchSpaceForm({
);
}
export default SearchSpaceForm;
export default WorkspaceForm;