pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import ZShell.Config import ZShell.Components import ZShell.Llm import ZShell.Blobs import qs.Modules.Notifications.Sidebar.Chat import qs.Components import qs.Services Item { id: root required property Item rootParent required property Item fabRoot property bool open: false property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2 property int openHeight: Math.min(modelsContent.item?.implicitHeight, maxHeight) property int openWidth: root.rootParent.width - Tokens.padding.medium * 2 property real openHeightProg: openHeight / maxHeight property int collapsedY: 0 property int horizontalContentMargin property string header: qsTr("Available models") property string acceptLabel: qsTr("Accept") z: open || dialogTransition.running ? 2 : 0 Layout.fillWidth: true implicitHeight: fabRoot.implicitHeight function prettyModelName(): string { if (modelsContent.item?.modelsList.count < 1) return qsTr("No models found"); const pathList = LlmClient.model.split("\/"); const name = pathList[pathList.length - 1]; if (!name) return qsTr("No model selected"); return name; } Behavior on implicitHeight { Anim {} } function reparentWrapper(): void { const newParent = open ? root.rootParent : root; const c = modelsWrapper.parent.mapToItem(newParent, modelsWrapper.x + modelsWrapper.width / 2, modelsWrapper.y + modelsWrapper.height / 2); root.collapsedY = modelsWrapper.y; modelsWrapper.parent = newParent; modelsWrapper.x = c.x - modelsWrapper.width / 2; modelsWrapper.y = c.y - modelsWrapper.height / 2; } BlobGroup { id: blobGroup color: root.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainer Behavior on color { CAnim {} } } MouseArea { id: backdrop anchors.fill: parent enabled: false hoverEnabled: enabled preventStealing: true parent: root.open ? root.rootParent : root onClicked: root.open = false } Item { id: modelsWrapper width: root.width height: modelBox.implicitHeight z: 1 states: State { name: "open" when: root.open PropertyChanges { backdrop.enabled: true modelsBackground.radius: Tokens.rounding.largeIncreased modelsContent.opacity: 1 modelsWrapper.height: root.openHeight modelsWrapper.width: root.openWidth modelsWrapper.x: (root.rootParent.width - root.openWidth) / 2 modelsWrapper.y: root.rootParent.height - root.openHeight - Tokens.spacing.medium - Tokens.padding.medium - root.fabRoot.implicitHeight elevation.opacity: 1 modelBox.opacity: 0 } } transitions: Transition { id: dialogTransition SequentialAnimation { ScriptAction { script: root.reparentWrapper() } Anim { properties: "x,y" } } PropertyAction { property: "enabled" } Anim { properties: "opacity,radius" type: Anim.DefaultEffects } Anim { properties: "width,height" } } Elevation { id: elevation anchors.fill: parent level: 4 opacity: 0 radius: modelsBackground.radius transform: Matrix4x4 { matrix: modelsBackground.deformMatrix } } BlobRect { id: modelsBackground anchors.fill: parent radius: modelLayer.pressed ? Tokens.rounding.small : Tokens.rounding.extraLarge deformScale: 0 group: blobGroup opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5) Behavior on radius { Anim { type: Anim.DefaultEffects } } } CustomRect { id: modelBox anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top implicitHeight: root.fabRoot.implicitHeight radius: modelsBackground.radius CustomText { id: label text: root.prettyModelName() color: Colors.palette.m3onSurfaceVariant anchors.left: parent.left anchors.leftMargin: Tokens.padding.large anchors.verticalCenter: parent.verticalCenter } MaterialIcon { color: Colors.palette.m3onSurfaceVariant anchors.right: parent.right anchors.rightMargin: Tokens.padding.large anchors.verticalCenter: parent.verticalCenter text: "unfold_more" fontStyle: Tokens.font.icon.large } StateLayer { id: modelLayer onClicked: { ChatState.fabExpanded = false; root.open = true; } } } Loader { id: modelsContent active: opacity > 0 anchors.fill: parent asynchronous: false opacity: 0 sourceComponent: MouseArea { readonly property alias modelsList: modelsList onWheel: event => event.accepted = true preventStealing: true clip: true implicitHeight: innerLayout.implicitHeight + innerLayout.anchors.margins * 2 ColumnLayout { id: innerLayout anchors.bottomMargin: Tokens.padding.largeIncreased anchors.fill: parent anchors.margins: Tokens.padding.extraLarge spacing: 0 CustomText { font: Tokens.font.title.builders.large.weight(Font.Normal).build() text: root.header } VerticalFadeListView { id: modelsList Layout.fillWidth: true Layout.leftMargin: root.horizontalContentMargin Layout.topMargin: Tokens.spacing.medium Layout.bottomMargin: Tokens.spacing.medium Layout.rightMargin: root.horizontalContentMargin Layout.preferredHeight: 400 Layout.maximumHeight: 400 model: LlmClient.availableModels currentIndex: LlmClient.availableModels.indexOf(LlmClient.model) visible: opacity > 0 Behavior on opacity { Anim {} } CustomScrollBar.vertical: CustomScrollBar { flickable: modelsList } delegate: CustomRect { id: model required property var modelData required property int index readonly property bool selected: ListView.view.currentIndex === model.index implicitWidth: ListView.view.width implicitHeight: name.implicitHeight + Tokens.padding.small * 2 radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0) Behavior on radius { Anim { type: Anim.SlowEffects } } RowLayout { id: layout anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.small anchors.leftMargin: Tokens.padding.large anchors.rightMargin: Tokens.padding.large CustomText { id: name text: model.modelData elide: CustomText.ElideRight color: model.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface Layout.fillWidth: true } MaterialIcon { id: check color: Colors.palette.m3onTertiaryContainer fontStyle: Tokens.font.icon.medium opacity: model.selected ? 1 : 0 visible: opacity > 0 text: "check" Layout.alignment: Qt.AlignRight | Qt.AlignVCenter Behavior on opacity { Anim { type: Anim.SlowEffects } } } } StateLayer { id: stateLayer onClicked: { model.ListView.view.currentIndex = model.index; } } } } RowLayout { Layout.alignment: Qt.AlignRight spacing: Tokens.spacing.extraSmall TextButton { horizontalPadding: Tokens.padding.largeIncreased isRound: true text: qsTr("Cancel") type: TextButton.Text verticalPadding: Tokens.padding.small stateLayer.preventStealing: true onClicked: { root.open = false; } } TextButton { horizontalPadding: Tokens.padding.largeIncreased isRound: true text: root.acceptLabel type: TextButton.Text stateLayer.preventStealing: true verticalPadding: Tokens.padding.small onClicked: { LlmClient.selectModel(modelsList.currentItem.modelData); root.open = false; } } } } } } } }