Skip to content

Commit

Permalink
Merge pull request #238 from s-kostyaev/ux-improvements
Browse files Browse the repository at this point in the history
Improve transient menu
  • Loading branch information
s-kostyaev authored Feb 23, 2025
2 parents 1c52902 + f94002a commit ad88edf
Show file tree
Hide file tree
Showing 5 changed files with 355 additions and 52 deletions.
7 changes: 7 additions & 0 deletions NEWS.org
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
* Version 1.4.0
- Improved transient menus.
- Added ellama-session-kill functionality.
- Added community prompt collection feature.
- Refactored ~ellama-context-mode~ to be a major mode.
- Added functionality to remove context elements by name.
- Added option to always show context line in header or mode line.
* Version 1.3.0
- Implemented ellama context header line and mode line features.
- Added ~ellama-context-header-line-mode~, ~ellama-context-mode-line-mode~ and
Expand Down
20 changes: 20 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ Delete ellama session.

Change current active session.

*** ellama-session-kill

Select and kill one of active sessions.

*** ellama-session-rename

Rename current ellama session.
Expand Down Expand Up @@ -320,6 +324,16 @@ provides much better results on reasoning tasks using AoT.
Solve domain specific problem with simple chain. It makes LLMs act
like a professional and adds a planning step.

*** ellama-community-prompts-select-blueprint

Select a prompt from the community prompt collection.
The user is prompted to choose a role, and then a
corresponding prompt is inserted into a blueprint buffer.

*** ellama-community-prompts-update-variables

Prompt user for values of variables found in current buffer and update them.

** Keymap

In any buffer where there is active ellama streaming, you can press
Expand Down Expand Up @@ -444,6 +458,12 @@ argument generated text string.
~display-buffer-same-window~.
- ~ellama-preview-context-element-display-action-function~: Display
action function for ~ellama-preview-context-element~.
- ~ellama-context-line-always-visible~: Make context header or mode line always
visible, even with empty context.
- ~ellama-community-prompts-url~: The URL of the community prompts collection.
- ~ellama-community-prompts-file~: Path to the CSV file containing community prompts.
This file is expected to be located inside an ~ellama~ subdirectory
within your ~user-emacs-directory~.

** Minor modes

Expand Down
200 changes: 200 additions & 0 deletions ellama-community-prompts.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
;;; ellama-community-prompts.el --- Community prompt collection -*- lexical-binding: t; package-lint-main-file: "ellama.el"; -*-

;; Copyright (C) 2023-2025 Free Software Foundation, Inc.

;; Author: Sergey Kostyaev <[email protected]>
;; SPDX-License-Identifier: GPL-3.0-or-later

;; This file 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, or (at your option)
;; any later version.

;; This file 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Ellama is a tool for interacting with large language models from Emacs.
;; It allows you to ask questions and receive responses from the
;; LLMs. Ellama can perform various tasks such as translation, code
;; review, summarization, enhancing grammar/spelling or wording and
;; more through the Emacs interface. Ellama natively supports streaming
;; output, making it effortless to use with your preferred text editor.
;;

;;; Code:
(require 'plz)
(require 'ellama)

(defcustom ellama-community-prompts-url "https://raw.githubusercontent.com/f/awesome-chatgpt-prompts/main/prompts.csv"
"The URL of the community prompts collection."
:type 'string
:group 'ellama)

(defcustom ellama-community-prompts-file (expand-file-name
"community-prompts.csv"
(file-name-concat
user-emacs-directory
"ellama"))
"Path to the CSV file containing community prompts.
This file is expected to be located inside an `ellama' subdirectory
within your `user-emacs-directory'."
:type 'file
:group 'ellama)

(defun ellama-community-prompts-ensure-file ()
"Ensure that the community prompt collection file is downloaded.
Downloads the file from `ellama-community-prompts-url` if it does
not already exist."
(unless (file-exists-p ellama-community-prompts-file)
(let* ((directory (file-name-directory ellama-community-prompts-file))
(response (plz 'get ellama-community-prompts-url
:as 'file
:then (lambda (filename)
(rename-file filename ellama-community-prompts-file t))
:else (lambda (error)
(message "Failed to download community prompts: %s" error)))))
(when (and response (not (file-directory-p directory)))
(make-directory directory t))
(when response
(message "Community prompts file downloaded successfully.")))))

(defun ellama-community-prompts-parse-csv-line (line)
"Parse a single CSV LINE into a list of fields, handling quotes.
LINE is the string to be parsed."
(let ((i 0)
(len (length line)))
(cl-loop
with fields = '()
with current-field = ""
with inside-quotes = nil
while (< i len)
do (let ((char (aref line i)))
(cond
;; Opening quote (start of field)
((and (eq char ?\") (not inside-quotes))
(setq inside-quotes t)
(cl-incf i))
;; Closing quote (end of field or escaped quote)
((and (eq char ?\") inside-quotes)
(if (and (< (1+ i) len) (eq (aref line (1+ i)) ?\"))
(progn ; Escaped quote: add single quote, skip next character
(setq current-field (concat current-field "\""))
(cl-incf i 2))
(setq inside-quotes nil) ; End of quoted field
(cl-incf i)))
;; Comma separator (outside quotes)
((and (eq char ?,) (not inside-quotes))
(push current-field fields)
(setq current-field "")
(cl-incf i))
;; Regular character
(t
(setq current-field (concat current-field (string char)))
(cl-incf i))))
;; Add the last field after loop ends
finally return (nreverse (cons current-field fields)))))

(defun ellama-community-prompts-convert-to-plist (parsed-line)
"Convert PARSED-LINE to plist.
PARSED-LINE is expected to be a list with three elements: :act,
:prompt, and :for-devs."
(let ((act (cl-first parsed-line))
(prompt (cl-second parsed-line))
(for-devs (string= "TRUE" (cl-third parsed-line))))
`(:act ,act :prompt ,prompt :for-devs ,for-devs)))

(defvar ellama-community-prompts-collection nil
"Community prompts collection.")

(defun ellama-community-prompts-ensure ()
"Ensure that the community prompt collection are loaded and available.
This function ensures that the file specified by `ellama-community-prompts-file'
is read and parsed, and the resulting collection of prompts is stored in
`ellama-community-prompts-collection'. If the collection is already populated,
this function does nothing.
Returns the collection of community prompts."
(ellama-community-prompts-ensure-file)
(unless ellama-community-prompts-collection
(setq ellama-community-prompts-collection
(let ((buf (find-file-noselect ellama-community-prompts-file)))
(with-current-buffer buf
(mapcar (lambda (line)
(ellama-community-prompts-convert-to-plist
(ellama-community-prompts-parse-csv-line
line)))
(cdr (string-lines
(buffer-substring-no-properties
(point-min) (point-max)))))))))
ellama-community-prompts-collection)

(defvar ellama-community-prompts-blurpint-buffer " *ellama-community-prompts-blueprint-buffer*"
"Buffer for community prompt blueprint.")

;;;###autoload
(defun ellama-community-prompts-select-blueprint (&optional for-devs)
"Select a prompt from the community prompt collection.
The user is prompted to choose a role, and then a
corresponding prompt is inserted into a blueprint buffer.
Optional argument FOR-DEVS filters prompts for developers."
(interactive "P")
(let ((acts '())
selected-act selected-prompt)
;; Collect unique acts from the filtered collection
(dolist (prompt (ellama-community-prompts-ensure))
(when (or (not for-devs) (eq for-devs (plist-get prompt :for-devs)))
(cl-pushnew (plist-get prompt :act) acts)))
;; Prompt user to select an act
(setq selected-act (completing-read "Select Act: " acts))
;; Find the corresponding prompt
(catch 'found-prompt
(dolist (prompt ellama-community-prompts-collection)
(when (and (string= selected-act (plist-get prompt :act))
(or (not for-devs) (eq for-devs (plist-get prompt :for-devs))))
(setq selected-prompt (plist-get prompt :prompt))
(throw 'found-prompt nil))))
;; Create a new buffer and insert the selected prompt
(with-current-buffer (get-buffer-create ellama-community-prompts-blurpint-buffer)
(erase-buffer)
(let ((hard-newline t))
(insert selected-prompt)
(fill-region (point-min) (point-max))
(ellama-blueprint-mode))
(switch-to-buffer (current-buffer))
(ellama-community-prompts-update-variables))))

(defun ellama-community-prompts-get-variable-list ()
"Return a deduplicated list of variables found in the current buffer."
(save-excursion
(let ((vars '()))
(goto-char (point-min))
(while (re-search-forward "\{\\([^}]+\\)}" nil t)
(push (match-string 1) vars))
(seq-uniq vars))))

(defun ellama-community-prompts-set-variable (var value)
"Replace VAR with VALUE in blueprint buffer."
(save-excursion
(goto-char (point-min))
(while (search-forward (format "{%s}" var) nil t)
(replace-match value))))

;;;###autoload
(defun ellama-community-prompts-update-variables ()
"Prompt user for values of variables found in current buffer and update them."
(interactive)
(let ((vars (ellama-community-prompts-get-variable-list)))
(dolist (var vars)
(let ((value (read-string (format "Enter value for {%s}: " var))))
(ellama-community-prompts-set-variable var value)))))

(provide 'ellama-community-prompts)
;;; ellama-community-prompts.el ends here.
Loading

0 comments on commit ad88edf

Please sign in to comment.