├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── api │ ├── query-kg │ │ └── route.ts │ └── query-vector-only │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── chat-interface.tsx ├── response-comparison.tsx └── ui │ ├── alert.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── loading-spinner.tsx │ ├── skeleton.tsx │ ├── sonner.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ └── toaster.tsx ├── eslint.config.mjs ├── hooks ├── use-mobile.tsx └── use-toast.tsx ├── lib └── utils.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── openai-logo.png ├── vercel.svg └── window.svg └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@typescript-eslint/no-explicit-any": "off" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 GPTechday 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI Academy Knowledge Graph Demo 2 | 3 | A demo application showcasing vector search vs. knowledge graph-based retrieval approaches. 4 | 5 | ## Features 6 | 7 | - Chat interface with query comparison 8 | - Vector-only search endpoint 9 | - Knowledge graph-enhanced search endpoint 10 | 11 | ## Getting Started 12 | 13 | ```bash 14 | # Install dependencies 15 | npm install 16 | 17 | # Run the development server 18 | npm run dev 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) in your browser to see the application. 22 | 23 | ## Project Structure 24 | 25 | - `app/` - Next.js application routes and pages 26 | - `app/api/` - API endpoints for vector and knowledge graph queries 27 | - `components/` - React components including chat interface 28 | - `hooks/` - Custom React hooks 29 | - `lib/` - Utility functions 30 | - `public/` - Static assets -------------------------------------------------------------------------------- /app/api/query-kg/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest, NextResponse } from "next/server" 2 | 3 | /** 4 | * Knowledge Graph Query API Route Handler 5 | * 6 | * This API route proxies user queries to a backend knowledge graph service and returns the results. 7 | * It handles parameter validation, error handling, and response formatting. 8 | * 9 | * Endpoint: GET /api/query-kg?query= 10 | * 11 | * @param request - The incoming NextRequest object containing query parameters 12 | * @returns NextResponse with either the processed results or an appropriate error message 13 | */ 14 | export async function GET(request: NextRequest) { 15 | const searchParams = request.nextUrl.searchParams 16 | const query = searchParams.get("query") 17 | 18 | if (!query) { 19 | return NextResponse.json({ error: "Query parameter is required" }, { status: 400 }) 20 | } 21 | 22 | try { 23 | /** 24 | * The base URL for the backend API 25 | * Falls back to localhost:8000 if the environment variable is not set 26 | */ 27 | const apiUrl = process.env.API_BASE_URL || "http://localhost:8000" 28 | 29 | console.log(`Calling KG API with query: ${query}`) 30 | 31 | const response = await fetch(`${apiUrl}/query-with-kg?query=${encodeURIComponent(query)}`, { 32 | method: 'GET', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | 'Cache-Control': 'no-cache', 36 | }, 37 | }) 38 | 39 | if (!response.ok) { 40 | throw new Error(`Backend API responded with status: ${response.status}`) 41 | } 42 | 43 | const text = await response.text() 44 | console.log("Raw KG response:", text.substring(0, 200) + "...") 45 | 46 | try { 47 | const data = JSON.parse(text) 48 | console.log("Parsed KG data (summary):", data.summary ? data.summary.substring(0, 100) + "..." : "No summary") 49 | 50 | return NextResponse.json(data, { 51 | headers: { 52 | 'Cache-Control': 'no-store, max-age=0', 53 | } 54 | }) 55 | } catch (parseError) { 56 | console.error("JSON parse error:", parseError) 57 | return NextResponse.json({ error: "Failed to parse JSON response from backend" }, { status: 500 }) 58 | } 59 | } catch (error) { 60 | console.error("Error querying KG endpoint:", error) 61 | return NextResponse.json({ 62 | error: "Failed to fetch from KG endpoint", 63 | details: error instanceof Error ? error.message : String(error) 64 | }, { status: 500 }) 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /app/api/query-vector-only/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest, NextResponse } from "next/server" 2 | 3 | /** 4 | * Vector-Only Search API Route Handler 5 | * 6 | * This API route proxies user queries to a backend vector search service and returns the results. 7 | * It handles parameter validation, error handling, and response formatting. 8 | * 9 | * Endpoint: GET /api/query-vector-only?query= 10 | * 11 | * @param request - The incoming NextRequest object containing query parameters 12 | * @returns NextResponse with either the processed results or an appropriate error message 13 | */ 14 | export async function GET(request: NextRequest) { 15 | const searchParams = request.nextUrl.searchParams 16 | const query = searchParams.get("query") 17 | 18 | if (!query) { 19 | return NextResponse.json({ error: "Query parameter is required" }, { status: 400 }) 20 | } 21 | 22 | try { 23 | /** 24 | * The base URL for the backend API 25 | * Falls back to localhost:8000 if the environment variable is not set 26 | */ 27 | const apiUrl = process.env.API_BASE_URL || "http://localhost:8000" 28 | 29 | console.log(`Calling vector-only API with query: ${query}`) 30 | 31 | const response = await fetch(`${apiUrl}/query-with-vector-only?query=${encodeURIComponent(query)}`, { 32 | method: 'GET', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | 'Cache-Control': 'no-cache', 36 | }, 37 | }) 38 | 39 | if (!response.ok) { 40 | throw new Error(`Backend API responded with status: ${response.status}`) 41 | } 42 | 43 | const text = await response.text() 44 | console.log("Raw vector-only response:", text.substring(0, 200) + "...") 45 | 46 | try { 47 | const data = JSON.parse(text) 48 | console.log("Parsed vector-only data (summary):", data.summary ? data.summary.substring(0, 100) + "..." : "No summary") 49 | 50 | return NextResponse.json(data, { 51 | headers: { 52 | 'Cache-Control': 'no-store, max-age=0', 53 | } 54 | }) 55 | } catch (parseError) { 56 | console.error("JSON parse error:", parseError) 57 | return NextResponse.json({ error: "Failed to parse JSON response from backend" }, { status: 500 }) 58 | } 59 | } catch (error) { 60 | console.error("Error querying vector-only endpoint:", error) 61 | return NextResponse.json({ 62 | error: "Failed to fetch from vector-only endpoint", 63 | details: error instanceof Error ? error.message : String(error) 64 | }, { status: 500 }) 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptechday/openai-academy-rag-comparison-demo/ca0d6b96c30d9b15e61881b60349d3802718470f/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --card: oklch(1 0 0); 49 | --card-foreground: oklch(0.145 0 0); 50 | --popover: oklch(1 0 0); 51 | --popover-foreground: oklch(0.145 0 0); 52 | --primary: oklch(0.205 0 0); 53 | --primary-foreground: oklch(0.985 0 0); 54 | --secondary: oklch(0.97 0 0); 55 | --secondary-foreground: oklch(0.205 0 0); 56 | --muted: oklch(0.97 0 0); 57 | --muted-foreground: oklch(0.556 0 0); 58 | --accent: oklch(0.97 0 0); 59 | --accent-foreground: oklch(0.205 0 0); 60 | --destructive: oklch(0.577 0.245 27.325); 61 | --border: oklch(0.922 0 0); 62 | --input: oklch(0.922 0 0); 63 | --ring: oklch(0.708 0 0); 64 | --chart-1: oklch(0.646 0.222 41.116); 65 | --chart-2: oklch(0.6 0.118 184.704); 66 | --chart-3: oklch(0.398 0.07 227.392); 67 | --chart-4: oklch(0.828 0.189 84.429); 68 | --chart-5: oklch(0.769 0.188 70.08); 69 | --sidebar: oklch(0.985 0 0); 70 | --sidebar-foreground: oklch(0.145 0 0); 71 | --sidebar-primary: oklch(0.205 0 0); 72 | --sidebar-primary-foreground: oklch(0.985 0 0); 73 | --sidebar-accent: oklch(0.97 0 0); 74 | --sidebar-accent-foreground: oklch(0.205 0 0); 75 | --sidebar-border: oklch(0.922 0 0); 76 | --sidebar-ring: oklch(0.708 0 0); 77 | --background: oklch(1 0 0); 78 | --foreground: oklch(0.145 0 0); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.145 0 0); 83 | --foreground: oklch(0.985 0 0); 84 | --card: oklch(0.205 0 0); 85 | --card-foreground: oklch(0.985 0 0); 86 | --popover: oklch(0.205 0 0); 87 | --popover-foreground: oklch(0.985 0 0); 88 | --primary: oklch(0.922 0 0); 89 | --primary-foreground: oklch(0.205 0 0); 90 | --secondary: oklch(0.269 0 0); 91 | --secondary-foreground: oklch(0.985 0 0); 92 | --muted: oklch(0.269 0 0); 93 | --muted-foreground: oklch(0.708 0 0); 94 | --accent: oklch(0.269 0 0); 95 | --accent-foreground: oklch(0.985 0 0); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.556 0 0); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.205 0 0); 106 | --sidebar-foreground: oklch(0.985 0 0); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.985 0 0); 109 | --sidebar-accent: oklch(0.269 0 0); 110 | --sidebar-accent-foreground: oklch(0.985 0 0); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.556 0 0); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "@/components/ui/toaster"; 5 | import { ToastProvider } from "@/hooks/use-toast"; 6 | 7 | /** 8 | * Root Layout Component 9 | * 10 | * This component serves as the application's root layout and provides: 11 | * - Font configuration using Geist Sans and Geist Mono fonts 12 | * - Application metadata for SEO and browser display 13 | * - Toast notification system setup 14 | * - Base HTML structure with language configuration 15 | * 16 | * All pages in the application inherit from this layout to maintain consistent styling 17 | * and functionality throughout the application. 18 | */ 19 | 20 | /** 21 | * Configure Geist Sans as the primary font 22 | * Sets CSS variable --font-geist-sans for use throughout the app 23 | */ 24 | const geistSans = Geist({ 25 | variable: "--font-geist-sans", 26 | subsets: ["latin"], 27 | }); 28 | 29 | /** 30 | * Configure Geist Mono as the monospace font 31 | * Sets CSS variable --font-geist-mono for code blocks and other monospaced text 32 | */ 33 | const geistMono = Geist_Mono({ 34 | variable: "--font-geist-mono", 35 | subsets: ["latin"], 36 | }); 37 | 38 | /** 39 | * Application metadata for SEO and browser display 40 | */ 41 | export const metadata: Metadata = { 42 | title: "Vector Search vs Knowledge Graph Demo", 43 | description: "A demonstration comparing vector search and knowledge graph approaches for information retrieval", 44 | }; 45 | 46 | export default function RootLayout({ 47 | children, 48 | }: Readonly<{ 49 | children: React.ReactNode; 50 | }>) { 51 | return ( 52 | 53 | 56 | 57 | {children} 58 | 59 | 60 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import ChatInterface from "@/components/chat-interface" 2 | import ResponseComparison from "@/components/response-comparison" 3 | import { ArrowRight } from "lucide-react" 4 | import Image from "next/image" 5 | 6 | /** 7 | * Home Page Component 8 | * 9 | * Root page component that renders the main application layout with the following sections: 10 | * - Header with application branding and GitHub link 11 | * - Main content area displaying the response comparison component 12 | * - Footer containing the chat interface for user input 13 | * 14 | * The application demonstrates the difference between vector search and knowledge graph 15 | * approaches for retrieving and processing information. 16 | */ 17 | export default function Home() { 18 | return ( 19 |
20 |
21 |
22 | OpenAI Logo 29 |

Open AI Academy Vector Search vs Knowledge Graph Demo!

30 |
31 |
32 | 38 | 39 | View this demo on the GPTechday GitHub! 40 | 41 |
42 |
43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 |
51 |
52 | ) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/chat-interface.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import type React from "react" 4 | 5 | import { useState, useRef, useEffect } from "react" 6 | import { Button } from "@/components/ui/button" 7 | import { Textarea } from "@/components/ui/textarea" 8 | import { Send, ArrowUp, ArrowDown } from "lucide-react" 9 | import { useToast } from "@/hooks/use-toast" 10 | import { LoadingSpinner } from "@/components/ui/loading-spinner" 11 | 12 | /** 13 | * ChatInterface Component 14 | * 15 | * A client-side component that provides a user interface for submitting queries and managing query history. 16 | * Features include: 17 | * - Text input with submit button 18 | * - Query history tracking and browsing 19 | * - Keyboard shortcuts for submission and history navigation 20 | * - Loading state management 21 | * - Dispatch of query events to other components 22 | * 23 | * This component does not directly fetch or display query results - it emits events that are 24 | * handled by the ResponseComparison component. 25 | */ 26 | 27 | /** 28 | * Represents a single item in the query history 29 | */ 30 | type QueryHistoryItem = { 31 | id: string 32 | text: string 33 | timestamp: Date 34 | } 35 | 36 | /** 37 | * Main ChatInterface component that handles user input and query history management 38 | * @returns A form with textarea input, submit button, and query history buttons 39 | */ 40 | export default function ChatInterface() { 41 | const [query, setQuery] = useState("") 42 | const [isLoading, setIsLoading] = useState(false) 43 | const [queryHistory, setQueryHistory] = useState([]) 44 | const [historyIndex, setHistoryIndex] = useState(-1) 45 | const textareaRef = useRef(null) 46 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 47 | const { toast } = useToast() as any 48 | 49 | /** 50 | * Effect hook to automatically focus the textarea when the component mounts 51 | */ 52 | useEffect(() => { 53 | if (textareaRef.current) { 54 | textareaRef.current.focus() 55 | } 56 | }, []) 57 | 58 | /** 59 | * Handles form submission by processing the current query and dispatching an event 60 | * @param e - The form submission event 61 | */ 62 | const handleSubmit = async (e: React.FormEvent) => { 63 | e.preventDefault() 64 | 65 | if (!query.trim()) return 66 | 67 | try { 68 | setIsLoading(true) 69 | 70 | // Add to history 71 | const newQueryItem = { 72 | id: Date.now().toString(), 73 | text: query.trim(), 74 | timestamp: new Date(), 75 | } 76 | 77 | setQueryHistory((prev) => [newQueryItem, ...prev.slice(0, 19)]) // Keep last 20 queries 78 | setHistoryIndex(-1) 79 | 80 | // Create a custom event to send the query to the response comparison component 81 | const event = new CustomEvent("newQuery", { 82 | detail: { query: query.trim() }, 83 | }) 84 | window.dispatchEvent(event) 85 | 86 | // Clear the input after sending 87 | setQuery("") 88 | } catch (error) { 89 | console.error("Error sending query:", error) 90 | toast({ 91 | title: "Error", 92 | description: "Failed to send query. Please try again.", 93 | variant: "destructive", 94 | }) 95 | } finally { 96 | setIsLoading(false) 97 | } 98 | } 99 | 100 | /** 101 | * Navigates through query history in the specified direction 102 | * @param direction - Either "up" (older) or "down" (newer) in the history 103 | */ 104 | const navigateHistory = (direction: "up" | "down") => { 105 | if (queryHistory.length === 0) return 106 | 107 | if (direction === "up") { 108 | const newIndex = historyIndex < queryHistory.length - 1 ? historyIndex + 1 : historyIndex 109 | setHistoryIndex(newIndex) 110 | if (newIndex >= 0 && newIndex < queryHistory.length) { 111 | setQuery(queryHistory[newIndex].text) 112 | } 113 | } else { 114 | const newIndex = historyIndex > 0 ? historyIndex - 1 : -1 115 | setHistoryIndex(newIndex) 116 | if (newIndex >= 0) { 117 | setQuery(queryHistory[newIndex].text) 118 | } else { 119 | setQuery("") 120 | } 121 | } 122 | } 123 | 124 | /** 125 | * Handles keyboard events for form submission and history navigation 126 | * @param e - The keyboard event object 127 | */ 128 | const handleKeyDown = (e: React.KeyboardEvent) => { 129 | // Submit on Enter (without shift) 130 | if (e.key === "Enter" && !e.shiftKey) { 131 | e.preventDefault() 132 | handleSubmit(e) 133 | } 134 | 135 | // Navigate history with up/down arrows 136 | if (e.key === "ArrowUp" && !e.shiftKey && query === "") { 137 | e.preventDefault() 138 | navigateHistory("up") 139 | } else if (e.key === "ArrowDown" && !e.shiftKey) { 140 | e.preventDefault() 141 | navigateHistory("down") 142 | } 143 | } 144 | 145 | return ( 146 |
147 |
148 |
149 |