import "root:/" import "root:/services" import "root:/modules/common" import "root:/modules/common/widgets" import "./aiChat/" import "root:/modules/common/functions/fuzzysort.js" as Fuzzy import "root:/modules/common/functions/string_utils.js" as StringUtils import "root:/modules/common/functions/file_utils.js" as FileUtils import QtQuick import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects import Quickshell.Io import Quickshell import Quickshell.Hyprland Item { id: root property var inputField: messageInputField property string commandPrefix: "/" property var suggestionQuery: "" property var suggestionList: [] onFocusChanged: (focus) => { if (focus) { root.inputField.forceActiveFocus() } } Keys.onPressed: (event) => { messageInputField.forceActiveFocus() if (event.modifiers === Qt.NoModifier) { if (event.key === Qt.Key_PageUp) { messageListView.contentY = Math.max(0, messageListView.contentY - messageListView.height / 2) event.accepted = true } else if (event.key === Qt.Key_PageDown) { messageListView.contentY = Math.min(messageListView.contentHeight - messageListView.height / 2, messageListView.contentY + messageListView.height / 2) event.accepted = true } } } property var allCommands: [ { name: "model", description: qsTr("Choose model"), execute: (args) => { Ai.setModel(args[0]); } }, { name: "prompt", description: qsTr("Set the system prompt for the model."), execute: (args) => { if (args.length === 0 || args[0] === "get") { Ai.printPrompt(); return; } Ai.loadPrompt(args.join(" ").trim()); } }, { name: "key", description: qsTr("Set API key"), execute: (args) => { if (args[0] == "get") { Ai.printApiKey() } else { Ai.setApiKey(args[0]); } } }, { name: "clear", description: qsTr("Clear chat history"), execute: () => { Ai.clearMessages(); } }, { name: "temp", description: qsTr("Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5."), execute: (args) => { // console.log(args) if (args.length == 0 || args[0] == "get") { Ai.printTemperature() } else { const temp = parseFloat(args[0]); Ai.setTemperature(temp); } } }, { name: "test", description: qsTr("Markdown test"), execute: () => { Ai.addMessage(` A longer think block to test revealing animation OwO wem ipsum dowo sit amet, consekituwet awipiscing ewit, sed do eiuwsmod tempow inwididunt ut wabowe et dowo mawa. Ut enim ad minim weniam, quis nostwud exeucitation uwuwamcow bowowis nisi ut awiquip ex ea commowo consequat. Duuis aute iwuwe dowo in wepwependewit in wowuptate velit esse ciwwum dowo eu fugiat nuwa pawiatuw. Excepteuw sint occaecat cupidatat non pwowoident, sunt in cuwpa qui officia desewunt mowit anim id est wabowum. Meouw! >w< Mowe uwu wem ipsum! ## ✏️ Markdown test ### Formatting - *Italic*, \`Monospace\`, **Bold**, [Link](https://example.com) - Arch lincox icon ### Table Quickshell vs AGS/Astal | | Quickshell | AGS/Astal | |--------------------------|------------------|-------------------| | UI Toolkit | Qt | Gtk3/Gtk4 | | Language | QML | Js/Ts/Lua | | Reactivity | Implied | Needs declaration | | Widget placement | Mildly difficult | More intuitive | | Bluetooth & Wifi support | ❌ | ✅ | | No-delay keybinds | ✅ | ❌ | | Development | New APIs | New syntax | ### Code block Just a hello world... \`\`\`cpp #include // This is intentionally very long to test scrolling const std::string GREETING = \"UwU\"; int main(int argc, char* argv[]) { std::cout << GREETING; } \`\`\` ### LaTeX Inline w/ dollar signs: $\\frac{1}{2} = \\frac{2}{4}$ Inline w/ double dollar signs: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$ Inline w/ backslash and square brackets \\[\\int_0^\\infty \\frac{1}{x^2} dx = \\infty\\] Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\) `, Ai.interfaceRole); } }, ] function handleInput(inputText) { if (inputText.startsWith(root.commandPrefix)) { // Handle special commands const command = inputText.split(" ")[0].substring(1); const args = inputText.split(" ").slice(1); const commandObj = root.allCommands.find(cmd => cmd.name === `${command}`); if (commandObj) { commandObj.execute(args); } else { Ai.addMessage(qsTr("Unknown command: ") + command, Ai.interfaceRole); } } else { Ai.sendUserMessage(inputText); } } ColumnLayout { id: columnLayout anchors.fill: parent Item { // Messages Layout.fillWidth: true Layout.fillHeight: true StyledListView { // Message list id: messageListView anchors.fill: parent spacing: 10 popin: false property int lastResponseLength: 0 clip: true layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { width: swipeView.width height: swipeView.height radius: Appearance.rounding.small } } add: null // Prevent function calls from being janky Behavior on contentY { NumberAnimation { id: scrollAnim duration: Appearance.animation.scroll.duration easing.type: Appearance.animation.scroll.type easing.bezierCurve: Appearance.animation.scroll.bezierCurve } } model: ScriptModel { values: Ai.messageIDs.filter(id => { const message = Ai.messageByID[id]; return message?.visibleToUser ?? true; }) } delegate: AiMessage { required property var modelData required property int index messageIndex: index messageData: { Ai.messageByID[modelData] } messageInputField: root.inputField } } Item { // Placeholder when list is empty opacity: Ai.messageIDs.length === 0 ? 1 : 0 visible: opacity > 0 anchors.fill: parent Behavior on opacity { animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) } ColumnLayout { anchors.centerIn: parent spacing: 5 MaterialSymbol { Layout.alignment: Qt.AlignHCenter iconSize: 60 color: Appearance.m3colors.m3outline text: "neurology" } StyledText { id: widgetNameText Layout.alignment: Qt.AlignHCenter font.pixelSize: Appearance.font.pixelSize.larger font.family: Appearance.font.family.title color: Appearance.m3colors.m3outline horizontalAlignment: Text.AlignHCenter text: qsTr("Large language models") } StyledText { id: widgetDescriptionText Layout.fillWidth: true font.pixelSize: Appearance.font.pixelSize.small color: Appearance.m3colors.m3outline horizontalAlignment: Text.AlignLeft wrapMode: Text.Wrap text: qsTr("Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window") } } } } DescriptionBox { text: root.suggestionList[suggestions.selectedIndex]?.description ?? "" showArrows: root.suggestionList.length > 1 } FlowButtonGroup { // Suggestions id: suggestions visible: root.suggestionList.length > 0 && messageInputField.text.length > 0 property int selectedIndex: 0 Layout.fillWidth: true spacing: 5 Repeater { id: suggestionRepeater model: { suggestions.selectedIndex = 0 return root.suggestionList.slice(0, 10) } delegate: ApiCommandButton { id: commandButton colBackground: suggestions.selectedIndex === index ? Appearance.colors.colSecondaryContainerHover : Appearance.colors.colSecondaryContainer bounce: false contentItem: StyledText { font.pixelSize: Appearance.font.pixelSize.small color: Appearance.m3colors.m3onSurface horizontalAlignment: Text.AlignHCenter text: modelData.displayName ?? modelData.name } onHoveredChanged: { if (commandButton.hovered) { suggestions.selectedIndex = index; } } onClicked: { suggestions.acceptSuggestion(modelData.name) } } } function acceptSuggestion(word) { const words = messageInputField.text.trim().split(/\s+/); if (words.length > 0) { words[words.length - 1] = word; } else { words.push(word); } const updatedText = words.join(" ") + " "; messageInputField.text = updatedText; messageInputField.cursorPosition = messageInputField.text.length; messageInputField.forceActiveFocus(); } function acceptSelectedWord() { if (suggestions.selectedIndex >= 0 && suggestions.selectedIndex < suggestionRepeater.count) { const word = root.suggestionList[suggestions.selectedIndex].name; suggestions.acceptSuggestion(word); } } } Rectangle { // Input area id: inputWrapper property real columnSpacing: 5 Layout.fillWidth: true radius: Appearance.rounding.small color: Appearance.colors.colLayer1 implicitWidth: messageInputField.implicitWidth implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin + commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45) clip: true border.color: Appearance.colors.colOutlineVariant border.width: 1 Behavior on implicitHeight { animation: Appearance.animation.elementMove.numberAnimation.createObject(this) } RowLayout { // Input field and send button id: inputFieldRowLayout anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.topMargin: 5 spacing: 0 StyledTextArea { // The actual TextArea id: messageInputField wrapMode: TextArea.Wrap Layout.fillWidth: true padding: 10 color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant placeholderText: StringUtils.format(qsTr('Message the model... "{0}" for commands'), root.commandPrefix) background: null onTextChanged: { // Handle suggestions if(messageInputField.text.length === 0) { root.suggestionQuery = "" root.suggestionList = [] return } else if(messageInputField.text.startsWith(`${root.commandPrefix}model`)) { root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" const modelResults = Fuzzy.go(root.suggestionQuery, Ai.modelList.map(model => { return { name: Fuzzy.prepare(model), obj: model, } }), { all: true, key: "name" }) root.suggestionList = modelResults.map(model => { return { name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "model ") : ""}${model.target}`, displayName: `${Ai.models[model.target].name}`, description: `${Ai.models[model.target].description}`, } }) } else if(messageInputField.text.startsWith(`${root.commandPrefix}prompt`)) { root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.promptFiles.map(file => { return { name: Fuzzy.prepare(file), obj: file, } }), { all: true, key: "name" }) root.suggestionList = promptFileResults.map(file => { return { name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "prompt ") : ""}${file.target}`, displayName: `${FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target))}`, description: `Load prompt from ${file.target}`, } }) } else if(messageInputField.text.startsWith(root.commandPrefix)) { root.suggestionQuery = messageInputField.text root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => { return { name: `${root.commandPrefix}${cmd.name}`, description: `${cmd.description}`, } }) } } function accept() { root.handleInput(text) text = "" } Keys.onPressed: (event) => { if (event.key === Qt.Key_Tab) { suggestions.acceptSelectedWord(); event.accepted = true; } else if (event.key === Qt.Key_Up && suggestions.visible) { suggestions.selectedIndex = Math.max(0, suggestions.selectedIndex - 1); event.accepted = true; } else if (event.key === Qt.Key_Down && suggestions.visible) { suggestions.selectedIndex = Math.min(root.suggestionList.length - 1, suggestions.selectedIndex + 1); event.accepted = true; } else if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) { if (event.modifiers & Qt.ShiftModifier) { // Insert newline messageInputField.insert(messageInputField.cursorPosition, "\n") event.accepted = true } else { // Accept text const inputText = messageInputField.text messageInputField.clear() root.handleInput(inputText) event.accepted = true } } } } RippleButton { // Send button id: sendButton Layout.alignment: Qt.AlignTop Layout.rightMargin: 5 implicitWidth: 40 implicitHeight: 40 buttonRadius: Appearance.rounding.small enabled: messageInputField.text.length > 0 toggled: enabled MouseArea { anchors.fill: parent cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor onClicked: { const inputText = messageInputField.text root.handleInput(inputText) messageInputField.clear() } } contentItem: MaterialSymbol { anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter iconSize: Appearance.font.pixelSize.larger // fill: sendButton.enabled ? 1 : 0 color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled text: "send" } } } RowLayout { // Controls id: commandButtonsRow anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.bottomMargin: 5 anchors.leftMargin: 5 anchors.rightMargin: 5 spacing: 5 property var commandsShown: [ { name: "model", sendDirectly: false, }, { name: "clear", sendDirectly: true, }, ] Item { implicitHeight: providerRowLayout.implicitHeight + 5 * 2 implicitWidth: providerRowLayout.implicitWidth + 10 * 2 RowLayout { id: providerRowLayout anchors.centerIn: parent MaterialSymbol { text: "api" iconSize: Appearance.font.pixelSize.large } StyledText { id: providerName font.pixelSize: Appearance.font.pixelSize.small color: Appearance.m3colors.m3onSurface elide: Text.ElideRight text: Ai.getModel().name } } StyledToolTip { id: toolTip extraVisibleCondition: false alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered content: StringUtils.format(qsTr("Current model: {0}\nSet it with {1}model MODEL"), Ai.getModel().name, root.commandPrefix) } MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true } } Item { Layout.fillWidth: true } ButtonGroup { padding: 0 Repeater { // Command buttons model: commandButtonsRow.commandsShown delegate: ApiCommandButton { property string commandRepresentation: `${root.commandPrefix}${modelData.name}` buttonText: commandRepresentation onClicked: { if(modelData.sendDirectly) { root.handleInput(commandRepresentation) } else { messageInputField.text = commandRepresentation + " " messageInputField.cursorPosition = messageInputField.text.length messageInputField.forceActiveFocus() } if (modelData.name === "clear") { messageInputField.text = "" } } } } } } } } }