Skip to content

Commit 345e472

Browse files
committed
Render tokens
1 parent 70e4201 commit 345e472

File tree

2 files changed

+138
-11
lines changed

2 files changed

+138
-11
lines changed

Eask

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
(depends-on "lv")
2020
(depends-on "ht")
2121
(depends-on "markdown-mode")
22+
(depends-on "spinner")
2223

2324
(setq network-security-level 'low) ; see https://github.com/jcs090218/setup-emacs-windows/issues/156#issuecomment-932956432

chatgpt.el

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
;; Maintainer: Shen, Jen-Chieh <[email protected]>
77
;; URL: https://github.com/emacs-openai/chatgpt
88
;; Version: 0.1.0
9-
;; Package-Requires: ((emacs "26.1") (openai "0.1.0") (lv "0.0") (ht "2.0") (markdown-mode "2.1"))
9+
;; Package-Requires: ((emacs "26.1") (openai "0.1.0") (lv "0.0") (ht "2.0") (markdown-mode "2.1") (spinner "1.7.4"))
1010
;; Keywords: comm openai
1111

1212
;; This file is not part of GNU Emacs.
@@ -38,6 +38,7 @@
3838
(require 'lv)
3939
(require 'ht)
4040
(require 'markdown-mode)
41+
(require 'spinner)
4142

4243
(defgroup chatgpt nil
4344
"Use ChatGPT inside Emacs."
@@ -71,6 +72,22 @@
7172
:type 'boolean
7273
:group 'chatgpt)
7374

75+
(defcustom chatgpt-spinner-type 'moon
76+
"The type of the spinner."
77+
:type '(choice (const :tag "Key to variable `spinner-types'" symbol)
78+
(const :tag "Vector of characters" vector))
79+
:group 'openai)
80+
81+
(defcustom chatgpt-display-tokens-info t
82+
"Non-nil we display tokens infomration for each request."
83+
:type 'boolean
84+
:group 'chatgpt)
85+
86+
(defcustom chatgpt-priority 100
87+
"Overlays' priority."
88+
:type 'integer
89+
:group 'chatgpt)
90+
7491
(defconst chatgpt-buffer-name-format "*ChatGPT: <%s>*"
7592
"Name of the buffer to use for the `chatgpt' instance.")
7693

@@ -86,6 +103,12 @@
86103
(defvar-local chatgpt-requesting-p nil
87104
"Non-nil when requesting; waiting for the response.")
88105

106+
(defvar-local chatgpt-spinner-counter 0
107+
"Spinner counter.")
108+
109+
(defvar-local chatgpt-spinner-timer nil
110+
"Spinner timer.")
111+
89112
(defvar-local chatgpt-data (ht-create)
90113
"Store other information other than messages.")
91114

@@ -97,6 +120,16 @@
97120
"Face used for user."
98121
:group 'chatgpt)
99122

123+
(defface chatgpt-tip
124+
'((t :foreground "#848484"))
125+
"Face used for tip."
126+
:group 'chatgpt)
127+
128+
(defface chatgpt-info
129+
'((t :height 0.8 :foreground "#999999"))
130+
"Face added to codemetrics display."
131+
:group 'chatgpt)
132+
100133
;;
101134
;;; Util
102135

@@ -114,6 +147,26 @@
114147
window-state-change-hook)
115148
,@body))
116149

150+
;; TODO: Use function `string-pixel-width' after 29.1
151+
(defun chatgpt--string-pixel-width (str)
152+
"Return the width of STR in pixels."
153+
(if (fboundp #'string-pixel-width)
154+
(string-pixel-width str)
155+
(require 'shr)
156+
(shr-string-pixel-width str)))
157+
158+
(defun chatgpt--str-len (str)
159+
"Calculate STR in pixel width."
160+
(let ((width (frame-char-width))
161+
(len (chatgpt--string-pixel-width str)))
162+
(+ (/ len width)
163+
(if (zerop (% len width)) 0 1)))) ; add one if exceeed
164+
165+
(defun chatgpt--align (&rest lengths)
166+
"Align sideline string by LENGTHS from the right of the window."
167+
(list (* (window-font-width)
168+
(+ (apply #'+ lengths) (if (display-graphic-p) 1 2)))))
169+
117170
(defun chatgpt--kill-buffer (buffer-or-name)
118171
"Like function `kill-buffer' (BUFFER-OR-NAME) but in the safe way."
119172
(when-let ((buffer (get-buffer buffer-or-name)))
@@ -133,14 +186,33 @@ Display buffer from BUFFER-OR-NAME."
133186
"user" ; this is free?
134187
openai-user))
135188

189+
;;
190+
;;; Spinner
191+
192+
(defun chatgpt-mode--cancel-timer ()
193+
"Cancel spinner timer."
194+
(when (timerp chatgpt-spinner-timer)
195+
(cancel-timer chatgpt-spinner-timer)))
196+
197+
(defun chatgpt--start-spinner ()
198+
"Start spinner."
199+
(chatgpt-mode--cancel-timer)
200+
(setq chatgpt-spinner-counter 0
201+
chatgpt-spinner-timer (run-with-timer (/ spinner-frames-per-second 60.0)
202+
(/ spinner-frames-per-second 60.0)
203+
(lambda ()
204+
(cl-incf chatgpt-spinner-counter)
205+
(force-mode-line-update)))))
206+
136207
;;
137208
;;; Instances
138209

139210
(defmacro chatgpt-with-instance (instance &rest body)
140211
"Execute BODY within instance."
141212
(declare (indent 1))
142-
`(when-let ((buffer (and ,instance
143-
(get-buffer (cdr ,instance)))))
213+
`(when-let* ((buffer (and ,instance
214+
(get-buffer (cdr ,instance))))
215+
((buffer-live-p buffer)))
144216
(with-current-buffer buffer
145217
(let ((inhibit-read-only t))
146218
,@body))))
@@ -195,6 +267,30 @@ Display buffer from BUFFER-OR-NAME."
195267
;;
196268
;;; Core
197269

270+
(defun chatgpt--get-face-height ()
271+
"Make sure we get the face height."
272+
(let ((height (face-attribute 'chatgpt-info :height)))
273+
(if (numberp height) height
274+
1)))
275+
276+
(defun chatgpt--create-tokens-overlay (prompt-tokens completion-tokens total-tokens)
277+
"Display tokens information."
278+
(when chatgpt-display-tokens-info
279+
(let* ((ov (make-overlay (1- (point)) (1- (point)) nil t t))
280+
(content (format "prompt %s, completion %s, total: %s"
281+
prompt-tokens completion-tokens total-tokens))
282+
(content-len (* (chatgpt--str-len content)
283+
(chatgpt--get-face-height)))
284+
(str (concat
285+
(propertize " " 'display
286+
`((space :align-to (- right ,(chatgpt--align (1- content-len))))
287+
(space :width 0))
288+
`cursor t)
289+
(propertize content 'face 'chatgpt-info))))
290+
(overlay-put ov 'chatgpt t)
291+
(overlay-put ov 'priority chatgpt-priority)
292+
(overlay-put ov 'after-string str))))
293+
198294
(defun chatgpt--add-tokens (data)
199295
"Record all tokens information from DATA."
200296
(let-alist data
@@ -206,7 +302,9 @@ Display buffer from BUFFER-OR-NAME."
206302
;; Then we add it up!
207303
(ht-set chatgpt-data 'prompt_tokens (+ prompt-tokens .prompt_tokens))
208304
(ht-set chatgpt-data 'completion_tokens (+ completion-tokens .completion_tokens))
209-
(ht-set chatgpt-data 'total_tokens (+ total-tokens .total_tokens))))))
305+
(ht-set chatgpt-data 'total_tokens (+ total-tokens .total_tokens))
306+
;; Render it!
307+
(chatgpt--create-tokens-overlay .prompt_tokens .completion_tokens .total_tokens)))))
210308

211309
(defun chatgpt--add-message (role content)
212310
"Add a message to history.
@@ -252,6 +350,8 @@ The data is consist of ROLE and CONTENT."
252350

253351
(defun chatgpt--display-messages ()
254352
"Display all messages to latest response."
353+
(when (zerop chatgpt--display-pointer) ; clear up the tip message
354+
(erase-buffer))
255355
(while (< chatgpt--display-pointer (length chatgpt-chat-history))
256356
(let ((message (elt chatgpt-chat-history chatgpt--display-pointer)))
257357
(let-alist message
@@ -280,13 +380,16 @@ The data is consist of ROLE and CONTENT."
280380
(chatgpt-with-instance instance
281381
(chatgpt--display-messages)) ; display it
282382
(setq chatgpt-requesting-p t)
383+
(chatgpt--start-spinner)
283384
(openai-chat chatgpt-chat-history
284385
(lambda (data)
285386
(chatgpt-with-instance instance
286387
(setq chatgpt-requesting-p nil)
287-
(chatgpt--add-tokens data)
288-
(chatgpt--add-response-messages data)
289-
(chatgpt--display-messages)))
388+
(chatgpt-mode--cancel-timer)
389+
(unless openai-error
390+
(chatgpt--add-response-messages data)
391+
(chatgpt--display-messages)
392+
(chatgpt--add-tokens data))))
290393
:model chatgpt-model
291394
:max-tokens chatgpt-max-tokens
292395
:temperature chatgpt-temperature
@@ -366,7 +469,7 @@ The data is consist of ROLE and CONTENT."
366469

367470
(defun chatgpt-input-header-line ()
368471
"The display for input header line."
369-
(format " Session: %s" (cdr chatgpt-input-instance)))
472+
(format " [Session] %s" (cdr chatgpt-input-instance)))
370473

371474
(defvar chatgpt-input-mode-map
372475
(let ((map (make-sparse-keymap)))
@@ -375,7 +478,6 @@ The data is consist of ROLE and CONTENT."
375478
map)
376479
"Keymap for `chatgpt-mode'.")
377480

378-
;;;###autoload
379481
(define-derived-mode chatgpt-input-mode fundamental-mode "ChatGPT Input"
380482
"Major mode for `chatgpt-input-mode'.
381483
@@ -426,6 +528,8 @@ The data is consist of ROLE and CONTENT."
426528

427529
(defun chatgpt-mode--kill-buffer-hook ()
428530
"Kill buffer hook."
531+
(ht-clear chatgpt-data)
532+
(chatgpt-mode--cancel-timer)
429533
(let ((instance chatgpt-instances))
430534
(when (get-buffer chatgpt-input-buffer-name)
431535
(with-current-buffer chatgpt-input-buffer-name
@@ -435,7 +539,16 @@ The data is consist of ROLE and CONTENT."
435539

436540
(defun chatgpt-header-line ()
437541
"The display for header line."
438-
(format " Session: %s, History: %s, User: %s (M-x chatgpt-info)"
542+
(format " %s[Session] %s [History] %s [User] %s"
543+
(if chatgpt-requesting-p
544+
(let* ((spinner (if (symbolp chatgpt-spinner-type)
545+
(cdr (assoc chatgpt-spinner-type spinner-types))
546+
chatgpt-spinner-type))
547+
(len (length spinner)))
548+
(when (<= len chatgpt-spinner-counter)
549+
(setq chatgpt-spinner-counter 0))
550+
(format "%s " (elt spinner chatgpt-spinner-counter)))
551+
"")
439552
(cdr chatgpt-instance)
440553
(length chatgpt-chat-history)
441554
(chatgpt-user)))
@@ -446,6 +559,17 @@ The data is consist of ROLE and CONTENT."
446559
map)
447560
"Keymap for `chatgpt-mode'.")
448561

562+
(defun chatgpt-mode-insert-tip ()
563+
"Insert tip to output buffer."
564+
(when (string-empty-p (buffer-string))
565+
(let ((inhibit-read-only t)
566+
(tip "Press <return> to start asking questions
567+
568+
`M-x chatgpt-info` will print out more information about the current session!
569+
"))
570+
(add-face-text-property 0 (length tip) 'chatgpt-tip nil tip)
571+
(insert tip))))
572+
449573
;;;###autoload
450574
(define-derived-mode chatgpt-mode fundamental-mode "ChatGPT"
451575
"Major mode for `chatgpt-mode'.
@@ -454,7 +578,9 @@ The data is consist of ROLE and CONTENT."
454578
(setq-local buffer-read-only t)
455579
(font-lock-mode -1)
456580
(add-hook 'kill-buffer-hook #'chatgpt-mode--kill-buffer-hook nil t)
457-
(setq-local header-line-format `((:eval (chatgpt-header-line)))))
581+
(setq-local header-line-format `((:eval (chatgpt-header-line))))
582+
(setq-local chatgpt-data (ht-create))
583+
(chatgpt-mode-insert-tip))
458584

459585
(defun chatgpt-register-instance (index buffer-or-name)
460586
"Register BUFFER-OR-NAME with INDEX as an instance.

0 commit comments

Comments
 (0)