mirror of https://github.com/jlelse/GoBlog
193 lines
4.6 KiB
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
|
|
}
|