update persona defaults (#3042)

* evaluate None to default

* fix usage report pagination

* update persona defaults

* update user preferences

* k

* validate

* update typing

* nit

* formating nits

* fallback to all assistants

* update ux + spacing

* udpate refresh logic

* minor update to refresh

* nit

* touchup

* update starter message

* update default live assistant logic

---------

Co-authored-by: Yuhong Sun <yuhongsun96@gmail.com>
This commit is contained in:
pablodanswer
2024-11-06 16:03:14 -08:00
committed by GitHub
parent 43d8daa5bc
commit 07a1b49b4f
25 changed files with 532 additions and 265 deletions

View File

@@ -0,0 +1,46 @@
"""remove description from starter messages
Revision ID: b72ed7a5db0e
Revises: 33cb72ea4d80
Create Date: 2024-11-03 15:55:28.944408
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "b72ed7a5db0e"
down_revision = "33cb72ea4d80"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
sa.text(
"""
UPDATE persona
SET starter_messages = (
SELECT jsonb_agg(elem - 'description')
FROM jsonb_array_elements(starter_messages) elem
)
WHERE starter_messages IS NOT NULL
"""
)
)
def downgrade() -> None:
op.execute(
sa.text(
"""
UPDATE persona
SET starter_messages = (
SELECT jsonb_agg(elem || '{"description": ""}')
FROM jsonb_array_elements(starter_messages) elem
)
WHERE starter_messages IS NOT NULL
"""
)
)

View File

@@ -0,0 +1,29 @@
"""add recent assistants
Revision ID: c0fd6e4da83a
Revises: b72ed7a5db0e
Create Date: 2024-11-03 17:28:54.916618
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "c0fd6e4da83a"
down_revision = "b72ed7a5db0e"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"user",
sa.Column(
"recent_assistants", postgresql.JSONB(), server_default="[]", nullable=False
),
)
def downgrade() -> None:
op.drop_column("user", "recent_assistants")

View File

@@ -42,18 +42,14 @@ personas:
display_priority: 1 display_priority: 1
is_visible: true is_visible: true
starter_messages: starter_messages:
- name: "General Information" - name: "Give me an overview of what's here"
description: "Ask about available information" message: "Sample some documents and tell me what you find."
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: "Use AI to solve a work related problem"
- name: "Specific Topic Search" message: "Ask me what problem I would like to solve, then search the knowledge base to help me find a solution."
description: "Search for specific information" - name: "Find updates on a topic of interest"
message: "Hi! I'd like to learn more about a specific topic. Could you help me find relevant documents and information?" message: "Once I provide a topic, retrieve related documents and tell me when there was last activity on the topic if available."
- name: "Recent Updates" - name: "Surface contradictions"
description: "Inquire about latest additions" message: "Have me choose a subject. Once I have provided it, check against the knowledge base and point out any inconsistencies. For all your following responses, focus on identifying contradictions."
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 - id: 1
name: "General" name: "General"
@@ -71,18 +67,14 @@ personas:
display_priority: 0 display_priority: 0
is_visible: true is_visible: true
starter_messages: starter_messages:
- name: "Open Discussion" - name: "Summarize a document"
description: "Start an open-ended conversation" message: "If I have provided a document please summarize it for me. If not, please ask me to upload a document either by dragging it into the input bar or clicking the +file icon."
message: "Hi! Can you help me write a professional email?" - name: "Help me with coding"
- name: "Problem Solving" message: 'Write me a "Hello World" script in 5 random languages to show off the functionality.'
description: "Get help with a challenge" - name: "Draft a professional email"
message: "Hello! I need help managing my daily tasks better. Do you have any simple tips?" message: "Help me craft a professional email. Let's establish the context and the anticipated outcomes of the email before proposing a draft."
- name: "Learn Something New" - name: "Learn something new"
description: "Explore a new topic" message: "What is the difference between a Gantt chart, a Burndown chart and a Kanban board?"
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 - id: 2
name: "Paraphrase" name: "Paraphrase"
@@ -101,16 +93,12 @@ personas:
is_visible: false is_visible: false
starter_messages: starter_messages:
- name: "Document Search" - 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?" message: "Hi! Could you help me find information about our team structure and reporting lines from our internal documents?"
- name: "Process Verification" - 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?" message: "Hello! I need to understand our project approval process. Could you find the exact steps from our documentation?"
- name: "Technical 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?" 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" - 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." message: "Hello! Could you help me find our official guidelines about client communication? I need the exact wording from our documentation."
- id: 3 - id: 3
@@ -130,15 +118,11 @@ personas:
display_priority: 3 display_priority: 3
is_visible: true is_visible: true
starter_messages: starter_messages:
- name: "Landscape" - name: "Create visuals for a presentation"
description: "Generate a landscape image" message: "Generate someone presenting a graph which clearly demonstrates an upwards trajectory."
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: "Find inspiration for a marketing campaign"
- name: "Character" message: "Generate an image of two happy individuals sipping on a soda drink in a glass bottle."
description: "Generate a character image" - name: "Visualize a product design"
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." message: "I want to add a search bar to my Iphone app. Generate me generic examples of how other apps implement this."
- name: "Abstract" - name: "Generate a humorous image response"
description: "Create an abstract image" message: "My teammate just made a silly mistake and I want to respond with a facepalm. Can you generate me one?"
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

@@ -135,6 +135,9 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
hidden_assistants: Mapped[list[int]] = mapped_column( hidden_assistants: Mapped[list[int]] = mapped_column(
postgresql.JSONB(), nullable=False, default=[] postgresql.JSONB(), nullable=False, default=[]
) )
recent_assistants: Mapped[list[dict]] = mapped_column(
postgresql.JSONB(), nullable=False, default=list, server_default="[]"
)
oidc_expiry: Mapped[datetime.datetime] = mapped_column( oidc_expiry: Mapped[datetime.datetime] = mapped_column(
TIMESTAMPAware(timezone=True), nullable=True TIMESTAMPAware(timezone=True), nullable=True
@@ -1321,7 +1324,6 @@ class StarterMessage(TypedDict):
in Postgres""" in Postgres"""
name: str name: str
description: str
message: str message: str

View File

@@ -42,7 +42,7 @@ class UserPreferences(BaseModel):
chosen_assistants: list[int] | None = None chosen_assistants: list[int] | None = None
hidden_assistants: list[int] = [] hidden_assistants: list[int] = []
visible_assistants: list[int] = [] visible_assistants: list[int] = []
recent_assistants: list[int] | None = None
default_model: str | None = None default_model: str | None = None

View File

@@ -518,6 +518,59 @@ class ChosenDefaultModelRequest(BaseModel):
default_model: str | None = None default_model: str | None = None
class RecentAssistantsRequest(BaseModel):
current_assistant: int
def update_recent_assistants(
recent_assistants: list[int] | None, current_assistant: int
) -> list[int]:
if recent_assistants is None:
recent_assistants = []
else:
recent_assistants = [x for x in recent_assistants if x != current_assistant]
# Add current assistant to start of list
recent_assistants.insert(0, current_assistant)
# Keep only the 5 most recent assistants
recent_assistants = recent_assistants[:5]
return recent_assistants
@router.patch("/user/recent-assistants")
def update_user_recent_assistants(
request: RecentAssistantsRequest,
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> None:
if user is None:
if AUTH_TYPE == AuthType.DISABLED:
store = get_kv_store()
no_auth_user = fetch_no_auth_user(store)
preferences = no_auth_user.preferences
recent_assistants = preferences.recent_assistants
updated_preferences = update_recent_assistants(
recent_assistants, request.current_assistant
)
preferences.recent_assistants = updated_preferences
set_no_auth_user_preferences(store, preferences)
return
else:
raise RuntimeError("This should never happen")
recent_assistants = UserInfo.from_model(user).preferences.recent_assistants
updated_recent_assistants = update_recent_assistants(
recent_assistants, request.current_assistant
)
db_session.execute(
update(User)
.where(User.id == user.id) # type: ignore
.values(recent_assistants=updated_recent_assistants)
)
db_session.commit()
@router.patch("/user/default-model") @router.patch("/user/default-model")
def update_user_default_model( def update_user_default_model(
request: ChosenDefaultModelRequest, request: ChosenDefaultModelRequest,

View File

@@ -3,7 +3,7 @@ kind: Deployment
metadata: metadata:
name: celery-worker-indexing name: celery-worker-indexing
spec: spec:
replicas: 3 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: celery-worker-indexing app: celery-worker-indexing

View File

@@ -3,7 +3,7 @@ kind: Deployment
metadata: metadata:
name: celery-worker-light name: celery-worker-light
spec: spec:
replicas: 2 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: celery-worker-light app: celery-worker-light

View File

@@ -246,9 +246,6 @@ export function AssistantEditor({
name: Yup.string().required( name: Yup.string().required(
"Each starter message must have a name" "Each starter message must have a name"
), ),
description: Yup.string().required(
"Each starter message must have a description"
),
message: Yup.string().required( message: Yup.string().required(
"Each starter message must have a message" "Each starter message must have a message"
), ),
@@ -1053,36 +1050,6 @@ export function AssistantEditor({
/> />
</div> </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"> <div className="mt-3">
<Label small>Message</Label> <Label small>Message</Label>
<SubLabel> <SubLabel>

View File

@@ -3,7 +3,6 @@ import { DocumentSet, MinimalUserSnapshot } from "@/lib/types";
export interface StarterMessage { export interface StarterMessage {
name: string; name: string;
description: string | null;
message: string; message: string;
} }

View File

@@ -191,19 +191,15 @@ const DocumentSetTable = ({
</TableCell> </TableCell>
<TableCell> <TableCell>
{documentSet.is_up_to_date ? ( {documentSet.is_up_to_date ? (
<Badge size="md" variant="success" icon={FiCheckCircle}> <Badge variant="success" icon={FiCheckCircle}>
Up to Date Up to Date
</Badge> </Badge>
) : documentSet.cc_pair_descriptors.length > 0 ? ( ) : documentSet.cc_pair_descriptors.length > 0 ? (
<Badge size="md" variant="in_progress" icon={FiClock}> <Badge variant="in_progress" icon={FiClock}>
Syncing Syncing
</Badge> </Badge>
) : ( ) : (
<Badge <Badge variant="destructive" icon={FiAlertTriangle}>
size="md"
variant="destructive"
icon={FiAlertTriangle}
>
Deleting Deleting
</Badge> </Badge>
)} )}
@@ -211,7 +207,6 @@ const DocumentSetTable = ({
<TableCell> <TableCell>
{documentSet.is_public ? ( {documentSet.is_public ? (
<Badge <Badge
size="md"
variant={isEditable ? "success" : "default"} variant={isEditable ? "success" : "default"}
icon={FiUnlock} icon={FiUnlock}
> >
@@ -219,7 +214,6 @@ const DocumentSetTable = ({
</Badge> </Badge>
) : ( ) : (
<Badge <Badge
size="md"
variant={isEditable ? "in_progress" : "outline"} variant={isEditable ? "in_progress" : "outline"}
icon={FiLock} icon={FiLock}
> >

View File

@@ -200,8 +200,8 @@ function ConnectorRow({
router.push(`/admin/connector/${ccPairsIndexingStatus.cc_pair_id}`); router.push(`/admin/connector/${ccPairsIndexingStatus.cc_pair_id}`);
}} }}
> >
<TableCell className="!w-[300px]"> <TableCell className="">
<p className="w-[200px] xl:w-[400px] inline-block ellipsis truncate"> <p className="lg:w-[200px] xl:w-[400px] inline-block ellipsis truncate">
{ccPairsIndexingStatus.name} {ccPairsIndexingStatus.name}
</p> </p>
</TableCell> </TableCell>

View File

@@ -1,46 +1,42 @@
import { Persona } from "../admin/assistants/interfaces"; import { Persona } from "../admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon"; import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { useState } from "react"; import { useState } from "react";
import { DisplayAssistantCard } from "@/components/assistants/AssistantCards"; import { DisplayAssistantCard } from "@/components/assistants/AssistantDescriptionCard";
export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) { export function ChatIntro({ selectedPersona }: { selectedPersona: Persona }) {
const [hoveredAssistant, setHoveredAssistant] = useState(false); const [hoveredAssistant, setHoveredAssistant] = useState(false);
return ( return (
<> <div className="flex flex-col items-center gap-6">
<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="relative flex w-fit mx-auto justify-center"> <div className="absolute z-10 -left-20 top-1/2 -translate-y-1/2">
<div className="absolute z-10 -left-20 top-1/2 -translate-y-1/2"> <div className="relative">
<div className="relative"> <div
<div onMouseEnter={() => setHoveredAssistant(true)}
onMouseEnter={() => setHoveredAssistant(true)} onMouseLeave={() => setHoveredAssistant(false)}
onMouseLeave={() => setHoveredAssistant(false)} className="p-4 scale-[.7] cursor-pointer border-dashed rounded-full flex border border-gray-300 border-2 border-dashed"
className="p-4 scale-[.8] cursor-pointer border-dashed rounded-full flex border border-border border-2 border-dashed" >
style={{ <AssistantIcon
borderStyle: "dashed", disableToolip
borderWidth: "1.5px", size="large"
borderSpacing: "4px", assistant={selectedPersona}
}} />
> </div>
<AssistantIcon <div className="absolute right-full mr-1 w-[300px] top-0">
disableToolip {hoveredAssistant && (
size={"large"} <DisplayAssistantCard selectedPersona={selectedPersona} />
assistant={selectedPersona} )}
/>
</div>
<div className="absolute right-full mr-2 w-[300px] top-0">
{hoveredAssistant && (
<DisplayAssistantCard selectedPersona={selectedPersona} />
)}
</div>
</div> </div>
</div> </div>
</div>
<div className="text-3xl line-clamp-2 text-text-800 font-base font-semibold text-strong"> <div className="text-2xl text-black font-semibold text-center">
{selectedPersona?.name || "How can I help you today?"} {selectedPersona.name}
</div>
</div> </div>
</div> </div>
</> <p className="text-base text-black font-normal text-center">
{selectedPersona.description}
</p>
</div>
); );
} }

View File

@@ -66,7 +66,7 @@ import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
import { FiArrowDown } from "react-icons/fi"; import { FiArrowDown } from "react-icons/fi";
import { ChatIntro } from "./ChatIntro"; import { ChatIntro } from "./ChatIntro";
import { AIMessage, HumanMessage } from "./message/Messages"; import { AIMessage, HumanMessage } from "./message/Messages";
import { StarterMessage } from "./StarterMessage"; import { StarterMessages } from "../../components/assistants/StarterMessage";
import { import {
AnswerPiecePacket, AnswerPiecePacket,
DanswerDocument, DanswerDocument,
@@ -104,6 +104,14 @@ import BlurBackground from "./shared_chat_search/BlurBackground";
import { NoAssistantModal } from "@/components/modals/NoAssistantModal"; import { NoAssistantModal } from "@/components/modals/NoAssistantModal";
import { useAssistants } from "@/components/context/AssistantsContext"; import { useAssistants } from "@/components/context/AssistantsContext";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import {
Card,
CardContent,
CardDescription,
CardHeader,
} from "@/components/ui/card";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import AssistantBanner from "../../components/assistants/AssistantBanner";
const TEMP_USER_MESSAGE_ID = -1; const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2; const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -140,7 +148,7 @@ export function ChatPage({
!shouldShowWelcomeModal !shouldShowWelcomeModal
); );
const { user, isAdmin, isLoadingUser } = useUser(); const { user, isAdmin, isLoadingUser, refreshUser } = useUser();
const existingChatIdRaw = searchParams.get("chatId"); const existingChatIdRaw = searchParams.get("chatId");
const [sendOnLoad, setSendOnLoad] = useState<string | null>( const [sendOnLoad, setSendOnLoad] = useState<string | null>(
@@ -233,9 +241,17 @@ export function ChatPage({
const [alternativeAssistant, setAlternativeAssistant] = const [alternativeAssistant, setAlternativeAssistant] =
useState<Persona | null>(null); useState<Persona | null>(null);
const {
visibleAssistants: assistants,
recentAssistants,
assistants: allAssistants,
refreshRecentAssistants,
} = useAssistants();
const liveAssistant = const liveAssistant =
alternativeAssistant || alternativeAssistant ||
selectedAssistant || selectedAssistant ||
recentAssistants[0] ||
finalAssistants[0] || finalAssistants[0] ||
availableAssistants[0]; availableAssistants[0];
@@ -737,7 +753,7 @@ export function ChatPage({
setMaxTokens(maxTokens); setMaxTokens(maxTokens);
} }
} }
refreshRecentAssistants(liveAssistant?.id);
fetchMaxTokens(); fetchMaxTokens();
}, [liveAssistant]); }, [liveAssistant]);
@@ -2005,48 +2021,35 @@ export function ChatPage({
!isFetchingChatMessages && !isFetchingChatMessages &&
currentSessionChatState == "input" && currentSessionChatState == "input" &&
!loadingError && ( !loadingError && (
<div className="h-full flex flex-col justify-center items-center"> <div className="h-full mt-12 flex flex-col justify-center items-center">
<ChatIntro selectedPersona={liveAssistant} /> <ChatIntro selectedPersona={liveAssistant} />
<div <StarterMessages
key={-4} currentPersona={currentPersona}
className={` onSubmit={(messageOverride) =>
mx-auto onSubmit({
px-4 messageOverride,
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 && (
<>
<Separator className="mx-2" />
{currentPersona.starter_messages {!isFetchingChatMessages &&
.slice(0, 4) currentSessionChatState == "input" &&
.map((starterMessage, i) => ( !loadingError &&
<div key={i} className="w-1/2"> allAssistants.length > 1 && (
<StarterMessage <div className="mx-auto px-4 w-full max-w-[750px] flex flex-col items-center">
starterMessage={starterMessage} <Separator className="mx-2 w-full my-12" />
onClick={() => <div className="text-sm text-black font-medium mb-4">
onSubmit({ Recent Assistants
messageOverride: </div>
starterMessage.message, <AssistantBanner
}) recentAssistants={recentAssistants}
} liveAssistant={liveAssistant}
/> allAssistants={allAssistants}
</div> onAssistantChange={onAssistantChange}
))} />
</> </div>
)} )}
</div>
</div> </div>
)} )}

View File

@@ -1,29 +0,0 @@
import { StarterMessage as StarterMessageType } from "../admin/assistants/interfaces";
export function StarterMessage({
starterMessage,
onClick,
}: {
starterMessage: StarterMessageType;
onClick: () => void;
}) {
return (
<div
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}
>
<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

@@ -0,0 +1,118 @@
import { Persona } from "../../app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "@/components/ui/tooltip";
export default function AssistantBanner({
recentAssistants,
liveAssistant,
allAssistants,
onAssistantChange,
}: {
recentAssistants: Persona[];
liveAssistant: Persona | undefined;
allAssistants: Persona[];
onAssistantChange: (assistant: Persona) => void;
}) {
return (
<div className="flex mx-auto mt-2 gap-4 ">
{recentAssistants
// First filter out the current assistant
.filter((assistant) => assistant.id !== liveAssistant?.id)
// Combine with visible assistants to get up to 4 total
.concat(
allAssistants.filter(
(assistant) =>
// Exclude current assistant
assistant.id !== liveAssistant?.id &&
// Exclude assistants already in recentAssistants
!recentAssistants.some((recent) => recent.id === assistant.id)
)
)
// Take first 4
.slice(0, 4)
.map((assistant) => (
<TooltipProvider key={assistant.id}>
<Tooltip>
<TooltipTrigger asChild>
<div
className="flex w-36 mx-3 py-1.5 scale-[1.] rounded-full border border-background-150 justify-center items-center gap-x-2 py-1 px-3 hover:bg-background-125 transition-colors cursor-pointer"
onClick={() => onAssistantChange(assistant)}
>
<AssistantIcon
disableToolip
size="xs"
assistant={assistant}
/>
<span className="font-semibold text-text-800 text-xs truncate max-w-[120px]">
{assistant.name}
</span>
</div>
</TooltipTrigger>
<TooltipContent backgroundColor="bg-background">
<AssistantCard assistant={assistant} />
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
);
}
export function AssistantCard({ assistant }: { assistant: Persona }) {
return (
<div className="p-6 backdrop-blur-sm rounded-lg max-w-md w-full mx-auto">
<div className="flex items-center mb-4">
<div className="mb-auto mt-2">
<AssistantIcon disableToolip size="small" assistant={assistant} />
</div>
<div className="ml-3">
<h2 className="text-lg font-semibold text-gray-800">
{assistant.name}
</h2>
<p className="text-sm text-gray-600">{assistant.description}</p>
</div>
</div>
{assistant.tools.length > 0 ||
assistant.llm_relevance_filter ||
assistant.llm_filter_extraction ? (
<div className="space-y-4">
<h3 className="text-base font-medium text-gray-800">Capabilities</h3>
<ul className="space-y-2">
{assistant.tools.map((tool, index) => (
<li
key={index}
className="flex items-center text-sm text-gray-700"
>
<span className="mr-2 text-gray-500"></span>
{tool.display_name}
</li>
))}
{assistant.llm_relevance_filter && (
<li className="flex items-center text-sm text-gray-700">
<span className="mr-2 text-gray-500"></span>
Advanced Relevance Filtering
</li>
)}
{assistant.llm_filter_extraction && (
<li className="flex items-center text-sm text-gray-700">
<span className="mr-2 text-gray-500"></span>
Smart Information Extraction
</li>
)}
</ul>
</div>
) : (
<p className="text-sm text-gray-600 italic">
No specific capabilities listed for this assistant.
</p>
)}
</div>
);
}

View File

@@ -1,5 +1,3 @@
import { CSS } from "@dnd-kit/utilities";
import { Persona } from "@/app/admin/assistants/interfaces"; import { Persona } from "@/app/admin/assistants/interfaces";
import { AssistantTools } from "@/app/assistants/ToolsDisplay"; import { AssistantTools } from "@/app/assistants/ToolsDisplay";
import { Bubble } from "@/components/Bubble"; import { Bubble } from "@/components/Bubble";
@@ -115,63 +113,3 @@ export function DraggableAssistantCard(props: {
</div> </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

@@ -0,0 +1,63 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import React from "react";
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

@@ -25,7 +25,7 @@ export function AssistantIcon({
disableToolip, disableToolip,
}: { }: {
assistant: Persona; assistant: Persona;
size?: "small" | "medium" | "large" | "header"; size?: "xs" | "small" | "medium" | "large" | "header";
border?: boolean; border?: boolean;
disableToolip?: boolean; disableToolip?: boolean;
}) { }) {
@@ -52,7 +52,9 @@ export function AssistantIcon({
? "w-14 h-14" ? "w-14 h-14"
: size === "medium" : size === "medium"
? "w-8 h-8" ? "w-8 h-8"
: "w-6 h-6" : size === "xs"
? "w-4 h-4"
: "w-6 h-6"
}`} }`}
src={buildImgUrl(assistant.uploaded_image_id)} src={buildImgUrl(assistant.uploaded_image_id)}
loading="lazy" loading="lazy"
@@ -68,7 +70,9 @@ export function AssistantIcon({
? "w-14 h-14" ? "w-14 h-14"
: size === "medium" : size === "medium"
? "w-8 h-8" ? "w-8 h-8"
: "w-6 h-6" : size === "xs"
? "w-4 h-4"
: "w-6 h-6"
} `} } `}
> >
{createSVG( {createSVG(
@@ -80,7 +84,9 @@ export function AssistantIcon({
? 56 ? 56
: size === "medium" : size === "medium"
? 32 ? 32
: 24 : size === "xs"
? 16
: 24
)} )}
</div> </div>
) : ( ) : (
@@ -90,6 +96,7 @@ export function AssistantIcon({
${size === "large" ? "w-10 h-10" : ""} ${size === "large" ? "w-10 h-10" : ""}
${size === "header" ? "w-14 h-14" : ""} ${size === "header" ? "w-14 h-14" : ""}
${size === "medium" ? "w-8 h-8" : ""} ${size === "medium" ? "w-8 h-8" : ""}
${size === "xs" ? "w-4 h-4" : ""}
${!size || size === "small" ? "w-6 h-6" : ""} `} ${!size || size === "small" ? "w-6 h-6" : ""} `}
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
/> />

View File

@@ -0,0 +1,51 @@
import { useContext } from "react";
import { Persona } from "../../app/admin/assistants/interfaces";
import { SettingsContext } from "../settings/SettingsProvider";
export function StarterMessages({
currentPersona,
onSubmit,
}: {
currentPersona: Persona;
onSubmit: (messageOverride: string) => void;
}) {
const settings = useContext(SettingsContext);
const isMobile = settings?.isMobile;
return (
<div
key={-4}
className={`
mx-auto
w-full
${
isMobile
? "gap-x-2 w-2/3 justify-between"
: "justify-center max-w-[750px] items-start"
}
flex
mt-6
`}
>
{currentPersona?.starter_messages &&
currentPersona.starter_messages.length > 0 && (
<>
{currentPersona.starter_messages
.slice(0, isMobile ? 2 : 4)
.map((starterMessage, i) => (
<div key={i} className={`${isMobile ? "w-1/2" : "w-1/4"}`}>
<button
onClick={() => onSubmit(starterMessage.message)}
className={`relative flex ${
!isMobile && "w-40"
} flex-col gap-2 rounded-2xl shadow-sm border border-border px-3 py-2 text-start align-to text-wrap text-[15px] shadow-xs transition enabled:hover:bg-background-100 disabled:cursor-not-allowed line-clamp-3`}
style={{ height: `5.2rem` }}
>
{starterMessage.name}
</button>
</div>
))}
</>
)}
</div>
);
}

View File

@@ -22,7 +22,8 @@ interface AssistantsContextProps {
ownedButHiddenAssistants: Persona[]; ownedButHiddenAssistants: Persona[];
refreshAssistants: () => Promise<void>; refreshAssistants: () => Promise<void>;
isImageGenerationAvailable: boolean; isImageGenerationAvailable: boolean;
recentAssistants: Persona[];
refreshRecentAssistants: (currentAssistant: number) => Promise<void>;
// Admin only // Admin only
editablePersonas: Persona[]; editablePersonas: Persona[];
allAssistants: Persona[]; allAssistants: Persona[];
@@ -46,10 +47,21 @@ export const AssistantsProvider: React.FC<{
const [assistants, setAssistants] = useState<Persona[]>( const [assistants, setAssistants] = useState<Persona[]>(
initialAssistants || [] initialAssistants || []
); );
const { user, isLoadingUser, isAdmin } = useUser(); const { user, isLoadingUser, refreshUser, isAdmin } = useUser();
const [editablePersonas, setEditablePersonas] = useState<Persona[]>([]); const [editablePersonas, setEditablePersonas] = useState<Persona[]>([]);
const [allAssistants, setAllAssistants] = useState<Persona[]>([]); const [allAssistants, setAllAssistants] = useState<Persona[]>([]);
const [recentAssistants, setRecentAssistants] = useState<Persona[]>(
user?.preferences.recent_assistants
?.filter((assistantId) =>
assistants.find((assistant) => assistant.id === assistantId)
)
.map(
(assistantId) =>
assistants.find((assistant) => assistant.id === assistantId)!
) || []
);
const [isImageGenerationAvailable, setIsImageGenerationAvailable] = const [isImageGenerationAvailable, setIsImageGenerationAvailable] =
useState<boolean>(false); useState<boolean>(false);
@@ -98,6 +110,28 @@ export const AssistantsProvider: React.FC<{
fetchPersonas(); fetchPersonas();
}, [isAdmin]); }, [isAdmin]);
const refreshRecentAssistants = async (currentAssistant: number) => {
const response = await fetch("/api/user/recent-assistants", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
current_assistant: currentAssistant,
}),
});
if (!response.ok) {
return;
}
setRecentAssistants((recentAssistants) => [
assistants.find((assistant) => assistant.id === currentAssistant)!,
...recentAssistants.filter(
(assistant) => assistant.id !== currentAssistant
),
]);
};
const refreshAssistants = async () => { const refreshAssistants = async () => {
try { try {
const response = await fetch("/api/persona", { const response = await fetch("/api/persona", {
@@ -125,6 +159,12 @@ export const AssistantsProvider: React.FC<{
} catch (error) { } catch (error) {
console.error("Error refreshing assistants:", error); console.error("Error refreshing assistants:", error);
} }
setRecentAssistants(
assistants.filter(
(assistant) =>
user?.preferences.recent_assistants?.includes(assistant.id) || false
)
);
}; };
const { const {
@@ -167,6 +207,8 @@ export const AssistantsProvider: React.FC<{
editablePersonas, editablePersonas,
allAssistants, allAssistants,
isImageGenerationAvailable, isImageGenerationAvailable,
recentAssistants,
refreshRecentAssistants,
}} }}
> >
{children} {children}

View File

@@ -203,7 +203,7 @@ export default function CreateCredential({
for information on setting up this connector. for information on setting up this connector.
</p> </p>
)} )}
<CardSection className="!border-0 mt-4 flex flex-col gap-y-6"> <CardSection className="w-full !border-0 mt-4 flex flex-col gap-y-6">
<TextFormField <TextFormField
name="name" name="name"
placeholder="(Optional) credential name.." placeholder="(Optional) credential name.."

View File

@@ -471,9 +471,9 @@ export function HorizontalSourceSelector({
icon={<FiBook size={16} />} icon={<FiBook size={16} />}
defaultDisplay="Sets" defaultDisplay="Sets"
resetValues={resetDocuments} resetValues={resetDocuments}
width="w-fit max-w-24 ellipsis truncate" width="w-fit max-w-24 etext-llipsis truncate"
dropdownWidth="max-w-36 w-fit" dropdownWidth="max-w-36 w-fit"
optionClassName="truncate break-all ellipsis" optionClassName="truncate w-full break-all"
/> />
)} )}
@@ -506,7 +506,7 @@ export function HorizontalSourceSelector({
resetValues={resetTags} resetValues={resetTags}
width="w-fit max-w-24 ellipsis truncate" width="w-fit max-w-24 ellipsis truncate"
dropdownWidth="max-w-80 w-fit" dropdownWidth="max-w-80 w-fit"
optionClassName="truncate break-all ellipsis" optionClassName="truncate w-full break-all ellipsis"
/> />
)} )}
</div> </div>

View File

@@ -14,13 +14,16 @@ const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & { React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
maxWidth?: string; maxWidth?: string;
backgroundColor?: string;
} }
>(({ className, sideOffset = 4, maxWidth, ...props }, ref) => ( >(({ className, sideOffset = 4, maxWidth, backgroundColor, ...props }, ref) => (
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md border border-neutral-200 text-white bg-background-900 px-2 py-1.5 text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50 max-w-sm", `z-50 overflow-hidden rounded-md border border-neutral-200 text-white ${
backgroundColor || "bg-background-900"
} px-2 py-1.5 text-sm shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50 max-w-sm`,
className className
)} )}
{...props} {...props}

View File

@@ -8,6 +8,7 @@ interface UserPreferences {
visible_assistants: number[]; visible_assistants: number[];
hidden_assistants: number[]; hidden_assistants: number[];
default_model: string | null; default_model: string | null;
recent_assistants: number[];
} }
export enum UserStatus { export enum UserStatus {