* auto generate start prompts

* post rebase clean up

* update for clarity
This commit is contained in:
pablonyx
2024-12-16 13:34:43 -08:00
committed by GitHub
parent 1df6a506ec
commit 2847ab003e
13 changed files with 778 additions and 135 deletions

View File

@@ -75,7 +75,8 @@ export default function Page() {
},
{} as Record<SourceCategory, SourceMetadata[]>
);
}, [sources, searchTerm]);
}, [sources, filterSources, searchTerm]);
const handleKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
const filteredCategories = Object.entries(categorizedSources).filter(

View File

@@ -1,7 +1,7 @@
"use client";
import { Option } from "@/components/Dropdown";
import { generateRandomIconShape, createSVG } from "@/lib/assistantIconUtils";
import { CCPairBasicInfo, DocumentSet, User } from "@/lib/types";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
@@ -9,12 +9,11 @@ import { Textarea } from "@/components/ui/textarea";
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
import {
ArrayHelpers,
ErrorMessage,
Field,
FieldArray,
Form,
Formik,
FormikProps,
useFormikContext,
} from "formik";
import {
@@ -27,7 +26,6 @@ import {
import { usePopup } from "@/components/admin/connectors/Popup";
import { getDisplayNameForModel, useCategories } from "@/lib/hooks";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import { Option } from "@/components/Dropdown";
import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences";
import { checkLLMSupportsImageInput, destructureValue } from "@/lib/llm/utils";
import { ToolSnapshot } from "@/lib/tools/interfaces";
@@ -41,10 +39,9 @@ import {
} from "@/components/ui/tooltip";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { FiInfo, FiX } from "react-icons/fi";
import { useEffect, useMemo, useState } from "react";
import { FiInfo, FiRefreshCcw } from "react-icons/fi";
import * as Yup from "yup";
import { FullLLMProvider } from "../configuration/llm/interfaces";
import CollapsibleSection from "./CollapsibleSection";
import { SuccessfulPersonaUpdateRedirectType } from "./enums";
import { Persona, PersonaCategory, StarterMessage } from "./interfaces";
@@ -66,6 +63,9 @@ import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
import { buildImgUrl } from "@/app/chat/files/images/utils";
import { LlmList } from "@/components/llm/LLMList";
import { useAssistants } from "@/components/context/AssistantsContext";
import { debounce } from "lodash";
import { FullLLMProvider } from "../configuration/llm/interfaces";
import StarterMessagesList from "./StarterMessageList";
import { Input } from "@/components/ui/input";
import { CategoryCard } from "./CategoryCard";
@@ -129,12 +129,14 @@ export function AssistantEditor({
];
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [hasEditedStarterMessage, setHasEditedStarterMessage] = useState(false);
const [showPersonaCategory, setShowPersonaCategory] = useState(!admin);
// 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);
@@ -148,6 +150,10 @@ export function AssistantEditor({
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(
@@ -217,7 +223,24 @@ export function AssistantEditor({
existingPersona?.llm_model_provider_override ?? null,
llm_model_version_override:
existingPersona?.llm_model_version_override ?? null,
starter_messages: existingPersona?.starter_messages ?? [],
starter_messages: existingPersona?.starter_messages ?? [
{
name: "",
message: "",
},
{
name: "",
message: "",
},
{
name: "",
message: "",
},
{
name: "",
message: "",
},
],
enabled_tools_map: enabledToolsMap,
icon_color: existingPersona?.icon_color ?? defautIconColor,
icon_shape: existingPersona?.icon_shape ?? defaultIconShape,
@@ -228,6 +251,44 @@ export function AssistantEditor({
groups: existingPersona?.groups ?? [],
};
interface AssistantPrompt {
message: string;
name: string;
}
const debouncedRefreshPrompts = debounce(
async (values: 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: values.name,
description: values.description,
document_set_ids: values.document_set_ids,
instructions: values.system_prompt || values.task_prompt,
}),
});
const data: AssistantPrompt = await response.json();
if (response.ok) {
setFieldValue("starter_messages", data);
}
} catch (error) {
console.error("Failed to refresh prompts:", error);
} finally {
setIsRefreshing(false);
}
},
1000
);
const [isRequestSuccessful, setIsRequestSuccessful] = useState(false);
return (
@@ -421,6 +482,8 @@ export function AssistantEditor({
isSubmitting,
values,
setFieldValue,
errors,
...formikProps
}: FormikProps<any>) => {
function toggleToolInValues(toolId: number) {
@@ -445,6 +508,7 @@ export function AssistantEditor({
return (
<Form className="w-full text-text-950">
{/* Refresh starter messages when name or description changes */}
<div className="w-full flex gap-x-2 justify-center">
<Popover
open={isIconDropdownOpen}
@@ -984,6 +1048,91 @@ export function AssistantEditor({
</div>
</div>
<div className="mb-6 w-full flex flex-col">
<div className="flex gap-x-2 items-center">
<div className="block font-medium text-base">
Starter Messages
</div>
</div>
<SubLabel>
Pre-configured messages that help users understand what this
assistant can do and how to interact with it effectively.
</SubLabel>
<div className="relative w-fit">
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger asChild>
<div>
<Button
type="button"
size="sm"
onClick={() =>
debouncedRefreshPrompts(values, setFieldValue)
}
disabled={
!autoStarterMessageEnabled ||
isRefreshing ||
(Object.keys(errors).length > 0 &&
Object.keys(errors).some(
(key) => !key.startsWith("starter_messages")
))
}
className={`
px-3 py-2
mr-auto
my-2
flex gap-x-2
text-sm font-medium
rounded-lg shadow-sm
items-center gap-2
transition-colors duration-200
${
isRefreshing || !autoStarterMessageEnabled
? "bg-gray-100 text-gray-400 cursor-not-allowed"
: "bg-blue-50 text-blue-600 hover:bg-blue-100 active:bg-blue-200"
}
`}
>
<div className="flex items-center gap-x-2">
{isRefreshing ? (
<FiRefreshCcw className="w-4 h-4 animate-spin text-gray-400" />
) : (
<SwapIcon className="w-4 h-4 text-blue-600" />
)}
Generate
</div>
</Button>
</div>
</TooltipTrigger>
{!autoStarterMessageEnabled && (
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
No LLM providers configured. Generation is not
available.
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
<div className="w-full">
<FieldArray
name="starter_messages"
render={(arrayHelpers: ArrayHelpers) => (
<StarterMessagesList
isRefreshing={isRefreshing}
values={values.starter_messages}
arrayHelpers={arrayHelpers}
touchStarterMessages={() => {
setHasEditedStarterMessage(true);
}}
/>
)}
/>
</div>
</div>
{admin && (
<AdvancedOptionsToggle
title="Categories"
@@ -1190,136 +1339,12 @@ export function AssistantEditor({
</>
)}
<div className="mb-6 flex flex-col">
<div className="flex gap-x-2 items-center">
<div className="block font-medium text-base">
Starter Messages (Optional){" "}
</div>
</div>
<SubLabel>
Add pre-defined messages to help users get started. Only
the first 4 will be displayed.
</SubLabel>
<FieldArray
name="starter_messages"
render={(
arrayHelpers: ArrayHelpers<StarterMessage[]>
) => (
<div>
{values.starter_messages &&
values.starter_messages.length > 0 &&
values.starter_messages.map(
(
starterMessage: StarterMessage,
index: number
) => {
return (
<div
key={index}
className={index === 0 ? "mt-2" : "mt-6"}
>
<div className="flex">
<div className="w-full mr-6 border border-border p-3 rounded">
<div>
<Label small>Name</Label>
<SubLabel>
Shows up as the &quot;title&quot;
for this Starter Message. For
example, &quot;Write an email&quot;.
</SubLabel>
<Field
name={`starter_messages[${index}].name`}
className={`
border
border-border
bg-background
rounded
w-full
py-2
px-3
mr-4
`}
autoComplete="off"
/>
<ErrorMessage
name={`starter_messages[${index}].name`}
component="div"
className="text-error text-sm mt-1"
/>
</div>
<div className="mt-3">
<Label small>Message</Label>
<SubLabel>
The actual message to be sent as the
initial user message if a user
selects this starter prompt. For
example, &quot;Write me an email to
a client about a new billing feature
we just released.&quot;
</SubLabel>
<Field
name={`starter_messages[${index}].message`}
className={`
border
border-border
bg-background
rounded
w-full
py-2
px-3
min-h-12
mr-4
line-clamp-
`}
as="textarea"
autoComplete="off"
/>
<ErrorMessage
name={`starter_messages[${index}].message`}
component="div"
className="text-error text-sm mt-1"
/>
</div>
</div>
<div className="my-auto">
<FiX
className="my-auto w-10 h-10 cursor-pointer hover:bg-hover rounded p-2"
onClick={() =>
arrayHelpers.remove(index)
}
/>
</div>
</div>
</div>
);
}
)}
<Button
onClick={() => {
arrayHelpers.push({
name: "",
description: "",
message: "",
});
}}
className="mt-3"
size="sm"
variant="next"
>
Add New
</Button>
</div>
)}
/>
</div>
<IsPublicGroupSelector
formikProps={{
values,
isSubmitting,
setFieldValue,
errors,
...formikProps,
}}
objectName="assistant"

View File

@@ -0,0 +1,198 @@
"use client";
import { ArrayHelpers, ErrorMessage, Field, useFormikContext } from "formik";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { useEffect } from "react";
import { FiInfo, FiTrash2, FiPlus } from "react-icons/fi";
import { StarterMessage } from "./interfaces";
import { Label } from "@/components/admin/connectors/Field";
export default function StarterMessagesList({
values,
arrayHelpers,
isRefreshing,
touchStarterMessages,
}: {
values: StarterMessage[];
arrayHelpers: ArrayHelpers;
isRefreshing: boolean;
touchStarterMessages: () => void;
}) {
const { handleChange } = useFormikContext();
// Group starter messages into rows of 2 for display purposes
const rows = values.reduce((acc: StarterMessage[][], curr, i) => {
if (i % 2 === 0) acc.push([curr]);
else acc[acc.length - 1].push(curr);
return acc;
}, []);
const canAddMore = values.length <= 6;
return (
<div className="mt-4 flex flex-col gap-6">
{rows.map((row, rowIndex) => (
<div key={rowIndex} className="flex items-start gap-4">
<div className="grid grid-cols-2 gap-6 w-full xl:w-fit">
{row.map((starterMessage, colIndex) => (
<div
key={rowIndex * 2 + colIndex}
className="bg-white max-w-full w-full xl:w-[500px] border border-border rounded-lg shadow-md transition-shadow duration-200 p-6"
>
<div className="space-y-5">
{isRefreshing ? (
<div className="w-full">
<div className="w-full">
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-10 w-full bg-gray-200 rounded animate-pulse" />
</div>
<div>
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-10 w-full bg-gray-200 rounded animate-pulse" />
</div>
<div>
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse mb-2" />
<div className="h-24 w-full bg-gray-200 rounded animate-pulse" />
</div>
</div>
) : (
<>
<div>
<div className="flex w-full items-center gap-x-1">
<Label
small
className="text-sm font-medium text-gray-700"
>
Name
</Label>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<FiInfo size={12} />
</TooltipTrigger>
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
Shows up as the &quot;title&quot; for this
Starter Message. For example, &quot;Write an
email.&quot;
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Field
name={`starter_messages.${
rowIndex * 2 + colIndex
}.name`}
className="mt-1 w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
autoComplete="off"
placeholder="Enter a name..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
<ErrorMessage
name={`starter_messages.${
rowIndex * 2 + colIndex
}.name`}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
<div>
<div className="flex w-full items-center gap-x-1">
<Label
small
className="text-sm font-medium text-gray-700"
>
Message
</Label>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<FiInfo size={12} />
</TooltipTrigger>
<TooltipContent side="top" align="center">
<p className="bg-background-900 max-w-[200px] mb-1 text-sm rounded-lg p-1.5 text-white">
The actual message to be sent as the initial
user message.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Field
name={`starter_messages.${
rowIndex * 2 + colIndex
}.message`}
className="mt-1 text-sm w-full px-4 py-2.5 bg-background border border-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition min-h-[100px] resize-y"
as="textarea"
autoComplete="off"
placeholder="Enter the message..."
onChange={(e: any) => {
touchStarterMessages();
handleChange(e);
}}
/>
<ErrorMessage
name={`starter_messages.${
rowIndex * 2 + colIndex
}.message`}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
</>
)}
</div>
</div>
))}
</div>
<button
type="button"
onClick={() => {
arrayHelpers.remove(rowIndex * 2 + 1);
arrayHelpers.remove(rowIndex * 2);
}}
className="p-1.5 bg-white border border-gray-200 rounded-full text-gray-400 hover:text-red-500 hover:border-red-200 transition-colors mt-2"
aria-label="Delete row"
>
<FiTrash2 size={14} />
</button>
</div>
))}
{canAddMore && (
<button
type="button"
onClick={() => {
arrayHelpers.push({
name: "",
message: "",
});
arrayHelpers.push({
name: "",
message: "",
});
}}
className="self-start flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 rounded-lg text-gray-600 hover:bg-gray-50 hover:border-gray-300 transition-colors"
>
<FiPlus size={16} />
<span>Add Row</span>
</button>
)}
</div>
);
}