* add(deepseek_usage): add DeepSeek usage balance monitor plugin * add(deepseek_usage): add thumbnail.webp * chore(deepseek_usage): update plugin id to coder/deepseek_usage * fix(deepseek_usage): update 960x540 thumbnail, README IPC docs, and plugin.toml schema * fix(deepseek_usage): fix panel ID, normalize ui.graph values 0-1, declare xdg-open dependency * fix(deepseek_usage): replace thumbnail with official Noctalia generator asset
236 lines
6.7 KiB
Luau
236 lines
6.7 KiB
Luau
--!nonstrict
|
|
-- DeepSeek Usage Panel Entry — Graphical Balance & Top-Up Dashboard
|
|
|
|
local CURRENCY_SYMBOLS = {
|
|
USD = "$",
|
|
CNY = "¥",
|
|
EUR = "€",
|
|
GBP = "£",
|
|
JPY = "¥",
|
|
}
|
|
|
|
local function formatBalance(balance: number?, currency: string?): string
|
|
if balance == nil then
|
|
return "--.--"
|
|
end
|
|
local curr = currency or "USD"
|
|
local symbol = CURRENCY_SYMBOLS[curr]
|
|
if symbol then
|
|
return string.format("%s%.2f", symbol, balance)
|
|
else
|
|
return string.format("%.2f %s", balance, curr)
|
|
end
|
|
end
|
|
|
|
local state = {
|
|
balance = nil :: number?,
|
|
currency = "USD",
|
|
lastUpdated = nil :: string?,
|
|
errorMsg = nil :: string?,
|
|
isFetching = false,
|
|
}
|
|
|
|
local history = {}
|
|
|
|
function openTopUpPage()
|
|
noctalia.runAsync("xdg-open https://platform.deepseek.com/top_up")
|
|
end
|
|
|
|
function triggerRefresh()
|
|
noctalia.state.set("deepseek.refresh_requested", true)
|
|
end
|
|
|
|
function onClosePanel()
|
|
panel.close()
|
|
end
|
|
|
|
function onOpenSettings()
|
|
noctalia.openSettings()
|
|
end
|
|
|
|
local function buildGraphValues(): ({ number }, number, number)
|
|
if #history == 0 then
|
|
return { 0 }, 0, 0
|
|
end
|
|
|
|
local minVal = history[1].balance
|
|
local maxVal = history[1].balance
|
|
|
|
for _, pt in ipairs(history) do
|
|
if pt.balance < minVal then minVal = pt.balance end
|
|
if pt.balance > maxVal then maxVal = pt.balance end
|
|
end
|
|
|
|
local range = maxVal - minVal
|
|
local values = {}
|
|
for _, pt in ipairs(history) do
|
|
table.insert(values, range > 0 and (pt.balance - minVal) / range or 0.5)
|
|
end
|
|
|
|
return values, minVal, maxVal
|
|
end
|
|
|
|
local function renderGraphSection(values: { number }, minVal: number, maxVal: number)
|
|
-- graph props unverified — adjust after hot-reload test
|
|
local graphOk, graphNode = pcall(function()
|
|
return ui.graph({
|
|
values = values,
|
|
height = 64,
|
|
color = "primary",
|
|
})
|
|
end)
|
|
|
|
local graphElement
|
|
if graphOk and type(graphNode) == "table" then
|
|
graphElement = graphNode
|
|
else
|
|
graphElement = ui.label({
|
|
text = string.format("History (%d data points)", #history),
|
|
fontSize = 11,
|
|
color = "on_surface_variant",
|
|
})
|
|
end
|
|
|
|
return ui.column({ gap = 6 }, {
|
|
ui.row({ justify = "space_between", align = "center" }, {
|
|
ui.label({ text = noctalia.tr("panel.history_title"), fontSize = 12, fontWeight = "medium", color = "on_surface_variant" }),
|
|
ui.label({ text = string.format("%d samples", #history), fontSize = 10, color = "on_surface_variant" }),
|
|
}),
|
|
ui.column({
|
|
fill = "surface_variant/0.2",
|
|
radius = 8,
|
|
padding = 8,
|
|
gap = 4,
|
|
}, {
|
|
graphElement,
|
|
ui.row({ justify = "space_between" }, {
|
|
ui.label({ text = string.format("Min: %s", formatBalance(minVal, state.currency)), fontSize = 10, color = "on_surface_variant" }),
|
|
ui.label({ text = string.format("Max: %s", formatBalance(maxVal, state.currency)), fontSize = 10, color = "on_surface_variant" }),
|
|
}),
|
|
}),
|
|
})
|
|
end
|
|
|
|
local function render()
|
|
local apiKey = noctalia.getConfig("api_key") or ""
|
|
local isUnconfigured = apiKey == ""
|
|
|
|
local values, minVal, maxVal = buildGraphValues()
|
|
local balanceDisplay = formatBalance(state.balance, state.currency)
|
|
|
|
-- Header Row
|
|
local headerRow = ui.row({ align = "center", justify = "space_between", gap = 8 }, {
|
|
ui.row({ gap = 8, align = "center" }, {
|
|
ui.glyph({ name = "wallet", size = 18, color = "primary" }),
|
|
ui.label({ text = noctalia.tr("panel.title"), fontSize = 16, fontWeight = "bold", color = "on_surface" }),
|
|
}),
|
|
ui.row({ gap = 4, align = "center" }, {
|
|
ui.button({ glyph = "refresh", variant = "ghost", onClick = "triggerRefresh", tooltip = noctalia.tr("panel.refresh_tooltip") }),
|
|
ui.button({ glyph = "close", variant = "ghost", onClick = "onClosePanel" }),
|
|
}),
|
|
})
|
|
|
|
-- Scrollable Body Items
|
|
local bodyItems = {}
|
|
|
|
-- Unconfigured State Banner
|
|
if isUnconfigured then
|
|
table.insert(bodyItems, ui.column({
|
|
fill = "surface_variant/0.5",
|
|
radius = 8,
|
|
padding = 12,
|
|
gap = 8,
|
|
align = "stretch",
|
|
}, {
|
|
ui.label({ text = noctalia.tr("panel.unconfigured_title"), fontWeight = "bold", color = "error" }),
|
|
ui.label({ text = noctalia.tr("panel.unconfigured_desc"), fontSize = 12, color = "on_surface_variant" }),
|
|
ui.button({
|
|
text = noctalia.tr("panel.open_settings"),
|
|
variant = "primary",
|
|
onClick = "onOpenSettings",
|
|
}),
|
|
}))
|
|
end
|
|
|
|
-- Error Banner
|
|
if not isUnconfigured and state.errorMsg ~= nil then
|
|
table.insert(bodyItems, ui.row({
|
|
fill = "error/0.15",
|
|
radius = 8,
|
|
padding = 10,
|
|
gap = 8,
|
|
align = "center",
|
|
}, {
|
|
ui.glyph({ name = "alert-circle", color = "error", size = 16 }),
|
|
ui.label({ text = state.errorMsg, color = "error", fontSize = 12, flexGrow = 1 }),
|
|
}))
|
|
end
|
|
|
|
-- Hero Balance Card
|
|
if not isUnconfigured then
|
|
table.insert(bodyItems, ui.column({
|
|
fill = "surface_variant/0.35",
|
|
radius = 12,
|
|
padding = 16,
|
|
align = "center",
|
|
gap = 6,
|
|
}, {
|
|
ui.label({ text = noctalia.tr("panel.current_balance"), fontSize = 12, color = "on_surface_variant" }),
|
|
ui.label({ text = balanceDisplay .. " " .. state.currency, fontSize = 28, fontWeight = "bold", color = "primary" }),
|
|
ui.label({ text = noctalia.tr("panel.wallet_info"), fontSize = 11, color = "on_surface_variant" }),
|
|
ui.spacer({ height = 4 }),
|
|
ui.button({
|
|
text = " " .. noctalia.tr("panel.add_credits") .. " ",
|
|
glyph = "external-link",
|
|
variant = "primary",
|
|
onClick = "openTopUpPage",
|
|
tooltip = noctalia.tr("panel.add_credits_tooltip"),
|
|
}),
|
|
}))
|
|
|
|
-- Graph / Trend Section
|
|
table.insert(bodyItems, renderGraphSection(values, minVal, maxVal))
|
|
end
|
|
|
|
-- Footer Status Info
|
|
local footerRow = ui.row({ justify = "space_between", align = "center" }, {
|
|
ui.label({
|
|
text = state.lastUpdated and (noctalia.tr("panel.updated_at") .. ": " .. state.lastUpdated) or "",
|
|
fontSize = 10,
|
|
color = "on_surface_variant",
|
|
}),
|
|
ui.button({
|
|
text = noctalia.tr("panel.settings_btn"),
|
|
variant = "ghost",
|
|
onClick = "onOpenSettings",
|
|
}),
|
|
})
|
|
|
|
panel.render(ui.column({ flexGrow = 1, gap = 10, padding = 12 }, {
|
|
headerRow,
|
|
ui.separator({}),
|
|
ui.scroll({ flexGrow = 1, gap = 10 }, bodyItems),
|
|
footerRow,
|
|
}))
|
|
end
|
|
|
|
function onOpen(_context)
|
|
state = noctalia.state.get("deepseek.state") or state
|
|
history = noctalia.state.get("deepseek.history") or {}
|
|
render()
|
|
end
|
|
|
|
noctalia.state.watch("deepseek.state", function(newState)
|
|
if type(newState) == "table" then
|
|
state = newState
|
|
render()
|
|
end
|
|
end)
|
|
|
|
noctalia.state.watch("deepseek.history", function(newHistory)
|
|
if type(newHistory) == "table" then
|
|
history = newHistory
|
|
render()
|
|
end
|
|
end)
|