diff --git a/nano-gpt.el b/nano-gpt.el deleted file mode 100644 index 94dfd1c..0000000 --- a/nano-gpt.el +++ /dev/null @@ -1,399 +0,0 @@ -;;; nano-gpt-status.el --- Dashboard for Nano-GPT API -*- lexical-binding: t; -*- - -;; Copyright (C) 2024 Nano-GPT Contributors - -;; Author: Nano-GPT Contributors -;; Maintainer: Nano-GPT Contributors -;; URL: https://github.com/nano-gpt/emacs -;; Version: 0.1.0 -;; Package-Requires: ((emacs "28.1") (qrencode "0.1")) -;; Keywords: convenience, api, dashboard - -;; This file is not part of GNU Emacs. - -;; This program is free software: you can redistribute it and/or modify -;; it under the terms of the GNU General Public License as published by -;; the Free Software Foundation, either version 3 of the License, or -;; (at your option) any later version. - -;; This program is distributed in the hope that it will be useful, -;; but WITHOUT ANY WARRANTY; without even the implied warranty of -;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -;; GNU General Public License for more details. - -;; You should have received a copy of the GNU General Public License -;; along with this program. If not, see . - -;;; Commentary: - -;; A dashboard for the Nano-GPT API showing models, usage, balance and deposits. - -;; Setup: -;; (require 'nano-gpt-status) -;; M-x nano-gpt-status -;; -;; Configure API key in auth-sources: -;; (add-to-list 'auth-sources '("nano-gpt.gpg" :host "nano-gpt.com")) - -;;; Code: - -(require 'url) -(require 'json) -(require 'auth-source) -(require 'org) -(require 'tabulated-list) -(require 'hl-line) - -(declare-function qrencode-string "qrencode") - -(defgroup nano-gpt nil - "Dashboard for Nano-GPT API." - :group 'convenience - :prefix "nano-gpt-" - :link '(url-link :tag "GitHub" "https://github.com/nano-gpt/emacs")) - -(defcustom nano-gpt-api-base-url "https://nano-gpt.com/api" - "Base URL for Nano-GPT API." - :type 'string - :group 'nano-gpt) - -(defcustom nano-gpt-models-endpoint "/v1/models?detailed=true" - "Endpoint for fetching models." - :type 'string - :group 'nano-gpt) - -(defvar nano-gpt--window-config nil - "Saved window configuration before dashboard was shown.") - -(defun nano-gpt--get-api-key () - "Get API key from auth-source for nano-gpt.com." - (let ((secret (auth-source-search :host "nano-gpt.com" :secret t))) - (when secret - (funcall (plist-get (car secret) :secret))))) - -;; --- Table 1: Fruits --- - -(defvar nano-gpt-fruits-mode-map - (let ((map (make-sparse-keymap))) - (set-keymap-parent map tabulated-list-mode-map) - (define-key map "q" #'nano-gpt-quit) - map)) - -(define-derived-mode nano-gpt-fruits-mode tabulated-list-mode "Fruits" - "A table of fruits." - (setq tabulated-list-format [("Fruit" 15 t) ("Color" 10 t) ("Taste" 10 t)]) - (setq tabulated-list-entries - '((1 ["Apple" "Red" "Sweet"]) - (2 ["Banana" "Yellow" "Sweet"]) - (3 ["Lemon" "Yellow" "Sour"]) - (4 ["Grape" "Purple" "Sweet"]))) - (tabulated-list-init-header) - (tabulated-list-print)) - -;; --- Table 2: Models --- - -(defvar nano-gpt--models-data nil - "Cached models data from API.") - -(defun nano-gpt--aget (alist key) - "Safely get value from ALIST for KEY, returns nil if null." - (let ((val (cdr (assq key alist)))) - (unless (eq val :null) val))) - -(defun nano-gpt--format-context-length (num) - "Format NUM as human-readable context length." - (if (numberp num) - (cond - ((>= num 1000000) (format "%.1fM" (/ num 1000000.0))) - ((>= num 1000) (format "%.0fK" (/ num 1000.0))) - (t (number-to-string num))) - "N/A")) - -(defun nano-gpt--format-pricing (prompt completion) - "Format pricing from PROMPT and COMPLETION values." - (if (and (numberp prompt) (numberp completion) - (eq prompt 0) (eq completion 0)) - "Free" - (format "$%.2f/$%.2f" - (or prompt 0) - (or completion 0)))) - -(defun nano-gpt--fetch-models-callback (status) - "Callback for async model fetching. STATUS is the url-retrieve status." - (unless (plist-get status :error) - (goto-char (point-min)) - (re-search-forward "^$" nil t) - (let* ((json (json-parse-string (buffer-substring (point) (point-max)) :object-type 'alist)) - (models (cdr (assq 'data json)))) - (setq nano-gpt--models-data models) - (when (get-buffer "*Dashboard:Models*") - (with-current-buffer "*Dashboard:Models*" - (nano-gpt-models-mode))) - (message "Models loaded: %d entries" (length models))))) - -(defun nano-gpt--fetch-models-async () - "Fetch models from Nano-GPT API asynchronously." - (url-retrieve - (concat nano-gpt-api-base-url nano-gpt-models-endpoint) - #'nano-gpt--fetch-models-callback - nil t t)) - -;; --- Usage --- - -(defun nano-gpt--format-usage-time (epoch-ms) - "Format EPOCH-MS as human-readable local time." - (when epoch-ms - (format-time-string "%Y-%m-%d %H:%M" (/ epoch-ms 1000)))) - -(defun nano-gpt--fetch-usage-callback (status) - "Callback for async usage fetching. STATUS is the url-retrieve status." - (unless (plist-get status :error) - (goto-char (point-min)) - (re-search-forward "^$" nil t) - (let* ((json-str (buffer-substring (point) (point-max))) - (json (json-parse-string json-str :object-type 'alist)) - (usage json) - (state (nano-gpt--aget usage 'state)) - (active (nano-gpt--aget usage 'active)) - (limits (nano-gpt--aget usage 'limits)) - (weekly (nano-gpt--aget usage 'weeklyInputTokens)) - (period (nano-gpt--aget usage 'period))) - (when (get-buffer "*Dashboard:Info*") - (with-current-buffer "*Dashboard:Info*" - (let ((inhibit-read-only t)) - (goto-char (point-min)) - (when (re-search-forward "^\\*\\* Usage" nil t) - (delete-region (match-beginning 0) (progn (forward-line) (point))) - (insert "** Usage\n") - (insert (format "- State: %s\n" (or state "N/A"))) - (insert (format "- Active: %s\n\n" (if active "Yes" "No"))) - (insert "*** Weekly Input Tokens\n") - (insert (format "- Used: %s / %s\n" - (nano-gpt--aget weekly 'used) - (nano-gpt--aget limits 'weeklyInputTokens))) - (insert (format "- Remaining: %s\n" (nano-gpt--aget weekly 'remaining))) - (insert (format "- Percent: %s%%\n" - (let ((pct (nano-gpt--aget weekly 'percentUsed))) - (when pct (format "%.2f" (* pct 100)))))) - (insert (format "- Resets at: %s\n\n" - (nano-gpt--format-usage-time (nano-gpt--aget weekly 'resetAt)))) - (insert "*** Billing Period\n") - (insert (format "- Ends: %s\n" - (let ((end (nano-gpt--aget period 'currentPeriodEnd))) - (when end - (format-time-string "%Y-%m-%d %H:%M" - (date-to-time end)))))))))) - (message "Usage loaded")))) - -(defun nano-gpt--fetch-usage-async () - "Fetch usage from Nano-GPT API asynchronously." - (let ((api-key (nano-gpt--get-api-key))) - (when api-key - (let ((url-request-extra-headers (list (cons "Authorization" (format "Bearer %s" api-key))))) - (url-retrieve - (concat nano-gpt-api-base-url "/subscription/v1/usage") - #'nano-gpt--fetch-usage-callback - nil nil t))))) - -;; --- Balance --- - -(defun nano-gpt--fetch-balance-callback (status) - "Callback for async balance fetching. STATUS is the url-retrieve status." - (unless (plist-get status :error) - (goto-char (point-min)) - (re-search-forward "^$" nil t) - (let* ((json (json-parse-string (buffer-substring (point) (point-max)) :object-type 'alist)) - (usd (nano-gpt--aget json 'usd_balance))) - (when (get-buffer "*Dashboard:Info*") - (with-current-buffer "*Dashboard:Info*" - (let ((inhibit-read-only t)) - (goto-char (point-min)) - (when (re-search-forward "^\\*\\* Balance" nil t) - (delete-region (match-beginning 0) (progn (forward-line) (point))) - (insert "** Balance\n") - (insert (format "- USD: $%s\n" (or usd "N/A"))))))) - (message "Balance loaded")))) - -(defun nano-gpt--fetch-balance-async () - "Fetch balance from Nano-GPT API asynchronously." - (let ((api-key (nano-gpt--get-api-key))) - (when api-key - (let ((url-request-method "POST") - (url-request-extra-headers (list (cons "x-api-key" api-key)))) - (url-retrieve - (concat nano-gpt-api-base-url "/check-balance") - #'nano-gpt--fetch-balance-callback - nil nil t))))) - -;; --- Deposit --- - -(defun nano-gpt--create-deposit-callback (status) - "Callback for async deposit creation. STATUS is the url-retrieve status." - (unless (plist-get status :error) - (goto-char (point-min)) - (re-search-forward "^$" nil t) - (let* ((json-str (buffer-substring (point) (point-max))) - (json (json-parse-string json-str :object-type 'alist)) - (tx-id (nano-gpt--aget json 'txId)) - (amount (nano-gpt--aget json 'amount)) - (address (nano-gpt--aget json 'address))) - (when (get-buffer "*Dashboard:Info*") - (with-current-buffer "*Dashboard:Info*" - (let ((inhibit-read-only t)) - (goto-char (point-max)) - (insert (format "\n*** Deposit Created\n")) - (insert (format "- txId: %s\n" (or tx-id "N/A"))) - (insert (format "- Amount: %s BTC\n" (or amount "N/A"))) - (when address - (insert (format "- Invoice (raw):\n%s\n" address)) - (insert (format "- Invoice (BOLT-11):\n")) - (insert (qrencode-string address)) - (insert "\n")) - (insert "\n")) - (goto-char (point-min))) - (message "Deposit created: %s" tx-id))))) - -(defun nano-gpt--create-deposit-async (amount) - "Create BTC-LN deposit for AMOUNT." - (let ((api-key (nano-gpt--get-api-key))) - (when api-key - (let ((url-request-method "POST") - (url-request-extra-headers (list (cons "Authorization" (format "Bearer %s" api-key)) - (cons "Content-Type" "application/json"))) - (url-request-data (json-encode (list (cons 'amount amount))))) - (url-retrieve - (concat nano-gpt-api-base-url "/transaction/create/btc-ln") - #'nano-gpt--create-deposit-callback - nil nil t))))) - -(defun nano-gpt--deposit-button-action (_) - "Action for deposit button." - (let* ((satoshis (read-number "Enter amount in satoshis: ")) - (amount (/ satoshis 100000000.0))) - (message "Creating BTC-LN deposit for %s satoshis (%s BTC)..." satoshis amount) - (nano-gpt--create-deposit-async amount))) - -(defun nano-gpt--insert-deposit-button () - "Insert a deposit button in the current buffer." - (insert-text-button "[Deposit BTC-LN]" - 'action (lambda (_) (nano-gpt--deposit-button-action 0.00001)) - 'follow-link t)) - -;; --- Models Mode --- - -(defvar nano-gpt-models-mode-map - (let ((map (make-sparse-keymap))) - (set-keymap-parent map tabulated-list-mode-map) - (define-key map "q" #'nano-gpt-quit) - (define-key map "w" #'nano-gpt-models-copy-id) - map)) - -(defun nano-gpt-models-copy-id () - "Copy the ID of the current model to clipboard." - (interactive) - (when tabulated-list-entries - (let* ((entry (tabulated-list-get-entry)) - (id (aref entry 1))) - (kill-new id) - (message "Copied: %s" id)))) - -(define-derived-mode nano-gpt-models-mode tabulated-list-mode "Models" - "A table of Nano-GPT models." - (hl-line-mode 1) - (setq tabulated-list-format - [("Name" 30 t) - ("ID" 40 t) - ("Created" 10 t) - ("Ctx" 6 t :right-align t :pad-right 3) - ("Pricing" 18 t)]) - (setq tabulated-list-sort-key (cons "Created" t)) - (setq tabulated-list-entries - (mapcar - (lambda (model) - (let* ((pricing (nano-gpt--aget model 'pricing)) - (subscription (nano-gpt--aget model 'subscription)) - (has-sub (eq (nano-gpt--aget subscription 'included) t)) - (face (if has-sub '(:foreground "green") nil))) - (list (nano-gpt--aget model 'id) - (vector - (propertize (or (nano-gpt--aget model 'name) "N/A") 'font-lock-face face) - (propertize (or (nano-gpt--aget model 'id) "N/A") 'font-lock-face face) - (propertize (format-time-string "%Y-%m-%d" (nano-gpt--aget model 'created)) 'font-lock-face face) - (propertize (nano-gpt--format-context-length (nano-gpt--aget model 'context_length)) 'font-lock-face face) - (propertize (nano-gpt--format-pricing - (nano-gpt--aget pricing 'prompt) - (nano-gpt--aget pricing 'completion)) 'font-lock-face face))))) - nano-gpt--models-data)) - (tabulated-list-init-header) - (tabulated-list-print)) - -;; --- Quit function --- - -(defun nano-gpt-quit () - "Quit the dashboard, restore previous window configuration, and kill buffers." - (interactive) - (dolist (buf (list (get-buffer "*Dashboard:Fruits*") - (get-buffer "*Dashboard:Models*") - (get-buffer "*Dashboard:Info*"))) - (when buf (kill-buffer buf))) - (setq nano-gpt--models-data nil) - (when nano-gpt--window-config - (set-window-configuration nano-gpt--window-config) - (setq nano-gpt--window-config nil))) - -;; --- Main entry point --- - -;;;###autoload -(defun nano-gpt-status () - "Show a simple dashboard with two tables and an org buffer." - (interactive) - (setq nano-gpt--window-config (current-window-configuration)) - - (let ((fruits-buf (get-buffer-create "*Dashboard:Fruits*")) - (models-buf (get-buffer-create "*Dashboard:Models*")) - (org-buf (get-buffer-create "*Dashboard:Info*"))) - - (with-current-buffer fruits-buf - (nano-gpt-fruits-mode)) - - (with-current-buffer models-buf - (nano-gpt-models-mode)) - - (with-current-buffer org-buf - (let ((inhibit-read-only t)) - (erase-buffer) - (org-mode) - (org-indent-mode 1) - (insert "* Dashboard\n\n") - (insert "Welcome to the *dashboard*!\n\n") - (insert "** Notes\n") - (insert "- Press ~q~ in any table to quit\n") - (insert "- Table 1 :: Fruits of the world\n") - (insert "- Table 2 :: Nano-GPT Models (loading...)\n\n") - (insert "** Usage\n\n") - (insert "** Balance\n\n") - (insert "** Deposit (BTC-LN)\n") - (nano-gpt--insert-deposit-button) - (insert "\n")) - (goto-char (point-min)) - (use-local-map (let ((map (copy-keymap org-mode-map))) - (define-key map "q" #'nano-gpt-quit) - map))) - - (delete-other-windows) - (let* ((top-left (selected-window)) - (top-right (split-window-right)) - (bottom (split-window-below (/ (window-height) 2)))) - (set-window-buffer top-left fruits-buf) - (set-window-buffer top-right models-buf) - (set-window-buffer bottom org-buf)) - - (select-window (get-buffer-window fruits-buf))) - - (nano-gpt--fetch-models-async) - (nano-gpt--fetch-usage-async) - (nano-gpt--fetch-balance-async)) - -(provide 'nano-gpt-status) - -;;; nano-gpt-status.el ends here