mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Search scope refactor (#405)
* new demo card ui * rename search context to search scope * rename everything to use search scope * add changelog entry
This commit is contained in:
parent
be9979f18a
commit
6662d20ee8
19 changed files with 396 additions and 465 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
### Added
|
### Added
|
||||||
- Add search context to ask sourcebot context selector. [#397](https://github.com/sourcebot-dev/sourcebot/pull/397)
|
- Add search context to ask sourcebot context selector. [#397](https://github.com/sourcebot-dev/sourcebot/pull/397)
|
||||||
- Add ability to include/exclude connection in search context. [#399](https://github.com/sourcebot-dev/sourcebot/pull/399)
|
- Add ability to include/exclude connection in search context. [#399](https://github.com/sourcebot-dev/sourcebot/pull/399)
|
||||||
|
- Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398)
|
- Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
|
|
||||||
import { ResizablePanel } from '@/components/ui/resizable';
|
import { ResizablePanel } from '@/components/ui/resizable';
|
||||||
import { ChatThread } from '@/features/chat/components/chatThread';
|
import { ChatThread } from '@/features/chat/components/chatThread';
|
||||||
import { LanguageModelInfo, SBChatMessage, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from '@/features/chat/types';
|
import { LanguageModelInfo, SBChatMessage, SearchScope, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from '@/features/chat/types';
|
||||||
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
||||||
import { CreateUIMessage } from 'ai';
|
import { CreateUIMessage } from 'ai';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useChatId } from '../../useChatId';
|
import { useChatId } from '../../useChatId';
|
||||||
import { ContextItem } from '@/features/chat/components/chatBox/contextSelector';
|
|
||||||
|
|
||||||
interface ChatThreadPanelProps {
|
interface ChatThreadPanelProps {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
|
|
@ -36,29 +35,8 @@ export const ChatThreadPanel = ({
|
||||||
|
|
||||||
// Use the last user's last message to determine what repos and contexts we should select by default.
|
// 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 lastUserMessage = messages.findLast((message) => message.role === "user");
|
||||||
const defaultSelectedRepos = lastUserMessage?.metadata?.selectedRepos ?? [];
|
const defaultSelectedSearchScopes = lastUserMessage?.metadata?.selectedSearchScopes ?? [];
|
||||||
const defaultSelectedContexts = lastUserMessage?.metadata?.selectedContexts ?? [];
|
const [selectedSearchScopes, setSelectedSearchScopes] = useState<SearchScope[]>(defaultSelectedSearchScopes);
|
||||||
|
|
||||||
const [selectedItems, setSelectedItems] = useState<ContextItem[]>([
|
|
||||||
...defaultSelectedRepos.map(repoName => {
|
|
||||||
const repoInfo = repos.find(r => r.repoName === repoName);
|
|
||||||
return {
|
|
||||||
type: 'repo' as const,
|
|
||||||
value: repoName,
|
|
||||||
name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName,
|
|
||||||
codeHostType: repoInfo?.codeHostType || ''
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
...defaultSelectedContexts.map(contextName => {
|
|
||||||
const context = searchContexts.find(c => c.name === contextName);
|
|
||||||
return {
|
|
||||||
type: 'context' as const,
|
|
||||||
value: contextName,
|
|
||||||
name: contextName,
|
|
||||||
repoCount: context?.repoNames.length || 0
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setChatState = searchParams.get(SET_CHAT_STATE_QUERY_PARAM);
|
const setChatState = searchParams.get(SET_CHAT_STATE_QUERY_PARAM);
|
||||||
|
|
@ -67,28 +45,9 @@ export const ChatThreadPanel = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { inputMessage, selectedRepos, selectedContexts } = JSON.parse(setChatState) as SetChatStatePayload;
|
const { inputMessage, selectedSearchScopes } = JSON.parse(setChatState) as SetChatStatePayload;
|
||||||
setInputMessage(inputMessage);
|
setInputMessage(inputMessage);
|
||||||
setSelectedItems([
|
setSelectedSearchScopes(selectedSearchScopes);
|
||||||
...selectedRepos.map(repoName => {
|
|
||||||
const repoInfo = repos.find(r => r.repoName === repoName);
|
|
||||||
return {
|
|
||||||
type: 'repo' as const,
|
|
||||||
value: repoName,
|
|
||||||
name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName,
|
|
||||||
codeHostType: repoInfo?.codeHostType || ''
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
...selectedContexts.map(contextName => {
|
|
||||||
const context = searchContexts.find(c => c.name === contextName);
|
|
||||||
return {
|
|
||||||
type: 'context' as const,
|
|
||||||
value: contextName,
|
|
||||||
name: contextName,
|
|
||||||
repoCount: context?.repoNames.length || 0
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
} catch {
|
} catch {
|
||||||
console.error('Invalid message in URL');
|
console.error('Invalid message in URL');
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +56,7 @@ export const ChatThreadPanel = ({
|
||||||
const newSearchParams = new URLSearchParams(searchParams.toString());
|
const newSearchParams = new URLSearchParams(searchParams.toString());
|
||||||
newSearchParams.delete(SET_CHAT_STATE_QUERY_PARAM);
|
newSearchParams.delete(SET_CHAT_STATE_QUERY_PARAM);
|
||||||
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
|
router.replace(`?${newSearchParams.toString()}`, { scroll: false });
|
||||||
}, [searchParams, router, repos, searchContexts]);
|
}, [searchParams, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
|
|
@ -113,8 +72,8 @@ export const ChatThreadPanel = ({
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
repos={repos}
|
repos={repos}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
onSelectedItemsChange={setSelectedItems}
|
onSelectedSearchScopesChange={setSelectedSearchScopes}
|
||||||
isChatReadonly={isChatReadonly}
|
isChatReadonly={isChatReadonly}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,11 @@ import { ChatBox } from "@/features/chat/components/chatBox";
|
||||||
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
|
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
|
||||||
import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
|
import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
|
||||||
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
|
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
|
||||||
import { LanguageModelInfo } from "@/features/chat/types";
|
import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
|
||||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Descendant } from "slate";
|
import { Descendant } from "slate";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
|
|
||||||
|
|
||||||
interface NewChatPanelProps {
|
interface NewChatPanelProps {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
|
|
@ -25,13 +24,13 @@ export const NewChatPanel = ({
|
||||||
searchContexts,
|
searchContexts,
|
||||||
order,
|
order,
|
||||||
}: NewChatPanelProps) => {
|
}: NewChatPanelProps) => {
|
||||||
const [selectedItems, setSelectedItems] = useLocalStorage<ContextItem[]>("selectedContextItems", [], { initializeWithValue: false });
|
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
|
||||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||||
|
|
||||||
const onSubmit = useCallback((children: Descendant[]) => {
|
const onSubmit = useCallback((children: Descendant[]) => {
|
||||||
createNewChatThread(children, selectedItems);
|
createNewChatThread(children, selectedSearchScopes);
|
||||||
}, [createNewChatThread, selectedItems]);
|
}, [createNewChatThread, selectedSearchScopes]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -50,7 +49,7 @@ export const NewChatPanel = ({
|
||||||
preferredSuggestionsBoxPlacement="bottom-start"
|
preferredSuggestionsBoxPlacement="bottom-start"
|
||||||
isRedirecting={isLoading}
|
isRedirecting={isLoading}
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
/>
|
/>
|
||||||
|
|
@ -59,8 +58,8 @@ export const NewChatPanel = ({
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
repos={repos}
|
repos={repos}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
onSelectedItemsChange={setSelectedItems}
|
onSelectedSearchScopesChange={setSelectedSearchScopes}
|
||||||
isContextSelectorOpen={isContextSelectorOpen}
|
isContextSelectorOpen={isContextSelectorOpen}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { ChatBox } from "@/features/chat/components/chatBox";
|
import { ChatBox } from "@/features/chat/components/chatBox";
|
||||||
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
|
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
|
||||||
import { LanguageModelInfo } from "@/features/chat/types";
|
import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
|
||||||
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
|
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
|
||||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
|
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
|
|
||||||
import { DemoExamples } from "@/types";
|
import { DemoExamples } from "@/types";
|
||||||
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
|
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
|
||||||
|
|
||||||
|
|
@ -34,7 +33,7 @@ export const AgenticSearch = ({
|
||||||
demoExamples,
|
demoExamples,
|
||||||
}: AgenticSearchProps) => {
|
}: AgenticSearchProps) => {
|
||||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||||
const [selectedItems, setSelectedItems] = useLocalStorage<ContextItem[]>("selectedContextItems", [], { initializeWithValue: false });
|
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
|
||||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -42,12 +41,12 @@ export const AgenticSearch = ({
|
||||||
<div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]">
|
<div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]">
|
||||||
<ChatBox
|
<ChatBox
|
||||||
onSubmit={(children) => {
|
onSubmit={(children) => {
|
||||||
createNewChatThread(children, selectedItems);
|
createNewChatThread(children, selectedSearchScopes);
|
||||||
}}
|
}}
|
||||||
className="min-h-[50px]"
|
className="min-h-[50px]"
|
||||||
isRedirecting={isLoading}
|
isRedirecting={isLoading}
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
/>
|
/>
|
||||||
|
|
@ -58,8 +57,8 @@ export const AgenticSearch = ({
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
repos={repos}
|
repos={repos}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
onSelectedItemsChange={setSelectedItems}
|
onSelectedSearchScopesChange={setSelectedSearchScopes}
|
||||||
isContextSelectorOpen={isContextSelectorOpen}
|
isContextSelectorOpen={isContextSelectorOpen}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
/>
|
/>
|
||||||
|
|
@ -74,10 +73,6 @@ export const AgenticSearch = ({
|
||||||
{demoExamples && (
|
{demoExamples && (
|
||||||
<AskSourcebotDemoCards
|
<AskSourcebotDemoCards
|
||||||
demoExamples={demoExamples}
|
demoExamples={demoExamples}
|
||||||
selectedItems={selectedItems}
|
|
||||||
setSelectedItems={setSelectedItems}
|
|
||||||
searchContexts={searchContexts}
|
|
||||||
repos={repos}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div >
|
</div >
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,25 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Search, LibraryBigIcon, Code, Layers } from "lucide-react";
|
import { Search, LibraryBigIcon, Code, Info } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
import { ContextItem, RepoContextItem, SearchContextItem } from "@/features/chat/components/chatBox/contextSelector";
|
import { DemoExamples, DemoSearchExample, DemoSearchScope } from "@/types";
|
||||||
import { DemoExamples, DemoSearchExample, DemoSearchContextExample, DemoSearchContext } from "@/types";
|
|
||||||
import { cn, getCodeHostIcon } from "@/lib/utils";
|
import { cn, getCodeHostIcon } from "@/lib/utils";
|
||||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
|
||||||
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
||||||
|
import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard";
|
||||||
|
|
||||||
interface AskSourcebotDemoCardsProps {
|
interface AskSourcebotDemoCardsProps {
|
||||||
demoExamples: DemoExamples;
|
demoExamples: DemoExamples;
|
||||||
selectedItems: ContextItem[];
|
|
||||||
setSelectedItems: (items: ContextItem[]) => void;
|
|
||||||
searchContexts: SearchContextQuery[];
|
|
||||||
repos: RepositoryQuery[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AskSourcebotDemoCards = ({
|
export const AskSourcebotDemoCards = ({
|
||||||
demoExamples,
|
demoExamples,
|
||||||
selectedItems,
|
|
||||||
setSelectedItems,
|
|
||||||
searchContexts,
|
|
||||||
repos,
|
|
||||||
}: AskSourcebotDemoCardsProps) => {
|
}: AskSourcebotDemoCardsProps) => {
|
||||||
const captureEvent = useCaptureEvent();
|
const captureEvent = useCaptureEvent();
|
||||||
|
const [selectedFilterSearchScope, setSelectedFilterSearchScope] = useState<number | null>(null);
|
||||||
|
|
||||||
const handleExampleClick = (example: DemoSearchExample) => {
|
const handleExampleClick = (example: DemoSearchExample) => {
|
||||||
captureEvent('wa_demo_search_example_card_pressed', {
|
captureEvent('wa_demo_search_example_card_pressed', {
|
||||||
|
|
@ -39,87 +32,37 @@ export const AskSourcebotDemoCards = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getContextIcon = (context: DemoSearchContext, size: number = 20) => {
|
const getSearchScopeIcon = (searchScope: DemoSearchScope, size: number = 20, isSelected: boolean = false) => {
|
||||||
const sizeClass = size === 12 ? "h-3 w-3" : "h-5 w-5";
|
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, "text-muted-foreground")} />;
|
return <LibraryBigIcon className={cn(sizeClass, colorClass)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.codeHostType) {
|
if (searchScope.codeHostType) {
|
||||||
const codeHostIcon = getCodeHostIcon(context.codeHostType);
|
const codeHostIcon = getCodeHostIcon(searchScope.codeHostType);
|
||||||
if (codeHostIcon) {
|
if (codeHostIcon) {
|
||||||
|
// When selected, icons need to match the inverted badge colors
|
||||||
|
// In light mode selected: light icon on dark bg (invert)
|
||||||
|
// In dark mode selected: dark icon on light bg (no invert, override dark:invert)
|
||||||
|
const selectedIconClass = isSelected
|
||||||
|
? "invert dark:invert-0"
|
||||||
|
: codeHostIcon.className;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
src={codeHostIcon.src}
|
src={codeHostIcon.src}
|
||||||
alt={`${context.codeHostType} icon`}
|
alt={`${searchScope.codeHostType} icon`}
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
className={cn(sizeClass, codeHostIcon.className)}
|
className={cn(sizeClass, selectedIconClass)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Code className={cn(sizeClass, "text-muted-foreground")} />;
|
return <Code className={cn(sizeClass, colorClass)} />;
|
||||||
}
|
|
||||||
|
|
||||||
const handleContextClick = (demoSearchContexts: DemoSearchContext[], contextExample: DemoSearchContextExample) => {
|
|
||||||
const context = demoSearchContexts.find((context) => context.id === contextExample.searchContext)
|
|
||||||
if (!context) {
|
|
||||||
console.error(`Search context ${contextExample.searchContext} not found on handleContextClick`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
captureEvent('wa_demo_search_context_card_pressed', {
|
|
||||||
contextType: context.type,
|
|
||||||
contextName: context.value,
|
|
||||||
contextDisplayName: context.displayName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo";
|
|
||||||
const isSelected = selectedItems.some((item) => item.value === context.value);
|
|
||||||
if (isSelected) {
|
|
||||||
setSelectedItems(selectedItems.filter((item) => item.value !== context.value));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getNewSelectedItem = (): ContextItem | null => {
|
|
||||||
if (context.type === "set") {
|
|
||||||
const searchContext = searchContexts.find((item) => item.name === context.value);
|
|
||||||
if (!searchContext) {
|
|
||||||
console.error(`Search context ${context.value} not found on handleContextClick`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'context',
|
|
||||||
value: context.value,
|
|
||||||
name: context.displayName,
|
|
||||||
repoCount: searchContext.repoNames.length
|
|
||||||
} as SearchContextItem;
|
|
||||||
} else {
|
|
||||||
const repo = repos.find((repo) => repo.repoName === context.value);
|
|
||||||
if (!repo) {
|
|
||||||
console.error(`Repo ${context.value} not found on handleContextClick`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'repo',
|
|
||||||
value: context.value,
|
|
||||||
name: context.displayName,
|
|
||||||
codeHostType: repo.codeHostType
|
|
||||||
} as RepoContextItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSelectedItem = getNewSelectedItem();
|
|
||||||
if (newSelectedItem) {
|
|
||||||
setSelectedItems(isDemoMode ? [newSelectedItem] : [...selectedItems, newSelectedItem]);
|
|
||||||
} else {
|
|
||||||
console.error(`No new selected item found on handleContextClick`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -139,110 +82,91 @@ export const AskSourcebotDemoCards = ({
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-full mt-8 space-y-12 px-4 max-w-[1000px]">
|
<div className="w-full mt-16 space-y-12 px-4 max-w-[1000px]">
|
||||||
{/* Search Context Row */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="text-center mb-8">
|
|
||||||
<div className="flex items-center justify-center gap-2 mb-2">
|
|
||||||
<Layers className="h-5 w-5 text-muted-foreground" />
|
|
||||||
<h3 className="text-lg font-semibold">Search Contexts</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">Select the context you want to ask questions about</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap justify-center gap-3">
|
|
||||||
{demoExamples.searchContextExamples.map((contextExample) => {
|
|
||||||
const context = demoExamples.searchContexts.find((context) => context.id === contextExample.searchContext)
|
|
||||||
if (!context) {
|
|
||||||
console.error(`Search context ${contextExample.searchContext} not found on handleContextClick`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = selectedItems.some(
|
|
||||||
(selected) => (selected.type === 'context' && selected.value === context.value) ||
|
|
||||||
(selected.type === 'repo' && selected.value === context.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchContext = searchContexts.find((item) => item.name === context.value);
|
|
||||||
const numRepos = searchContext ? searchContext.repoNames.length : undefined;
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={context.value}
|
|
||||||
className={`cursor-pointer transition-all duration-200 hover:shadow-md hover:scale-105 group w-full max-w-[280px] ${isSelected ? "border-primary bg-primary/5 shadow-sm" : "hover:border-primary/50"
|
|
||||||
}`}
|
|
||||||
onClick={() => handleContextClick(demoExamples.searchContexts, contextExample)}
|
|
||||||
>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div
|
|
||||||
className={`flex-shrink-0 p-2 rounded-lg transition-transform group-hover:scale-105`}
|
|
||||||
>
|
|
||||||
{getContextIcon(context)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<h4
|
|
||||||
className={`font-medium text-sm transition-colors ${isSelected ? "text-primary" : "group-hover:text-primary"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{context.displayName}
|
|
||||||
</h4>
|
|
||||||
{numRepos && (
|
|
||||||
<Badge className="text-[10px] px-1.5 py-0.5 h-4">
|
|
||||||
{numRepos} repos
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">{contextExample.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Example Searches Row */}
|
{/* Example Searches Row */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-6">
|
||||||
<div className="flex items-center justify-center gap-2 mb-2">
|
<div className="flex items-center justify-center gap-3 mb-4">
|
||||||
<Search className="h-5 w-5 text-muted-foreground" />
|
<Search className="h-7 w-7 text-muted-foreground" />
|
||||||
<h3 className="text-lg font-semibold">Community Ask Results</h3>
|
<h3 className="text-2xl font-bold">Community Ask Results</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">Check out these featured ask results from the community</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 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">
|
||||||
|
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||||
|
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10 pointer-events-none">
|
||||||
|
<SearchScopeInfoCard />
|
||||||
|
<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 Scope:</span>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
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={() => {
|
||||||
|
setSelectedFilterSearchScope(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</Badge>
|
||||||
|
{demoExamples.searchScopes.map((searchScope) => (
|
||||||
|
<Badge
|
||||||
|
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={() => {
|
||||||
|
setSelectedFilterSearchScope(searchScope.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getSearchScopeIcon(searchScope, 12, selectedFilterSearchScope === searchScope.id)}
|
||||||
|
{searchScope.displayName}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap justify-center gap-3">
|
<div className="flex flex-wrap justify-center gap-3">
|
||||||
{demoExamples.searchExamples.map((example) => {
|
{demoExamples.searchExamples
|
||||||
const searchContexts = demoExamples.searchContexts.filter((context) => example.searchContext.includes(context.id))
|
.filter((example) => {
|
||||||
return (
|
if (selectedFilterSearchScope === null) return true;
|
||||||
<Card
|
return example.searchScopes.includes(selectedFilterSearchScope);
|
||||||
key={example.url}
|
})
|
||||||
className="cursor-pointer transition-all duration-200 hover:shadow-md hover:scale-105 hover:border-primary/50 group w-full max-w-[350px]"
|
.map((example) => {
|
||||||
onClick={() => handleExampleClick(example)}
|
const searchScopes = demoExamples.searchScopes.filter((searchScope) => example.searchScopes.includes(searchScope.id))
|
||||||
>
|
return (
|
||||||
<CardContent className="p-4">
|
<Card
|
||||||
<div className="space-y-3">
|
key={example.url}
|
||||||
<div className="flex items-center justify-between">
|
className="cursor-pointer transition-all duration-200 hover:shadow-md hover:scale-105 hover:border-primary/50 group w-full max-w-[350px]"
|
||||||
{searchContexts.map((context) => (
|
onClick={() => handleExampleClick(example)}
|
||||||
<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)}
|
<CardContent className="p-4">
|
||||||
{context.displayName}
|
<div className="space-y-3">
|
||||||
</Badge>
|
<div className="flex items-center justify-between">
|
||||||
))}
|
{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>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h4 className="font-semibold text-sm group-hover:text-primary transition-colors line-clamp-2">
|
||||||
|
{example.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-muted-foreground line-clamp-3 leading-relaxed">
|
||||||
|
{example.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
</CardContent>
|
||||||
<h4 className="font-semibold text-sm group-hover:text-primary transition-colors line-clamp-2">
|
</Card>
|
||||||
{example.title}
|
)
|
||||||
</h4>
|
})}
|
||||||
<p className="text-xs text-muted-foreground line-clamp-3 leading-relaxed">
|
|
||||||
{example.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||||
import { env } from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import { _getConfiguredLanguageModelsFull, updateChatMessages, updateChatName } from "@/features/chat/actions";
|
import { _getConfiguredLanguageModelsFull, updateChatMessages, updateChatName } from "@/features/chat/actions";
|
||||||
import { createAgentStream } from "@/features/chat/agent";
|
import { createAgentStream } from "@/features/chat/agent";
|
||||||
import { additionalChatRequestParamsSchema, SBChatMessage } from "@/features/chat/types";
|
import { additionalChatRequestParamsSchema, SBChatMessage, SearchScope } from "@/features/chat/types";
|
||||||
import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils";
|
import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils";
|
||||||
import { ErrorCode } from "@/lib/errorCodes";
|
import { ErrorCode } from "@/lib/errorCodes";
|
||||||
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||||
|
|
@ -64,12 +64,11 @@ export async function POST(req: Request) {
|
||||||
return serviceErrorResponse(schemaValidationError(parsed.error));
|
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { messages, id, selectedRepos, selectedContexts, languageModelId } = parsed.data;
|
const { messages, id, selectedSearchScopes, languageModelId } = parsed.data;
|
||||||
const response = await chatHandler({
|
const response = await chatHandler({
|
||||||
messages,
|
messages,
|
||||||
id,
|
id,
|
||||||
selectedRepos,
|
selectedSearchScopes,
|
||||||
selectedContexts,
|
|
||||||
languageModelId,
|
languageModelId,
|
||||||
}, domain);
|
}, domain);
|
||||||
|
|
||||||
|
|
@ -93,12 +92,11 @@ const mergeStreamAsync = async (stream: StreamTextResult<any, any>, writer: UIMe
|
||||||
interface ChatHandlerProps {
|
interface ChatHandlerProps {
|
||||||
messages: SBChatMessage[];
|
messages: SBChatMessage[];
|
||||||
id: string;
|
id: string;
|
||||||
selectedRepos: string[];
|
selectedSearchScopes: SearchScope[];
|
||||||
selectedContexts?: string[];
|
|
||||||
languageModelId: string;
|
languageModelId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageModelId }: ChatHandlerProps, domain: string) => sew(async () =>
|
const chatHandler = ({ messages, id, selectedSearchScopes, languageModelId }: ChatHandlerProps, domain: string) => sew(async () =>
|
||||||
withAuth((userId) =>
|
withAuth((userId) =>
|
||||||
withOrgMembership(userId, domain, async ({ org }) => {
|
withOrgMembership(userId, domain, async ({ org }) => {
|
||||||
const chat = await prisma.chat.findUnique({
|
const chat = await prisma.chat.findUnique({
|
||||||
|
|
@ -188,26 +186,30 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo
|
||||||
|
|
||||||
const startTime = new Date();
|
const startTime = new Date();
|
||||||
|
|
||||||
// Expand search contexts to repos
|
const expandedReposArrays = await Promise.all(selectedSearchScopes.map(async (scope) => {
|
||||||
let expandedRepos = [...selectedRepos];
|
if (scope.type === 'repo') {
|
||||||
if (selectedContexts && selectedContexts.length > 0) {
|
return [scope.value];
|
||||||
const searchContexts = await prisma.searchContext.findMany({
|
}
|
||||||
where: {
|
|
||||||
orgId: org.id,
|
if (scope.type === 'reposet') {
|
||||||
name: { in: selectedContexts }
|
const reposet = await prisma.searchContext.findFirst({
|
||||||
},
|
where: {
|
||||||
include: {
|
orgId: org.id,
|
||||||
repos: true
|
name: scope.value
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
repos: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (reposet) {
|
||||||
|
return reposet.repos.map(repo => repo.name);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
const contextRepos = searchContexts.flatMap(context =>
|
return [];
|
||||||
context.repos.map(repo => repo.name)
|
}));
|
||||||
);
|
const expandedRepos = expandedReposArrays.flat();
|
||||||
|
|
||||||
// Combine and deduplicate repos
|
|
||||||
expandedRepos = Array.from(new Set([...selectedRepos, ...contextRepos]));
|
|
||||||
}
|
|
||||||
|
|
||||||
const researchStream = await createAgentStream({
|
const researchStream = await createAgentStream({
|
||||||
model,
|
model,
|
||||||
|
|
@ -215,7 +217,7 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo
|
||||||
headers,
|
headers,
|
||||||
inputMessages: messageHistory,
|
inputMessages: messageHistory,
|
||||||
inputSources: sources,
|
inputSources: sources,
|
||||||
selectedRepos: expandedRepos,
|
searchScopeRepoNames: expandedRepos,
|
||||||
onWriteSource: (source) => {
|
onWriteSource: (source) => {
|
||||||
writer.write({
|
writer.write({
|
||||||
type: 'data-source',
|
type: 'data-source',
|
||||||
|
|
@ -241,6 +243,7 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo
|
||||||
totalOutputTokens: totalUsage.outputTokens,
|
totalOutputTokens: totalUsage.outputTokens,
|
||||||
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
|
totalResponseTimeMs: new Date().getTime() - startTime.getTime(),
|
||||||
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
|
modelName: languageModelConfig.displayName ?? languageModelConfig.model,
|
||||||
|
selectedSearchScopes,
|
||||||
traceId,
|
traceId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
41
packages/web/src/components/searchScopeInfoCard.tsx
Normal file
41
packages/web/src/components/searchScopeInfoCard.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import Image from "next/image";
|
||||||
|
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">
|
||||||
|
<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 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">
|
||||||
|
{(() => {
|
||||||
|
const githubIcon = getCodeHostIcon("github");
|
||||||
|
return githubIcon ? (
|
||||||
|
<Image
|
||||||
|
src={githubIcon.src}
|
||||||
|
alt="GitHub icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className={cn("h-4 w-4 flex-shrink-0", githubIcon.className)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Code className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
<span><strong>Repository</strong>: A single repository, indicated by the code host icon.</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LibraryBigIcon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
|
<span><strong>Reposet</strong>: A set of repositories, indicated by the library icon.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -16,7 +16,7 @@ interface AgentOptions {
|
||||||
model: LanguageModel;
|
model: LanguageModel;
|
||||||
providerOptions?: ProviderOptions;
|
providerOptions?: ProviderOptions;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
selectedRepos: string[];
|
searchScopeRepoNames: string[];
|
||||||
inputMessages: ModelMessage[];
|
inputMessages: ModelMessage[];
|
||||||
inputSources: Source[];
|
inputSources: Source[];
|
||||||
onWriteSource: (source: Source) => void;
|
onWriteSource: (source: Source) => void;
|
||||||
|
|
@ -35,12 +35,12 @@ export const createAgentStream = async ({
|
||||||
headers,
|
headers,
|
||||||
inputMessages,
|
inputMessages,
|
||||||
inputSources,
|
inputSources,
|
||||||
selectedRepos,
|
searchScopeRepoNames,
|
||||||
onWriteSource,
|
onWriteSource,
|
||||||
traceId,
|
traceId,
|
||||||
}: AgentOptions) => {
|
}: AgentOptions) => {
|
||||||
const baseSystemPrompt = createBaseSystemPrompt({
|
const baseSystemPrompt = createBaseSystemPrompt({
|
||||||
selectedRepos,
|
searchScopeRepoNames,
|
||||||
});
|
});
|
||||||
|
|
||||||
const stream = streamText({
|
const stream = streamText({
|
||||||
|
|
@ -50,7 +50,7 @@ export const createAgentStream = async ({
|
||||||
system: baseSystemPrompt,
|
system: baseSystemPrompt,
|
||||||
messages: inputMessages,
|
messages: inputMessages,
|
||||||
tools: {
|
tools: {
|
||||||
[toolNames.searchCode]: createCodeSearchTool(selectedRepos),
|
[toolNames.searchCode]: createCodeSearchTool(searchScopeRepoNames),
|
||||||
[toolNames.readFiles]: readFilesTool,
|
[toolNames.readFiles]: readFilesTool,
|
||||||
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
|
[toolNames.findSymbolReferences]: findSymbolReferencesTool,
|
||||||
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
|
[toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool,
|
||||||
|
|
@ -150,11 +150,11 @@ export const createAgentStream = async ({
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseSystemPromptOptions {
|
interface BaseSystemPromptOptions {
|
||||||
selectedRepos: string[];
|
searchScopeRepoNames: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createBaseSystemPrompt = ({
|
export const createBaseSystemPrompt = ({
|
||||||
selectedRepos,
|
searchScopeRepoNames,
|
||||||
}: BaseSystemPromptOptions) => {
|
}: BaseSystemPromptOptions) => {
|
||||||
return `
|
return `
|
||||||
You are a powerful agentic AI code assistant built into Sourcebot, the world's best code-intelligence platform. Your job is to help developers understand and navigate their large codebases.
|
You are a powerful agentic AI code assistant built into Sourcebot, the world's best code-intelligence platform. Your job is to help developers understand and navigate their large codebases.
|
||||||
|
|
@ -176,7 +176,7 @@ Your workflow has two distinct phases:
|
||||||
|
|
||||||
<available_repositories>
|
<available_repositories>
|
||||||
The user has selected the following repositories for analysis:
|
The user has selected the following repositories for analysis:
|
||||||
${selectedRepos.map(repo => `- ${repo}`).join('\n')}
|
${searchScopeRepoNames.map(repo => `- ${repo}`).join('\n')}
|
||||||
</available_repositories>
|
</available_repositories>
|
||||||
|
|
||||||
<research_phase_instructions>
|
<research_phase_instructions>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor } from "@/features/chat/types";
|
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
|
||||||
import { insertMention, slateContentToString } from "@/features/chat/utils";
|
import { insertMention, slateContentToString } from "@/features/chat/utils";
|
||||||
import { cn, IS_MAC } from "@/lib/utils";
|
import { cn, IS_MAC } from "@/lib/utils";
|
||||||
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
|
import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react";
|
||||||
|
|
@ -18,7 +18,6 @@ import { Suggestion } from "./types";
|
||||||
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
|
import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery";
|
||||||
import { useSuggestionsData } from "./useSuggestionsData";
|
import { useSuggestionsData } from "./useSuggestionsData";
|
||||||
import { useToast } from "@/components/hooks/use-toast";
|
import { useToast } from "@/components/hooks/use-toast";
|
||||||
import { ContextItem } from "./contextSelector";
|
|
||||||
import { SearchContextQuery } from "@/lib/types";
|
import { SearchContextQuery } from "@/lib/types";
|
||||||
|
|
||||||
interface ChatBoxProps {
|
interface ChatBoxProps {
|
||||||
|
|
@ -29,7 +28,7 @@ interface ChatBoxProps {
|
||||||
isRedirecting?: boolean;
|
isRedirecting?: boolean;
|
||||||
isGenerating?: boolean;
|
isGenerating?: boolean;
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
selectedItems: ContextItem[];
|
selectedSearchScopes: SearchScope[];
|
||||||
searchContexts: SearchContextQuery[];
|
searchContexts: SearchContextQuery[];
|
||||||
onContextSelectorOpenChanged: (isOpen: boolean) => void;
|
onContextSelectorOpenChanged: (isOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +41,7 @@ export const ChatBox = ({
|
||||||
isRedirecting,
|
isRedirecting,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
languageModels,
|
languageModels,
|
||||||
selectedItems,
|
selectedSearchScopes,
|
||||||
searchContexts,
|
searchContexts,
|
||||||
onContextSelectorOpenChanged,
|
onContextSelectorOpenChanged,
|
||||||
}: ChatBoxProps) => {
|
}: ChatBoxProps) => {
|
||||||
|
|
@ -53,15 +52,15 @@ export const ChatBox = ({
|
||||||
const { suggestions, isLoading } = useSuggestionsData({
|
const { suggestions, isLoading } = useSuggestionsData({
|
||||||
suggestionMode,
|
suggestionMode,
|
||||||
suggestionQuery,
|
suggestionQuery,
|
||||||
selectedRepos: selectedItems.map((item) => {
|
selectedRepos: selectedSearchScopes.map((item) => {
|
||||||
if (item.type === 'repo') {
|
if (item.type === 'repo') {
|
||||||
return [item.value];
|
return [item.value];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'context') {
|
if (item.type === 'reposet') {
|
||||||
const context = searchContexts.find((context) => context.name === item.value);
|
const reposet = searchContexts.find((reposet) => reposet.name === item.value);
|
||||||
if (context) {
|
if (reposet) {
|
||||||
return context.repoNames;
|
return reposet.repoNames;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,7 +129,7 @@ export const ChatBox = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedItems.length === 0) {
|
if (selectedSearchScopes.length === 0) {
|
||||||
return {
|
return {
|
||||||
isSubmitDisabled: true,
|
isSubmitDisabled: true,
|
||||||
isSubmitDisabledReason: "no-repos-selected",
|
isSubmitDisabledReason: "no-repos-selected",
|
||||||
|
|
@ -154,7 +153,7 @@ export const ChatBox = ({
|
||||||
editor.children,
|
editor.children,
|
||||||
isRedirecting,
|
isRedirecting,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
selectedItems.length,
|
selectedSearchScopes.length,
|
||||||
selectedLanguageModel,
|
selectedLanguageModel,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
@ -162,7 +161,7 @@ export const ChatBox = ({
|
||||||
if (isSubmitDisabled) {
|
if (isSubmitDisabled) {
|
||||||
if (isSubmitDisabledReason === "no-repos-selected") {
|
if (isSubmitDisabledReason === "no-repos-selected") {
|
||||||
toast({
|
toast({
|
||||||
description: "⚠️ You must select at least one search context",
|
description: "⚠️ You must select at least one search scope",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
onContextSelectorOpenChanged(true);
|
onContextSelectorOpenChanged(true);
|
||||||
|
|
@ -284,7 +283,7 @@ export const ChatBox = ({
|
||||||
>
|
>
|
||||||
<Editable
|
<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"
|
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}
|
renderElement={renderElement}
|
||||||
renderLeaf={renderLeaf}
|
renderLeaf={renderLeaf}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
|
@ -339,7 +338,7 @@ export const ChatBox = ({
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<TriangleAlertIcon className="h-4 w-4 text-warning mr-1" />
|
<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>
|
</div>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { LanguageModelInfo } from "@/features/chat/types";
|
import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
|
||||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||||
import { AtSignIcon } from "lucide-react";
|
import { AtSignIcon } from "lucide-react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
|
||||||
import { ReactEditor, useSlate } from "slate-react";
|
import { ReactEditor, useSlate } from "slate-react";
|
||||||
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
|
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
|
||||||
import { LanguageModelSelector } from "./languageModelSelector";
|
import { LanguageModelSelector } from "./languageModelSelector";
|
||||||
import { ContextSelector, type ContextItem } from "./contextSelector";
|
import { SearchScopeSelector } from "./searchScopeSelector";
|
||||||
|
import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard";
|
||||||
|
import { AtMentionInfoCard } from "@/components/atMentionInfoCard";
|
||||||
|
|
||||||
export interface ChatBoxToolbarProps {
|
export interface ChatBoxToolbarProps {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
repos: RepositoryQuery[];
|
repos: RepositoryQuery[];
|
||||||
searchContexts: SearchContextQuery[];
|
searchContexts: SearchContextQuery[];
|
||||||
selectedItems: ContextItem[];
|
selectedSearchScopes: SearchScope[];
|
||||||
onSelectedItemsChange: (items: ContextItem[]) => void;
|
onSelectedSearchScopesChange: (items: SearchScope[]) => void;
|
||||||
isContextSelectorOpen: boolean;
|
isContextSelectorOpen: boolean;
|
||||||
onContextSelectorOpenChanged: (isOpen: boolean) => void;
|
onContextSelectorOpenChanged: (isOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -28,8 +28,8 @@ export const ChatBoxToolbar = ({
|
||||||
languageModels,
|
languageModels,
|
||||||
repos,
|
repos,
|
||||||
searchContexts,
|
searchContexts,
|
||||||
selectedItems,
|
selectedSearchScopes,
|
||||||
onSelectedItemsChange,
|
onSelectedSearchScopesChange,
|
||||||
isContextSelectorOpen,
|
isContextSelectorOpen,
|
||||||
onContextSelectorOpenChanged,
|
onContextSelectorOpenChanged,
|
||||||
}: ChatBoxToolbarProps) => {
|
}: ChatBoxToolbarProps) => {
|
||||||
|
|
@ -40,15 +40,6 @@ export const ChatBoxToolbar = ({
|
||||||
ReactEditor.focus(editor);
|
ReactEditor.focus(editor);
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
useHotkeys("alt+mod+p", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onAddContext();
|
|
||||||
}, {
|
|
||||||
enableOnFormTags: true,
|
|
||||||
enableOnContentEditable: true,
|
|
||||||
description: "Add context",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
|
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
|
||||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||||
});
|
});
|
||||||
|
|
@ -66,30 +57,25 @@ export const ChatBoxToolbar = ({
|
||||||
<AtSignIcon className="w-4 h-4" />
|
<AtSignIcon className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
||||||
side="bottom"
|
<AtMentionInfoCard />
|
||||||
className="flex flex-row items-center gap-2"
|
|
||||||
>
|
|
||||||
<KeyboardShortcutHint shortcut="⌥ ⌘ P" />
|
|
||||||
<Separator orientation="vertical" className="h-4" />
|
|
||||||
<span>Add context</span>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Separator orientation="vertical" className="h-3 mx-1" />
|
<Separator orientation="vertical" className="h-3 mx-1" />
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<ContextSelector
|
<SearchScopeSelector
|
||||||
className="bg-inherit w-fit h-6 min-h-6"
|
className="bg-inherit w-fit h-6 min-h-6"
|
||||||
repos={repos}
|
repos={repos}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
onSelectedItemsChange={onSelectedItemsChange}
|
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||||
isOpen={isContextSelectorOpen}
|
isOpen={isContextSelectorOpen}
|
||||||
onOpenChanged={onContextSelectorOpenChanged}
|
onOpenChanged={onContextSelectorOpenChanged}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
||||||
<span>Search contexts and repositories to scope conversation to.</span>
|
<SearchScopeInfoCard />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{languageModels.length > 0 && (
|
{languageModels.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,10 @@ import * as React from "react";
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
FolderIcon,
|
ScanSearchIcon,
|
||||||
LayersIcon,
|
|
||||||
LibraryBigIcon,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
import { cn, getCodeHostIcon } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
@ -28,44 +25,30 @@ import {
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import { RepoSetSearchScope, RepoSearchScope, SearchScope } from "../../types";
|
||||||
|
import { SearchScopeIcon } from "../searchScopeIcon";
|
||||||
|
|
||||||
export type RepoContextItem = {
|
interface SearchScopeSelectorProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
type: 'repo';
|
|
||||||
value: string;
|
|
||||||
name: string;
|
|
||||||
codeHostType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SearchContextItem = {
|
|
||||||
type: 'context';
|
|
||||||
value: string;
|
|
||||||
name: string;
|
|
||||||
repoCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ContextItem = RepoContextItem | SearchContextItem;
|
|
||||||
|
|
||||||
interface ContextSelectorProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
||||||
repos: RepositoryQuery[];
|
repos: RepositoryQuery[];
|
||||||
searchContexts: SearchContextQuery[];
|
searchContexts: SearchContextQuery[];
|
||||||
selectedItems: ContextItem[];
|
selectedSearchScopes: SearchScope[];
|
||||||
onSelectedItemsChange: (items: ContextItem[]) => void;
|
onSelectedSearchScopesChange: (items: SearchScope[]) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChanged: (isOpen: boolean) => void;
|
onOpenChanged: (isOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContextSelector = React.forwardRef<
|
export const SearchScopeSelector = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
ContextSelectorProps
|
SearchScopeSelectorProps
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
repos,
|
repos,
|
||||||
searchContexts,
|
searchContexts,
|
||||||
onSelectedItemsChange,
|
|
||||||
className,
|
className,
|
||||||
selectedItems,
|
selectedSearchScopes,
|
||||||
|
onSelectedSearchScopesChange,
|
||||||
isOpen,
|
isOpen,
|
||||||
onOpenChanged,
|
onOpenChanged,
|
||||||
...props
|
...props
|
||||||
|
|
@ -81,72 +64,62 @@ export const ContextSelector = React.forwardRef<
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
onOpenChanged(true);
|
onOpenChanged(true);
|
||||||
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
||||||
const newSelectedItems = [...selectedItems];
|
const newSelectedItems = [...selectedSearchScopes];
|
||||||
newSelectedItems.pop();
|
newSelectedItems.pop();
|
||||||
onSelectedItemsChange(newSelectedItems);
|
onSelectedSearchScopesChange(newSelectedItems);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleItem = (item: ContextItem) => {
|
const toggleItem = (item: SearchScope) => {
|
||||||
// Store current scroll position before state update
|
// Store current scroll position before state update
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
scrollPosition.current = scrollContainerRef.current.scrollTop;
|
scrollPosition.current = scrollContainerRef.current.scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSelected = selectedItems.some(
|
const isSelected = selectedSearchScopes.some(
|
||||||
(selected) => selected.type === item.type && selected.value === item.value
|
(selected) => selected.type === item.type && selected.value === item.value
|
||||||
);
|
);
|
||||||
|
|
||||||
const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo";
|
const newSelectedItems = isSelected ?
|
||||||
|
selectedSearchScopes.filter(
|
||||||
let newSelectedItems: ContextItem[];
|
|
||||||
if (isSelected) {
|
|
||||||
newSelectedItems = selectedItems.filter(
|
|
||||||
(selected) => !(selected.type === item.type && selected.value === item.value)
|
(selected) => !(selected.type === item.type && selected.value === item.value)
|
||||||
);
|
) :
|
||||||
} else {
|
[...selectedSearchScopes, item];
|
||||||
// Limit selected context to 1 in demo mode
|
|
||||||
if (isDemoMode) {
|
|
||||||
newSelectedItems = [item];
|
|
||||||
} else {
|
|
||||||
newSelectedItems = [...selectedItems, item];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectedItemsChange(newSelectedItems);
|
onSelectedSearchScopesChange(newSelectedItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
onSelectedItemsChange([]);
|
onSelectedSearchScopesChange([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTogglePopover = () => {
|
const handleTogglePopover = () => {
|
||||||
onOpenChanged(!isOpen);
|
onOpenChanged(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
const allItems = React.useMemo(() => {
|
const allSearchScopeItems = React.useMemo(() => {
|
||||||
const contextItems: ContextItem[] = searchContexts.map(context => ({
|
const repoSetSearchScopeItems: RepoSetSearchScope[] = searchContexts.map(context => ({
|
||||||
type: 'context' as const,
|
type: 'reposet' as const,
|
||||||
value: context.name,
|
value: context.name,
|
||||||
name: context.name,
|
name: context.name,
|
||||||
repoCount: context.repoNames.length
|
repoCount: context.repoNames.length
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const repoItems: ContextItem[] = repos.map(repo => ({
|
const repoSearchScopeItems: RepoSearchScope[] = repos.map(repo => ({
|
||||||
type: 'repo' as const,
|
type: 'repo' as const,
|
||||||
value: repo.repoName,
|
value: repo.repoName,
|
||||||
name: repo.repoDisplayName || repo.repoName.split('/').pop() || repo.repoName,
|
name: repo.repoDisplayName || repo.repoName.split('/').pop() || repo.repoName,
|
||||||
codeHostType: repo.codeHostType,
|
codeHostType: repo.codeHostType,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return [...contextItems, ...repoItems];
|
return [...repoSetSearchScopeItems, ...repoSearchScopeItems];
|
||||||
}, [repos, searchContexts]);
|
}, [repos, searchContexts]);
|
||||||
|
|
||||||
const sortedItems = React.useMemo(() => {
|
const sortedSearchScopeItems = React.useMemo(() => {
|
||||||
return allItems
|
return allSearchScopeItems
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
item,
|
item,
|
||||||
isSelected: selectedItems.some(
|
isSelected: selectedSearchScopes.some(
|
||||||
(selected) => selected.type === item.type && selected.value === item.value
|
(selected) => selected.type === item.type && selected.value === item.value
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
|
|
@ -154,19 +127,19 @@ export const ContextSelector = React.forwardRef<
|
||||||
// Selected items first
|
// Selected items first
|
||||||
if (a.isSelected && !b.isSelected) return -1;
|
if (a.isSelected && !b.isSelected) return -1;
|
||||||
if (!a.isSelected && b.isSelected) return 1;
|
if (!a.isSelected && b.isSelected) return 1;
|
||||||
// Then contexts before repos
|
// Then reposets before repos
|
||||||
if (a.item.type === 'context' && b.item.type === 'repo') return -1;
|
if (a.item.type === 'reposet' && b.item.type === 'repo') return -1;
|
||||||
if (a.item.type === 'repo' && b.item.type === 'context') return 1;
|
if (a.item.type === 'repo' && b.item.type === 'reposet') return 1;
|
||||||
return 0;
|
return 0;
|
||||||
})
|
})
|
||||||
}, [allItems, selectedItems]);
|
}, [allSearchScopeItems, selectedSearchScopes]);
|
||||||
|
|
||||||
// Restore scroll position after re-render
|
// Restore scroll position after re-render
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (scrollContainerRef.current && scrollPosition.current > 0) {
|
if (scrollContainerRef.current && scrollPosition.current > 0) {
|
||||||
scrollContainerRef.current.scrollTop = scrollPosition.current;
|
scrollContainerRef.current.scrollTop = scrollPosition.current;
|
||||||
}
|
}
|
||||||
}, [sortedItems]);
|
}, [sortedSearchScopeItems]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
|
@ -184,14 +157,14 @@ export const ContextSelector = React.forwardRef<
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between w-full mx-auto">
|
<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
|
<span
|
||||||
className={cn("text-sm text-muted-foreground mx-1 font-medium")}
|
className={cn("text-sm text-muted-foreground mx-1 font-medium")}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
selectedItems.length === 0 ? `Select context` :
|
selectedSearchScopes.length === 0 ? `Search scopes` :
|
||||||
selectedItems.length === 1 ? selectedItems[0].name :
|
selectedSearchScopes.length === 1 ? selectedSearchScopes[0].name :
|
||||||
`${selectedItems.length} selected`
|
`${selectedSearchScopes.length} selected`
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground ml-2" />
|
<ChevronDown className="h-4 cursor-pointer text-muted-foreground ml-2" />
|
||||||
|
|
@ -205,13 +178,13 @@ export const ContextSelector = React.forwardRef<
|
||||||
>
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search contexts..."
|
placeholder="Search scopes..."
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
/>
|
/>
|
||||||
<CommandList ref={scrollContainerRef}>
|
<CommandList ref={scrollContainerRef}>
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
<CommandEmpty>No results found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{sortedItems.map(({ item, isSelected }) => {
|
{sortedSearchScopeItems.map(({ item, isSelected }) => {
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={`${item.type}-${item.value}`}
|
key={`${item.type}-${item.value}`}
|
||||||
|
|
@ -229,31 +202,13 @@ export const ContextSelector = React.forwardRef<
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CheckIcon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-1">
|
<div className="flex items-center gap-2 flex-1">
|
||||||
{item.type === 'context' ? (
|
<SearchScopeIcon searchScope={item} />
|
||||||
<LibraryBigIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
) : (
|
|
||||||
// Render code host icon for repos
|
|
||||||
(() => {
|
|
||||||
const codeHostIcon = item.codeHostType ? getCodeHostIcon(item.codeHostType) : null;
|
|
||||||
return codeHostIcon ? (
|
|
||||||
<Image
|
|
||||||
src={codeHostIcon.src}
|
|
||||||
alt={`${item.codeHostType} icon`}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className={cn("h-4 w-4", codeHostIcon.className)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FolderIcon className="h-4 w-4 text-muted-foreground" />
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col flex-1">
|
<div className="flex flex-col flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
{item.type === 'context' && (
|
{item.type === 'reposet' && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="default"
|
variant="default"
|
||||||
className="text-[10px] px-1.5 py-0 h-4 bg-primary text-primary-foreground"
|
className="text-[10px] px-1.5 py-0 h-4 bg-primary text-primary-foreground"
|
||||||
|
|
@ -269,7 +224,7 @@ export const ContextSelector = React.forwardRef<
|
||||||
})}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
{selectedItems.length > 0 && (
|
{selectedSearchScopes.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandItem
|
<CommandItem
|
||||||
|
|
@ -287,4 +242,4 @@ export const ContextSelector = React.forwardRef<
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
ContextSelector.displayName = "ContextSelector";
|
SearchScopeSelector.displayName = "SearchScopeSelector";
|
||||||
|
|
@ -5,14 +5,14 @@ import { Button } from '@/components/ui/button';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { CustomSlateEditor } from '@/features/chat/customSlateEditor';
|
import { CustomSlateEditor } from '@/features/chat/customSlateEditor';
|
||||||
import { AdditionalChatRequestParams, CustomEditor, LanguageModelInfo, SBChatMessage, Source } from '@/features/chat/types';
|
import { AdditionalChatRequestParams, CustomEditor, LanguageModelInfo, SBChatMessage, SearchScope, Source } from '@/features/chat/types';
|
||||||
import { createUIMessage, getAllMentionElements, resetEditor, slateContentToString } from '@/features/chat/utils';
|
import { createUIMessage, getAllMentionElements, resetEditor, slateContentToString } from '@/features/chat/utils';
|
||||||
import { useDomain } from '@/hooks/useDomain';
|
import { useDomain } from '@/hooks/useDomain';
|
||||||
import { useChat } from '@ai-sdk/react';
|
import { useChat } from '@ai-sdk/react';
|
||||||
import { CreateUIMessage, DefaultChatTransport } from 'ai';
|
import { CreateUIMessage, DefaultChatTransport } from 'ai';
|
||||||
import { ArrowDownIcon } from 'lucide-react';
|
import { ArrowDownIcon } from 'lucide-react';
|
||||||
import { useNavigationGuard } from 'next-navigation-guard';
|
import { useNavigationGuard } from 'next-navigation-guard';
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Descendant } from 'slate';
|
import { Descendant } from 'slate';
|
||||||
import { useMessagePairs } from '../../useMessagePairs';
|
import { useMessagePairs } from '../../useMessagePairs';
|
||||||
import { useSelectedLanguageModel } from '../../useSelectedLanguageModel';
|
import { useSelectedLanguageModel } from '../../useSelectedLanguageModel';
|
||||||
|
|
@ -23,7 +23,6 @@ import { ErrorBanner } from './errorBanner';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { usePrevious } from '@uidotdev/usehooks';
|
import { usePrevious } from '@uidotdev/usehooks';
|
||||||
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
||||||
import { ContextItem } from '../chatBox/contextSelector';
|
|
||||||
|
|
||||||
type ChatHistoryState = {
|
type ChatHistoryState = {
|
||||||
scrollOffset?: number;
|
scrollOffset?: number;
|
||||||
|
|
@ -36,8 +35,8 @@ interface ChatThreadProps {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
repos: RepositoryQuery[];
|
repos: RepositoryQuery[];
|
||||||
searchContexts: SearchContextQuery[];
|
searchContexts: SearchContextQuery[];
|
||||||
selectedItems: ContextItem[];
|
selectedSearchScopes: SearchScope[];
|
||||||
onSelectedItemsChange: (items: ContextItem[]) => void;
|
onSelectedSearchScopesChange: (items: SearchScope[]) => void;
|
||||||
isChatReadonly: boolean;
|
isChatReadonly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,8 +47,8 @@ export const ChatThread = ({
|
||||||
languageModels,
|
languageModels,
|
||||||
repos,
|
repos,
|
||||||
searchContexts,
|
searchContexts,
|
||||||
selectedItems,
|
selectedSearchScopes,
|
||||||
onSelectedItemsChange,
|
onSelectedSearchScopesChange,
|
||||||
isChatReadonly,
|
isChatReadonly,
|
||||||
}: ChatThreadProps) => {
|
}: ChatThreadProps) => {
|
||||||
const domain = useDomain();
|
const domain = useDomain();
|
||||||
|
|
@ -62,12 +61,6 @@ export const ChatThread = ({
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||||
|
|
||||||
const { selectedRepos, selectedContexts } = 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 };
|
|
||||||
}, [selectedItems]);
|
|
||||||
|
|
||||||
// Initial state is from attachments that exist in in the chat history.
|
// Initial state is from attachments that exist in in the chat history.
|
||||||
const [sources, setSources] = useState<Source[]>(
|
const [sources, setSources] = useState<Source[]>(
|
||||||
initialMessages?.flatMap((message) =>
|
initialMessages?.flatMap((message) =>
|
||||||
|
|
@ -122,12 +115,11 @@ export const ChatThread = ({
|
||||||
|
|
||||||
_sendMessage(message, {
|
_sendMessage(message, {
|
||||||
body: {
|
body: {
|
||||||
selectedRepos,
|
selectedSearchScopes,
|
||||||
selectedContexts,
|
|
||||||
languageModelId: selectedLanguageModel.model,
|
languageModelId: selectedLanguageModel.model,
|
||||||
} satisfies AdditionalChatRequestParams,
|
} satisfies AdditionalChatRequestParams,
|
||||||
});
|
});
|
||||||
}, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedContexts]);
|
}, [_sendMessage, selectedLanguageModel, toast, selectedSearchScopes]);
|
||||||
|
|
||||||
|
|
||||||
const messagePairs = useMessagePairs(messages);
|
const messagePairs = useMessagePairs(messages);
|
||||||
|
|
@ -243,13 +235,13 @@ export const ChatThread = ({
|
||||||
const text = slateContentToString(children);
|
const text = slateContentToString(children);
|
||||||
const mentions = getAllMentionElements(children);
|
const mentions = getAllMentionElements(children);
|
||||||
|
|
||||||
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos, selectedContexts);
|
const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes);
|
||||||
sendMessage(message);
|
sendMessage(message);
|
||||||
|
|
||||||
setIsAutoScrollEnabled(true);
|
setIsAutoScrollEnabled(true);
|
||||||
|
|
||||||
resetEditor(editor);
|
resetEditor(editor);
|
||||||
}, [sendMessage, selectedRepos, selectedContexts]);
|
}, [sendMessage, selectedSearchScopes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -327,7 +319,7 @@ export const ChatThread = ({
|
||||||
isGenerating={status === "streaming" || status === "submitted"}
|
isGenerating={status === "streaming" || status === "submitted"}
|
||||||
onStop={stop}
|
onStop={stop}
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
/>
|
/>
|
||||||
|
|
@ -336,8 +328,8 @@ export const ChatThread = ({
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
repos={repos}
|
repos={repos}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
selectedItems={selectedItems}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
onSelectedItemsChange={onSelectedItemsChange}
|
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||||
isContextSelectorOpen={isContextSelectorOpen}
|
isContextSelectorOpen={isContextSelectorOpen}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,16 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, Zap } from 'lucide-react';
|
import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, ScanSearchIcon, Zap } from 'lucide-react';
|
||||||
import { MarkdownRenderer } from './markdownRenderer';
|
import { MarkdownRenderer } from './markdownRenderer';
|
||||||
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
|
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
|
||||||
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
|
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
|
||||||
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
|
import { ReadFilesToolComponent } from './tools/readFilesToolComponent';
|
||||||
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
|
import { SearchCodeToolComponent } from './tools/searchCodeToolComponent';
|
||||||
import { SBChatMessageMetadata, SBChatMessagePart } from '../../types';
|
import { SBChatMessageMetadata, SBChatMessagePart } from '../../types';
|
||||||
|
import { SearchScopeIcon } from '../searchScopeIcon';
|
||||||
|
|
||||||
|
|
||||||
interface DetailsCardProps {
|
interface DetailsCardProps {
|
||||||
|
|
@ -61,6 +63,28 @@ export const DetailsCard = ({
|
||||||
{!isStreaming && (
|
{!isStreaming && (
|
||||||
<>
|
<>
|
||||||
<Separator orientation="vertical" className="h-4" />
|
<Separator orientation="vertical" className="h-4" />
|
||||||
|
{metadata?.selectedSearchScopes && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center text-xs cursor-help">
|
||||||
|
<ScanSearchIcon className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||||
|
{metadata.selectedSearchScopes.length} search scope{metadata.selectedSearchScopes.length === 1 ? '' : 's'}
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
<div className="max-w-xs">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{metadata.selectedSearchScopes.map((item) => (
|
||||||
|
<div key={item.value} className="flex items-center gap-2 text-xs">
|
||||||
|
<SearchScopeIcon searchScope={item} className="h-3 w-3" />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{metadata?.modelName && (
|
{metadata?.modelName && (
|
||||||
<div className="flex items-center text-xs">
|
<div className="flex items-center text-xs">
|
||||||
<Cpu className="w-3 h-3 mr-1 flex-shrink-0" />
|
<Cpu className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { cn, getCodeHostIcon } from "@/lib/utils";
|
||||||
|
import { FolderIcon, LibraryBigIcon } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { SearchScope } from "../types";
|
||||||
|
|
||||||
|
interface SearchScopeIconProps {
|
||||||
|
searchScope: SearchScope;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchScopeIcon = ({ searchScope, className = "h-4 w-4" }: SearchScopeIconProps) => {
|
||||||
|
if (searchScope.type === 'reposet') {
|
||||||
|
return <LibraryBigIcon className={cn(className, "text-muted-foreground flex-shrink-0")} />;
|
||||||
|
} else {
|
||||||
|
// Render code host icon for repos
|
||||||
|
const codeHostIcon = searchScope.codeHostType ? getCodeHostIcon(searchScope.codeHostType) : null;
|
||||||
|
if (codeHostIcon) {
|
||||||
|
const size = className.includes('h-3') ? 12 : 16;
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={codeHostIcon.src}
|
||||||
|
alt={`${searchScope.codeHostType} icon`}
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
className={cn(className, "flex-shrink-0", codeHostIcon.className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <FolderIcon className={cn(className, "text-muted-foreground flex-shrink-0")} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -39,6 +39,28 @@ export const referenceSchema = z.discriminatedUnion('type', [
|
||||||
]);
|
]);
|
||||||
export type Reference = z.infer<typeof referenceSchema>;
|
export type Reference = z.infer<typeof referenceSchema>;
|
||||||
|
|
||||||
|
export const repoSearchScopeSchema = z.object({
|
||||||
|
type: z.literal('repo'),
|
||||||
|
value: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
codeHostType: z.string(),
|
||||||
|
});
|
||||||
|
export type RepoSearchScope = z.infer<typeof repoSearchScopeSchema>;
|
||||||
|
|
||||||
|
export const repoSetSearchScopeSchema = z.object({
|
||||||
|
type: z.literal('reposet'),
|
||||||
|
value: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
repoCount: z.number(),
|
||||||
|
});
|
||||||
|
export type RepoSetSearchScope = z.infer<typeof repoSetSearchScopeSchema>;
|
||||||
|
|
||||||
|
export const searchScopeSchema = z.discriminatedUnion('type', [
|
||||||
|
repoSearchScopeSchema,
|
||||||
|
repoSetSearchScopeSchema,
|
||||||
|
]);
|
||||||
|
export type SearchScope = z.infer<typeof searchScopeSchema>;
|
||||||
|
|
||||||
export const sbChatMessageMetadataSchema = z.object({
|
export const sbChatMessageMetadataSchema = z.object({
|
||||||
modelName: z.string().optional(),
|
modelName: z.string().optional(),
|
||||||
totalInputTokens: z.number().optional(),
|
totalInputTokens: z.number().optional(),
|
||||||
|
|
@ -50,8 +72,7 @@ export const sbChatMessageMetadataSchema = z.object({
|
||||||
timestamp: z.string(), // ISO date string
|
timestamp: z.string(), // ISO date string
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
})).optional(),
|
})).optional(),
|
||||||
selectedRepos: z.array(z.string()).optional(),
|
selectedSearchScopes: z.array(searchScopeSchema).optional(),
|
||||||
selectedContexts: z.array(z.string()).optional(),
|
|
||||||
traceId: z.string().optional(),
|
traceId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -139,8 +160,7 @@ export const SET_CHAT_STATE_QUERY_PARAM = 'setChatState';
|
||||||
|
|
||||||
export type SetChatStatePayload = {
|
export type SetChatStatePayload = {
|
||||||
inputMessage: CreateUIMessage<SBChatMessage>;
|
inputMessage: CreateUIMessage<SBChatMessage>;
|
||||||
selectedRepos: string[];
|
selectedSearchScopes: SearchScope[];
|
||||||
selectedContexts: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,7 +177,6 @@ export type LanguageModelInfo = {
|
||||||
// Additional request body data that we send along to the chat API.
|
// Additional request body data that we send along to the chat API.
|
||||||
export const additionalChatRequestParamsSchema = z.object({
|
export const additionalChatRequestParamsSchema = z.object({
|
||||||
languageModelId: z.string(),
|
languageModelId: z.string(),
|
||||||
selectedRepos: z.array(z.string()),
|
selectedSearchScopes: z.array(searchScopeSchema),
|
||||||
selectedContexts: z.array(z.string()),
|
|
||||||
});
|
});
|
||||||
export type AdditionalChatRequestParams = z.infer<typeof additionalChatRequestParamsSchema>;
|
export type AdditionalChatRequestParams = z.infer<typeof additionalChatRequestParamsSchema>;
|
||||||
|
|
@ -10,8 +10,7 @@ import { useRouter } from "next/navigation";
|
||||||
import { createChat } from "./actions";
|
import { createChat } from "./actions";
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
import { createPathWithQueryParams } from "@/lib/utils";
|
import { createPathWithQueryParams } from "@/lib/utils";
|
||||||
import { SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from "./types";
|
import { SearchScope, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from "./types";
|
||||||
import { ContextItem } from "./components/chatBox/contextSelector";
|
|
||||||
|
|
||||||
export const useCreateNewChatThread = () => {
|
export const useCreateNewChatThread = () => {
|
||||||
const domain = useDomain();
|
const domain = useDomain();
|
||||||
|
|
@ -19,15 +18,11 @@ export const useCreateNewChatThread = () => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: ContextItem[]) => {
|
const createNewChatThread = useCallback(async (children: Descendant[], selectedSearchScopes: SearchScope[]) => {
|
||||||
const text = slateContentToString(children);
|
const text = slateContentToString(children);
|
||||||
const mentions = getAllMentionElements(children);
|
const mentions = getAllMentionElements(children);
|
||||||
|
|
||||||
// Extract repos and contexts from selectedItems
|
const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedSearchScopes);
|
||||||
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 inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedContexts);
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await createChat(domain);
|
const response = await createChat(domain);
|
||||||
|
|
@ -42,8 +37,7 @@ export const useCreateNewChatThread = () => {
|
||||||
const url = createPathWithQueryParams(`/${domain}/chat/${response.id}`,
|
const url = createPathWithQueryParams(`/${domain}/chat/${response.id}`,
|
||||||
[SET_CHAT_STATE_QUERY_PARAM, JSON.stringify({
|
[SET_CHAT_STATE_QUERY_PARAM, JSON.stringify({
|
||||||
inputMessage,
|
inputMessage,
|
||||||
selectedRepos,
|
selectedSearchScopes,
|
||||||
selectedContexts,
|
|
||||||
} satisfies SetChatStatePayload)],
|
} satisfies SetChatStatePayload)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
SBChatMessage,
|
SBChatMessage,
|
||||||
SBChatMessagePart,
|
SBChatMessagePart,
|
||||||
SBChatMessageToolTypes,
|
SBChatMessageToolTypes,
|
||||||
|
SearchScope,
|
||||||
Source,
|
Source,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
|
|
||||||
|
|
@ -172,7 +173,7 @@ export const addLineNumbers = (source: string, lineOffset = 1) => {
|
||||||
return source.split('\n').map((line, index) => `${index + lineOffset}:${line}`).join('\n');
|
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[], selectedSearchScopes: SearchScope[]): CreateUIMessage<SBChatMessage> => {
|
||||||
// Converts applicable mentions into sources.
|
// Converts applicable mentions into sources.
|
||||||
const sources: Source[] = mentions
|
const sources: Source[] = mentions
|
||||||
.map((mention) => {
|
.map((mention) => {
|
||||||
|
|
@ -205,8 +206,7 @@ export const createUIMessage = (text: string, mentions: MentionData[], selectedR
|
||||||
})) as UIMessagePart<{ source: Source }, SBChatMessageToolTypes>[],
|
})) as UIMessagePart<{ source: Source }, SBChatMessageToolTypes>[],
|
||||||
],
|
],
|
||||||
metadata: {
|
metadata: {
|
||||||
selectedRepos,
|
selectedSearchScopes,
|
||||||
selectedContexts,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ export const orgMetadataSchema = z.object({
|
||||||
anonymousAccessEnabled: z.boolean().optional(),
|
anonymousAccessEnabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const demoSearchContextSchema = z.object({
|
export const demoSearchScopeSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
displayName: z.string(),
|
displayName: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
type: z.enum(["repo", "set"]),
|
type: z.enum(["repo", "reposet"]),
|
||||||
codeHostType: z.string().optional(),
|
codeHostType: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -16,22 +16,15 @@ export const demoSearchExampleSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
url: z.string(),
|
url: z.string(),
|
||||||
searchContext: z.array(z.number())
|
searchScopes: z.array(z.number())
|
||||||
})
|
|
||||||
|
|
||||||
export const demoSearchContextExampleSchema = z.object({
|
|
||||||
searchContext: z.number(),
|
|
||||||
description: z.string(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const demoExamplesSchema = z.object({
|
export const demoExamplesSchema = z.object({
|
||||||
searchContexts: demoSearchContextSchema.array(),
|
searchScopes: demoSearchScopeSchema.array(),
|
||||||
searchExamples: demoSearchExampleSchema.array(),
|
searchExamples: demoSearchExampleSchema.array(),
|
||||||
searchContextExamples: demoSearchContextExampleSchema.array(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type OrgMetadata = z.infer<typeof orgMetadataSchema>;
|
export type OrgMetadata = z.infer<typeof orgMetadataSchema>;
|
||||||
export type DemoExamples = z.infer<typeof demoExamplesSchema>;
|
export type DemoExamples = z.infer<typeof demoExamplesSchema>;
|
||||||
export type DemoSearchContext = z.infer<typeof demoSearchContextSchema>;
|
export type DemoSearchScope = z.infer<typeof demoSearchScopeSchema>;
|
||||||
export type DemoSearchExample = z.infer<typeof demoSearchExampleSchema>;
|
export type DemoSearchExample = z.infer<typeof demoSearchExampleSchema>;
|
||||||
export type DemoSearchContextExample = z.infer<typeof demoSearchContextExampleSchema>;
|
|
||||||
Loading…
Reference in a new issue