add(deepseek_usage): DeepSeek API credit & balance monitor (#220)
* 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
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
--!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)
|
||||
Reference in New Issue
Block a user