From 2b01d513073a8c4d950b0ca4c75ae35c99c8ce74 Mon Sep 17 00:00:00 2001 From: djohnson Date: Tue, 12 Nov 2024 18:13:07 +0900 Subject: [PATCH] Refactor: update settings ui with react router --- src/components/BreadcrumbBar.tsx | 53 ++-- src/components/ChatBoxInputRow.tsx | 9 +- src/components/ChatBoxPrompt.tsx | 21 +- src/components/HostInput.tsx | 22 +- src/components/ModelSelector.tsx | 8 +- src/components/NavButton.tsx | 46 +++ src/components/Navbar.tsx | 10 +- src/components/OmniBar.tsx | 13 +- src/components/SelectionTablePanel.tsx | 2 +- src/components/form/FormInput.tsx | 5 +- src/components/listItem/BaseListItem.tsx | 77 +++++ src/containers/Drawer.tsx | 129 ++++++++ src/containers/SettingSection.tsx | 134 ++++++++ .../viewModels/OllamaConnectionViewModel.tsx | 5 + src/core/setting/SettingStore.ts | 19 -- .../settings/containers/SettingsModal.tsx | 143 ++++----- src/features/settings/panels/HelpPanel.tsx | 2 +- .../settings/panels/MobileSplashPanel.tsx | 9 +- src/features/settings/panels/PersonaPanel.tsx | 2 +- .../panels/connections/ConnectionPanel.tsx | 190 +++++++---- .../ConnectionParameterSection.tsx | 294 ++++++++---------- .../panels/connections/ConnectionsPanel.tsx | 80 ----- .../settings/panels/general/GeneralPanel.tsx | 2 +- .../settings/panels/model/ModelPanel.tsx | 125 ++++---- .../panels/model/NotConnectedPanelSection.tsx | 12 +- .../panels/model/OllamaModelPanel.tsx | 260 +++++++--------- src/features/settings/settingRoutes.tsx | 49 +++ src/features/settings/settingsPanels.tsx | 29 -- src/main.tsx | 17 +- 29 files changed, 1045 insertions(+), 722 deletions(-) create mode 100644 src/components/NavButton.tsx create mode 100644 src/components/listItem/BaseListItem.tsx create mode 100644 src/containers/Drawer.tsx create mode 100644 src/containers/SettingSection.tsx delete mode 100644 src/features/settings/panels/connections/ConnectionsPanel.tsx create mode 100644 src/features/settings/settingRoutes.tsx delete mode 100644 src/features/settings/settingsPanels.tsx diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx index f5b8d77..3eef9b9 100644 --- a/src/components/BreadcrumbBar.tsx +++ b/src/components/BreadcrumbBar.tsx @@ -1,34 +1,39 @@ +import { useNavigate } from 'react-router-dom' import { observer } from 'mobx-react-lite' import { Breadcrumbs, BreadcrumbItem } from '@nextui-org/react' import _ from 'lodash' export type BreadcrumbType = { - isSelected: boolean label: string - onClick: () => void + path: string } -const BreadcrumbBar = observer( - ({ breadcrumbs }: { breadcrumbs: Array }) => { - return ( - - {_.compact(breadcrumbs).map(breadcrumb => ( - *]:!text-primary' - : 'scale-90 [&>*]:!text-base-content/70' - } - isCurrent={breadcrumb.isSelected} - onPress={breadcrumb.onClick} - key={breadcrumb.label} - > - {breadcrumb.label} - - ))} - - ) - }, -) +type BreadcrumbBarProps = { + breadcrumbs: BreadcrumbType[] +} + +// Gets a list of crumbs and paths and determines the selected one (might be overkill and maybe I should just use the last index) +const BreadcrumbBar = observer(({ breadcrumbs }: BreadcrumbBarProps) => { + const navigate = useNavigate() + + return ( + + {breadcrumbs.map((breadcrumb, index) => ( + *]:!text-primary' + : 'underline decoration-base-content/70 [&>*]:!text-base-content/70' + } + isCurrent={index === breadcrumbs.length - 1} + key={breadcrumb.label} + onClick={() => navigate(breadcrumb.path)} + > + {breadcrumb.label} + + ))} + + ) +}) export default BreadcrumbBar diff --git a/src/components/ChatBoxInputRow.tsx b/src/components/ChatBoxInputRow.tsx index 5e23819..c838e4d 100644 --- a/src/components/ChatBoxInputRow.tsx +++ b/src/components/ChatBoxInputRow.tsx @@ -4,13 +4,13 @@ import { observer } from 'mobx-react-lite' import TextareaAutosize from 'react-textarea-autosize' import { ChatViewModel } from '~/core/chat/ChatViewModel' -import { settingStore } from '~/core/setting/SettingStore' import { personaStore } from '~/core/persona/PersonaStore' import { connectionStore } from '~/core/connection/ConnectionStore' import { incomingMessageStore } from '~/core/IncomingMessageStore' import AttachmentWrapper from '~/components/AttachmentWrapper' import CachedImage from '~/components/CachedImage' +import { NavButton } from '~/components/NavButton' import { TransferHandler } from '~/utils/transfer/TransferHandler' @@ -198,16 +198,15 @@ const ChatBoxInputRow = observer(({ chat, onSend, children }: ChatBoxInputRowPro onSubmit={onFormSubmit} >
- +
diff --git a/src/components/ChatBoxPrompt.tsx b/src/components/ChatBoxPrompt.tsx index cb11bd9..b421791 100644 --- a/src/components/ChatBoxPrompt.tsx +++ b/src/components/ChatBoxPrompt.tsx @@ -4,10 +4,11 @@ import { observer } from 'mobx-react-lite' import AttachmentWrapper from '~/components/AttachmentWrapper' import FunTitle from '~/components/FunTitle' +import { NavButtonDiv } from '~/components/NavButton' + import { ChatViewModel } from '~/core/chat/ChatViewModel' import { personaTable } from '~/core/persona/PersonaTable' import { connectionStore } from '~/core/connection/ConnectionStore' -import { settingStore } from '~/core/setting/SettingStore' type StepProps = { isCompleted?: boolean; type?: 'primary' | 'secondary'; inCompleteIcon?: string } @@ -71,16 +72,16 @@ const ChatBoxPrompt = observer(({ chat }: { chat: ChatViewModel }) => {
-
    +
      {'Tell LM Studio, Ollama, AUTOMATIC1111, or Open AI that '} we're cool: - + @@ -107,12 +108,12 @@ const ChatBoxPrompt = observer(({ chat }: { chat: ChatViewModel }) => { {'Create and Select a'} - + {'to give your bot some pizzaz'} diff --git a/src/components/HostInput.tsx b/src/components/HostInput.tsx index 0a0cd76..1404889 100644 --- a/src/components/HostInput.tsx +++ b/src/components/HostInput.tsx @@ -5,7 +5,8 @@ import Question from '~/icons/Question' import Refresh from '~/icons/Refresh' import FormInput from '~/components/form/FormInput' -import { settingStore } from '~/core/setting/SettingStore' +import { NavButtonDiv } from '~/components/NavButton' + import { ConnectionViewModelTypes } from '~/core/connection/viewModels' import { ConnectionModel } from '~/core/connection/ConnectionModel' @@ -27,13 +28,9 @@ const HostInput = observer(({ connection, isEnabled }: HostInputProps) => { const modelsFoundLabel = isDirty ? ( 'Save to see model length' ) : ( - + ) return ( @@ -50,16 +47,15 @@ const HostInput = observer(({ connection, isEnabled }: HostInputProps) => { See connection instructions here: - + - {modelsFoundLabel} + {modelsFoundLabel} } endContent={ diff --git a/src/components/ModelSelector.tsx b/src/components/ModelSelector.tsx index 07a7e44..3a65163 100644 --- a/src/components/ModelSelector.tsx +++ b/src/components/ModelSelector.tsx @@ -1,12 +1,14 @@ import { useMemo } from 'react' import { observer } from 'mobx-react-lite' import _ from 'lodash' +import { useNavigate } from 'react-router-dom' import ChevronDown from '~/icons/ChevronDown' -import { settingStore } from '~/core/setting/SettingStore' import { connectionStore } from '~/core/connection/ConnectionStore' const ModelSelector = observer(() => { + const navigate = useNavigate() + const { selectedModelLabel, isAnyServerConnected, selectedConnection } = connectionStore const noServer = !isAnyServerConnected @@ -28,9 +30,9 @@ const ModelSelector = observer(() => { const handleClick = () => { if (!selectedConnection) { - settingStore.openSettingsModal('connections') + navigate('/models') } else { - settingStore.openSettingsModal('models') + navigate('/models/' + selectedConnection.id) } } diff --git a/src/components/NavButton.tsx b/src/components/NavButton.tsx new file mode 100644 index 0000000..a83a41c --- /dev/null +++ b/src/components/NavButton.tsx @@ -0,0 +1,46 @@ +import { MouseEventHandler } from 'react' +import { To, useNavigate } from 'react-router-dom' + +type NavButtonProps = React.HTMLAttributes & { + to: To + replace?: boolean + disabled?: boolean +} + +export const NavButton = ({ + to, + replace = false, + onClick, + ...rest +}: NavButtonProps) => { + const navigate = useNavigate() + + const handleClick: MouseEventHandler = e => { + onClick?.(e) + + if (!e.isDefaultPrevented()) { + navigate(to, { replace }) + } + } + + return + ) }, ) }, ) -const MobileSettingsSidePanel = observer( - ({ selectedPanel }: { selectedPanel?: SettingPanelOptionsType }) => { - const containerRef = useRef(null) - - const handleSectionClick = (panelName: SettingPanelOptionsType) => { - settingStore.openSettingsModal(panelName) - - containerRef.current?.removeAttribute('open') - } - - if (!selectedPanel || !settingsPanelByName[selectedPanel]) return null +const MobileSettingsSidePanel = observer(() => { + const containerRef = useRef(null) - return ( -
      - - Go to section - + const handleSectionClick = () => { + containerRef.current?.removeAttribute('open') + } -
        - -
      -
      - ) - }, -) + return ( +
      + + Go to section + + +
        + +
      +
      + ) +}) const SettingsModal = observer(() => { + const { pathname } = useLocation() + const navigate = useNavigate() + const modalRef = useRef(null) const isMobile = useMedia('(max-width: 1024px)') - let panelName = settingStore.settingPanelName - if (panelName === 'initial' && !isMobile) { - panelName = 'general' - } - - const { subtitle, Component } = useMemo>(() => { - if (!panelName) return {} - - return settingsPanelByName[panelName] - }, [panelName, isMobile]) - - const isOpen = !!Component - - const handleClose = () => { - settingStore.closeSettingsModal() - } + const isOpen = pathname !== '/' - const shouldShowBackButton = panelName !== 'initial' && isMobile + const shouldShowBackButton = pathname !== '/initial' && isMobile useEffect(() => { if (isOpen) { @@ -109,11 +82,18 @@ const SettingsModal = observer(() => { } }, [isOpen]) + // if the panel name was changed outside of the router scope, we need to reset it + useEffect(() => { + if (pathname === '/initial' && !isMobile) { + navigate('general', { replace: true }) + } + }, [pathname, isMobile]) + return ( navigate('/')} size={isMobile ? 'full' : undefined} classNames={{ base: isMobile ? '' : '!container', @@ -133,23 +113,21 @@ const SettingsModal = observer(() => { 'btn btn-circle btn-ghost btn-sm !text-lg opacity-70 ' + (shouldShowBackButton ? '' : ' pointer-events-none !opacity-0') // hack to hide the button but keep spacing } - onClick={() => settingStore.openSettingsModal('initial')} + onClick={() => navigate(-1)} >
- Settings{subtitle && `: ${subtitle}`} + Settings + {/* {subtitle && `: ${subtitle}`} */}
- +
@@ -161,11 +139,7 @@ const SettingsModal = observer(() => { role="complementary" >
- {isMobile ? ( - - ) : ( - - )} + {isMobile ? : } {
- {Component && } + + {_.map(settingRoutesByName, ({ Component, label, ...rest }, key) => ( + + + + } + {...rest} + /> + ))} +
diff --git a/src/features/settings/panels/HelpPanel.tsx b/src/features/settings/panels/HelpPanel.tsx index 2086523..87e4c21 100644 --- a/src/features/settings/panels/HelpPanel.tsx +++ b/src/features/settings/panels/HelpPanel.tsx @@ -15,7 +15,7 @@ const LMS_CODE = 'lms server start --cors=true' const HelpPanel = observer(() => { return ( - +
{__TARGET__ === 'chrome' && ( <> diff --git a/src/features/settings/panels/MobileSplashPanel.tsx b/src/features/settings/panels/MobileSplashPanel.tsx index 9f0484c..a69986e 100644 --- a/src/features/settings/panels/MobileSplashPanel.tsx +++ b/src/features/settings/panels/MobileSplashPanel.tsx @@ -1,23 +1,24 @@ import { observer } from 'mobx-react-lite' import { useKBar } from 'kbar' +import { useNavigate } from 'react-router-dom' import Github from '~/icons/Github' import Search from '~/icons/Search' import { SideBar } from '~/containers/SideBar' -import { settingStore } from '~/core/setting/SettingStore' const MobileSplashPanel = observer(() => { + const navigate = useNavigate() const { query } = useKBar() const handleQuickSearchClicked = () => { - settingStore.closeSettingsModal() - query.toggle() + + navigate('/') } return ( -
+
-
- -
- -
+
+ +
+ + +
+ {isMobile ? ( +
+
+ +
+ +
    +
  • + +
  • + +
  • + +
  • + +
  • + +
  • +
+
+ ) : ( + <> +
+ +
+ +
+ +
+ + )} + +
+ {!isMobile && ( + + )} -
- - - + +
-
- - -
+ + +
+ ) }) diff --git a/src/features/settings/panels/connections/ConnectionParameterSection.tsx b/src/features/settings/panels/connections/ConnectionParameterSection.tsx index 3195419..38a5144 100644 --- a/src/features/settings/panels/connections/ConnectionParameterSection.tsx +++ b/src/features/settings/panels/connections/ConnectionParameterSection.tsx @@ -1,14 +1,17 @@ -import { MouseEventHandler, useCallback, useMemo, useState } from 'react' +import { KeyboardEventHandler, MouseEventHandler } from 'react' +import { useNavigate, useOutletContext, useParams } from 'react-router-dom' import _ from 'lodash' -import { Controller, useFormContext, useFieldArray } from 'react-hook-form' +import { Controller, useFieldArray, Control, useFormState, useWatch } from 'react-hook-form' import { Checkbox, Select, SelectItem } from '@nextui-org/react' import Delete from '~/icons/Delete' -import Back from '~/icons/Back' -import { ConnectionFormDataType } from '~/features/settings/panels/connections/ConnectionPanel' +import { ConnectionParameterModel } from '~/core/connection/ConnectionModel' + import ToolTip from '~/components/Tooltip' import FormInput from '~/components/form/FormInput' +import SettingSection, { SettingSectionItem } from '~/containers/SettingSection' +import { useCrumb } from '~/containers/Drawer' type ParameterOptionsType = 'system' | 'valueRequired' | 'fieldRequired' @@ -18,31 +21,41 @@ const parameterOptions: Array<{ key: ParameterOptionsType; label: string }> = [ { key: 'fieldRequired', label: 'Is the field required by the api?' }, ] -type ConnectionDataParameterRowProps = { - index: number - onRemove: () => void -} +type ParameterFormOutletContext = { control: Control<{ parameters: ConnectionParameterModel[] }> } -const ParameterForm = ({ index, onRemove }: ConnectionDataParameterRowProps) => { - const { - register, - control, - getValues, - formState: { errors }, - } = useFormContext() - const { fields: parameters } = useFieldArray({ +export const ParameterForm = () => { + const { parameterId } = useParams() + const { control } = useOutletContext() + const navigate = useNavigate() + + const { errors } = useFormState({ control }) + + const { fields: parameters, remove } = useFieldArray({ control, name: 'parameters', }) + const index = _.findIndex(parameters, { field: parameterId }) const parameter = parameters[index] - const currentParameter = getValues(`parameters.${index}`) - if (!parameter || !currentParameter) return null + useCrumb({ label: parameter?.field, path: parameterId! }) + + // if this does not exist, go back + // (reproduce by deleting a parameter on mobile and then pressing back) + if (!parameter) { + navigate(-1) + return + } + + const handleRemove = () => { + navigate(-1) + + remove(index) + } const validateUniqueField = (nextField: string) => { // skip if the field did not change - if (currentParameter.field === parameter.field) return + if (parameter.field === parameter.field) return const otherParameter = _.find(parameters, { field: nextField }) @@ -52,10 +65,10 @@ const ParameterForm = ({ index, onRemove }: ConnectionDataParameterRowProps) => return true } - const valueRequired = currentParameter.types?.includes('valueRequired') + const valueRequired = parameter.types?.includes('valueRequired') const validateValue = (nextValue?: string) => { - if (currentParameter.isJson) { + if (parameter.isJson) { try { JSON.stringify(nextValue) } catch (e) { @@ -63,7 +76,7 @@ const ParameterForm = ({ index, onRemove }: ConnectionDataParameterRowProps) => } } - if (valueRequired && _.isEmpty(nextValue) && _.isEmpty(currentParameter.defaultValue)) { + if (valueRequired && _.isEmpty(nextValue) && _.isEmpty(parameter.defaultValue)) { return 'A value or default value is required' } @@ -82,12 +95,19 @@ const ParameterForm = ({ index, onRemove }: ConnectionDataParameterRowProps) => return true } - console.log('field: ', getValues(`parameters.${index}.label`), currentParameter.label) - console.log('parameter types: ', currentParameter.types) - console.log('valueRequired: ', valueRequired) + const handleEnterPressedOnDrawer: KeyboardEventHandler = e => { + if (e.key === 'Enter') { + e.preventDefault() + + navigate(-1) + } + } return ( -
+
(
- -
- - Convert to JSON? - -
-
+ ( + +
+ + Convert to JSON? + +
+
+ )} + control={control} + name={`parameters.${index}.isJson`} + defaultValue={parameter.isJson || false} + />
className="w-full min-w-[20ch] rounded-md border border-base-content/30 bg-transparent" selectionMode="multiple" size="sm" - // variant="bordered" classNames={{ value: '!text-base-content min-w-[20ch]', trigger: 'bg-base-100 hover:!bg-base-200 rounded-md', @@ -217,44 +243,42 @@ const ParameterForm = ({ index, onRemove }: ConnectionDataParameterRowProps) => defaultValue={parameter.types} /> - +
+ + + +
) } -const ConnectionDataParameterSection = () => { - const { control } = useFormContext() - - const [selectedIndex, setSelectedIndex] = useState() - const [filterText, setFilterText] = useState('') +const ConnectionDataParameterSection = ({ subControl }: { subControl: unknown }) => { + // we really just want the field array options + const control = subControl as Control<{ parameters: ConnectionParameterModel[] }> + const navigate = useNavigate() - const { - fields: parameters, - append, - remove, - } = useFieldArray({ + const { append, remove } = useFieldArray({ control, // control props comes from useForm (optional: if you are using FormProvider) name: 'parameters', // unique name for your Field Array }) + // we need to use this to get the updated ids + const parameters = useWatch({ control, name: 'parameters' }) + const hasNewField = !!_.find(parameters, { field: 'newField' }) - const hasSelectedParameter = selectedIndex !== undefined - - const filteredParameters = useMemo(() => { - const lowerCaseFilter = filterText.toLowerCase() - return parameters.filter( - parameter => - parameter.field.toLowerCase().includes(lowerCaseFilter) || - parameter.label?.toLowerCase().includes(lowerCaseFilter), - ) - }, [parameters, filterText]) const addParameter: MouseEventHandler = e => { e.preventDefault() @@ -267,108 +291,58 @@ const ConnectionDataParameterSection = () => { isJson: false, }) - setSelectedIndex(parameters.length) + navigate('newField') } const removeParameterByIndex = (index: number) => { - setSelectedIndex(undefined) - remove(index) } - const Form = useCallback( - () => - hasSelectedParameter && ( - removeParameterByIndex(selectedIndex)} - /> - ), - [selectedIndex, parameters, hasSelectedParameter], - ) - - return ( -
-
- {hasSelectedParameter && ( - - )} - Parameters: - setFilterText(e.target.value)} - /> -
+ const parameterToSectionItem = ( + parameter: ConnectionParameterModel, + ): SettingSectionItem => ({ + id: parameter.field || '', + label: parameter.field, + subLabels: parameter.helpText && [parameter.helpText], + data: parameter, + }) -
-
    - {filteredParameters.map((parameter, index) => ( -
  • setSelectedIndex(index)} - className={ - 'mx-2 rounded-md ' + - (selectedIndex === index ? 'bg-base-content/10' : 'bg-base-300') - } - > - {hasSelectedParameter ? ( - {parameter.field} - ) : ( -
    -
    - {parameter.field} - - {parameter.label} - - - -
    - - {parameter.helpText && ( -

    - {parameter.helpText} -

    - )} -
    - )} -
  • - ))} - - {!filteredParameters[0] && parameters[0] && ( - - No parameter fields or labels matches this filter - - )} + const itemFilter = (parameter: ConnectionParameterModel, filterText: string) => { + return ( + parameter.field.toLowerCase().includes(filterText) || + parameter.label?.toLowerCase().includes(filterText) + ) + } -
  • - -
  • -
+ // watched parameters still loading + if (!parameters) return null -
-
-
+ return ( + ( + + )} + selectedItem={undefined} + isSubSection + /> ) } diff --git a/src/features/settings/panels/connections/ConnectionsPanel.tsx b/src/features/settings/panels/connections/ConnectionsPanel.tsx deleted file mode 100644 index 4970a20..0000000 --- a/src/features/settings/panels/connections/ConnectionsPanel.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' -import { observer } from 'mobx-react-lite' -import { ScrollShadow, Tab, Tabs } from '@nextui-org/react' -import _ from 'lodash' - -import NewConnectionPanel from '~/features/settings/panels/connections/NewConnectionPanel' - -import ConnectionPanel from '~/features/settings/panels/connections/ConnectionPanel' -import { connectionStore } from '~/core/connection/ConnectionStore' - -const ConnectionsPanel = observer(() => { - const { selectedConnection, connections } = connectionStore - - const [selectedTabId, setSelectedTabId] = useState(selectedConnection?.id ?? 'App') - - useEffect(() => { - setSelectedTabId(selectedConnection?.id ?? 'App') - }, [selectedConnection]) - - const selectedPanel = useMemo(() => { - if (selectedTabId === 'new_connection') { - return - } - - const connection = connectionStore.getConnectionById(selectedTabId) - - if (connection) { - return - } - - setSelectedTabId('new_connection') - - return null - }, [selectedTabId, connections]) - - return ( -
- - setSelectedTabId(_.toString(key))} - classNames={{ - tab: 'overflow-hidden flex-shrink-0 w-fit', - tabList: - 'gap-3 w-full flex max-w-full relative rounded-none p-0 overflow-x-scroll flex-shrink-0', - cursor: 'group-data-[selected=true]:bg-primary w-full bg-base-content', - tabContent: - 'group-data-[selected=true]:text-primary group-data-[selected=true]:border-b-primary flex-shrink-0 group-[.is-active-parent]:text-primary/60', - }} - > - {_.map(connections, connection => ( - - ))} - - - - - - {selectedPanel && ( -
- - {selectedPanel} - -
- )} -
- ) -}) - -export default ConnectionsPanel diff --git a/src/features/settings/panels/general/GeneralPanel.tsx b/src/features/settings/panels/general/GeneralPanel.tsx index 973522b..46f377f 100644 --- a/src/features/settings/panels/general/GeneralPanel.tsx +++ b/src/features/settings/panels/general/GeneralPanel.tsx @@ -17,7 +17,7 @@ const GeneralModelPanel = observer(() => { }, [selectedTabId]) return ( -
+
{ - const { selectedConnection, connections } = connectionStore - const { modelPanelConnectionId } = settingStore +export const ConnectionModelPanel = observer(() => { + const { id } = useParams() - const [selectedTabId, setSelectedTabId] = useState( - modelPanelConnectionId ?? selectedConnection?.id ?? connections[0]?.id, - ) + const connection = connectionStore.getConnectionById(id)! - const connection = useMemo(() => { - return connectionStore.getConnectionById(selectedTabId) - }, [selectedTabId, connections]) + return ( + +
+ + {connection.type === 'LMS' && } + {connection.type === 'A1111' && } + {connection.type === 'Ollama' && } + {connection.type === 'OpenAi' && } + {connection.type === 'Gemini' && } + +
+
+ ) +}) - useEffect(() => { - settingStore.setModelPanelOverride(undefined) - }, []) +const ModelPanel = observer(() => { + const { selectedConnection, connections } = connectionStore - return ( -
- {selectedTabId && ( - <> - - setSelectedTabId(_.toString(key))} - classNames={{ - tab: 'overflow-hidden flex-shrink-0 w-fit', - tabList: - 'gap-3 w-full flex max-w-full relative rounded-none p-0 overflow-x-scroll flex-shrink-0', - cursor: 'group-data-[selected=true]:bg-primary w-full bg-base-content', - tabContent: - 'group-data-[selected=true]:text-primary group-data-[selected=true]:border-b-primary flex-shrink-0 group-[.is-active-parent]:text-primary/60', - }} - > - {_.map(connections, connection => ( - - ))} - - + const connectionToSectionItem = ( + connection: ConnectionViewModelTypes, + ): SettingSectionItem => ({ + id: connection.id, + label: connection.label, + subLabels: [connection.source.enabled ? 'Enabled' : 'Disabled'], + data: connection, + }) - {connection && ( -
- - {connection.type === 'LMS' && } - {connection.type === 'A1111' && } - {connection.type === 'Ollama' && } - {connection.type === 'OpenAi' && } - {connection.type === 'Gemini' && } - -
- )} - - )} + const itemFilter = (connection: ConnectionViewModelTypes, filterText: string) => { + return connection.label.toLowerCase().includes(filterText) + } - {!selectedTabId && ( -
- Add a connection in the - -
+ return ( + ( + + + )} -
+ /> ) }) diff --git a/src/features/settings/panels/model/NotConnectedPanelSection.tsx b/src/features/settings/panels/model/NotConnectedPanelSection.tsx index d8b5eb3..854ed3e 100644 --- a/src/features/settings/panels/model/NotConnectedPanelSection.tsx +++ b/src/features/settings/panels/model/NotConnectedPanelSection.tsx @@ -1,15 +1,9 @@ import Refresh from '~/icons/Refresh' -import { settingStore } from '~/core/setting/SettingStore' -import { connectionStore } from '~/core/connection/ConnectionStore' import { ConnectionViewModelTypes } from '~/core/connection/viewModels' +import { NavButtonDiv } from '~/components/NavButton' const NotConnectedPanelSection = ({ connection }: { connection: ConnectionViewModelTypes }) => { - const openConnectionSettings = () => { - connectionStore.setSelectedConnection(connection) - settingStore.openSettingsModal('connections') - } - return (
@@ -19,9 +13,9 @@ const NotConnectedPanelSection = ({ connection }: { connection: ConnectionViewMo - +
) } diff --git a/src/features/settings/panels/model/OllamaModelPanel.tsx b/src/features/settings/panels/model/OllamaModelPanel.tsx index c61d7bd..a2f7855 100644 --- a/src/features/settings/panels/model/OllamaModelPanel.tsx +++ b/src/features/settings/panels/model/OllamaModelPanel.tsx @@ -1,15 +1,16 @@ import { observer } from 'mobx-react-lite' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import _ from 'lodash' +import { useNavigate, useParams } from 'react-router-dom' import { settingStore } from '~/core/setting/SettingStore' -import { IOllamaModel } from '~/core/connection/types' +import { OllamaLanguageModel } from '~/core/connection/types' -import OllamaStore, { CorrectShowResponse } from '~/features/ollama/OllamaStore' +import { CorrectShowResponse } from '~/features/ollama/OllamaStore' +import { NavButtonDiv } from '~/components/NavButton' import SelectionPanelTable from '~/components/SelectionTablePanel' import CopyButton from '~/components/CopyButton' -import BreadcrumbBar, { BreadcrumbType } from '~/components/BreadcrumbBar' import NotConnectedPanelSection from '~/features/settings/panels/model/NotConnectedPanelSection' import Globe from '~/icons/Globe' @@ -19,121 +20,134 @@ import Edit from '~/icons/Edit' import OllamaConnectionViewModel from '~/core/connection/viewModels/OllamaConnectionViewModel' import { connectionStore } from '~/core/connection/ConnectionStore' +import { useCrumb } from '~/containers/Drawer' type PanelTableProps = { connection: OllamaConnectionViewModel - ollamaStore: OllamaStore - onShowDetails: () => void } -const OllamaModelPanelTable = observer( - ({ onShowDetails, connection, ollamaStore }: PanelTableProps) => { - const selectedModelId = settingStore.setting?.selectedModelId +const OllamaModelPanelTable = observer(({ connection }: PanelTableProps) => { + const navigate = useNavigate() - const [filterText, setFilterText] = useState('') + const selectedModelId = settingStore.setting?.selectedModelId + const ollamaStore = connection.store - const pullModel = () => { - ollamaStore.pull(filterText) + const [filterText, setFilterText] = useState('') - settingStore.closeSettingsModal() - } + const pullModel = () => { + ollamaStore.pull(filterText) - const updateAllModels = () => { - ollamaStore.updateAll() + navigate('/') + } + + const updateAllModels = () => { + ollamaStore.updateAll() - settingStore.closeSettingsModal() - } + navigate('/') + } - if (!connection.isConnected) { - return - } + if (!connection.isConnected) { + return + } - const renderRow = (model: IOllamaModel) => ( - <> - - - {model.name} - - + const renderRow = (model: OllamaLanguageModel) => ( + <> + + + {model.name} + + + + {model.details.parameter_size} + + + e.preventDefault()} + /> + - {model.details.parameter_size} + {model.gbSize} + {model.timeAgo} - - e.preventDefault()} - /> - + + + + + + + ) - {model.gbSize} - {model.timeAgo} + return ( + model.id} + onItemSelected={model => connection.selectModel(model)} + onFilterChanged={setFilterText} + getIsItemSelected={model => selectedModelId === model.id} + filterInputPlaceholder="Filter by name or pull..." + includeEmptyHeader + > +
+ + Ollama Library + + - + {filterText ? ( - - - ) - - return ( - model.id} - onItemSelected={model => connection.selectModel(model)} - onFilterChanged={setFilterText} - getIsItemSelected={model => selectedModelId === model.id} - filterInputPlaceholder="Filter by name or pull..." - includeEmptyHeader - > -
- - Ollama Library - - - - {filterText ? ( - - ) : ( - - )} -
-
- ) - }, -) + Update all models + + + )} +
+ + ) +}) + +export const OllamaModelSettings = observer(() => { + const { modelName, id } = useParams() + const navigate = useNavigate() + + const viewModel = connectionStore.getConnectionById(id)! + + useCrumb({ label: modelName!, path: 'ollama/' + modelName! }) -const OllamaModelSettings = observer(({ ollamaStore }: { ollamaStore: OllamaStore }) => { - const { selectedModelName } = connectionStore + if (viewModel.type !== 'Ollama') { + throw new Error('Ollama connection not found') + } + const ollamaStore = viewModel.store + const model = _.find(viewModel.models, { modelName })! + + const selectedModelName = model.modelName const [modelData, setModelData] = useState() @@ -148,13 +162,13 @@ const OllamaModelSettings = observer(({ ollamaStore }: { ollamaStore: OllamaStor const details = modelData.details || {} const updateModel = () => { - settingStore.closeSettingsModal() + navigate('/') ollamaStore.pull(selectedModelName) } return ( -
+