GoBlog/plugins/aitldr/src/aitldr/aitldr.go

193 lines
4.6 KiB
Go

package aitldr
import (
"context"
"log"
"net/http"
"strings"
"sync"
"github.com/PuerkitoBio/goquery"
"github.com/carlmjohnson/requests"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/htmlbuilder"
"go.goblog.app/app/pkgs/plugintypes"
)
type plugin struct {
app plugintypes.App
config map[string]any
initCSS sync.Once
}
func GetPlugin() (
plugintypes.SetConfig, plugintypes.SetApp,
plugintypes.PostCreatedHook, plugintypes.PostUpdatedHook,
plugintypes.UIPost, plugintypes.UI2,
) {
p := &plugin{}
return p, p, p, p, p, p
}
func (p *plugin) SetApp(app plugintypes.App) {
p.app = app
}
func (p *plugin) SetConfig(config map[string]any) {
p.config = config
}
func (p *plugin) PostCreated(post plugintypes.Post) {
p.summarize(post)
}
func (p *plugin) PostUpdated(post plugintypes.Post) {
p.summarize(post)
}
const postParam = "aitldr"
func (p *plugin) RenderPost(renderContext plugintypes.RenderContext, post plugintypes.Post, doc *goquery.Document) {
tldr := post.GetFirstParameterValue(postParam)
if tldr == "" {
return
}
title := "AI generated summary:"
if blogConfig, ok := p.config[renderContext.GetBlog()]; ok {
if blogConfigAsMap, ok := blogConfig.(map[string]any); ok {
if blogSpecificTitle, ok := blogConfigAsMap["title"]; ok {
if blogSpecificTitleAsString, ok := blogSpecificTitle.(string); ok {
title = blogSpecificTitleAsString
}
}
}
}
buf := bufferpool.Get()
defer bufferpool.Put(buf)
hw := htmlbuilder.NewHtmlBuilder(buf)
hw.WriteElementOpen("div", "class", "p aitldr")
hw.WriteElementOpen("b")
hw.WriteEscaped(title)
hw.WriteElementClose("b")
hw.WriteEscaped(" ")
hw.WriteElementOpen("i")
hw.WriteEscaped(tldr)
hw.WriteElementsClose("i", "div")
doc.Find(".e-content").BeforeHtml(buf.String())
}
const customCSS = ".aitldr { border: 1px dashed; padding: 1em; }"
func (p *plugin) RenderWithDocument(_ plugintypes.RenderContext, doc *goquery.Document) {
if p.app == nil {
return
}
// Init custom CSS for plugin
p.initCSS.Do(func() {
_ = p.app.CompileAsset("aitldr.css", strings.NewReader(customCSS))
})
// Check if page has AI TLDR, then add the custom CSS
doc.Find(".aitldr").First().Each(func(_ int, _ *goquery.Selection) {
buf := bufferpool.Get()
defer bufferpool.Put(buf)
hb := htmlbuilder.NewHtmlBuilder(buf)
hb.WriteElementOpen("link", "rel", "stylesheet", "href", p.app.AssetPath("aitldr.css"))
doc.Find("head").AppendHtml(buf.String())
})
}
type apiMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}
type apiResponse struct {
Choices []struct {
Message apiMessage `json:"message"`
} `json:"choices"`
}
func (p *plugin) summarize(post plugintypes.Post) {
if post.GetFirstParameterValue("noaitldr") == "true" {
log.Println("aitldr: Skip summarizing", post.GetPath())
return
}
apikey := ""
if k, ok := p.config["apikey"]; ok {
if ks, ok := k.(string); ok {
apikey = ks
}
}
if apikey == "" {
log.Println("Config for aitldr plugin not correct! apikey missing!")
return
}
var response apiResponse
err := requests.URL("https://api.openai.com/v1/chat/completions").
Method(http.MethodPost).
Header("Authorization", "Bearer "+apikey).
BodyJSON(map[string]any{
"model": "gpt-3.5-turbo",
"temperature": 0,
"max_tokens": 200,
"messages": []apiMessage{
{
Role: "user",
Content: p.createPrompt(post),
},
},
}).
ToJSON(&response).
Fetch(context.Background())
if err != nil {
log.Println("aitldr plugin:", err.Error())
return
}
if len(response.Choices) < 1 {
return
}
summary := response.Choices[0].Message.Content
summary = strings.TrimSpace(summary)
err = p.app.SetPostParameter(post.GetPath(), postParam, []string{summary})
if err != nil {
log.Println("aitldr plugin:", err.Error())
return
}
p.app.PurgeCache()
}
func (p *plugin) createPrompt(post plugintypes.Post) string {
lang := "en"
if blog, ok := p.app.GetBlog(post.GetBlog()); ok {
if blogLang := blog.GetLanguage(); lang != "" {
lang = blogLang
}
}
prompt := "Summarize the content of following text in one sentence in the language \"" + lang + "\". Answer with just the summary.\n\n\n"
if title, err := p.app.RenderMarkdownAsText(post.GetTitle()); err == nil && title != "" {
prompt += title + "\n\n"
} else if err != nil {
log.Println("aitldr plugin: Rendering markdown as text failed:", err.Error())
}
if text, err := p.app.RenderMarkdownAsText(post.GetContent()); err == nil && text != "" {
prompt += text + "\n\n"
} else if err != nil {
log.Println("aitldr plugin: Rendering markdown as text failed:", err.Error())
}
return prompt
}