forked from Shinonome/dots-hyprland
ai chat
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
import "root:/"
|
||||
import "root:/services"
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "./aiChat/"
|
||||
import "root:/modules/common/functions/string_utils.js" as StringUtils
|
||||
import Qt.labs.platform
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var panelWindow
|
||||
property var inputField: messageInputField
|
||||
readonly property var messages: Ai.messages
|
||||
property string commandPrefix: "/"
|
||||
property real scrollOnNewResponse: 60
|
||||
|
||||
Connections {
|
||||
target: panelWindow
|
||||
function onVisibleChanged(visible) {
|
||||
messageInputField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
onFocusChanged: (focus) => {
|
||||
if (focus) {
|
||||
messageInputField.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: "clear",
|
||||
description: qsTr("Clear chat history"),
|
||||
execute: () => {
|
||||
Ai.clearMessages();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "model",
|
||||
description: qsTr("Choose model"),
|
||||
execute: (args) => {
|
||||
Ai.setModel(args[0]);
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
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, "interface");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Ai.sendUserMessage(inputText);
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
ListView { // Messages
|
||||
id: messageListView
|
||||
anchors.fill: parent
|
||||
|
||||
property int lastResponseLength: 0
|
||||
|
||||
clip: true
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: swipeView.width
|
||||
height: swipeView.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on contentY {
|
||||
NumberAnimation {
|
||||
id: scrollAnim
|
||||
duration: Appearance.animation.scroll.duration
|
||||
easing.type: Appearance.animation.scroll.type
|
||||
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
spacing: 10
|
||||
model: ScriptModel {
|
||||
values: {
|
||||
if(root.messages.length > messageListView.lastResponseLength) {
|
||||
if (messageListView.lastResponseLength > 0 && root.messages[messageListView.lastResponseLength].provider != "system")
|
||||
messageListView.contentY = messageListView.contentY + root.scrollOnNewResponse
|
||||
messageListView.lastResponseLength = root.messages.length
|
||||
}
|
||||
return root.messages
|
||||
}
|
||||
// values: root.messages
|
||||
}
|
||||
delegate: AiMessage {
|
||||
messageData: modelData
|
||||
messageInputField: root.inputField
|
||||
}
|
||||
}
|
||||
|
||||
Item { // Placeholder when list is empty
|
||||
opacity: root.messages.length === 0 ? 1 : 0
|
||||
visible: opacity > 0
|
||||
anchors.fill: parent
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
MaterialSymbol {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pixelSize: 55
|
||||
color: Appearance.m3colors.m3outline
|
||||
text: "neurology"
|
||||
}
|
||||
StyledText {
|
||||
id: widgetNameText
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.m3colors.m3outline
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("Large language models")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { // Tag input area
|
||||
id: tagInputContainer
|
||||
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.m3colors.m3outlineVariant
|
||||
border.width: 1
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
TextArea { // The actual TextArea
|
||||
id: messageInputField
|
||||
wrapMode: TextArea.Wrap
|
||||
Layout.fillWidth: true
|
||||
padding: 10
|
||||
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
|
||||
renderType: Text.NativeRendering
|
||||
selectedTextColor: Appearance.m3colors.m3onPrimary
|
||||
selectionColor: Appearance.m3colors.m3primary
|
||||
placeholderText: StringUtils.format(qsTr('Message the model... "{0}" for commands'), root.commandPrefix)
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
|
||||
background: Item {}
|
||||
|
||||
function accept() {
|
||||
root.handleInput(text)
|
||||
text = ""
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
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
|
||||
root.handleInput(inputText)
|
||||
messageInputField.clear()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button { // Send button
|
||||
id: sendButton
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.rightMargin: 5
|
||||
implicitWidth: 40
|
||||
implicitHeight: 40
|
||||
enabled: messageInputField.text.length > 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: sendButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
const inputText = messageInputField.text
|
||||
root.handleInput(inputText)
|
||||
messageInputField.clear()
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
radius: Appearance.rounding.small
|
||||
color: sendButton.enabled ? (sendButton.down ? Appearance.colors.colPrimaryActive :
|
||||
sendButton.hovered ? Appearance.colors.colPrimaryHover :
|
||||
Appearance.m3colors.m3primary) : Appearance.colors.colLayer2Disabled
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
text: "send"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
color: sendButton.enabled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
font.pixelSize: Appearance.font.pixelSize.large
|
||||
}
|
||||
StyledText {
|
||||
id: providerName
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
font.weight: Font.DemiBold
|
||||
color: Appearance.m3colors.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
text: Ai.models[Ai.currentModel].name
|
||||
}
|
||||
}
|
||||
StyledToolTip {
|
||||
id: toolTip
|
||||
extraVisibleCondition: false
|
||||
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
|
||||
// content: qsTr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + qsTr("\nSet with /mode PROVIDER")
|
||||
content: StringUtils.format(qsTr("Current model: {0}\nSet it with {1}model MODEL"),
|
||||
Ai.models[Ai.currentModel].name, root.commandPrefix)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Repeater { // Command buttons
|
||||
id: commandRepeater
|
||||
model: commandButtonsRow.commandsShown
|
||||
delegate: ApiCommandButton {
|
||||
id: tagButton
|
||||
property string commandRepresentation: `${root.commandPrefix}${modelData.name}`
|
||||
buttonText: commandRepresentation
|
||||
background: Rectangle {
|
||||
radius: Appearance.rounding.small
|
||||
color: tagButton.down ? Appearance.colors.colLayer2Active :
|
||||
tagButton.hovered ? Appearance.colors.colLayer2Hover :
|
||||
Appearance.colors.colLayer2
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.animation.elementMove.duration
|
||||
easing.type: Appearance.animation.elementMove.type
|
||||
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
if(modelData.sendDirectly) {
|
||||
root.handleInput(commandRepresentation)
|
||||
} else {
|
||||
messageInputField.text = commandRepresentation + " "
|
||||
messageInputField.cursorPosition = messageInputField.text.length
|
||||
messageInputField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -91,7 +91,7 @@ Item {
|
||||
if (commandObj) {
|
||||
commandObj.execute(args);
|
||||
} else {
|
||||
root.addSystemMessage(qsTr("Unknown command: ") + command);
|
||||
Booru.addSystemMessage(qsTr("Unknown command: ") + command);
|
||||
}
|
||||
}
|
||||
else if (inputText.trim() == "+") {
|
||||
@@ -121,6 +121,11 @@ Item {
|
||||
tagInputField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
onFocusChanged: (focus) => {
|
||||
if (focus) {
|
||||
tagInputField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
tagInputField.forceActiveFocus()
|
||||
@@ -135,11 +140,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onFocusChanged: (focus) => {
|
||||
if (focus) {
|
||||
tagInputField.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
@@ -311,7 +311,7 @@ Item {
|
||||
tagSuggestions.selectedIndex = 0
|
||||
return root.suggestionList.slice(0, 10)
|
||||
}
|
||||
delegate: BooruTagButton {
|
||||
delegate: ApiCommandButton {
|
||||
id: tagButton
|
||||
|
||||
background: Rectangle {
|
||||
@@ -419,7 +419,7 @@ Item {
|
||||
|
||||
background: Item {}
|
||||
|
||||
property Timer searchTimer: Timer {
|
||||
property Timer searchTimer: Timer { // Timer for tag suggestions
|
||||
interval: root.tagSuggestionDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
@@ -431,7 +431,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
onTextChanged: { // Handle tag suggestions
|
||||
if(tagInputField.text.length === 0) {
|
||||
root.suggestionQuery = ""
|
||||
root.suggestionList = []
|
||||
@@ -618,7 +618,6 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
PointingHandInteraction {}
|
||||
onClicked: {
|
||||
@@ -653,7 +652,7 @@ Item {
|
||||
Repeater { // Command buttons
|
||||
id: commandRepeater
|
||||
model: commandButtonsRow.commandsShown
|
||||
delegate: BooruTagButton {
|
||||
delegate: ApiCommandButton {
|
||||
id: tagButton
|
||||
property string commandRepresentation: `${root.commandPrefix}${modelData.name}`
|
||||
buttonText: commandRepresentation
|
||||
|
||||
@@ -140,9 +140,8 @@ Scope { // Scope
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "To be implemented"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
AiChat {
|
||||
panelWindow: sidebarRoot
|
||||
}
|
||||
Anime {
|
||||
panelWindow: sidebarRoot
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import "root:/"
|
||||
import "root:/services"
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "../"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property var messageData
|
||||
property var messageInputField
|
||||
|
||||
property real availableWidth: parent.width ?? 0
|
||||
property real messagePadding: 7
|
||||
property real contentSpacing: 3
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
implicitHeight: columnLayout.implicitHeight + root.messagePadding * 2
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
ColumnLayout {
|
||||
id: columnLayout
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: messagePadding
|
||||
spacing: root.contentSpacing
|
||||
|
||||
RowLayout { // Header
|
||||
Rectangle { // Name
|
||||
id: nameWrapper
|
||||
color: Appearance.m3colors.m3secondaryContainer
|
||||
radius: Appearance.rounding.small
|
||||
implicitWidth: providerName.implicitWidth + 10 * 2
|
||||
implicitHeight: Math.max(providerName.implicitHeight + 5 * 2, 30)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
StyledText {
|
||||
id: providerName
|
||||
anchors.centerIn: parent
|
||||
font.pixelSize: Appearance.font.pixelSize.large
|
||||
font.weight: Font.DemiBold
|
||||
color: Appearance.m3colors.m3onSecondaryContainer
|
||||
text: messageData.role == 'assistant' ? Ai.models[messageData.model].name :
|
||||
messageData.role == 'user' ? "User" :
|
||||
"System"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText { // Message
|
||||
id: messageText
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: messagePadding
|
||||
|
||||
// font.family: Appearance.font.family.reading
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
wrapMode: Text.WordWrap
|
||||
color: Appearance.colors.colOnLayer1
|
||||
textFormat: Text.MarkdownText
|
||||
text: root.messageData.content
|
||||
|
||||
onLinkActivated: (link) => {
|
||||
Qt.openUrlExternally(link)
|
||||
Hyprland.dispatch("global quickshell:sidebarLeftClose")
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton // Only for hover
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.hoveredLink !== "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import "root:/"
|
||||
import "root:/services"
|
||||
import "root:/modules/common"
|
||||
import "root:/modules/common/widgets"
|
||||
import "../"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
@@ -21,10 +22,6 @@ Rectangle {
|
||||
property string downloadPath
|
||||
property string nsfwPath
|
||||
|
||||
onResponseDataChanged: {
|
||||
console.log("Response data changed:", responseData)
|
||||
}
|
||||
|
||||
property real availableWidth: parent.width ?? 0
|
||||
property real rowTooShortThreshold: 185
|
||||
property real imageSpacing: 5
|
||||
@@ -126,7 +123,7 @@ Rectangle {
|
||||
id: tagRepeater
|
||||
model: root.responseData.tags
|
||||
|
||||
BooruTagButton {
|
||||
ApiCommandButton {
|
||||
Layout.fillWidth: false
|
||||
buttonText: modelData
|
||||
onClicked: {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/modules/common"
|
||||
import Quickshell;
|
||||
import Quickshell.Io;
|
||||
import Qt.labs.platform
|
||||
import QtQuick;
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property Component aiMessageComponent: AiMessageData {}
|
||||
property var messages: []
|
||||
property var modelList: ["ollama-llama-3.2", "gemini-2.0-flash"]
|
||||
property var models: { // TODO: Auto-detect installed ollama models
|
||||
"interface": {
|
||||
"name": "System",
|
||||
},
|
||||
"ollama-llama-3.2": {
|
||||
"name": "Ollama - Llama 3.2",
|
||||
"icon": "ollama-symbolic",
|
||||
"description": "Ollama - Llama 3.2",
|
||||
"endpoint": "http://localhost:11434/api/chat",
|
||||
"model": "llama3.2",
|
||||
},
|
||||
"gemini-2.0-flash": {
|
||||
"name": "Gemini 2.0 Flash",
|
||||
"icon": "gemini-symbolic",
|
||||
"description": "Gemini 2.0 Flash",
|
||||
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
|
||||
"model": "gemini-2.0-flash",
|
||||
"messageMapFunc": function (message) {
|
||||
return {
|
||||
"role": message.role,
|
||||
"parts": [{text: message.content}],
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
property var currentModel: "ollama-llama-3.2"
|
||||
|
||||
function addMessage(message, role) {
|
||||
if (message.length === 0) return;
|
||||
const aiMessage = aiMessageComponent.createObject(root, {
|
||||
"role": role,
|
||||
"content": message,
|
||||
"thinking": false,
|
||||
"done": true,
|
||||
});
|
||||
root.messages = [...root.messages, aiMessage];
|
||||
}
|
||||
|
||||
function setModel(model) {
|
||||
if (!model) model = ""
|
||||
model = model.toLowerCase()
|
||||
if (modelList.indexOf(model) !== -1) {
|
||||
currentModel = model
|
||||
root.addMessage("Model set to " + models[model].name, "interface")
|
||||
} else {
|
||||
root.addMessage(qsTr("Invalid model. Supported: \n- ") + modelList.join("\n- "), "interface")
|
||||
}
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
Process {
|
||||
id: requester
|
||||
property var baseCommand: ["curl", "--no-buffer"]
|
||||
property var message
|
||||
|
||||
function makeRequest() {
|
||||
const model = models[currentModel];
|
||||
|
||||
let endpoint = model.endpoint;
|
||||
|
||||
// Build request data using OpenAI's format. If the model has a custom requestDataBuilder, use that instead.
|
||||
let data = model.requestDataBuilder ? model.requestDataBuilder(root.messages.filter(message => (message.role != "interface"))) : {
|
||||
"model": model.model,
|
||||
"messages": root.messages.filter(message => (message.role != "interface")).map(message => {
|
||||
return { // Remove unecessary properties
|
||||
"role": message.role,
|
||||
"content": message.content,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
let requestHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
// "Authorization": model.endpoint.startsWith("http") ? "Bearer " + model.apiKey : "",
|
||||
}
|
||||
|
||||
requester.message = root.aiMessageComponent.createObject(root, {
|
||||
"role": "assistant",
|
||||
"model": currentModel,
|
||||
"content": "",
|
||||
"thinking": true,
|
||||
"done": false,
|
||||
});
|
||||
root.messages = [...root.messages, requester.message];
|
||||
requester.command = baseCommand.concat([endpoint, "-d", JSON.stringify(data)]);
|
||||
console.log("Request command: ", requester.command.join(" "));
|
||||
requester.running = true
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
// console.log("Received data: ", data);
|
||||
if (data.length === 0) return;
|
||||
const dataJson = JSON.parse(data);
|
||||
if (requester.message.thinking) requester.message.thinking = false;
|
||||
|
||||
requester.message.content += dataJson.message.content
|
||||
|
||||
if (dataJson.done) requester.message.done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendUserMessage(message) {
|
||||
if (message.length === 0) return;
|
||||
|
||||
const userMessage = aiMessageComponent.createObject(root, {
|
||||
"role": "user",
|
||||
"content": message,
|
||||
"thinking": false,
|
||||
"done": true,
|
||||
});
|
||||
root.messages = [...root.messages, userMessage];
|
||||
|
||||
requester.makeRequest();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import "root:/modules/common"
|
||||
import QtQuick;
|
||||
|
||||
QtObject {
|
||||
property string role
|
||||
property string content
|
||||
property string model
|
||||
property bool thinking: true
|
||||
property bool done: false
|
||||
}
|
||||
Reference in New Issue
Block a user