mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 20:35:24 +00:00
rename search context to search scope
This commit is contained in:
parent
a5562d7942
commit
f9e0500ef8
15 changed files with 153 additions and 164 deletions
|
|
@ -8,7 +8,7 @@ import { CreateUIMessage } from 'ai';
|
|||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useChatId } from '../../useChatId';
|
||||
import { ContextItem } from '@/features/chat/components/chatBox/contextSelector';
|
||||
import { SearchScopeItem } from '@/features/chat/components/chatBox/searchScopeSelector';
|
||||
|
||||
interface ChatThreadPanelProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
|
|
@ -37,9 +37,9 @@ export const ChatThreadPanel = ({
|
|||
// Use the last user's last message to determine what repos and contexts we should select by default.
|
||||
const lastUserMessage = messages.findLast((message) => message.role === "user");
|
||||
const defaultSelectedRepos = lastUserMessage?.metadata?.selectedRepos ?? [];
|
||||
const defaultSelectedContexts = lastUserMessage?.metadata?.selectedContexts ?? [];
|
||||
const defaultSelectedReposets = lastUserMessage?.metadata?.selectedReposets ?? [];
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<ContextItem[]>([
|
||||
const [selectedItems, setSelectedItems] = useState<SearchScopeItem[]>([
|
||||
...defaultSelectedRepos.map(repoName => {
|
||||
const repoInfo = repos.find(r => r.repoName === repoName);
|
||||
return {
|
||||
|
|
@ -49,13 +49,13 @@ export const ChatThreadPanel = ({
|
|||
codeHostType: repoInfo?.codeHostType || ''
|
||||
};
|
||||
}),
|
||||
...defaultSelectedContexts.map(contextName => {
|
||||
const context = searchContexts.find(c => c.name === contextName);
|
||||
...defaultSelectedReposets.map(reposetName => {
|
||||
const reposet = searchContexts.find(c => c.name === reposetName);
|
||||
return {
|
||||
type: 'context' as const,
|
||||
value: contextName,
|
||||
name: contextName,
|
||||
repoCount: context?.repoNames.length || 0
|
||||
type: 'reposet' as const,
|
||||
value: reposetName,
|
||||
name: reposetName,
|
||||
repoCount: reposet?.repoNames.length || 0
|
||||
};
|
||||
})
|
||||
]);
|
||||
|
|
@ -67,7 +67,7 @@ export const ChatThreadPanel = ({
|
|||
}
|
||||
|
||||
try {
|
||||
const { inputMessage, selectedRepos, selectedContexts } = JSON.parse(setChatState) as SetChatStatePayload;
|
||||
const { inputMessage, selectedRepos, selectedReposets } = JSON.parse(setChatState) as SetChatStatePayload;
|
||||
setInputMessage(inputMessage);
|
||||
setSelectedItems([
|
||||
...selectedRepos.map(repoName => {
|
||||
|
|
@ -79,13 +79,13 @@ export const ChatThreadPanel = ({
|
|||
codeHostType: repoInfo?.codeHostType || ''
|
||||
};
|
||||
}),
|
||||
...selectedContexts.map(contextName => {
|
||||
const context = searchContexts.find(c => c.name === contextName);
|
||||
...selectedReposets.map(reposetName => {
|
||||
const reposet = searchContexts.find(c => c.name === reposetName);
|
||||
return {
|
||||
type: 'context' as const,
|
||||
value: contextName,
|
||||
name: contextName,
|
||||
repoCount: context?.repoNames.length || 0
|
||||
type: 'reposet' as const,
|
||||
value: reposetName,
|
||||
name: reposetName,
|
||||
repoCount: reposet?.repoNames.length || 0
|
||||
};
|
||||
})
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
|||
import { useCallback, useState } from "react";
|
||||
import { Descendant } from "slate";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
|
||||
import { SearchScopeItem } from "@/features/chat/components/chatBox/searchScopeSelector";
|
||||
|
||||
interface NewChatPanelProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
|
|
@ -25,7 +25,7 @@ export const NewChatPanel = ({
|
|||
searchContexts,
|
||||
order,
|
||||
}: NewChatPanelProps) => {
|
||||
const [selectedItems, setSelectedItems] = useLocalStorage<ContextItem[]>("selectedContextItems", [], { initializeWithValue: false });
|
||||
const [selectedItems, setSelectedItems] = useLocalStorage<SearchScopeItem[]>("selectedContextItems", [], { initializeWithValue: false });
|
||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
|||
import { useState } from "react";
|
||||
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
|
||||
import { SearchScopeItem } from "@/features/chat/components/chatBox/searchScopeSelector";
|
||||
import { DemoExamples } from "@/types";
|
||||
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
|
||||
|
||||
|
|
@ -34,7 +34,7 @@ export const AgenticSearch = ({
|
|||
demoExamples,
|
||||
}: AgenticSearchProps) => {
|
||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||
const [selectedItems, setSelectedItems] = useLocalStorage<ContextItem[]>("selectedContextItems", [], { initializeWithValue: false });
|
||||
const [selectedItems, setSelectedItems] = useLocalStorage<SearchScopeItem[]>("selectedContextItems", [], { initializeWithValue: false });
|
||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -74,10 +74,6 @@ export const AgenticSearch = ({
|
|||
{demoExamples && (
|
||||
<AskSourcebotDemoCards
|
||||
demoExamples={demoExamples}
|
||||
selectedItems={selectedItems}
|
||||
setSelectedItems={setSelectedItems}
|
||||
searchContexts={searchContexts}
|
||||
repos={repos}
|
||||
/>
|
||||
)}
|
||||
</div >
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Search, LibraryBigIcon, Code, Info } from "lucide-react";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import { DemoExamples, DemoSearchExample, DemoSearchContext } from "@/types";
|
||||
import { DemoExamples, DemoSearchExample, DemoSearchScope } from "@/types";
|
||||
import { cn, getCodeHostIcon } from "@/lib/utils";
|
||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||
import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard";
|
||||
|
|
@ -19,7 +19,7 @@ export const AskSourcebotDemoCards = ({
|
|||
demoExamples,
|
||||
}: AskSourcebotDemoCardsProps) => {
|
||||
const captureEvent = useCaptureEvent();
|
||||
const [selectedFilterContext, setSelectedFilterContext] = useState<number | null>(null);
|
||||
const [selectedFilterSearchScope, setSelectedFilterSearchScope] = useState<number | null>(null);
|
||||
|
||||
const handleExampleClick = (example: DemoSearchExample) => {
|
||||
captureEvent('wa_demo_search_example_card_pressed', {
|
||||
|
|
@ -32,16 +32,16 @@ export const AskSourcebotDemoCards = ({
|
|||
}
|
||||
}
|
||||
|
||||
const getContextIcon = (context: DemoSearchContext, size: number = 20, isSelected: boolean = false) => {
|
||||
const getSearchScopeIcon = (searchScope: DemoSearchScope, size: number = 20, isSelected: boolean = false) => {
|
||||
const sizeClass = size === 12 ? "h-3 w-3" : "h-5 w-5";
|
||||
const colorClass = isSelected ? "text-primary-foreground" : "text-muted-foreground";
|
||||
|
||||
if (context.type === "set") {
|
||||
if (searchScope.type === "reposet") {
|
||||
return <LibraryBigIcon className={cn(sizeClass, colorClass)} />;
|
||||
}
|
||||
|
||||
if (context.codeHostType) {
|
||||
const codeHostIcon = getCodeHostIcon(context.codeHostType);
|
||||
if (searchScope.codeHostType) {
|
||||
const codeHostIcon = getCodeHostIcon(searchScope.codeHostType);
|
||||
if (codeHostIcon) {
|
||||
// When selected, icons need to match the inverted badge colors
|
||||
// In light mode selected: light icon on dark bg (invert)
|
||||
|
|
@ -53,7 +53,7 @@ export const AskSourcebotDemoCards = ({
|
|||
return (
|
||||
<Image
|
||||
src={codeHostIcon.src}
|
||||
alt={`${context.codeHostType} icon`}
|
||||
alt={`${searchScope.codeHostType} icon`}
|
||||
width={size}
|
||||
height={size}
|
||||
className={cn(sizeClass, selectedIconClass)}
|
||||
|
|
@ -92,7 +92,7 @@ export const AskSourcebotDemoCards = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Context Filter */}
|
||||
{/* Search Scope Filter */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 mb-6">
|
||||
<div className="flex items-center gap-2 mr-2">
|
||||
<div className="relative group">
|
||||
|
|
@ -102,30 +102,30 @@ export const AskSourcebotDemoCards = ({
|
|||
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-border"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted-foreground">Search Context:</span>
|
||||
<span className="text-sm font-medium text-muted-foreground">Search Scope:</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={selectedFilterContext === null ? "default" : "secondary"}
|
||||
className={`cursor-pointer transition-all duration-200 hover:shadow-sm ${selectedFilterContext === null ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
|
||||
variant={selectedFilterSearchScope === null ? "default" : "secondary"}
|
||||
className={`cursor-pointer transition-all duration-200 hover:shadow-sm ${selectedFilterSearchScope === null ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedFilterContext(null);
|
||||
setSelectedFilterSearchScope(null);
|
||||
}}
|
||||
>
|
||||
All
|
||||
</Badge>
|
||||
{demoExamples.searchContexts.map((context) => (
|
||||
{demoExamples.searchScopes.map((searchScope) => (
|
||||
<Badge
|
||||
key={context.id}
|
||||
variant={selectedFilterContext === context.id ? "default" : "secondary"}
|
||||
className={`cursor-pointer transition-all duration-200 hover:shadow-sm flex items-center gap-1 ${selectedFilterContext === context.id ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
|
||||
key={searchScope.id}
|
||||
variant={selectedFilterSearchScope === searchScope.id ? "default" : "secondary"}
|
||||
className={`cursor-pointer transition-all duration-200 hover:shadow-sm flex items-center gap-1 ${selectedFilterSearchScope === searchScope.id ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedFilterContext(context.id);
|
||||
setSelectedFilterSearchScope(searchScope.id);
|
||||
}}
|
||||
>
|
||||
{getContextIcon(context, 12, selectedFilterContext === context.id)}
|
||||
{context.displayName}
|
||||
{getSearchScopeIcon(searchScope, 12, selectedFilterSearchScope === searchScope.id)}
|
||||
{searchScope.displayName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -133,11 +133,11 @@ export const AskSourcebotDemoCards = ({
|
|||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{demoExamples.searchExamples
|
||||
.filter((example) => {
|
||||
if (selectedFilterContext === null) return true;
|
||||
return example.searchContext.includes(selectedFilterContext);
|
||||
if (selectedFilterSearchScope === null) return true;
|
||||
return example.searchScopes.includes(selectedFilterSearchScope);
|
||||
})
|
||||
.map((example) => {
|
||||
const searchContexts = demoExamples.searchContexts.filter((context) => example.searchContext.includes(context.id))
|
||||
const searchScopes = demoExamples.searchScopes.filter((searchScope) => example.searchScopes.includes(searchScope.id))
|
||||
return (
|
||||
<Card
|
||||
key={example.url}
|
||||
|
|
@ -147,10 +147,10 @@ export const AskSourcebotDemoCards = ({
|
|||
<CardContent className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
{searchContexts.map((context) => (
|
||||
<Badge key={context.value} variant="secondary" className="text-[10px] px-1.5 py-0.5 h-4 flex items-center gap-1">
|
||||
{getContextIcon(context, 12)}
|
||||
{context.displayName}
|
||||
{searchScopes.map((searchScope) => (
|
||||
<Badge key={searchScope.value} variant="secondary" className="text-[10px] px-1.5 py-0.5 h-4 flex items-center gap-1">
|
||||
{getSearchScopeIcon(searchScope, 12)}
|
||||
{searchScope.displayName}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -64,12 +64,12 @@ export async function POST(req: Request) {
|
|||
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||
}
|
||||
|
||||
const { messages, id, selectedRepos, selectedContexts, languageModelId } = parsed.data;
|
||||
const { messages, id, selectedRepos, selectedReposets, languageModelId } = parsed.data;
|
||||
const response = await chatHandler({
|
||||
messages,
|
||||
id,
|
||||
selectedRepos,
|
||||
selectedContexts,
|
||||
selectedReposets,
|
||||
languageModelId,
|
||||
}, domain);
|
||||
|
||||
|
|
@ -94,11 +94,11 @@ interface ChatHandlerProps {
|
|||
messages: SBChatMessage[];
|
||||
id: string;
|
||||
selectedRepos: string[];
|
||||
selectedContexts?: string[];
|
||||
selectedReposets?: string[];
|
||||
languageModelId: string;
|
||||
}
|
||||
|
||||
const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageModelId }: ChatHandlerProps, domain: string) => sew(async () =>
|
||||
const chatHandler = ({ messages, id, selectedRepos, selectedReposets, languageModelId }: ChatHandlerProps, domain: string) => sew(async () =>
|
||||
withAuth((userId) =>
|
||||
withOrgMembership(userId, domain, async ({ org }) => {
|
||||
const chat = await prisma.chat.findUnique({
|
||||
|
|
@ -190,23 +190,23 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo
|
|||
|
||||
// Expand search contexts to repos
|
||||
let expandedRepos = [...selectedRepos];
|
||||
if (selectedContexts && selectedContexts.length > 0) {
|
||||
const searchContexts = await prisma.searchContext.findMany({
|
||||
if (selectedReposets && selectedReposets.length > 0) {
|
||||
const searchReposets = await prisma.searchContext.findMany({
|
||||
where: {
|
||||
orgId: org.id,
|
||||
name: { in: selectedContexts }
|
||||
name: { in: selectedReposets }
|
||||
},
|
||||
include: {
|
||||
repos: true
|
||||
}
|
||||
});
|
||||
|
||||
const contextRepos = searchContexts.flatMap(context =>
|
||||
context.repos.map(repo => repo.name)
|
||||
const reposetRepos = searchReposets.flatMap(reposet =>
|
||||
reposet.repos.map(repo => repo.name)
|
||||
);
|
||||
|
||||
// Combine and deduplicate repos
|
||||
expandedRepos = Array.from(new Set([...selectedRepos, ...contextRepos]));
|
||||
expandedRepos = Array.from(new Set([...selectedRepos, ...reposetRepos]));
|
||||
}
|
||||
|
||||
const researchStream = await createAgentStream({
|
||||
|
|
|
|||
15
packages/web/src/components/atMentionInfoCard.tsx
Normal file
15
packages/web/src/components/atMentionInfoCard.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { AtSignIcon } from "lucide-react";
|
||||
|
||||
export const AtMentionInfoCard = () => {
|
||||
return (
|
||||
<div className="bg-popover border border-border rounded-lg shadow-lg p-4 w-80 max-w-[90vw]">
|
||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-border/50">
|
||||
<AtSignIcon className="h-4 w-4 text-primary" />
|
||||
<h4 className="text-sm font-semibold text-popover-foreground">Mention</h4>
|
||||
</div>
|
||||
<div className="text-sm text-popover-foreground leading-relaxed">
|
||||
When asking Sourcebot a question, you can @ mention files to include them in the context of the search.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import Image from "next/image";
|
||||
import { LibraryBigIcon, Code, Layers } from "lucide-react";
|
||||
import { LibraryBigIcon, Code, ScanSearchIcon } from "lucide-react";
|
||||
import { cn, getCodeHostIcon } from "@/lib/utils";
|
||||
|
||||
export const SearchScopeInfoCard = () => {
|
||||
return (
|
||||
<div className="bg-popover border border-border rounded-lg shadow-lg p-4 w-80 max-w-[90vw]">
|
||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-border/50">
|
||||
<Layers className="h-4 w-4 text-primary" />
|
||||
<h4 className="text-sm font-semibold text-popover-foreground">Search Context</h4>
|
||||
<ScanSearchIcon className="h-4 w-4 text-primary" />
|
||||
<h4 className="text-sm font-semibold text-popover-foreground">Search Scope</h4>
|
||||
</div>
|
||||
<div className="text-sm text-popover-foreground leading-relaxed">
|
||||
When asking Sourcebot a question, you can select one or more search contexts to constrain its scope. There
|
||||
are two different types of search contexts:
|
||||
When asking Sourcebot a question, you can select one or more scopes to constrain the search.
|
||||
There are two different types of search scopes:
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{(() => {
|
||||
|
|
@ -32,7 +32,7 @@ export const SearchScopeInfoCard = () => {
|
|||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LibraryBigIcon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<span><strong>Set</strong>: A set of repositories, indicated by the library icon.</span>
|
||||
<span><strong>Reposet</strong>: A set of repositories, indicated by the library icon.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { Suggestion } from "./types";
|
|||
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
|
||||
import { useSuggestionsData } from "./useSuggestionsData";
|
||||
import { useToast } from "@/components/hooks/use-toast";
|
||||
import { ContextItem } from "./contextSelector";
|
||||
import { SearchScopeItem } from "./searchScopeSelector";
|
||||
import { SearchContextQuery } from "@/lib/types";
|
||||
|
||||
interface ChatBoxProps {
|
||||
|
|
@ -29,7 +29,7 @@ interface ChatBoxProps {
|
|||
isRedirecting?: boolean;
|
||||
isGenerating?: boolean;
|
||||
languageModels: LanguageModelInfo[];
|
||||
selectedItems: ContextItem[];
|
||||
selectedItems: SearchScopeItem[];
|
||||
searchContexts: SearchContextQuery[];
|
||||
onContextSelectorOpenChanged: (isOpen: boolean) => void;
|
||||
}
|
||||
|
|
@ -58,10 +58,10 @@ export const ChatBox = ({
|
|||
return [item.value];
|
||||
}
|
||||
|
||||
if (item.type === 'context') {
|
||||
const context = searchContexts.find((context) => context.name === item.value);
|
||||
if (context) {
|
||||
return context.repoNames;
|
||||
if (item.type === 'reposet') {
|
||||
const reposet = searchContexts.find((reposet) => reposet.name === item.value);
|
||||
if (reposet) {
|
||||
return reposet.repoNames;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ export const ChatBox = ({
|
|||
if (isSubmitDisabled) {
|
||||
if (isSubmitDisabledReason === "no-repos-selected") {
|
||||
toast({
|
||||
description: "⚠️ You must select at least one search context",
|
||||
description: "⚠️ You must select at least one search scope",
|
||||
variant: "destructive",
|
||||
});
|
||||
onContextSelectorOpenChanged(true);
|
||||
|
|
@ -284,7 +284,7 @@ export const ChatBox = ({
|
|||
>
|
||||
<Editable
|
||||
className="w-full focus-visible:outline-none focus-visible:ring-0 bg-background text-base disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
||||
placeholder="Ask questions about the selected search contexts. @mention files to refine your query."
|
||||
placeholder="Ask a question about the selected search scopes. @mention files to refine your query."
|
||||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
onKeyDown={onKeyDown}
|
||||
|
|
@ -339,7 +339,7 @@ export const ChatBox = ({
|
|||
<TooltipContent>
|
||||
<div className="flex flex-row items-center">
|
||||
<TriangleAlertIcon className="h-4 w-4 text-warning mr-1" />
|
||||
<span className="text-destructive">You must select at least one search context</span>
|
||||
<span className="text-destructive">You must select at least one search scope</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
'use client';
|
||||
|
||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
|
@ -8,19 +7,19 @@ import { LanguageModelInfo } from "@/features/chat/types";
|
|||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||
import { AtSignIcon } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { ReactEditor, useSlate } from "slate-react";
|
||||
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
|
||||
import { LanguageModelSelector } from "./languageModelSelector";
|
||||
import { ContextSelector, type ContextItem } from "./contextSelector";
|
||||
import { SearchScopeSelector, type SearchScopeItem } from "./searchScopeSelector";
|
||||
import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard";
|
||||
import { AtMentionInfoCard } from "@/components/atMentionInfoCard";
|
||||
|
||||
export interface ChatBoxToolbarProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
repos: RepositoryQuery[];
|
||||
searchContexts: SearchContextQuery[];
|
||||
selectedItems: ContextItem[];
|
||||
onSelectedItemsChange: (items: ContextItem[]) => void;
|
||||
selectedItems: SearchScopeItem[];
|
||||
onSelectedItemsChange: (items: SearchScopeItem[]) => void;
|
||||
isContextSelectorOpen: boolean;
|
||||
onContextSelectorOpenChanged: (isOpen: boolean) => void;
|
||||
}
|
||||
|
|
@ -41,15 +40,6 @@ export const ChatBoxToolbar = ({
|
|||
ReactEditor.focus(editor);
|
||||
}, [editor]);
|
||||
|
||||
useHotkeys("alt+mod+p", (e) => {
|
||||
e.preventDefault();
|
||||
onAddContext();
|
||||
}, {
|
||||
enableOnFormTags: true,
|
||||
enableOnContentEditable: true,
|
||||
description: "Add context",
|
||||
});
|
||||
|
||||
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
|
||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||
});
|
||||
|
|
@ -67,19 +57,14 @@ export const ChatBoxToolbar = ({
|
|||
<AtSignIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
className="flex flex-row items-center gap-2"
|
||||
>
|
||||
<KeyboardShortcutHint shortcut="⌥ ⌘ P" />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<span>Add context</span>
|
||||
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
||||
<AtMentionInfoCard />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="h-3 mx-1" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<ContextSelector
|
||||
<SearchScopeSelector
|
||||
className="bg-inherit w-fit h-6 min-h-6"
|
||||
repos={repos}
|
||||
searchContexts={searchContexts}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
CheckIcon,
|
||||
ChevronDown,
|
||||
FolderIcon,
|
||||
LayersIcon,
|
||||
ScanSearchIcon,
|
||||
LibraryBigIcon,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
|
@ -29,35 +29,35 @@ import {
|
|||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
|
||||
export type RepoContextItem = {
|
||||
export type RepoSearchScopeItem = {
|
||||
type: 'repo';
|
||||
value: string;
|
||||
name: string;
|
||||
codeHostType: string;
|
||||
}
|
||||
|
||||
export type SearchContextItem = {
|
||||
type: 'context';
|
||||
export type RepoSetSearchScopeItem = {
|
||||
type: 'reposet';
|
||||
value: string;
|
||||
name: string;
|
||||
repoCount: number;
|
||||
}
|
||||
|
||||
export type ContextItem = RepoContextItem | SearchContextItem;
|
||||
export type SearchScopeItem = RepoSearchScopeItem | RepoSetSearchScopeItem;
|
||||
|
||||
interface ContextSelectorProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
interface SearchScopeSelectorProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
repos: RepositoryQuery[];
|
||||
searchContexts: SearchContextQuery[];
|
||||
selectedItems: ContextItem[];
|
||||
onSelectedItemsChange: (items: ContextItem[]) => void;
|
||||
selectedItems: SearchScopeItem[];
|
||||
onSelectedItemsChange: (items: SearchScopeItem[]) => void;
|
||||
className?: string;
|
||||
isOpen: boolean;
|
||||
onOpenChanged: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
export const ContextSelector = React.forwardRef<
|
||||
export const SearchScopeSelector = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ContextSelectorProps
|
||||
SearchScopeSelectorProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
|
|
@ -87,7 +87,7 @@ export const ContextSelector = React.forwardRef<
|
|||
}
|
||||
};
|
||||
|
||||
const toggleItem = (item: ContextItem) => {
|
||||
const toggleItem = (item: SearchScopeItem) => {
|
||||
// Store current scroll position before state update
|
||||
if (scrollContainerRef.current) {
|
||||
scrollPosition.current = scrollContainerRef.current.scrollTop;
|
||||
|
|
@ -99,13 +99,13 @@ export const ContextSelector = React.forwardRef<
|
|||
|
||||
const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo";
|
||||
|
||||
let newSelectedItems: ContextItem[];
|
||||
let newSelectedItems: SearchScopeItem[];
|
||||
if (isSelected) {
|
||||
newSelectedItems = selectedItems.filter(
|
||||
(selected) => !(selected.type === item.type && selected.value === item.value)
|
||||
);
|
||||
} else {
|
||||
// Limit selected context to 1 in demo mode
|
||||
// Limit selected search scope to 1 in demo mode
|
||||
if (isDemoMode) {
|
||||
newSelectedItems = [item];
|
||||
} else {
|
||||
|
|
@ -124,26 +124,26 @@ export const ContextSelector = React.forwardRef<
|
|||
onOpenChanged(!isOpen);
|
||||
};
|
||||
|
||||
const allItems = React.useMemo(() => {
|
||||
const contextItems: ContextItem[] = searchContexts.map(context => ({
|
||||
type: 'context' as const,
|
||||
const allSearchScopeItems = React.useMemo(() => {
|
||||
const repoSetSearchScopeItems: RepoSetSearchScopeItem[] = searchContexts.map(context => ({
|
||||
type: 'reposet' as const,
|
||||
value: context.name,
|
||||
name: context.name,
|
||||
repoCount: context.repoNames.length
|
||||
}));
|
||||
|
||||
const repoItems: ContextItem[] = repos.map(repo => ({
|
||||
const repoSearchScopeItems: RepoSearchScopeItem[] = repos.map(repo => ({
|
||||
type: 'repo' as const,
|
||||
value: repo.repoName,
|
||||
name: repo.repoDisplayName || repo.repoName.split('/').pop() || repo.repoName,
|
||||
codeHostType: repo.codeHostType,
|
||||
}));
|
||||
|
||||
return [...contextItems, ...repoItems];
|
||||
return [...repoSetSearchScopeItems, ...repoSearchScopeItems];
|
||||
}, [repos, searchContexts]);
|
||||
|
||||
const sortedItems = React.useMemo(() => {
|
||||
return allItems
|
||||
const sortedSearchScopeItems = React.useMemo(() => {
|
||||
return allSearchScopeItems
|
||||
.map((item) => ({
|
||||
item,
|
||||
isSelected: selectedItems.some(
|
||||
|
|
@ -154,19 +154,19 @@ export const ContextSelector = React.forwardRef<
|
|||
// Selected items first
|
||||
if (a.isSelected && !b.isSelected) return -1;
|
||||
if (!a.isSelected && b.isSelected) return 1;
|
||||
// Then contexts before repos
|
||||
if (a.item.type === 'context' && b.item.type === 'repo') return -1;
|
||||
if (a.item.type === 'repo' && b.item.type === 'context') return 1;
|
||||
// Then reposets before repos
|
||||
if (a.item.type === 'reposet' && b.item.type === 'repo') return -1;
|
||||
if (a.item.type === 'repo' && b.item.type === 'reposet') return 1;
|
||||
return 0;
|
||||
})
|
||||
}, [allItems, selectedItems]);
|
||||
}, [allSearchScopeItems, selectedItems]);
|
||||
|
||||
// Restore scroll position after re-render
|
||||
React.useEffect(() => {
|
||||
if (scrollContainerRef.current && scrollPosition.current > 0) {
|
||||
scrollContainerRef.current.scrollTop = scrollPosition.current;
|
||||
}
|
||||
}, [sortedItems]);
|
||||
}, [sortedSearchScopeItems]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
|
|
@ -184,12 +184,12 @@ export const ContextSelector = React.forwardRef<
|
|||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full mx-auto">
|
||||
<LayersIcon className="h-4 w-4 text-muted-foreground mr-1" />
|
||||
<ScanSearchIcon className="h-4 w-4 text-muted-foreground mr-1" />
|
||||
<span
|
||||
className={cn("text-sm text-muted-foreground mx-1 font-medium")}
|
||||
>
|
||||
{
|
||||
selectedItems.length === 0 ? `Select context` :
|
||||
selectedItems.length === 0 ? `Search scopes` :
|
||||
selectedItems.length === 1 ? selectedItems[0].name :
|
||||
`${selectedItems.length} selected`
|
||||
}
|
||||
|
|
@ -205,13 +205,13 @@ export const ContextSelector = React.forwardRef<
|
|||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search contexts..."
|
||||
placeholder="Search scopes..."
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<CommandList ref={scrollContainerRef}>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{sortedItems.map(({ item, isSelected }) => {
|
||||
{sortedSearchScopeItems.map(({ item, isSelected }) => {
|
||||
return (
|
||||
<CommandItem
|
||||
key={`${item.type}-${item.value}`}
|
||||
|
|
@ -229,7 +229,7 @@ export const ContextSelector = React.forwardRef<
|
|||
<CheckIcon className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
{item.type === 'context' ? (
|
||||
{item.type === 'reposet' ? (
|
||||
<LibraryBigIcon className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
// Render code host icon for repos
|
||||
|
|
@ -253,7 +253,7 @@ export const ContextSelector = React.forwardRef<
|
|||
<span className="font-medium">
|
||||
{item.name}
|
||||
</span>
|
||||
{item.type === 'context' && (
|
||||
{item.type === 'reposet' && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="text-[10px] px-1.5 py-0 h-4 bg-primary text-primary-foreground"
|
||||
|
|
@ -287,4 +287,4 @@ export const ContextSelector = React.forwardRef<
|
|||
}
|
||||
);
|
||||
|
||||
ContextSelector.displayName = "ContextSelector";
|
||||
SearchScopeSelector.displayName = "SearchScopeSelector";
|
||||
|
|
@ -23,7 +23,7 @@ import { ErrorBanner } from './errorBanner';
|
|||
import { useRouter } from 'next/navigation';
|
||||
import { usePrevious } from '@uidotdev/usehooks';
|
||||
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
||||
import { ContextItem } from '../chatBox/contextSelector';
|
||||
import { SearchScopeItem } from '../chatBox/searchScopeSelector';
|
||||
|
||||
type ChatHistoryState = {
|
||||
scrollOffset?: number;
|
||||
|
|
@ -36,8 +36,8 @@ interface ChatThreadProps {
|
|||
languageModels: LanguageModelInfo[];
|
||||
repos: RepositoryQuery[];
|
||||
searchContexts: SearchContextQuery[];
|
||||
selectedItems: ContextItem[];
|
||||
onSelectedItemsChange: (items: ContextItem[]) => void;
|
||||
selectedItems: SearchScopeItem[];
|
||||
onSelectedItemsChange: (items: SearchScopeItem[]) => void;
|
||||
isChatReadonly: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -62,10 +62,10 @@ export const ChatThread = ({
|
|||
const router = useRouter();
|
||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||
|
||||
const { selectedRepos, selectedContexts } = useMemo(() => {
|
||||
const { selectedRepos, selectedReposets } = useMemo(() => {
|
||||
const repos = selectedItems.filter(item => item.type === 'repo').map(item => item.value);
|
||||
const contexts = selectedItems.filter(item => item.type === 'context').map(item => item.value);
|
||||
return { selectedRepos: repos, selectedContexts: contexts };
|
||||
const reposets = selectedItems.filter(item => item.type === 'reposet').map(item => item.value);
|
||||
return { selectedRepos: repos, selectedReposets: reposets };
|
||||
}, [selectedItems]);
|
||||
|
||||
// Initial state is from attachments that exist in in the chat history.
|
||||
|
|
@ -123,11 +123,11 @@ export const ChatThread = ({
|
|||
_sendMessage(message, {
|
||||
body: {
|
||||
selectedRepos,
|
||||
selectedContexts,
|
||||
selectedReposets,
|
||||
languageModelId: selectedLanguageModel.model,
|
||||
} satisfies AdditionalChatRequestParams,
|
||||
});
|
||||
}, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedContexts]);
|
||||
});
|
||||
}, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedReposets]);
|
||||
|
||||
|
||||
const messagePairs = useMessagePairs(messages);
|
||||
|
|
@ -243,13 +243,13 @@ export const ChatThread = ({
|
|||
const text = slateContentToString(children);
|
||||
const mentions = getAllMentionElements(children);
|
||||
|
||||
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos, selectedContexts);
|
||||
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos, selectedReposets);
|
||||
sendMessage(message);
|
||||
|
||||
setIsAutoScrollEnabled(true);
|
||||
|
||||
resetEditor(editor);
|
||||
}, [sendMessage, selectedRepos, selectedContexts]);
|
||||
}, [sendMessage, selectedRepos, selectedReposets]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const sbChatMessageMetadataSchema = z.object({
|
|||
userId: z.string(),
|
||||
})).optional(),
|
||||
selectedRepos: z.array(z.string()).optional(),
|
||||
selectedContexts: z.array(z.string()).optional(),
|
||||
selectedReposets: z.array(z.string()).optional(),
|
||||
traceId: z.string().optional(),
|
||||
});
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ export const SET_CHAT_STATE_QUERY_PARAM = 'setChatState';
|
|||
export type SetChatStatePayload = {
|
||||
inputMessage: CreateUIMessage<SBChatMessage>;
|
||||
selectedRepos: string[];
|
||||
selectedContexts: string[];
|
||||
selectedReposets: string[];
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -158,6 +158,6 @@ export type LanguageModelInfo = {
|
|||
export const additionalChatRequestParamsSchema = z.object({
|
||||
languageModelId: z.string(),
|
||||
selectedRepos: z.array(z.string()),
|
||||
selectedContexts: z.array(z.string()),
|
||||
selectedReposets: z.array(z.string()),
|
||||
});
|
||||
export type AdditionalChatRequestParams = z.infer<typeof additionalChatRequestParamsSchema>;
|
||||
|
|
@ -11,7 +11,7 @@ import { createChat } from "./actions";
|
|||
import { isServiceError } from "@/lib/utils";
|
||||
import { createPathWithQueryParams } from "@/lib/utils";
|
||||
import { SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from "./types";
|
||||
import { ContextItem } from "./components/chatBox/contextSelector";
|
||||
import { SearchScopeItem } from "./components/chatBox/searchScopeSelector";
|
||||
|
||||
export const useCreateNewChatThread = () => {
|
||||
const domain = useDomain();
|
||||
|
|
@ -19,15 +19,15 @@ export const useCreateNewChatThread = () => {
|
|||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
|
||||
const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: ContextItem[]) => {
|
||||
const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: SearchScopeItem[]) => {
|
||||
const text = slateContentToString(children);
|
||||
const mentions = getAllMentionElements(children);
|
||||
|
||||
// Extract repos and contexts from selectedItems
|
||||
// Extract repos and reposets from selectedItems
|
||||
const selectedRepos = selectedItems.filter(item => item.type === 'repo').map(item => item.value);
|
||||
const selectedContexts = selectedItems.filter(item => item.type === 'context').map(item => item.value);
|
||||
const selectedReposets = selectedItems.filter(item => item.type === 'reposet').map(item => item.value);
|
||||
|
||||
const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedContexts);
|
||||
const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedReposets);
|
||||
|
||||
setIsLoading(true);
|
||||
const response = await createChat(domain);
|
||||
|
|
@ -43,7 +43,7 @@ export const useCreateNewChatThread = () => {
|
|||
[SET_CHAT_STATE_QUERY_PARAM, JSON.stringify({
|
||||
inputMessage,
|
||||
selectedRepos,
|
||||
selectedContexts,
|
||||
selectedReposets,
|
||||
} satisfies SetChatStatePayload)],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ export const addLineNumbers = (source: string, lineOffset = 1) => {
|
|||
return source.split('\n').map((line, index) => `${index + lineOffset}:${line}`).join('\n');
|
||||
}
|
||||
|
||||
export const createUIMessage = (text: string, mentions: MentionData[], selectedRepos: string[], selectedContexts: string[]): CreateUIMessage<SBChatMessage> => {
|
||||
export const createUIMessage = (text: string, mentions: MentionData[], selectedRepos: string[], selectedReposets: string[]): CreateUIMessage<SBChatMessage> => {
|
||||
// Converts applicable mentions into sources.
|
||||
const sources: Source[] = mentions
|
||||
.map((mention) => {
|
||||
|
|
@ -206,7 +206,7 @@ export const createUIMessage = (text: string, mentions: MentionData[], selectedR
|
|||
],
|
||||
metadata: {
|
||||
selectedRepos,
|
||||
selectedContexts,
|
||||
selectedReposets,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ export const orgMetadataSchema = z.object({
|
|||
anonymousAccessEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const demoSearchContextSchema = z.object({
|
||||
export const demoSearchScopeSchema = z.object({
|
||||
id: z.number(),
|
||||
displayName: z.string(),
|
||||
value: z.string(),
|
||||
type: z.enum(["repo", "set"]),
|
||||
type: z.enum(["repo", "reposet"]),
|
||||
codeHostType: z.string().optional(),
|
||||
})
|
||||
|
||||
|
|
@ -16,22 +16,15 @@ export const demoSearchExampleSchema = z.object({
|
|||
title: z.string(),
|
||||
description: z.string(),
|
||||
url: z.string(),
|
||||
searchContext: z.array(z.number())
|
||||
})
|
||||
|
||||
export const demoSearchContextExampleSchema = z.object({
|
||||
searchContext: z.number(),
|
||||
description: z.string(),
|
||||
searchScopes: z.array(z.number())
|
||||
})
|
||||
|
||||
export const demoExamplesSchema = z.object({
|
||||
searchContexts: demoSearchContextSchema.array(),
|
||||
searchScopes: demoSearchScopeSchema.array(),
|
||||
searchExamples: demoSearchExampleSchema.array(),
|
||||
searchContextExamples: demoSearchContextExampleSchema.array(),
|
||||
})
|
||||
|
||||
export type OrgMetadata = z.infer<typeof orgMetadataSchema>;
|
||||
export type DemoExamples = z.infer<typeof demoExamplesSchema>;
|
||||
export type DemoSearchContext = z.infer<typeof demoSearchContextSchema>;
|
||||
export type DemoSearchExample = z.infer<typeof demoSearchExampleSchema>;
|
||||
export type DemoSearchContextExample = z.infer<typeof demoSearchContextExampleSchema>;
|
||||
export type DemoSearchScope = z.infer<typeof demoSearchScopeSchema>;
|
||||
export type DemoSearchExample = z.infer<typeof demoSearchExampleSchema>;
|
||||
Loading…
Reference in a new issue