contents/ui/components/ProviderCostSection.qml

import QtQuick
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import "." as Components
import "../CostPresentation.js" as CostPresentation

ColumnLayout {
    id: tokenCostSection
    objectName: "providerLocalCostSection"

    required property var applet
    required property var providerData
    property string accountSelectionKey: applet.accountLabel(providerData)
    property bool presentationVisible: false
    property bool detailsExpanded: false
    property var daySelectionMemo: ({ points: [], index: -1 })
    readonly property bool costHistoryShowsTokens: applet.costHistoryShowsTokens
    readonly property string selectionScope: providerData ? JSON.stringify([providerData.provider, accountSelectionKey,
        tokenCost ? tokenCost.historyDays : 0]) : ""
    readonly property var selectedDay: CostPresentation.selectedCostDay(tokenCost ? tokenCost.daily : [], chartPoints, costChart.selectedIndex)
    readonly property bool hasVisibleDetails: detailsExpanded || selectedDay !== null

    onSelectionScopeChanged: {
        detailsExpanded = false;
        clearDaySelection();
    }
    onCostHistoryShowsTokensChanged: clearDaySelection()

    function amountText(amounts, mode) {
        return CostPresentation.hasMetricValue(amounts, false) ? applet.qualifiedCostValue(applet.amountString(amounts.cost, amounts.currency), mode) : i18n("Cost unavailable");
    }

    function tokensText(amounts) {
        return CostPresentation.hasMetricValue(amounts, true) ? applet.usageCountText(amounts.tokens, "tokens") : i18n("Tokens unavailable");
    }

    function daySummaryText() {
        return CostPresentation.amountSummary(applet.costNumberFormat, selectedDay, function (tokens) {
            return applet.usageCountText(tokens, "tokens");
        });
    }

    function clearDaySelection() {
        if (!costChart)
            return;
        daySelectionMemo = { points: costChart.points, index: -1 };
        costChart.selectedIndex = -1;
        costChart.hoveredIndex = -1;
    }

    function rememberDaySelection() {
        // Ignore the chart's index clamp while a new points array is awaiting reconciliation.
        if (daySelectionMemo.points === costChart.points)
            daySelectionMemo = { points: costChart.points, index: costChart.selectedIndex };
    }

    function reconcileDaySelection() {
        var points = costChart.points;
        var index = CostPresentation.costDayIndexAfterRefresh(daySelectionMemo.points, daySelectionMemo.index, points);
        daySelectionMemo = { points: points, index: index };
        costChart.selectedIndex = index;
        costChart.hoveredIndex = -1;
    }

    readonly property var tokenCost: tokenCostSection.providerData ? tokenCostSection.providerData.tokenCost : null
    readonly property var chartPoints: tokenCost ? applet.costChartPoints(tokenCost.daily) : []
    readonly property var costTrustSummary: CostPresentation.costTrustSummary(tokenCost ? [tokenCost] : [])
    readonly property string costErrorText: applet.privateErrorText(applet.costErrorText)
    readonly property bool supportsLocalCost: tokenCostSection.providerData && applet.tokenCostHint(tokenCostSection.providerData.provider).length > 0

    visible: tokenCostSection.tokenCost ? true : tokenCostSection.supportsLocalCost && tokenCostSection.costErrorText.length > 0
    Layout.fillWidth: true
    spacing: Kirigami.Units.smallSpacing / 1.5

    Kirigami.Separator {
        Layout.fillWidth: true
    }

    RowLayout {
        Layout.fillWidth: true

        PlainHeading {
            text: i18n("Cost")
            level: 4
            type: Kirigami.Heading.Type.Primary
            Layout.fillWidth: true
        }

        Components.PlainComboBox {
            objectName: "providerCostMetricCombo"
            visible: tokenCostSection.tokenCost !== null
            textRole: "text"
            valueRole: "value"
            model: [
                {
                    text: i18n("Cost"),
                    value: "cost"
                },
                {
                    text: i18n("Tokens"),
                    value: "tokens"
                }
            ]
            currentIndex: tokenCostSection.costHistoryShowsTokens ? 1 : 0
            Accessible.name: i18n("History metric")
            onActivated: function (index) {
                tokenCostSection.applet.setCostHistoryMetric(valueAt(index));
            }
        }
    }

    PlainPlasmaLabel {
        visible: !tokenCostSection.tokenCost && tokenCostSection.costErrorText.length > 0
        text: i18n("Cost unavailable: %1", tokenCostSection.costErrorText)
        color: Kirigami.Theme.negativeTextColor
        Layout.fillWidth: true
        wrapMode: Text.WordWrap
    }

    GridLayout {
        id: costSummaryGrid
        objectName: "costSummaryGrid"
        visible: tokenCostSection.tokenCost !== null
        Layout.fillWidth: true
        columns: 2
        columnSpacing: Kirigami.Units.largeSpacing

        Repeater {
            model: tokenCostSection.tokenCost ? [
                {
                    label: i18n("Today"),
                    amounts: tokenCostSection.tokenCost.today,
                    valueMode: "plain"
                },
                {
                    label: tokenCostSection.tokenCost.windowLabel,
                    amounts: tokenCostSection.tokenCost.totals,
                    valueMode: tokenCostSection.tokenCost.valueMode
                }
            ] : []

            delegate: ColumnLayout {
                required property var modelData
                Layout.fillWidth: true
                Layout.preferredWidth: 1
                spacing: Kirigami.Units.smallSpacing / 2

                PlainPlasmaLabel {
                    text: modelData.label
                    font: Kirigami.Theme.smallFont
                    opacity: tokenCostSection.applet.secondaryTextOpacity
                    Layout.fillWidth: true
                    wrapMode: Text.Wrap
                }
                PlainPlasmaLabel {
                    text: tokenCostSection.amountText(modelData.amounts, modelData.valueMode)
                    font.weight: Font.DemiBold
                    Layout.fillWidth: true
                    wrapMode: Text.Wrap
                }
                PlainPlasmaLabel {
                    text: tokenCostSection.tokensText(modelData.amounts)
                    font: Kirigami.Theme.smallFont
                    opacity: tokenCostSection.applet.secondaryTextOpacity
                    Layout.fillWidth: true
                    wrapMode: Text.Wrap
                }
            }
        }
    }

    Components.CostTrustNotice {
        noticeScope: "provider:" + (tokenCostSection.providerData ? tokenCostSection.providerData.provider : "")
        stateOwner: tokenCostSection.applet
        presentationVisible: tokenCostSection.presentationVisible
        summary: tokenCostSection.costTrustSummary
    }

    Components.InteractiveChart {
        id: costChart
        objectName: "providerCostChart"
        onPointsChanged: tokenCostSection.reconcileDaySelection()
        onSelectedIndexChanged: tokenCostSection.rememberDaySelection()
        readonly property var tokenCost: tokenCostSection.tokenCost
        readonly property var providerData: tokenCostSection.providerData

        visible: tokenCostSection.chartPoints.length > 0
        applet: tokenCostSection.applet
        points: tokenCostSection.chartPoints
        accent: applet.providerReadableColor(providerData ? providerData.provider : "")
        kind: "bar"
        accessibleTitle: tokenCostSection.costHistoryShowsTokens ? i18n("Daily token history") : i18n("Daily cost history")
        Layout.topMargin: Kirigami.Units.smallSpacing / 2
    }

    RowLayout {
        visible: tokenCostSection.chartPoints.length > 0
        Layout.fillWidth: true
        spacing: Kirigami.Units.smallSpacing

        PlainPlasmaLabel {
            id: costSparklineSummaryLabel

            text: tokenCostSection.tokenCost ? applet.costSparklineSummary(tokenCostSection.tokenCost.daily) : ""
            font: Kirigami.Theme.smallFont
            opacity: applet.secondaryTextOpacity
            Layout.fillWidth: true
            elide: Text.ElideRight
        }

        PlainPlasmaLabel {
            id: costSparklineRangeLabel

            text: tokenCostSection.tokenCost ? i18np("%1 day", "%1 days", tokenCostSection.tokenCost.daily.length) : ""
            font: Kirigami.Theme.smallFont
            opacity: applet.secondaryTextOpacity
            horizontalAlignment: Text.AlignRight
            elide: Text.ElideRight
        }
    }

    Components.PlainButton {
        objectName: "costDetailsToggle"
        visible: tokenCostSection.tokenCost !== null
        plainText: tokenCostSection.hasVisibleDetails ? i18n("Hide details") : i18n("Show details")
        icon.name: tokenCostSection.hasVisibleDetails ? "arrow-up" : "arrow-down"
        onClicked: {
            if (tokenCostSection.hasVisibleDetails) {
                tokenCostSection.detailsExpanded = false;
                tokenCostSection.clearDaySelection();
            } else {
                tokenCostSection.detailsExpanded = true;
            }
        }
    }

    ColumnLayout {
        id: costDrillDownSection

        readonly property var detailData: tokenCostSection.selectedDay ? {
            totals: tokenCostSection.selectedDay,
            models: tokenCostSection.selectedDay.models,
            modelsTruncated: tokenCostSection.selectedDay.modelsTruncated
        } : tokenCostSection.tokenCost
        readonly property var breakdownRows: applet.costBreakdownRows(detailData)
        readonly property var modelRows: applet.costModelRows(detailData)
        readonly property real metricValueColumnWidth: Kirigami.Units.gridUnit * 9

        objectName: "costDrillDownSection"
        visible: tokenCostSection.tokenCost !== null && tokenCostSection.hasVisibleDetails
        Layout.fillWidth: true
        spacing: Kirigami.Units.smallSpacing

        RowLayout {
            Layout.fillWidth: true

            PlainPlasmaLabel {
                objectName: "costDetailsTitle"
                text: tokenCostSection.selectedDay ? i18n("Details for %1", tokenCostSection.selectedDay.label) : i18n("Cost details")
                font.weight: Font.DemiBold
                Layout.fillWidth: true
                wrapMode: Text.Wrap
            }

            Components.PlainButton {
                plainText: i18n("All days")
                visible: tokenCostSection.selectedDay !== null
                onClicked: {
                    tokenCostSection.detailsExpanded = true;
                    tokenCostSection.clearDaySelection();
                }
            }
        }

        PlainPlasmaLabel {
            visible: tokenCostSection.selectedDay !== null
            text: tokenCostSection.daySummaryText()
            Layout.fillWidth: true
            wrapMode: Text.Wrap
        }

        PlainPlasmaLabel {
            objectName: "costModelsEmptyNotice"
            visible: costDrillDownSection.modelRows.length === 0
            text: tokenCostSection.selectedDay !== null ? i18n("No model breakdown for this day.")
                : i18n("No model breakdown for this period.")
            Layout.fillWidth: true
            wrapMode: Text.Wrap
        }

        PlainPlasmaLabel {
            objectName: "costModelsPartialNotice"
            visible: costDrillDownSection.detailData !== null && costDrillDownSection.detailData.modelsTruncated === true
            text: i18n("Model breakdown is partial.")
            Layout.fillWidth: true
            wrapMode: Text.Wrap
        }

        PlainPlasmaLabel {
            visible: !tokenCostSection.selectedDay && tokenCostSection.tokenCost && applet.costPerMillionLine(tokenCostSection.tokenCost).length > 0
            text: tokenCostSection.tokenCost ? applet.costPerMillionLine(tokenCostSection.tokenCost) : ""
            font: Kirigami.Theme.smallFont
            opacity: applet.secondaryTextOpacity
            Layout.fillWidth: true
            elide: Text.ElideRight
        }

        ColumnLayout {
            visible: costDrillDownSection.breakdownRows.length > 0
            Layout.fillWidth: true
            spacing: Kirigami.Units.smallSpacing / 2

            Repeater {
                model: costDrillDownSection.breakdownRows

                delegate: RowLayout {
                    required property var modelData

                    Layout.fillWidth: true
                    spacing: Kirigami.Units.smallSpacing

                    PlainPlasmaLabel {
                        text: modelData.label
                        font.pixelSize: Kirigami.Theme.smallFont.pixelSize
                        opacity: applet.secondaryTextOpacity
                        Layout.fillWidth: true
                        elide: Text.ElideRight
                    }

                    PlainPlasmaLabel {
                        id: costBreakdownValueLabel

                        text: modelData.value
                        font.pixelSize: Kirigami.Theme.smallFont.pixelSize
                        opacity: applet.valueTextOpacity
                        font.weight: Font.Medium
                        horizontalAlignment: Text.AlignRight
                        Layout.preferredWidth: costDrillDownSection.metricValueColumnWidth
                        Layout.maximumWidth: costDrillDownSection.metricValueColumnWidth
                        elide: Text.ElideRight
                    }
                }
            }
        }

        Kirigami.Separator {
            visible: costDrillDownSection.modelRows.length > 0
            Layout.fillWidth: true
            opacity: 0.55
        }

        ColumnLayout {
            visible: costDrillDownSection.modelRows.length > 0
            Layout.fillWidth: true
            spacing: Kirigami.Units.smallSpacing / 2

            PlainPlasmaLabel {
                id: costModelsHeading

                text: i18n("Models")
                font.pixelSize: Kirigami.Theme.smallFont.pixelSize
                font.weight: Font.DemiBold
                opacity: applet.secondaryTextOpacity
                Layout.fillWidth: true
                elide: Text.ElideRight
            }

            Repeater {
                model: costDrillDownSection.modelRows

                delegate: RowLayout {
                    required property var modelData

                    Layout.fillWidth: true
                    spacing: Kirigami.Units.smallSpacing

                    PlainPlasmaLabel {
                        text: modelData.label
                        font.pixelSize: Kirigami.Theme.smallFont.pixelSize
                        opacity: applet.secondaryTextOpacity
                        Layout.fillWidth: true
                        elide: Text.ElideRight
                    }

                    PlainPlasmaLabel {
                        id: costModelValueLabel

                        text: modelData.value
                        font.pixelSize: Kirigami.Theme.smallFont.pixelSize
                        opacity: applet.valueTextOpacity
                        font.weight: Font.Medium
                        horizontalAlignment: Text.AlignRight
                        Layout.preferredWidth: costDrillDownSection.metricValueColumnWidth
                        Layout.maximumWidth: costDrillDownSection.metricValueColumnWidth
                        elide: Text.ElideRight
                    }
                }
            }
        }
    }

    ColumnLayout {
        id: costHistoryChartSection

        readonly property var rows: applet.costHistoryRows(tokenCostSection.tokenCost)
        readonly property string peakLine: tokenCostSection.tokenCost ? applet.costPeakLine(tokenCostSection.tokenCost.daily) : ""
        readonly property string averageLine: tokenCostSection.tokenCost ? applet.costAverageDailyLine(tokenCostSection.tokenCost.daily) : ""
        readonly property color accent: applet.providerReadableColor(tokenCostSection.providerData ? tokenCostSection.providerData.provider : "")

        visible: tokenCostSection.detailsExpanded && rows.length > 1
        Layout.fillWidth: true
        spacing: Kirigami.Units.smallSpacing / 2

        RowLayout {
            id: costHistoryHeaderRow

            Layout.fillWidth: true
            spacing: Kirigami.Units.smallSpacing

            PlainPlasmaLabel {
                text: i18n("Cost history")
                font.weight: Font.DemiBold
                Layout.fillWidth: true
                elide: Text.ElideRight
            }

            PlainPlasmaLabel {
                visible: costHistoryChartSection.averageLine.length > 0
                text: costHistoryChartSection.averageLine
                font: Kirigami.Theme.smallFont
                opacity: applet.secondaryTextOpacity
                horizontalAlignment: Text.AlignRight
                elide: Text.ElideRight
            }
        }

        PlainPlasmaLabel {
            visible: costHistoryChartSection.peakLine.length > 0
            text: costHistoryChartSection.peakLine
            font: Kirigami.Theme.smallFont
            opacity: applet.secondaryTextOpacity
            Layout.fillWidth: true
            elide: Text.ElideRight
        }

        Repeater {
            model: costHistoryChartSection.rows

            delegate: RowLayout {
                id: costHistoryMetricRow

                required property var modelData

                Layout.fillWidth: true
                spacing: Kirigami.Units.smallSpacing

                PlainPlasmaLabel {
                    id: costHistoryDateLabel

                    text: modelData.label
                    font.pixelSize: Kirigami.Theme.smallFont.pixelSize
                    opacity: applet.secondaryTextOpacity
                    Layout.preferredWidth: Kirigami.Units.gridUnit * 5
                    elide: Text.ElideRight
                }

                Rectangle {
                    id: costHistoryBarTrack

                    Layout.fillWidth: true
                    Layout.preferredHeight: applet.compactMeterTrackHeight
                    radius: height / 2
                    color: applet.withAlpha(Kirigami.Theme.textColor, 0.055)
                    clip: true
                    antialiasing: true

                    Rectangle {
                        width: parent.width * Math.max(0, Math.min(100, modelData.percent)) / 100
                        height: parent.height
                        radius: parent.radius
                        antialiasing: true
                        gradient: Gradient {
                            orientation: Gradient.Horizontal

                            GradientStop {
                                position: 0
                                color: applet.withAlpha(costHistoryChartSection.accent, modelData.isPeak ? 0.72 : 0.46)
                            }

                            GradientStop {
                                position: 1
                                color: applet.withAlpha(costHistoryChartSection.accent, modelData.isPeak ? 1 : 0.8)
                            }
                        }

                        Behavior on width {
                            NumberAnimation {
                                duration: Kirigami.Units.longDuration
                                easing.type: Easing.OutCubic
                            }
                        }
                    }
                }

                PlainPlasmaLabel {
                    id: costHistoryValueLabel

                    text: modelData.value
                    font.pixelSize: Kirigami.Theme.smallFont.pixelSize
                    opacity: modelData.isPeak ? applet.valueTextOpacity : applet.secondaryTextOpacity
                    font.weight: modelData.isPeak ? Font.DemiBold : Font.Normal
                    horizontalAlignment: Text.AlignRight
                    Layout.preferredWidth: Kirigami.Units.gridUnit * 8
                    elide: Text.ElideRight
                }
            }
        }
    }

    ColumnLayout {
        visible: tokenCostSection.detailsExpanded
        Layout.fillWidth: true
        Components.ProjectCostSection {
            applet: tokenCostSection.applet
            providerCosts: tokenCostSection.tokenCost ? [tokenCostSection.tokenCost] : []
        }
    }

    PlainPlasmaLabel {
        visible: tokenCostSection.tokenCost && tokenCostSection.tokenCost.hintLine.length > 0 ? true : false
        text: tokenCostSection.tokenCost ? tokenCostSection.tokenCost.hintLine : ""
        opacity: applet.secondaryTextOpacity
        Layout.fillWidth: true
        wrapMode: Text.WordWrap
    }
}