From f93cca8a134fbc1cf56dfb233608c0ba69a072c6 Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 10 May 2025 10:56:35 +0200 Subject: [PATCH] ai: gemini: annotation sources --- .../modules/common/functions/string_utils.js | 5 ++ .../quickshell/modules/sidebarLeft/AiChat.qml | 6 ++ .../modules/sidebarLeft/aiChat/AiMessage.qml | 24 +++++- .../aiChat/AnnotationSourceButton.qml | 73 +++++++++++++++++++ .config/quickshell/services/Ai.qml | 22 ++++++ .config/quickshell/services/AiMessageData.qml | 2 + 6 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 .config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml diff --git a/.config/quickshell/modules/common/functions/string_utils.js b/.config/quickshell/modules/common/functions/string_utils.js index d36066235..e52f167dc 100644 --- a/.config/quickshell/modules/common/functions/string_utils.js +++ b/.config/quickshell/modules/common/functions/string_utils.js @@ -9,6 +9,11 @@ function getDomain(url) { return match ? match[1] : null; } +function getBaseUrl(url) { + const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/); + return match ? match[1] : null; +} + function shellSingleQuoteEscape(str) { // escape single quotes return String(str) diff --git a/.config/quickshell/modules/sidebarLeft/AiChat.qml b/.config/quickshell/modules/sidebarLeft/AiChat.qml index 7635b291d..77aac582a 100644 --- a/.config/quickshell/modules/sidebarLeft/AiChat.qml +++ b/.config/quickshell/modules/sidebarLeft/AiChat.qml @@ -20,10 +20,15 @@ Item { property var inputField: messageInputField readonly property var messages: Ai.messages property string commandPrefix: "/" + property string faviconDownloadPath: StringUtils.trimFileProtocol(`${StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]}/media/favicons`) property var suggestionQuery: "" property var suggestionList: [] + Component.onCompleted: { + Hyprland.dispatch(`exec mkdir -p ${faviconDownloadPath}`) + } + Connections { target: panelWindow function onVisibleChanged(visible) { @@ -205,6 +210,7 @@ int main(int argc, char* argv[]) { messageIndex: index messageData: modelData messageInputField: root.inputField + faviconDownloadPath: root.faviconDownloadPath } } diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml index 9da0b4cc8..7418e31ab 100644 --- a/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml +++ b/.config/quickshell/modules/sidebarLeft/aiChat/AiMessage.qml @@ -20,6 +20,7 @@ Rectangle { property int messageIndex property var messageData property var messageInputField + property string faviconDownloadPath property real messagePadding: 7 property real contentSpacing: 3 @@ -74,7 +75,7 @@ Rectangle { } } - ColumnLayout { + ColumnLayout { // Main layout of the whole thing id: columnLayout anchors.left: parent.left @@ -228,7 +229,7 @@ Rectangle { } } - ColumnLayout { + ColumnLayout { // Message content id: messageContentColumnLayout spacing: 0 @@ -257,6 +258,25 @@ Rectangle { } } + Flow { // Annotations + id: annotationFlowLayout + visible: root.messageData?.annotationSources?.length > 0 + spacing: 5 + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + + Repeater { + model: root.messageData.annotationSources + delegate: AnnotationSourceButton { + id: annotationButton + faviconDownloadPath: root.faviconDownloadPath + displayText: modelData.text + url: modelData.url + } + } + + } + } } diff --git a/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml b/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml new file mode 100644 index 000000000..d60077851 --- /dev/null +++ b/.config/quickshell/modules/sidebarLeft/aiChat/AnnotationSourceButton.qml @@ -0,0 +1,73 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import Qt.labs.platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Hyprland + +Button { + id: root + property string displayText + property string url + + implicitHeight: 30 + leftPadding: 10 + rightPadding: 10 + + property string downloadUserAgent: ConfigOptions.networking.userAgent + property string faviconDownloadPath + property string domainName: url.includes("vertexaisearch") ? displayText : StringUtils.getBaseUrl(url) + // property string faviconUrl: `https://${domainName}/favicon.ico` + property string faviconUrl: `https://www.google.com/s2/favicons?domain=${domainName}&sz=32` + property string fileName: `${domainName}.ico` + property string faviconFilePath: `${faviconDownloadPath}/${fileName}` + + Process { + id: faviconDownloadProcess + running: false + command: ["bash", "-c", `[ -f ${faviconFilePath} ] || curl -s '${root.faviconUrl}' -o '${faviconFilePath}' -L -H 'User-Agent: ${downloadUserAgent}'`] + onExited: (exitCode, exitStatus) => { + root.faviconUrl = root.faviconFilePath + } + } + + Component.onCompleted: { + console.log("Favicon download:", faviconDownloadProcess.command.join(" ")) + faviconDownloadProcess.running = true + } + + PointingHandInteraction {} + onClicked: { + if (url) { + Qt.openUrlExternally(url) + Hyprland.dispatch("global quickshell:sidebarLeftClose") + } + } + + background: Rectangle { + radius: Appearance.rounding.full + color: (root.down ? Appearance.colors.colSurfaceContainerHighestActive : + root.hovered ? Appearance.colors.colSurfaceContainerHighestHover : + Appearance.m3colors.m3surfaceContainerHighest) + } + + contentItem: RowLayout { + spacing: 5 + IconImage { + id: iconImage + source: Qt.resolvedUrl(root.faviconUrl) + implicitSize: text.implicitHeight + } + StyledText { + id: text + horizontalAlignment: Text.AlignHCenter + text: displayText + color: Appearance.m3colors.m3onSurface + } + } +} diff --git a/.config/quickshell/services/Ai.qml b/.config/quickshell/services/Ai.qml index 0cccb740d..770c99fe2 100644 --- a/.config/quickshell/services/Ai.qml +++ b/.config/quickshell/services/Ai.qml @@ -310,11 +310,33 @@ Singleton { } function parseGeminiBuffer() { + // console.log("BUFFER DATA: ", requester.geminiBuffer); try { const dataJson = JSON.parse(requester.geminiBuffer); const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text requester.message.content += responseContent; + const annotationSources = dataJson.candidates[0]?.groundingMetadata.groundingChunks?.map(chunk => { + return { + "type": "url_citation", + "text": chunk?.web?.title, + "url": chunk?.web?.uri, + } + }); + const annotations = dataJson.candidates[0]?.groundingMetadata.groundingSupports?.map(citation => { + return { + "type": "url_citation", + "start_index": citation.segment?.startIndex, + "end_index": citation.segment?.endIndex, + "text": citation?.segment.text, + "url": annotationSources[citation.groundingChunkIndices[0]]?.url, + "sources": citation.groundingChunkIndices + } + }); + requester.message.annotationSources = annotationSources; + requester.message.annotations = annotations; + // console.log(JSON.stringify(requester.message, null, 2)); } catch (e) { + console.log("[AI] Could not parse response from stream: ", e); requester.message.content += requester.geminiBuffer } finally { requester.geminiBuffer = ""; diff --git a/.config/quickshell/services/AiMessageData.qml b/.config/quickshell/services/AiMessageData.qml index 7bc5108e7..625818a05 100644 --- a/.config/quickshell/services/AiMessageData.qml +++ b/.config/quickshell/services/AiMessageData.qml @@ -7,4 +7,6 @@ QtObject { property string model property bool thinking: true property bool done: false + property var annotations: [] + property var annotationSources: [] }