Add ux improvements (#2130)

* add ux improvements

* add danswer version display

* show version properly

* improve copy + add web version to settings context

* update copy + danswer version
This commit is contained in:
pablodanswer 2024-08-14 09:43:52 -07:00 committed by GitHub
parent 54732a83c9
commit 3540aa579b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 270 additions and 126 deletions

View File

@ -1,5 +1,6 @@
import json
from collections.abc import Generator
from enum import Enum
from typing import Any
from typing import cast
@ -20,6 +21,7 @@ from danswer.tools.tool import ToolResponse
from danswer.utils.logger import setup_logger
from danswer.utils.threadpool_concurrency import run_functions_tuples_in_parallel
logger = setup_logger()
@ -54,6 +56,12 @@ class ImageGenerationResponse(BaseModel):
url: str
class ImageShape(str, Enum):
SQUARE = "square"
PORTRAIT = "portrait"
LANDSCAPE = "landscape"
class ImageGenerationTool(Tool):
_NAME = "run_image_generation"
_DESCRIPTION = "Generate an image from a prompt."
@ -102,6 +110,11 @@ class ImageGenerationTool(Tool):
"type": "string",
"description": "Prompt used to generate the image",
},
"shape": {
"type": "string",
"description": "Optional. Image shape: 'square', 'portrait', or 'landscape'",
"enum": [shape.value for shape in ImageShape],
},
},
"required": ["prompt"],
},
@ -161,7 +174,16 @@ class ImageGenerationTool(Tool):
# img_urls=[image_generation.url for image_generation in image_generations],
)
def _generate_image(self, prompt: str) -> ImageGenerationResponse:
def _generate_image(
self, prompt: str, shape: ImageShape
) -> ImageGenerationResponse:
if shape == ImageShape.LANDSCAPE:
size = "1792x1024"
elif shape == ImageShape.PORTRAIT:
size = "1024x1792"
else:
size = "1024x1024"
try:
response = image_generation(
prompt=prompt,
@ -170,6 +192,7 @@ class ImageGenerationTool(Tool):
# need to pass in None rather than empty str
api_base=self.api_base or None,
api_version=self.api_version or None,
size=size,
n=1,
extra_headers=build_llm_extra_headers(self.additional_headers),
)
@ -202,13 +225,23 @@ class ImageGenerationTool(Tool):
def run(self, **kwargs: str) -> Generator[ToolResponse, None, None]:
prompt = cast(str, kwargs["prompt"])
shape = ImageShape(kwargs.get("shape", ImageShape.SQUARE))
# dalle3 only supports 1 image at a time, which is why we have to
# parallelize this via threading
results = cast(
list[ImageGenerationResponse],
run_functions_tuples_in_parallel(
[(self._generate_image, (prompt,)) for _ in range(self.num_imgs)]
[
(
self._generate_image,
(
prompt,
shape,
),
)
for _ in range(self.num_imgs)
]
),
)
yield ToolResponse(

View File

@ -24,7 +24,7 @@ import { getDisplayNameForModel } from "@/lib/hooks";
import { Bubble } from "@/components/Bubble";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import { Option } from "@/components/Dropdown";
import { GroupsIcon, PaintingIcon, SwapIcon } from "@/components/icons/icons";
import { GroupsIcon } from "@/components/icons/icons";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences";
import { useUserGroups } from "@/lib/hooks";
@ -48,7 +48,6 @@ import { SuccessfulPersonaUpdateRedirectType } from "./enums";
import { Persona, StarterMessage } from "./interfaces";
import { buildFinalPrompt, createPersona, updatePersona } from "./lib";
import { IconImageSelection } from "@/components/assistants/AssistantIconCreation";
import { FaSwatchbook } from "react-icons/fa";
function findSearchTool(tools: ToolSnapshot[]) {
return tools.find((tool) => tool.in_code_tool_id === "SearchTool");
@ -604,40 +603,95 @@ export function AssistantEditor({
</div>
</div>
<div className="mt-2 ml-1">
{imageGenerationTool &&
checkLLMSupportsImageInput(
providerDisplayNameToProviderName.get(
values.llm_model_provider_override || ""
) ||
defaultProviderName ||
"",
values.llm_model_version_override ||
defaultModelName ||
""
) && (
<BooleanFormField
noPadding
name={`enabled_tools_map.${imageGenerationTool.id}`}
label="Image Generation Tool"
onChange={() => {
toggleToolInValues(imageGenerationTool.id);
}}
/>
)}
<div className="mt-2 flex flex-col ml-1">
{imageGenerationTool && (
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={`w-fit ${
!checkLLMSupportsImageInput(
providerDisplayNameToProviderName.get(
values.llm_model_provider_override || ""
) || "",
values.llm_model_version_override || ""
)
? "opacity-50 cursor-not-allowed"
: ""
}`}
>
<BooleanFormField
noPadding
name={`enabled_tools_map.${imageGenerationTool.id}`}
label="Image Generation Tool"
onChange={() => {
toggleToolInValues(imageGenerationTool.id);
}}
disabled={
!checkLLMSupportsImageInput(
providerDisplayNameToProviderName.get(
values.llm_model_provider_override || ""
) || "",
values.llm_model_version_override || ""
)
}
/>
</div>
</TooltipTrigger>
{!checkLLMSupportsImageInput(
providerDisplayNameToProviderName.get(
values.llm_model_provider_override || ""
) || "",
values.llm_model_version_override || ""
) && (
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
To use Image Generation, select GPT-4o as the
default model for this Assistant.
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)}
{searchTool && (
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger asChild>
<div
className={`w-fit ${
ccPairs.length === 0
? "opacity-50 cursor-not-allowed"
: ""
}`}
>
<BooleanFormField
name={`enabled_tools_map.${searchTool.id}`}
label="Search Tool"
noPadding
onChange={() => {
setFieldValue("num_chunks", null);
toggleToolInValues(searchTool.id);
}}
disabled={ccPairs.length === 0}
/>
</div>
</TooltipTrigger>
{ccPairs.length === 0 && (
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
To use the Search Tool, you need to have at
least one Connector-Credential pair configured.
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
)}
{ccPairs.length > 0 && searchTool && (
<>
<BooleanFormField
name={`enabled_tools_map.${searchTool.id}`}
label="Search Tool"
noPadding
onChange={() => {
setFieldValue("num_chunks", null);
toggleToolInValues(searchTool.id);
}}
/>
{searchToolEnabled() && (
<CollapsibleSection prompt="Configure Search">
<div>

View File

@ -20,4 +20,5 @@ export interface CombinedSettings {
enterpriseSettings: EnterpriseSettings | null;
customAnalyticsScript: string | null;
isMobile?: boolean;
webVersion: string | null;
}

View File

@ -5,6 +5,7 @@ import {
BackendChatSession,
BackendMessage,
ChatFileType,
ChatSession,
ChatSessionSharedStatus,
DocumentsResponse,
FileDescriptor,
@ -26,6 +27,7 @@ import {
buildLatestMessageChain,
checkAnyAssistantHasSearch,
createChatSession,
deleteChatSession,
getCitedDocumentsFromMessage,
getHumanAndAIMessageFromMessageNumber,
getLastSuccessfulMessageId,
@ -79,6 +81,7 @@ import { SIDEBAR_TOGGLED_COOKIE_NAME } from "@/components/resizable/constants";
import FixedLogo from "./shared_chat_search/FixedLogo";
import { getSecondsUntilExpiration } from "@/lib/time";
import { SetDefaultModelModal } from "./modal/SetDefaultModelModal";
import { DeleteChatModal } from "./modal/DeleteChatModal";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@ -1188,7 +1191,17 @@ export function ChatPage({
window.removeEventListener("keydown", handleKeyDown);
};
}, [router]);
const [sharedChatSession, setSharedChatSession] =
useState<ChatSession | null>();
const [deletingChatSession, setDeletingChatSession] =
useState<ChatSession | null>();
const showDeleteModal = (chatSession: ChatSession) => {
setDeletingChatSession(chatSession);
};
const showShareModal = (chatSession: ChatSession) => {
setSharedChatSession(chatSession);
};
const [documentSelection, setDocumentSelection] = useState(false);
const toggleDocumentSelectionAspects = () => {
setDocumentSelection((documentSelection) => !documentSelection);
@ -1229,11 +1242,29 @@ export function ChatPage({
onClose={() => setSettingsToggled(false)}
/>
)}
{sharingModalVisible && chatSessionIdRef.current !== null && (
{deletingChatSession && (
<DeleteChatModal
onClose={() => setDeletingChatSession(null)}
onSubmit={async () => {
const response = await deleteChatSession(deletingChatSession.id);
if (response.ok) {
setDeletingChatSession(null);
// go back to the main page
router.push("/chat");
} else {
alert("Failed to delete chat session");
}
}}
chatSessionName={deletingChatSession.name}
/>
)}
{sharedChatSession && (
<ShareChatSessionModal
chatSessionId={chatSessionIdRef.current}
existingSharedStatus={chatSessionSharedStatus}
onClose={() => setSharingModalVisible(false)}
chatSessionId={sharedChatSession.id}
existingSharedStatus={sharedChatSession.shared_status}
onClose={() => setSharedChatSession(null)}
onShare={(shared) =>
setChatSessionSharedStatus(
shared
@ -1243,6 +1274,13 @@ export function ChatPage({
}
/>
)}
{sharingModalVisible && chatSessionIdRef.current !== null && (
<ShareChatSessionModal
chatSessionId={chatSessionIdRef.current}
existingSharedStatus={chatSessionSharedStatus}
onClose={() => setSharingModalVisible(false)}
/>
)}
<div className="fixed inset-0 flex flex-col text-default">
<div className="h-[100dvh] overflow-y-hidden">
<div className="w-full">
@ -1277,6 +1315,8 @@ export function ChatPage({
folders={folders}
openedFolders={openedFolders}
removeToggle={removeToggle}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
/>
</div>
</div>

View File

@ -24,7 +24,7 @@ export function InMessageImage({ fileId }: { fileId: string }) {
height={1200}
alt="Chat Message Image"
onLoad={() => setImageLoaded(true)}
className={`object-cover object-center overflow-hidden rounded-lg w-full h-full max-w-96 max-h-96 transition-opacity duration-300
className={`object-contain object-left overflow-hidden rounded-lg w-full h-full max-w-96 max-h-96 transition-opacity duration-300
${imageLoaded ? "opacity-100" : "opacity-0"}`}
onClick={() => setFullImageShowing(true)}
src={buildImgUrl(fileId)}

View File

@ -82,10 +82,12 @@ const FolderItem = ({
}
};
const saveFolderName = async () => {
const saveFolderName = async (continueEditing?: boolean) => {
try {
await updateFolderName(folder.folder_id, editedFolderName);
setIsEditing(false);
if (!continueEditing) {
setIsEditing(false);
}
router.refresh(); // Refresh values to update the sidebar
} catch (error) {
setPopup({ message: "Failed to save folder name", type: "error" });
@ -171,7 +173,7 @@ const FolderItem = ({
{showDeleteConfirm && (
<div
ref={deleteConfirmRef}
className="absolute max-w-xs border z-[100] border-border-medium top-0 right-0 w-[250px] -bo-0 top-2 mt-4 p-2 bg-background-100 rounded shadow-lg z-10"
className="absolute max-w-xs border z-[100] border-border-medium top-0 right-0 w-[225px] -bo-0 top-2 mt-4 p-2 bg-background-100 rounded shadow-lg z-10"
>
<p className="text-sm mb-2">
Are you sure you want to delete <i>{folder.folder_name}</i>? All the
@ -216,6 +218,7 @@ const FolderItem = ({
value={editedFolderName}
onChange={handleFolderNameChange}
onKeyDown={handleKeyDown}
onBlur={() => saveFolderName(true)}
className="text-sm px-1 flex-1 min-w-0 -my-px mr-2"
/>
) : (
@ -239,20 +242,13 @@ const FolderItem = ({
<FiTrash size={16} />
</div>
</div>
{/* <div
onClick={deleteFolderHandler}
className="hover:bg-black/10 p-1 -m-1 rounded ml-2"
>
<FiTrash size={16} />
</div> */}
</div>
)}
{isEditing && (
<div className="flex ml-auto my-auto">
<div
onClick={saveFolderName}
onClick={() => saveFolderName()}
className="hover:bg-black/10 p-1 -m-1 rounded"
>
<FiCheck size={16} />
@ -310,6 +306,12 @@ export const FolderList = ({
}
/>
))}
{folders.length == 1 && folders[0].chat_sessions.length == 0 && (
<p className="text-sm font-normal text-subtle mt-2">
{" "}
Drag a chat into a folder to save for later{" "}
</p>
)}
</div>
);
};

View File

@ -16,12 +16,6 @@ export const DeleteChatModal = ({
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">Delete chat?</h2>
<div
onClick={onClose}
className="my-auto ml-auto p-2 hover:bg-hover rounded cursor-pointer"
>
<FiX size={20} />
</div>
</div>
<p className="mb-4">
Click below to confirm that you want to delete{" "}

View File

@ -98,6 +98,9 @@ export function SetDefaultModelModal({
});
}
};
const defaultProvider = llmProviders.find(
(llmProvider) => llmProvider.is_default_provider
);
return (
<ModalWrapper
@ -118,46 +121,46 @@ export function SetDefaultModelModal({
{defaultModel == null && " No default model has been selected!"}
</Text>
<div className="w-full flex text-sm flex-col">
<div key={-1} className="w-full border-b hover:bg-background-50">
<td className="min-w-[80px]">
{defaultModel == null ? (
<Badge>selected</Badge>
) : (
<input
type="radio"
name="credentialSelection"
onChange={(e) => {
e.preventDefault();
handleChangedefaultModel(null);
}}
className="form-radio ml-4 h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
)}
</td>
<td className="p-2">System default</td>
<div
key={-1}
className="w-full border-b flex items-center gap-x-2 hover:bg-background-50"
>
<input
checked={defaultModelDestructured?.modelName == null}
type="radio"
name="credentialSelection"
onChange={(e) => {
e.preventDefault();
handleChangedefaultModel(null);
}}
className="form-radio ml-4 h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
{
<td className="p-2">
System default{" "}
{defaultProvider?.default_model_name &&
`(${getDisplayNameForModel(defaultProvider?.default_model_name)})`}
</td>
}
</div>
{llmOptions.map(({ name, value }, index) => {
return (
<div
key={index}
className="w-full border-b hover:bg-background-50"
className="w-full flex items-center gap-x-2 border-b hover:bg-background-50"
>
<td className="min-w-[80px]">
{defaultModelDestructured?.modelName != name ? (
<input
type="radio"
name="credentialSelection"
onChange={(e) => {
e.preventDefault();
handleChangedefaultModel(value);
}}
className="form-radio ml-4 h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
) : (
<Badge>selected</Badge>
)}
</td>
<input
checked={defaultModelDestructured?.modelName == name}
type="radio"
name="credentialSelection"
onChange={(e) => {
e.preventDefault();
handleChangedefaultModel(value);
}}
className="form-radio ml-4 h-4 w-4 text-blue-600 transition duration-150 ease-in-out"
/>
<td className="p-2">
{getDisplayNameForModel(name)}{" "}
{defaultModelDestructured &&

View File

@ -27,6 +27,8 @@ export function ChatSessionDisplay({
isSelected,
skipGradient,
closeSidebar,
showShareModal,
showDeleteModal,
}: {
chatSession: ChatSession;
isSelected: boolean;
@ -35,6 +37,8 @@ export function ChatSessionDisplay({
// if not set, the gradient will still be applied and cause weirdness
skipGradient?: boolean;
closeSidebar?: () => void;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
}) {
const router = useRouter();
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
@ -77,22 +81,6 @@ export function ChatSessionDisplay({
/>
)}
{isDeletionModalVisible && (
<DeleteChatModal
onClose={() => setIsDeletionModalVisible(false)}
onSubmit={async () => {
const response = await deleteChatSession(chatSession.id);
if (response.ok) {
setIsDeletionModalVisible(false);
// go back to the main page
router.push("/chat");
} else {
alert("Failed to delete chat session");
}
}}
chatSessionName={chatSession.name}
/>
)}
<Link
className="flex my-1 group relative"
key={chatSession.id}
@ -186,11 +174,13 @@ export function ChatSessionDisplay({
}
popover={
<div className="border border-border rounded-lg bg-background z-50 w-32">
<DefaultDropdownElement
name="Share"
icon={FiShare2}
onSelect={() => setIsShareModalVisible(true)}
/>
{showShareModal && (
<DefaultDropdownElement
name="Share"
icon={FiShare2}
onSelect={() => showShareModal(chatSession)}
/>
)}
<DefaultDropdownElement
name="Rename"
icon={FiEdit2}
@ -204,12 +194,14 @@ export function ChatSessionDisplay({
/>
</div>
</div>
<div
onClick={() => setIsDeletionModalVisible(true)}
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2`}
>
<FiTrash size={16} />
</div>
{showDeleteModal && (
<div
onClick={() => showDeleteModal(chatSession)}
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2`}
>
<FiTrash size={16} />
</div>
)}
</div>
))}
</div>

View File

@ -32,6 +32,8 @@ interface HistorySidebarProps {
toggled?: boolean;
removeToggle?: () => void;
reset?: () => void;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
}
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
@ -46,6 +48,8 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
openedFolders,
toggleSidebar,
removeToggle,
showShareModal,
showDeleteModal,
},
ref: ForwardedRef<HTMLDivElement>
) => {
@ -168,6 +172,8 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
)}
<div className="border-b border-border pb-4 mx-3" />
<PagesTab
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
closeSidebar={removeToggle}
page={page}
existingChats={existingChats}

View File

@ -17,6 +17,8 @@ export function PagesTab({
folders,
openedFolders,
closeSidebar,
showShareModal,
showDeleteModal,
}: {
page: pageType;
existingChats?: ChatSession[];
@ -24,6 +26,8 @@ export function PagesTab({
folders?: Folder[];
openedFolders?: { [key: number]: boolean };
closeSidebar?: () => void;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
}) {
const groupedChatSessions = existingChats
? groupSessionsByDateRange(existingChats)
@ -117,6 +121,8 @@ export function PagesTab({
return (
<div key={`${chat.id}-${chat.name}`}>
<ChatSessionDisplay
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
closeSidebar={closeSidebar}
search={page == "search"}
chatSession={chat}

View File

@ -27,7 +27,6 @@ import { FiActivity, FiBarChart2 } from "react-icons/fi";
import { UserDropdown } from "../UserDropdown";
import { User } from "@/lib/types";
import { usePathname } from "next/navigation";
import { PencilCircle } from "@phosphor-icons/react";
export function ClientLayout({
user,

View File

@ -23,6 +23,7 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
if (!combinedSettings) {
return null;
}
const settings = combinedSettings.settings;
const enterpriseSettings = combinedSettings.enterpriseSettings;
@ -93,6 +94,16 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
</div>
))}
</nav>
{combinedSettings.webVersion && (
<div
className="flex flex-col mt-6 items-center justify-center w-full"
key={"danswerVersion"}
>
<h2 className="text-xs text-text w-52 font-medium pb-2">
Danswer version: {combinedSettings.webVersion}
</h2>
</div>
)}
</div>
);
}

View File

@ -344,6 +344,7 @@ interface BooleanFormFieldProps {
small?: boolean;
alignTop?: boolean;
noLabel?: boolean;
disabled?: boolean;
}
export const BooleanFormField = ({
@ -354,12 +355,14 @@ export const BooleanFormField = ({
noPadding,
noLabel,
small,
disabled,
alignTop,
}: BooleanFormFieldProps) => {
return (
<div className="mb-4">
<label className="flex text-sm">
<Field
disabled={disabled}
name={name}
type="checkbox"
className={`${noPadding ? "mr-3" : "mx-3"} px-5 w-3.5 h-3.5 ${

View File

@ -1,9 +1,14 @@
import { EnterpriseSettings, Settings } from "@/app/admin/settings/interfaces";
import {
CombinedSettings,
EnterpriseSettings,
Settings,
} from "@/app/admin/settings/interfaces";
import {
CUSTOM_ANALYTICS_ENABLED,
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
} from "@/lib/constants";
import { fetchSS } from "@/lib/utilsSS";
import { getWebVersion } from "@/lib/version";
export async function fetchSettingsSS() {
const tasks = [fetchSS("/settings")];
@ -22,23 +27,18 @@ export async function fetchSettingsSS() {
const customAnalyticsScript = (
tasks.length > 2 ? await results[2].json() : null
) as string | null;
const webVersion = getWebVersion();
const combinedSettings: CombinedSettings = {
settings,
enterpriseSettings,
customAnalyticsScript,
webVersion,
};
return combinedSettings;
}
export interface CombinedSettings {
settings: Settings;
enterpriseSettings: EnterpriseSettings | null;
customAnalyticsScript: string | null;
isMobile?: boolean;
}
let cachedSettings: CombinedSettings;
export async function getCombinedSettings({