mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Add demo example cards (#401)
* wip demo example path * load demo example * nit: format * refactor demo cards to their own component * ui nits * more ui nits * feedback
This commit is contained in:
parent
aebd8df193
commit
f720ec945d
10 changed files with 345 additions and 235 deletions
|
|
@ -12,6 +12,7 @@ export type {
|
|||
export {
|
||||
base64Decode,
|
||||
loadConfig,
|
||||
loadJsonFile,
|
||||
isRemotePath,
|
||||
} from "./utils.js";
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { indexSchema } from "@sourcebot/schemas/v3/index.schema";
|
|||
import { readFile } from 'fs/promises';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { Ajv } from "ajv";
|
||||
import { z } from "zod";
|
||||
|
||||
const ajv = new Ajv({
|
||||
validateFormats: false,
|
||||
|
|
@ -18,6 +19,66 @@ export const isRemotePath = (path: string) => {
|
|||
return path.startsWith('https://') || path.startsWith('http://');
|
||||
}
|
||||
|
||||
// TODO: Merge this with config loading logic which uses AJV
|
||||
export const loadJsonFile = async <T>(
|
||||
filePath: string,
|
||||
schema: any
|
||||
): Promise<T> => {
|
||||
const fileContent = await (async () => {
|
||||
if (isRemotePath(filePath)) {
|
||||
const response = await fetch(filePath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch file ${filePath}: ${response.statusText}`);
|
||||
}
|
||||
return response.text();
|
||||
} else {
|
||||
// Retry logic for handling race conditions with mounted volumes
|
||||
const maxAttempts = 5;
|
||||
const retryDelayMs = 2000;
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await readFile(filePath, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// Only retry on ENOENT errors (file not found)
|
||||
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
||||
throw error; // Throw immediately for non-ENOENT errors
|
||||
}
|
||||
|
||||
// Log warning before retry (except on the last attempt)
|
||||
if (attempt < maxAttempts) {
|
||||
console.warn(`File not found, retrying in 2s... (Attempt ${attempt}/${maxAttempts})`);
|
||||
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we've exhausted all retries, throw the last ENOENT error
|
||||
if (lastError) {
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
throw new Error('Failed to load file after all retry attempts');
|
||||
}
|
||||
})();
|
||||
|
||||
const parsedData = JSON.parse(stripJsonComments(fileContent));
|
||||
|
||||
try {
|
||||
return schema.parse(parsedData);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
throw new Error(`File '${filePath}' is invalid: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const loadConfig = async (configPath: string): Promise<SourcebotConfig> => {
|
||||
const configContent = await (async () => {
|
||||
if (isRemotePath(configPath)) {
|
||||
|
|
|
|||
|
|
@ -1,110 +1,17 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ChatBox } from "@/features/chat/components/chatBox";
|
||||
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
|
||||
import { LanguageModelInfo } from "@/features/chat/types";
|
||||
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
|
||||
import { resetEditor } from "@/features/chat/utils";
|
||||
import { useDomain } from "@/hooks/useDomain";
|
||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||
import { getDisplayTime } from "@/lib/utils";
|
||||
import { BrainIcon, FileIcon, LucideIcon, SearchIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ReactEditor, useSlate } from "slate-react";
|
||||
import { useState } from "react";
|
||||
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
|
||||
|
||||
// @todo: we should probably rename this to a different type since it sort-of clashes
|
||||
// with the Suggestion system we have built into the chat box.
|
||||
type SuggestionType = "understand" | "find" | "summarize";
|
||||
|
||||
const suggestionTypes: Record<SuggestionType, {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}> = {
|
||||
understand: {
|
||||
icon: BrainIcon,
|
||||
title: "Understand",
|
||||
description: "Understand the codebase",
|
||||
},
|
||||
find: {
|
||||
icon: SearchIcon,
|
||||
title: "Find",
|
||||
description: "Find the codebase",
|
||||
},
|
||||
summarize: {
|
||||
icon: FileIcon,
|
||||
title: "Summarize",
|
||||
description: "Summarize the codebase",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const Highlight = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<span className="text-highlight">
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const suggestions: Record<SuggestionType, {
|
||||
queryText: string;
|
||||
queryNode?: ReactNode;
|
||||
openRepoSelector?: boolean;
|
||||
}[]> = {
|
||||
understand: [
|
||||
{
|
||||
queryText: "How does authentication work in this codebase?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "How are API endpoints structured and organized?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "How does the build and deployment process work?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "How is error handling implemented across the application?",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
],
|
||||
find: [
|
||||
{
|
||||
queryText: "Find examples of different logging libraries used throughout the codebase.",
|
||||
},
|
||||
{
|
||||
queryText: "Find examples of potential security vulnerabilities or authentication issues.",
|
||||
},
|
||||
{
|
||||
queryText: "Find examples of API endpoints and route handlers.",
|
||||
}
|
||||
],
|
||||
summarize: [
|
||||
{
|
||||
queryText: "Summarize the purpose of this file @file:",
|
||||
queryNode: <span>Summarize the purpose of this file <Highlight>@file:</Highlight></span>
|
||||
},
|
||||
{
|
||||
queryText: "Summarize the project structure and architecture.",
|
||||
openRepoSelector: true,
|
||||
},
|
||||
{
|
||||
queryText: "Provide a quick start guide for ramping up on this codebase.",
|
||||
openRepoSelector: true,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
const MAX_RECENT_CHAT_HISTORY_COUNT = 10;
|
||||
|
||||
import { DemoExamples } from "@/types";
|
||||
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
|
||||
|
||||
interface AgenticSearchProps {
|
||||
searchModeSelectorProps: SearchModeSelectorProps;
|
||||
|
|
@ -116,6 +23,7 @@ interface AgenticSearchProps {
|
|||
createdAt: Date;
|
||||
name: string | null;
|
||||
}[];
|
||||
demoExamples: DemoExamples | undefined;
|
||||
}
|
||||
|
||||
export const AgenticSearch = ({
|
||||
|
|
@ -123,42 +31,15 @@ export const AgenticSearch = ({
|
|||
languageModels,
|
||||
repos,
|
||||
searchContexts,
|
||||
chatHistory,
|
||||
demoExamples,
|
||||
}: AgenticSearchProps) => {
|
||||
const [selectedSuggestionType, _setSelectedSuggestionType] = useState<SuggestionType | undefined>(undefined);
|
||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const editor = useSlate();
|
||||
const [selectedItems, setSelectedItems] = useLocalStorage<ContextItem[]>("selectedContextItems", [], { initializeWithValue: false });
|
||||
const domain = useDomain();
|
||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||
|
||||
const setSelectedSuggestionType = useCallback((type: SuggestionType | undefined) => {
|
||||
_setSelectedSuggestionType(type);
|
||||
if (type) {
|
||||
ReactEditor.focus(editor);
|
||||
}
|
||||
}, [editor, _setSelectedSuggestionType]);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
!dropdownRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
setSelectedSuggestionType(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||
}, [setSelectedSuggestionType]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full max-w-[800px]">
|
||||
<div
|
||||
className="mt-4 w-full border rounded-md shadow-sm"
|
||||
>
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]">
|
||||
<ChatBox
|
||||
onSubmit={(children) => {
|
||||
createNewChatThread(children, selectedItems);
|
||||
|
|
@ -187,111 +68,18 @@ export const AgenticSearch = ({
|
|||
className="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedSuggestionType && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="w-full absolute top-10 z-10 drop-shadow-2xl bg-background border rounded-md p-2"
|
||||
>
|
||||
<p className="text-muted-foreground text-sm mb-2">
|
||||
{suggestionTypes[selectedSuggestionType].title}
|
||||
</p>
|
||||
{suggestions[selectedSuggestionType].map(({ queryText, queryNode, openRepoSelector }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-row items-center gap-2 cursor-pointer hover:bg-muted rounded-md px-1 py-0.5"
|
||||
onClick={() => {
|
||||
resetEditor(editor);
|
||||
editor.insertText(queryText);
|
||||
setSelectedSuggestionType(undefined);
|
||||
|
||||
if (openRepoSelector) {
|
||||
setIsContextSelectorOpen(true);
|
||||
} else {
|
||||
ReactEditor.focus(editor);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchIcon className="w-4 h-4" />
|
||||
{queryNode ?? queryText}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center w-fit gap-6 mt-8 relative">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
{Object.entries(suggestionTypes).map(([type, suggestion], index) => (
|
||||
<ExampleButton
|
||||
key={index}
|
||||
Icon={suggestion.icon}
|
||||
title={suggestion.title}
|
||||
onClick={() => {
|
||||
setSelectedSuggestionType(type as SuggestionType);
|
||||
}}
|
||||
{demoExamples && (
|
||||
<AskSourcebotDemoCards
|
||||
demoExamples={demoExamples}
|
||||
selectedItems={selectedItems}
|
||||
setSelectedItems={setSelectedItems}
|
||||
searchContexts={searchContexts}
|
||||
repos={repos}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{chatHistory.length > 0 && (
|
||||
<div className="flex flex-col items-center w-[80%]">
|
||||
<Separator className="my-6" />
|
||||
<span className="font-semibold mb-2">Recent conversations</span>
|
||||
<div
|
||||
className="flex flex-col gap-1 w-full"
|
||||
>
|
||||
{chatHistory
|
||||
.slice(0, MAX_RECENT_CHAT_HISTORY_COUNT)
|
||||
.map((chat) => (
|
||||
<Link
|
||||
key={chat.id}
|
||||
className="flex flex-row items-center justify-between gap-1 w-full rounded-md hover:bg-muted px-2 py-0.5 cursor-pointer group"
|
||||
href={`/${domain}/chat/${chat.id}`}
|
||||
>
|
||||
<span className="text-sm text-muted-foreground group-hover:text-foreground">
|
||||
{chat.name ?? "Untitled Chat"}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground group-hover:text-foreground">
|
||||
{getDisplayTime(chat.createdAt)}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
{chatHistory.length > MAX_RECENT_CHAT_HISTORY_COUNT && (
|
||||
<Link
|
||||
href={`/${domain}/chat`}
|
||||
className="text-sm text-link hover:underline mt-6"
|
||||
>
|
||||
View all
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
interface ExampleButtonProps {
|
||||
Icon: LucideIcon;
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const ExampleButton = ({
|
||||
Icon,
|
||||
title,
|
||||
onClick,
|
||||
}: ExampleButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
className="h-9"
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{title}
|
||||
</Button>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
'use client';
|
||||
|
||||
import Image from "next/image";
|
||||
import { Search, LibraryBigIcon, Code, Layers } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import { ContextItem, RepoContextItem, SearchContextItem } from "@/features/chat/components/chatBox/contextSelector";
|
||||
import { DemoExamples, DemoSearchExample, DemoSearchContextExample, DemoSearchContext } from "@/types";
|
||||
import { cn, getCodeHostIcon } from "@/lib/utils";
|
||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||
|
||||
interface AskSourcebotDemoCardsProps {
|
||||
demoExamples: DemoExamples;
|
||||
selectedItems: ContextItem[];
|
||||
setSelectedItems: (items: ContextItem[]) => void;
|
||||
searchContexts: SearchContextQuery[];
|
||||
repos: RepositoryQuery[];
|
||||
}
|
||||
|
||||
export const AskSourcebotDemoCards = ({
|
||||
demoExamples,
|
||||
selectedItems,
|
||||
setSelectedItems,
|
||||
searchContexts,
|
||||
repos,
|
||||
}: AskSourcebotDemoCardsProps) => {
|
||||
const handleExampleClick = (example: DemoSearchExample) => {
|
||||
if (example.url) {
|
||||
window.open(example.url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
const getContextIcon = (context: DemoSearchContext, size: number = 20) => {
|
||||
const sizeClass = size === 12 ? "h-3 w-3" : "h-5 w-5";
|
||||
|
||||
if (context.type === "set") {
|
||||
return <LibraryBigIcon className={cn(sizeClass, "text-muted-foreground")} />;
|
||||
}
|
||||
|
||||
if (context.codeHostType) {
|
||||
const codeHostIcon = getCodeHostIcon(context.codeHostType);
|
||||
if (codeHostIcon) {
|
||||
return (
|
||||
<Image
|
||||
src={codeHostIcon.src}
|
||||
alt={`${context.codeHostType} icon`}
|
||||
width={size}
|
||||
height={size}
|
||||
className={cn(sizeClass, codeHostIcon.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <Code className={cn(sizeClass, "text-muted-foreground")} />;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const isSelected = selectedItems.some(
|
||||
(selected) => selected.type === 'context' && selected.value === context.value
|
||||
);
|
||||
const newSelectedItems = isSelected
|
||||
? selectedItems.filter(
|
||||
(selected) => !(selected.type === 'context' && selected.value === context.value)
|
||||
)
|
||||
: [...selectedItems, { type: 'context', value: context.value, name: context.displayName, repoCount: searchContext.repoNames.length } as SearchContextItem];
|
||||
|
||||
setSelectedItems(newSelectedItems);
|
||||
} else {
|
||||
const repo = repos.find((repo) => repo.repoName === context.value);
|
||||
if (!repo) {
|
||||
console.error(`Repo ${context.value} not found on handleContextClick`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = selectedItems.some(
|
||||
(selected) => selected.type === 'repo' && selected.value === context.value
|
||||
);
|
||||
const newSelectedItems = isSelected
|
||||
? selectedItems.filter(
|
||||
(selected) => !(selected.type === 'repo' && selected.value === context.value)
|
||||
)
|
||||
: [...selectedItems, { type: 'repo', value: context.value, name: context.displayName, codeHostType: repo.codeHostType } as RepoContextItem];
|
||||
|
||||
setSelectedItems(newSelectedItems);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mt-8 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 Context</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 */}
|
||||
<div className="space-y-4">
|
||||
<div className="text-center mb-8">
|
||||
<div className="flex items-center justify-center gap-2 mb-2">
|
||||
<Search className="h-5 w-5 text-muted-foreground" />
|
||||
<h3 className="text-lg font-semibold">Community Ask Results</h3>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Check out these featured ask results from the community</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{demoExamples.searchExamples.map((example) => {
|
||||
const searchContexts = demoExamples.searchContexts.filter((context) => example.searchContext.includes(context.id))
|
||||
return (
|
||||
<Card
|
||||
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]"
|
||||
onClick={() => handleExampleClick(example)}
|
||||
>
|
||||
<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}
|
||||
</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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -10,6 +10,7 @@ import { SearchMode } from "./toolbar";
|
|||
import { CustomSlateEditor } from "@/features/chat/customSlateEditor";
|
||||
import { setSearchModeCookie } from "@/actions";
|
||||
import { useCallback, useState } from "react";
|
||||
import { DemoExamples } from "@/types";
|
||||
|
||||
interface HomepageProps {
|
||||
initialRepos: RepositoryQuery[];
|
||||
|
|
@ -21,6 +22,7 @@ interface HomepageProps {
|
|||
name: string | null;
|
||||
}[];
|
||||
initialSearchMode: SearchMode;
|
||||
demoExamples: DemoExamples | undefined;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -30,6 +32,7 @@ export const Homepage = ({
|
|||
languageModels,
|
||||
chatHistory,
|
||||
initialSearchMode,
|
||||
demoExamples,
|
||||
}: HomepageProps) => {
|
||||
const [searchMode, setSearchMode] = useState<SearchMode>(initialSearchMode);
|
||||
const isAgenticSearchEnabled = languageModels.length > 0;
|
||||
|
|
@ -86,6 +89,7 @@ export const Homepage = ({
|
|||
repos={initialRepos}
|
||||
searchContexts={searchContexts}
|
||||
chatHistory={chatHistory}
|
||||
demoExamples={demoExamples}
|
||||
/>
|
||||
</CustomSlateEditor>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import { ServiceErrorException } from "@/lib/serviceError";
|
|||
import { auth } from "@/auth";
|
||||
import { cookies } from "next/headers";
|
||||
import { SEARCH_MODE_COOKIE_NAME } from "@/lib/constants";
|
||||
import { env } from "@/env.mjs";
|
||||
import { loadJsonFile } from "@sourcebot/shared";
|
||||
import { DemoExamples, demoExamplesSchema } from "@/types";
|
||||
|
||||
export default async function Home({ params: { domain } }: { params: { domain: string } }) {
|
||||
const org = await getOrgFromDomain(domain);
|
||||
|
|
@ -48,6 +51,15 @@ export default async function Home({ params: { domain } }: { params: { domain: s
|
|||
searchModeCookie?.value === "precise"
|
||||
) ? searchModeCookie.value : models.length > 0 ? "agentic" : "precise";
|
||||
|
||||
const demoExamples = env.SOURCEBOT_DEMO_EXAMPLES_PATH ? await (async () => {
|
||||
try {
|
||||
return await loadJsonFile<DemoExamples>(env.SOURCEBOT_DEMO_EXAMPLES_PATH!, demoExamplesSchema);
|
||||
} catch (error) {
|
||||
console.error('Failed to load demo examples:', error);
|
||||
return undefined;
|
||||
}
|
||||
})() : undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center overflow-hidden min-h-screen">
|
||||
<NavigationMenu
|
||||
|
|
@ -61,6 +73,7 @@ export default async function Home({ params: { domain } }: { params: { domain: s
|
|||
languageModels={models}
|
||||
chatHistory={chatHistory}
|
||||
initialSearchMode={initialSearchMode}
|
||||
demoExamples={demoExamples}
|
||||
/>
|
||||
<Footer />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ export const env = createEnv({
|
|||
DEBUG_WRITE_CHAT_MESSAGES_TO_FILE: booleanSchema.default('false'),
|
||||
|
||||
LANGFUSE_SECRET_KEY: z.string().optional(),
|
||||
|
||||
SOURCEBOT_DEMO_EXAMPLES_PATH: z.string().optional(),
|
||||
},
|
||||
// @NOTE: Please make sure of the following:
|
||||
// - Make sure you destructure all client variables in
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export const ChatBox = ({
|
|||
if (isSubmitDisabled) {
|
||||
if (isSubmitDisabledReason === "no-repos-selected") {
|
||||
toast({
|
||||
description: "⚠️ One or more repositories or search contexts must be selected.",
|
||||
description: "⚠️ You must select at least one search context",
|
||||
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, plan, or search your codebase. @mention files to refine your query."
|
||||
placeholder="Ask questions about the selected search contexts. @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">One or more repositories or search contexts must be selected.</span>
|
||||
<span className="text-destructive">You must select at least one search context</span>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export const ContextSelector = React.forwardRef<
|
|||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search contexts and repos..."
|
||||
placeholder="Search contexts..."
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<CommandList ref={scrollContainerRef}>
|
||||
|
|
|
|||
|
|
@ -4,4 +4,34 @@ export const orgMetadataSchema = z.object({
|
|||
anonymousAccessEnabled: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export const demoSearchContextSchema = z.object({
|
||||
id: z.number(),
|
||||
displayName: z.string(),
|
||||
value: z.string(),
|
||||
type: z.enum(["repo", "set"]),
|
||||
codeHostType: z.string().optional(),
|
||||
})
|
||||
|
||||
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(),
|
||||
})
|
||||
|
||||
export const demoExamplesSchema = z.object({
|
||||
searchContexts: demoSearchContextSchema.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>;
|
||||
Loading…
Reference in a new issue