Compare commits

...

10 Commits

Author SHA1 Message Date
pablodanswer
06bc8f1f92 base functionality 2024-10-28 14:51:15 -07:00
pablodanswer
8093ceeb45 formatting 2024-10-28 13:24:40 -07:00
pablodanswer
3d0ace1e45 minor nits 2024-10-28 13:21:29 -07:00
pablodanswer
553aba79dc update based on feedback 2024-10-28 13:17:27 -07:00
pablodanswer
da038b317a remove logs 2024-10-28 12:32:12 -07:00
pablodanswer
6769dc373f minor udpate to ui 2024-10-28 12:32:12 -07:00
pablodanswer
b35e05315c use display name + minor updates to models 2024-10-28 12:32:00 -07:00
pablodanswer
7cfd3d2d44 cleaner cards 2024-10-28 12:31:42 -07:00
pablodanswer
b2aa1c864b slightly cleaner animation 2024-10-28 12:31:42 -07:00
pablodanswer
d2f8177b8f cleaner initial chat screen 2024-10-28 12:31:40 -07:00
12 changed files with 595 additions and 306 deletions

View File

@@ -41,6 +41,19 @@ personas:
icon_color: "#6FB1FF"
display_priority: 1
is_visible: true
starter_messages:
- name: "General Information"
description: "Ask about available information"
message: "Hello! I'm interested in learning more about the information available here. Could you give me an overview of the types of data or documents that might be accessible?"
- name: "Specific Topic Search"
description: "Search for specific information"
message: "Hi! I'd like to learn more about a specific topic. Could you help me find relevant documents and information?"
- name: "Recent Updates"
description: "Inquire about latest additions"
message: "Hello! I'm curious about any recent updates or additions to the knowledge base. Can you tell me what new information has been added lately?"
- name: "Cross-referencing Information"
description: "Connect information from different sources"
message: "Hi! I'm working on a project that requires connecting information from multiple sources. How can I effectively cross-reference data across different documents or categories?"
- id: 1
name: "General"
@@ -57,6 +70,19 @@ personas:
icon_color: "#FF6F6F"
display_priority: 0
is_visible: true
starter_messages:
- name: "Open Discussion"
description: "Start an open-ended conversation"
message: "Hi! Can you help me write a professional email?"
- name: "Problem Solving"
description: "Get help with a challenge"
message: "Hello! I need help managing my daily tasks better. Do you have any simple tips?"
- name: "Learn Something New"
description: "Explore a new topic"
message: "Hi! Could you explain what project management is in simple terms?"
- name: "Creative Brainstorming"
description: "Generate creative ideas"
message: "Hello! I need to brainstorm some team building activities. Do you have any fun suggestions?"
- id: 2
name: "Paraphrase"
@@ -73,7 +99,19 @@ personas:
icon_color: "#6FFF8D"
display_priority: 2
is_visible: false
starter_messages:
- name: "Document Search"
description: "Find exact information"
message: "Hi! Could you help me find information about our team structure and reporting lines from our internal documents?"
- name: "Process Verification"
description: "Find exact quotes"
message: "Hello! I need to understand our project approval process. Could you find the exact steps from our documentation?"
- name: "Technical Documentation"
description: "Search technical details"
message: "Hi there! I'm looking for information about our deployment procedures. Can you find the specific steps from our technical guides?"
- name: "Policy Reference"
description: "Check official policies"
message: "Hello! Could you help me find our official guidelines about client communication? I need the exact wording from our documentation."
- id: 3
name: "Art"
@@ -86,8 +124,21 @@ personas:
llm_filter_extraction: false
recency_bias: "no_decay"
document_sets: []
icon_shape: 234124
icon_shape: 234124
icon_color: "#9B59B6"
image_generation: true
image_generation: true
display_priority: 3
is_visible: true
starter_messages:
- name: "Landscape"
description: "Generate a landscape image"
message: "Create an image of a serene mountain lake at sunset, with snow-capped peaks reflected in the calm water and a small wooden cabin on the shore."
- name: "Character"
description: "Generate a character image"
message: "Generate an image of a futuristic robot with glowing blue eyes, sleek metallic body, and intricate circuitry visible through transparent panels on its chest and arms."
- name: "Abstract"
description: "Create an abstract image"
message: "Create an abstract image representing the concept of time, using swirling clock hands, fragmented hourglasses, and streaks of light to convey the passage of moments and eras."
- name: "Urban Scene"
description: "Generate an urban landscape"
message: "Generate an image of a bustling futuristic cityscape at night, with towering skyscrapers, flying vehicles, holographic advertisements, and a mix of neon and bioluminescent lighting."

View File

@@ -6,6 +6,7 @@ from typing import Any
from danswer.access.models import DocumentAccess
from danswer.indexing.models import DocMetadataAwareIndexChunk
from danswer.search.models import IndexFilters
from danswer.search.models import InferenceChunk
from danswer.search.models import InferenceChunkUncleaned
from shared_configs.model_server_models import Embedding
@@ -363,6 +364,19 @@ class AdminCapable(abc.ABC):
raise NotImplementedError
class RandomCapable(abc.ABC):
"""Class must implement random document retrieval capability"""
@abc.abstractmethod
def random_retrieval(
self,
filters: IndexFilters,
num_to_retrieve: int = 10,
) -> list[InferenceChunk]:
"""Retrieve random chunks matching the filters"""
raise NotImplementedError
class BaseIndex(
Verifiable,
Indexable,
@@ -370,6 +384,7 @@ class BaseIndex(
Deletable,
AdminCapable,
IdRetrievalCapable,
RandomCapable,
abc.ABC,
):
"""

View File

@@ -69,6 +69,7 @@ from danswer.document_index.vespa_constants import YQL_BASE
from danswer.indexing.models import DocMetadataAwareIndexChunk
from danswer.key_value_store.factory import get_kv_store
from danswer.search.models import IndexFilters
from danswer.search.models import InferenceChunk
from danswer.search.models import InferenceChunkUncleaned
from danswer.utils.batching import batch_generator
from danswer.utils.logger import setup_logger
@@ -899,6 +900,41 @@ class VespaIndex(DocumentIndex):
logger.info("Batch deletion completed")
def random_retrieval(
self,
filters: IndexFilters,
num_to_retrieve: int = 10,
) -> list[InferenceChunk]:
"""Retrieve random chunks matching the filters"""
vespa_where_clauses = build_vespa_filters(filters)
# Remove trailing 'and' if it exists
if vespa_where_clauses.strip().endswith("and"):
vespa_where_clauses = vespa_where_clauses.strip()[:-3].strip()
# Add a true condition if we only have hidden filter
if vespa_where_clauses == "!(hidden=true)":
vespa_where_clauses += " and true"
yql = YQL_BASE.format(index_name=self.index_name) + vespa_where_clauses
import random
params: dict[str, str | int | float] = {
"yql": yql,
"hits": num_to_retrieve
* 10, # Request more hits to get better randomization
"timeout": VESPA_TIMEOUT,
"ranking.softtimeout.enable": "false", # Ensure we get all results
}
results = query_vespa(params)
# Randomly sample from the results
if len(results) > num_to_retrieve:
return random.sample(results, num_to_retrieve)
return results
class _VespaDeleteRequest:
def __init__(self, document_id: str, index_name: str) -> None:

View File

@@ -14,6 +14,7 @@ from danswer.auth.users import current_curator_or_admin_user
from danswer.auth.users import current_user
from danswer.configs.constants import FileOrigin
from danswer.configs.constants import NotificationType
from danswer.db.document_set import get_document_sets_by_ids
from danswer.db.engine import get_session
from danswer.db.models import User
from danswer.db.notification import create_notification
@@ -26,10 +27,16 @@ from danswer.db.persona import update_all_personas_display_priority
from danswer.db.persona import update_persona_public_status
from danswer.db.persona import update_persona_shared_users
from danswer.db.persona import update_persona_visibility
from danswer.document_index.document_index_utils import get_both_index_names
from danswer.document_index.factory import get_default_document_index
from danswer.file_store.file_store import get_default_file_store
from danswer.file_store.models import ChatFileType
from danswer.llm.answering.prompts.utils import build_dummy_prompt
from danswer.llm.factory import get_default_llms
from danswer.search.models import IndexFilters
from danswer.search.models import InferenceChunk
from danswer.server.features.persona.models import CreatePersonaRequest
from danswer.server.features.persona.models import GeneratePersonaPromptRequest
from danswer.server.features.persona.models import ImageGenerationToolStatus
from danswer.server.features.persona.models import PersonaSharedNotificationData
from danswer.server.features.persona.models import PersonaSnapshot
@@ -299,3 +306,68 @@ def build_final_template_prompt(
retrieval_disabled=retrieval_disabled,
)
)
class StarterMessage(BaseModel):
name: str
description: str
message: str
def get_random_chunks_from_doc_sets(
doc_set_ids: list[str], num_chunks: int, db_session: Session
) -> list[InferenceChunk]:
# Get the document index
curr_ind_name, sec_ind_name = get_both_index_names(db_session)
document_index = get_default_document_index(curr_ind_name, sec_ind_name)
# Create filters to only get chunks from specified document sets
filters = IndexFilters(document_set_ids=doc_set_ids, access_control_list=None)
# Get random chunks directly from Vespa
return document_index.random_retrieval(filters=filters, num_to_retrieve=num_chunks)
# Based on an assistant schema, generates 4 prompts
@basic_router.post("/assistant-prompt-refresh")
def build_assistant_prompts(
generate_persona_prompt_request: GeneratePersonaPromptRequest,
db_session: Session = Depends(get_session),
_: User | None = Depends(current_user),
) -> list[StarterMessage]:
document_sets = get_document_sets_by_ids(
document_set_ids=generate_persona_prompt_request.document_set_ids,
db_session=db_session,
)
print(document_sets)
llm, fast_llm = get_default_llms(temperature=1.0)
chunks = get_random_chunks_from_doc_sets(
doc_set_ids=generate_persona_prompt_request.document_set_ids,
num_chunks=4,
db_session=db_session,
)
random_content = "".join([chunk.content for chunk in chunks])
prompt = (
"Based on the following content sample, create a natural, engaging starter message for a chatbot. "
"The message should demonstrate knowledge of the content domain while inviting user interaction. "
"and 'message' (the actual conversation starter). Content sample: "
+ random_content
)
prompts: StarterMessage = []
for _ in range(4):
import json
response = json.loads(
fast_llm.invoke(prompt, structured_response_format=StarterMessage).content
)
# Convert the LLM response into a StarterMessage object
starter_message = StarterMessage(
name=response["name"],
description=response["description"],
message=response["message"],
)
prompts.append(starter_message)
return prompts
# return build_assistant_prompts(assistant_schema=assistant_schema)

View File

@@ -16,6 +16,13 @@ from danswer.utils.logger import setup_logger
logger = setup_logger()
# More minimal request for generating a persona prompt
class GeneratePersonaPromptRequest(BaseModel):
name: str
description: str
document_set_ids: list[int]
class CreatePersonaRequest(BaseModel):
name: str
description: str

View File

@@ -57,6 +57,7 @@ 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";
function findSearchTool(tools: ToolSnapshot[]) {
return tools.find((tool) => tool.in_code_tool_id === "SearchTool");
@@ -214,6 +215,43 @@ export function AssistantEditor({
groups: existingPersona?.groups ?? [],
};
const [isRefreshing, setIsRefreshing] = useState(false);
interface AssistantPrompt {
message: string;
description: string;
name: string;
}
// Create debounced refresh function
const debouncedRefreshPrompts = debounce(
async (values: any, setFieldValue: any) => {
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 || [],
}),
});
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 (
@@ -407,6 +445,8 @@ export function AssistantEditor({
isSubmitting,
values,
setFieldValue,
errors,
...formikProps
}: FormikProps<any>) => {
function toggleToolInValues(toolId: number) {
@@ -429,6 +469,22 @@ export function AssistantEditor({
values.llm_model_version_override || defaultModelName || ""
);
useEffect(() => {
// Only refresh if we have required fields and no errors
if (
values.name &&
values.description &&
Object.keys(errors).length === 0
) {
debouncedRefreshPrompts(values, setFieldValue);
}
}, [
values.name,
values.description,
values.document_set_ids,
errors,
]);
return (
<Form className="w-full text-text-950">
<div className="w-full flex gap-x-2 justify-center">
@@ -627,7 +683,10 @@ export function AssistantEditor({
otherwise specified below.
{admin &&
user?.preferences.default_model &&
` Your current (user-specific) default model is ${getDisplayNameForModel(destructureValue(user?.preferences?.default_model!).modelName)}`}
` Your current (user-specific) default model is ${getDisplayNameForModel(
destructureValue(user?.preferences?.default_model!)
.modelName
)}`}
</p>
{admin ? (
<div className="mb-2 flex items-starts">
@@ -972,6 +1031,162 @@ export function AssistantEditor({
)}
</div>
</div>
<div className="mb-6 flex flex-col">
<div className="flex gap-x-2 items-center">
<div className="block font-medium text-base">
Starter Messages
</div>
</div>
<Button
type="button"
size="xs"
onClick={() => debouncedRefreshPrompts(values, setFieldValue)}
disabled={isRefreshing || Object.keys(errors).length > 0}
>
{isRefreshing ? "Refreshing..." : "Refresh Messages"}
</Button>
<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>Description</Label>
<SubLabel>
A description which tells the user what
they might want to use this Starter
Message for. For example &quot;to a
client about a new feature&quot;
</SubLabel>
<Field
name={`starter_messages.${index}.description`}
className={`
border
border-border
bg-background
rounded
w-full
py-2
px-3
mr-4
`}
autoComplete="off"
/>
<ErrorMessage
name={`starter_messages[${index}].description`}
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
mr-4
`}
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="text-white mt-3"
size="xs"
type="button"
icon={FiPlus}
color="green"
>
Add New
</Button>
</div>
)}
/>
</div>
<Divider />
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
@@ -996,157 +1211,6 @@ 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>
<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>Description</Label>
<SubLabel>
A description which tells the user
what they might want to use this
Starter Message for. For example
&quot;to a client about a new
feature&quot;
</SubLabel>
<Field
name={`starter_messages.${index}.description`}
className={`
border
border-border
bg-background
rounded
w-full
py-2
px-3
mr-4
`}
autoComplete="off"
/>
<ErrorMessage
name={`starter_messages[${index}].description`}
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
mr-4
`}
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="text-white mt-3"
size="xs"
type="button"
icon={FiPlus}
color="green"
>
Add New
</Button>
</div>
)}
/>
</div>
<IsPublicGroupSelector
formikProps={{
values,

View File

@@ -1,96 +1,44 @@
import { getSourceMetadataForSources } from "@/lib/sources";
import { ValidSources } from "@/lib/types";
import { Persona } from "../admin/assistants/interfaces";
import { Divider } from "@tremor/react";
import { FiBookmark, FiInfo } from "react-icons/fi";
import { HoverPopup } from "@/components/HoverPopup";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { useState } from "react";
import { DisplayAssistantCard } from "@/components/assistants/AssistantCards";
export function ChatIntro({
availableSources,
selectedPersona,
}: {
availableSources: ValidSources[];
selectedPersona: Persona;
}) {
const availableSourceMetadata = getSourceMetadataForSources(availableSources);
export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
const [hoveredAssistant, setHoveredAssistant] = useState(false);
return (
<>
<div className="flex justify-center items-center h-full">
<div className="mobile:w-[90%] mobile:px-4 w-message-xs 2xl:w-message-sm 3xl:w-message">
<div className="flex">
<div className="mx-auto">
<div className="m-auto text-3xl font-strong font-bold text-strong w-fit">
{selectedPersona?.name || "How can I help you today?"}
<div className="mobile:w-[90%] mobile:px-4 w-message-xs 2xl:w-message-sm 3xl:w-message">
<div className="relative flex w-fit mx-auto justify-center">
<div className="absolute z-10 -left-20 top-1/2 -translate-y-1/2">
<div className="relative">
<div
onMouseEnter={() => setHoveredAssistant(true)}
onMouseLeave={() => setHoveredAssistant(false)}
className="p-4 scale-[.8] cursor-pointer border-dashed rounded-full flex border border-border border-2 border-dashed"
style={{
borderStyle: "dashed",
borderWidth: "1.5px",
borderSpacing: "4px",
}}
>
<AssistantIcon
disableToolip
size={"large"}
assistant={selectedPersona}
/>
</div>
<div className="absolute right-full mr-2 w-[300px] top-0">
{hoveredAssistant && (
<DisplayAssistantCard selectedPersona={selectedPersona} />
)}
</div>
{selectedPersona && (
<div className="mt-1">{selectedPersona.description}</div>
)}
</div>
</div>
{selectedPersona && selectedPersona.num_chunks !== 0 && (
<>
<Divider />
<div>
{selectedPersona.document_sets.length > 0 && (
<div className="mt-2">
<p className="font-bold mb-1 mt-4 text-emphasis">
Knowledge Sets:{" "}
</p>
<div className="flex flex-wrap gap-2">
{selectedPersona.document_sets.map((documentSet) => (
<div key={documentSet.id} className="w-fit">
<HoverPopup
mainContent={
<span className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default">
<div className="mr-1 my-auto">
<FiBookmark />
</div>
{documentSet.name}
</span>
}
popupContent={
<div className="flex py-1 w-96">
<FiInfo className="my-auto mr-2" />
<div className="text-sm">
{documentSet.description}
</div>
</div>
}
direction="top"
/>
</div>
))}
</div>
</div>
)}
{availableSources.length > 0 && (
<div className="mt-1">
<p className="font-bold mb-1 mt-4 text-emphasis">
Connected Sources:{" "}
</p>
<div className={`flex flex-wrap gap-2`}>
{availableSourceMetadata.map((sourceMetadata) => (
<span
key={sourceMetadata.internalName}
className="flex w-fit p-1 rounded border border-border text-xs font-medium cursor-default"
>
<div className="mr-1 my-auto">
{sourceMetadata.icon({})}
</div>
<div className="my-auto">
{sourceMetadata.displayName}
</div>
</span>
))}
</div>
</div>
)}
</div>
</>
)}
<div className="text-3xl line-clamp-2 text-text-800 font-base font-semibold text-strong">
{selectedPersona?.name || "How can I help you today?"}
</div>
</div>
</div>
</>

View File

@@ -103,6 +103,7 @@ import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
import BlurBackground from "./shared_chat_search/BlurBackground";
import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
import { useAssistants } from "@/components/context/AssistantsContext";
import { Divider } from "@tremor/react";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -741,12 +742,6 @@ export function ChatPage({
}, [liveAssistant]);
const filterManager = useFilters();
const [finalAvailableSources, finalAvailableDocumentSets] =
computeAvailableFilters({
selectedPersona: selectedAssistant,
availableSources,
availableDocumentSets,
});
const [currentFeedback, setCurrentFeedback] = useState<
[FeedbackType, number] | null
@@ -2002,7 +1997,7 @@ export function ChatPage({
{...getRootProps()}
>
<div
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
ref={scrollableDivRef}
>
{/* ChatBanner is a custom banner that displays a admin-specified message at
@@ -2012,11 +2007,51 @@ export function ChatPage({
!isFetchingChatMessages &&
currentSessionChatState == "input" &&
!loadingError && (
<ChatIntro
availableSources={finalAvailableSources}
selectedPersona={liveAssistant}
/>
<div className="h-full flex flex-col justify-center items-center">
<ChatIntro selectedPersona={liveAssistant} />
<div
key={-4}
className={`
mx-auto
px-4
w-full
max-w-[750px]
flex
flex-wrap
justify-center
mt-2
h-40
items-start
mb-6`}
>
{currentPersona?.starter_messages &&
currentPersona.starter_messages.length >
0 && (
<>
<Divider className="mx-2" />
{currentPersona.starter_messages
.slice(0, 4)
.map((starterMessage, i) => (
<div key={i} className="w-1/2">
<StarterMessage
starterMessage={starterMessage}
onClick={() =>
onSubmit({
messageOverride:
starterMessage.message,
})
}
/>
</div>
))}
</>
)}
</div>
</div>
)}
<div
className={
"-ml-4 w-full mx-auto " +
@@ -2383,45 +2418,6 @@ export function ChatPage({
/>
</div>
)}
{currentPersona &&
currentPersona.starter_messages &&
currentPersona.starter_messages.length > 0 &&
selectedAssistant &&
messageHistory.length === 0 &&
!isFetchingChatMessages && (
<div
key={-4}
className={`
mx-auto
px-4
w-searchbar-xs
2xl:w-searchbar-sm
3xl:w-searchbar
grid
gap-4
grid-cols-1
grid-rows-1
mt-4
md:grid-cols-2
mb-6`}
>
{currentPersona.starter_messages.map(
(starterMessage, i) => (
<div key={i} className="w-full">
<StarterMessage
starterMessage={starterMessage}
onClick={() =>
onSubmit({
messageOverride:
starterMessage.message,
})
}
/>
</div>
)
)}
</div>
)}
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
<div ref={endPaddingRef} className="h-[95px]" />

View File

@@ -1,21 +1,29 @@
import { StarterMessage } from "../admin/assistants/interfaces";
import { StarterMessage as StarterMessageType } from "../admin/assistants/interfaces";
export function StarterMessage({
starterMessage,
onClick,
}: {
starterMessage: StarterMessage;
starterMessage: StarterMessageType;
onClick: () => void;
}) {
return (
<div
className={
"py-2 px-3 rounded border border-border bg-white cursor-pointer hover:bg-hover-light h-full"
}
className="mb-4 mx-2 group relative overflow-hidden rounded-xl border border-border bg-gradient-to-br from-white to-background p-4 shadow-sm transition-all duration-300 hover:shadow-md hover:scale-[1.005] cursor-pointer"
onClick={onClick}
>
<p className="font-medium text-emphasis">{starterMessage.name}</p>
<p className="text-subtle text-sm">{starterMessage.description}</p>
<div className="absolute inset-0 bg-gradient-to-r from-blue-100 to-purple-100 opacity-0 group-hover:opacity-20 transition-opacity duration-300" />
<h3
className="text-base flex items-center font-medium text-text-800 group-hover:text-text-900 transition-colors duration-300
line-clamp-2 gap-x-2 overflow-hidden"
>
{starterMessage.name}
</h3>
<div className={`overflow-hidden transition-all duration-300 max-h-20}`}>
<p className="text-sm text-text-600 mt-2">
{starterMessage.description}
</p>
</div>
</div>
);
}

View File

@@ -115,3 +115,63 @@ export function DraggableAssistantCard(props: {
</div>
);
}
export function DisplayAssistantCard({
selectedPersona,
}: {
selectedPersona: Persona;
}) {
return (
<div className="p-4 bg-white/90 backdrop-blur-sm rounded-lg shadow-md border border-border/50 max-w-md w-full mx-auto transition-all duration-300 ease-in-out hover:shadow-lg">
<div className="flex items-center mb-3">
<AssistantIcon
disableToolip
size="medium"
assistant={selectedPersona}
/>
<h2 className="ml-3 text-xl font-semibold text-text-900">
{selectedPersona.name}
</h2>
</div>
<p className="text-sm text-text-600 mb-3 leading-relaxed">
{selectedPersona.description}
</p>
{selectedPersona.tools.length > 0 ||
selectedPersona.llm_relevance_filter ||
selectedPersona.llm_filter_extraction ? (
<div className="space-y-2">
<h3 className="text-base font-medium text-text-900">Capabilities:</h3>
<ul className="space-y-.5">
{/* display all tools */}
{selectedPersona.tools.map((tool, index) => (
<li
key={index}
className="flex items-center text-sm text-text-700"
>
<span className="mr-2 text-text-500 opacity-70"></span>{" "}
{tool.display_name}
</li>
))}
{/* Built in capabilities */}
{selectedPersona.llm_relevance_filter && (
<li className="flex items-center text-sm text-text-700">
<span className="mr-2 text-text-500 opacity-70"></span>{" "}
Advanced Relevance Filtering
</li>
)}
{selectedPersona.llm_filter_extraction && (
<li className="flex items-center text-sm text-text-700">
<span className="mr-2 text-text-500 opacity-70"></span> Smart
Information Extraction
</li>
)}
</ul>
</div>
) : (
<p className="text-sm text-text-600 italic">
No specific capabilities listed for this assistant.
</p>
)}
</div>
);
}

View File

@@ -23,22 +23,38 @@ export function AssistantIcon({
assistant,
size,
border,
disableToolip,
}: {
assistant: Persona;
size?: "small" | "medium" | "large";
size?: "small" | "medium" | "large" | "header";
border?: boolean;
disableToolip?: boolean;
}) {
const color = darkerGenerateColorFromId(assistant.id.toString());
return (
<CustomTooltip showTick line wrap content={assistant.description}>
<CustomTooltip
disabled={disableToolip}
showTick
line
wrap
content={assistant.description}
>
{
// Prioritization order: image, graph, defaults
assistant.uploaded_image_id ? (
<img
alt={assistant.name}
className={`object-cover object-center rounded-sm overflow-hidden transition-opacity duration-300 opacity-100
${size === "large" ? "w-8 h-8" : "w-6 h-6"}`}
${
size === "large"
? "w-10 h-10"
: size === "header"
? "w-14 h-14"
: size === "medium"
? "w-8 h-8"
: "w-6 h-6"
}`}
src={buildImgUrl(assistant.uploaded_image_id)}
loading="lazy"
/>
@@ -46,20 +62,36 @@ export function AssistantIcon({
<div
className={`flex-none
${border && "ring ring-[1px] ring-border-strong "}
${size === "large" ? "w-10 h-10" : "w-6 h-6"} `}
${
size === "large"
? "w-10 h-10"
: size === "header"
? "w-14 h-14"
: size === "medium"
? "w-8 h-8"
: "w-6 h-6"
} `}
>
{createSVG(
{ encodedGrid: assistant.icon_shape, filledSquares: 0 },
assistant.icon_color,
size == "large" ? 36 : 24
size === "large"
? 40
: size === "header"
? 56
: size === "medium"
? 32
: 24
)}
</div>
) : (
<div
className={`flex-none rounded-sm overflow-hidden
${border && "border border-.5 border-border-strong "}
${size === "large" && "w-12 h-12"}
${(!size || size === "small") && "w-6 h-6"} `}
${size === "large" ? "w-10 h-10" : ""}
${size === "header" ? "w-14 h-14" : ""}
${size === "medium" ? "w-8 h-8" : ""}
${!size || size === "small" ? "w-6 h-6" : ""} `}
style={{ backgroundColor: color }}
/>
)

View File

@@ -124,9 +124,9 @@ export const CustomTooltip = ({
!disabled &&
createPortal(
<div
className={`min-w-8 fixed z-[1000] ${citation ? "max-w-[350px]" : "w-40"} ${
large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"
}
className={`min-w-8 fixed z-[1000] ${
citation ? "max-w-[350px]" : "w-40"
} ${large ? (medium ? "w-88" : "w-96") : line && "max-w-64 w-auto"}
transform -translate-x-1/2 text-sm
${
light