Update Assistants Creation UI (#1714)

* slide up "Tools"

* rework assistants page

* update layout

* reorg complete

- pending: useful header text?

* add tooltips

* alter organizational structure

* rm shadcn

* rm dependencies

* revalidate dependencies

* restore

* update component structure

* [s] format

* rm package json

* add package-lock.json [s]

* collapsible

* naming + width

* formatting

* formatting

* updated user flow

- Fix error/detail messages
- Fix tooltip delay
- Fix icons

* 1 -> 2

* naming fixes

* ran pretty

* fix build issue?

* web build issues?
This commit is contained in:
pablodanswer
2024-07-03 10:11:14 -07:00
committed by GitHub
parent a7da07afc0
commit ae4e643266
14 changed files with 736 additions and 531 deletions

BIN
web/public/Amazon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

8
web/public/Anthropic.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="176px" viewBox="0 0 256 176" version="1.1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<title>Anthropic</title>
<g fill="#181818">
<path d="M147.486878,0 C147.486878,0 217.568251,175.780074 217.568251,175.780074 C217.568251,175.780074 256,175.780074 256,175.780074 C256,175.780074 185.918621,0 185.918621,0 C185.918621,0 147.486878,0 147.486878,0 C147.486878,0 147.486878,0 147.486878,0 Z"></path>
<path d="M66.1828124,106.221191 C66.1828124,106.221191 90.1624677,44.4471185 90.1624677,44.4471185 C90.1624677,44.4471185 114.142128,106.221191 114.142128,106.221191 C114.142128,106.221191 66.1828124,106.221191 66.1828124,106.221191 C66.1828124,106.221191 66.1828124,106.221191 66.1828124,106.221191 Z M70.0705318,0 C70.0705318,0 0,175.780074 0,175.780074 C0,175.780074 39.179211,175.780074 39.179211,175.780074 C39.179211,175.780074 53.5097704,138.86606 53.5097704,138.86606 C53.5097704,138.86606 126.817544,138.86606 126.817544,138.86606 C126.817544,138.86606 141.145724,175.780074 141.145724,175.780074 C141.145724,175.780074 180.324935,175.780074 180.324935,175.780074 C180.324935,175.780074 110.254409,0 110.254409,0 C110.254409,0 70.0705318,0 70.0705318,0 C70.0705318,0 70.0705318,0 70.0705318,0 Z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
web/public/Azure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
web/public/OpenSource.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

1
web/public/Openai.svg Normal file
View File

@ -0,0 +1 @@
<svg viewBox="0 0 320 320" xmlns="http://www.w3.org/2000/svg"><path d="m297.06 130.97c7.26-21.79 4.76-45.66-6.85-65.48-17.46-30.4-52.56-46.04-86.84-38.68-15.25-17.18-37.16-26.95-60.13-26.81-35.04-.08-66.13 22.48-76.91 55.82-22.51 4.61-41.94 18.7-53.31 38.67-17.59 30.32-13.58 68.54 9.92 94.54-7.26 21.79-4.76 45.66 6.85 65.48 17.46 30.4 52.56 46.04 86.84 38.68 15.24 17.18 37.16 26.95 60.13 26.8 35.06.09 66.16-22.49 76.94-55.86 22.51-4.61 41.94-18.7 53.31-38.67 17.57-30.32 13.55-68.51-9.94-94.51zm-120.28 168.11c-14.03.02-27.62-4.89-38.39-13.88.49-.26 1.34-.73 1.89-1.07l63.72-36.8c3.26-1.85 5.26-5.32 5.24-9.07v-89.83l26.93 15.55c.29.14.48.42.52.74v74.39c-.04 33.08-26.83 59.9-59.91 59.97zm-128.84-55.03c-7.03-12.14-9.56-26.37-7.15-40.18.47.28 1.3.79 1.89 1.13l63.72 36.8c3.23 1.89 7.23 1.89 10.47 0l77.79-44.92v31.1c.02.32-.13.63-.38.83l-64.41 37.19c-28.69 16.52-65.33 6.7-81.92-21.95zm-16.77-139.09c7-12.16 18.05-21.46 31.21-26.29 0 .55-.03 1.52-.03 2.2v73.61c-.02 3.74 1.98 7.21 5.23 9.06l77.79 44.91-26.93 15.55c-.27.18-.61.21-.91.08l-64.42-37.22c-28.63-16.58-38.45-53.21-21.95-81.89zm221.26 51.49-77.79-44.92 26.93-15.54c.27-.18.61-.21.91-.08l64.42 37.19c28.68 16.57 38.51 53.26 21.94 81.94-7.01 12.14-18.05 21.44-31.2 26.28v-75.81c.03-3.74-1.96-7.2-5.2-9.06zm26.8-40.34c-.47-.29-1.3-.79-1.89-1.13l-63.72-36.8c-3.23-1.89-7.23-1.89-10.47 0l-77.79 44.92v-31.1c-.02-.32.13-.63.38-.83l64.41-37.16c28.69-16.55 65.37-6.7 81.91 22 6.99 12.12 9.52 26.31 7.15 40.1zm-168.51 55.43-26.94-15.55c-.29-.14-.48-.42-.52-.74v-74.39c.02-33.12 26.89-59.96 60.01-59.94 14.01 0 27.57 4.92 38.34 13.88-.49.26-1.33.73-1.89 1.07l-63.72 36.8c-3.26 1.85-5.26 5.31-5.24 9.06l-.04 89.79zm14.63-31.54 34.65-20.01 34.65 20v40.01l-34.65 20-34.65-20z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -20,11 +20,12 @@ import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
BooleanFormField, BooleanFormField,
Label,
SelectorFormField, SelectorFormField,
TextFormField, TextFormField,
} from "@/components/admin/connectors/Field"; } from "@/components/admin/connectors/Field";
import { HidableSection } from "./HidableSection"; import CollapsibleSection from "./CollapsibleSection";
import { FiPlus, FiX } from "react-icons/fi"; import { FiInfo, FiPlus, FiX } from "react-icons/fi";
import { useUserGroups } from "@/lib/hooks"; import { useUserGroups } from "@/lib/hooks";
import { Bubble } from "@/components/Bubble"; import { Bubble } from "@/components/Bubble";
import { GroupsIcon } from "@/components/icons/icons"; import { GroupsIcon } from "@/components/icons/icons";
@ -36,8 +37,13 @@ import { ToolSnapshot } from "@/lib/tools/interfaces";
import { checkUserIsNoAuthUser } from "@/lib/user"; import { checkUserIsNoAuthUser } from "@/lib/user";
import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences"; import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences";
import { checkLLMSupportsImageInput } from "@/lib/llm/utils"; import { checkLLMSupportsImageInput } from "@/lib/llm/utils";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled"; import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import {
TooltipProvider,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
function findSearchTool(tools: ToolSnapshot[]) { function findSearchTool(tools: ToolSnapshot[]) {
return tools.find((tool) => tool.in_code_tool_id === "SearchTool"); return tools.find((tool) => tool.in_code_tool_id === "SearchTool");
@ -47,12 +53,6 @@ function findImageGenerationTool(tools: ToolSnapshot[]) {
return tools.find((tool) => tool.in_code_tool_id === "ImageGenerationTool"); return tools.find((tool) => tool.in_code_tool_id === "ImageGenerationTool");
} }
function Label({ children }: { children: string | JSX.Element }) {
return (
<div className="block font-medium text-base text-emphasis">{children}</div>
);
}
function SubLabel({ children }: { children: string | JSX.Element }) { function SubLabel({ children }: { children: string | JSX.Element }) {
return <div className="text-sm text-subtle mb-2">{children}</div>; return <div className="text-sm text-subtle mb-2">{children}</div>;
} }
@ -202,9 +202,11 @@ export function AssistantEditor({
initialValues={initialValues} initialValues={initialValues}
validationSchema={Yup.object() validationSchema={Yup.object()
.shape({ .shape({
name: Yup.string().required("Must give the Assistant a name!"), name: Yup.string().required(
"Must provide a name for the Assistant"
),
description: Yup.string().required( description: Yup.string().required(
"Must give the Assistant a description!" "Must provide a description for the Assistant"
), ),
system_prompt: Yup.string(), system_prompt: Yup.string(),
task_prompt: Yup.string(), task_prompt: Yup.string(),
@ -227,29 +229,29 @@ export function AssistantEditor({
}) })
.test( .test(
"system-prompt-or-task-prompt", "system-prompt-or-task-prompt",
"Must provide at least one of System Prompt or Task Prompt", "Must provide either System Prompt or Additional Instructions",
(values) => { function (values) {
const systemPromptSpecified = values.system_prompt const systemPromptSpecified =
? values.system_prompt.length > 0 values.system_prompt && values.system_prompt.trim().length > 0;
: false; const taskPromptSpecified =
const taskPromptSpecified = values.task_prompt values.task_prompt && values.task_prompt.trim().length > 0;
? values.task_prompt.length > 0
: false;
if (systemPromptSpecified || taskPromptSpecified) {
setFinalPromptError("");
return true;
} // Return true if at least one field has a value
setFinalPromptError( if (systemPromptSpecified || taskPromptSpecified) {
"Must provide at least one of System Prompt or Task Prompt" return true;
); }
return this.createError({
path: "system_prompt",
message:
"Must provide either System Prompt or Additional Instructions",
});
} }
)} )}
onSubmit={async (values, formikHelpers) => { onSubmit={async (values, formikHelpers) => {
if (finalPromptError) { if (finalPromptError) {
setPopup({ setPopup({
type: "error", type: "error",
message: "Cannot submit while there are errors in the form!", message: "Cannot submit while there are errors in the form",
}); });
return; return;
} }
@ -392,28 +394,26 @@ export function AssistantEditor({
return ( return (
<Form> <Form>
<div className="pb-6"> <div className="pb-6">
<HidableSection sectionTitle="Basics">
<>
<TextFormField <TextFormField
name="name" name="name"
tooltip="Used to identify the Assistant in the UI."
label="Name" label="Name"
disabled={isUpdate} disabled={isUpdate}
subtext="Users will be able to select this Assistant based on this name." placeholder="e.g. 'Email Assistant'"
/> />
<TextFormField <TextFormField
tooltip="Used for identifying assistants and their use cases."
name="description" name="description"
label="Description" label="Description"
subtext="Provide a short descriptions which gives users a hint as to what they should use this Assistant for." placeholder="e.g. 'Use this Assistant to help draft professional emails'"
/> />
<TextFormField <TextFormField
tooltip="Gives your assistant a prime directive"
name="system_prompt" name="system_prompt"
label="System Prompt" label="System Prompt"
isTextArea={true} isTextArea={true}
subtext={ placeholder="e.g. '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'"
'Give general info about what the Assistant is about. For example, "You are an assistant for On-Call engineers. Your goal is to read the provided context documents and give recommendations as to how to resolve the issue."' //
}
onChange={(e) => { onChange={(e) => {
setFieldValue("system_prompt", e.target.value); setFieldValue("system_prompt", e.target.value);
triggerFinalPromptUpdate( triggerFinalPromptUpdate(
@ -425,46 +425,91 @@ export function AssistantEditor({
error={finalPromptError} error={finalPromptError}
/> />
<TextFormField <div className="mb-6">
name="task_prompt" <div className="flex gap-x-2 items-center">
label="Task Prompt (Optional)" <div className="block font-medium text-base">
isTextArea={true} LLM Provider{" "}
subtext={`Give specific instructions as to what to do with the user query. </div>
For example, "Find any relevant sections from the provided documents that can <TooltipProvider delayDuration={50}>
help the user resolve their issue and explain how they are relevant."`} <Tooltip>
onChange={(e) => { <TooltipTrigger>
setFieldValue("task_prompt", e.target.value); <FiInfo size={12} />
triggerFinalPromptUpdate( </TooltipTrigger>
values.system_prompt, <TooltipContent side="top" align="center">
e.target.value, <p className="bg-neutral-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
searchToolEnabled() Select a Large Language Model (Generative AI model)
to power this Assistant
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="mb-2 flex items-starts">
<div className="w-96">
<SelectorFormField
defaultValue={`Default (${defaultModelName})`}
name="llm_model_provider_override"
options={llmProviders.map((llmProvider) => ({
name: llmProvider.name,
value: llmProvider.name,
icon: llmProvider.icon,
}))}
includeDefault={true}
onSelect={(selected) => {
if (selected !== values.llm_model_provider_override) {
setFieldValue("llm_model_version_override", null);
}
setFieldValue(
"llm_model_provider_override",
selected
); );
}} }}
error={finalPromptError}
/> />
</div>
<Label>Final Prompt</Label> {values.llm_model_provider_override && (
<div className="w-96 ml-4">
{finalPrompt ? ( <SelectorFormField
<pre className="text-sm mt-2 whitespace-pre-wrap"> name="llm_model_version_override"
{finalPrompt} options={
</pre> modelOptionsByProvider.get(
) : ( values.llm_model_provider_override
"-" ) || []
}
maxHeight="max-h-72"
/>
</div>
)} )}
</> </div>
</HidableSection>
<Divider /> <div className="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);
}}
/>
)}
<HidableSection sectionTitle="Tools">
<>
{ccPairs.length > 0 && searchTool && ( {ccPairs.length > 0 && searchTool && (
<> <>
<BooleanFormField <BooleanFormField
name={`enabled_tools_map.${searchTool.id}`} name={`enabled_tools_map.${searchTool.id}`}
label="Search Tool" label="Search Tool"
subtext={`The Search Tool allows the Assistant to search through connected knowledge to help build an answer.`} noPadding
onChange={() => { onChange={() => {
setFieldValue("num_chunks", null); setFieldValue("num_chunks", null);
toggleToolInValues(searchTool.id); toggleToolInValues(searchTool.id);
@ -472,10 +517,11 @@ export function AssistantEditor({
/> />
{searchToolEnabled() && ( {searchToolEnabled() && (
<div className="pl-4 border-l-2 ml-4 border-border"> <CollapsibleSection prompt="Configure Search">
<div className=" ">
{ccPairs.length > 0 && ( {ccPairs.length > 0 && (
<> <>
<Label>Document Sets</Label> <Label small>Document Sets</Label>
<div> <div>
<SubLabel> <SubLabel>
@ -492,10 +538,11 @@ export function AssistantEditor({
) : ( ) : (
"Document Sets" "Document Sets"
)}{" "} )}{" "}
that this Assistant should search through. that this Assistant should search
If none are specified, the Assistant will through. If none are specified, the
search through all available documents in Assistant will search through all
order to try and respond to queries. available documents in order to try and
respond to queries.
</> </>
</SubLabel> </SubLabel>
</div> </div>
@ -546,21 +593,15 @@ export function AssistantEditor({
</Italic> </Italic>
)} )}
<> <div className="mt-6">
<TextFormField <TextFormField
small={true}
name="num_chunks" name="num_chunks"
label="Number of Chunks" label="Number of Chunks"
placeholder="If unspecified, will use 10 chunks." tooltip="How many chunks to feed the LLM"
subtext={ placeholder="Defaults to 10 chunks."
<div>
How many chunks should we feed into the
LLM when generating the final response?
Each chunk is ~400 words long.
</div>
}
onChange={(e) => { onChange={(e) => {
const value = e.target.value; const value = e.target.value;
// Allow only integer values
if ( if (
value === "" || value === "" ||
/^[0-9]+$/.test(value) /^[0-9]+$/.test(value)
@ -570,9 +611,10 @@ export function AssistantEditor({
}} }}
/> />
<Label>Misc</Label>
<BooleanFormField <BooleanFormField
small
noPadding
alignTop
name="llm_relevance_filter" name="llm_relevance_filter"
label="Apply LLM Relevance Filter" label="Apply LLM Relevance Filter"
subtext={ subtext={
@ -581,6 +623,9 @@ export function AssistantEditor({
/> />
<BooleanFormField <BooleanFormField
small
noPadding
alignTop
name="include_citations" name="include_citations"
label="Include Citations" label="Include Citations"
subtext={` subtext={`
@ -589,35 +634,15 @@ export function AssistantEditor({
the same technique used by the default Assistants. In general, we recommend the same technique used by the default Assistants. In general, we recommend
to leave this enabled in order to increase trust in the LLM answer.`} to leave this enabled in order to increase trust in the LLM answer.`}
/> />
</> </div>
</> </>
)} )}
</div> </div>
</CollapsibleSection>
)} )}
</> </>
)} )}
{imageGenerationTool &&
checkLLMSupportsImageInput(
providerDisplayNameToProviderName.get(
values.llm_model_provider_override || ""
) ||
defaultProviderName ||
"",
values.llm_model_version_override ||
defaultModelName ||
""
) && (
<BooleanFormField
name={`enabled_tools_map.${imageGenerationTool.id}`}
label="Image Generation Tool"
subtext="The Image Generation Tool allows the assistant to use DALL-E 3 to generate images. The tool will be used when the user asks the assistant to generate an image."
onChange={() => {
toggleToolInValues(imageGenerationTool.id);
}}
/>
)}
{customTools.length > 0 && ( {customTools.length > 0 && (
<> <>
{customTools.map((tool) => ( {customTools.map((tool) => (
@ -633,106 +658,40 @@ export function AssistantEditor({
))} ))}
</> </>
)} )}
</> </div>
</HidableSection> </div>
<Divider />
{llmProviders.length > 0 && ( {llmProviders.length > 0 && (
<> <>
<HidableSection <Divider />
sectionTitle="[Advanced] Model Selection"
defaultHidden
>
<>
<Text>
Pick which LLM to use for this Assistant. If left as
Default, will use{" "}
<b className="italic">{defaultModelName}</b>
.
<br />
<br />
For more information on the different LLMs, checkout
the{" "}
<a
href="https://platform.openai.com/docs/models"
target="_blank"
className="text-blue-500"
>
OpenAI docs
</a>
.
</Text>
<div className="flex mt-6"> <TextFormField
<div className="w-96"> name="task_prompt"
<SubLabel>LLM Provider</SubLabel> label="Additional instructions (Optional)"
<SelectorFormField isTextArea={true}
name="llm_model_provider_override" placeholder="e.g. 'Remember to reference all of the points mentioned in my message to you and focus on identifying action items that can move things forward'"
options={llmProviders.map((llmProvider) => ({ onChange={(e) => {
name: llmProvider.name, setFieldValue("task_prompt", e.target.value);
value: llmProvider.name, triggerFinalPromptUpdate(
}))} values.system_prompt,
includeDefault={true} e.target.value,
onSelect={(selected) => { searchToolEnabled()
if (
selected !==
values.llm_model_provider_override
) {
setFieldValue(
"llm_model_version_override",
null
);
}
setFieldValue(
"llm_model_provider_override",
selected
); );
}} }}
explanationText="Learn about prompting in our docs!"
explanationLink="https://docs.danswer.dev/guides/assistants"
/> />
</div>
{values.llm_model_provider_override && (
<div className="w-96 ml-4">
<SubLabel>Model</SubLabel>
<SelectorFormField
name="llm_model_version_override"
options={
modelOptionsByProvider.get(
values.llm_model_provider_override
) || []
}
maxHeight="max-h-72"
/>
</div>
)}
</div>
</>
</HidableSection>
<Divider />
</> </>
)} )}
<div className="mb-6">
<HidableSection <div className="flex gap-x-2 items-center">
sectionTitle="[Advanced] Starter Messages" <div className="block font-medium text-base">
defaultHidden Add Starter Messages (Optional){" "}
> </div>
<>
<div className="mb-4">
<SubLabel>
Starter Messages help guide users to use this Assistant.
They are shown to the user as clickable options when
they select this Assistant. When selected, the specified
message is sent to the LLM as the initial user message.
</SubLabel>
</div> </div>
<FieldArray <FieldArray
name="starter_messages" name="starter_messages"
render={( render={(arrayHelpers: ArrayHelpers<StarterMessage[]>) => (
arrayHelpers: ArrayHelpers<StarterMessage[]>
) => (
<div> <div>
{values.starter_messages && {values.starter_messages &&
values.starter_messages.length > 0 && values.starter_messages.length > 0 &&
@ -745,7 +704,7 @@ export function AssistantEditor({
<div className="flex"> <div className="flex">
<div className="w-full mr-6 border border-border p-3 rounded"> <div className="w-full mr-6 border border-border p-3 rounded">
<div> <div>
<Label>Name</Label> <Label small>Name</Label>
<SubLabel> <SubLabel>
Shows up as the &quot;title&quot; for Shows up as the &quot;title&quot; for
this Starter Message. For example, this Starter Message. For example,
@ -773,13 +732,12 @@ export function AssistantEditor({
</div> </div>
<div className="mt-3"> <div className="mt-3">
<Label>Description</Label> <Label small>Description</Label>
<SubLabel> <SubLabel>
A description which tells the user A description which tells the user what
what they might want to use this they might want to use this Starter
Starter Message for. For example Message for. For example &quot;to a
&quot;to a client about a new client about a new feature&quot;
feature&quot;
</SubLabel> </SubLabel>
<Field <Field
name={`starter_messages.${index}.description`} name={`starter_messages.${index}.description`}
@ -803,7 +761,7 @@ export function AssistantEditor({
</div> </div>
<div className="mt-3"> <div className="mt-3">
<Label>Message</Label> <Label small>Message</Label>
<SubLabel> <SubLabel>
The actual message to be sent as the The actual message to be sent as the
initial user message if a user selects initial user message if a user selects
@ -837,9 +795,7 @@ export function AssistantEditor({
<div className="my-auto"> <div className="my-auto">
<FiX <FiX
className="my-auto w-10 h-10 cursor-pointer hover:bg-hover rounded p-2" className="my-auto w-10 h-10 cursor-pointer hover:bg-hover rounded p-2"
onClick={() => onClick={() => arrayHelpers.remove(index)}
arrayHelpers.remove(index)
}
/> />
</div> </div>
</div> </div>
@ -866,18 +822,18 @@ export function AssistantEditor({
</div> </div>
)} )}
/> />
</> </div>
</HidableSection>
<Divider />
{isPaidEnterpriseFeaturesEnabled && {isPaidEnterpriseFeaturesEnabled &&
userGroups && userGroups &&
(!user || user.role === "admin") && ( (!user || user.role === "admin") && (
<> <>
<HidableSection sectionTitle="Access"> <Divider />
<>
<BooleanFormField <BooleanFormField
small
noPadding
alignTop
name="is_public" name="is_public"
label="Is Public?" label="Is Public?"
subtext="If set, this Assistant will be available to all users. If not, only the specified User Groups will be able to access it." subtext="If set, this Assistant will be available to all users. If not, only the specified User Groups will be able to access it."
@ -929,9 +885,6 @@ export function AssistantEditor({
</div> </div>
)} )}
</> </>
</HidableSection>
<Divider />
</>
)} )}
<div className="flex"> <div className="flex">

View File

@ -0,0 +1,55 @@
"use client";
import { Button } from "@tremor/react";
import React, { ReactNode, useState } from "react";
import { FiSettings } from "react-icons/fi";
interface CollapsibleSectionProps {
children: ReactNode;
prompt?: string;
className?: string;
}
const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
children,
prompt,
className = "",
}) => {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
const toggleCollapse = (e?: React.MouseEvent<HTMLDivElement>) => {
// Only toggle if the click is on the border or plus sign
if (
!e ||
e.currentTarget === e.target ||
(e.target as HTMLElement).classList.contains("collapse-toggle")
) {
setIsCollapsed(!isCollapsed);
}
};
return (
<div
className={`relative ${isCollapsed ? "h-6" : ""} ${className}`}
style={{ transition: "height 0.3s ease-out" }}
>
<div
className={`
cursor-pointer
${isCollapsed ? "h-6" : "pl-4 border-l-2 border-border"}
`}
onClick={toggleCollapse}
>
{isCollapsed ? (
<span className="collapse-toggle text-lg absolute left-0 top-0 text-sm flex items-center gap-x-3 cursor-pointer">
<FiSettings className="pointer-events-none my-auto" size={16} />
{prompt}{" "}
</span>
) : (
<>{children}</>
)}
</div>
</div>
);
};
export default CollapsibleSection;

View File

@ -44,7 +44,7 @@ export function HidableSection({
</div> </div>
</div> </div>
{!isHidden && <div className="mx-2 mt-2">{children}</div>} {!isHidden && <div className="mx-2 gap-y-2 mt-2">{children}</div>}
</div> </div>
); );
} }

View File

@ -30,12 +30,10 @@ export default async function Page() {
return ( return (
<div> <div>
<BackButton /> <BackButton />
<AdminPageTitle <AdminPageTitle
title="Create a New Persona" title="Create a New Assistant"
icon={<RobotIcon size={32} />} icon={<RobotIcon size={32} />}
/> />
{body} {body}
</div> </div>
); );

View File

@ -34,6 +34,7 @@ export interface FullLLMProvider extends LLMProvider {
id: number; id: number;
is_default_provider: boolean | null; is_default_provider: boolean | null;
model_names: string[]; model_names: string[];
icon?: React.FC<{ size?: number; className?: string }>;
} }
export interface LLMProviderDescriptor { export interface LLMProviderDescriptor {

View File

@ -8,6 +8,7 @@ export interface Option<T> {
value: T; value: T;
description?: string; description?: string;
metadata?: { [key: string]: any }; metadata?: { [key: string]: any };
icon?: React.FC<{ size?: number; className?: string }>;
} }
export type StringOrNumberOption = Option<string | number>; export type StringOrNumberOption = Option<string | number>;
@ -24,9 +25,7 @@ function StandardDropdownOption<T>({
return ( return (
<button <button
onClick={() => handleSelect(option)} onClick={() => handleSelect(option)}
className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800 ${ className={`w-full text-left block px-4 py-2.5 text-sm hover:bg-gray-800 ${index !== 0 ? " border-t-2 border-gray-600" : ""}`}
index !== 0 ? " border-t-2 border-gray-600" : ""
}`}
role="menuitem" role="menuitem"
> >
<p className="font-medium">{option.name}</p> <p className="font-medium">{option.name}</p>
@ -216,9 +215,7 @@ export const CustomDropdown = ({
{isOpen && ( {isOpen && (
<div <div
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className={`absolute ${ className={`absolute ${direction === "up" ? "bottom-full pb-2" : "pt-2"} w-full z-30 box-shadow`}
direction === "up" ? "bottom-full pb-2" : "pt-2 "
} w-full z-30 box-shadow`}
> >
{dropdown} {dropdown}
</div> </div>
@ -269,7 +266,7 @@ export function DefaultDropdownElement({
onChange={() => null} onChange={() => null}
/> />
)} )}
{icon && icon({ size: 16, className: "mr-2 my-auto" })} {icon && icon({ size: 16, className: "mr-2 h-4 w-4 my-auto" })}
{name} {name}
</div> </div>
{description && <div className="text-xs">{description}</div>} {description && <div className="text-xs">{description}</div>}
@ -290,11 +287,13 @@ export function DefaultDropdown({
includeDefault = false, includeDefault = false,
side, side,
maxHeight, maxHeight,
defaultValue,
}: { }: {
options: StringOrNumberOption[]; options: StringOrNumberOption[];
selected: string | null; selected: string | null;
onSelect: (value: string | number | null) => void; onSelect: (value: string | number | null) => void;
includeDefault?: boolean; includeDefault?: boolean;
defaultValue?: string;
side?: "top" | "right" | "bottom" | "left"; side?: "top" | "right" | "bottom" | "left";
maxHeight?: string; maxHeight?: string;
}) { }) {
@ -316,7 +315,7 @@ export function DefaultDropdown({
> >
<p className="line-clamp-1"> <p className="line-clamp-1">
{selectedOption?.name || {selectedOption?.name ||
(includeDefault ? "Default" : "Select an option...")} (includeDefault ? defaultValue ?? "Default" : "Select an option...")}
</p> </p>
<FiChevronDown className="my-auto ml-auto" /> <FiChevronDown className="my-auto ml-auto" />
</div> </div>
@ -354,6 +353,7 @@ export function DefaultDropdown({
description={option.description} description={option.description}
onSelect={() => onSelect(option.value)} onSelect={() => onSelect(option.value)}
isSelected={isSelected} isSelected={isSelected}
icon={option.icon}
/> />
); );
})} })}

View File

@ -10,7 +10,13 @@ import {
import * as Yup from "yup"; import * as Yup from "yup";
import { FormBodyBuilder } from "./types"; import { FormBodyBuilder } from "./types";
import { DefaultDropdown, StringOrNumberOption } from "@/components/Dropdown"; import { DefaultDropdown, StringOrNumberOption } from "@/components/Dropdown";
import { FiPlus, FiX } from "react-icons/fi"; import { FiInfo, FiPlus, FiX } from "react-icons/fi";
import {
TooltipProvider,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
export function SectionHeader({ export function SectionHeader({
children, children,
@ -20,8 +26,20 @@ export function SectionHeader({
return <div className="mb-4 font-bold text-lg">{children}</div>; return <div className="mb-4 font-bold text-lg">{children}</div>;
} }
export function Label({ children }: { children: string | JSX.Element }) { export function Label({
return <div className="block font-medium text-base">{children}</div>; children,
small,
}: {
children: string | JSX.Element;
small?: boolean;
}) {
return (
<div
className={`block font-medium base ${small ? "text-sm" : "text-base"}`}
>
{children}
</div>
);
} }
export function SubLabel({ children }: { children: string | JSX.Element }) { export function SubLabel({ children }: { children: string | JSX.Element }) {
@ -29,7 +47,48 @@ export function SubLabel({ children }: { children: string | JSX.Element }) {
} }
export function ManualErrorMessage({ children }: { children: string }) { export function ManualErrorMessage({ children }: { children: string }) {
return <div className="text-error text-sm mt-1">{children}</div>; return <div className="text-error text-sm">{children}</div>;
}
export function ExplanationText({
text,
link,
}: {
text: string;
link?: string;
}) {
return link ? (
<a
className="underline cursor-pointer text-sm font-medium"
target="_blank"
href={link}
>
{text}
</a>
) : (
<div className="text-sm font-semibold">{text}</div>
);
}
export function ToolTipDetails({
children,
}: {
children: string | JSX.Element;
}) {
return (
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<FiInfo size={12} />
</TooltipTrigger>
<TooltipContent side="top" align="center">
<p className="bg-background-dark max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-inverted">
{children}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
} }
export function TextFormField({ export function TextFormField({
@ -47,6 +106,10 @@ export function TextFormField({
isCode = false, isCode = false,
fontSize, fontSize,
hideError, hideError,
tooltip,
explanationText,
explanationLink,
small,
}: { }: {
name: string; name: string;
label: string; label: string;
@ -62,6 +125,10 @@ export function TextFormField({
isCode?: boolean; isCode?: boolean;
fontSize?: "text-sm" | "text-base" | "text-lg"; fontSize?: "text-sm" | "text-base" | "text-lg";
hideError?: boolean; hideError?: boolean;
tooltip?: string;
explanationText?: string;
explanationLink?: string;
small?: boolean;
}) { }) {
let heightString = defaultHeight || ""; let heightString = defaultHeight || "";
if (isTextArea && !heightString) { if (isTextArea && !heightString) {
@ -69,8 +136,25 @@ export function TextFormField({
} }
return ( return (
<div className="mb-4"> <div className="mb-6">
<Label>{label}</Label> <div className="flex gap-x-2 items-center">
<Label small={small}>{label}</Label>
{tooltip && <ToolTipDetails>{tooltip}</ToolTipDetails>}
{error ? (
<ManualErrorMessage>{error}</ManualErrorMessage>
) : (
!hideError && (
<ErrorMessage
name={name}
component="div"
className="text-error my-auto text-sm"
/>
)
)}
</div>
{subtext && <SubLabel>{subtext}</SubLabel>} {subtext && <SubLabel>{subtext}</SubLabel>}
<Field <Field
as={isTextArea ? "textarea" : "input"} as={isTextArea ? "textarea" : "input"}
@ -78,6 +162,7 @@ export function TextFormField({
name={name} name={name}
id={name} id={name}
className={` className={`
${small && "text-sm"}
border border
border-border border-border
rounded rounded
@ -95,16 +180,8 @@ export function TextFormField({
autoComplete={autoCompleteDisabled ? "off" : undefined} autoComplete={autoCompleteDisabled ? "off" : undefined}
{...(onChange ? { onChange } : {})} {...(onChange ? { onChange } : {})}
/> />
{error ? ( {explanationText && (
<ManualErrorMessage>{error}</ManualErrorMessage> <ExplanationText link={explanationLink} text={explanationText} />
) : (
!hideError && (
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-sm mt-1"
/>
)
)} )}
</div> </div>
); );
@ -115,6 +192,9 @@ interface BooleanFormFieldProps {
label: string; label: string;
subtext?: string | JSX.Element; subtext?: string | JSX.Element;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
noPadding?: boolean;
small?: boolean;
alignTop?: boolean;
} }
export const BooleanFormField = ({ export const BooleanFormField = ({
@ -122,6 +202,9 @@ export const BooleanFormField = ({
label, label,
subtext, subtext,
onChange, onChange,
noPadding,
small,
alignTop,
}: BooleanFormFieldProps) => { }: BooleanFormFieldProps) => {
return ( return (
<div className="mb-4"> <div className="mb-4">
@ -129,19 +212,18 @@ export const BooleanFormField = ({
<Field <Field
name={name} name={name}
type="checkbox" type="checkbox"
className="mx-3 px-5 w-3.5 h-3.5 my-auto" className={`${noPadding ? "mr-3" : "mx-3"} px-5 w-3.5 h-3.5 ${alignTop ? "mt-1" : "my-auto"}`}
{...(onChange ? { onChange } : {})} {...(onChange ? { onChange } : {})}
/> />
<div> <div>
<Label>{label}</Label> <Label small={small}>{label}</Label>
{subtext && <SubLabel>{subtext}</SubLabel>} {subtext && <SubLabel>{subtext}</SubLabel>}
</div> </div>
</label> </label>
<ErrorMessage <ErrorMessage
name={name} name={name}
component="div" component="div"
className="text-red-500 text-sm mt-1" className="text-error text-sm mt-1"
/> />
</div> </div>
); );
@ -252,6 +334,7 @@ interface SelectorFormFieldProps {
side?: "top" | "right" | "bottom" | "left"; side?: "top" | "right" | "bottom" | "left";
maxHeight?: string; maxHeight?: string;
onSelect?: (selected: string | number | null) => void; onSelect?: (selected: string | number | null) => void;
defaultValue?: string;
} }
export function SelectorFormField({ export function SelectorFormField({
@ -263,6 +346,7 @@ export function SelectorFormField({
side = "bottom", side = "bottom",
maxHeight, maxHeight,
onSelect, onSelect,
defaultValue,
}: SelectorFormFieldProps) { }: SelectorFormFieldProps) {
const [field] = useField<string>(name); const [field] = useField<string>(name);
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
@ -280,13 +364,14 @@ export function SelectorFormField({
includeDefault={includeDefault} includeDefault={includeDefault}
side={side} side={side}
maxHeight={maxHeight} maxHeight={maxHeight}
defaultValue={defaultValue}
/> />
</div> </div>
<ErrorMessage <ErrorMessage
name={name} name={name}
component="div" component="div"
className="text-red-500 text-sm mt-1" className="text-error text-sm mt-1"
/> />
</div> </div>
); );

View File

@ -44,6 +44,14 @@ import { SiBookstack } from "react-icons/si";
import Image from "next/image"; import Image from "next/image";
import jiraSVG from "../../../public/Jira.svg"; import jiraSVG from "../../../public/Jira.svg";
import confluenceSVG from "../../../public/Confluence.svg"; import confluenceSVG from "../../../public/Confluence.svg";
import openAISVG from "../../../public/Openai.svg";
import openSourceIcon from "../../../public/OpenSource.png";
import awsWEBP from "../../../public/Amazon.webp";
import azureIcon from "../../../public/Azure.png";
import anthropicSVG from "../../../public/Anthropic.svg";
import OCIStorageSVG from "../../../public/OCI.svg"; import OCIStorageSVG from "../../../public/OCI.svg";
import googleCloudStorageIcon from "../../../public/GoogleCloudStorage.png"; import googleCloudStorageIcon from "../../../public/GoogleCloudStorage.png";
import guruIcon from "../../../public/Guru.svg"; import guruIcon from "../../../public/Guru.svg";
@ -75,6 +83,48 @@ interface IconProps {
export const defaultTailwindCSS = "my-auto flex flex-shrink-0 text-default"; export const defaultTailwindCSS = "my-auto flex flex-shrink-0 text-default";
export const defaultTailwindCSSBlue = "my-auto flex flex-shrink-0 text-link"; export const defaultTailwindCSSBlue = "my-auto flex flex-shrink-0 text-link";
export const OpenAIIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
>
<Image src={openAISVG} alt="Logo" width="96" height="96" />
</div>
);
};
export const OpenSourceIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
>
<Image src={openSourceIcon} alt="Logo" width="96" height="96" />
</div>
);
};
export const AnthropicIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
>
<Image src={anthropicSVG} alt="Logo" width="96" height="96" />
</div>
);
};
export const PlugIcon = ({ export const PlugIcon = ({
size = 16, size = 16,
className = defaultTailwindCSS, className = defaultTailwindCSS,
@ -498,6 +548,36 @@ export const ProductboardIcon = ({
); );
}; };
export const AWSIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
// Linear Icon has a bit more surrounding whitespace than other icons, which is why we need to adjust it here
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
>
<Image src={awsWEBP} alt="Logo" width="96" height="96" />
</div>
);
};
export const AzureIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return (
<div
// Linear Icon has a bit more surrounding whitespace than other icons, which is why we need to adjust it here
style={{ width: `${size + 4}px`, height: `${size + 4}px` }}
className={`w-[${size + 4}px] h-[${size + 4}px] -m-0.5 ` + className}
>
<Image src={azureIcon} alt="Logo" width="96" height="96" />
</div>
);
};
export const LinearIcon = ({ export const LinearIcon = ({
size = 16, size = 16,
className = defaultTailwindCSS, className = defaultTailwindCSS,

View File

@ -5,6 +5,14 @@ import { fetchSS } from "../utilsSS";
import { FullLLMProvider } from "@/app/admin/models/llm/interfaces"; import { FullLLMProvider } from "@/app/admin/models/llm/interfaces";
import { ToolSnapshot } from "../tools/interfaces"; import { ToolSnapshot } from "../tools/interfaces";
import { fetchToolsSS } from "../tools/fetchTools"; import { fetchToolsSS } from "../tools/fetchTools";
import { IconManifestType } from "react-icons/lib";
import {
OpenAIIcon,
AnthropicIcon,
AWSIcon,
AzureIcon,
OpenSourceIcon,
} from "@/components/icons/icons";
export async function fetchAssistantEditorInfoSS( export async function fetchAssistantEditorInfoSS(
personaId?: number | string personaId?: number | string
@ -79,11 +87,27 @@ export async function fetchAssistantEditorInfoSS(
`Failed to fetch LLM providers - ${await llmProvidersResponse.text()}`, `Failed to fetch LLM providers - ${await llmProvidersResponse.text()}`,
]; ];
} }
const llmProviders = (await llmProvidersResponse.json()) as FullLLMProvider[]; const llmProviders = (await llmProvidersResponse.json()) as FullLLMProvider[];
if (personaId && personaResponse && !personaResponse.ok) { if (personaId && personaResponse && !personaResponse.ok) {
return [null, `Failed to fetch Persona - ${await personaResponse.text()}`]; return [null, `Failed to fetch Persona - ${await personaResponse.text()}`];
} }
for (const provider of llmProviders) {
if (provider.provider == "openai") {
provider.icon = OpenAIIcon;
} else if (provider.provider == "anthropic") {
provider.icon = AnthropicIcon;
} else if (provider.provider == "bedrock") {
provider.icon = AzureIcon;
} else if (provider.provider == "azure") {
provider.icon = AWSIcon;
} else {
provider.icon = OpenSourceIcon;
}
}
const existingPersona = personaResponse const existingPersona = personaResponse
? ((await personaResponse.json()) as Persona) ? ((await personaResponse.json()) as Persona)
: null; : null;