diff --git a/Dockerfile b/Dockerfile index 41243119e..46cabe6df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# v0.7.6 +# v0.7.7-rc1 # Base node image FROM node:20-alpine AS node diff --git a/Dockerfile.multi b/Dockerfile.multi index c5450026d..570fbecf3 100644 --- a/Dockerfile.multi +++ b/Dockerfile.multi @@ -1,5 +1,5 @@ # Dockerfile.multi -# v0.7.6 +# v0.7.7-rc1 # Base for all builds FROM node:20-alpine AS base-min diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index 9334f1c28..368e7d6e8 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -506,9 +506,8 @@ class OpenAIClient extends BaseClient { if (promptPrefix && this.isOmni === true) { const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user'); if (lastUserMessageIndex !== -1) { - payload[ - lastUserMessageIndex - ].content = `${promptPrefix}\n${payload[lastUserMessageIndex].content}`; + payload[lastUserMessageIndex].content = + `${promptPrefix}\n${payload[lastUserMessageIndex].content}`; } } @@ -1067,14 +1066,36 @@ ${convo} }); } - getStreamText() { + /** + * + * @param {string[]} [intermediateReply] + * @returns {string} + */ + getStreamText(intermediateReply) { if (!this.streamHandler) { - return ''; + return intermediateReply?.join('') ?? ''; + } + + let thinkMatch; + let remainingText; + let reasoningText = ''; + + if (this.streamHandler.reasoningTokens.length > 0) { + reasoningText = this.streamHandler.reasoningTokens.join(''); + thinkMatch = reasoningText.match(/([\s\S]*?)<\/think>/)?.[1]?.trim(); + if (thinkMatch != null && thinkMatch) { + const reasoningTokens = `:::thinking\n${thinkMatch}\n:::\n`; + remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || ''; + return `${reasoningTokens}${remainingText}${this.streamHandler.tokens.join('')}`; + } else if (thinkMatch === '') { + remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || ''; + return `${remainingText}${this.streamHandler.tokens.join('')}`; + } } const reasoningTokens = - this.streamHandler.reasoningTokens.length > 0 - ? `:::thinking\n${this.streamHandler.reasoningTokens.join('')}\n:::\n` + reasoningText.length > 0 + ? `:::thinking\n${reasoningText.replace('', '').replace('', '').trim()}\n:::\n` : ''; return `${reasoningTokens}${this.streamHandler.tokens.join('')}`; @@ -1314,11 +1335,19 @@ ${convo} streamPromise = new Promise((resolve) => { streamResolve = resolve; }); + /** @type {OpenAI.OpenAI.CompletionCreateParamsStreaming} */ + const params = { + ...modelOptions, + stream: true, + }; + if ( + this.options.endpoint === EModelEndpoint.openAI || + this.options.endpoint === EModelEndpoint.azureOpenAI + ) { + params.stream_options = { include_usage: true }; + } const stream = await openai.beta.chat.completions - .stream({ - ...modelOptions, - stream: true, - }) + .stream(params) .on('abort', () => { /* Do nothing here */ }) @@ -1449,7 +1478,7 @@ ${convo} this.options.context !== 'title' && message.content.startsWith('') ) { - return message.content.replace('', ':::thinking').replace('', ':::'); + return this.getStreamText(); } return message.content; @@ -1458,7 +1487,7 @@ ${convo} err?.message?.includes('abort') || (err instanceof OpenAI.APIError && err?.message?.includes('abort')) ) { - return intermediateReply.join(''); + return this.getStreamText(intermediateReply); } if ( err?.message?.includes( @@ -1473,14 +1502,18 @@ ${convo} (err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason')) ) { logger.error('[OpenAIClient] Known OpenAI error:', err); - if (intermediateReply.length > 0) { - return intermediateReply.join(''); + if (this.streamHandler && this.streamHandler.reasoningTokens.length) { + return this.getStreamText(); + } else if (intermediateReply.length > 0) { + return this.getStreamText(intermediateReply); } else { throw err; } } else if (err instanceof OpenAI.APIError) { - if (intermediateReply.length > 0) { - return intermediateReply.join(''); + if (this.streamHandler && this.streamHandler.reasoningTokens.length) { + return this.getStreamText(); + } else if (intermediateReply.length > 0) { + return this.getStreamText(intermediateReply); } else { throw err; } diff --git a/api/models/schema/agent.js b/api/models/schema/agent.js index 2006859ab..53e49e1cf 100644 --- a/api/models/schema/agent.js +++ b/api/models/schema/agent.js @@ -35,6 +35,9 @@ const agentSchema = mongoose.Schema( model_parameters: { type: Object, }, + artifacts: { + type: String, + }, access_level: { type: Number, }, diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js index 15a310030..3e03a4512 100644 --- a/api/server/services/Endpoints/agents/initialize.js +++ b/api/server/services/Endpoints/agents/initialize.js @@ -13,6 +13,7 @@ const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options') const initOpenAI = require('~/server/services/Endpoints/openAI/initialize'); const initCustom = require('~/server/services/Endpoints/custom/initialize'); const initGoogle = require('~/server/services/Endpoints/google/initialize'); +const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts'); const { getCustomEndpointConfig } = require('~/server/services/Config'); const { loadAgentTools } = require('~/server/services/ToolService'); const AgentClient = require('~/server/controllers/agents/client'); @@ -72,6 +73,16 @@ const primeResources = async (_attachments, _tool_resources) => { } }; +/** + * @param {object} params + * @param {ServerRequest} params.req + * @param {ServerResponse} params.res + * @param {Agent} params.agent + * @param {object} [params.endpointOption] + * @param {AgentToolResources} [params.tool_resources] + * @param {boolean} [params.isInitialAgent] + * @returns {Promise} + */ const initializeAgentOptions = async ({ req, res, @@ -132,6 +143,13 @@ const initializeAgentOptions = async ({ agent.model_parameters.model = agent.model; } + if (typeof agent.artifacts === 'string' && agent.artifacts !== '') { + agent.additional_instructions = generateArtifactsPrompt({ + endpoint: agent.provider, + artifacts: agent.artifacts, + }); + } + const tokensModel = agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model; diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index e46584c80..8c681d8f4 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -200,6 +200,7 @@ function generateConfig(key, baseURL, endpoint) { config.capabilities = [ AgentCapabilities.execute_code, AgentCapabilities.file_search, + AgentCapabilities.artifacts, AgentCapabilities.actions, AgentCapabilities.tools, ]; diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts index 7f64f0788..a9c24106b 100644 --- a/client/src/common/agents-types.ts +++ b/client/src/common/agents-types.ts @@ -1,4 +1,4 @@ -import { AgentCapabilities } from 'librechat-data-provider'; +import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider'; import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider'; import type { OptionWithIcon, ExtendedFile } from './types'; @@ -9,8 +9,8 @@ export type TAgentOption = OptionWithIcon & }; export type TAgentCapabilities = { - [AgentCapabilities.execute_code]: boolean; [AgentCapabilities.file_search]: boolean; + [AgentCapabilities.execute_code]: boolean; [AgentCapabilities.end_after_tools]?: boolean; [AgentCapabilities.hide_sequential_outputs]?: boolean; }; @@ -26,4 +26,5 @@ export type AgentForm = { tools?: string[]; provider?: AgentProvider | OptionWithIcon; agent_ids?: string[]; + [AgentCapabilities.artifacts]?: ArtifactModes | string; } & TAgentCapabilities; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 151314faa..3d61eccb1 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -68,8 +68,8 @@ export type GenericSetter = (value: T | ((currentValue: T) => T)) => void; export type LastSelectedModels = Record; export type LocalizeFunction = ( - phraseKey: TranslationKeys, - options?: Record + phraseKey: TranslationKeys, + options?: Record, ) => string; export type ChatFormValues = { text: string }; @@ -89,6 +89,7 @@ export type IconMapProps = { iconURL?: string; context?: 'landing' | 'menu-item' | 'nav' | 'message'; endpoint?: string | null; + endpointType?: string; assistantName?: string; agentName?: string; avatar?: string; diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index 0ee64ef62..dbf39ee84 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -28,7 +28,7 @@ function ChatView({ index = 0 }: { index?: number }) { select: useCallback( (data: TMessage[]) => { const dataTree = buildTree({ messages: data, fileMap }); - return dataTree?.length === 0 ? null : dataTree ?? null; + return dataTree?.length === 0 ? null : (dataTree ?? null); }, [fileMap], ), @@ -62,7 +62,7 @@ function ChatView({ index = 0 }: { index?: number }) { - + {content}
diff --git a/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx b/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx index d062f4276..f0918240b 100644 --- a/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx +++ b/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx @@ -7,6 +7,9 @@ import FinishedIcon from './FinishedIcon'; import MarkdownLite from './MarkdownLite'; import store from '~/store'; +const radius = 56.08695652173913; +const circumference = 2 * Math.PI * radius; + export default function CodeAnalyze({ initialProgress = 0.1, code, @@ -22,9 +25,6 @@ export default function CodeAnalyze({ const progress = useProgress(initialProgress); const showAnalysisCode = useRecoilValue(store.showCode); const [showCode, setShowCode] = useState(showAnalysisCode); - - const radius = 56.08695652173913; - const circumference = 2 * Math.PI * radius; const offset = circumference - progress * circumference; const logs = outputs.reduce((acc, output) => { @@ -53,9 +53,10 @@ export default function CodeAnalyze({ setShowCode((prev) => !prev)} - inProgressText="Analyzing" - finishedText="Finished analyzing" + inProgressText={localize('com_ui_analyzing')} + finishedText={localize('com_ui_analyzing_finished')} hasInput={!!code.length} + isExpanded={showCode} />
{showCode && ( diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx index ce77e3bfd..b997060c6 100644 --- a/client/src/components/Chat/Messages/Content/ContentParts.tsx +++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx @@ -50,11 +50,24 @@ const ContentParts = memo( [attachments, messageAttachmentsMap, messageId], ); - const hasReasoningParts = useMemo( - () => content?.some((part) => part?.type === ContentTypes.THINK && part.think) ?? false, - [content], - ); + const hasReasoningParts = useMemo(() => { + const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false; + const allThinkPartsHaveContent = + content?.every((part) => { + if (part?.type !== ContentTypes.THINK) { + return true; + } + if (typeof part.think === 'string') { + const cleanedContent = part.think.replace(/<\/?think>/g, '').trim(); + return cleanedContent.length > 0; + } + + return false; + }) ?? false; + + return hasThinkPart && allThinkPartsHaveContent; + }, [content]); if (!content) { return null; } diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx index 21bbe231e..1547a01d8 100644 --- a/client/src/components/Chat/Messages/Content/MessageContent.tsx +++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx @@ -159,7 +159,9 @@ const MessageContent = ({ return ( <> - {thinkingContent && {thinkingContent}} + {thinkingContent.length > 0 && ( + {thinkingContent} + )} setShowCode((prev) => !prev)} - inProgressText="Analyzing" - finishedText="Finished analyzing" + inProgressText={localize('com_ui_analyzing')} + finishedText={localize('com_ui_analyzing_finished')} hasInput={!!code.length} + isExpanded={showCode} /> {showCode && ( @@ -105,9 +107,7 @@ export default function ExecuteCode({ )} )} - {attachments?.map((attachment, index) => ( - - ))} + {attachments?.map((attachment, index) => )} ); } diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx index 447bf2f2c..fd84b0361 100644 --- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx +++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx @@ -11,9 +11,16 @@ type ReasoningProps = { const Reasoning = memo(({ reasoning }: ReasoningProps) => { const { isExpanded, nextType } = useMessageContext(); const reasoningText = useMemo(() => { - return reasoning.replace(/^\s*/, '').replace(/\s*<\/think>$/, ''); + return reasoning + .replace(/^\s*/, '') + .replace(/\s*<\/think>$/, '') + .trim(); }, [reasoning]); + if (!reasoningText) { + return null; + } + return (
void; @@ -50,8 +51,9 @@ export default function ProgressText({ authText?: string; hasInput?: boolean; popover?: boolean; + isExpanded?: boolean; }) { - const text = progress < 1 ? authText ?? inProgressText : finishedText; + const text = progress < 1 ? (authText ?? inProgressText) : finishedText; return (
); - if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) { - return ( - - 0 ? ( - - - - ) : null - } - > -
- {children} -
-
-
- ); - } - return ( - {layout()} - {panel != null && panel} + 0 ? ( + + + + ) : null + } + > +
+ {children} +
+
); } diff --git a/client/src/components/Input/ModelSelect/TemporaryChat.tsx b/client/src/components/Input/ModelSelect/TemporaryChat.tsx index ad403549b..3b0cae491 100644 --- a/client/src/components/Input/ModelSelect/TemporaryChat.tsx +++ b/client/src/components/Input/ModelSelect/TemporaryChat.tsx @@ -40,13 +40,15 @@ export const TemporaryChat = () => { }; return ( -
-
-
+
+
+
-
+
agentsConfig?.capabilities.includes(AgentCapabilities.actions), [agentsConfig], ); + const artifactsEnabled = useMemo( + () => agentsConfig?.capabilities.includes(AgentCapabilities.artifacts) ?? false, + [agentsConfig], + ); const fileSearchEnabled = useMemo( () => agentsConfig?.capabilities.includes(AgentCapabilities.file_search) ?? false, [agentsConfig], @@ -150,7 +155,7 @@ export default function AgentConfig({ onSuccess: (data) => { setCurrentAgentId(data.id); showToast({ - message: `${localize('com_assistants_create_success ')} ${ + message: `${localize('com_assistants_create_success')} ${ data.name ?? localize('com_ui_agent') }`, }); @@ -178,18 +183,10 @@ export default function AgentConfig({ }, [agent_id, setActivePanel, showToast, localize]); const providerValue = typeof provider === 'string' ? provider : provider?.value; + let Icon: IconComponentTypes | null | undefined; let endpointType: EModelEndpoint | undefined; let endpointIconURL: string | undefined; let iconKey: string | undefined; - let Icon: - | React.ComponentType< - React.SVGProps & { - endpoint: string; - endpointType: EModelEndpoint | undefined; - iconURL: string | undefined; - } - > - | undefined; if (providerValue !== undefined) { endpointType = getEndpointField(endpointsConfig, providerValue as string, 'type'); @@ -337,7 +334,7 @@ export default function AgentConfig({
- {(codeEnabled || fileSearchEnabled) && ( + {(codeEnabled || fileSearchEnabled || artifactsEnabled) && (
)} {/* Agent Tools & Actions */} diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx index 0f49dc30e..cea3265eb 100644 --- a/client/src/components/SidePanel/Agents/AgentPanel.tsx +++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx @@ -120,6 +120,7 @@ export default function AgentPanel({ const { name, + artifacts, description, instructions, model: _model, @@ -139,6 +140,7 @@ export default function AgentPanel({ agent_id, data: { name, + artifacts, description, instructions, model, @@ -162,6 +164,7 @@ export default function AgentPanel({ create.mutate({ name, + artifacts, description, instructions, model, @@ -184,7 +187,7 @@ export default function AgentPanel({ const canEditAgent = useMemo(() => { const canEdit = - agentQuery.data?.isCollaborative ?? false + (agentQuery.data?.isCollaborative ?? false) ? true : agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN; diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx index 5d406c48b..caeb0457e 100644 --- a/client/src/components/SidePanel/Agents/AgentSelect.tsx +++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx @@ -55,8 +55,8 @@ export default function AgentSelect({ }; const capabilities: TAgentCapabilities = { - [AgentCapabilities.execute_code]: false, [AgentCapabilities.file_search]: false, + [AgentCapabilities.execute_code]: false, [AgentCapabilities.end_after_tools]: false, [AgentCapabilities.hide_sequential_outputs]: false, }; diff --git a/client/src/components/SidePanel/Agents/Artifacts.tsx b/client/src/components/SidePanel/Agents/Artifacts.tsx new file mode 100644 index 000000000..2a814cc7f --- /dev/null +++ b/client/src/components/SidePanel/Agents/Artifacts.tsx @@ -0,0 +1,124 @@ +import { useFormContext } from 'react-hook-form'; +import { ArtifactModes, AgentCapabilities } from 'librechat-data-provider'; +import type { AgentForm } from '~/common'; +import { + Switch, + HoverCard, + HoverCardPortal, + HoverCardContent, + HoverCardTrigger, +} from '~/components/ui'; +import { useLocalize } from '~/hooks'; +import { CircleHelpIcon } from '~/components/svg'; +import { ESide } from '~/common'; + +export default function Artifacts() { + const localize = useLocalize(); + const methods = useFormContext(); + const { setValue, watch } = methods; + + const artifactsMode = watch(AgentCapabilities.artifacts); + + const handleArtifactsChange = (value: boolean) => { + setValue(AgentCapabilities.artifacts, value ? ArtifactModes.DEFAULT : '', { + shouldDirty: true, + }); + }; + + const handleShadcnuiChange = (value: boolean) => { + setValue(AgentCapabilities.artifacts, value ? ArtifactModes.SHADCNUI : ArtifactModes.DEFAULT, { + shouldDirty: true, + }); + }; + + const handleCustomModeChange = (value: boolean) => { + setValue(AgentCapabilities.artifacts, value ? ArtifactModes.CUSTOM : ArtifactModes.DEFAULT, { + shouldDirty: true, + }); + }; + + const isEnabled = artifactsMode !== undefined && artifactsMode !== ''; + const isCustomEnabled = artifactsMode === ArtifactModes.CUSTOM; + const isShadcnEnabled = artifactsMode === ArtifactModes.SHADCNUI; + + return ( +
+
+ + + +
+
+ + + +
+
+ ); +} + +function SwitchItem({ + id, + label, + checked, + onCheckedChange, + hoverCardText, + disabled = false, +}: { + id: string; + label: string; + checked: boolean; + onCheckedChange: (value: boolean) => void; + hoverCardText: string; + disabled?: boolean; +}) { + return ( + +
+
+
{label}
+ + + +
+ + +
+

{hoverCardText}

+
+
+
+ +
+
+ ); +} diff --git a/client/src/components/SidePanel/Agents/Code/Action.tsx b/client/src/components/SidePanel/Agents/Code/Action.tsx index a2a16d441..e655101b7 100644 --- a/client/src/components/SidePanel/Agents/Code/Action.tsx +++ b/client/src/components/SidePanel/Agents/Code/Action.tsx @@ -86,7 +86,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) { )} - +
diff --git a/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx b/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx index 5d827dd81..f006d9769 100644 --- a/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx +++ b/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx @@ -29,7 +29,7 @@ export default function FileSearchCheckbox() { {...field} checked={field.value} onCheckedChange={field.onChange} - className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer" + className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer" value={field.value.toString()} /> )} @@ -38,7 +38,6 @@ export default function FileSearchCheckbox() { type="button" className="flex items-center space-x-2" onClick={() => - setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), { shouldDirty: true, }) @@ -51,7 +50,7 @@ export default function FileSearchCheckbox() { {localize('com_agents_enable_file_search')} - + diff --git a/client/src/components/SidePanel/SidePanel.tsx b/client/src/components/SidePanel/SidePanel.tsx index 51977ab15..7660e8b27 100644 --- a/client/src/components/SidePanel/SidePanel.tsx +++ b/client/src/components/SidePanel/SidePanel.tsx @@ -1,78 +1,58 @@ -import throttle from 'lodash/throttle'; -import { getConfigDefaults } from 'librechat-data-provider'; +import { useState, useCallback, useMemo, memo } from 'react'; import { useUserKeyQuery } from 'librechat-data-provider/react-query'; -import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react'; import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider'; import type { ImperativePanelHandle } from 'react-resizable-panels'; -import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable'; -import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider'; +import { ResizableHandleAlt, ResizablePanel } from '~/components/ui/Resizable'; import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks'; import useSideNavLinks from '~/hooks/Nav/useSideNavLinks'; +import { useGetEndpointsQuery } from '~/data-provider'; import NavToggle from '~/components/Nav/NavToggle'; import { cn, getEndpointField } from '~/utils'; import { useChatContext } from '~/Providers'; import Switcher from './Switcher'; import Nav from './Nav'; -interface SidePanelProps { - defaultLayout?: number[] | undefined; - defaultCollapsed?: boolean; - navCollapsedSize?: number; - fullPanelCollapse?: boolean; - artifacts?: React.ReactNode; - children: React.ReactNode; -} - const defaultMinSize = 20; -const defaultInterface = getConfigDefaults().interface; - -const normalizeLayout = (layout: number[]) => { - const sum = layout.reduce((acc, size) => acc + size, 0); - if (Math.abs(sum - 100) < 0.01) { - return layout.map((size) => Number(size.toFixed(2))); - } - - const factor = 100 / sum; - const normalizedLayout = layout.map((size) => Number((size * factor).toFixed(2))); - - const adjustedSum = normalizedLayout.reduce( - (acc, size, index) => (index === layout.length - 1 ? acc : acc + size), - 0, - ); - normalizedLayout[normalizedLayout.length - 1] = Number((100 - adjustedSum).toFixed(2)); - - return normalizedLayout; -}; const SidePanel = ({ - defaultLayout = [97, 3], - defaultCollapsed = false, - fullPanelCollapse = false, + defaultSize, + panelRef, navCollapsedSize = 3, - artifacts, - children, -}: SidePanelProps) => { + hasArtifacts, + minSize, + setMinSize, + collapsedSize, + setCollapsedSize, + isCollapsed, + setIsCollapsed, + fullCollapse, + setFullCollapse, + interfaceConfig, +}: { + defaultSize?: number; + hasArtifacts: boolean; + navCollapsedSize?: number; + minSize: number; + setMinSize: React.Dispatch>; + collapsedSize: number; + setCollapsedSize: React.Dispatch>; + isCollapsed: boolean; + setIsCollapsed: React.Dispatch>; + fullCollapse: boolean; + setFullCollapse: React.Dispatch>; + panelRef: React.RefObject; + interfaceConfig: TInterfaceConfig; +}) => { const localize = useLocalize(); const [isHovering, setIsHovering] = useState(false); - const [minSize, setMinSize] = useState(defaultMinSize); const [newUser, setNewUser] = useLocalStorage('newUser', true); - const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed); - const [fullCollapse, setFullCollapse] = useState(fullPanelCollapse); - const [collapsedSize, setCollapsedSize] = useState(navCollapsedSize); const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); - const { data: startupConfig } = useGetStartupConfig(); - const interfaceConfig = useMemo( - () => (startupConfig?.interface ?? defaultInterface) as Partial, - [startupConfig], - ); const isSmallScreen = useMediaQuery('(max-width: 767px)'); const { conversation } = useChatContext(); const { endpoint } = conversation ?? {}; const { data: keyExpiry = { expiresAt: undefined } } = useUserKeyQuery(endpoint ?? ''); - const panelRef = useRef(null); - const defaultActive = useMemo(() => { const activePanel = localStorage.getItem('side:active-panel'); return typeof activePanel === 'string' ? activePanel : undefined; @@ -113,46 +93,6 @@ const SidePanel = ({ interfaceConfig, }); - const calculateLayout = useCallback(() => { - if (artifacts == null) { - const navSize = defaultLayout.length === 2 ? defaultLayout[1] : defaultLayout[2]; - return [100 - navSize, navSize]; - } else { - const navSize = 0; - const remainingSpace = 100 - navSize; - const newMainSize = Math.floor(remainingSpace / 2); - const artifactsSize = remainingSpace - newMainSize; - return [newMainSize, artifactsSize, navSize]; - } - }, [artifacts, defaultLayout]); - - const currentLayout = useMemo(() => normalizeLayout(calculateLayout()), [calculateLayout]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const throttledSaveLayout = useCallback( - throttle((sizes: number[]) => { - const normalizedSizes = normalizeLayout(sizes); - localStorage.setItem('react-resizable-panels:layout', JSON.stringify(normalizedSizes)); - }, 350), - [], - ); - - useEffect(() => { - if (isSmallScreen) { - setIsCollapsed(true); - setCollapsedSize(0); - setMinSize(defaultMinSize); - setFullCollapse(true); - localStorage.setItem('fullPanelCollapse', 'true'); - panelRef.current?.collapse(); - return; - } else { - setIsCollapsed(defaultCollapsed); - setCollapsedSize(navCollapsedSize); - setMinSize(defaultMinSize); - } - }, [isSmallScreen, defaultCollapsed, navCollapsedSize, fullPanelCollapse]); - const toggleNavVisible = useCallback(() => { if (newUser) { setNewUser(false); @@ -173,127 +113,84 @@ const SidePanel = ({ } }, [isCollapsed, newUser, setNewUser, navCollapsedSize]); - const minSizeMain = useMemo(() => (artifacts != null ? 15 : 30), [artifacts]); - return ( <> - throttledSaveLayout(sizes)} - className="transition-width relative h-full w-full flex-1 overflow-auto bg-presentation" +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + className="relative flex w-px items-center justify-center" > - - {children} - - {artifacts != null && ( - <> - - - {artifacts} - - - )} -
setIsHovering(true)} - onMouseLeave={() => setIsHovering(false)} - className="relative flex w-px items-center justify-center" - > - -
- {(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && ( - - )} - { - setIsCollapsed(false); - localStorage.setItem('react-resizable-panels:collapsed', 'false'); - }} - onCollapse={() => { - setIsCollapsed(true); - localStorage.setItem('react-resizable-panels:collapsed', 'true'); - }} + - {interfaceConfig.modelSelect === true && ( -
- -
- )} -
+ {(!isCollapsed || minSize > 0) && !isSmallScreen && !fullCollapse && ( + + )} + + onExpand={() => { + setIsCollapsed(false); + localStorage.setItem('react-resizable-panels:collapsed', 'false'); + }} + onCollapse={() => { + setIsCollapsed(true); + localStorage.setItem('react-resizable-panels:collapsed', 'true'); + }} + className={cn( + 'sidenav hide-scrollbar border-l border-border-light bg-background transition-opacity', + isCollapsed ? 'min-w-[50px]' : 'min-w-[340px] sm:min-w-[352px]', + (isSmallScreen && isCollapsed && (minSize === 0 || collapsedSize === 0)) || fullCollapse + ? 'hidden min-w-0' + : 'opacity-100', + )} + > + {interfaceConfig.modelSelect === true && ( +
+ +
+ )} +