mirror of
https://github.com/danbulant/dots-hyprland
synced 2026-05-24 12:22:09 +00:00
701 lines
No EOL
29 KiB
QML
701 lines
No EOL
29 KiB
QML
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(`
|
||
<think>
|
||
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!
|
||
</think>
|
||
## ✏️ Markdown test
|
||
### Formatting
|
||
|
||
- *Italic*, \`Monospace\`, **Bold**, [Link](https://example.com)
|
||
- Arch lincox icon <img src="${Quickshell.shellPath("assets/icons/arch-symbolic.svg")}" height="${Appearance.font.pixelSize.small}"/>
|
||
|
||
### 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 <bits/stdc++.h>
|
||
// 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 = ""
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
|
||
} |