import qs import qs.services import qs.modules.common import qs.modules.common.widgets import qs.modules.common.functions import "./aiChat/" import QtQuick import QtQuick.Controls import QtQuick.Layouts import Qt5Compat.GraphicalEffects import Quickshell 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: Translation.tr("Choose model"), execute: (args) => { Ai.setModel(args[0]); } }, { name: "tool", description: Translation.tr("Set the tool to use for the model."), execute: (args) => { // console.log(args) if (args.length == 0 || args[0] == "get") { Ai.addMessage(Translation.tr("Usage: %1tool TOOL_NAME").arg(root.commandPrefix), Ai.interfaceRole); } else { const tool = args[0]; const switched = Ai.setTool(tool); if (switched) { Ai.addMessage(Translation.tr("Tool set to: %1").arg(tool), Ai.interfaceRole); } } } }, { name: "prompt", description: Translation.tr("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: Translation.tr("Set API key"), execute: (args) => { if (args[0] == "get") { Ai.printApiKey() } else { Ai.setApiKey(args[0]); } } }, { name: "save", description: Translation.tr("Save chat"), execute: (args) => { const joinedArgs = args.join(" ") if (joinedArgs.trim().length == 0) { Ai.addMessage(Translation.tr("Usage: %1save CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole); return; } Ai.saveChat(joinedArgs) } }, { name: "load", description: Translation.tr("Load chat"), execute: (args) => { const joinedArgs = args.join(" ") if (joinedArgs.trim().length == 0) { Ai.addMessage(Translation.tr("Usage: %1load CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole); return; } Ai.loadChat(joinedArgs) } }, { name: "clear", description: Translation.tr("Clear chat history"), execute: () => { Ai.clearMessages(); } }, { name: "temp", description: Translation.tr("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: Translation.tr("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(Translation.tr("Unknown command: ") + command, Ai.interfaceRole); } } else { Ai.sendUserMessage(inputText); } } component StatusItem: MouseArea { id: statusItem property string icon property string statusText property string description hoverEnabled: true implicitHeight: statusItemRowLayout.implicitHeight implicitWidth: statusItemRowLayout.implicitWidth RowLayout { id: statusItemRowLayout spacing: 0 MaterialSymbol { text: statusItem.icon iconSize: Appearance.font.pixelSize.huge color: Appearance.colors.colSubtext } StyledText { font.pixelSize: Appearance.font.pixelSize.small text: statusItem.statusText color: Appearance.colors.colSubtext } } StyledToolTip { content: statusItem.description extraVisibleCondition: false alternativeVisibleCondition: statusItem.containsMouse } } component StatusSeparator: Rectangle { implicitWidth: 4 implicitHeight: 4 radius: implicitWidth / 2 color: Appearance.colors.colOutlineVariant } ColumnLayout { id: columnLayout anchors.fill: parent RowLayout { // Status Layout.alignment: Qt.AlignHCenter spacing: 10 StatusItem { icon: Ai.currentModelHasApiKey ? "key" : "key_off" statusText: "" description: Ai.currentModelHasApiKey ? Translation.tr("API key is set\nChange with /key YOUR_API_KEY") : Translation.tr("No API key\nSet it with /key YOUR_API_KEY") } StatusSeparator {} StatusItem { icon: "device_thermostat" statusText: Ai.temperature.toFixed(1) description: Translation.tr("Temperature\nChange with /temp VALUE") } StatusSeparator { visible: Ai.tokenCount.total > 0 } StatusItem { visible: Ai.tokenCount.total > 0 icon: "token" statusText: Ai.tokenCount.total description: Translation.tr("Total token count\nInput: %1\nOutput: %2") .arg(Ai.tokenCount.input) .arg(Ai.tokenCount.output) } } 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: Translation.tr("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: Translation.tr("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: Translation.tr('Message the model... "%1" for commands').arg(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: Translation.tr("Load prompt from %1").arg(file.target), } }) } else if (messageInputField.text.startsWith(`${root.commandPrefix}save`)) { root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => { return { name: Fuzzy.prepare(file), obj: file, } }), { all: true, key: "name" }) root.suggestionList = promptFileResults.map(file => { const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim() return { name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "save ") : ""}${chatName}`, displayName: `${chatName}`, description: Translation.tr("Save chat to %1").arg(chatName), } }) } else if (messageInputField.text.startsWith(`${root.commandPrefix}load`)) { root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => { return { name: Fuzzy.prepare(file), obj: file, } }), { all: true, key: "name" }) root.suggestionList = promptFileResults.map(file => { const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim() return { name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "load ") : ""}${chatName}`, displayName: `${chatName}`, description: Translation.tr(`Load chat from %1`).arg(file.target), } }) } else if (messageInputField.text.startsWith(`${root.commandPrefix}tool`)) { root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" const toolResults = Fuzzy.go(root.suggestionQuery, Ai.availableTools.map(tool => { return { name: Fuzzy.prepare(tool), obj: tool, } }), { all: true, key: "name" }) root.suggestionList = toolResults.map(tool => { const toolName = tool.target return { name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "tool ") : ""}${tool.target}`, displayName: toolName, description: Ai.toolDescriptions[toolName], } }) } 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: "", sendDirectly: false, dontAddSpace: true, }, { name: "clear", sendDirectly: true, }, ] ApiInputBoxIndicator { // Model indicator icon: "api" text: Ai.getModel().name tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL") .arg(Ai.getModel().name) .arg(root.commandPrefix) } ApiInputBoxIndicator { // Tool indicator icon: "service_toolbox" text: Ai.currentTool.charAt(0).toUpperCase() + Ai.currentTool.slice(1) tooltipText: Translation.tr("Current tool: %1\nSet it with %2tool TOOL") .arg(Ai.currentTool) .arg(root.commandPrefix) } Item { Layout.fillWidth: true } ButtonGroup { // Command buttons 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 + (modelData.dontAddSpace ? "" : " ") messageInputField.cursorPosition = messageInputField.text.length messageInputField.forceActiveFocus() } if (modelData.name === "clear") { messageInputField.text = "" } } } } } } } } }