diff --git a/.env.example b/.env.example index ee2a838c..681280f2 100644 --- a/.env.example +++ b/.env.example @@ -1,42 +1,60 @@ # Ingestion Configuration -# Set to true to disable Langflow ingestion and use traditional OpenRAG processor -# If unset or false, Langflow pipeline will be used (default: upload -> ingest -> delete) +# Set to true to disable Langflow ingestion and use the traditional OpenRAG processor. +# If unset or false, the Langflow pipeline is used (default: upload -> ingest -> delete). DISABLE_INGEST_WITH_LANGFLOW=false -# make one like so https://docs.langflow.org/api-keys-and-authentication#langflow-secret-key + +# Create a Langflow secret key: +# https://docs.langflow.org/api-keys-and-authentication#langflow-secret-key LANGFLOW_SECRET_KEY= -# flow ids for chat and ingestion flows + +# Flow IDs for chat and ingestion LANGFLOW_CHAT_FLOW_ID=1098eea1-6649-4e1d-aed1-b77249fb8dd0 LANGFLOW_INGEST_FLOW_ID=5488df7c-b93f-4f87-a446-b67028bc0813 -# Ingest flow using docling +# Ingest flow using Docling # LANGFLOW_INGEST_FLOW_ID=1402618b-e6d1-4ff2-9a11-d6ce71186915 NUDGES_FLOW_ID=ebc01d31-1976-46ce-a385-b0240327226c -# Set a strong admin password for OpenSearch; a bcrypt hash is generated at -# container startup from this value. Do not commit real secrets. -# must match the hashed password in secureconfig, must change for secure deployment!!! + +# OpenSearch Auth +# Set a strong admin password for OpenSearch. +# A bcrypt hash is generated at container startup from this value. +# Do not commit real secrets. +# Must be changed for secure deployments. OPENSEARCH_PASSWORD= -# make here https://console.cloud.google.com/apis/credentials + +# Google OAuth +# Create credentials here: +# https://console.cloud.google.com/apis/credentials GOOGLE_OAUTH_CLIENT_ID= GOOGLE_OAUTH_CLIENT_SECRET= -# Azure app registration credentials for SharePoint/OneDrive + +# Microsoft (SharePoint/OneDrive) OAuth +# Azure app registration credentials. MICROSOFT_GRAPH_OAUTH_CLIENT_ID= MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET= -# OPTIONAL: dns routable from google (etc.) to handle continous ingest (something like ngrok works). This enables continous ingestion + +# Webhooks (optional) +# Public, DNS-resolvable base URL (e.g., via ngrok) for continuous ingestion. WEBHOOK_BASE_URL= + +# API Keys OPENAI_API_KEY= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= -# OPTIONAL url for openrag link to langflow in the UI + +# Langflow UI URL (optional) +# Public URL to link OpenRAG to Langflow in the UI. LANGFLOW_PUBLIC_URL= -# Langflow auth + +# Langflow Auth LANGFLOW_AUTO_LOGIN=False LANGFLOW_SUPERUSER= LANGFLOW_SUPERUSER_PASSWORD= diff --git a/frontend/components/knowledge-filter-list.tsx b/frontend/components/knowledge-filter-list.tsx index 129e8204..8e62058f 100644 --- a/frontend/components/knowledge-filter-list.tsx +++ b/frontend/components/knowledge-filter-list.tsx @@ -1,8 +1,7 @@ "use client"; import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Loader2, Plus } from "lucide-react"; +import { Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import { useGetFiltersSearchQuery, @@ -65,97 +64,102 @@ export function KnowledgeFilterList({ }; return ( - <> -
-
-
- Knowledge Filters -
- -
- {loading ? ( -
- - - Loading... - -
- ) : filters.length === 0 ? ( -
- {searchQuery ? "No filters found" : "No saved filters"} -
- ) : ( - filters.map((filter) => ( -
handleFilterSelect(filter)} - className={cn( - "flex items-center gap-3 px-3 py-2 w-full rounded-lg hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors", - selectedFilter?.id === filter.id && - "active bg-accent text-accent-foreground" - )} +
+
+
+
+

+ Knowledge Filters +

+ +
+
+ {loading ? ( +
+ Loading...
-
- )) - )} + ) : filters.length === 0 ? ( +
+ {searchQuery ? "No filters found" : "No saved filters"} +
+ ) : ( + filters.map(filter => ( +
handleFilterSelect(filter)} + className={cn( + "flex items-center gap-3 px-3 py-2 w-full rounded-lg hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors", + selectedFilter?.id === filter.id && + "active bg-accent text-accent-foreground" + )} + > +
+
+ {(() => { + const parsed = parseQueryData( + filter.query_data + ) as ParsedQueryData; + const Icon = iconKeyToComponent(parsed.icon); + return ( +
+ {Icon && } +
+ ); + })()} +
+ {filter.name} +
+
+ {filter.description && ( +
+ {filter.description} +
+ )} +
+
+ {new Date(filter.created_at).toLocaleDateString( + undefined, + { + month: "short", + day: "numeric", + year: "numeric", + } + )} +
+ + {(() => { + const dataSources = parseQueryData(filter.query_data) + .filters.data_sources; + if (dataSources[0] === "*") return "All sources"; + const count = dataSources.length; + return `${count} ${ + count === 1 ? "source" : "sources" + }`; + })()} + +
+
+
+ )) + )} +
+
+ {/* Create flow moved to panel create mode */}
- {/* Create flow moved to panel create mode */} - +
); } diff --git a/frontend/components/navigation-layout.tsx b/frontend/components/navigation-layout.tsx index a2fdbe8f..2722c81f 100644 --- a/frontend/components/navigation-layout.tsx +++ b/frontend/components/navigation-layout.tsx @@ -1,7 +1,10 @@ "use client"; import { usePathname } from "next/navigation"; -import { useGetConversationsQuery, type ChatConversation } from "@/app/api/queries/useGetConversationsQuery"; +import { + useGetConversationsQuery, + type ChatConversation, +} from "@/app/api/queries/useGetConversationsQuery"; import { KnowledgeFilterDropdown } from "@/components/knowledge-filter-dropdown"; import { ModeToggle } from "@/components/mode-toggle"; import { Navigation } from "@/components/navigation"; diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx index c193b9f6..423172f5 100644 --- a/frontend/components/navigation.tsx +++ b/frontend/components/navigation.tsx @@ -5,7 +5,6 @@ import { FileText, Library, MessageSquare, - MoreHorizontal, Plus, Settings2, Trash2, @@ -111,7 +110,7 @@ export function Navigation({ ) { // Filter out the deleted conversation and find the next one const remainingConversations = conversations.filter( - (conv) => conv.response_id !== conversationToDelete.response_id, + conv => conv.response_id !== conversationToDelete.response_id ); if (remainingConversations.length > 0) { @@ -132,7 +131,7 @@ export function Navigation({ setDeleteModalOpen(false); setConversationToDelete(null); }, - onError: (error) => { + onError: error => { toast.error(`Failed to delete conversation: ${error.message}`); }, }); @@ -164,7 +163,7 @@ export function Navigation({ window.dispatchEvent( new CustomEvent("fileUploadStart", { detail: { filename: file.name }, - }), + }) ); try { @@ -188,7 +187,7 @@ export function Navigation({ filename: file.name, error: "Failed to process document", }, - }), + }) ); // Trigger loading end event @@ -208,7 +207,7 @@ export function Navigation({ window.dispatchEvent( new CustomEvent("fileUploaded", { detail: { file, result }, - }), + }) ); // Trigger loading end event @@ -222,7 +221,7 @@ export function Navigation({ window.dispatchEvent( new CustomEvent("fileUploadError", { detail: { filename: file.name, error: "Failed to process document" }, - }), + }) ); } }; @@ -244,7 +243,7 @@ export function Navigation({ const handleDeleteConversation = ( conversation: ChatConversation, - event?: React.MouseEvent, + event?: React.MouseEvent ) => { if (event) { event.preventDefault(); @@ -256,7 +255,7 @@ export function Navigation({ const handleContextMenuAction = ( action: string, - conversation: ChatConversation, + conversation: ChatConversation ) => { switch (action) { case "delete": @@ -332,33 +331,33 @@ export function Navigation({ return (
-
+
- {routes.map((route) => ( + {routes.map(route => (
{route.label}
{route.label === "Settings" && ( -
+
)}
))} @@ -374,11 +373,11 @@ export function Navigation({ {/* Chat Page Specific Sections */} {isOnChatPage && ( -
+
{/* Conversations Section */} -
-
-

+
+
+

Conversations

-
+
{/* Conversations List - grows naturally, doesn't fill all space */}
{loadingNewConversation || isConversationsLoading ? ( -
+
Loading...
) : ( @@ -406,7 +405,7 @@ export function Navigation({ {placeholderConversation && (
- +
{ + onClick={e => { e.stopPropagation(); }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + onKeyDown={e => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); } @@ -479,14 +483,14 @@ export function Navigation({ side="bottom" align="end" className="w-48" - onClick={(e) => e.stopPropagation()} + onClick={e => e.stopPropagation()} > { + onClick={e => { e.stopPropagation(); handleContextMenuAction( "delete", - conversation, + conversation ); }} className="cursor-pointer text-destructive focus:text-destructive" @@ -506,8 +510,8 @@ export function Navigation({ {/* Conversation Knowledge Section - appears right after last conversation */}
-
-

+
+

Conversation knowledge

+ {/* Separator */} -
+
-
+
{ + const { theme, setTheme } = useTheme(); + const [selectedTheme, setSelectedTheme] = useState("dark"); + + // Sync local state with theme context + useEffect(() => { + if (theme) { + setSelectedTheme(theme); + } + }, [theme]); + + const handleThemeChange = (newTheme: string) => { + setSelectedTheme(newTheme); + setTheme(newTheme); + }; + + return ( +
+ {/* Light Theme Button */} + + + {/* Dark Theme Button */} + + + {/* System Theme Button */} + +
+ ); +}; + +export default ThemeButtons; diff --git a/frontend/src/components/user-nav.tsx b/frontend/src/components/user-nav.tsx index 85ec5ddd..a3a6054e 100644 --- a/frontend/src/components/user-nav.tsx +++ b/frontend/src/components/user-nav.tsx @@ -1,94 +1,100 @@ -"use client" +"use client"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Button } from "@/components/ui/button" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuContent, - DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { useAuth } from "@/contexts/auth-context" -import { LogIn, LogOut, User, Moon, Sun, ChevronsUpDown } from "lucide-react" -import { useTheme } from "next-themes" +} from "@/components/ui/dropdown-menu"; +import { useAuth } from "@/contexts/auth-context"; +import { LogOut, User, Moon, Sun, ChevronsUpDown } from "lucide-react"; +import { useTheme } from "next-themes"; +import ThemeButtons from "./ui/buttonTheme"; export function UserNav() { - const { user, isLoading, isAuthenticated, isNoAuthMode, login, logout } = useAuth() - const { theme, setTheme } = useTheme() + const { user, isLoading, isAuthenticated, isNoAuthMode, login, logout } = + useAuth(); + const { theme, setTheme } = useTheme(); if (isLoading) { - return ( -
- ) + return
; } // In no-auth mode, show a simple theme switcher instead of auth UI if (isNoAuthMode) { return ( - - ) + {theme === "dark" ? ( + + ) : ( + + )} + + ); } if (!isAuthenticated) { return ( - - ) + + ); } return ( - + + - + -
+

{user?.name}

{user?.email}

- - setTheme(theme === "light" ? "dark" : "light")}> - {theme === "light" ? ( - - ) : ( - - )} - Toggle Theme - - - - - Log out - + +
+ Theme + +
+ + - ) -} \ No newline at end of file + ); +} diff --git a/frontend/src/contexts/chat-context.tsx b/frontend/src/contexts/chat-context.tsx index db79e0d3..f41cd5d9 100644 --- a/frontend/src/contexts/chat-context.tsx +++ b/frontend/src/contexts/chat-context.tsx @@ -96,7 +96,7 @@ export function ChatProvider({ children }: ChatProviderProps) { const refreshConversations = useCallback((force = false) => { if (force) { // Immediate refresh for important updates like new conversations - setRefreshTrigger((prev) => prev + 1); + setRefreshTrigger(prev => prev + 1); return; } @@ -107,7 +107,7 @@ export function ChatProvider({ children }: ChatProviderProps) { // Set a new timeout to debounce multiple rapid refresh calls refreshTimeoutRef.current = setTimeout(() => { - setRefreshTrigger((prev) => prev + 1); + setRefreshTrigger(prev => prev + 1); }, 250); // 250ms debounce }, []); @@ -123,7 +123,7 @@ export function ChatProvider({ children }: ChatProviderProps) { // Silent refresh - updates data without loading states const refreshConversationsSilent = useCallback(async () => { // Trigger silent refresh that updates conversation data without showing loading states - setRefreshTriggerSilent((prev) => prev + 1); + setRefreshTriggerSilent(prev => prev + 1); }, []); const loadConversation = useCallback((conversation: ConversationData) => { @@ -164,7 +164,7 @@ export function ChatProvider({ children }: ChatProviderProps) { }, [endpoint, refreshConversations]); const addConversationDoc = useCallback((filename: string) => { - setConversationDocs((prev) => [ + setConversationDocs(prev => [ ...prev, { filename, uploadTime: new Date() }, ]); @@ -180,7 +180,7 @@ export function ChatProvider({ children }: ChatProviderProps) { setCurrentConversationId(null); // Clear current conversation to indicate new conversation setConversationData(null); // Clear conversation data to prevent reloading // Set the response ID that we're forking from as the previous response ID - setPreviousResponseIds((prev) => ({ + setPreviousResponseIds(prev => ({ ...prev, [endpoint]: responseId, }));