mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-22 03:14:06 -05:00
refactor: Refresh "Agent Icon" to the new "Agent Avatar" standard (#6509)
This commit is contained in:
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")
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
@@ -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]]
|
||||
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user