mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-29 03:01:59 +01:00
* sanitize llm keys and handle updates properly * fix llm provider testing * fix test * mypy * fix default model editing --------- Co-authored-by: Richard Kuo (Danswer) <rkuo@onyx.app> Co-authored-by: Richard Kuo <rkuo@rkuo.com>
1452 lines
58 KiB
TypeScript
1452 lines
58 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { Option } from "@/components/Dropdown";
|
|
import { generateRandomIconShape } from "@/lib/assistantIconUtils";
|
|
import {
|
|
CCPairBasicInfo,
|
|
DocumentSet,
|
|
User,
|
|
UserGroup,
|
|
UserRole,
|
|
} from "@/lib/types";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Button } from "@/components/ui/button";
|
|
import { ArrayHelpers, FieldArray, Form, Formik, FormikProps } from "formik";
|
|
|
|
import {
|
|
BooleanFormField,
|
|
Label,
|
|
TextFormField,
|
|
} from "@/components/admin/connectors/Field";
|
|
|
|
import { usePopup } from "@/components/admin/connectors/Popup";
|
|
import { getDisplayNameForModel, useLabels } from "@/lib/hooks";
|
|
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
|
|
import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences";
|
|
import {
|
|
checkLLMSupportsImageInput,
|
|
destructureValue,
|
|
structureValue,
|
|
} from "@/lib/llm/utils";
|
|
import { ToolSnapshot } from "@/lib/tools/interfaces";
|
|
import { checkUserIsNoAuthUser } from "@/lib/user";
|
|
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from "@/components/ui/tooltip";
|
|
import Link from "next/link";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import * as Yup from "yup";
|
|
import CollapsibleSection from "./CollapsibleSection";
|
|
import { SuccessfulPersonaUpdateRedirectType } from "./enums";
|
|
import { Persona, PersonaLabel, StarterMessage } from "./interfaces";
|
|
import {
|
|
PersonaUpsertParameters,
|
|
createPersona,
|
|
updatePersona,
|
|
deletePersona,
|
|
} from "./lib";
|
|
import {
|
|
CameraIcon,
|
|
GroupsIconSkeleton,
|
|
NewChatIcon,
|
|
SwapIcon,
|
|
TrashIcon,
|
|
} from "@/components/icons/icons";
|
|
import { buildImgUrl } from "@/app/chat/files/images/utils";
|
|
import { useAssistants } from "@/components/context/AssistantsContext";
|
|
import { debounce } from "lodash";
|
|
import { LLMProviderView } from "../configuration/llm/interfaces";
|
|
import StarterMessagesList from "./StarterMessageList";
|
|
|
|
import { Switch, SwitchField } from "@/components/ui/switch";
|
|
import { generateIdenticon } from "@/components/assistants/AssistantIcon";
|
|
import { BackButton } from "@/components/BackButton";
|
|
import { Checkbox, CheckboxField } from "@/components/ui/checkbox";
|
|
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
|
|
import { MinimalUserSnapshot } from "@/lib/types";
|
|
import { useUserGroups } from "@/lib/hooks";
|
|
import {
|
|
SearchMultiSelectDropdown,
|
|
Option as DropdownOption,
|
|
} from "@/components/Dropdown";
|
|
import { SourceChip } from "@/app/chat/input/ChatInputBar";
|
|
import { TagIcon, UserIcon, XIcon, InfoIcon } from "lucide-react";
|
|
import { LLMSelector } from "@/components/llm/LLMSelector";
|
|
import useSWR from "swr";
|
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
|
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
|
import Title from "@/components/ui/title";
|
|
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
|
|
|
|
function findSearchTool(tools: ToolSnapshot[]) {
|
|
return tools.find((tool) => tool.in_code_tool_id === SEARCH_TOOL_ID);
|
|
}
|
|
|
|
function findImageGenerationTool(tools: ToolSnapshot[]) {
|
|
return tools.find((tool) => tool.in_code_tool_id === "ImageGenerationTool");
|
|
}
|
|
|
|
function findInternetSearchTool(tools: ToolSnapshot[]) {
|
|
return tools.find((tool) => tool.in_code_tool_id === "InternetSearchTool");
|
|
}
|
|
|
|
function SubLabel({ children }: { children: string | JSX.Element }) {
|
|
return (
|
|
<div
|
|
className="text-sm text-description font-description mb-2"
|
|
style={{ color: "rgb(113, 114, 121)" }}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AssistantEditor({
|
|
existingPersona,
|
|
ccPairs,
|
|
documentSets,
|
|
user,
|
|
defaultPublic,
|
|
llmProviders,
|
|
tools,
|
|
shouldAddAssistantToUserPreferences,
|
|
admin,
|
|
}: {
|
|
existingPersona?: Persona | null;
|
|
ccPairs: CCPairBasicInfo[];
|
|
documentSets: DocumentSet[];
|
|
user: User | null;
|
|
defaultPublic: boolean;
|
|
llmProviders: LLMProviderView[];
|
|
tools: ToolSnapshot[];
|
|
shouldAddAssistantToUserPreferences?: boolean;
|
|
admin?: boolean;
|
|
}) {
|
|
const { refreshAssistants, isImageGenerationAvailable } = useAssistants();
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const isAdminPage = searchParams.get("admin") === "true";
|
|
|
|
const { popup, setPopup } = usePopup();
|
|
const { labels, refreshLabels, createLabel, updateLabel, deleteLabel } =
|
|
useLabels();
|
|
|
|
const colorOptions = [
|
|
"#FF6FBF",
|
|
"#6FB1FF",
|
|
"#B76FFF",
|
|
"#FFB56F",
|
|
"#6FFF8D",
|
|
"#FF6F6F",
|
|
"#6FFFFF",
|
|
];
|
|
|
|
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
|
|
|
// state to persist across formik reformatting
|
|
const [defautIconColor, _setDeafultIconColor] = useState(
|
|
colorOptions[Math.floor(Math.random() * colorOptions.length)]
|
|
);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
|
|
const [defaultIconShape, setDefaultIconShape] = useState<any>(null);
|
|
|
|
useEffect(() => {
|
|
if (defaultIconShape === null) {
|
|
setDefaultIconShape(generateRandomIconShape().encodedGrid);
|
|
}
|
|
}, [defaultIconShape]);
|
|
|
|
const [isIconDropdownOpen, setIsIconDropdownOpen] = useState(false);
|
|
|
|
const [removePersonaImage, setRemovePersonaImage] = useState(false);
|
|
|
|
const autoStarterMessageEnabled = useMemo(
|
|
() => llmProviders.length > 0,
|
|
[llmProviders.length]
|
|
);
|
|
const isUpdate = existingPersona !== undefined && existingPersona !== null;
|
|
const existingPrompt = existingPersona?.prompts[0] ?? null;
|
|
const defaultProvider = llmProviders.find(
|
|
(llmProvider) => llmProvider.is_default_provider
|
|
);
|
|
const defaultModelName = defaultProvider?.default_model_name;
|
|
const providerDisplayNameToProviderName = new Map<string, string>();
|
|
llmProviders.forEach((llmProvider) => {
|
|
providerDisplayNameToProviderName.set(
|
|
llmProvider.name,
|
|
llmProvider.provider
|
|
);
|
|
});
|
|
|
|
const modelOptionsByProvider = new Map<string, Option<string>[]>();
|
|
llmProviders.forEach((llmProvider) => {
|
|
const providerOptions = llmProvider.model_names.map((modelName) => {
|
|
return {
|
|
name: getDisplayNameForModel(modelName),
|
|
value: modelName,
|
|
};
|
|
});
|
|
modelOptionsByProvider.set(llmProvider.name, providerOptions);
|
|
});
|
|
|
|
const personaCurrentToolIds =
|
|
existingPersona?.tools.map((tool) => tool.id) || [];
|
|
|
|
const searchTool = findSearchTool(tools);
|
|
const imageGenerationTool = findImageGenerationTool(tools);
|
|
const internetSearchTool = findInternetSearchTool(tools);
|
|
|
|
const customTools = tools.filter(
|
|
(tool) =>
|
|
tool.in_code_tool_id !== searchTool?.in_code_tool_id &&
|
|
tool.in_code_tool_id !== imageGenerationTool?.in_code_tool_id &&
|
|
tool.in_code_tool_id !== internetSearchTool?.in_code_tool_id
|
|
);
|
|
|
|
const availableTools = [
|
|
...customTools,
|
|
...(searchTool ? [searchTool] : []),
|
|
...(imageGenerationTool ? [imageGenerationTool] : []),
|
|
...(internetSearchTool ? [internetSearchTool] : []),
|
|
];
|
|
const enabledToolsMap: { [key: number]: boolean } = {};
|
|
availableTools.forEach((tool) => {
|
|
enabledToolsMap[tool.id] = personaCurrentToolIds.includes(tool.id);
|
|
});
|
|
|
|
const [showVisibilityWarning, setShowVisibilityWarning] = useState(false);
|
|
|
|
const initialValues = {
|
|
name: existingPersona?.name ?? "",
|
|
description: existingPersona?.description ?? "",
|
|
datetime_aware: existingPrompt?.datetime_aware ?? false,
|
|
system_prompt: existingPrompt?.system_prompt ?? "",
|
|
task_prompt: existingPrompt?.task_prompt ?? "",
|
|
is_public: existingPersona?.is_public ?? defaultPublic,
|
|
document_set_ids:
|
|
existingPersona?.document_sets?.map((documentSet) => documentSet.id) ??
|
|
([] as number[]),
|
|
num_chunks: existingPersona?.num_chunks ?? null,
|
|
search_start_date: existingPersona?.search_start_date
|
|
? existingPersona?.search_start_date.toString().split("T")[0]
|
|
: null,
|
|
include_citations: existingPersona?.prompts[0]?.include_citations ?? true,
|
|
llm_relevance_filter: existingPersona?.llm_relevance_filter ?? false,
|
|
llm_model_provider_override:
|
|
existingPersona?.llm_model_provider_override ?? null,
|
|
llm_model_version_override:
|
|
existingPersona?.llm_model_version_override ?? null,
|
|
starter_messages: existingPersona?.starter_messages?.length
|
|
? existingPersona.starter_messages
|
|
: [{ message: "" }],
|
|
enabled_tools_map: enabledToolsMap,
|
|
icon_color: existingPersona?.icon_color ?? defautIconColor,
|
|
icon_shape: existingPersona?.icon_shape ?? defaultIconShape,
|
|
uploaded_image: null,
|
|
labels: existingPersona?.labels ?? null,
|
|
|
|
// EE Only
|
|
label_ids: existingPersona?.labels?.map((label) => label.id) ?? [],
|
|
selectedUsers:
|
|
existingPersona?.users?.filter(
|
|
(u) => u.id !== existingPersona.owner?.id
|
|
) ?? [],
|
|
selectedGroups: existingPersona?.groups ?? [],
|
|
is_default_persona: existingPersona?.is_default_persona ?? false,
|
|
};
|
|
|
|
interface AssistantPrompt {
|
|
message: string;
|
|
name: string;
|
|
}
|
|
|
|
const debouncedRefreshPrompts = debounce(
|
|
async (formValues: any, setFieldValue: any) => {
|
|
if (!autoStarterMessageEnabled) {
|
|
return;
|
|
}
|
|
setIsRefreshing(true);
|
|
try {
|
|
const response = await fetch("/api/persona/assistant-prompt-refresh", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
name: formValues.name || "",
|
|
description: formValues.description || "",
|
|
document_set_ids: formValues.document_set_ids || [],
|
|
instructions:
|
|
formValues.system_prompt || formValues.task_prompt || "",
|
|
generation_count:
|
|
4 -
|
|
formValues.starter_messages.filter(
|
|
(message: StarterMessage) => message.message.trim() !== ""
|
|
).length,
|
|
}),
|
|
});
|
|
|
|
const data: AssistantPrompt[] = await response.json();
|
|
if (response.ok) {
|
|
const filteredStarterMessages = formValues.starter_messages.filter(
|
|
(message: StarterMessage) => message.message.trim() !== ""
|
|
);
|
|
setFieldValue("starter_messages", [
|
|
...filteredStarterMessages,
|
|
...data,
|
|
]);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to refresh prompts:", error);
|
|
} finally {
|
|
setIsRefreshing(false);
|
|
}
|
|
},
|
|
1000
|
|
);
|
|
|
|
const [labelToDelete, setLabelToDelete] = useState<PersonaLabel | null>(null);
|
|
const [isRequestSuccessful, setIsRequestSuccessful] = useState(false);
|
|
|
|
const { data: userGroups } = useUserGroups();
|
|
|
|
const { data: users } = useSWR<MinimalUserSnapshot[]>(
|
|
"/api/users",
|
|
errorHandlingFetcher
|
|
);
|
|
|
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
|
|
if (!labels) {
|
|
return <></>;
|
|
}
|
|
|
|
const openDeleteModal = () => {
|
|
setDeleteModalOpen(true);
|
|
};
|
|
|
|
const closeDeleteModal = () => {
|
|
setDeleteModalOpen(false);
|
|
};
|
|
|
|
const handleDeletePersona = async () => {
|
|
if (existingPersona) {
|
|
const response = await deletePersona(existingPersona.id);
|
|
if (response.ok) {
|
|
await refreshAssistants();
|
|
router.push(
|
|
isAdminPage ? `/admin/assistants?u=${Date.now()}` : `/chat`
|
|
);
|
|
} else {
|
|
setPopup({
|
|
type: "error",
|
|
message: `Failed to delete persona - ${await response.text()}`,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="mx-auto max-w-4xl">
|
|
<style>
|
|
{`
|
|
.assistant-editor input::placeholder,
|
|
.assistant-editor textarea::placeholder {
|
|
opacity: 0.5;
|
|
}
|
|
`}
|
|
</style>
|
|
{!admin && (
|
|
<div className="absolute top-4 left-4">
|
|
<BackButton />
|
|
</div>
|
|
)}
|
|
|
|
{labelToDelete && (
|
|
<ConfirmEntityModal
|
|
entityType="label"
|
|
entityName={labelToDelete.name}
|
|
onClose={() => setLabelToDelete(null)}
|
|
onSubmit={async () => {
|
|
const response = await deleteLabel(labelToDelete.id);
|
|
if (response?.ok) {
|
|
setPopup({
|
|
message: `Label deleted successfully`,
|
|
type: "success",
|
|
});
|
|
await refreshLabels();
|
|
} else {
|
|
setPopup({
|
|
message: `Failed to delete label - ${await response.text()}`,
|
|
type: "error",
|
|
});
|
|
}
|
|
setLabelToDelete(null);
|
|
}}
|
|
/>
|
|
)}
|
|
{deleteModalOpen && existingPersona && (
|
|
<ConfirmEntityModal
|
|
entityType="Persona"
|
|
entityName={existingPersona.name}
|
|
onClose={closeDeleteModal}
|
|
onSubmit={handleDeletePersona}
|
|
/>
|
|
)}
|
|
{popup}
|
|
<Formik
|
|
enableReinitialize={true}
|
|
initialValues={initialValues}
|
|
validationSchema={Yup.object()
|
|
.shape({
|
|
name: Yup.string().required(
|
|
"Must provide a name for the Assistant"
|
|
),
|
|
description: Yup.string().required(
|
|
"Must provide a description for the Assistant"
|
|
),
|
|
system_prompt: Yup.string(),
|
|
task_prompt: Yup.string(),
|
|
is_public: Yup.boolean().required(),
|
|
document_set_ids: Yup.array().of(Yup.number()),
|
|
num_chunks: Yup.number().nullable(),
|
|
include_citations: Yup.boolean().required(),
|
|
llm_relevance_filter: Yup.boolean().required(),
|
|
llm_model_version_override: Yup.string().nullable(),
|
|
llm_model_provider_override: Yup.string().nullable(),
|
|
starter_messages: Yup.array().of(
|
|
Yup.object().shape({
|
|
message: Yup.string(),
|
|
})
|
|
),
|
|
search_start_date: Yup.date().nullable(),
|
|
icon_color: Yup.string(),
|
|
icon_shape: Yup.number(),
|
|
uploaded_image: Yup.mixed().nullable(),
|
|
// EE Only
|
|
label_ids: Yup.array().of(Yup.number()),
|
|
selectedUsers: Yup.array().of(Yup.object()),
|
|
selectedGroups: Yup.array().of(Yup.number()),
|
|
is_default_persona: Yup.boolean().required(),
|
|
})
|
|
.test(
|
|
"system-prompt-or-task-prompt",
|
|
"Must provide either Instructions or Reminders (Advanced)",
|
|
function (values) {
|
|
const systemPromptSpecified =
|
|
values.system_prompt && values.system_prompt.trim().length > 0;
|
|
const taskPromptSpecified =
|
|
values.task_prompt && values.task_prompt.trim().length > 0;
|
|
|
|
if (systemPromptSpecified || taskPromptSpecified) {
|
|
return true;
|
|
}
|
|
|
|
return this.createError({
|
|
path: "system_prompt",
|
|
message:
|
|
"Must provide either Instructions or Reminders (Advanced)",
|
|
});
|
|
}
|
|
)
|
|
.test(
|
|
"default-persona-public",
|
|
"Default persona must be public",
|
|
function (values) {
|
|
if (values.is_default_persona && !values.is_public) {
|
|
return this.createError({
|
|
path: "is_public",
|
|
message: "Default persona must be public",
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
)}
|
|
onSubmit={async (values, formikHelpers) => {
|
|
if (
|
|
values.llm_model_provider_override &&
|
|
!values.llm_model_version_override
|
|
) {
|
|
setPopup({
|
|
type: "error",
|
|
message:
|
|
"Must select a model if a non-default LLM provider is chosen.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
formikHelpers.setSubmitting(true);
|
|
let enabledTools = Object.keys(values.enabled_tools_map)
|
|
.map((toolId) => Number(toolId))
|
|
.filter((toolId) => values.enabled_tools_map[toolId]);
|
|
|
|
const searchToolEnabled = searchTool
|
|
? enabledTools.includes(searchTool.id)
|
|
: false;
|
|
|
|
// if disable_retrieval is set, set num_chunks to 0
|
|
// to tell the backend to not fetch any documents
|
|
const numChunks = searchToolEnabled ? values.num_chunks || 10 : 0;
|
|
const starterMessages = values.starter_messages
|
|
.filter(
|
|
(message: { message: string }) => message.message.trim() !== ""
|
|
)
|
|
.map((message: { message: string; name?: string }) => ({
|
|
message: message.message,
|
|
name: message.message,
|
|
}));
|
|
|
|
// don't set groups if marked as public
|
|
const groups = values.is_public ? [] : values.selectedGroups;
|
|
const submissionData: PersonaUpsertParameters = {
|
|
...values,
|
|
existing_prompt_id: existingPrompt?.id ?? null,
|
|
starter_messages: starterMessages,
|
|
groups: groups,
|
|
users: values.is_public
|
|
? undefined
|
|
: [
|
|
...(user && !checkUserIsNoAuthUser(user.id) ? [user.id] : []),
|
|
...values.selectedUsers.map((u: MinimalUserSnapshot) => u.id),
|
|
],
|
|
tool_ids: enabledTools,
|
|
remove_image: removePersonaImage,
|
|
search_start_date: values.search_start_date
|
|
? new Date(values.search_start_date)
|
|
: null,
|
|
num_chunks: numChunks,
|
|
};
|
|
|
|
let personaResponse;
|
|
if (isUpdate) {
|
|
personaResponse = await updatePersona(
|
|
existingPersona.id,
|
|
submissionData
|
|
);
|
|
} else {
|
|
personaResponse = await createPersona(submissionData);
|
|
}
|
|
|
|
let error = null;
|
|
|
|
if (!personaResponse) {
|
|
error = "Failed to create Assistant - no response received";
|
|
} else if (!personaResponse.ok) {
|
|
error = await personaResponse.text();
|
|
}
|
|
|
|
if (error || !personaResponse) {
|
|
setPopup({
|
|
type: "error",
|
|
message: `Failed to create Assistant - ${error}`,
|
|
});
|
|
formikHelpers.setSubmitting(false);
|
|
} else {
|
|
const assistant = await personaResponse.json();
|
|
const assistantId = assistant.id;
|
|
if (
|
|
shouldAddAssistantToUserPreferences &&
|
|
user?.preferences?.chosen_assistants
|
|
) {
|
|
const success = await addAssistantToList(assistantId);
|
|
if (success) {
|
|
setPopup({
|
|
message: `"${assistant.name}" has been added to your list.`,
|
|
type: "success",
|
|
});
|
|
await refreshAssistants();
|
|
} else {
|
|
setPopup({
|
|
message: `"${assistant.name}" could not be added to your list.`,
|
|
type: "error",
|
|
});
|
|
}
|
|
}
|
|
|
|
await refreshAssistants();
|
|
|
|
router.push(
|
|
isAdminPage
|
|
? `/admin/assistants?u=${Date.now()}`
|
|
: `/chat?assistantId=${assistantId}`
|
|
);
|
|
setIsRequestSuccessful(true);
|
|
}
|
|
}}
|
|
>
|
|
{({
|
|
isSubmitting,
|
|
values,
|
|
setFieldValue,
|
|
errors,
|
|
...formikProps
|
|
}: FormikProps<any>) => {
|
|
function toggleToolInValues(toolId: number) {
|
|
const updatedEnabledToolsMap = {
|
|
...values.enabled_tools_map,
|
|
[toolId]: !values.enabled_tools_map[toolId],
|
|
};
|
|
setFieldValue("enabled_tools_map", updatedEnabledToolsMap);
|
|
}
|
|
|
|
// model must support image input for image generation
|
|
// to work
|
|
const currentLLMSupportsImageOutput = checkLLMSupportsImageInput(
|
|
values.llm_model_version_override || defaultModelName || ""
|
|
);
|
|
|
|
return (
|
|
<Form className="w-full text-text-950 assistant-editor">
|
|
{/* Refresh starter messages when name or description changes */}
|
|
<p className="text-base font-normal text-2xl">
|
|
{existingPersona ? (
|
|
<>
|
|
Edit assistant <b>{existingPersona.name}</b>
|
|
</>
|
|
) : (
|
|
"Create an Assistant"
|
|
)}
|
|
</p>
|
|
<div className="max-w-4xl w-full">
|
|
<Separator />
|
|
<div className="flex gap-x-2 items-center">
|
|
<div className="block font-medium text-sm">
|
|
Assistant Icon
|
|
</div>
|
|
</div>
|
|
<SubLabel>
|
|
The icon that will visually represent your Assistant
|
|
</SubLabel>
|
|
<div className="flex gap-x-2 items-center">
|
|
<div
|
|
className="p-4 cursor-pointer rounded-full flex "
|
|
style={{
|
|
borderStyle: "dashed",
|
|
borderWidth: "1.5px",
|
|
borderSpacing: "4px",
|
|
}}
|
|
>
|
|
{values.uploaded_image ? (
|
|
<img
|
|
src={URL.createObjectURL(values.uploaded_image)}
|
|
alt="Uploaded assistant icon"
|
|
className="w-12 h-12 rounded-full object-cover"
|
|
/>
|
|
) : existingPersona?.uploaded_image_id &&
|
|
!removePersonaImage ? (
|
|
<img
|
|
src={buildImgUrl(existingPersona?.uploaded_image_id)}
|
|
alt="Uploaded assistant icon"
|
|
className="w-12 h-12 rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
generateIdenticon((values.icon_shape || 0).toString(), 36)
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-xs flex justify-start gap-x-2"
|
|
onClick={() => {
|
|
const fileInput = document.createElement("input");
|
|
fileInput.type = "file";
|
|
fileInput.accept = "image/*";
|
|
fileInput.onchange = (e) => {
|
|
const file = (e.target as HTMLInputElement)
|
|
.files?.[0];
|
|
if (file) {
|
|
setFieldValue("uploaded_image", file);
|
|
}
|
|
};
|
|
fileInput.click();
|
|
}}
|
|
>
|
|
<CameraIcon size={14} />
|
|
Upload {values.uploaded_image && "New "}Image
|
|
</Button>
|
|
|
|
{values.uploaded_image && (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
className="flex justify-start gap-x-2 text-xs"
|
|
onClick={() => {
|
|
setFieldValue("uploaded_image", null);
|
|
setRemovePersonaImage(false);
|
|
}}
|
|
>
|
|
<TrashIcon className="h-3 w-3" />
|
|
{removePersonaImage ? "Revert to Previous " : "Remove "}
|
|
Image
|
|
</Button>
|
|
)}
|
|
|
|
{!values.uploaded_image &&
|
|
(!existingPersona?.uploaded_image_id ||
|
|
removePersonaImage) && (
|
|
<Button
|
|
type="button"
|
|
className="text-xs"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
const newShape = generateRandomIconShape();
|
|
const randomColor =
|
|
colorOptions[
|
|
Math.floor(Math.random() * colorOptions.length)
|
|
];
|
|
setFieldValue("icon_shape", newShape.encodedGrid);
|
|
setFieldValue("icon_color", randomColor);
|
|
}}
|
|
>
|
|
<NewChatIcon size={14} />
|
|
Generate Icon
|
|
</Button>
|
|
)}
|
|
|
|
{existingPersona?.uploaded_image_id &&
|
|
removePersonaImage &&
|
|
!values.uploaded_image && (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="text-xs"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setRemovePersonaImage(false);
|
|
setFieldValue("uploaded_image", null);
|
|
}}
|
|
>
|
|
<SwapIcon className="h-3 w-3" />
|
|
Revert to Previous Image
|
|
</Button>
|
|
)}
|
|
|
|
{existingPersona?.uploaded_image_id &&
|
|
!removePersonaImage &&
|
|
!values.uploaded_image && (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="text-xs"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setRemovePersonaImage(true);
|
|
}}
|
|
>
|
|
<TrashIcon className="h-3 w-3" />
|
|
Remove Image
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<TextFormField
|
|
maxWidth="max-w-lg"
|
|
name="name"
|
|
label="Name"
|
|
placeholder="Email Assistant"
|
|
aria-label="assistant-name-input"
|
|
className="[&_input]:placeholder:text-text-muted/50"
|
|
/>
|
|
|
|
<TextFormField
|
|
maxWidth="max-w-lg"
|
|
name="description"
|
|
label="Description"
|
|
placeholder="Use this Assistant to help draft professional emails"
|
|
className="[&_input]:placeholder:text-text-muted/50"
|
|
/>
|
|
|
|
<Separator />
|
|
|
|
<TextFormField
|
|
maxWidth="max-w-4xl"
|
|
name="system_prompt"
|
|
label="Instructions"
|
|
isTextArea={true}
|
|
placeholder="You are a professional email writing assistant that always uses a polite enthusiastic tone, emphasizes action items, and leaves blanks for the human to fill in when you have unknowns"
|
|
data-testid="assistant-instructions-input"
|
|
className="[&_textarea]:placeholder:text-text-muted/50"
|
|
/>
|
|
|
|
<div className="w-full max-w-4xl">
|
|
<div className="flex flex-col">
|
|
{searchTool && (
|
|
<>
|
|
<Separator />
|
|
<div className="flex gap-x-2 py-2 flex justify-start">
|
|
<div>
|
|
<div
|
|
className="flex items-start gap-x-2
|
|
"
|
|
>
|
|
<p className="block font-medium text-sm">
|
|
Knowledge
|
|
</p>
|
|
<div className="flex items-center">
|
|
<TooltipProvider delayDuration={0}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div
|
|
className={`${
|
|
ccPairs.length === 0
|
|
? "opacity-70 cursor-not-allowed"
|
|
: ""
|
|
}`}
|
|
>
|
|
<SwitchField
|
|
size="sm"
|
|
onCheckedChange={(checked) => {
|
|
setFieldValue("num_chunks", null);
|
|
toggleToolInValues(searchTool.id);
|
|
}}
|
|
name={`enabled_tools_map.${searchTool.id}`}
|
|
disabled={ccPairs.length === 0}
|
|
/>
|
|
</div>
|
|
</TooltipTrigger>
|
|
|
|
{ccPairs.length === 0 && (
|
|
<TooltipContent side="top" align="center">
|
|
<p className="bg-background-900 max-w-[200px] text-sm rounded-lg p-1.5 text-white">
|
|
To use the Knowledge Action, you need to
|
|
have at least one Connector configured.
|
|
</p>
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
</div>
|
|
<p className="text-sm text-neutral-700 dark:text-neutral-400">
|
|
Attach additional unique knowledge to this assistant
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
{ccPairs.length > 0 &&
|
|
searchTool &&
|
|
values.enabled_tools_map[searchTool.id] &&
|
|
!(user?.role != "admin" && documentSets.length === 0) && (
|
|
<CollapsibleSection>
|
|
<div className="mt-2">
|
|
{ccPairs.length > 0 && (
|
|
<>
|
|
<Label small>Document Sets</Label>
|
|
<div>
|
|
<SubLabel>
|
|
<>
|
|
Select which{" "}
|
|
{!user || user.role === "admin" ? (
|
|
<Link
|
|
href="/admin/documents/sets"
|
|
className="font-semibold underline hover:underline text-text"
|
|
target="_blank"
|
|
>
|
|
Document Sets
|
|
</Link>
|
|
) : (
|
|
"Document Sets"
|
|
)}{" "}
|
|
this Assistant should use to inform its
|
|
responses. If none are specified, the
|
|
Assistant will reference all available
|
|
documents.
|
|
</>
|
|
</SubLabel>
|
|
</div>
|
|
|
|
{documentSets.length > 0 ? (
|
|
<FieldArray
|
|
name="document_set_ids"
|
|
render={(arrayHelpers: ArrayHelpers) => (
|
|
<div>
|
|
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
|
|
{documentSets.map((documentSet) => (
|
|
<DocumentSetSelectable
|
|
key={documentSet.id}
|
|
documentSet={documentSet}
|
|
isSelected={values.document_set_ids.includes(
|
|
documentSet.id
|
|
)}
|
|
onSelect={() => {
|
|
const index =
|
|
values.document_set_ids.indexOf(
|
|
documentSet.id
|
|
);
|
|
if (index !== -1) {
|
|
arrayHelpers.remove(index);
|
|
} else {
|
|
arrayHelpers.push(
|
|
documentSet.id
|
|
);
|
|
}
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
/>
|
|
) : (
|
|
<p className="text-sm">
|
|
<Link
|
|
href="/admin/documents/sets/new"
|
|
className="text-primary hover:underline"
|
|
>
|
|
+ Create Document Set
|
|
</Link>
|
|
</p>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</CollapsibleSection>
|
|
)}
|
|
|
|
<Separator />
|
|
<div className="py-2">
|
|
<p className="block font-medium text-sm mb-2">Actions</p>
|
|
|
|
{imageGenerationTool && (
|
|
<>
|
|
<div className="flex items-center content-start mb-2">
|
|
<BooleanFormField
|
|
name={`enabled_tools_map.${imageGenerationTool.id}`}
|
|
label={imageGenerationTool.display_name}
|
|
subtext="Generate and manipulate images using AI-powered tools"
|
|
disabled={
|
|
!currentLLMSupportsImageOutput ||
|
|
!isImageGenerationAvailable
|
|
}
|
|
disabledTooltip={
|
|
!currentLLMSupportsImageOutput
|
|
? "To use Image Generation, select GPT-4 or another image compatible model as the default model for this Assistant."
|
|
: "Image Generation requires an OpenAI or Azure Dall-E configuration."
|
|
}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{internetSearchTool && (
|
|
<>
|
|
<BooleanFormField
|
|
name={`enabled_tools_map.${internetSearchTool.id}`}
|
|
label={internetSearchTool.display_name}
|
|
subtext="Access real-time information and search the web for up-to-date results"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{customTools.length > 0 &&
|
|
customTools.map((tool) => (
|
|
<BooleanFormField
|
|
key={tool.id}
|
|
name={`enabled_tools_map.${tool.id}`}
|
|
label={tool.display_name}
|
|
subtext={tool.description}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Separator className="max-w-4xl mt-0" />
|
|
|
|
<div className="-mt-2">
|
|
<div className="flex gap-x-2 mb-2 items-center">
|
|
<div className="block font-medium text-sm">Default Model</div>
|
|
</div>
|
|
<LLMSelector
|
|
llmProviders={llmProviders}
|
|
currentLlm={
|
|
values.llm_model_version_override
|
|
? structureValue(
|
|
values.llm_model_provider_override,
|
|
"",
|
|
values.llm_model_version_override
|
|
)
|
|
: null
|
|
}
|
|
requiresImageGeneration={
|
|
imageGenerationTool
|
|
? values.enabled_tools_map[imageGenerationTool.id]
|
|
: false
|
|
}
|
|
onSelect={(selected) => {
|
|
if (selected === null) {
|
|
setFieldValue("llm_model_version_override", null);
|
|
setFieldValue("llm_model_provider_override", null);
|
|
} else {
|
|
const { modelName, provider, name } =
|
|
destructureValue(selected);
|
|
if (modelName && name) {
|
|
setFieldValue("llm_model_version_override", modelName);
|
|
setFieldValue("llm_model_provider_override", name);
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
<AdvancedOptionsToggle
|
|
showAdvancedOptions={showAdvancedOptions}
|
|
setShowAdvancedOptions={setShowAdvancedOptions}
|
|
/>
|
|
{showAdvancedOptions && (
|
|
<>
|
|
<div className="max-w-4xl w-full">
|
|
{user?.role == UserRole.ADMIN && (
|
|
<BooleanFormField
|
|
onChange={(checked) => {
|
|
if (checked) {
|
|
setFieldValue("is_public", true);
|
|
setFieldValue("is_default_persona", true);
|
|
}
|
|
}}
|
|
name="is_default_persona"
|
|
label="Featured Assistant"
|
|
subtext="If set, this assistant will be pinned for all new users and appear in the Featured list in the assistant explorer. This also makes the assistant public."
|
|
/>
|
|
)}
|
|
|
|
<Separator />
|
|
|
|
<div className="flex gap-x-2 items-center ">
|
|
<div className="block font-medium text-sm">Access</div>
|
|
</div>
|
|
<SubLabel>
|
|
Control who can access and use this assistant
|
|
</SubLabel>
|
|
|
|
<div className="min-h-[100px]">
|
|
<div className="flex items-center mb-2">
|
|
<TooltipProvider delayDuration={0}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div>
|
|
<SwitchField
|
|
name="is_public"
|
|
size="md"
|
|
onCheckedChange={(checked) => {
|
|
if (values.is_default_persona && !checked) {
|
|
setShowVisibilityWarning(true);
|
|
} else {
|
|
setFieldValue("is_public", checked);
|
|
if (!checked) {
|
|
// Even though this code path should not be possible,
|
|
// we set the default persona to false to be safe
|
|
setFieldValue(
|
|
"is_default_persona",
|
|
false
|
|
);
|
|
}
|
|
if (checked) {
|
|
setFieldValue("selectedUsers", []);
|
|
setFieldValue("selectedGroups", []);
|
|
}
|
|
}
|
|
}}
|
|
disabled={values.is_default_persona}
|
|
/>
|
|
</div>
|
|
</TooltipTrigger>
|
|
{values.is_default_persona && (
|
|
<TooltipContent side="top" align="center">
|
|
Default persona must be public. Set
|
|
"Default Persona" to false to change
|
|
visibility.
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
<span className="text-sm ml-2">
|
|
{values.is_public ? "Public" : "Private"}
|
|
</span>
|
|
</div>
|
|
|
|
{showVisibilityWarning && (
|
|
<div className="flex items-center text-warning mt-2">
|
|
<InfoIcon size={16} className="mr-2" />
|
|
<span className="text-sm">
|
|
Default persona must be public. Visibility has been
|
|
automatically set to public.
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{values.is_public ? (
|
|
<p className="text-sm text-text-dark">
|
|
Anyone from your team can view and use this assistant
|
|
</p>
|
|
) : (
|
|
<>
|
|
<div className="mt-2">
|
|
<Label className="mb-2" small>
|
|
Share with Users and Groups
|
|
</Label>
|
|
|
|
<SearchMultiSelectDropdown
|
|
options={[
|
|
...(Array.isArray(users) ? users : [])
|
|
.filter(
|
|
(u: MinimalUserSnapshot) =>
|
|
!values.selectedUsers.some(
|
|
(su: MinimalUserSnapshot) =>
|
|
su.id === u.id
|
|
) && u.id !== user?.id
|
|
)
|
|
.map((u: MinimalUserSnapshot) => ({
|
|
name: u.email,
|
|
value: u.id,
|
|
type: "user",
|
|
})),
|
|
...(userGroups || [])
|
|
.filter(
|
|
(g: UserGroup) =>
|
|
!values.selectedGroups.includes(g.id)
|
|
)
|
|
.map((g: UserGroup) => ({
|
|
name: g.name,
|
|
value: g.id,
|
|
type: "group",
|
|
})),
|
|
]}
|
|
onSelect={(
|
|
selected: DropdownOption<string | number>
|
|
) => {
|
|
const option = selected as {
|
|
name: string;
|
|
value: string | number;
|
|
type: "user" | "group";
|
|
};
|
|
if (option.type === "user") {
|
|
setFieldValue("selectedUsers", [
|
|
...values.selectedUsers,
|
|
{ id: option.value, email: option.name },
|
|
]);
|
|
} else {
|
|
setFieldValue("selectedGroups", [
|
|
...values.selectedGroups,
|
|
option.value,
|
|
]);
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{values.selectedUsers.map(
|
|
(user: MinimalUserSnapshot) => (
|
|
<SourceChip
|
|
key={user.id}
|
|
onRemove={() => {
|
|
setFieldValue(
|
|
"selectedUsers",
|
|
values.selectedUsers.filter(
|
|
(u: MinimalUserSnapshot) =>
|
|
u.id !== user.id
|
|
)
|
|
);
|
|
}}
|
|
title={user.email}
|
|
icon={<UserIcon size={12} />}
|
|
/>
|
|
)
|
|
)}
|
|
{values.selectedGroups.map((groupId: number) => {
|
|
const group = (userGroups || []).find(
|
|
(g: UserGroup) => g.id === groupId
|
|
);
|
|
return group ? (
|
|
<SourceChip
|
|
key={group.id}
|
|
title={group.name}
|
|
onRemove={() => {
|
|
setFieldValue(
|
|
"selectedGroups",
|
|
values.selectedGroups.filter(
|
|
(id: number) => id !== group.id
|
|
)
|
|
);
|
|
}}
|
|
icon={<GroupsIconSkeleton size={12} />}
|
|
/>
|
|
) : null;
|
|
})}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="w-full flex flex-col">
|
|
<div className="flex gap-x-2 items-center">
|
|
<div className="block font-medium text-sm">
|
|
[Optional] Starter Messages
|
|
</div>
|
|
</div>
|
|
|
|
<SubLabel>
|
|
Sample messages that help users understand what this
|
|
assistant can do and how to interact with it effectively.
|
|
New input fields will appear automatically as you type.
|
|
</SubLabel>
|
|
|
|
<div className="w-full">
|
|
<FieldArray
|
|
name="starter_messages"
|
|
render={(arrayHelpers: ArrayHelpers) => (
|
|
<StarterMessagesList
|
|
debouncedRefreshPrompts={() =>
|
|
debouncedRefreshPrompts(values, setFieldValue)
|
|
}
|
|
autoStarterMessageEnabled={
|
|
autoStarterMessageEnabled
|
|
}
|
|
isRefreshing={isRefreshing}
|
|
values={values.starter_messages}
|
|
arrayHelpers={arrayHelpers}
|
|
setFieldValue={setFieldValue}
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className=" w-full max-w-4xl">
|
|
<Separator />
|
|
<div className="flex gap-x-2 items-center mt-4 ">
|
|
<div className="block font-medium text-sm">Labels</div>
|
|
</div>
|
|
<p
|
|
className="text-sm text-subtle"
|
|
style={{ color: "rgb(113, 114, 121)" }}
|
|
>
|
|
Select labels to categorize this assistant
|
|
</p>
|
|
<div className="mt-3">
|
|
<SearchMultiSelectDropdown
|
|
onCreate={async (name: string) => {
|
|
await createLabel(name);
|
|
const currentLabels = await refreshLabels();
|
|
|
|
setTimeout(() => {
|
|
const newLabelId = currentLabels.find(
|
|
(l: { name: string }) => l.name === name
|
|
)?.id;
|
|
const updatedLabelIds = [
|
|
...values.label_ids,
|
|
newLabelId as number,
|
|
];
|
|
setFieldValue("label_ids", updatedLabelIds);
|
|
}, 300);
|
|
}}
|
|
options={Array.from(
|
|
new Set(labels.map((label) => label.name))
|
|
).map((name) => ({
|
|
name,
|
|
value: name,
|
|
}))}
|
|
onSelect={(selected) => {
|
|
const newLabelIds = [
|
|
...values.label_ids,
|
|
labels.find((l) => l.name === selected.value)
|
|
?.id as number,
|
|
];
|
|
setFieldValue("label_ids", newLabelIds);
|
|
}}
|
|
itemComponent={({ option }) => (
|
|
<div className="flex items-center justify-between px-4 py-3 text-sm hover:bg-accent-background-hovered cursor-pointer border-b border-border last:border-b-0">
|
|
<div
|
|
className="flex-grow"
|
|
onClick={() => {
|
|
const label = labels.find(
|
|
(l) => l.name === option.value
|
|
);
|
|
if (label) {
|
|
const isSelected = values.label_ids.includes(
|
|
label.id
|
|
);
|
|
const newLabelIds = isSelected
|
|
? values.label_ids.filter(
|
|
(id: number) => id !== label.id
|
|
)
|
|
: [...values.label_ids, label.id];
|
|
setFieldValue("label_ids", newLabelIds);
|
|
}
|
|
}}
|
|
>
|
|
<span className="font-normal leading-none">
|
|
{option.name}
|
|
</span>
|
|
</div>
|
|
{admin && (
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
const label = labels.find(
|
|
(l) => l.name === option.value
|
|
);
|
|
if (label) {
|
|
deleteLabel(label.id);
|
|
}
|
|
}}
|
|
className="ml-2 p-1 hover:bg-background-hover rounded"
|
|
>
|
|
<TrashIcon size={16} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
/>
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{values.label_ids.map((labelId: number) => {
|
|
const label = labels.find((l) => l.id === labelId);
|
|
return label ? (
|
|
<SourceChip
|
|
key={label.id}
|
|
onRemove={() => {
|
|
setFieldValue(
|
|
"label_ids",
|
|
values.label_ids.filter(
|
|
(id: number) => id !== label.id
|
|
)
|
|
);
|
|
}}
|
|
title={label.name}
|
|
icon={<TagIcon size={12} />}
|
|
/>
|
|
) : null;
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
|
|
<div className="flex flex-col gap-y-4">
|
|
<div className="flex flex-col gap-y-4">
|
|
<h3 className="font-medium text-sm">Knowledge Options</h3>
|
|
<div className="flex flex-col gap-y-4 ml-4">
|
|
<TextFormField
|
|
small={true}
|
|
name="num_chunks"
|
|
label="[Optional] Number of Context Documents"
|
|
placeholder="Default 10"
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
if (value === "" || /^[0-9]+$/.test(value)) {
|
|
setFieldValue("num_chunks", value);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<TextFormField
|
|
width="max-w-xl"
|
|
type="date"
|
|
small
|
|
subtext="Documents prior to this date will be ignored."
|
|
label="[Optional] Knowledge Cutoff Date"
|
|
name="search_start_date"
|
|
/>
|
|
|
|
<BooleanFormField
|
|
small
|
|
removeIndent
|
|
name="llm_relevance_filter"
|
|
label="AI Relevance Filter"
|
|
subtext="If enabled, the LLM will filter out documents that are not useful for answering the user query prior to generating a response. This typically improves the quality of the response but incurs slightly higher cost."
|
|
/>
|
|
|
|
<BooleanFormField
|
|
small
|
|
removeIndent
|
|
name="include_citations"
|
|
label="Citations"
|
|
subtext="Response will include citations ([1], [2], etc.) for documents referenced by the LLM. In general, we recommend to leave this enabled in order to increase trust in the LLM answer."
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
|
|
<BooleanFormField
|
|
small
|
|
removeIndent
|
|
name="datetime_aware"
|
|
label="Date and Time Aware"
|
|
subtext='Toggle this option to let the assistant know the current date and time (formatted like: "Thursday Jan 1, 1970 00:01"). To inject it in a specific place in the prompt, use the pattern [[CURRENT_DATETIME]]'
|
|
/>
|
|
|
|
<Separator />
|
|
|
|
<TextFormField
|
|
maxWidth="max-w-4xl"
|
|
name="task_prompt"
|
|
label="[Optional] Reminders"
|
|
isTextArea={true}
|
|
placeholder="Remember to reference all of the points mentioned in my message to you and focus on identifying action items that can move things forward"
|
|
onChange={(e) => {
|
|
setFieldValue("task_prompt", e.target.value);
|
|
}}
|
|
explanationText="Learn about prompting in our docs!"
|
|
explanationLink="https://docs.onyx.app/guides/assistants"
|
|
className="[&_textarea]:placeholder:text-text-muted/50"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<div className="mt-12 gap-x-2 w-full justify-end flex">
|
|
<Button
|
|
type="submit"
|
|
disabled={isSubmitting || isRequestSuccessful}
|
|
>
|
|
{isUpdate ? "Update" : "Create"}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => router.back()}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
{existingPersona && (
|
|
<Button
|
|
variant="destructive"
|
|
onClick={openDeleteModal}
|
|
type="button"
|
|
>
|
|
Delete
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Form>
|
|
);
|
|
}}
|
|
</Formik>
|
|
</div>
|
|
);
|
|
}
|