Skip to content

Commit f720ec9

Browse files
authored
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
1 parent aebd8df commit f720ec9

10 files changed

Lines changed: 345 additions & 235 deletions

File tree

packages/shared/src/index.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type {
1212
export {
1313
base64Decode,
1414
loadConfig,
15+
loadJsonFile,
1516
isRemotePath,
1617
} from "./utils.js";
1718
export {

packages/shared/src/utils.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { indexSchema } from "@sourcebot/schemas/v3/index.schema";
33
import { readFile } from 'fs/promises';
44
import stripJsonComments from 'strip-json-comments';
55
import { Ajv } from "ajv";
6+
import { z } from "zod";
67

78
const ajv = new Ajv({
89
validateFormats: false,
@@ -18,6 +19,66 @@ export const isRemotePath = (path: string) => {
1819
return path.startsWith('https://') || path.startsWith('http://');
1920
}
2021

22+
// TODO: Merge this with config loading logic which uses AJV
23+
export const loadJsonFile = async <T>(
24+
filePath: string,
25+
schema: any
26+
): Promise<T> => {
27+
const fileContent = await (async () => {
28+
if (isRemotePath(filePath)) {
29+
const response = await fetch(filePath);
30+
if (!response.ok) {
31+
throw new Error(`Failed to fetch file ${filePath}: ${response.statusText}`);
32+
}
33+
return response.text();
34+
} else {
35+
// Retry logic for handling race conditions with mounted volumes
36+
const maxAttempts = 5;
37+
const retryDelayMs = 2000;
38+
let lastError: Error | null = null;
39+
40+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
41+
try {
42+
return await readFile(filePath, {
43+
encoding: 'utf-8',
44+
});
45+
} catch (error) {
46+
lastError = error as Error;
47+
48+
// Only retry on ENOENT errors (file not found)
49+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
50+
throw error; // Throw immediately for non-ENOENT errors
51+
}
52+
53+
// Log warning before retry (except on the last attempt)
54+
if (attempt < maxAttempts) {
55+
console.warn(`File not found, retrying in 2s... (Attempt ${attempt}/${maxAttempts})`);
56+
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
57+
}
58+
}
59+
}
60+
61+
// If we've exhausted all retries, throw the last ENOENT error
62+
if (lastError) {
63+
throw lastError;
64+
}
65+
66+
throw new Error('Failed to load file after all retry attempts');
67+
}
68+
})();
69+
70+
const parsedData = JSON.parse(stripJsonComments(fileContent));
71+
72+
try {
73+
return schema.parse(parsedData);
74+
} catch (error) {
75+
if (error instanceof z.ZodError) {
76+
throw new Error(`File '${filePath}' is invalid: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`);
77+
}
78+
throw error;
79+
}
80+
}
81+
2182
export const loadConfig = async (configPath: string): Promise<SourcebotConfig> => {
2283
const configContent = await (async () => {
2384
if (isRemotePath(configPath)) {
Lines changed: 18 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,17 @@
11
'use client';
22

3-
import { Button } from "@/components/ui/button";
43
import { Separator } from "@/components/ui/separator";
54
import { ChatBox } from "@/features/chat/components/chatBox";
65
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
76
import { LanguageModelInfo } from "@/features/chat/types";
87
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
9-
import { resetEditor } from "@/features/chat/utils";
10-
import { useDomain } from "@/hooks/useDomain";
118
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
12-
import { getDisplayTime } from "@/lib/utils";
13-
import { BrainIcon, FileIcon, LucideIcon, SearchIcon } from "lucide-react";
14-
import Link from "next/link";
15-
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
16-
import { ReactEditor, useSlate } from "slate-react";
9+
import { useState } from "react";
1710
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
1811
import { useLocalStorage } from "usehooks-ts";
1912
import { ContextItem } from "@/features/chat/components/chatBox/contextSelector";
20-
21-
// @todo: we should probably rename this to a different type since it sort-of clashes
22-
// with the Suggestion system we have built into the chat box.
23-
type SuggestionType = "understand" | "find" | "summarize";
24-
25-
const suggestionTypes: Record<SuggestionType, {
26-
icon: LucideIcon;
27-
title: string;
28-
description: string;
29-
}> = {
30-
understand: {
31-
icon: BrainIcon,
32-
title: "Understand",
33-
description: "Understand the codebase",
34-
},
35-
find: {
36-
icon: SearchIcon,
37-
title: "Find",
38-
description: "Find the codebase",
39-
},
40-
summarize: {
41-
icon: FileIcon,
42-
title: "Summarize",
43-
description: "Summarize the codebase",
44-
},
45-
}
46-
47-
48-
const Highlight = ({ children }: { children: React.ReactNode }) => {
49-
return (
50-
<span className="text-highlight">
51-
{children}
52-
</span>
53-
)
54-
}
55-
56-
const suggestions: Record<SuggestionType, {
57-
queryText: string;
58-
queryNode?: ReactNode;
59-
openRepoSelector?: boolean;
60-
}[]> = {
61-
understand: [
62-
{
63-
queryText: "How does authentication work in this codebase?",
64-
openRepoSelector: true,
65-
},
66-
{
67-
queryText: "How are API endpoints structured and organized?",
68-
openRepoSelector: true,
69-
},
70-
{
71-
queryText: "How does the build and deployment process work?",
72-
openRepoSelector: true,
73-
},
74-
{
75-
queryText: "How is error handling implemented across the application?",
76-
openRepoSelector: true,
77-
},
78-
],
79-
find: [
80-
{
81-
queryText: "Find examples of different logging libraries used throughout the codebase.",
82-
},
83-
{
84-
queryText: "Find examples of potential security vulnerabilities or authentication issues.",
85-
},
86-
{
87-
queryText: "Find examples of API endpoints and route handlers.",
88-
}
89-
],
90-
summarize: [
91-
{
92-
queryText: "Summarize the purpose of this file @file:",
93-
queryNode: <span>Summarize the purpose of this file <Highlight>@file:</Highlight></span>
94-
},
95-
{
96-
queryText: "Summarize the project structure and architecture.",
97-
openRepoSelector: true,
98-
},
99-
{
100-
queryText: "Provide a quick start guide for ramping up on this codebase.",
101-
openRepoSelector: true,
102-
}
103-
],
104-
}
105-
106-
const MAX_RECENT_CHAT_HISTORY_COUNT = 10;
107-
13+
import { DemoExamples } from "@/types";
14+
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
10815

10916
interface AgenticSearchProps {
11017
searchModeSelectorProps: SearchModeSelectorProps;
@@ -116,49 +23,23 @@ interface AgenticSearchProps {
11623
createdAt: Date;
11724
name: string | null;
11825
}[];
26+
demoExamples: DemoExamples | undefined;
11927
}
12028

12129
export const AgenticSearch = ({
12230
searchModeSelectorProps,
12331
languageModels,
12432
repos,
12533
searchContexts,
126-
chatHistory,
34+
demoExamples,
12735
}: AgenticSearchProps) => {
128-
const [selectedSuggestionType, _setSelectedSuggestionType] = useState<SuggestionType | undefined>(undefined);
12936
const { createNewChatThread, isLoading } = useCreateNewChatThread();
130-
const dropdownRef = useRef<HTMLDivElement>(null);
131-
const editor = useSlate();
13237
const [selectedItems, setSelectedItems] = useLocalStorage<ContextItem[]>("selectedContextItems", [], { initializeWithValue: false });
133-
const domain = useDomain();
13438
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
13539

136-
const setSelectedSuggestionType = useCallback((type: SuggestionType | undefined) => {
137-
_setSelectedSuggestionType(type);
138-
if (type) {
139-
ReactEditor.focus(editor);
140-
}
141-
}, [editor, _setSelectedSuggestionType]);
142-
143-
// Close dropdown when clicking outside
144-
useEffect(() => {
145-
function handleClickOutside(event: MouseEvent) {
146-
if (
147-
!dropdownRef.current?.contains(event.target as Node)
148-
) {
149-
setSelectedSuggestionType(undefined);
150-
}
151-
}
152-
153-
document.addEventListener("mousedown", handleClickOutside)
154-
return () => document.removeEventListener("mousedown", handleClickOutside)
155-
}, [setSelectedSuggestionType]);
156-
15740
return (
158-
<div className="flex flex-col items-center w-full max-w-[800px]">
159-
<div
160-
className="mt-4 w-full border rounded-md shadow-sm"
161-
>
41+
<div className="flex flex-col items-center w-full">
42+
<div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]">
16243
<ChatBox
16344
onSubmit={(children) => {
16445
createNewChatThread(children, selectedItems);
@@ -187,111 +68,18 @@ export const AgenticSearch = ({
18768
className="ml-auto"
18869
/>
18970
</div>
190-
191-
{selectedSuggestionType && (
192-
<div
193-
ref={dropdownRef}
194-
className="w-full absolute top-10 z-10 drop-shadow-2xl bg-background border rounded-md p-2"
195-
>
196-
<p className="text-muted-foreground text-sm mb-2">
197-
{suggestionTypes[selectedSuggestionType].title}
198-
</p>
199-
{suggestions[selectedSuggestionType].map(({ queryText, queryNode, openRepoSelector }, index) => (
200-
<div
201-
key={index}
202-
className="flex flex-row items-center gap-2 cursor-pointer hover:bg-muted rounded-md px-1 py-0.5"
203-
onClick={() => {
204-
resetEditor(editor);
205-
editor.insertText(queryText);
206-
setSelectedSuggestionType(undefined);
207-
208-
if (openRepoSelector) {
209-
setIsContextSelectorOpen(true);
210-
} else {
211-
ReactEditor.focus(editor);
212-
}
213-
}}
214-
>
215-
<SearchIcon className="w-4 h-4" />
216-
{queryNode ?? queryText}
217-
</div>
218-
))}
219-
</div>
220-
)}
22171
</div>
22272
</div>
223-
<div className="flex flex-col items-center w-fit gap-6 mt-8 relative">
224-
<div className="flex flex-row items-center gap-4">
225-
{Object.entries(suggestionTypes).map(([type, suggestion], index) => (
226-
<ExampleButton
227-
key={index}
228-
Icon={suggestion.icon}
229-
title={suggestion.title}
230-
onClick={() => {
231-
setSelectedSuggestionType(type as SuggestionType);
232-
}}
233-
/>
234-
))}
235-
</div>
236-
</div>
237-
{chatHistory.length > 0 && (
238-
<div className="flex flex-col items-center w-[80%]">
239-
<Separator className="my-6" />
240-
<span className="font-semibold mb-2">Recent conversations</span>
241-
<div
242-
className="flex flex-col gap-1 w-full"
243-
>
244-
{chatHistory
245-
.slice(0, MAX_RECENT_CHAT_HISTORY_COUNT)
246-
.map((chat) => (
247-
<Link
248-
key={chat.id}
249-
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"
250-
href={`/${domain}/chat/${chat.id}`}
251-
>
252-
<span className="text-sm text-muted-foreground group-hover:text-foreground">
253-
{chat.name ?? "Untitled Chat"}
254-
</span>
255-
<span className="text-sm text-muted-foreground group-hover:text-foreground">
256-
{getDisplayTime(chat.createdAt)}
257-
</span>
258-
</Link>
259-
))}
260-
</div>
261-
{chatHistory.length > MAX_RECENT_CHAT_HISTORY_COUNT && (
262-
<Link
263-
href={`/${domain}/chat`}
264-
className="text-sm text-link hover:underline mt-6"
265-
>
266-
View all
267-
</Link>
268-
)}
269-
</div>
270-
)}
271-
</div>
272-
)
273-
}
274-
27573

276-
interface ExampleButtonProps {
277-
Icon: LucideIcon;
278-
title: string;
279-
onClick: () => void;
280-
}
281-
282-
const ExampleButton = ({
283-
Icon,
284-
title,
285-
onClick,
286-
}: ExampleButtonProps) => {
287-
return (
288-
<Button
289-
variant="secondary"
290-
onClick={onClick}
291-
className="h-9"
292-
>
293-
<Icon className="w-4 h-4" />
294-
{title}
295-
</Button>
74+
{demoExamples && (
75+
<AskSourcebotDemoCards
76+
demoExamples={demoExamples}
77+
selectedItems={selectedItems}
78+
setSelectedItems={setSelectedItems}
79+
searchContexts={searchContexts}
80+
repos={repos}
81+
/>
82+
)}
83+
</div >
29684
)
297-
}
85+
}

0 commit comments

Comments
 (0)