start creating a new filters component
This commit is contained in:
parent
b223f183ee
commit
e33adda6fb
7 changed files with 428 additions and 1 deletions
340
frontend/components/knowledge-filter-list.tsx
Normal file
340
frontend/components/knowledge-filter-list.tsx
Normal file
|
|
@ -0,0 +1,340 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
Filter,
|
||||||
|
Search,
|
||||||
|
X,
|
||||||
|
Loader2,
|
||||||
|
Plus,
|
||||||
|
Save,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
useGetFiltersSearchQuery,
|
||||||
|
type KnowledgeFilter,
|
||||||
|
} from "@/src/app/api/queries/useGetFiltersSearchQuery";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
interface ParsedQueryData {
|
||||||
|
query: string;
|
||||||
|
filters: {
|
||||||
|
data_sources: string[];
|
||||||
|
document_types: string[];
|
||||||
|
owners: string[];
|
||||||
|
};
|
||||||
|
limit: number;
|
||||||
|
scoreThreshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KnowledgeFilterListProps {
|
||||||
|
selectedFilter: KnowledgeFilter | null;
|
||||||
|
onFilterSelect: (filter: KnowledgeFilter | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function KnowledgeFilterList({
|
||||||
|
selectedFilter,
|
||||||
|
onFilterSelect,
|
||||||
|
}: KnowledgeFilterListProps) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
|
const [createName, setCreateName] = useState("");
|
||||||
|
const [createDescription, setCreateDescription] = useState("");
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isFetching: loading,
|
||||||
|
refetch,
|
||||||
|
} = useGetFiltersSearchQuery(searchQuery, 20, { enabled: true });
|
||||||
|
const filters: KnowledgeFilter[] = (data ?? []) as KnowledgeFilter[];
|
||||||
|
|
||||||
|
const deleteFilter = async (filterId: string, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/knowledge-filter/${filterId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// If this was the selected filter, clear selection
|
||||||
|
if (selectedFilter?.id === filterId) {
|
||||||
|
onFilterSelect(null);
|
||||||
|
}
|
||||||
|
// Refresh list
|
||||||
|
refetch();
|
||||||
|
} else {
|
||||||
|
console.error("Failed to delete knowledge filter");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting knowledge filter:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilterSelect = (filter: KnowledgeFilter) => {
|
||||||
|
onFilterSelect(filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearFilter = () => {
|
||||||
|
onFilterSelect(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateNew = () => {
|
||||||
|
setShowCreateModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateFilter = async () => {
|
||||||
|
if (!createName.trim()) return;
|
||||||
|
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
// Create a basic filter with wildcards (match everything by default)
|
||||||
|
const defaultFilterData = {
|
||||||
|
query: "",
|
||||||
|
filters: {
|
||||||
|
data_sources: ["*"],
|
||||||
|
document_types: ["*"],
|
||||||
|
owners: ["*"],
|
||||||
|
},
|
||||||
|
limit: 10,
|
||||||
|
scoreThreshold: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("/api/knowledge-filter", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: createName.trim(),
|
||||||
|
description: createDescription.trim(),
|
||||||
|
queryData: JSON.stringify(defaultFilterData),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
// Create the new filter object
|
||||||
|
const newFilter: KnowledgeFilter = {
|
||||||
|
id: result.filter.id,
|
||||||
|
name: createName.trim(),
|
||||||
|
description: createDescription.trim(),
|
||||||
|
query_data: JSON.stringify(defaultFilterData),
|
||||||
|
owner: result.filter.owner,
|
||||||
|
created_at: result.filter.created_at,
|
||||||
|
updated_at: result.filter.updated_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Select the new filter
|
||||||
|
onFilterSelect(newFilter);
|
||||||
|
|
||||||
|
// Close modal and reset form
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setCreateName("");
|
||||||
|
setCreateDescription("");
|
||||||
|
// Refresh list to include newly created filter
|
||||||
|
refetch();
|
||||||
|
} else {
|
||||||
|
console.error("Failed to create knowledge filter:", result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating knowledge filter:", error);
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelCreate = () => {
|
||||||
|
setShowCreateModal(false);
|
||||||
|
setCreateName("");
|
||||||
|
setCreateDescription("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseQueryData = (queryData: string): ParsedQueryData => {
|
||||||
|
return JSON.parse(queryData) as ParsedQueryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilterSummary = (filter: KnowledgeFilter): string => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(filter.query_data) as ParsedQueryData;
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (parsed.query) parts.push(`"${parsed.query}"`);
|
||||||
|
if (parsed.filters.data_sources.length > 0)
|
||||||
|
parts.push(`${parsed.filters.data_sources.length} sources`);
|
||||||
|
if (parsed.filters.document_types.length > 0)
|
||||||
|
parts.push(`${parsed.filters.document_types.length} types`);
|
||||||
|
if (parsed.filters.owners.length > 0)
|
||||||
|
parts.push(`${parsed.filters.owners.length} owners`);
|
||||||
|
|
||||||
|
return parts.join(" • ") || "No filters";
|
||||||
|
} catch {
|
||||||
|
return "Invalid filter";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col items-center gap-1 px-3">
|
||||||
|
<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}
|
||||||
|
className="h-8 px-3"
|
||||||
|
>
|
||||||
|
<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="p-4 text-center 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 &&
|
||||||
|
"bg-accent text-accent-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Filter className="h-4 w-4" />
|
||||||
|
<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 group-hover:text-accent-foreground/70 line-clamp-2">
|
||||||
|
{filter.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-xs text-muted-foreground group-hover:text-accent-foreground/70">
|
||||||
|
{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">
|
||||||
|
{(() => {
|
||||||
|
const count = parseQueryData(filter.query_data).filters
|
||||||
|
.data_sources.length;
|
||||||
|
return `${count} ${count === 1 ? "source" : "sources"}`;
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => deleteFilter(filter.id, e)}
|
||||||
|
className="opacity-0 group-hover:opacity-100 h-6 w-6 p-0 bg-transparent hover:bg-gray-700 hover:text-white transition-all duration-200 border border-transparent hover:border-gray-600"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3 text-gray-400 hover:text-white" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Create Filter Dialog */}
|
||||||
|
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Knowledge Filter</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Save a reusable filter to quickly scope searches across your
|
||||||
|
knowledge base.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-2 space-y-2">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="filter-name" className="font-medium">
|
||||||
|
Name<span className="text-red-400">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="filter-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter filter name"
|
||||||
|
value={createName}
|
||||||
|
onChange={(e) => setCreateName(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="filter-description" className="font-medium">
|
||||||
|
Description (optional)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="filter-description"
|
||||||
|
placeholder="Brief description of this filter"
|
||||||
|
value={createDescription}
|
||||||
|
onChange={(e) => setCreateDescription(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancelCreate}
|
||||||
|
disabled={creating}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateFilter}
|
||||||
|
disabled={!createName.trim() || creating}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{creating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Create Filter
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { EndpointType } from "@/contexts/chat-context";
|
import { EndpointType } from "@/contexts/chat-context";
|
||||||
import { useLoadingStore } from "@/stores/loadingStore";
|
import { useLoadingStore } from "@/stores/loadingStore";
|
||||||
|
import { KnowledgeFilterList } from "./knowledge-filter-list";
|
||||||
|
import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context";
|
||||||
|
|
||||||
interface RawConversation {
|
interface RawConversation {
|
||||||
response_id: string;
|
response_id: string;
|
||||||
|
|
@ -74,6 +76,8 @@ export function Navigation() {
|
||||||
const [previousConversationCount, setPreviousConversationCount] = useState(0);
|
const [previousConversationCount, setPreviousConversationCount] = useState(0);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const { selectedFilter, setSelectedFilter } = useKnowledgeFilter();
|
||||||
|
|
||||||
const handleNewConversation = () => {
|
const handleNewConversation = () => {
|
||||||
setLoadingNewConversation(true);
|
setLoadingNewConversation(true);
|
||||||
refreshConversations();
|
refreshConversations();
|
||||||
|
|
@ -194,6 +198,7 @@ export function Navigation() {
|
||||||
];
|
];
|
||||||
|
|
||||||
const isOnChatPage = pathname === "/" || pathname === "/chat";
|
const isOnChatPage = pathname === "/" || pathname === "/chat";
|
||||||
|
const isOnKnowledgePage = pathname.startsWith("/knowledge");
|
||||||
|
|
||||||
const createDefaultPlaceholder = useCallback(() => {
|
const createDefaultPlaceholder = useCallback(() => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -344,6 +349,13 @@ export function Navigation() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isOnKnowledgePage && (
|
||||||
|
<KnowledgeFilterList
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
onFilterSelect={setSelectedFilter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Chat Page Specific Sections */}
|
{/* Chat Page Specific Sections */}
|
||||||
{isOnChatPage && (
|
{isOnChatPage && (
|
||||||
<div className="flex-1 min-h-0 flex flex-col">
|
<div className="flex-1 min-h-0 flex flex-col">
|
||||||
|
|
|
||||||
9
frontend/package-lock.json
generated
9
frontend/package-lock.json
generated
|
|
@ -23,6 +23,7 @@
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/react-query": "^5.86.0",
|
"@tanstack/react-query": "^5.86.0",
|
||||||
"ag-grid-community": "^34.2.0",
|
"ag-grid-community": "^34.2.0",
|
||||||
|
|
@ -2161,6 +2162,14 @@
|
||||||
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/line-clamp": {
|
||||||
|
"version": "0.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
|
||||||
|
"integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/typography": {
|
"node_modules/@tailwindcss/typography": {
|
||||||
"version": "0.5.16",
|
"version": "0.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/react-query": "^5.86.0",
|
"@tanstack/react-query": "^5.86.0",
|
||||||
"ag-grid-community": "^34.2.0",
|
"ag-grid-community": "^34.2.0",
|
||||||
|
|
|
||||||
63
frontend/src/app/api/queries/useGetFiltersSearchQuery.ts
Normal file
63
frontend/src/app/api/queries/useGetFiltersSearchQuery.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import {
|
||||||
|
type UseQueryOptions,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
|
||||||
|
export interface KnowledgeFilter {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
query_data: string;
|
||||||
|
owner: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FiltersSearchResponse {
|
||||||
|
success: boolean;
|
||||||
|
filters: KnowledgeFilter[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetFiltersSearchQuery = (
|
||||||
|
search: string,
|
||||||
|
limit = 20,
|
||||||
|
options?: Omit<UseQueryOptions, "queryKey" | "queryFn">
|
||||||
|
) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
async function getFilters(): Promise<KnowledgeFilter[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/knowledge-filter/search", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query: search, limit }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: FiltersSearchResponse = await response.json();
|
||||||
|
if (response.ok && result.success) {
|
||||||
|
return result.filters || [];
|
||||||
|
}
|
||||||
|
console.error("Failed to load knowledge filters:", result.error);
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading knowledge filters:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryResult = useQuery(
|
||||||
|
{
|
||||||
|
queryKey: ["knowledge-filters", search, limit],
|
||||||
|
placeholderData: (prev) => prev,
|
||||||
|
queryFn: getFilters,
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
queryClient,
|
||||||
|
);
|
||||||
|
|
||||||
|
return queryResult;
|
||||||
|
};
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-bar-arrangement {
|
.side-bar-arrangement {
|
||||||
@apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
|
@apply flex h-full w-[18rem] flex-col overflow-hidden border-r scrollbar-hide;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-bar-search-div-placement {
|
.side-bar-search-div-placement {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import tailwindcssTypography from "@tailwindcss/typography";
|
||||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||||
import plugin from "tailwindcss/plugin";
|
import plugin from "tailwindcss/plugin";
|
||||||
import tailwindcssAnimate from "tailwindcss-animate";
|
import tailwindcssAnimate from "tailwindcss-animate";
|
||||||
|
import tailwindcssLineClamp from "@tailwindcss/line-clamp";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
|
|
@ -140,6 +141,7 @@ const config = {
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tailwindcssAnimate,
|
tailwindcssAnimate,
|
||||||
|
tailwindcssLineClamp,
|
||||||
tailwindcssForms({
|
tailwindcssForms({
|
||||||
strategy: "class",
|
strategy: "class",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue