left sidebar: make content containerized

This commit is contained in:
end-4
2025-11-03 18:30:11 +01:00
parent 4270d2fe56
commit 467b84d3e2
5 changed files with 249 additions and 210 deletions
@@ -13,27 +13,28 @@ import Quickshell.Io
Item { Item {
id: root id: root
property real padding: 4
property var inputField: messageInputField property var inputField: messageInputField
property string commandPrefix: "/" property string commandPrefix: "/"
property var suggestionQuery: "" property var suggestionQuery: ""
property var suggestionList: [] property var suggestionList: []
onFocusChanged: (focus) => { onFocusChanged: focus => {
if (focus) { if (focus) {
root.inputField.forceActiveFocus() root.inputField.forceActiveFocus();
} }
} }
Keys.onPressed: (event) => { Keys.onPressed: event => {
messageInputField.forceActiveFocus() messageInputField.forceActiveFocus();
if (event.modifiers === Qt.NoModifier) { if (event.modifiers === Qt.NoModifier) {
if (event.key === Qt.Key_PageUp) { if (event.key === Qt.Key_PageUp) {
messageListView.contentY = Math.max(0, messageListView.contentY - messageListView.height / 2) messageListView.contentY = Math.max(0, messageListView.contentY - messageListView.height / 2);
event.accepted = true event.accepted = true;
} else if (event.key === Qt.Key_PageDown) { } else if (event.key === Qt.Key_PageDown) {
messageListView.contentY = Math.min(messageListView.contentHeight - messageListView.height / 2, messageListView.contentY + messageListView.height / 2) messageListView.contentY = Math.min(messageListView.contentHeight - messageListView.height / 2, messageListView.contentY + messageListView.height / 2);
event.accepted = true event.accepted = true;
} }
} }
if ((event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier) && event.key === Qt.Key_O) { if ((event.modifiers & Qt.ControlModifier) && (event.modifiers & Qt.ShiftModifier) && event.key === Qt.Key_O) {
@@ -45,21 +46,21 @@ Item {
{ {
name: "attach", name: "attach",
description: Translation.tr("Attach a file. Only works with Gemini."), description: Translation.tr("Attach a file. Only works with Gemini."),
execute: (args) => { execute: args => {
Ai.attachFile(args.join(" ").trim()); Ai.attachFile(args.join(" ").trim());
} }
}, },
{ {
name: "model", name: "model",
description: Translation.tr("Choose model"), description: Translation.tr("Choose model"),
execute: (args) => { execute: args => {
Ai.setModel(args[0]); Ai.setModel(args[0]);
} }
}, },
{ {
name: "tool", name: "tool",
description: Translation.tr("Set the tool to use for the model."), description: Translation.tr("Set the tool to use for the model."),
execute: (args) => { execute: args => {
// console.log(args) // console.log(args)
if (args.length == 0 || args[0] == "get") { if (args.length == 0 || args[0] == "get") {
Ai.addMessage(Translation.tr("Usage: %1tool TOOL_NAME").arg(root.commandPrefix), Ai.interfaceRole); Ai.addMessage(Translation.tr("Usage: %1tool TOOL_NAME").arg(root.commandPrefix), Ai.interfaceRole);
@@ -75,7 +76,7 @@ Item {
{ {
name: "prompt", name: "prompt",
description: Translation.tr("Set the system prompt for the model."), description: Translation.tr("Set the system prompt for the model."),
execute: (args) => { execute: args => {
if (args.length === 0 || args[0] === "get") { if (args.length === 0 || args[0] === "get") {
Ai.printPrompt(); Ai.printPrompt();
return; return;
@@ -86,9 +87,9 @@ Item {
{ {
name: "key", name: "key",
description: Translation.tr("Set API key"), description: Translation.tr("Set API key"),
execute: (args) => { execute: args => {
if (args[0] == "get") { if (args[0] == "get") {
Ai.printApiKey() Ai.printApiKey();
} else { } else {
Ai.setApiKey(args[0]); Ai.setApiKey(args[0]);
} }
@@ -97,25 +98,25 @@ Item {
{ {
name: "save", name: "save",
description: Translation.tr("Save chat"), description: Translation.tr("Save chat"),
execute: (args) => { execute: args => {
const joinedArgs = args.join(" ") const joinedArgs = args.join(" ");
if (joinedArgs.trim().length == 0) { if (joinedArgs.trim().length == 0) {
Ai.addMessage(Translation.tr("Usage: %1save CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole); Ai.addMessage(Translation.tr("Usage: %1save CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole);
return; return;
} }
Ai.saveChat(joinedArgs) Ai.saveChat(joinedArgs);
} }
}, },
{ {
name: "load", name: "load",
description: Translation.tr("Load chat"), description: Translation.tr("Load chat"),
execute: (args) => { execute: args => {
const joinedArgs = args.join(" ") const joinedArgs = args.join(" ");
if (joinedArgs.trim().length == 0) { if (joinedArgs.trim().length == 0) {
Ai.addMessage(Translation.tr("Usage: %1load CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole); Ai.addMessage(Translation.tr("Usage: %1load CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole);
return; return;
} }
Ai.loadChat(joinedArgs) Ai.loadChat(joinedArgs);
} }
}, },
{ {
@@ -128,10 +129,10 @@ Item {
{ {
name: "temp", 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."), 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) => { execute: args => {
// console.log(args) // console.log(args)
if (args.length == 0 || args[0] == "get") { if (args.length == 0 || args[0] == "get") {
Ai.printTemperature() Ai.printTemperature();
} else { } else {
const temp = parseFloat(args[0]); const temp = parseFloat(args[0]);
Ai.setTemperature(temp); Ai.setTemperature(temp);
@@ -191,8 +192,7 @@ Inline w/ double dollar signs: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\p
Inline w/ backslash and square brackets \\[\\int_0^\\infty \\frac{1}{x^2} dx = \\infty\\] 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\\) Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
`, `, Ai.interfaceRole);
Ai.interfaceRole);
} }
}, },
] ]
@@ -208,13 +208,12 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
} else { } else {
Ai.addMessage(Translation.tr("Unknown command: ") + command, Ai.interfaceRole); Ai.addMessage(Translation.tr("Unknown command: ") + command, Ai.interfaceRole);
} }
} } else {
else {
Ai.sendUserMessage(inputText); Ai.sendUserMessage(inputText);
} }
// Always scroll to bottom when user sends a message // Always scroll to bottom when user sends a message
messageListView.positionViewAtEnd() messageListView.positionViewAtEnd();
} }
Process { Process {
@@ -223,16 +222,14 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
property string imageDecodeFileName: "image" property string imageDecodeFileName: "image"
property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}` property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}`
function handleEntry(entry: string) { function handleEntry(entry: string) {
imageDecodeFileName = parseInt(entry.match(/^(\d+)\t/)[1]) imageDecodeFileName = parseInt(entry.match(/^(\d+)\t/)[1]);
decodeImageAndAttachProc.exec(["bash", "-c", decodeImageAndAttachProc.exec(["bash", "-c", `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(entry)}' | ${Cliphist.cliphistBinary} decode > '${imageDecodeFilePath}'`]);
`[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(entry)}' | ${Cliphist.cliphistBinary} decode > '${imageDecodeFilePath}'`
])
} }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode === 0) { if (exitCode === 0) {
Ai.attachFile(imageDecodeFilePath); Ai.attachFile(imageDecodeFilePath);
} else { } else {
console.error("[AiChat] Failed to decode image in clipboard content") console.error("[AiChat] Failed to decode image in clipboard content");
} }
} }
} }
@@ -278,37 +275,14 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
ColumnLayout { ColumnLayout {
id: columnLayout id: columnLayout
anchors.fill: parent anchors {
fill: parent
RowLayout { // Status margins: root.padding
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)
}
} }
spacing: root.padding
Item { // Messages Item {
// Messages
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
layer.enabled: true layer.enabled: true
@@ -320,6 +294,55 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
} }
} }
StyledRectangularShadow {
z: 1
target: statusBg
opacity: messageListView.atYBeginning ? 0 : 1
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Rectangle {
id: statusBg
z: 2
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 4
}
implicitWidth: statusRowLayout.implicitWidth + 10 * 2
implicitHeight: Math.max(statusRowLayout.implicitHeight, 38)
radius: Appearance.rounding.normal - root.padding
color: Appearance.colors.colLayer2
RowLayout {
id: statusRowLayout
anchors.centerIn: parent
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)
}
}
}
ScrollEdgeFade { ScrollEdgeFade {
z: 1 z: 1
target: messageListView target: messageListView
@@ -332,16 +355,20 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
anchors.fill: parent anchors.fill: parent
spacing: 10 spacing: 10
popin: false popin: false
topMargin: statusBg.implicitHeight + statusBg.anchors.topMargin * 2
touchpadScrollFactor: Config.options.interactions.scrolling.touchpadScrollFactor * 1.4 touchpadScrollFactor: Config.options.interactions.scrolling.touchpadScrollFactor * 1.4
mouseScrollFactor: Config.options.interactions.scrolling.mouseScrollFactor * 1.4 mouseScrollFactor: Config.options.interactions.scrolling.mouseScrollFactor * 1.4
property int lastResponseLength: 0 property int lastResponseLength: 0
onContentHeightChanged: { onContentHeightChanged: {
if (atYEnd) Qt.callLater(positionViewAtEnd); if (atYEnd)
Qt.callLater(positionViewAtEnd);
} }
onCountChanged: { // Auto-scroll when new messages are added onCountChanged: {
if (atYEnd) Qt.callLater(positionViewAtEnd); // Auto-scroll when new messages are added
if (atYEnd)
Qt.callLater(positionViewAtEnd);
} }
add: null // Prevent function calls from being janky add: null // Prevent function calls from being janky
@@ -357,7 +384,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
required property int index required property int index
messageIndex: index messageIndex: index
messageData: { messageData: {
Ai.messageByID[modelData] Ai.messageByID[modelData];
} }
messageInputField: root.inputField messageInputField: root.inputField
} }
@@ -393,8 +420,8 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
Repeater { Repeater {
id: suggestionRepeater id: suggestionRepeater
model: { model: {
suggestions.selectedIndex = 0 suggestions.selectedIndex = 0;
return root.suggestionList.slice(0, 10) return root.suggestionList.slice(0, 10);
} }
delegate: ApiCommandButton { delegate: ApiCommandButton {
id: commandButton id: commandButton
@@ -413,7 +440,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
} }
} }
onClicked: { onClicked: {
suggestions.acceptSuggestion(modelData.name) suggestions.acceptSuggestion(modelData.name);
} }
} }
} }
@@ -443,14 +470,10 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
id: inputWrapper id: inputWrapper
property real spacing: 5 property real spacing: 5
Layout.fillWidth: true Layout.fillWidth: true
radius: Appearance.rounding.small radius: Appearance.rounding.normal - root.padding
color: Appearance.colors.colLayer1 color: Appearance.colors.colLayer2
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin + commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + spacing, 45) + (attachedFileIndicator.implicitHeight + spacing + attachedFileIndicator.anchors.topMargin)
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + spacing, 45)
+ (attachedFileIndicator.implicitHeight + spacing + attachedFileIndicator.anchors.topMargin)
clip: true clip: true
border.color: Appearance.colors.colOutlineVariant
border.width: 1
Behavior on implicitHeight { Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
@@ -488,121 +511,122 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
background: null background: null
onTextChanged: { // Handle suggestions onTextChanged: {
// Handle suggestions
if (messageInputField.text.length === 0) { if (messageInputField.text.length === 0) {
root.suggestionQuery = "" root.suggestionQuery = "";
root.suggestionList = [] root.suggestionList = [];
return return;
} else if (messageInputField.text.startsWith(`${root.commandPrefix}model`)) { } else if (messageInputField.text.startsWith(`${root.commandPrefix}model`)) {
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
const modelResults = Fuzzy.go(root.suggestionQuery, Ai.modelList.map(model => { const modelResults = Fuzzy.go(root.suggestionQuery, Ai.modelList.map(model => {
return { return {
name: Fuzzy.prepare(model), name: Fuzzy.prepare(model),
obj: model, obj: model
} };
}), { }), {
all: true, all: true,
key: "name" key: "name"
}) });
root.suggestionList = modelResults.map(model => { root.suggestionList = modelResults.map(model => {
return { return {
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "model ") : ""}${model.target}`, name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "model ") : ""}${model.target}`,
displayName: `${Ai.models[model.target].name}`, displayName: `${Ai.models[model.target].name}`,
description: `${Ai.models[model.target].description}`, description: `${Ai.models[model.target].description}`
} };
}) });
} else if (messageInputField.text.startsWith(`${root.commandPrefix}prompt`)) { } else if (messageInputField.text.startsWith(`${root.commandPrefix}prompt`)) {
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.promptFiles.map(file => { const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.promptFiles.map(file => {
return { return {
name: Fuzzy.prepare(file), name: Fuzzy.prepare(file),
obj: file, obj: file
} };
}), { }), {
all: true, all: true,
key: "name" key: "name"
}) });
root.suggestionList = promptFileResults.map(file => { root.suggestionList = promptFileResults.map(file => {
return { return {
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "prompt ") : ""}${file.target}`, name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "prompt ") : ""}${file.target}`,
displayName: `${FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target))}`, displayName: `${FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target))}`,
description: Translation.tr("Load prompt from %1").arg(file.target), description: Translation.tr("Load prompt from %1").arg(file.target)
} };
}) });
} else if (messageInputField.text.startsWith(`${root.commandPrefix}save`)) { } else if (messageInputField.text.startsWith(`${root.commandPrefix}save`)) {
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => { const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => {
return { return {
name: Fuzzy.prepare(file), name: Fuzzy.prepare(file),
obj: file, obj: file
} };
}), { }), {
all: true, all: true,
key: "name" key: "name"
}) });
root.suggestionList = promptFileResults.map(file => { root.suggestionList = promptFileResults.map(file => {
const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim() const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim();
return { return {
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "save ") : ""}${chatName}`, name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "save ") : ""}${chatName}`,
displayName: `${chatName}`, displayName: `${chatName}`,
description: Translation.tr("Save chat to %1").arg(chatName), description: Translation.tr("Save chat to %1").arg(chatName)
} };
}) });
} else if (messageInputField.text.startsWith(`${root.commandPrefix}load`)) { } else if (messageInputField.text.startsWith(`${root.commandPrefix}load`)) {
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => { const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => {
return { return {
name: Fuzzy.prepare(file), name: Fuzzy.prepare(file),
obj: file, obj: file
} };
}), { }), {
all: true, all: true,
key: "name" key: "name"
}) });
root.suggestionList = promptFileResults.map(file => { root.suggestionList = promptFileResults.map(file => {
const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim() const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim();
return { return {
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "load ") : ""}${chatName}`, name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "load ") : ""}${chatName}`,
displayName: `${chatName}`, displayName: `${chatName}`,
description: Translation.tr(`Load chat from %1`).arg(file.target), description: Translation.tr(`Load chat from %1`).arg(file.target)
} };
}) });
} else if (messageInputField.text.startsWith(`${root.commandPrefix}tool`)) { } else if (messageInputField.text.startsWith(`${root.commandPrefix}tool`)) {
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "" root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
const toolResults = Fuzzy.go(root.suggestionQuery, Ai.availableTools.map(tool => { const toolResults = Fuzzy.go(root.suggestionQuery, Ai.availableTools.map(tool => {
return { return {
name: Fuzzy.prepare(tool), name: Fuzzy.prepare(tool),
obj: tool, obj: tool
} };
}), { }), {
all: true, all: true,
key: "name" key: "name"
}) });
root.suggestionList = toolResults.map(tool => { root.suggestionList = toolResults.map(tool => {
const toolName = tool.target const toolName = tool.target;
return { return {
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "tool ") : ""}${tool.target}`, name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "tool ") : ""}${tool.target}`,
displayName: toolName, displayName: toolName,
description: Ai.toolDescriptions[toolName], description: Ai.toolDescriptions[toolName]
} };
}) });
} else if(messageInputField.text.startsWith(root.commandPrefix)) { } else if (messageInputField.text.startsWith(root.commandPrefix)) {
root.suggestionQuery = messageInputField.text root.suggestionQuery = messageInputField.text;
root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => { root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => {
return { return {
name: `${root.commandPrefix}${cmd.name}`, name: `${root.commandPrefix}${cmd.name}`,
description: `${cmd.description}`, description: `${cmd.description}`
} };
}) });
} }
} }
function accept() { function accept() {
root.handleInput(text) root.handleInput(text);
text = "" text = "";
} }
Keys.onPressed: (event) => { Keys.onPressed: event => {
if (event.key === Qt.Key_Tab) { if (event.key === Qt.Key_Tab) {
suggestions.acceptSelectedWord(); suggestions.acceptSelectedWord();
event.accepted = true; event.accepted = true;
@@ -615,35 +639,41 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
} else if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) { } else if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
if (event.modifiers & Qt.ShiftModifier) { if (event.modifiers & Qt.ShiftModifier) {
// Insert newline // Insert newline
messageInputField.insert(messageInputField.cursorPosition, "\n") messageInputField.insert(messageInputField.cursorPosition, "\n");
event.accepted = true event.accepted = true;
} else { // Accept text } else {
const inputText = messageInputField.text // Accept text
messageInputField.clear() const inputText = messageInputField.text;
root.handleInput(inputText) messageInputField.clear();
event.accepted = true root.handleInput(inputText);
event.accepted = true;
} }
} else if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) { // Intercept Ctrl+V to handle image/file pasting } else if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) {
if (event.modifiers & Qt.ShiftModifier) { // Let Shift+Ctrl+V = plain paste // Intercept Ctrl+V to handle image/file pasting
messageInputField.text += Quickshell.clipboardText if (event.modifiers & Qt.ShiftModifier) {
// Let Shift+Ctrl+V = plain paste
messageInputField.text += Quickshell.clipboardText;
event.accepted = true; event.accepted = true;
return; return;
} }
// Try image paste first // Try image paste first
const currentClipboardEntry = Cliphist.entries[0] const currentClipboardEntry = Cliphist.entries[0];
const cleanCliphistEntry = StringUtils.cleanCliphistEntry(currentClipboardEntry) const cleanCliphistEntry = StringUtils.cleanCliphistEntry(currentClipboardEntry);
if (/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(currentClipboardEntry)) { // First entry = currently copied entry = image? if (/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(currentClipboardEntry)) {
decodeImageAndAttachProc.handleEntry(currentClipboardEntry) // First entry = currently copied entry = image?
decodeImageAndAttachProc.handleEntry(currentClipboardEntry);
event.accepted = true; event.accepted = true;
return; return;
} else if (cleanCliphistEntry.startsWith("file://")) { // First entry = currently copied entry = image? } else if (cleanCliphistEntry.startsWith("file://")) {
const fileName = decodeURIComponent(cleanCliphistEntry) // First entry = currently copied entry = image?
const fileName = decodeURIComponent(cleanCliphistEntry);
Ai.attachFile(fileName); Ai.attachFile(fileName);
event.accepted = true; event.accepted = true;
return; return;
} }
event.accepted = false; // No image, let text pasting proceed event.accepted = false; // No image, let text pasting proceed
} else if (event.key === Qt.Key_Escape) { // Esc to detach file } else if (event.key === Qt.Key_Escape) {
// Esc to detach file
if (Ai.pendingFilePath.length > 0) { if (Ai.pendingFilePath.length > 0) {
Ai.attachFile(""); Ai.attachFile("");
event.accepted = true; event.accepted = true;
@@ -668,19 +698,18 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
anchors.fill: parent anchors.fill: parent
cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: { onClicked: {
const inputText = messageInputField.text const inputText = messageInputField.text;
root.handleInput(inputText) root.handleInput(inputText);
messageInputField.clear() messageInputField.clear();
} }
} }
contentItem: MaterialSymbol { contentItem: MaterialSymbol {
anchors.centerIn: parent anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger iconSize: 22
// fill: sendButton.enabled ? 1 : 0
color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
text: "send" text: "arrow_upward"
} }
} }
} }
@@ -699,59 +728,58 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
{ {
name: "", name: "",
sendDirectly: false, sendDirectly: false,
dontAddSpace: true, dontAddSpace: true
}, },
{ {
name: "clear", name: "clear",
sendDirectly: true, sendDirectly: true
}, },
] ]
ApiInputBoxIndicator { // Model indicator ApiInputBoxIndicator {
// Model indicator
icon: "api" icon: "api"
text: Ai.getModel().name text: Ai.getModel().name
tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL") tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL").arg(Ai.getModel().name).arg(root.commandPrefix)
.arg(Ai.getModel().name)
.arg(root.commandPrefix)
} }
ApiInputBoxIndicator { // Tool indicator ApiInputBoxIndicator {
// Tool indicator
icon: "service_toolbox" icon: "service_toolbox"
text: Ai.currentTool.charAt(0).toUpperCase() + Ai.currentTool.slice(1) text: Ai.currentTool.charAt(0).toUpperCase() + Ai.currentTool.slice(1)
tooltipText: Translation.tr("Current tool: %1\nSet it with %2tool TOOL") tooltipText: Translation.tr("Current tool: %1\nSet it with %2tool TOOL").arg(Ai.currentTool).arg(root.commandPrefix)
.arg(Ai.currentTool)
.arg(root.commandPrefix)
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
ButtonGroup { // Command buttons ButtonGroup {
// Command buttons
padding: 0 padding: 0
Repeater { // Command buttons Repeater {
// Command buttons
model: commandButtonsRow.commandsShown model: commandButtonsRow.commandsShown
delegate: ApiCommandButton { delegate: ApiCommandButton {
property string commandRepresentation: `${root.commandPrefix}${modelData.name}` property string commandRepresentation: `${root.commandPrefix}${modelData.name}`
buttonText: commandRepresentation buttonText: commandRepresentation
downAction: () => { downAction: () => {
if (modelData.sendDirectly) { if (modelData.sendDirectly) {
root.handleInput(commandRepresentation) root.handleInput(commandRepresentation);
} else { } else {
messageInputField.text = commandRepresentation + (modelData.dontAddSpace ? "" : " ") messageInputField.text = commandRepresentation + (modelData.dontAddSpace ? "" : " ");
messageInputField.cursorPosition = messageInputField.text.length messageInputField.cursorPosition = messageInputField.text.length;
messageInputField.forceActiveFocus() messageInputField.forceActiveFocus();
} }
if (modelData.name === "clear") { if (modelData.name === "clear") {
messageInputField.text = "" messageInputField.text = "";
} }
} }
} }
} }
} }
} }
} }
} }
} }
@@ -12,6 +12,8 @@ import Quickshell
Item { Item {
id: root id: root
property real padding: 4
property var inputField: tagInputField property var inputField: tagInputField
readonly property var responses: Booru.responses readonly property var responses: Booru.responses
property string previewDownloadPath: Directories.booruPreviews property string previewDownloadPath: Directories.booruPreviews
@@ -141,7 +143,11 @@ Item {
ColumnLayout { ColumnLayout {
id: columnLayout id: columnLayout
anchors.fill: parent anchors {
fill: parent
margins: root.padding
}
spacing: root.padding
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
@@ -317,14 +323,12 @@ Item {
id: tagInputContainer id: tagInputContainer
property real columnSpacing: 5 property real columnSpacing: 5
Layout.fillWidth: true Layout.fillWidth: true
radius: Appearance.rounding.small radius: Appearance.rounding.normal - root.padding
color: Appearance.colors.colLayer1 color: Appearance.colors.colLayer2
implicitWidth: tagInputField.implicitWidth implicitWidth: tagInputField.implicitWidth
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45) + commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45)
clip: true clip: true
border.color: Appearance.colors.colOutlineVariant
border.width: 1
Behavior on implicitHeight { Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
@@ -456,10 +460,9 @@ Item {
contentItem: MaterialSymbol { contentItem: MaterialSymbol {
anchors.centerIn: parent anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
iconSize: Appearance.font.pixelSize.larger iconSize: 22
// fill: sendButton.enabled ? 1 : 0
color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
text: "send" text: "arrow_upward"
} }
} }
} }
@@ -37,14 +37,6 @@ Item {
swipeView.decrementCurrentIndex() swipeView.decrementCurrentIndex()
event.accepted = true; event.accepted = true;
} }
else if (event.key === Qt.Key_Tab) {
swipeView.setCurrentIndex((swipeView.currentIndex + 1) % swipeView.count);
event.accepted = true;
}
else if (event.key === Qt.Key_Backtab) {
swipeView.setCurrentIndex((swipeView.currentIndex - 1 + swipeView.count) % swipeView.count);
event.accepted = true;
}
} }
} }
@@ -66,29 +58,36 @@ Item {
} }
} }
SwipeView { // Content pages Rectangle {
id: swipeView
Layout.topMargin: 5
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
spacing: 10 implicitWidth: swipeView.implicitWidth
currentIndex: tabBar.currentIndex implicitHeight: swipeView.implicitHeight
radius: Appearance.rounding.normal
color: Appearance.colors.colLayer1
clip: true SwipeView { // Content pages
layer.enabled: true id: swipeView
layer.effect: OpacityMask { anchors.fill: parent
maskSource: Rectangle { spacing: 10
width: swipeView.width currentIndex: tabBar.currentIndex
height: swipeView.height
radius: Appearance.rounding.small clip: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: swipeView.width
height: swipeView.height
radius: Appearance.rounding.small
}
} }
}
contentChildren: [ contentChildren: [
...((root.aiChatEnabled || (!root.translatorEnabled && !root.animeEnabled)) ? [aiChat.createObject()] : []), ...((root.aiChatEnabled || (!root.translatorEnabled && !root.animeEnabled)) ? [aiChat.createObject()] : []),
...(root.translatorEnabled ? [translator.createObject()] : []), ...(root.translatorEnabled ? [translator.createObject()] : []),
...(root.animeEnabled ? [anime.createObject()] : []) ...(root.animeEnabled ? [anime.createObject()] : [])
] ]
}
} }
Component { Component {
@@ -13,17 +13,24 @@ import Quickshell.Io
*/ */
Item { Item {
id: root id: root
// Sizes
property real padding: 4
// Widgets // Widgets
property var inputField: inputCanvas.inputTextArea property var inputField: inputCanvas.inputTextArea
// Widget variables // Widget variables
property bool translationFor: false // Indicates if the translation is for an autocorrected text property bool translationFor: false // Indicates if the translation is for an autocorrected text
property string translatedText: "" property string translatedText: ""
property list<string> languages: [] property list<string> languages: []
// Options // Options
property string targetLanguage: Config.options.language.translator.targetLanguage property string targetLanguage: Config.options.language.translator.targetLanguage
property string sourceLanguage: Config.options.language.translator.sourceLanguage property string sourceLanguage: Config.options.language.translator.sourceLanguage
property string hostLanguage: targetLanguage property string hostLanguage: targetLanguage
// States
property bool showLanguageSelector: false property bool showLanguageSelector: false
property bool languageSelectorTarget: false // true for target language, false for source language property bool languageSelectorTarget: false // true for target language, false for source language
@@ -99,7 +106,11 @@ Item {
} }
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors {
fill: parent
margins: root.padding
}
StyledFlickable { StyledFlickable {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@@ -17,10 +17,8 @@ Rectangle {
default property alias actionButtons: actions.data default property alias actionButtons: actions.data
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: Math.max(150, inputColumn.implicitHeight) implicitHeight: Math.max(150, inputColumn.implicitHeight)
color: isInput ? Appearance.colors.colLayer1 : Appearance.colors.colSurfaceContainer color: Appearance.colors.colLayer2
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
border.color: isInput ? Appearance.colors.colOutlineVariant : "transparent"
border.width: isInput ? 1 : 0
signal inputTextChanged(); // Signal emitted when text changes signal inputTextChanged(); // Signal emitted when text changes