Refactor KnowledgeFilterList component layout and improve loading states. Update Navigation component styles for consistency. Adjust LayoutWrapper sidebar positioning. Fix UserNav conditional rendering logic. Clean up ChatProvider state update functions for clarity.

This commit is contained in:
Deon Sanchez 2025-10-01 14:26:08 -06:00
parent 84e3703832
commit b27ec87fe9
5 changed files with 109 additions and 104 deletions

View file

@ -1,8 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react";
import { Loader2, Plus } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
useGetFiltersSearchQuery, useGetFiltersSearchQuery,
@ -65,95 +64,102 @@ export function KnowledgeFilterList({
}; };
return ( return (
<> <div className="flex-1 min-h-0 flex flex-col">
<div className="flex flex-col gap-1 px-3 !mb-12 mt-0 h-full overflow-y-auto"> <div className="px-3 flex-1 min-h-0 flex flex-col">
<div className="flex items-center w-full justify-between pl-3"> <div className="flex-shrink-0">
<div className="text-sm font-medium text-muted-foreground"> <div className="flex items-center justify-between mb-3 ml-3 mr-2">
Knowledge Filters <h3 className="text-xs font-medium text-muted-foreground">
</div> Knowledge Filters
<Button </h3>
variant="ghost" <button
size="sm" type="button"
onClick={handleCreateNew} className="p-1 hover:bg-accent rounded"
title="Create New Filter" onClick={handleCreateNew}
className="h-8 px-3 text-muted-foreground" title="Create New Filter"
>
<Plus className="h-3 w-3" />
</Button>
</div>
{loading ? (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="ml-2 text-sm text-muted-foreground">
Loading...
</span>
</div>
) : filters.length === 0 ? (
<div className="py-2 px-4 text-sm text-muted-foreground">
{searchQuery ? "No filters found" : "No saved filters"}
</div>
) : (
filters.map((filter) => (
<div
key={filter.id}
onClick={() => 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"
)}
> >
<div className="flex flex-col gap-1 flex-1 min-w-0"> <Plus className="h-4 w-4 text-muted-foreground" />
<div className="flex items-center gap-2"> </button>
{(() => { </div>
const parsed = parseQueryData(filter.query_data) as ParsedQueryData; <div className="overflow-y-auto scrollbar-hide space-y-1">
const Icon = iconKeyToComponent(parsed.icon); {loading ? (
return ( <div className="text-[13px] text-muted-foreground p-2 ml-1">
<div Loading...
className={cn(
"flex items-center justify-center w-5 h-5 rounded transition-colors",
filterAccentClasses[parsed.color],
parsed.color === "zinc" &&
"group-hover:bg-background group-[.active]:bg-background"
)}
>
{Icon && <Icon className="h-3 w-3" />}
</div>
);
})()}
<div className="text-sm font-medium truncate group-hover:text-accent-foreground">
{filter.name}
</div>
</div>
{filter.description && (
<div className="text-xs text-muted-foreground line-clamp-2">
{filter.description}
</div>
)}
<div className="flex items-center gap-2">
<div className="text-xs text-muted-foreground">
{new Date(filter.created_at).toLocaleDateString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
})}
</div>
<span className="text-xs bg-muted text-muted-foreground px-1 py-0.5 rounded-sm group-hover:bg-background group-[.active]:bg-background transition-colors">
{(() => {
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"}`;
})()}
</span>
</div>
</div> </div>
</div> ) : filters.length === 0 ? (
)) <div className="text-[13px] text-muted-foreground p-2 ml-1">
)} {searchQuery ? "No filters found" : "No saved filters"}
</div>
) : (
filters.map(filter => (
<div
key={filter.id}
onClick={() => 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"
)}
>
<div className="flex flex-col gap-1 flex-1 min-w-0">
<div className="flex items-center gap-2">
{(() => {
const parsed = parseQueryData(
filter.query_data
) as ParsedQueryData;
const Icon = iconKeyToComponent(parsed.icon);
return (
<div
className={cn(
"flex items-center justify-center w-5 h-5 rounded transition-colors",
filterAccentClasses[parsed.color],
parsed.color === "zinc" &&
"group-hover:bg-background group-[.active]:bg-background"
)}
>
{Icon && <Icon className="h-3 w-3" />}
</div>
);
})()}
<div className="text-sm font-medium truncate group-hover:text-accent-foreground">
{filter.name}
</div>
</div>
{filter.description && (
<div className="text-xs text-muted-foreground line-clamp-2">
{filter.description}
</div>
)}
<div className="flex items-center gap-2">
<div className="text-xs text-muted-foreground">
{new Date(filter.created_at).toLocaleDateString(
undefined,
{
month: "short",
day: "numeric",
year: "numeric",
}
)}
</div>
<span className="text-xs bg-muted text-muted-foreground px-1 py-0.5 rounded-sm group-hover:bg-background group-[.active]:bg-background transition-colors">
{(() => {
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"
}`;
})()}
</span>
</div>
</div>
</div>
))
)}
</div>
</div>
{/* Create flow moved to panel create mode */}
</div> </div>
{/* Create flow moved to panel create mode */} </div>
</>
); );
} }

View file

@ -5,7 +5,6 @@ import {
FileText, FileText,
Library, Library,
MessageSquare, MessageSquare,
MoreHorizontal,
Plus, Plus,
Settings2, Settings2,
Trash2, Trash2,
@ -397,7 +396,7 @@ export function Navigation({
{/* Conversations List - grows naturally, doesn't fill all space */} {/* Conversations List - grows naturally, doesn't fill all space */}
<div className="flex-shrink-0 overflow-y-auto scrollbar-hide space-y-1 max-h-full"> <div className="flex-shrink-0 overflow-y-auto scrollbar-hide space-y-1 max-h-full">
{loadingNewConversation || isConversationsLoading ? ( {loadingNewConversation || isConversationsLoading ? (
<div className="text-sm text-muted-foreground p-2"> <div className="text-[13px] text-muted-foreground p-2">
Loading... Loading...
</div> </div>
) : ( ) : (
@ -406,7 +405,7 @@ export function Navigation({
{placeholderConversation && ( {placeholderConversation && (
<button <button
type="button" type="button"
className="w-full p-2 rounded-lg bg-accent/50 border border-dashed border-accent cursor-pointer group text-left" className="w-full px-3 rounded-lg bg-accent border border-dashed border-accent cursor-pointer group text-left h-[44px]"
onClick={() => { onClick={() => {
// Don't load placeholder as a real conversation, just focus the input // Don't load placeholder as a real conversation, just focus the input
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@ -414,7 +413,7 @@ export function Navigation({
} }
}} }}
> >
<div className="text-sm font-medium text-foreground mb-1 truncate"> <div className="text-[13px] font-medium text-foreground truncate">
{placeholderConversation.title} {placeholderConversation.title}
</div> </div>
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
@ -425,7 +424,7 @@ export function Navigation({
{/* Show regular conversations */} {/* Show regular conversations */}
{conversations.length === 0 && !placeholderConversation ? ( {conversations.length === 0 && !placeholderConversation ? (
<div className="text-[13px] text-muted-foreground p-2"> <div className="text-[13px] text-muted-foreground p-2 ml-1">
No conversations yet No conversations yet
</div> </div>
) : ( ) : (
@ -531,16 +530,16 @@ export function Navigation({
className="hidden" className="hidden"
accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt" accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt"
/> />
<div className="overflow-y-auto scrollbar-hide space-y-1 max-h-40 ml-1"> <div className="overflow-y-auto scrollbar-hide space-y-1">
{conversationDocs.length === 0 ? ( {conversationDocs.length === 0 ? (
<div className="text-[13px] text-muted-foreground p-2"> <div className="text-[13px] text-muted-foreground p-2 ml-1">
No documents yet No documents yet
</div> </div>
) : ( ) : (
conversationDocs.map(doc => ( conversationDocs.map(doc => (
<div <div
key={`${doc.filename}-${doc.uploadTime.getTime()}`} key={`${doc.filename}-${doc.uploadTime.getTime()}`}
className="p-2 rounded-lg hover:bg-accent cursor-pointer group flex items-center" className="w-full px-3 pr-2 h-11 rounded-lg hover:bg-accent cursor-pointer group flex items-center"
> >
<FileText className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" /> <FileText className="h-4 w-4 mr-2 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">

View file

@ -119,7 +119,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
</div> </div>
</div> </div>
</header> </header>
<div className="side-bar-arrangement bg-background fixed left-0 top-[53px] bottom-0 md:flex hidden"> <div className="side-bar-arrangement bg-background fixed left-0 top-[40px] bottom-0 md:flex hidden pt-1">
<Navigation <Navigation
conversations={conversations} conversations={conversations}
isConversationsLoading={isConversationsLoading} isConversationsLoading={isConversationsLoading}

View file

@ -23,7 +23,7 @@ export function UserNav() {
} }
// In no-auth mode, show a simple theme switcher instead of auth UI // In no-auth mode, show a simple theme switcher instead of auth UI
if (!isNoAuthMode) { if (isNoAuthMode) {
return ( return (
<button <button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")} onClick={() => setTheme(theme === "dark" ? "light" : "dark")}

View file

@ -96,7 +96,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
const refreshConversations = useCallback((force = false) => { const refreshConversations = useCallback((force = false) => {
if (force) { if (force) {
// Immediate refresh for important updates like new conversations // Immediate refresh for important updates like new conversations
setRefreshTrigger((prev) => prev + 1); setRefreshTrigger(prev => prev + 1);
return; return;
} }
@ -107,7 +107,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
// Set a new timeout to debounce multiple rapid refresh calls // Set a new timeout to debounce multiple rapid refresh calls
refreshTimeoutRef.current = setTimeout(() => { refreshTimeoutRef.current = setTimeout(() => {
setRefreshTrigger((prev) => prev + 1); setRefreshTrigger(prev => prev + 1);
}, 250); // 250ms debounce }, 250); // 250ms debounce
}, []); }, []);
@ -123,7 +123,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
// Silent refresh - updates data without loading states // Silent refresh - updates data without loading states
const refreshConversationsSilent = useCallback(async () => { const refreshConversationsSilent = useCallback(async () => {
// Trigger silent refresh that updates conversation data without showing loading states // Trigger silent refresh that updates conversation data without showing loading states
setRefreshTriggerSilent((prev) => prev + 1); setRefreshTriggerSilent(prev => prev + 1);
}, []); }, []);
const loadConversation = useCallback((conversation: ConversationData) => { const loadConversation = useCallback((conversation: ConversationData) => {
@ -164,7 +164,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
}, [endpoint, refreshConversations]); }, [endpoint, refreshConversations]);
const addConversationDoc = useCallback((filename: string) => { const addConversationDoc = useCallback((filename: string) => {
setConversationDocs((prev) => [ setConversationDocs(prev => [
...prev, ...prev,
{ filename, uploadTime: new Date() }, { filename, uploadTime: new Date() },
]); ]);
@ -180,7 +180,7 @@ export function ChatProvider({ children }: ChatProviderProps) {
setCurrentConversationId(null); // Clear current conversation to indicate new conversation setCurrentConversationId(null); // Clear current conversation to indicate new conversation
setConversationData(null); // Clear conversation data to prevent reloading setConversationData(null); // Clear conversation data to prevent reloading
// Set the response ID that we're forking from as the previous response ID // Set the response ID that we're forking from as the previous response ID
setPreviousResponseIds((prev) => ({ setPreviousResponseIds(prev => ({
...prev, ...prev,
[endpoint]: responseId, [endpoint]: responseId,
})); }));