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";
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,95 +64,102 @@ export function KnowledgeFilterList({
};
return (
<>
<div className="flex flex-col gap-1 px-3 !mb-12 mt-0 h-full overflow-y-auto">
<div className="flex items-center w-full justify-between pl-3">
<div className="text-sm font-medium text-muted-foreground">
Knowledge Filters
</div>
<Button
variant="ghost"
size="sm"
onClick={handleCreateNew}
title="Create New Filter"
className="h-8 px-3 text-muted-foreground"
>
<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-1 min-h-0 flex flex-col">
<div className="px-3 flex-1 min-h-0 flex flex-col">
<div className="flex-shrink-0">
<div className="flex items-center justify-between mb-3 ml-3 mr-2">
<h3 className="text-xs font-medium text-muted-foreground">
Knowledge Filters
</h3>
<button
type="button"
className="p-1 hover:bg-accent rounded"
onClick={handleCreateNew}
title="Create New Filter"
>
<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>
<Plus className="h-4 w-4 text-muted-foreground" />
</button>
</div>
<div className="overflow-y-auto scrollbar-hide space-y-1">
{loading ? (
<div className="text-[13px] text-muted-foreground p-2 ml-1">
Loading...
</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>
{/* Create flow moved to panel create mode */}
</>
</div>
);
}

View file

@ -5,7 +5,6 @@ import {
FileText,
Library,
MessageSquare,
MoreHorizontal,
Plus,
Settings2,
Trash2,
@ -397,7 +396,7 @@ export function Navigation({
{/* 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">
{loadingNewConversation || isConversationsLoading ? (
<div className="text-sm text-muted-foreground p-2">
<div className="text-[13px] text-muted-foreground p-2">
Loading...
</div>
) : (
@ -406,7 +405,7 @@ export function Navigation({
{placeholderConversation && (
<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={() => {
// Don't load placeholder as a real conversation, just focus the input
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}
</div>
<div className="text-xs text-muted-foreground">
@ -425,7 +424,7 @@ export function Navigation({
{/* Show regular conversations */}
{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
</div>
) : (
@ -531,16 +530,16 @@ export function Navigation({
className="hidden"
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 ? (
<div className="text-[13px] text-muted-foreground p-2">
<div className="text-[13px] text-muted-foreground p-2 ml-1">
No documents yet
</div>
) : (
conversationDocs.map(doc => (
<div
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" />
<div className="flex-1 min-w-0">

View file

@ -119,7 +119,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
</div>
</div>
</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
conversations={conversations}
isConversationsLoading={isConversationsLoading}

View file

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

View file

@ -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,
}));