refactor: Refresh "Agent Icon" to the new "Agent Avatar" standard (#6509)

This commit is contained in:
Raunak Bhagat authored and GitHub committed 2025-12-03 21:18:54 -08:00
1 parent 307464a736
commit 7f36fb2a4c
34 files changed
+486 -732

No files matched your search

@@ -0,0 +1,34 @@
"""Add icon_name field
Revision ID: ed9e44312505
Revises: 5e6f7a8b9c0d
Create Date: 2025-12-03 16:35:07.828393
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "ed9e44312505"
down_revision = "5e6f7a8b9c0d"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Add icon_name column
op.add_column("persona", sa.Column("icon_name", sa.String(), nullable=True))
# Remove old icon columns
op.drop_column("persona", "icon_shape")
op.drop_column("persona", "icon_color")
def downgrade() -> None:
# Re-add old icon columns
op.add_column("persona", sa.Column("icon_color", sa.String(), nullable=True))
op.add_column("persona", sa.Column("icon_shape", sa.Integer(), nullable=True))
# Remove icon_name column
op.drop_column("persona", "icon_name")
+1 -2
View File
@@ -2791,8 +2791,7 @@ class Persona(Base):
datetime_aware: Mapped[bool] = mapped_column(Boolean, default=True)
uploaded_image_id: Mapped[str | None] = mapped_column(String, nullable=True)
icon_color: Mapped[str | None] = mapped_column(String, nullable=True)
icon_shape: Mapped[int | None] = mapped_column(Integer, nullable=True)
icon_name: Mapped[str | None] = mapped_column(String, nullable=True)
# These are only defaults, users can select from all if desired
document_sets: Mapped[list[DocumentSet]] = relationship(
+4 -8
View File
@@ -269,9 +269,8 @@ def create_update_persona(
system_prompt=create_persona_request.system_prompt,
task_prompt=create_persona_request.task_prompt,
datetime_aware=create_persona_request.datetime_aware,
icon_color=create_persona_request.icon_color,
icon_shape=create_persona_request.icon_shape,
uploaded_image_id=create_persona_request.uploaded_image_id,
icon_name=create_persona_request.icon_name,
display_priority=create_persona_request.display_priority,
remove_image=create_persona_request.remove_image,
search_start_date=create_persona_request.search_start_date,
@@ -738,9 +737,8 @@ def upsert_persona(
tool_ids: list[int] | None = None,
persona_id: int | None = None,
commit: bool = True,
icon_color: str | None = None,
icon_shape: int | None = None,
uploaded_image_id: str | None = None,
icon_name: str | None = None,
display_priority: int | None = None,
is_visible: bool = True,
remove_image: bool | None = None,
@@ -841,10 +839,9 @@ def upsert_persona(
existing_persona.starter_messages = starter_messages
existing_persona.deleted = False # Un-delete if previously deleted
existing_persona.is_public = is_public
existing_persona.icon_color = icon_color
existing_persona.icon_shape = icon_shape
if remove_image or uploaded_image_id:
existing_persona.uploaded_image_id = uploaded_image_id
existing_persona.icon_name = icon_name
existing_persona.is_visible = is_visible
existing_persona.search_start_date = search_start_date
existing_persona.labels = labels or []
@@ -905,9 +902,8 @@ def upsert_persona(
llm_model_version_override=llm_model_version_override,
starter_messages=starter_messages,
tools=tools or [],
icon_shape=icon_shape,
icon_color=icon_color,
uploaded_image_id=uploaded_image_id,
icon_name=icon_name,
display_priority=display_priority,
is_visible=is_visible,
search_start_date=search_start_date,
+3 -10
View File
@@ -47,12 +47,11 @@ class PrebuiltPersona(BaseModel):
)
# UI configuration
icon_shape: int = Field(default=0, description="Icon shape ID for UI")
icon_color: str = Field(default="#6FB1FF", description="Icon color hex code")
display_priority: int = Field(default=0, description="Display order priority")
is_visible: bool = Field(
default=True, description="Whether persona is visible in UI"
)
icon_name: Optional[str] = Field(default=None, description="A user-friendly icon")
# Special flags
is_default_persona: bool = Field(
@@ -111,10 +110,9 @@ PREBUILT_PERSONAS = [
llm_relevance_filter=False,
llm_filter_extraction=True,
recency_bias=RecencyBiasSetting.AUTO,
icon_shape=23013,
icon_color="#6FB1FF",
display_priority=0,
is_visible=True,
icon_name="search",
is_default_persona=True,
starter_messages=[
StarterMessage(
@@ -160,8 +158,6 @@ PREBUILT_PERSONAS = [
llm_relevance_filter=True,
llm_filter_extraction=True,
recency_bias=RecencyBiasSetting.AUTO,
icon_shape=50910,
icon_color="#FF6F6F",
display_priority=1,
is_visible=True,
is_default_persona=True,
@@ -213,8 +209,6 @@ PREBUILT_PERSONAS = [
llm_relevance_filter=True,
llm_filter_extraction=True,
recency_bias=RecencyBiasSetting.AUTO,
icon_shape=45519,
icon_color="#6FFF8D",
display_priority=2,
is_visible=False,
is_default_persona=True,
@@ -270,11 +264,10 @@ PREBUILT_PERSONAS = [
llm_relevance_filter=False,
llm_filter_extraction=False,
recency_bias=RecencyBiasSetting.NO_DECAY,
icon_shape=234124,
icon_color="#9B59B6",
image_generation=True,
display_priority=3,
is_visible=True,
icon_name="art",
is_default_persona=True,
starter_messages=[
StarterMessage(
+8 -12
View File
@@ -69,10 +69,11 @@ class PersonaUpsertRequest(BaseModel):
groups: list[int] = Field(default_factory=list)
# e.g. ID of SearchTool or ImageGenerationTool or <USER_DEFINED_TOOL>
tool_ids: list[int]
icon_color: str | None = None
icon_shape: int | None = None
remove_image: bool | None = None
uploaded_image_id: str | None = None # New field for uploaded image
icon_name: str | None = (
None # New field that is custom chosen during agent creation/editing
)
search_start_date: datetime | None = None
label_ids: list[int] | None = None
is_default_persona: bool = False
@@ -106,8 +107,7 @@ class MinimalPersonaSnapshot(BaseModel):
llm_model_provider_override: str | None
uploaded_image_id: str | None
icon_shape: int | None
icon_color: str | None
icon_name: str | None
is_public: bool
is_visible: bool
@@ -143,8 +143,7 @@ class MinimalPersonaSnapshot(BaseModel):
llm_model_version_override=persona.llm_model_version_override,
llm_model_provider_override=persona.llm_model_provider_override,
uploaded_image_id=persona.uploaded_image_id,
icon_shape=persona.icon_shape,
icon_color=persona.icon_color,
icon_name=persona.icon_name,
is_public=persona.is_public,
is_visible=persona.is_visible,
display_priority=persona.display_priority,
@@ -165,9 +164,8 @@ class PersonaSnapshot(BaseModel):
description: str
is_public: bool
is_visible: bool
icon_shape: int | None
icon_color: str | None
uploaded_image_id: str | None
icon_name: str | None
# Return string UUIDs to frontend for consistency
user_file_ids: list[str]
display_priority: int | None
@@ -199,9 +197,8 @@ class PersonaSnapshot(BaseModel):
description=persona.description,
is_public=persona.is_public,
is_visible=persona.is_visible,
icon_shape=persona.icon_shape,
icon_color=persona.icon_color,
uploaded_image_id=persona.uploaded_image_id,
icon_name=persona.icon_name,
user_file_ids=[str(file.id) for file in persona.user_files],
display_priority=persona.display_priority,
is_default_persona=persona.is_default_persona,
@@ -262,9 +259,8 @@ class FullPersonaSnapshot(PersonaSnapshot):
description=persona.description,
is_public=persona.is_public,
is_visible=persona.is_visible,
icon_shape=persona.icon_shape,
icon_color=persona.icon_color,
uploaded_image_id=persona.uploaded_image_id,
icon_name=persona.icon_name,
user_file_ids=[str(file.id) for file in persona.user_files],
display_priority=persona.display_priority,
is_default_persona=persona.is_default_persona,
@@ -47,6 +47,7 @@ class CreateAssistantRequest(BaseModel):
description: Optional[str] = None
instructions: Optional[str] = None
tools: Optional[list[dict[str, Any]]] = None
icon_name: Optional[str] = None
file_ids: Optional[list[str]] = None
metadata: Optional[dict[str, Any]] = None
@@ -139,8 +140,7 @@ def create_assistant(
db_session=db_session,
document_set_ids=[],
tool_ids=tool_ids,
icon_color=None,
icon_shape=None,
icon_name=request.icon_name,
is_visible=True,
system_prompt=request.instructions or "",
task_prompt="",
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from onyx.server.openai_assistants_api.asssistants_api import (
from onyx.server.openai_assistants_api.assistants_api import (
router as assistants_router,
)
from onyx.server.openai_assistants_api.messages_api import router as messages_router
@@ -288,12 +288,7 @@ def get_chat_session(
description=chat_session.description,
persona_id=chat_session.persona_id,
persona_name=chat_session.persona.name if chat_session.persona else None,
persona_icon_color=(
chat_session.persona.icon_color if chat_session.persona else None
),
persona_icon_shape=(
chat_session.persona.icon_shape if chat_session.persona else None
),
personal_icon_name=chat_session.persona.icon_name,
current_alternate_model=chat_session.current_alternate_model,
messages=chat_message_details,
time_created=chat_session.time_created,
+1 -3
View File
@@ -263,15 +263,13 @@ class ChatSessionDetailResponse(BaseModel):
description: str | None
persona_id: int | None = None
persona_name: str | None
persona_icon_color: str | None
persona_icon_shape: int | None
personal_icon_name: str | None
messages: list[ChatMessageDetail]
time_created: datetime
shared_status: ChatSessionSharedStatus
current_alternate_model: str | None
current_temperature_override: float | None
deleted: bool = False
packets: list[list[Packet]]
+87 -155
View File
@@ -1,8 +1,7 @@
"use client";
import React, { JSX } from "react";
import React from "react";
import { Option } from "@/components/Dropdown";
import { generateRandomIconShape } from "@/lib/assistantIconUtils";
import {
CCPairBasicInfo,
DocumentSetSummary,
@@ -53,12 +52,11 @@ import {
GroupsIconSkeleton,
SwapIcon,
} from "@/components/icons/icons";
import { buildImgUrl } from "@/app/chat/components/files/images/utils";
import { debounce } from "lodash";
import { LLMProviderView } from "@/app/admin/configuration/llm/interfaces";
import StarterMessagesList from "@/app/admin/assistants/StarterMessageList";
import UnlabeledSwitchField from "@/refresh-components/formik-fields/UnlabeledSwitchField";
import { generateIdenticon } from "@/refresh-components/AgentIcon";
import CustomAgentAvatar from "@/refresh-components/avatars/CustomAgentAvatar";
import { BackButton } from "@/components/BackButton";
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
import { MinimalUserSnapshot } from "@/lib/types";
@@ -93,7 +91,6 @@ import {
import { useProjectsContext } from "@/app/chat/projects/ProjectsContext";
import FilePickerPopover from "@/refresh-components/popovers/FilePickerPopover";
import SvgTrash from "@/icons/trash";
import SvgEditBig from "@/icons/edit-big";
import SvgFiles from "@/icons/files";
import { useAgents } from "@/lib/hooks/useAgents";
import Text from "@/refresh-components/texts/Text";
@@ -115,7 +112,11 @@ function findWebSearchTool(tools: ToolSnapshot[]) {
return tools.find((tool) => tool.in_code_tool_id === WEB_SEARCH_TOOL_ID);
}
function SubLabel({ children }: { children: string | JSX.Element }) {
interface SubLabelProps {
children: React.ReactNode;
}
function SubLabel({ children }: SubLabelProps) {
return (
<div
className="text-sm text-description font-description mb-2"
@@ -159,29 +160,11 @@ export default function AssistantEditor({
const { labels, refreshLabels, createLabel, deleteLabel } = useLabels();
const settings = useContext(SettingsContext);
const colorOptions = [
"#FF6FBF",
"#6FB1FF",
"#B76FFF",
"#FFB56F",
"#6FFF8D",
"#FF6F6F",
"#6FFFFF",
];
const [presentingDocument, setPresentingDocument] =
useState<MinimalOnyxDocument | null>(null);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const userFilesModal = useCreateModal();
// both `defautIconColor` and `defaultIconShape` are state so that they
// persist across formik reformatting
const [defautIconColor, _setDeafultIconColor] = useState(
colorOptions[Math.floor(Math.random() * colorOptions.length)]
);
const [defaultIconShape] = useState<any>(
() => generateRandomIconShape().encodedGrid
);
const [isRefreshing, setIsRefreshing] = useState(false);
const [removePersonaImage, setRemovePersonaImage] = useState(false);
@@ -267,8 +250,6 @@ export default function AssistantEditor({
? existingPersona.starter_messages
: [{ message: "", name: "" }],
enabled_tools_map: enabledToolsMap,
icon_color: existingPersona?.icon_color ?? defautIconColor,
icon_shape: existingPersona?.icon_shape ?? defaultIconShape,
uploaded_image: null,
labels: existingPersona?.labels ?? null,
@@ -374,9 +355,7 @@ export default function AssistantEditor({
fetchMcpServers();
}, []);
if (!labels) {
return <></>;
}
if (!labels) return null;
const openDeleteModal = () => {
setDeleteModalOpen(true);
@@ -487,8 +466,6 @@ export default function AssistantEditor({
})
),
search_start_date: Yup.date().nullable(),
icon_color: Yup.string(),
icon_shape: Yup.number(),
uploaded_image: Yup.mixed().nullable(),
// EE Only
label_ids: Yup.array().of(Yup.number()),
@@ -573,7 +550,6 @@ export default function AssistantEditor({
const submissionData: PersonaUpsertParameters = {
...values,
icon_color: values.icon_color ?? null,
starter_messages: starterMessages,
groups: groups,
users: values.is_public
@@ -686,29 +662,15 @@ export default function AssistantEditor({
values.llm_model_version_override || defaultModelName || ""
);
const iconElement = (() => {
if (uploadedImagePreview) {
return (
<img
src={uploadedImagePreview}
alt="Uploaded agent icon"
className="w-12 h-12 rounded-full object-cover"
/>
);
}
const src =
uploadedImagePreview ??
(existingPersona?.uploaded_image_id && !removePersonaImage
? existingPersona?.uploaded_image_id
: undefined);
if (existingPersona?.uploaded_image_id && !removePersonaImage) {
return (
<img
src={buildImgUrl(existingPersona?.uploaded_image_id)}
alt="Uploaded agent icon"
className="w-12 h-12 rounded-full object-cover"
/>
);
}
return generateIdenticon((values.icon_shape || 0).toString(), 36);
})();
const iconElement = (
<CustomAgentAvatar name={values.name} src={src} size={48} />
);
return (
<>
@@ -744,6 +706,7 @@ export default function AssistantEditor({
<Form className="w-full text-text-950 assistant-editor">
<FormErrorFocus />
{/* Refresh starter messages when name or description changes */}
<p className="text-base font-normal text-2xl">
{existingPersona ? (
@@ -754,6 +717,7 @@ export default function AssistantEditor({
"Create an Agent"
)}
</p>
<div className="max-w-4xl w-full">
<Separator />
<div className="flex gap-x-2 items-center">
@@ -815,29 +779,6 @@ export default function AssistantEditor({
</Button>
)}
{!values.uploaded_image &&
(!existingPersona?.uploaded_image_id ||
removePersonaImage) && (
<Button
secondary
onClick={(e) => {
e.stopPropagation();
const newShape = generateRandomIconShape();
const randomColor =
colorOptions[
Math.floor(
Math.random() * colorOptions.length
)
];
setFieldValue("icon_shape", newShape.encodedGrid);
setFieldValue("icon_color", randomColor);
}}
leftIcon={SvgEditBig}
>
Generate Icon
</Button>
)}
{existingPersona?.uploaded_image_id &&
removePersonaImage &&
!values.uploaded_image && (
@@ -883,98 +824,90 @@ export default function AssistantEditor({
<div className="w-full max-w-4xl">
<div className="flex flex-col">
<>
<Separator />
<div className="flex gap-x-2 py-2 justify-start">
<div>
<div className="flex items-start gap-x-2">
<p className="block font-medium text-sm">
Knowledge
</p>
<div className="flex items-center">
<SimpleTooltip
tooltip="To use Knowledge, you need to have at least one Connector configured. You can still upload user files to the agent below."
side="top"
align="center"
disabled={connectorsExist}
>
<div
className={`${
!connectorsExist || !searchTool
? "opacity-70 cursor-not-allowed"
: ""
}`}
>
<UnlabeledSwitchField
onCheckedChange={() =>
toggleToolInValues(searchTool?.id || -1)
}
name={`enabled_tools_map.${
searchTool?.id || -1
}`}
disabled={!connectorsExist || !searchTool}
/>
</div>
</SimpleTooltip>
<Separator />
<div className="flex gap-x-2 py-2 justify-start">
<div className="flex items-start gap-x-2">
<p className="block font-medium text-sm">Knowledge</p>
<div className="flex items-center">
<SimpleTooltip
tooltip="To use Knowledge, you need to have at least one Connector configured. You can still upload user files to the agent below."
side="top"
align="center"
disabled={connectorsExist}
>
<div
className={`${
!connectorsExist || !searchTool
? "opacity-70 cursor-not-allowed"
: ""
}`}
>
<UnlabeledSwitchField
onCheckedChange={() =>
toggleToolInValues(searchTool?.id || -1)
}
name={`enabled_tools_map.${
searchTool?.id || -1
}`}
disabled={!connectorsExist || !searchTool}
/>
</div>
</div>
</SimpleTooltip>
</div>
</div>
</>
</div>
{((searchTool && values.enabled_tools_map[searchTool.id]) ||
!connectorsExist) && (
<div>
{canShowKnowledgeSource && (
<>
<div className="mt-1.5 mb-2.5">
<div className="flex gap-2.5">
<div className="mt-1.5 mb-2.5">
<div className="flex gap-2.5">
<div
className={`w-[150px] h-[110px] rounded-lg border flex flex-col items-center justify-center cursor-pointer transition-all ${
values.knowledge_source === "team_knowledge"
? "border-2 border-blue-500 bg-blue-50 dark:bg-blue-950/20"
: "border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600"
}`}
onClick={() =>
setFieldValue(
"knowledge_source",
"team_knowledge"
)
}
>
<div className="text-blue-500 mb-2">
<BookIcon size={24} />
</div>
<p className="font-medium text-xs">
Team Knowledge
</p>
</div>
{userKnowledgeEnabled && (
<div
className={`w-[150px] h-[110px] rounded-lg border flex flex-col items-center justify-center cursor-pointer transition-all ${
values.knowledge_source === "team_knowledge"
values.knowledge_source === "user_files"
? "border-2 border-blue-500 bg-blue-50 dark:bg-blue-950/20"
: "border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600"
}`}
onClick={() =>
setFieldValue(
"knowledge_source",
"team_knowledge"
"user_files"
)
}
>
<div className="text-blue-500 mb-2">
<BookIcon size={24} />
<FileIcon size={24} />
</div>
<p className="font-medium text-xs">
Team Knowledge
User Knowledge
</p>
</div>
{userKnowledgeEnabled && (
<div
className={`w-[150px] h-[110px] rounded-lg border flex flex-col items-center justify-center cursor-pointer transition-all ${
values.knowledge_source === "user_files"
? "border-2 border-blue-500 bg-blue-50 dark:bg-blue-950/20"
: "border-gray-200 hover:border-gray-300 dark:border-gray-700 dark:hover:border-gray-600"
}`}
onClick={() =>
setFieldValue(
"knowledge_source",
"user_files"
)
}
>
<div className="text-blue-500 mb-2">
<FileIcon size={24} />
</div>
<p className="font-medium text-xs">
User Knowledge
</p>
</div>
)}
</div>
)}
</div>
</>
</div>
)}
{values.knowledge_source === "user_files" &&
@@ -1256,8 +1189,8 @@ export default function AssistantEditor({
</div>
</div>
</div>
<Separator />
<Separator />
<div className="-mt-2">
<div className="flex gap-x-2 mb-2 items-center">
<div className="block font-medium text-sm">
@@ -1311,6 +1244,7 @@ export default function AssistantEditor({
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
{showAdvancedOptions && (
<>
<div className="max-w-4xl w-full">
@@ -1635,8 +1569,8 @@ export default function AssistantEditor({
</div>
</div>
</div>
<Separator />
<Separator />
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-4">
<h3 className="font-medium text-sm">
@@ -1675,8 +1609,8 @@ export default function AssistantEditor({
</div>
</div>
</div>
<Separator />
<Separator />
<BooleanFormField
small
removeIndent
@@ -1686,19 +1620,17 @@ export default function AssistantEditor({
/>
<Separator />
<TaskPromptField />
</>
)}
<div className="mt-12 w-full flex justify-between items-center">
<div>
{existingPersona && (
<Button danger onClick={openDeleteModal}>
Delete
</Button>
)}
</div>
{existingPersona && (
<Button danger onClick={openDeleteModal}>
Delete
</Button>
)}
<div className="flex gap-x-2 items-center">
<Button
disabled={
+2 -2
View File
@@ -4,6 +4,7 @@ import { DocumentSetSummary, MinimalUserSnapshot } from "@/lib/types";
export interface StarterMessageBase {
message: string;
}
export interface StarterMessage extends StarterMessageBase {
name: string;
}
@@ -19,8 +20,7 @@ export interface MinimalPersonaSnapshot {
llm_model_provider_override?: string;
uploaded_image_id?: string;
icon_shape?: number;
icon_color?: string;
icon_name?: string;
is_public: boolean;
is_visible: boolean;
+16 -83
View File
@@ -1,5 +1,8 @@
import { LLMProviderView } from "../configuration/llm/interfaces";
import { MinimalPersonaSnapshot, Persona, StarterMessage } from "./interfaces";
import {
MinimalPersonaSnapshot,
Persona,
StarterMessage,
} from "@/app/admin/assistants/interfaces";
interface PersonaUpsertRequest {
name: string;
@@ -19,10 +22,9 @@ interface PersonaUpsertRequest {
users?: string[];
groups: number[];
tool_ids: number[];
icon_color: string | null;
icon_shape: number | null;
remove_image?: boolean;
uploaded_image_id: string | null;
icon_name: string | null;
search_start_date: Date | null;
is_default_persona: boolean;
display_priority: number | null;
@@ -46,8 +48,6 @@ export interface PersonaUpsertParameters {
users?: string[];
groups: number[];
tool_ids: number[];
icon_color: string | null;
icon_shape: number | null;
remove_image?: boolean;
search_start_date: Date | null;
uploaded_image: File | null;
@@ -56,43 +56,10 @@ export interface PersonaUpsertParameters {
user_file_ids: string[];
}
export const createPersonaLabel = (name: string) => {
return fetch("/api/persona/labels", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ name }),
});
};
export const deletePersonaLabel = (labelId: number) => {
return fetch(`/api/admin/persona/label/${labelId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
};
export const updatePersonaLabel = (
id: number,
name: string
): Promise<Response> => {
return fetch(`/api/admin/persona/label/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
label_name: name,
}),
});
};
function buildPersonaUpsertRequest(
creationRequest: PersonaUpsertParameters,
uploaded_image_id: string | null
uploaded_image_id: string | null,
icon_name: string | null
): PersonaUpsertRequest {
const {
name,
@@ -106,8 +73,6 @@ function buildPersonaUpsertRequest(
datetime_aware,
users,
tool_ids,
icon_color,
icon_shape,
remove_image,
search_start_date,
user_file_ids,
@@ -122,11 +87,10 @@ function buildPersonaUpsertRequest(
num_chunks,
is_public,
uploaded_image_id,
icon_name,
groups,
users,
tool_ids,
icon_color,
icon_shape,
remove_image,
search_start_date,
datetime_aware,
@@ -178,7 +142,7 @@ export async function createPersona(
"Content-Type": "application/json",
},
body: JSON.stringify(
buildPersonaUpsertRequest(personaUpsertParams, fileId)
buildPersonaUpsertRequest(personaUpsertParams, fileId, null)
),
});
@@ -203,7 +167,7 @@ export async function updatePersona(
"Content-Type": "application/json",
},
body: JSON.stringify(
buildPersonaUpsertRequest(personaUpsertParams, fileId)
buildPersonaUpsertRequest(personaUpsertParams, fileId, null)
),
});
@@ -260,10 +224,10 @@ export function personaComparator(
return closerToZeroNegativesFirstComparator(a.id, b.id);
}
export const togglePersonaDefault = async (
export async function togglePersonaDefault(
personaId: number,
isDefault: boolean
) => {
) {
const response = await fetch(`/api/admin/persona/${personaId}/default`, {
method: "PATCH",
headers: {
@@ -274,12 +238,12 @@ export const togglePersonaDefault = async (
}),
});
return response;
};
}
export const togglePersonaVisibility = async (
export async function togglePersonaVisibility(
personaId: number,
isVisible: boolean
) => {
) {
const response = await fetch(`/api/admin/persona/${personaId}/visible`, {
method: "PATCH",
headers: {
@@ -290,35 +254,4 @@ export const togglePersonaVisibility = async (
}),
});
return response;
};
export const togglePersonaPublicStatus = async (
personaId: number,
isPublic: boolean
) => {
const response = await fetch(`/api/persona/${personaId}/public`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
is_public: isPublic,
}),
});
return response;
};
export function checkPersonaRequiresImageGeneration(persona: Persona) {
for (const tool of persona.tools) {
if (tool.name === "ImageGenerationTool") {
return true;
}
}
return false;
}
export function providersContainImageGeneratingSupport(
providers: LLMProviderView[]
) {
return providers.some((provider) => provider.provider === "openai");
}
@@ -28,7 +28,7 @@ import {
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { SourceIcon } from "@/components/SourceIcon";
import Link from "next/link";
import AgentIcon from "@/refresh-components/AgentIcon";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import { Badge } from "@/components/ui/badge";
import {
Accordion,
@@ -390,7 +390,7 @@ export function SlackChannelConfigFormFields({
key={persona.id}
className="p-2 bg-background-100 cursor-pointer rounded-md flex items-center gap-2"
>
<AgentIcon agent={persona} size={16} />
<AgentAvatar agent={persona} size={16} />
{persona.name}
</button>
)
@@ -1,8 +1,9 @@
// import { AssistantIcon } from "@/components/assistants/AssistantIcon";
"use client";
import { Logo } from "@/components/logo/Logo";
import { getRandomGreeting } from "@/lib/chat/greetingMessages";
import { cn } from "@/lib/utils";
import AgentIcon from "@/refresh-components/AgentIcon";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import Text from "@/refresh-components/texts/Text";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { useMemo } from "react";
@@ -17,6 +18,7 @@ export default function WelcomeMessage({
isDefaultAgent,
}: WelcomeMessageProps) {
let content: React.ReactNode = null;
if (isDefaultAgent) {
const greeting = useMemo(getRandomGreeting, []);
content = (
@@ -32,7 +34,7 @@ export default function WelcomeMessage({
data-testid="assistant-name-display"
className="flex flex-row items-center gap-3"
>
<AgentIcon agent={agent} />
<AgentAvatar agent={agent} size={36} />
<Text headingH2>{agent.name}</Text>
</div>
{agent.description && (
@@ -5,7 +5,7 @@ import Link from "next/link";
import { ChatSessionMorePopup } from "@/components/sidebar/ChatSessionMorePopup";
import { useProjectsContext } from "../../projects/ProjectsContext";
import { ChatSession } from "@/app/chat/interfaces";
import AgentIcon from "@/refresh-components/AgentIcon";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import SvgBubbleText from "@/icons/bubble-text";
import { useAgents } from "@/lib/hooks/useAgents";
import { formatRelativeTime } from "./project_utils";
@@ -84,7 +84,7 @@ export default function ProjectChatSessionList() {
if (assistant) {
return (
<div className="h-full pt-1">
<AgentIcon agent={assistant} size={18} />
<AgentAvatar agent={assistant} size={18} />
</div>
);
}
-2
View File
@@ -150,8 +150,6 @@ export interface BackendChatSession {
description: string;
persona_id: number;
persona_name: string;
persona_icon_color: string | null;
persona_icon_shape: number | null;
messages: BackendMessage[];
time_created: string;
time_updated: string;
@@ -32,7 +32,7 @@ import {
import { useMessageSwitching } from "@/app/chat/message/messageComponents/hooks/useMessageSwitching";
import MultiToolRenderer from "@/app/chat/message/messageComponents/MultiToolRenderer";
import { RendererComponent } from "@/app/chat/message/messageComponents/renderMessageComponent";
import AgentIcon from "@/refresh-components/AgentIcon";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import IconButton from "@/refresh-components/buttons/IconButton";
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
import SvgThumbsUp from "@/icons/thumbs-up";
@@ -427,7 +427,7 @@ export default function AIMessage({
<div className="mx-auto w-[90%] max-w-message-max">
<div className="lg:mr-12 mobile:ml-0 md:ml-8">
<div className="flex items-start">
<AgentIcon agent={chatState.assistant} />
<AgentAvatar agent={chatState.assistant} size={24} />
<div className="w-full">
<div className="max-w-message-max break-words">
<div className="w-full desktop:ml-4">
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo } from "react";
import { useEffect, useMemo } from "react";
import SvgCode from "@/icons/code";
import {
PacketType,
@@ -6,9 +6,11 @@ import {
PythonToolStart,
PythonToolDelta,
SectionEnd,
} from "../../../services/streamingModels";
import { MessageRenderer, RenderType } from "../interfaces";
import { IconProps } from "@/icons";
} from "@/app/chat/services/streamingModels";
import {
MessageRenderer,
RenderType,
} from "@/app/chat/message/messageComponents/interfaces";
import { CodeBlock } from "@/app/chat/message/CodeBlock";
import hljs from "highlight.js/lib/core";
import python from "highlight.js/lib/languages/python";
@@ -71,10 +73,6 @@ function constructCurrentPythonState(packets: PythonToolPacket[]) {
};
}
function CodeIcon({ size = 16, ...props }: IconProps) {
return <SvgCode width={size} height={size} {...props} />;
}
export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
packets,
onComplete,
@@ -108,7 +106,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
// Loading state - when executing
if (isExecuting) {
return children({
icon: CodeIcon,
icon: SvgCode,
status: "Executing Python code...",
content: (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -132,7 +130,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
// Complete state - show output
if (isComplete) {
return children({
icon: CodeIcon,
icon: SvgCode,
status: hasError
? "Python execution failed"
: "Python execution completed",
@@ -140,7 +138,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
<div className="flex flex-col my-1 space-y-2">
{code && (
<div className="prose max-w-full">
{/* NOTE: note that we need to trim since otherwise there's a huge
{/* NOTE: note that we need to trim since otherwise there's a huge
"space" at the start of the code block */}
<CodeBlock className="language-python" codeText={code.trim()}>
<HighlightedPythonCode code={code.trim()} />
@@ -185,7 +183,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
// Fallback
return children({
icon: CodeIcon,
icon: SvgCode,
status: status,
content: <div></div>,
});
@@ -194,7 +192,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
// Highlight/Short rendering
if (isExecuting) {
return children({
icon: CodeIcon,
icon: SvgCode,
status: "Executing Python code...",
content: (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -217,7 +215,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
if (hasError) {
return children({
icon: CodeIcon,
icon: SvgCode,
status: "Python execution failed",
content: (
<div className="text-sm text-red-600 dark:text-red-400">
@@ -229,7 +227,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
if (isComplete) {
return children({
icon: CodeIcon,
icon: SvgCode,
status: "Python execution completed",
content: (
<div className="text-sm text-muted-foreground">
@@ -244,7 +242,7 @@ export const PythonToolRenderer: MessageRenderer<PythonToolPacket, {}> = ({
}
return children({
icon: CodeIcon,
icon: SvgCode,
status: "Python execution",
content: (
<div className="text-sm text-muted-foreground">Python execution</div>
+24 -3
View File
@@ -4,9 +4,32 @@ import { requireAuth } from "@/lib/auth/requireAuth";
import SharedChatDisplay from "@/app/chat/shared/[chatId]/SharedChatDisplay";
import AppPageLayout from "@/layouts/AppPageLayout";
import { Persona } from "@/app/admin/assistants/interfaces";
import { constructMiniFiedPersona } from "@/lib/assistantIconUtils";
import { fetchHeaderDataSS } from "@/lib/headers/fetchHeaderDataSS";
// This is used for rendering a persona in the shared chat display
export function constructMiniFiedPersona(name: string, id: number): Persona {
return {
id,
name,
is_visible: true,
is_public: true,
display_priority: 0,
description: "",
document_sets: [],
tools: [],
owner: null,
starter_messages: null,
builtin_persona: false,
is_default_persona: false,
users: [],
groups: [],
user_file_ids: [],
system_prompt: null,
task_prompt: null,
datetime_aware: true,
};
}
async function getSharedChat(chatId: string) {
const response = await fetchSS(
`/chat/get-chat-session/${chatId}?is_shared=True`
@@ -34,8 +57,6 @@ export default async function Page(props: PageProps) {
const chatSession = await getSharedChat(params.chatId).catch(() => null);
const persona: Persona = constructMiniFiedPersona(
chatSession?.persona_icon_color ?? null,
chatSession?.persona_icon_shape ?? null,
chatSession?.persona_name ?? "",
chatSession?.persona_id ?? 0
);
@@ -16,10 +16,10 @@ import {
CardTitle,
} from "@/components/ui/card";
import Button from "@/refresh-components/buttons/Button";
import { CreditCard } from "@phosphor-icons/react";
import { SubscriptionSummary } from "./SubscriptionSummary";
import { BillingAlerts } from "./BillingAlerts";
import { ClipboardIcon } from "@/components/icons/icons";
import SvgClipboard from "@/icons/clipboard";
import SvgWallet from "@/icons/wallet";
export default function BillingInformationPage() {
const router = useRouter();
@@ -95,7 +95,7 @@ export default function BillingInformationPage() {
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-2xl font-bold flex items-center">
<CreditCard className="mr-4 text-muted-foreground" size={24} />
<SvgWallet className="mr-4 text-muted-foreground h-6 w-6" />
Subscription Details
</CardTitle>
</CardHeader>
@@ -118,7 +118,7 @@ export default function BillingInformationPage() {
<Button
onClick={handleManageSubscription}
className="w-full"
leftIcon={ClipboardIcon}
leftIcon={SvgClipboard}
>
Manage Subscription
</Button>
@@ -1,4 +1,5 @@
"use client";
import { ThreeDotsLoader } from "@/components/Loading";
import { getDatesList } from "@/app/ee/admin/performance/lib";
import { useEffect, useState, useMemo } from "react";
@@ -7,7 +8,7 @@ import {
DateRange,
} from "@/components/dateRangeSelectors/AdminDateRangeSelector";
import { useAgents } from "@/lib/hooks/useAgents";
import AssistantIcon from "@/refresh-components/AgentIcon";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { AreaChartDisplay } from "@/components/ui/areaChart";
@@ -156,7 +157,7 @@ export function AssistantStats({ assistantId }: { assistantId: number }) {
<Card>
<CardContent className="pt-6">
<div className="flex items-center space-x-4">
{assistant && <AssistantIcon agent={assistant} />}
{assistant && <AgentAvatar agent={assistant} />}
<div>
<h3 className="text-lg font-normal">{assistant?.name}</h3>
<p className="text-sm text-text-500">
+53 -1
View File
@@ -4,7 +4,59 @@ import { OnyxDocument } from "@/lib/search/interfaces";
import { ValidSources } from "@/lib/types";
import React, { useEffect, useState, JSX } from "react";
import { SearchResultIcon } from "@/components/SearchResultIcon";
import { getFileIconFromFileNameAndLink } from "@/lib/assistantIconUtils";
import {
FileOptionIcon,
PDFIcon,
TXTIcon,
DOCIcon,
HTMLIcon,
JSONIcon,
ImagesIcon,
XMLIcon,
} from "@/components/icons/icons";
export function getFileIconFromFileNameAndLink(
fileName: string,
linkUrl?: string | null
) {
if (linkUrl) {
return <SearchResultIcon url={linkUrl} />;
}
const extension = fileName.split(".").pop()?.toLowerCase();
if (extension === "pdf") {
return <PDFIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "txt") {
return <TXTIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "doc" || extension === "docx") {
return <DOCIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "html" || extension === "htm") {
return <HTMLIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "json") {
return <JSONIcon className="h-4 w-4 shrink-0" />;
} else if (
["jpg", "jpeg", "png", "gif", "svg", "webp"].includes(extension || "")
) {
return <ImagesIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "xml") {
return <XMLIcon className="h-4 w-4 shrink-0" />;
} else {
if (fileName.includes(".")) {
try {
// Check if fileName could be a valid domain when prefixed with https://
const url = new URL(`https://${fileName}`);
if (url.hostname === fileName) {
return <SearchResultIcon url={`https://${fileName}`} />;
}
} catch (e) {
// If URL construction fails, it's not a valid domain
}
return <FileOptionIcon className="h-4 w-4 shrink-0" />;
} else {
return <FileOptionIcon className="h-4 w-4 shrink-0" />;
}
}
}
// Minimal shape needed locally for file icon rendering
type FileResponse = {
id: number;
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgClipboard = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10.6667 2.66665H12C12.3536 2.66665 12.6927 2.80712 12.9428 3.05717C13.1928 3.30722 13.3333 3.64636 13.3333 3.99998V13.3333C13.3333 13.6869 13.1928 14.0261 12.9428 14.2761C12.6927 14.5262 12.3536 14.6666 12 14.6666H3.99999C3.64637 14.6666 3.30723 14.5262 3.05718 14.2761C2.80713 14.0261 2.66666 13.6869 2.66666 13.3333V3.99998C2.66666 3.64636 2.80713 3.30722 3.05718 3.05717C3.30723 2.80712 3.64637 2.66665 3.99999 2.66665H5.33332M10.6667 2.66665V1.99998C10.6667 1.63179 10.3682 1.33331 9.99999 1.33331H5.99999C5.6318 1.33331 5.33332 1.63179 5.33332 1.99998V2.66665M10.6667 2.66665V3.33331C10.6667 3.7015 10.3682 3.99998 9.99999 3.99998H5.99999C5.6318 3.99998 5.33332 3.7015 5.33332 3.33331V2.66665"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgClipboard;
+18
View File
@@ -0,0 +1,18 @@
import type { SVGProps } from "react";
const SvgTwoLineSmall = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M6 6.50002V9.50002M10 6.50002V9.50002"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgTwoLineSmall;
+18
View File
@@ -0,0 +1,18 @@
import * as React from "react";
import type { SVGProps } from "react";
const SvgWallet = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M14 4.75H9C8.44772 4.75 8 5.19772 8 5.75L8 10.25C8 10.8023 8.44772 11.25 9 11.25H14M14 4.75C14.5523 4.75 15 5.19772 15 5.75V10.25C15 10.8023 14.5523 11.25 14 11.25M14 4.75V3.33333C14 2.6 13.4 2 12.6667 2H3.33333C2.6 2 2 2.6 2 3.33333V12.6667C2 13.4 2.6 14 3.33333 14H12.6667C13.4 14 14 13.4 14 12.6667L14 11.25M10.25 7V9"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgWallet;
-199
View File
@@ -1,199 +0,0 @@
import { Persona } from "@/app/admin/assistants/interfaces";
import {
FileOptionIcon,
PDFIcon,
TXTIcon,
DOCIcon,
HTMLIcon,
JSONIcon,
ImagesIcon,
XMLIcon,
} from "@/components/icons/icons";
import { SearchResultIcon } from "@/components/SearchResultIcon";
export interface GridShape {
encodedGrid: number;
filledSquares: number;
}
export function generateRandomIconShape(): GridShape {
const grid: boolean[][] = Array(4)
.fill(null)
.map(() => Array(4).fill(false));
const centerSquares: number[][] = [
[1, 1],
[1, 2],
[2, 1],
[2, 2],
];
shuffleArray(centerSquares);
const centerFillCount = Math.floor(Math.random() * 2) + 3; // 3 or 4
for (let i = 0; i < centerFillCount; i++) {
const centerSquare: number[] | undefined = centerSquares[i];
if (centerSquare === undefined) {
continue;
}
const [row, col] = centerSquare;
if (row === undefined || col === undefined) {
continue;
}
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
grid_row[col] = true;
}
// Randomly fill remaining squares up to 10 total
const remainingSquares = [];
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (!grid_row[col]) {
remainingSquares.push([row, col]);
}
}
}
shuffleArray(remainingSquares);
let filledSquares = centerFillCount;
for (const [row, col] of remainingSquares) {
if (row === undefined || col == undefined) {
continue;
}
if (filledSquares >= 10) break;
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
grid_row[col] = true;
filledSquares++;
}
let path = "";
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (grid_row[col]) {
const x = col * 12;
const y = row * 12;
path += `M ${x} ${y} L ${x + 12} ${y} L ${x + 12} ${y + 12} L ${x} ${
y + 12
} Z `;
}
}
}
const encodedGrid = encodeGrid(grid);
return { encodedGrid, filledSquares };
}
function encodeGrid(grid: boolean[][]): number {
let encoded = 0;
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (grid_row[col]) {
encoded |= 1 << (row * 4 + col);
}
}
}
return encoded;
}
function shuffleArray(array: any[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// This is used for rendering a persona in the shared chat display
export const constructMiniFiedPersona = (
assistant_icon_color: string | null,
assistant_icon_shape: number | null,
name: string,
id: number
): Persona => {
return {
id,
name,
icon_color: assistant_icon_color ?? undefined,
icon_shape: assistant_icon_shape ?? undefined,
is_visible: true,
is_public: true,
display_priority: 0,
description: "",
document_sets: [],
tools: [],
owner: null,
starter_messages: null,
builtin_persona: false,
is_default_persona: false,
users: [],
groups: [],
user_file_ids: [],
system_prompt: null,
task_prompt: null,
datetime_aware: true,
};
};
export const getFileIconFromFileNameAndLink = (
fileName: string,
linkUrl?: string | null
) => {
if (linkUrl) {
return <SearchResultIcon url={linkUrl} />;
}
const extension = fileName.split(".").pop()?.toLowerCase();
if (extension === "pdf") {
return <PDFIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "txt") {
return <TXTIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "doc" || extension === "docx") {
return <DOCIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "html" || extension === "htm") {
return <HTMLIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "json") {
return <JSONIcon className="h-4 w-4 shrink-0" />;
} else if (
["jpg", "jpeg", "png", "gif", "svg", "webp"].includes(extension || "")
) {
return <ImagesIcon className="h-4 w-4 shrink-0" />;
} else if (extension === "xml") {
return <XMLIcon className="h-4 w-4 shrink-0" />;
} else {
if (fileName.includes(".")) {
try {
// Check if fileName could be a valid domain when prefixed with https://
const url = new URL(`https://${fileName}`);
if (url.hostname === fileName) {
return <SearchResultIcon url={`https://${fileName}`} />;
}
} catch (e) {
// If URL construction fails, it's not a valid domain
}
return <FileOptionIcon className="h-4 w-4 shrink-0" />;
} else {
return <FileOptionIcon className="h-4 w-4 shrink-0" />;
}
}
};
+2 -2
View File
@@ -2,7 +2,7 @@
import React, { useMemo } from "react";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import AgentIcon from "@/refresh-components/AgentIcon";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
import Button from "@/refresh-components/buttons/Button";
import Text from "@/refresh-components/texts/Text";
import SvgBubbleText from "@/icons/bubble-text";
@@ -72,7 +72,7 @@ export default function AgentCard({ agent }: AgentCardProps) {
<div className="flex flex-row items-center w-full gap-1">
<div className="flex flex-row items-center w-full p-1.5 gap-1.5">
<div className="px-0.5">
<AgentIcon agent={agent} size={18} />
<AgentAvatar agent={agent} size={18} />
</div>
<Truncated mainContentBody className="flex-1">
{agent.name}
-133
View File
@@ -1,133 +0,0 @@
import React, { JSX } from "react";
import crypto from "crypto";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { buildImgUrl } from "@/app/chat/components/files/images/utils";
import { ArtAsistantIcon, OnyxIcon } from "@/components/icons/icons";
import SimpleTooltip from "@/refresh-components/SimpleTooltip";
import { cn } from "@/lib/utils";
import Text from "@/refresh-components/texts/Text";
import { useSettingsContext } from "@/components/settings/SettingsProvider";
function md5ToBits(str: string): number[] {
const md5hex = crypto.createHash("md5").update(str).digest("hex");
const bits: number[] = [];
for (let i = 0; i < md5hex.length; i += 2) {
const byteVal = parseInt(md5hex.substring(i, i + 2), 16);
for (let b = 7; b >= 0; b--) {
bits.push((byteVal >> b) & 1);
}
}
return bits;
}
export function generateIdenticon(str: string, size: number) {
const bits = md5ToBits(str);
const gridSize = 5;
const halfCols = 4;
const cellSize = size / gridSize;
let bitIndex = 0;
const squares: JSX.Element[] = [];
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < halfCols; col++) {
const bit = bits[bitIndex % bits.length];
bitIndex++;
if (bit === 1) {
const xPos = col * cellSize;
const yPos = row * cellSize;
squares.push(
<rect
key={`${xPos}-${yPos}`}
x={xPos - 0.5}
y={yPos - 0.5}
width={cellSize + 1}
height={cellSize + 1}
fill="var(--background-neutral-inverted-02)"
stroke="var(--background-neutral-inverted-02)"
/>
);
const mirrorCol = gridSize - 1 - col;
if (mirrorCol !== col) {
const mirrorX = mirrorCol * cellSize;
squares.push(
<rect
key={`a-${mirrorX}-${yPos}`}
x={mirrorX - 0.5}
y={yPos - 0.5}
width={cellSize + 1}
height={cellSize + 1}
fill="var(--background-neutral-inverted-02)"
stroke="var(--background-neutral-inverted-02)"
/>
);
}
}
}
}
return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
style={{ display: "block" }}
>
{squares}
</svg>
);
}
export interface AgentIconProps {
agent: MinimalPersonaSnapshot;
size?: number;
}
export default function AgentIcon({ agent, size = 24 }: AgentIconProps) {
const settings = useSettingsContext();
// Check if whitelabeling is enabled for the default assistant
const shouldUseWhitelabelLogo =
agent.id === 0 && settings?.enterpriseSettings?.use_custom_logo === true;
return (
<SimpleTooltip tooltip={agent.description} side="top">
<div className="text-text-04">
{agent.id === -3 ? (
<ArtAsistantIcon size={size} />
) : agent.id === 0 ? (
shouldUseWhitelabelLogo ? (
<img
alt="Logo"
src="/api/enterprise-settings/logo"
loading="lazy"
className={cn(
"rounded-full object-cover object-center transition-opacity duration-300"
)}
width={size}
height={size}
style={{ objectFit: "contain" }}
/>
) : (
<OnyxIcon size={size} />
)
) : agent.uploaded_image_id ? (
<img
alt={agent.name}
src={buildImgUrl(agent.uploaded_image_id)}
loading="lazy"
className={cn(
"rounded-full object-cover object-center transition-opacity duration-300"
)}
width={size}
height={size}
/>
) : (
generateIdenticon((agent.icon_shape || 0).toString(), size)
)}
</div>
</SimpleTooltip>
);
}
@@ -0,0 +1,48 @@
"use client";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import { buildImgUrl } from "@/app/chat/components/files/images/utils";
import { OnyxIcon } from "@/components/icons/icons";
import { useSettingsContext } from "@/components/settings/SettingsProvider";
import { DEFAULT_ASSISTANT_ID } from "@/lib/constants";
import CustomAgentAvatar from "@/refresh-components/avatars/CustomAgentAvatar";
export interface AgentAvatarProps {
agent: MinimalPersonaSnapshot;
size?: number;
}
export default function AgentAvatar({ agent, ...props }: AgentAvatarProps) {
const settings = useSettingsContext();
if (agent.id === DEFAULT_ASSISTANT_ID) {
return settings.enterpriseSettings?.use_custom_logo ? (
<div
className="aspect-square rounded-full overflow-hidden"
style={{ height: props.size, width: props.size }}
>
<img
alt="Logo"
src="/api/enterprise-settings/logo"
loading="lazy"
className="h-full w-full object-contain object-center"
/>
</div>
) : (
<OnyxIcon size={props.size} className="" />
);
}
return (
<CustomAgentAvatar
name={agent.name}
src={
agent.uploaded_image_id
? buildImgUrl(agent.uploaded_image_id)
: undefined
}
iconName={agent.icon_name}
{...props}
/>
);
}
@@ -0,0 +1,108 @@
"use client";
import { cn } from "@/lib/utils";
import SvgCheck from "@/icons/check";
import SvgCode from "@/icons/code";
import SvgTwoLineSmall from "@/icons/two-line-small";
import SvgOnyxOctagon from "@/icons/onyx-octagon";
import SvgSearch from "@/icons/search";
import { IconProps } from "@/icons";
import Text from "@/refresh-components/texts/Text";
interface IconConfig {
Icon: React.FunctionComponent<IconProps>;
className?: string;
}
const iconMap: Record<string, IconConfig> = {
search: { Icon: SvgSearch, className: "stroke-green-500" },
check: { Icon: SvgCheck, className: "stroke-green-500" },
code: { Icon: SvgCode, className: "stroke-orange-500" },
};
interface SvgOctagonWrapperProps {
size: number;
children: React.ReactNode;
}
function SvgOctagonWrapper({ size, children }: SvgOctagonWrapperProps) {
return (
<div className="relative flex flex-col items-center justify-center">
<div className="absolute inset-0 flex items-center justify-center">
{children}
</div>
<SvgOnyxOctagon className="stroke-text-04" height={size} width={size} />
</div>
);
}
export interface CustomAgentAvatarProps {
name?: string;
src?: string;
iconName?: string;
size?: number;
}
export default function CustomAgentAvatar({
name,
src,
iconName,
size = 18,
}: CustomAgentAvatarProps) {
if (src) {
return (
<div
className="aspect-square rounded-full overflow-hidden"
style={{ height: size, width: size }}
>
<img
alt={name}
src={src}
loading="lazy"
className="h-full w-full object-cover object-center"
/>
</div>
);
}
const iconConfig = iconName && iconMap[iconName];
if (iconConfig) {
const { Icon, className } = iconConfig;
return (
<SvgOctagonWrapper size={size}>
<Icon
className={cn("stroke-text-04", className)}
style={{ width: size * 0.4, height: size * 0.4 }}
/>
</SvgOctagonWrapper>
);
}
// Display first letter of name if available, otherwise fall back to two-line-small icon
const trimmedName = name?.trim();
const firstLetter =
trimmedName && trimmedName.length > 0
? trimmedName[0]!.toUpperCase()
: undefined;
const validFirstLetter = !!firstLetter && /^[a-zA-Z]$/.test(firstLetter);
if (validFirstLetter) {
return (
<SvgOctagonWrapper size={size}>
<Text as="span" style={{ fontSize: size * 0.5 }}>
{firstLetter}
</Text>
</SvgOctagonWrapper>
);
}
return (
<SvgOctagonWrapper size={size}>
<SvgTwoLineSmall
className="stroke-text-04"
style={{ width: size * 0.8, height: size * 0.8 }}
/>
</SvgOctagonWrapper>
);
}
@@ -1,16 +1,8 @@
import { OnboardingStep } from "./types";
import type { ReactNode } from "react";
import SvgGlobe from "@/icons/globe";
import SvgImage from "@/icons/image";
import SvgUsers from "@/icons/users";
import { FinalStepItemProps } from "./types";
import { IconProps } from "@/icons";
import { AzureIcon, GeminiIcon } from "@/components/icons/icons";
import SvgClaude from "@/icons/claude";
import SvgAws from "@/icons/aws";
import SvgOllama from "@/icons/ollama";
import SvgOpenai from "@/icons/openai";
import SvgOpenrouter from "@/icons/openrouter";
import { LLMProviderName } from "@/app/admin/configuration/llm/interfaces";
import InlineExternalLink from "../InlineExternalLink";
@@ -90,19 +82,6 @@ export const FINAL_SETUP_CONFIG: FinalStepItemProps[] = [
},
];
export const PROVIDER_ICON_MAP: Record<
string,
React.FunctionComponent<IconProps>
> = {
[LLMProviderName.ANTHROPIC]: SvgClaude,
[LLMProviderName.BEDROCK]: SvgAws,
[LLMProviderName.AZURE]: AzureIcon,
[LLMProviderName.VERTEX_AI]: GeminiIcon,
[LLMProviderName.OPENAI]: SvgOpenai,
[LLMProviderName.OLLAMA_CHAT]: SvgOllama,
[LLMProviderName.OPENROUTER]: SvgOpenrouter,
};
export const MODAL_CONTENT_MAP: Record<string, any> = {
[LLMProviderName.OPENAI]: {
description: "Connect to OpenAI and set up your chatGPT models.",
@@ -7,13 +7,13 @@ import Separator from "@/refresh-components/Separator";
import LLMProvider from "../components/LLMProvider";
import { OnboardingActions, OnboardingState, OnboardingStep } from "../types";
import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { PROVIDER_ICON_MAP } from "../constants";
import LLMConnectionModal, {
LLMConnectionModalProps,
} from "@/refresh-components/onboarding/components/LLMConnectionModal";
import { cn } from "@/lib/utils";
import SvgCheckCircle from "@/icons/check-circle";
import { useCreateModal } from "@/refresh-components/contexts/ModalContext";
import { getProviderIcon } from "@/app/admin/configuration/llm/utils";
type LLMStepProps = {
state: OnboardingState;
@@ -51,8 +51,7 @@ const StackedProviderIcons = ({ providers }: StackedProviderIconsProps) => {
return (
<div className="flex items-center">
{providers.slice(0, 3).map((provider, index) => {
const IconComponent = PROVIDER_ICON_MAP[provider];
if (!IconComponent) return null;
const IconComponent = getProviderIcon(provider);
return (
<div
@@ -165,7 +164,7 @@ const LLMStepInner = ({
onboardingActions={onboardingActions}
title={llmDescriptor.title}
subtitle={llmDescriptor.display_name}
icon={PROVIDER_ICON_MAP[llmDescriptor.name]}
icon={getProviderIcon(llmDescriptor.name)}
llmDescriptor={llmDescriptor}
disabled={disabled}
isConnected={onboardingState.data.llmProviders?.some(
+2 -2
View File
@@ -8,12 +8,12 @@ import SvgPin from "@/icons/pin";
import { cn, noProp } from "@/lib/utils";
import SidebarTab from "@/refresh-components/buttons/SidebarTab";
import IconButton from "@/refresh-components/buttons/IconButton";
import { getAgentIcon } from "@/sections/sidebar/utils";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import SvgX from "@/icons/x";
import useAppFocus from "@/hooks/useAppFocus";
import useIsMounted from "@/hooks/useIsMounted";
import AgentAvatar from "@/refresh-components/avatars/AgentAvatar";
interface SortableItemProps {
id: number;
@@ -62,7 +62,7 @@ function AgentButtonInner({ agent }: AgentButtonProps) {
<div className="flex flex-col w-full h-full">
<SidebarTab
key={agent.id}
leftIcon={getAgentIcon(agent)}
leftIcon={() => <AgentAvatar agent={agent} />}
onClick={() => route({ agentId: agent.id })}
active={
typeof activeSidebarTab === "object" &&
-50
View File
@@ -1,50 +0,0 @@
"use client";
import React from "react";
import { IconProps } from "@/icons";
import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import {
ART_ASSISTANT_ID,
DEFAULT_ASSISTANT_ID,
GENERAL_ASSISTANT_ID,
IMAGE_ASSISTANT_ID,
} from "@/lib/constants";
import SvgLightbulbSimple from "@/icons/lightbulb-simple";
import { OnyxIcon } from "@/components/icons/icons";
import SvgImage from "@/icons/image";
import { generateIdenticon } from "@/refresh-components/AgentIcon";
import { buildImgUrl } from "@/app/chat/components/files/images/utils";
import { cn } from "@/lib/utils";
export function getAgentIcon(
agent: MinimalPersonaSnapshot
): React.FunctionComponent<IconProps> {
if (agent.id === DEFAULT_ASSISTANT_ID) return OnyxIcon;
if (agent.id === GENERAL_ASSISTANT_ID) return SvgLightbulbSimple;
if (agent.id === IMAGE_ASSISTANT_ID || agent.id === ART_ASSISTANT_ID)
return SvgImage;
const uploadedImageId = agent.uploaded_image_id;
if (uploadedImageId) {
const UploadedImageIcon: React.FunctionComponent<IconProps> = ({
className,
}) => (
<div className={cn("w-full h-full", className)}>
<img
alt={agent.name}
src={buildImgUrl(uploadedImageId)}
loading="lazy"
className="w-full h-full rounded-full object-cover object-center"
/>
</div>
);
UploadedImageIcon.displayName = "SidebarUploadedAgentIcon";
return UploadedImageIcon;
}
const GeneratedIcon: React.FunctionComponent<IconProps> = ({ className }) => (
<div className={cn("w-full h-full", className)}>
{generateIdenticon((agent.icon_shape || 0).toString(), 16)}
</div>
);
GeneratedIcon.displayName = "SidebarGeneratedAgentIcon";
return GeneratedIcon;
}