;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (in-package #:live-chat-ui) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar *default-prefix* "/lisp-chat") (defun set-subpath-prefix (prefix) (setf *default-prefix* prefix)) (defun subpath-prefix (path) (format nil "~a~a" *default-prefix* path)) (defun static-asset-string (path) (uiop:read-file-string (merge-pathnames path (asdf:system-relative-pathname :live-chat #P"static/")))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Render chat messages (defun generate-html-message (content) (cl-who:with-html-output-to-string (*standard-output*) (:div :class "block" :hx-swap-oob "afterbegin:#chat-messages" (:div :class "box" :style "overflow: auto; white-space: nowrap;" (cl-who:str content))))) (defun render-chat-messages () "Render the list of chat messages as HTML." (let ((messages (fetch-messages))) (cl-who:with-html-output-to-string (*standard-output* nil :indent nil) (loop for msg in messages collect (cl-who:str (generate-html-message msg)))))) ;; Render chat UI (defun render-chat-ui () "Render the main chat page with HTMX integration." (cl-who:with-html-output-to-string (*standard-output* nil :prologue t :indent nil) (:html :lang "en" :class "has-navbar-fixed-bottom" (:head (:meta :charset "utf-8") (:meta :name "viewport" :content "width=device-width, initial-scale=1") ;; (:meta :http-equiv "Content-Security-Policy" ;; :content "default-src *; style-src self unsafe-inline; script-src self unsafe-inline unsafe-eval https://phntsm.ddns.net") (:title "Live Chat") (:style (cl-who:str (static-asset-string #P"bulma.min.css"))) ;; (:link :rel "stylesheet" ;; :href "https://cdnjs.cloudflare.com/ajax/libs/bulma/1.0.2/css/bulma.min.css" ;; :integrity "sha512-RpeJZX3aH5oZN3U3JhE7Sd+HG8XQsqmP3clIbu4G28p668yNsRNj3zMASKe1ATjl/W80wuEtCx2dFA8xaebG5w==" ;; :crossorigin "anonymous" ;; :referrerpolicy "no-referrer") (:script (cl-who:str (static-asset-string #P"htmx.min.js"))) ;; :integrity "sha512-dQu3OKLMpRu85mW24LA1CUZG67BgLPR8Px3mcxmpdyijgl1UpCM1RtJoQP6h8UkufSnaHVRTUx98EQT9fcKohw" ;; :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/htmx.min.js" ;; :crossorigin "anonymous" ;; :referrerpolicy "no-referrer") (:script (cl-who:str (static-asset-string #P"ws.min.js")))) ;; :src "https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/ext/ws.min.js" ;; :integrity "sha512-1OIiXEswZd/etj60BUwFmyoi0OhrWdoYlzayJpSBivoMV0pQPIQr+vtAn3W3htsbWtLRU8DrBl0epdK4DQbj/w" ;; :crossorigin "anonymous" ;; :referrerpolicy "no-referrer")) (:body (:section :class "section" (:div :class "container" (:div :class "container" :id "chat-messages" :hx-get "/chat-messages" (cl-who:str (render-chat-messages))))) (:nav :role "navigation" :class "navbar is-fixed-bottom is-spaced has-shadow" :aria-label "main navigation" (:div :class "navbar-brand" (:a :class "navbar-item" :href "https://phntsm.ddns.net/" (:img :src "/favicon.png" :alt "icon")) (:div :class "navbar-item" (:div :class "columns is-mobile" (:div :class "column" (:button :class "button is-link" :onclick "moreInfo();" "More Info 1")) (:div :class "column" (:button :class "button is-link" :onclick "moreInfo();" "More Info 2")) (:div :class "column" (:button :class "button is-link" :onclick "moreInfo();" "More Info 3")))) (:a :role "button" :class "navbar-burger" :aria-label "menu" :aria-expanded "false" :data-target "navbarBasicExample" (:span :aria-hidden "true") (:span :aria-hidden "true") (:span :aria-hidden "true") (:span :aria-hidden "true"))) (:div :class "navbar-menu is-active" :id "navbarBasicExample" (:div :class "navbar-start" :style "display: none;") (:div :class "container navbar-middle" (:div :class "navbar-item is-expanded" :hx-ext "ws" :ws-connect (subpath-prefix "/ws-chat-messages") (:form :class "form is-expanded" :style "width:100%;" :id "chat-form" :name "chat-form" :ws-send "" ;; :method :post ;; :action (subpath-prefix "/post-message") ;; :hx-post (subpath-prefix "/post-message") ;; :hx-target "#chat-messages" (:div :class "field has-addons is-expanded" (:div :class "control is-expanded" (:input :class "input is-expanded" :id "chat-input" :form "chat-form" :autocomplete "off" :placeholder "Enter your message..." :type "text" :name "message")) (:div :class "control" (:button :class "button is-link is-light" :id "submit-button" :form "chat-form" :type "submit" "Send")))))) (:div :class "navbar-end" :style "display: none;"))) (:script (cl-who:str (js-snippet (subpath-prefix "/ws-chat-messages")))))))) (defun js-snippet (path) (format nil (static-asset-string #P"chat.js") path))