From 463477c85e107f9dae847fc2338a169b13128553 Mon Sep 17 00:00:00 2001 From: msukkari Date: Sun, 27 Jul 2025 13:40:11 -0700 Subject: [PATCH] load demo example --- packages/shared/src/index.server.ts | 1 + packages/shared/src/utils.ts | 61 ++++++++++ .../components/homepage/agenticSearch.tsx | 105 +----------------- packages/web/src/app/[domain]/page.tsx | 6 +- packages/web/src/types.ts | 2 +- 5 files changed, 69 insertions(+), 106 deletions(-) diff --git a/packages/shared/src/index.server.ts b/packages/shared/src/index.server.ts index 3cc4be65..ce00642e 100644 --- a/packages/shared/src/index.server.ts +++ b/packages/shared/src/index.server.ts @@ -12,6 +12,7 @@ export type { export { base64Decode, loadConfig, + loadJsonFile, isRemotePath, } from "./utils.js"; export { diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 431ff741..69cbc52e 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -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 ( + filePath: string, + schema: any +): Promise => { + 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 => { const configContent = await (async () => { if (isRemotePath(configPath)) { diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx index 9374d2b5..5d09168a 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx @@ -5,11 +5,8 @@ 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 { Code, Database, FileIcon, FileText, Gamepad2, Globe, Layers, LucideIcon, Search, SearchIcon, Smartphone, Zap } from "lucide-react"; +import { Layers, Search } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { useState } from "react"; import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar"; @@ -19,14 +16,6 @@ import { useLocalStorage } from "usehooks-ts"; import { ContextItem } from "@/features/chat/components/chatBox/contextSelector"; import { DemoExamples, DemoSearchExample, DemoSearchContextExample } from "@/types"; -const Highlight = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ) -} - interface AgenticSearchProps { searchModeSelectorProps: SearchModeSelectorProps; languageModels: LanguageModelInfo[]; @@ -40,101 +29,11 @@ interface AgenticSearchProps { demoExamples: DemoExamples | undefined; } -const exampleSearches = [ - { - id: "1", - title: "Show me examples of how useMemo is used", - description: "Find React performance optimization patterns", - icon: , - category: "React", - }, - { - id: "2", - title: "How do I implement authentication?", - description: "Explore auth patterns and best practices", - icon: , - category: "Security", - }, - { - id: "3", - title: "Find API route handlers", - description: "Locate and analyze API endpoint implementations", - icon: , - category: "Backend", - }, - { - id: "4", - title: "Show me error handling patterns", - description: "Discover error boundary and exception handling", - icon: , - category: "Best Practices", - }, - { - id: "5", - title: "How are components structured?", - description: "Analyze component architecture and patterns", - icon: , - category: "Architecture", - }, -] - -const searchContextsExample = [ - { - id: "1", - displayName: "Next.js", - name: "nextjs", - description: "React framework for production", - icon: , - color: "bg-black text-white", - }, - { - id: "2", - displayName: "React", - name: "react", - description: "JavaScript library for building UIs", - icon: , - color: "bg-blue-500 text-white", - }, - { - id: "3", - displayName: "TypeScript", - name: "typescript", - description: "Typed JavaScript at scale", - icon: , - color: "bg-blue-600 text-white", - }, - { - id: "4", - displayName: "Tailwind CSS", - name: "tailwindcss", - description: "Utility-first CSS framework", - icon: , - color: "bg-cyan-500 text-white", - }, - { - id: "5", - displayName: "Godot Engine", - name: "godot", - description: "Open source game engine", - icon: , - color: "bg-blue-400 text-white", - }, - { - id: "6", - displayName: "React Native", - name: "react-native", - description: "Build mobile apps with React", - icon: , - color: "bg-purple-500 text-white", - }, -] - export const AgenticSearch = ({ searchModeSelectorProps, languageModels, repos, searchContexts, - chatHistory, demoExamples, }: AgenticSearchProps) => { const { createNewChatThread, isLoading } = useCreateNewChatThread(); @@ -226,7 +125,7 @@ export const AgenticSearch = ({

Search Contexts

- {demoExamples.searchContexts?.map((context) => { + {demoExamples.searchContextExamples.map((context) => { const searchContext = searchContexts.find((item) => item.name === context.name); if (!searchContext) return null; const isSelected = false; //selectedItems.some((item) => item.id === context.id) diff --git a/packages/web/src/app/[domain]/page.tsx b/packages/web/src/app/[domain]/page.tsx index b63348c6..5ede8743 100644 --- a/packages/web/src/app/[domain]/page.tsx +++ b/packages/web/src/app/[domain]/page.tsx @@ -2,7 +2,7 @@ import { getRepos, getSearchContexts } from "@/actions"; import { Footer } from "@/app/components/footer"; import { getOrgFromDomain } from "@/data/org"; import { getConfiguredLanguageModelsInfo, getUserChatHistory } from "@/features/chat/actions"; -import { isServiceError, loadDemoExamples } from "@/lib/utils"; +import { isServiceError } from "@/lib/utils"; import { Homepage } from "./components/homepage"; import { NavigationMenu } from "./components/navigationMenu"; import { PageNotFound } from "./components/pageNotFound"; @@ -12,6 +12,8 @@ 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); @@ -49,7 +51,7 @@ export default async function Home({ params: { domain } }: { params: { domain: s searchModeCookie?.value === "precise" ) ? searchModeCookie.value : models.length > 0 ? "agentic" : "precise"; - const demoExamples = undefined; //await loadDemoExamples(env.SOURCEBOT_DEMO_EXAMPLES_PATH); + const demoExamples = env.SOURCEBOT_DEMO_EXAMPLES_PATH ? await loadJsonFile(env.SOURCEBOT_DEMO_EXAMPLES_PATH, demoExamplesSchema) : undefined; return (
diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index a8cf2a38..8f41d89e 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -23,7 +23,7 @@ export const demoSearchContextExampleSchema = z.object({ export const demoExamplesSchema = z.object({ searchExamples: demoSearchExampleSchema.array(), - searchContexts: demoSearchContextExampleSchema.array(), + searchContextExamples: demoSearchContextExampleSchema.array(), }) export type OrgMetadata = z.infer;