Unified UI (#3308)

* fix typing

* add filters display
This commit is contained in:
pablodanswer
2024-12-02 15:12:13 -08:00
committed by GitHub
parent 03e2789392
commit 7c618c9d17
13 changed files with 246 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
from sqlalchemy.engine.base import Connection from sqlalchemy.engine.base import Connection
from typing import Any from typing import Literal
import asyncio import asyncio
from logging.config import fileConfig from logging.config import fileConfig
import logging import logging
@@ -8,6 +8,7 @@ from alembic import context
from sqlalchemy import pool from sqlalchemy import pool
from sqlalchemy.ext.asyncio import create_async_engine from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.sql import text from sqlalchemy.sql import text
from sqlalchemy.sql.schema import SchemaItem
from shared_configs.configs import MULTI_TENANT from shared_configs.configs import MULTI_TENANT
from danswer.db.engine import build_connection_string from danswer.db.engine import build_connection_string
@@ -35,7 +36,18 @@ logger = logging.getLogger(__name__)
def include_object( def include_object(
object: Any, name: str, type_: str, reflected: bool, compare_to: Any object: SchemaItem,
name: str | None,
type_: Literal[
"schema",
"table",
"column",
"index",
"unique_constraint",
"foreign_key_constraint",
],
reflected: bool,
compare_to: SchemaItem | None,
) -> bool: ) -> bool:
""" """
Determines whether a database object should be included in migrations. Determines whether a database object should be included in migrations.

View File

@@ -1,5 +1,6 @@
import asyncio import asyncio
from logging.config import fileConfig from logging.config import fileConfig
from typing import Literal
from sqlalchemy import pool from sqlalchemy import pool
from sqlalchemy.engine import Connection from sqlalchemy.engine import Connection
@@ -37,8 +38,15 @@ EXCLUDE_TABLES = {"kombu_queue", "kombu_message"}
def include_object( def include_object(
object: SchemaItem, object: SchemaItem,
name: str, name: str | None,
type_: str, type_: Literal[
"schema",
"table",
"column",
"index",
"unique_constraint",
"foreign_key_constraint",
],
reflected: bool, reflected: bool,
compare_to: SchemaItem | None, compare_to: SchemaItem | None,
) -> bool: ) -> bool:

View File

@@ -31,6 +31,7 @@ def llm_doc_from_inference_section(inference_section: InferenceSection) -> LlmDo
if inference_section.center_chunk.source_links if inference_section.center_chunk.source_links
else None, else None,
source_links=inference_section.center_chunk.source_links, source_links=inference_section.center_chunk.source_links,
match_highlights=inference_section.center_chunk.match_highlights,
) )

View File

@@ -25,6 +25,7 @@ class LlmDoc(BaseModel):
updated_at: datetime | None updated_at: datetime | None
link: str | None link: str | None
source_links: dict[int, str] | None source_links: dict[int, str] | None
match_highlights: list[str] | None
# First chunk of info for streaming QA # First chunk of info for streaming QA

View File

@@ -77,6 +77,7 @@ def llm_doc_from_internet_search_result(result: InternetSearchResult) -> LlmDoc:
updated_at=datetime.now(), updated_at=datetime.now(),
link=result.link, link=result.link,
source_links={0: result.link}, source_links={0: result.link},
match_highlights=[],
) )

View File

@@ -44,6 +44,7 @@ def test_persona_category_management(reset: None) -> None:
category=updated_persona_category, category=updated_persona_category,
user_performing_action=regular_user, user_performing_action=regular_user,
) )
assert exc_info.value.response is not None
assert exc_info.value.response.status_code == 403 assert exc_info.value.response.status_code == 403
assert PersonaCategoryManager.verify( assert PersonaCategoryManager.verify(

View File

@@ -64,6 +64,7 @@ def mock_search_results() -> list[LlmDoc]:
updated_at=datetime(2023, 1, 1), updated_at=datetime(2023, 1, 1),
link="https://example.com/doc1", link="https://example.com/doc1",
source_links={0: "https://example.com/doc1"}, source_links={0: "https://example.com/doc1"},
match_highlights=[],
), ),
LlmDoc( LlmDoc(
content="Search result 2", content="Search result 2",
@@ -75,6 +76,7 @@ def mock_search_results() -> list[LlmDoc]:
updated_at=datetime(2023, 1, 2), updated_at=datetime(2023, 1, 2),
link="https://example.com/doc2", link="https://example.com/doc2",
source_links={0: "https://example.com/doc2"}, source_links={0: "https://example.com/doc2"},
match_highlights=[],
), ),
] ]

View File

@@ -46,6 +46,7 @@ mock_docs = [
updated_at=datetime.now(), updated_at=datetime.now(),
link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None, link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None,
source_links={0: "https://mintlify.com/docs/settings/broken-links"}, source_links={0: "https://mintlify.com/docs/settings/broken-links"},
match_highlights=[],
) )
for id in range(10) for id in range(10)
] ]

View File

@@ -20,6 +20,7 @@ mock_docs = [
updated_at=datetime.now(), updated_at=datetime.now(),
link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None, link=f"https://{int(id/2)}.com" if int(id / 2) % 2 == 0 else None,
source_links={0: "https://mintlify.com/docs/settings/broken-links"}, source_links={0: "https://mintlify.com/docs/settings/broken-links"},
match_highlights=[],
) )
for id in range(10) for id in range(10)
] ]

View File

@@ -18,7 +18,7 @@ import {
SendIcon, SendIcon,
StopGeneratingIcon, StopGeneratingIcon,
} from "@/components/icons/icons"; } from "@/components/icons/icons";
import { DanswerDocument } from "@/lib/search/interfaces"; import { DanswerDocument, SourceMetadata } from "@/lib/search/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon"; import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { import {
Tooltip, Tooltip,
@@ -37,9 +37,41 @@ import { AssistantsTab } from "../modal/configuration/AssistantsTab";
import { IconType } from "react-icons"; import { IconType } from "react-icons";
import { LlmTab } from "../modal/configuration/LlmTab"; import { LlmTab } from "../modal/configuration/LlmTab";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { FilterPills } from "./FilterPills";
import { Tag } from "@/lib/types";
import FiltersDisplay from "./FilterDisplay";
const MAX_INPUT_HEIGHT = 200; const MAX_INPUT_HEIGHT = 200;
interface ChatInputBarProps {
removeFilters: () => void;
removeDocs: () => void;
openModelSettings: () => void;
showDocs: () => void;
showConfigureAPIKey: () => void;
selectedDocuments: DanswerDocument[];
message: string;
setMessage: (message: string) => void;
stopGenerating: () => void;
onSubmit: () => void;
filterManager: FilterManager;
llmOverrideManager: LlmOverrideManager;
chatState: ChatState;
alternativeAssistant: Persona | null;
inputPrompts: InputPrompt[];
// assistants
selectedAssistant: Persona;
setSelectedAssistant: (assistant: Persona) => void;
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
files: FileDescriptor[];
setFiles: (files: FileDescriptor[]) => void;
handleFileUpload: (files: File[]) => void;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
chatSessionId?: string;
toggleFilters?: () => void;
}
export function ChatInputBar({ export function ChatInputBar({
removeFilters, removeFilters,
removeDocs, removeDocs,
@@ -68,32 +100,7 @@ export function ChatInputBar({
chatSessionId, chatSessionId,
inputPrompts, inputPrompts,
toggleFilters, toggleFilters,
}: { }: ChatInputBarProps) {
removeFilters: () => void;
removeDocs: () => void;
showConfigureAPIKey: () => void;
openModelSettings: () => void;
chatState: ChatState;
stopGenerating: () => void;
showDocs: () => void;
selectedDocuments: DanswerDocument[];
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
setSelectedAssistant: (assistant: Persona) => void;
inputPrompts: InputPrompt[];
message: string;
setMessage: (message: string) => void;
onSubmit: () => void;
filterManager: FilterManager;
llmOverrideManager: LlmOverrideManager;
selectedAssistant: Persona;
alternativeAssistant: Persona | null;
files: FileDescriptor[];
setFiles: (files: FileDescriptor[]) => void;
handleFileUpload: (files: File[]) => void;
textAreaRef: React.RefObject<HTMLTextAreaElement>;
chatSessionId?: string;
toggleFilters?: () => void;
}) {
useEffect(() => { useEffect(() => {
const textarea = textAreaRef.current; const textarea = textAreaRef.current;
if (textarea) { if (textarea) {
@@ -340,23 +347,26 @@ export function ChatInputBar({
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full" className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
> >
<div className="rounded-lg py-1.5 bg-white border border-border-medium overflow-hidden shadow-lg mx-2 px-1.5 mt-2 rounded z-10"> <div className="rounded-lg py-1.5 bg-white border border-border-medium overflow-hidden shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
{filteredPrompts.map((currentPrompt, index) => ( {filteredPrompts.map(
<button (currentPrompt: InputPrompt, index: number) => (
key={index} <button
className={`px-2 ${ key={index}
tabbingIconIndex == index && "bg-hover" className={`px-2 ${
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`} tabbingIconIndex == index && "bg-hover"
onClick={() => { } rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`}
updateInputPrompt(currentPrompt); onClick={() => {
}} updateInputPrompt(currentPrompt);
> }}
<p className="font-bold">{currentPrompt.prompt}:</p> >
<p className="text-left flex-grow mr-auto line-clamp-1"> <p className="font-bold">{currentPrompt.prompt}:</p>
{currentPrompt.id == selectedAssistant.id && "(default) "} <p className="text-left flex-grow mr-auto line-clamp-1">
{currentPrompt.content?.trim()} {currentPrompt.id == selectedAssistant.id &&
</p> "(default) "}
</button> {currentPrompt.content?.trim()}
))} </p>
</button>
)
)}
<a <a
key={filteredPrompts.length} key={filteredPrompts.length}
@@ -430,6 +440,7 @@ export function ChatInputBar({
</div> </div>
</div> </div>
)} )}
{(selectedDocuments.length > 0 || files.length > 0) && ( {(selectedDocuments.length > 0 || files.length > 0) && (
<div className="flex gap-x-2 px-2 pt-2"> <div className="flex gap-x-2 px-2 pt-2">
<div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll"> <div className="flex gap-x-1 px-2 overflow-visible overflow-x-scroll items-end miniscroll">
@@ -564,6 +575,16 @@ export function ChatInputBar({
onClick={toggleFilters} onClick={toggleFilters}
/> />
)} )}
{(filterManager.selectedSources.length > 0 ||
filterManager.selectedDocumentSets.length > 0 ||
filterManager.selectedTags.length > 0 ||
filterManager.timeRange) &&
toggleFilters && (
<FiltersDisplay
filterManager={filterManager}
toggleFilters={toggleFilters}
/>
)}
</div> </div>
<div className="absolute bottom-2.5 mobile:right-4 desktop:right-10"> <div className="absolute bottom-2.5 mobile:right-4 desktop:right-10">

View File

@@ -0,0 +1,109 @@
import React from "react";
import { XIcon } from "lucide-react";
import { FilterPills } from "./FilterPills";
import { SourceMetadata } from "@/lib/search/interfaces";
import { FilterManager } from "@/lib/hooks";
import { Tag } from "@/lib/types";
interface FiltersDisplayProps {
filterManager: FilterManager;
toggleFilters: () => void;
}
export default function FiltersDisplay({
filterManager,
toggleFilters,
}: FiltersDisplayProps) {
return (
<div className="flex my-auto flex-wrap gap-2 px-2">
{(() => {
const allFilters = [
...filterManager.selectedSources,
...filterManager.selectedDocumentSets,
...filterManager.selectedTags,
...(filterManager.timeRange ? [filterManager.timeRange] : []),
];
const filtersToShow = allFilters.slice(0, 2);
const remainingFilters = allFilters.length - 2;
return (
<>
{filtersToShow.map((filter, index) => {
if (typeof filter === "object" && "displayName" in filter) {
return (
<FilterPills<SourceMetadata>
key={index}
item={filter}
itemToString={(source) => source.displayName}
onRemove={(source) =>
filterManager.setSelectedSources((prev) =>
prev.filter(
(s) => s.internalName !== source.internalName
)
)
}
toggleFilters={toggleFilters}
/>
);
} else if (typeof filter === "string") {
return (
<FilterPills<string>
key={index}
item={filter}
itemToString={(set) => set}
onRemove={(set) =>
filterManager.setSelectedDocumentSets((prev) =>
prev.filter((s) => s !== set)
)
}
toggleFilters={toggleFilters}
/>
);
} else if ("tag_key" in filter) {
return (
<FilterPills<Tag>
key={index}
item={filter}
itemToString={(tag) => `${tag.tag_key}:${tag.tag_value}`}
onRemove={(tag) =>
filterManager.setSelectedTags((prev) =>
prev.filter(
(t) =>
t.tag_key !== tag.tag_key ||
t.tag_value !== tag.tag_value
)
)
}
toggleFilters={toggleFilters}
/>
);
} else if ("from" in filter && "to" in filter) {
return (
<div
key={index}
className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm"
>
<span>
{filter.from.toLocaleDateString()} -{" "}
{filter.to.toLocaleDateString()}
</span>
<XIcon
onClick={() => filterManager.setTimeRange(null)}
size={16}
className="ml-2 text-text-400 hover:text-text-600 cursor-pointer"
/>
</div>
);
}
})}
{remainingFilters > 0 && (
<div className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm">
<span>+{remainingFilters} more</span>
</div>
)}
</>
);
})()}
</div>
);
}

View File

@@ -0,0 +1,39 @@
import React from "react";
import { XIcon } from "lucide-react";
import { SourceMetadata } from "@/lib/search/interfaces";
import { Tag } from "@/lib/types";
type FilterItem = SourceMetadata | string | Tag;
interface FilterPillsProps<T extends FilterItem> {
item: T;
itemToString: (item: T) => string;
onRemove: (item: T) => void;
toggleFilters?: () => void;
}
export function FilterPills<T extends FilterItem>({
item,
itemToString,
onRemove,
toggleFilters,
}: FilterPillsProps<T>) {
return (
<button
onClick={toggleFilters}
className="cursor-pointer flex flex-wrap gap-2"
>
<div className="flex items-center bg-background-150 rounded-full px-3 py-1 text-sm">
<span>{itemToString(item)}</span>
<XIcon
onClick={(e) => {
e.stopPropagation();
onRemove(item);
}}
size={16}
className="ml-2 text-text-400 hover:text-text-600 cursor-pointer"
/>
</div>
</button>
);
}

View File

@@ -96,7 +96,7 @@ export function SourceSelector({
}); });
}; };
let allSourcesSelected = selectedSources.length > 0; let allSourcesSelected = selectedSources.length == existingSources.length;
const toggleAllSources = () => { const toggleAllSources = () => {
if (allSourcesSelected) { if (allSourcesSelected) {