Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat(web): Add configurable default search mode setting #481

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
shohamd4 wants to merge 2 commits into sourcebot-dev:main
base: main
Choose a base branch
Loading
from shohamd4:default-search-mode-setting
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 96 additions & 1 deletion packages/web/src/actions.ts
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { getAuditService } from "@/ee/features/audit/factory";
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
import { getOrgMetadata } from "@/lib/utils";
import { getOrgFromDomain } from "./data/org";
import { searchModeSchema } from "@/types";

const ajv = new Ajv({
validateFormats: false,
Expand Down Expand Up @@ -2161,6 +2162,100 @@ export async function setAgenticSearchTutorialDismissedCookie(dismissed: boolean
});
}

export const getDefaultSearchMode = async (domain: string): Promise<"precise" | "agentic" | ServiceError> => sew(async () => {
const org = await getOrgFromDomain(domain);
if (!org) {
return {
statusCode: StatusCodes.NOT_FOUND,
errorCode: ErrorCode.NOT_FOUND,
message: "Organization not found",
} satisfies ServiceError;
}

// If no metadata is set, return default (precise)
if (org.metadata === null) {
return "precise";
}

const orgMetadata = getOrgMetadata(org);
if (!orgMetadata) {
return {
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
errorCode: ErrorCode.INVALID_ORG_METADATA,
message: "Invalid organization metadata",
} satisfies ServiceError;
}

return orgMetadata.defaultSearchMode ?? "precise";
});

export const setDefaultSearchMode = async (domain: string, mode: "precise" | "agentic"): Promise<{ success: boolean } | ServiceError> => sew(async () => {
// Runtime validation to guard server action from invalid input
const parsed = searchModeSchema.safeParse(mode);
if (!parsed.success) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "Invalid default search mode",
} satisfies ServiceError;
}
const validatedMode = parsed.data;

return await withAuth(async (userId) => {
return await withOrgMembership(userId, domain, async ({ org }) => {
// Validate that agentic mode is not being set when no language models are configured
if (validatedMode === "agentic") {
const { getConfiguredLanguageModelsInfo } = await import("@/features/chat/actions");
const languageModels = await getConfiguredLanguageModelsInfo();
if (languageModels.length === 0) {
return {
statusCode: StatusCodes.BAD_REQUEST,
errorCode: ErrorCode.INVALID_REQUEST_BODY,
message: "Cannot set Ask mode as default when no language models are configured",
} satisfies ServiceError;
}
}

const currentMetadata = getOrgMetadata(org);
const previousMode = currentMetadata?.defaultSearchMode ?? "precise";
const mergedMetadata = {
...(currentMetadata ?? {}),
defaultSearchMode: validatedMode,
};

await prisma.org.update({
where: {
id: org.id,
},
data: {
metadata: mergedMetadata,
},
});

await auditService.createAudit({
action: "org.settings.default_search_mode_updated",
actor: {
id: userId,
type: "user"
},
target: {
id: org.id.toString(),
type: "org"
},
orgId: org.id,
metadata: {
previousDefaultSearchMode: previousMode,
newDefaultSearchMode: validatedMode
}
});

return {
success: true,
};
}, /* minRequiredRole = */ OrgRole.OWNER);
});
});

////// Helpers ///////

const parseConnectionConfig = (config: string) => {
Expand Down Expand Up @@ -2266,4 +2361,4 @@ export const encryptValue = async (value: string) => {

export const decryptValue = async (iv: string, encryptedValue: string) => {
return decrypt(iv, encryptedValue);
}
}
15 changes: 11 additions & 4 deletions packages/web/src/app/[domain]/page.tsx
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getRepos, getSearchContexts } from "@/actions";
import { getDefaultSearchMode, getRepos, getSearchContexts } from "@/actions";
import { Footer } from "@/app/components/footer";
import { getOrgFromDomain } from "@/data/org";
import { getConfiguredLanguageModelsInfo, getUserChatHistory } from "@/features/chat/actions";
Expand Down Expand Up @@ -48,14 +48,21 @@ export default async function Home(props: { params: Promise<{ domain: string }>

const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined);

// Read search mode from cookie, defaulting to agentic if not set
// (assuming a language model is configured).
// Get org's default search mode
const defaultSearchMode = await getDefaultSearchMode(domain);
// If there was an error or no setting found, default to precise (search)
const orgDefaultMode = isServiceError(defaultSearchMode) ? "precise" : defaultSearchMode;
const effectiveOrgDefaultMode =
orgDefaultMode === "agentic" && models.length === 0 ? "precise" : orgDefaultMode;

// Read search mode from cookie, defaulting to the org's default setting if not set
const cookieStore = await cookies();
const searchModeCookie = cookieStore.get(SEARCH_MODE_COOKIE_NAME);
const initialSearchMode = (
searchModeCookie?.value === "agentic" ||
searchModeCookie?.value === "precise"
) ? searchModeCookie.value : models.length > 0 ? "agentic" : "precise";
) ? ((searchModeCookie.value === "agentic" && models.length === 0) ? "precise" : searchModeCookie.value)
: effectiveOrgDefaultMode;

const isAgenticSearchTutorialDismissed = cookieStore.get(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME)?.value === "true";

Expand Down
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use client';

import { setDefaultSearchMode } from "@/actions";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { LoadingButton } from "@/components/ui/loading-button";
import { OrgRole } from "@sourcebot/db";
import { MessageCircleIcon, SearchIcon } from "lucide-react";
import { useState } from "react";
import { useParams } from "next/navigation";
import { useToast } from "@/components/hooks/use-toast";

interface DefaultSearchModeCardProps {
initialDefaultMode: "precise" | "agentic";
currentUserRole: OrgRole;
isAskModeAvailable: boolean;
}

export const DefaultSearchModeCard = ({ initialDefaultMode, currentUserRole, isAskModeAvailable }: DefaultSearchModeCardProps) => {
const { domain } = useParams<{ domain: string }>();
// If Ask mode is not available and the initial mode is agentic, force it to precise
const effectiveInitialMode = !isAskModeAvailable && initialDefaultMode === "agentic" ? "precise" : initialDefaultMode;
const [defaultSearchMode, setDefaultSearchModeState] = useState<"precise" | "agentic">(effectiveInitialMode);
const [isUpdating, setIsUpdating] = useState(false);
const isReadOnly = currentUserRole !== OrgRole.OWNER;
const { toast } = useToast();

const handleUpdateDefaultSearchMode = async () => {
if (isReadOnly) {
return;
}

setIsUpdating(true);
try {
const result = await setDefaultSearchMode(domain as string, defaultSearchMode);
if (!result || typeof result !== 'object') {
throw new Error('Unexpected response');
}
// If this is a ServiceError, surface its message
if ('statusCode' in result && 'errorCode' in result && 'message' in result) {
toast({
title: "Failed to update",
description: result.message,
variant: "destructive",
});
return;
}
if (!result.success) {
throw new Error('Failed to update default search mode');
}
toast({
title: "Default search mode updated",
description: `Default search mode has been set to ${defaultSearchMode === "agentic" ? "Ask" : "Code Search"}.`,
variant: "success",
});
} catch (error) {
console.error('Error updating default search mode:', error);
// If we already showed a specific error above, do nothing here; otherwise fallback
if (!(error instanceof Error && /Unexpected response/.test(error.message))) {
toast({
title: "Failed to update",
description: "An error occurred while updating the default search mode.",
variant: "destructive",
});
}
} finally {
setIsUpdating(false);
}
};

return (
<Card>
<CardHeader>
<CardTitle>Default Search Mode</CardTitle>
<CardDescription>
Choose which search mode will be the default when users first visit Sourcebot
{!isAskModeAvailable && (
<span className="block text-yellow-600 dark:text-yellow-400 mt-1">
Ask mode is unavailable (no language models configured)
</span>
)}
</CardDescription>
</CardHeader>
<CardContent>
<Select
value={defaultSearchMode}
onValueChange={(value) => setDefaultSearchModeState(value as "precise" | "agentic")}
disabled={isReadOnly}
>
<SelectTrigger>
<SelectValue placeholder="Select default search mode">
{defaultSearchMode === "precise" ? "Code Search" : defaultSearchMode === "agentic" ? "Ask" : undefined}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="precise">Code Search</SelectItem>
<SelectItem value="agentic" disabled={!isAskModeAvailable}>
Ask {!isAskModeAvailable && "(unavailable)"}
</SelectItem>
</SelectContent>
</Select>
</CardContent>
<CardFooter>
<LoadingButton
onClick={handleUpdateDefaultSearchMode}
loading={isUpdating}
disabled={isReadOnly || isUpdating || defaultSearchMode === effectiveInitialMode}
>
Update
</LoadingButton>
</CardFooter>
</Card>
);
};
21 changes: 20 additions & 1 deletion packages/web/src/app/[domain]/settings/(general)/page.tsx
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ChangeOrgNameCard } from "./components/changeOrgNameCard";
import { isServiceError } from "@/lib/utils";
import { getCurrentUserRole } from "@/actions";
import { getCurrentUserRole, getDefaultSearchMode } from "@/actions";
import { getOrgFromDomain } from "@/data/org";
import { ChangeOrgDomainCard } from "./components/changeOrgDomainCard";
import { DefaultSearchModeCard } from "./components/defaultSearchModeCard";
import { ServiceErrorException } from "@/lib/serviceError";
import { ErrorCode } from "@/lib/errorCodes";
import { headers } from "next/headers";
import { getConfiguredLanguageModelsInfo } from "@/features/chat/actions";
import { OrgRole } from "@sourcebot/db";

interface GeneralSettingsPageProps {
params: Promise<{
Expand Down Expand Up @@ -36,6 +39,14 @@ export default async function GeneralSettingsPage(props: GeneralSettingsPageProp

const host = (await headers()).get('host') ?? '';

// Get the default search mode setting
const defaultSearchMode = await getDefaultSearchMode(domain);
const initialDefaultMode = isServiceError(defaultSearchMode) ? "precise" : defaultSearchMode;

// Get available language models to determine if "Ask" mode is available
const languageModels = await getConfiguredLanguageModelsInfo();
const isAskModeAvailable = languageModels.length > 0;

return (
<div className="flex flex-col gap-6">
<div>
Expand All @@ -52,6 +63,14 @@ export default async function GeneralSettingsPage(props: GeneralSettingsPageProp
currentUserRole={currentUserRole}
rootDomain={host}
/>

{currentUserRole === OrgRole.OWNER && (
<DefaultSearchModeCard
initialDefaultMode={initialDefaultMode}
currentUserRole={currentUserRole}
isAskModeAvailable={isAskModeAvailable}
/>
)}
</div>
)
}
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/types.ts
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { z } from "zod";

export const searchModeSchema = z.enum(["precise", "agentic"]);

export const orgMetadataSchema = z.object({
anonymousAccessEnabled: z.boolean().optional(),
defaultSearchMode: searchModeSchema.optional(),
})

export const demoSearchScopeSchema = z.object({
Expand Down

AltStyle によって変換されたページ (->オリジナル) /