Document explorer admin page (#590)

This commit is contained in:
Chris Weaver
2023-10-18 18:41:39 -07:00
committed by GitHub
parent a5d2759fbc
commit 1bd76f528f
20 changed files with 447 additions and 89 deletions

View File

@@ -0,0 +1,89 @@
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import { updateBoost } from "./lib";
import { CheckmarkIcon, EditIcon } from "@/components/icons/icons";
export const ScoreSection = ({
documentId,
initialScore,
setPopup,
refresh,
consistentWidth = true,
}: {
documentId: string;
initialScore: number;
setPopup: (popupSpec: PopupSpec | null) => void;
refresh: () => void;
consistentWidth?: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
const [score, setScore] = useState(initialScore.toString());
const onSubmit = async () => {
const numericScore = Number(score);
if (isNaN(numericScore)) {
setPopup({
message: "Score must be a number",
type: "error",
});
return;
}
const errorMsg = await updateBoost(documentId, numericScore);
if (errorMsg) {
setPopup({
message: errorMsg,
type: "error",
});
} else {
setPopup({
message: "Updated score!",
type: "success",
});
refresh();
setIsOpen(false);
}
};
if (isOpen) {
return (
<div className="my-auto h-full flex">
<input
value={score}
onChange={(e) => {
setScore(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
onSubmit();
}
if (e.key === "Escape") {
setIsOpen(false);
setScore(initialScore.toString());
}
}}
className="border bg-slate-700 text-gray-200 border-gray-300 rounded py-1 px-3 w-16 h-5 my-auto"
/>
<div onClick={onSubmit} className="cursor-pointer my-auto ml-2">
<CheckmarkIcon size={16} className="text-green-700" />
</div>
</div>
);
}
return (
<div className="h-full flex flex-col">
<div className="flex my-auto">
<div className={"flex" + (consistentWidth && " w-6")}>
<div className="ml-auto my-auto">{initialScore}</div>
</div>
<div
className="cursor-pointer ml-2 my-auto"
onClick={() => setIsOpen(true)}
>
<EditIcon size={16} />
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,12 @@
export const adminSearch = async (query: string) => {
const response = await fetch("/api/admin/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query,
}),
});
return response;
};

View File

@@ -0,0 +1,195 @@
"use client";
import { ZoomInIcon } from "@/components/icons/icons";
import { adminSearch } from "./lib";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { useState, useEffect } from "react";
import { DanswerDocument } from "@/lib/search/interfaces";
import { FiZap } from "react-icons/fi";
import { getSourceIcon } from "@/components/source";
import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay";
import { CustomCheckbox } from "@/components/CustomCheckbox";
import { updateHiddenStatus } from "../lib";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { getErrorMsg } from "@/lib/fetchUtils";
import { ScoreSection } from "../ScoreEditor";
import { useRouter } from "next/navigation";
const DocumentDisplay = ({
document,
refresh,
setPopup,
}: {
document: DanswerDocument;
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}) => {
return (
<div
key={document.document_id}
className="text-sm border-b border-gray-800 mb-3"
>
<div className="flex relative">
<a
className={
"rounded-lg flex font-bold " +
(document.link ? "" : "pointer-events-none")
}
href={document.link}
target="_blank"
rel="noopener noreferrer"
>
{getSourceIcon(document.source_type, 22)}
<p className="truncate break-all ml-2 my-auto text-base">
{document.semantic_identifier || document.document_id}
</p>
</a>
</div>
<div className="flex flex-wrap gap-x-2 mt-1 text-xs">
<div className="px-1 py-0.5 bg-gray-700 rounded flex">
<p className="mr-1 my-auto">Boost:</p>
<ScoreSection
documentId={document.document_id}
initialScore={document.boost}
setPopup={setPopup}
refresh={refresh}
consistentWidth={false}
/>
</div>
<div
onClick={async () => {
const response = await updateHiddenStatus(
document.document_id,
!document.hidden
);
if (response.ok) {
refresh();
} else {
setPopup({
type: "error",
message: `Failed to update document - ${getErrorMsg(
response
)}}`,
});
}
}}
className="px-1 py-0.5 bg-gray-700 hover:bg-gray-600 rounded flex cursor-pointer select-none"
>
<div className="my-auto">
{document.hidden ? (
<div className="text-red-500">Hidden</div>
) : (
"Visible"
)}
</div>
<div className="ml-1 my-auto">
<CustomCheckbox checked={!document.hidden} />
</div>
</div>
</div>
<p className="pl-1 pt-2 pb-3 text-gray-200 break-words">
{buildDocumentSummaryDisplay(document.match_highlights, document.blurb)}
</p>
</div>
);
};
const Main = ({
initialSearchValue,
}: {
initialSearchValue: string | undefined;
}) => {
const router = useRouter();
const { popup, setPopup } = usePopup();
const [query, setQuery] = useState(initialSearchValue || "");
const [timeoutId, setTimeoutId] = useState<number | null>(null);
const [results, setResults] = useState<DanswerDocument[]>([]);
const onSearch = async (query: string) => {
const results = await adminSearch(query);
if (results.ok) {
setResults((await results.json()).documents);
}
setTimeoutId(null);
};
useEffect(() => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
if (query && query.trim() !== "") {
router.replace(
`/admin/documents/explorer?query=${encodeURIComponent(query)}`
);
const timeoutId = window.setTimeout(() => onSearch(query), 300);
setTimeoutId(timeoutId);
} else {
setResults([]);
}
}, [query]);
return (
<div>
{popup}
<div className="flex justify-center py-2">
<div className="flex items-center w-full border-2 border-gray-600 rounded px-4 py-2 focus-within:border-blue-500">
<MagnifyingGlass className="text-gray-400" />
<textarea
autoFocus
className="flex-grow ml-2 h-6 bg-transparent outline-none placeholder-gray-400 overflow-hidden whitespace-normal resize-none"
role="textarea"
aria-multiline
placeholder="Find documents based on title / content..."
value={query}
onChange={(e) => {
setQuery(e.target.value);
}}
suppressContentEditableWarning={true}
/>
</div>
</div>
{results.length > 0 && (
<div className="mt-3">
{results.map((document) => {
return (
<DocumentDisplay
key={document.document_id}
document={document}
refresh={() => onSearch(query)}
setPopup={setPopup}
/>
);
})}
</div>
)}
{!query && (
<div className="flex">
<FiZap className="my-auto mr-1 text-blue-400" /> Search for a document
above to modify it&apos;s boost or hide it from searches.
</div>
)}
</div>
);
};
const Page = ({
searchParams,
}: {
searchParams: { [key: string]: string };
}) => {
return (
<div className="mx-auto container">
<div className="border-solid border-gray-600 border-b pb-2 mb-3 flex">
<ZoomInIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Document Explorer</h1>
</div>
<Main initialSearchValue={searchParams.query} />
</div>
);
};
export default Page;

View File

@@ -3,13 +3,14 @@ import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import { PageSelector } from "@/components/PageSelector";
import { DocumentBoostStatus } from "@/lib/types";
import { updateBoost, updateHiddenStatus } from "./lib";
import { updateBoost, updateHiddenStatus } from "../lib";
import { CheckmarkIcon, EditIcon } from "@/components/icons/icons";
import { numToDisplay } from "./constants";
import { FiCheck, FiCheckSquare, FiEye, FiEyeOff, FiX } from "react-icons/fi";
import { FiEye, FiEyeOff } from "react-icons/fi";
import { getErrorMsg } from "@/lib/fetchUtils";
import { HoverPopup } from "@/components/HoverPopup";
import { CustomCheckbox } from "@/components/CustomCheckbox";
import { ScoreSection } from "../ScoreEditor";
const IsVisibleSection = ({
document,
@@ -74,86 +75,6 @@ const IsVisibleSection = ({
);
};
const ScoreSection = ({
documentId,
initialScore,
setPopup,
refresh,
}: {
documentId: string;
initialScore: number;
setPopup: (popupSpec: PopupSpec | null) => void;
refresh: () => void;
}) => {
const [isOpen, setIsOpen] = useState(false);
const [score, setScore] = useState(initialScore.toString());
const onSubmit = async () => {
const numericScore = Number(score);
if (isNaN(numericScore)) {
setPopup({
message: "Score must be a number",
type: "error",
});
return;
}
const errorMsg = await updateBoost(documentId, numericScore);
if (errorMsg) {
setPopup({
message: errorMsg,
type: "error",
});
} else {
setPopup({
message: "Updated score!",
type: "success",
});
refresh();
setIsOpen(false);
}
};
if (isOpen) {
return (
<div className="m-auto flex">
<input
value={score}
onChange={(e) => {
setScore(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
onSubmit();
}
if (e.key === "Escape") {
setIsOpen(false);
setScore(initialScore.toString());
}
}}
className="border bg-slate-700 text-gray-200 border-gray-300 rounded py-1 px-3 w-16"
/>
<div onClick={onSubmit} className="cursor-pointer my-auto ml-2">
<CheckmarkIcon size={20} className="text-green-700" />
</div>
</div>
);
}
return (
<div className="h-full flex flex-col">
<div className="flex my-auto">
<div className="w-6 flex">
<div className="ml-auto">{initialScore}</div>
</div>
<div className="cursor-pointer ml-2" onClick={() => setIsOpen(true)}>
<EditIcon size={20} />
</div>
</div>
</div>
);
};
export const DocumentFeedbackTable = ({
documents,
refresh,

View File

@@ -12,6 +12,7 @@ export const CustomCheckbox = ({
className="hidden"
checked={checked}
onChange={onChange}
readOnly={onChange ? false : true}
/>
<span className="relative">
<span

View File

@@ -25,6 +25,7 @@ import {
Document360Icon,
GoogleSitesIcon,
GongIcon,
ZoomInIcon,
} from "@/components/icons/icons";
import { getAuthDisabledSS, getCurrentUserSS } from "@/lib/userSS";
import { redirect } from "next/navigation";
@@ -272,6 +273,15 @@ export async function Layout({ children }: { children: React.ReactNode }) {
),
link: "/admin/documents/sets",
},
{
name: (
<div className="flex">
<ZoomInIcon size={18} />
<div className="ml-1">Explorer</div>
</div>
),
link: "/admin/documents/explorer",
},
{
name: (
<div className="flex">

View File

@@ -4,7 +4,7 @@ import { getSourceIcon } from "../source";
import { useState } from "react";
import { PopupSpec } from "../admin/connectors/Popup";
const buildDocumentSummaryDisplay = (
export const buildDocumentSummaryDisplay = (
matchHighlights: string[],
blurb: string
) => {

View File

@@ -1,4 +1,4 @@
import React, { useState, KeyboardEvent, ChangeEvent } from "react";
import React, { KeyboardEvent, ChangeEvent } from "react";
import { MagnifyingGlass } from "@phosphor-icons/react";
interface SearchBarProps {

View File

@@ -32,6 +32,7 @@ export interface DanswerDocument {
blurb: string;
semantic_identifier: string | null;
boost: number;
hidden: boolean;
score: number;
match_highlights: string[];
}