import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { fn, hash } from "@ember/helper"; import { on } from "@ember/modifier"; import { action } from "@ember/object"; import { LinkTo } from "@ember/routing"; import { service } from "@ember/service"; import { eq } from "truth-helpers"; import DateTimeInputRange from "discourse/components/date-time-input-range"; import avatar from "discourse/helpers/avatar"; import { ajax } from "discourse/lib/ajax"; import i18n from "discourse-common/helpers/i18n"; import Chart from "admin/components/chart"; import ComboBox from "select-kit/components/combo-box"; export default class AiUsage extends Component { @service store; @tracked startDate = moment().subtract(30, "days").toDate(); @tracked endDate = new Date(); @tracked data = this.args.model; @tracked selectedFeature; @tracked selectedModel; @tracked selectedPeriod = "month"; @tracked isCustomDateActive = false; @action async fetchData() { const response = await ajax("/admin/plugins/discourse-ai/ai-usage.json", { data: { start_date: moment(this.startDate).format("YYYY-MM-DD"), end_date: moment(this.endDate).format("YYYY-MM-DD"), feature: this.selectedFeature, model: this.selectedModel, }, }); this.data = response; } @action async onFilterChange() { await this.fetchData(); } @action onFeatureChanged(value) { this.selectedFeature = value; this.onFilterChange(); } @action onModelChanged(value) { this.selectedModel = value; this.onFilterChange(); } normalizeTimeSeriesData(data) { if (!data?.length) { return []; } const startDate = moment(this.startDate); const endDate = moment(this.endDate); const normalized = []; let interval; let format; if (this.data.period === "hour") { interval = "hour"; format = "YYYY-MM-DD HH:00:00"; } else if (this.data.period === "day") { interval = "day"; format = "YYYY-MM-DD"; } else { interval = "month"; format = "YYYY-MM"; } const dataMap = new Map( data.map((d) => [moment(d.period).format(format), d]) ); for ( let m = moment(startDate); m.isSameOrBefore(endDate); m.add(1, interval) ) { const dateKey = m.format(format); const existingData = dataMap.get(dateKey); normalized.push( existingData || { period: m.format(), total_tokens: 0, total_cached_tokens: 0, total_request_tokens: 0, total_response_tokens: 0, } ); } return normalized; } get chartConfig() { if (!this.data?.data) { return; } const normalizedData = this.normalizeTimeSeriesData(this.data.data); const chartEl = document.querySelector(".ai-usage__chart"); const computedStyle = getComputedStyle(chartEl); const colors = { response: computedStyle.getPropertyValue("--chart-response-color").trim(), request: computedStyle.getPropertyValue("--chart-request-color").trim(), cached: computedStyle.getPropertyValue("--chart-cached-color").trim(), }; return { type: "bar", data: { labels: normalizedData.map((row) => { const date = moment(row.period); if (this.data.period === "hour") { return date.format("HH:00"); } else if (this.data.period === "day") { return date.format("DD-MMM"); } else { return date.format("MMM-YY"); } }), datasets: [ { label: "Response Tokens", data: normalizedData.map((row) => row.total_response_tokens), backgroundColor: colors.response, }, { label: "Net Request Tokens", data: normalizedData.map( (row) => row.total_request_tokens - row.total_cached_tokens ), backgroundColor: colors.request, }, { label: "Cached Request Tokens", data: normalizedData.map((row) => row.total_cached_tokens), backgroundColor: colors.cached, }, ], }, options: { responsive: true, scales: { x: { stacked: true, }, y: { stacked: true, beginAtZero: true, }, }, }, }; } get availableFeatures() { // when you switch we don't want the list to change // only when you switch durations this._cachedFeatures = this._cachedFeatures || (this.data?.features || []).map((f) => ({ id: f.feature_name, name: f.feature_name, })); return this._cachedFeatures; } get availableModels() { this._cachedModels = this._cachedModels || (this.data?.models || []).map((m) => ({ id: m.llm, name: m.llm, })); return this._cachedModels; } get periodOptions() { return [ { id: "day", name: "Last 24 Hours" }, { id: "week", name: "Last Week" }, { id: "month", name: "Last Month" }, ]; } @action setPeriodDates(period) { const now = moment(); switch (period) { case "day": this.startDate = now.clone().subtract(1, "day").toDate(); this.endDate = now.toDate(); break; case "week": this.startDate = now.clone().subtract(7, "days").toDate(); this.endDate = now.toDate(); break; case "month": this.startDate = now.clone().subtract(30, "days").toDate(); this.endDate = now.toDate(); break; } } @action onPeriodSelect(period) { this.selectedPeriod = period; this.isCustomDateActive = false; this.setPeriodDates(period); this.fetchData(); } @action onCustomDateClick() { this.isCustomDateActive = !this.isCustomDateActive; if (this.isCustomDateActive) { this.selectedPeriod = null; } } @action onDateChange() { this.isCustomDateActive = true; this.selectedPeriod = null; this.fetchData(); } @action onChangeDateRange({ from, to }) { this._startDate = from; this._endDate = to; } @action onRefreshDateRange() { this.startDate = this._startDate; this.endDate = this._endDate; this.fetchData(); } }