forked from Shinonome/dots-hyprland
feat(ai): auto-resize input with scroll (#3148)
This commit is contained in:
@@ -497,199 +497,206 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
|||||||
RowLayout { // Input field and send button
|
RowLayout { // Input field and send button
|
||||||
id: inputFieldRowLayout
|
id: inputFieldRowLayout
|
||||||
anchors {
|
anchors {
|
||||||
top: attachedFileIndicator.bottom
|
bottom: commandButtonsRow.top
|
||||||
left: parent.left
|
left: parent.left
|
||||||
right: parent.right
|
right: parent.right
|
||||||
topMargin: 5
|
bottomMargin: 5
|
||||||
}
|
}
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
StyledTextArea { // The actual TextArea
|
ScrollView {
|
||||||
id: messageInputField
|
id: inputScrollView
|
||||||
wrapMode: TextArea.Wrap
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
padding: 10
|
Layout.preferredHeight: Math.min(root.height * 3/5, messageInputField.height)
|
||||||
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
|
clip: true
|
||||||
placeholderText: Translation.tr('Message the model... "%1" for commands').arg(root.commandPrefix)
|
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||||
|
|
||||||
background: null
|
StyledTextArea { // The actual TextArea (inside ScrollView to enable scrolling)
|
||||||
|
id: messageInputField
|
||||||
|
anchors.fill: parent
|
||||||
|
wrapMode: TextArea.Wrap
|
||||||
|
padding: 10
|
||||||
|
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
|
||||||
|
placeholderText: Translation.tr('Message the model... "%1" for commands').arg(root.commandPrefix)
|
||||||
|
|
||||||
onTextChanged: {
|
background: null
|
||||||
// Handle suggestions
|
|
||||||
if (messageInputField.text.length === 0) {
|
onTextChanged: {
|
||||||
root.suggestionQuery = "";
|
// Handle suggestions
|
||||||
root.suggestionList = [];
|
if (messageInputField.text.length === 0) {
|
||||||
return;
|
root.suggestionQuery = "";
|
||||||
} else if (messageInputField.text.startsWith(`${root.commandPrefix}model`)) {
|
root.suggestionList = [];
|
||||||
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
|
return;
|
||||||
const modelResults = Fuzzy.go(root.suggestionQuery, Ai.modelList.map(model => {
|
} else if (messageInputField.text.startsWith(`${root.commandPrefix}model`)) {
|
||||||
return {
|
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
|
||||||
name: Fuzzy.prepare(model),
|
const modelResults = Fuzzy.go(root.suggestionQuery, Ai.modelList.map(model => {
|
||||||
obj: model
|
return {
|
||||||
};
|
name: Fuzzy.prepare(model),
|
||||||
}), {
|
obj: model
|
||||||
all: true,
|
};
|
||||||
key: "name"
|
}), {
|
||||||
});
|
all: true,
|
||||||
root.suggestionList = modelResults.map(model => {
|
key: "name"
|
||||||
return {
|
});
|
||||||
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "model ") : ""}${model.target}`,
|
root.suggestionList = modelResults.map(model => {
|
||||||
displayName: `${Ai.models[model.target].name}`,
|
return {
|
||||||
description: `${Ai.models[model.target].description}`
|
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 => {
|
} else if (messageInputField.text.startsWith(`${root.commandPrefix}prompt`)) {
|
||||||
return {
|
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
|
||||||
name: Fuzzy.prepare(file),
|
const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.promptFiles.map(file => {
|
||||||
obj: file
|
return {
|
||||||
};
|
name: Fuzzy.prepare(file),
|
||||||
}), {
|
obj: file
|
||||||
all: true,
|
};
|
||||||
key: "name"
|
}), {
|
||||||
});
|
all: true,
|
||||||
root.suggestionList = promptFileResults.map(file => {
|
key: "name"
|
||||||
return {
|
});
|
||||||
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "prompt ") : ""}${file.target}`,
|
root.suggestionList = promptFileResults.map(file => {
|
||||||
displayName: `${FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target))}`,
|
return {
|
||||||
description: Translation.tr("Load prompt from %1").arg(file.target)
|
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 => {
|
} else if (messageInputField.text.startsWith(`${root.commandPrefix}save`)) {
|
||||||
return {
|
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
|
||||||
name: Fuzzy.prepare(file),
|
const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => {
|
||||||
obj: file
|
return {
|
||||||
};
|
name: Fuzzy.prepare(file),
|
||||||
}), {
|
obj: file
|
||||||
all: true,
|
};
|
||||||
key: "name"
|
}), {
|
||||||
});
|
all: true,
|
||||||
root.suggestionList = promptFileResults.map(file => {
|
key: "name"
|
||||||
const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim();
|
});
|
||||||
return {
|
root.suggestionList = promptFileResults.map(file => {
|
||||||
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "save ") : ""}${chatName}`,
|
const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim();
|
||||||
displayName: `${chatName}`,
|
return {
|
||||||
description: Translation.tr("Save chat to %1").arg(chatName)
|
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 => {
|
} else if (messageInputField.text.startsWith(`${root.commandPrefix}load`)) {
|
||||||
return {
|
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
|
||||||
name: Fuzzy.prepare(file),
|
const promptFileResults = Fuzzy.go(root.suggestionQuery, Ai.savedChats.map(file => {
|
||||||
obj: file
|
return {
|
||||||
};
|
name: Fuzzy.prepare(file),
|
||||||
}), {
|
obj: file
|
||||||
all: true,
|
};
|
||||||
key: "name"
|
}), {
|
||||||
});
|
all: true,
|
||||||
root.suggestionList = promptFileResults.map(file => {
|
key: "name"
|
||||||
const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim();
|
});
|
||||||
return {
|
root.suggestionList = promptFileResults.map(file => {
|
||||||
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "load ") : ""}${chatName}`,
|
const chatName = FileUtils.trimFileExt(FileUtils.fileNameForPath(file.target)).trim();
|
||||||
displayName: `${chatName}`,
|
return {
|
||||||
description: Translation.tr(`Load chat from %1`).arg(file.target)
|
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 => {
|
} else if (messageInputField.text.startsWith(`${root.commandPrefix}tool`)) {
|
||||||
return {
|
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? "";
|
||||||
name: Fuzzy.prepare(tool),
|
const toolResults = Fuzzy.go(root.suggestionQuery, Ai.availableTools.map(tool => {
|
||||||
obj: tool
|
return {
|
||||||
};
|
name: Fuzzy.prepare(tool),
|
||||||
}), {
|
obj: tool
|
||||||
all: true,
|
};
|
||||||
key: "name"
|
}), {
|
||||||
});
|
all: true,
|
||||||
root.suggestionList = toolResults.map(tool => {
|
key: "name"
|
||||||
const toolName = tool.target;
|
});
|
||||||
return {
|
root.suggestionList = toolResults.map(tool => {
|
||||||
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "tool ") : ""}${tool.target}`,
|
const toolName = tool.target;
|
||||||
displayName: toolName,
|
return {
|
||||||
description: Ai.toolDescriptions[toolName]
|
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 => {
|
} else if (messageInputField.text.startsWith(root.commandPrefix)) {
|
||||||
return {
|
root.suggestionQuery = messageInputField.text;
|
||||||
name: `${root.commandPrefix}${cmd.name}`,
|
root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => {
|
||||||
description: `${cmd.description}`
|
return {
|
||||||
};
|
name: `${root.commandPrefix}${cmd.name}`,
|
||||||
});
|
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;
|
|
||||||
} 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;
|
event.accepted = true;
|
||||||
} else {
|
} else if (event.key === Qt.Key_Up && suggestions.visible) {
|
||||||
// Accept text
|
suggestions.selectedIndex = Math.max(0, suggestions.selectedIndex - 1);
|
||||||
const inputText = messageInputField.text;
|
|
||||||
messageInputField.clear();
|
|
||||||
root.handleInput(inputText);
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
} else if (event.key === Qt.Key_Down && suggestions.visible) {
|
||||||
} else if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) {
|
suggestions.selectedIndex = Math.min(root.suggestionList.length - 1, suggestions.selectedIndex + 1);
|
||||||
// Intercept Ctrl+V to handle image/file pasting
|
|
||||||
if (event.modifiers & Qt.ShiftModifier) {
|
|
||||||
// Let Shift+Ctrl+V = plain paste
|
|
||||||
messageInputField.text += Quickshell.clipboardText;
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
return;
|
} else if ((event.key === Qt.Key_Enter || event.key === Qt.Key_Return)) {
|
||||||
}
|
if (event.modifiers & Qt.ShiftModifier) {
|
||||||
// Try image paste first
|
// Insert newline
|
||||||
const currentClipboardEntry = Cliphist.entries[0];
|
messageInputField.insert(messageInputField.cursorPosition, "\n");
|
||||||
const cleanCliphistEntry = StringUtils.cleanCliphistEntry(currentClipboardEntry);
|
event.accepted = true;
|
||||||
if (/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(currentClipboardEntry)) {
|
} else {
|
||||||
// First entry = currently copied entry = image?
|
// Accept text
|
||||||
decodeImageAndAttachProc.handleEntry(currentClipboardEntry);
|
const inputText = messageInputField.text;
|
||||||
event.accepted = true;
|
messageInputField.clear();
|
||||||
return;
|
root.handleInput(inputText);
|
||||||
} else if (cleanCliphistEntry.startsWith("file://")) {
|
event.accepted = true;
|
||||||
// First entry = currently copied entry = image?
|
}
|
||||||
const fileName = decodeURIComponent(cleanCliphistEntry);
|
} else if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) {
|
||||||
Ai.attachFile(fileName);
|
// Intercept Ctrl+V to handle image/file pasting
|
||||||
event.accepted = true;
|
if (event.modifiers & Qt.ShiftModifier) {
|
||||||
return;
|
// Let Shift+Ctrl+V = plain paste
|
||||||
}
|
messageInputField.text += Quickshell.clipboardText;
|
||||||
event.accepted = false; // No image, let text pasting proceed
|
event.accepted = true;
|
||||||
} else if (event.key === Qt.Key_Escape) {
|
return;
|
||||||
// Esc to detach file
|
}
|
||||||
if (Ai.pendingFilePath.length > 0) {
|
// Try image paste first
|
||||||
Ai.attachFile("");
|
const currentClipboardEntry = Cliphist.entries[0];
|
||||||
event.accepted = true;
|
const cleanCliphistEntry = StringUtils.cleanCliphistEntry(currentClipboardEntry);
|
||||||
} else {
|
if (/^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(currentClipboardEntry)) {
|
||||||
event.accepted = false;
|
// First entry = currently copied entry = image?
|
||||||
|
decodeImageAndAttachProc.handleEntry(currentClipboardEntry);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
} else if (cleanCliphistEntry.startsWith("file://")) {
|
||||||
|
// First entry = currently copied entry = image?
|
||||||
|
const fileName = decodeURIComponent(cleanCliphistEntry);
|
||||||
|
Ai.attachFile(fileName);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.accepted = false; // No image, let text pasting proceed
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
// Esc to detach file
|
||||||
|
if (Ai.pendingFilePath.length > 0) {
|
||||||
|
Ai.attachFile("");
|
||||||
|
event.accepted = true;
|
||||||
|
} else {
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RippleButton { // Send button
|
RippleButton { // Send button
|
||||||
id: sendButton
|
id: sendButton
|
||||||
Layout.alignment: Qt.AlignTop
|
Layout.alignment: Qt.AlignBottom
|
||||||
Layout.rightMargin: 5
|
Layout.rightMargin: 5
|
||||||
implicitWidth: 40
|
implicitWidth: 40
|
||||||
implicitHeight: 40
|
implicitHeight: 40
|
||||||
|
|||||||
Reference in New Issue
Block a user