| @@ -58,6 +58,43 @@ Class AdminController_Class | |||
| <% | |||
| End Sub | |||
| '--------------------------------------------------------------- | |||
| ' Action: AIPrompt | |||
| '--------------------------------------------------------------- | |||
| Public Sub AIPrompt() | |||
| m_title = "AI Prompt Settings" | |||
| Dim promptTemplate : promptTemplate = GetGenerationPromptTemplate() | |||
| %> | |||
| <!--#include file="../views/Admin/ai-prompt.asp" --> | |||
| <% | |||
| End Sub | |||
| '--------------------------------------------------------------- | |||
| ' Action: UpdateAIPrompt | |||
| '--------------------------------------------------------------- | |||
| Public Sub UpdateAIPrompt() | |||
| Dim promptTemplate : promptTemplate = Trim(Request.Form("PromptTemplate")) | |||
| If Len(promptTemplate) = 0 Then | |||
| Flash().AddError "Prompt template cannot be empty." | |||
| Response.Redirect "/admin/ai-prompt" | |||
| Exit Sub | |||
| End If | |||
| On Error Resume Next | |||
| UpdateAppSetting "AbacusGenerationPrompt", promptTemplate | |||
| If Err.Number <> 0 Then | |||
| Flash().AddError "Unable to save prompt template: " & Err.Description | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| Response.Redirect "/admin/ai-prompt" | |||
| Exit Sub | |||
| End If | |||
| On Error GoTo 0 | |||
| Flash().Success = "AI prompt template saved." | |||
| Response.Redirect "/admin/ai-prompt" | |||
| End Sub | |||
| '--------------------------------------------------------------- | |||
| ' Action: PublishPost | |||
| '--------------------------------------------------------------- | |||
| @@ -166,10 +203,7 @@ Class AdminController_Class | |||
| Dim systemPrompt, userPrompt, payload, responseText, parsed, choices, choice, message, content, contentJson | |||
| systemPrompt = "You write clear, engaging blog post content for a classic ASP blog. Return only valid JSON with two keys: summary and body. Summary must be 1 to 2 sentences. Body must be 3 to 5 short paragraphs separated by blank lines. Do not use markdown fences, bullets, or code blocks." | |||
| userPrompt = "Create blog content for this post title: " & SafeText(post.Title) & vbCrLf & _ | |||
| "Existing summary: " & SafeText(post.Summary) & vbCrLf & _ | |||
| "Existing body: " & SafeText(post.Body) & vbCrLf & _ | |||
| "Keep the title unchanged. Make the content readable and helpful for a general audience." | |||
| userPrompt = BuildGenerationPrompt(post) | |||
| payload = "{""model"":""" & JsonEscape(modelName) & """,""messages"":[{""role"":""system"",""content"":""" & JsonEscape(systemPrompt) & """},{""role"":""user"",""content"":""" & JsonEscape(userPrompt) & """}],""temperature"":0.7}" | |||
| @@ -235,14 +269,6 @@ Class AdminController_Class | |||
| End If | |||
| End Function | |||
| Private Function SafeText(ByVal value) | |||
| If IsNull(value) Or IsEmpty(value) Then | |||
| SafeText = "" | |||
| Else | |||
| SafeText = CStr(value) | |||
| End If | |||
| End Function | |||
| End Class | |||
| Dim AdminController_Class__Singleton | |||
| @@ -53,7 +53,7 @@ Class CommentsController_Class | |||
| Dim slug : slug = ResolvePostSlug(comment.PostID) | |||
| If Len(slug) > 0 Then | |||
| Response.Redirect "/posts/" & Server.URLEncode(slug) | |||
| Response.Redirect PostUrl(slug) | |||
| Else | |||
| Response.Redirect "/posts" | |||
| End If | |||
| @@ -82,12 +82,13 @@ Class CommentsController_Class | |||
| Private Function ResolvePostSlug(ByVal postID) | |||
| Dim slug : slug = Trim(Request.Form("Slug")) | |||
| If Len(slug) = 0 Then slug = Trim(Request.Form("PostSlug")) | |||
| slug = NormalizeSlug(slug) | |||
| If Len(slug) = 0 And CLng(postID) > 0 Then | |||
| Dim post | |||
| On Error Resume Next | |||
| Set post = PostsRepository().FindByID(postID) | |||
| If Err.Number = 0 Then slug = CStr(post.Slug) | |||
| If Err.Number = 0 Then slug = NormalizeSlug(post.Slug) | |||
| Err.Clear | |||
| On Error GoTo 0 | |||
| End If | |||
| @@ -45,12 +45,19 @@ Class PostsController_Class | |||
| <% | |||
| End Sub | |||
| '--------------------------------------------------------------- | |||
| ' Action: Show | |||
| '--------------------------------------------------------------- | |||
| '--------------------------------------------------------------- | |||
| ' Action: Show | |||
| '--------------------------------------------------------------- | |||
| Public Sub Show(ByVal slug) | |||
| Dim requestedSlug : requestedSlug = Trim(CStr(slug)) | |||
| Dim canonicalSlug : canonicalSlug = NormalizeSlug(requestedSlug) | |||
| Dim matches | |||
| Set matches = PostsRepository().Find(Array("Slug", slug, "IsPublished", 1), Empty) | |||
| Set matches = PostsRepository().Find(Array("Slug", requestedSlug, "IsPublished", 1), Empty) | |||
| If matches.Count = 0 And canonicalSlug <> requestedSlug Then | |||
| Set matches = PostsRepository().Find(Array("Slug", canonicalSlug, "IsPublished", 1), Empty) | |||
| End If | |||
| If matches.Count = 0 Then | |||
| Response.Status = "404 Not Found" | |||
| @@ -63,6 +70,10 @@ Class PostsController_Class | |||
| Dim post | |||
| Set post = matches.Front() | |||
| If Len(canonicalSlug) > 0 And canonicalSlug <> requestedSlug Then | |||
| Response.Redirect PostUrl(canonicalSlug) | |||
| End If | |||
| Dim comments | |||
| Set comments = CommentsRepository().Find(Array("PostID", post.PostID, "IsApproved", 1), "CreatedDate") | |||
| %> | |||
| @@ -0,0 +1,25 @@ | |||
| <div class="row"> | |||
| <div class="col-lg-10 col-xl-8"> | |||
| <div class="card shadow-sm"> | |||
| <div class="card-body"> | |||
| <h1 class="h3 mb-2">AI Prompt Settings</h1> | |||
| <p class="text-muted mb-4">Edit the template used when generating AI post content.</p> | |||
| <form method="post" action="<%= AdminAIPromptUpdateUrl() %>"> | |||
| <div class="mb-3"> | |||
| <label class="form-label" for="PromptTemplate">Prompt Template</label> | |||
| <textarea class="form-control" id="PromptTemplate" name="PromptTemplate" rows="14" spellcheck="false"><%= H(promptTemplate) %></textarea> | |||
| <div class="form-text"> | |||
| Placeholders: <code>{TITLE}</code>, <code>{SUMMARY}</code>, <code>{BODY}</code> | |||
| </div> | |||
| </div> | |||
| <div class="d-flex flex-wrap gap-2"> | |||
| <button class="btn btn-primary" type="submit">Save Prompt</button> | |||
| <a class="btn btn-outline-secondary" href="<%= AdminUrl() %>">Cancel</a> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -22,4 +22,12 @@ | |||
| </div> | |||
| </a> | |||
| </div> | |||
| <div class="col-md-6"> | |||
| <a class="card shadow-sm text-decoration-none h-100" href="<%= AdminUrl() %>/ai-prompt"> | |||
| <div class="card-body"> | |||
| <h2 class="h5 mb-1">AI Prompt</h2> | |||
| <p class="text-muted mb-0">Edit the prompt used to generate AI post content.</p> | |||
| </div> | |||
| </a> | |||
| </div> | |||
| </div> | |||
| @@ -95,6 +95,76 @@ Public Function GetSecureSetting(key, envName) | |||
| GetSecureSetting = "" | |||
| End Function | |||
| Public Function GetGenerationPromptTemplate() | |||
| Dim prompt | |||
| prompt = Trim(CStr(GetAppSetting("AbacusGenerationPrompt"))) | |||
| If Len(prompt) = 0 Or LCase(prompt) = "nothing" Then | |||
| prompt = "You write clear, engaging blog post content for a classic ASP blog. Return only valid JSON with two keys: summary and body. Summary must be 1 to 2 sentences. Body must be 3 to 5 short paragraphs separated by blank lines. Do not use markdown fences, bullets, or code blocks." & vbCrLf & vbCrLf & _ | |||
| "Create blog content for this post title: {TITLE}" & vbCrLf & _ | |||
| "Existing summary: {SUMMARY}" & vbCrLf & _ | |||
| "Existing body: {BODY}" & vbCrLf & _ | |||
| "Keep the title unchanged. Make the content readable and helpful for a general audience." | |||
| End If | |||
| GetGenerationPromptTemplate = prompt | |||
| End Function | |||
| Public Function BuildGenerationPrompt(ByRef post) | |||
| Dim template | |||
| template = GetGenerationPromptTemplate() | |||
| template = Replace(template, "{TITLE}", SafePromptText(post.Title)) | |||
| template = Replace(template, "{SUMMARY}", SafePromptText(post.Summary)) | |||
| template = Replace(template, "{BODY}", SafePromptText(post.Body)) | |||
| BuildGenerationPrompt = template | |||
| End Function | |||
| Public Function SafePromptText(ByVal value) | |||
| If IsNull(value) Or IsEmpty(value) Then | |||
| SafePromptText = "" | |||
| Else | |||
| SafePromptText = CStr(value) | |||
| End If | |||
| End Function | |||
| Public Function UpdateAppSetting(key, value) | |||
| Dim xml, nodes, node, appSettings, found | |||
| Set xml = Server.CreateObject("Microsoft.XMLDOM") | |||
| xml.async = False | |||
| xml.preserveWhiteSpace = True | |||
| xml.Load Server.MapPath("web.config") | |||
| If xml.parseError.errorCode <> 0 Then | |||
| Err.Raise 1, "UpdateAppSetting", "Unable to load web.config: " & xml.parseError.reason | |||
| End If | |||
| Set nodes = xml.selectNodes("//appSettings/add[@key='" & key & "']") | |||
| found = False | |||
| If Not (nodes Is Nothing) Then | |||
| If nodes.Length > 0 Then | |||
| Set node = nodes.Item(0) | |||
| node.setAttribute "value", value | |||
| found = True | |||
| End If | |||
| End If | |||
| If Not found Then | |||
| Set appSettings = xml.selectSingleNode("//appSettings") | |||
| If appSettings Is Nothing Then | |||
| Err.Raise 1, "UpdateAppSetting", "<appSettings> section not found in web.config." | |||
| End If | |||
| Set node = xml.createElement("add") | |||
| node.setAttribute "key", key | |||
| node.setAttribute "value", value | |||
| appSettings.appendChild node | |||
| End If | |||
| xml.Save Server.MapPath("web.config") | |||
| Application.Contents.Remove("AppSetting_" & key) | |||
| UpdateAppSetting = True | |||
| End Function | |||
| Public Sub ShowServerVariables | |||
| Dim varName, htmlTable | |||
| htmlTable = "<table border='1' cellspacing='0' cellpadding='5'>" | |||
| @@ -183,6 +253,31 @@ Function TrimQueryParams(rawPath) | |||
| End If | |||
| End Function | |||
| Function DecodeUrlPath(ByVal rawPath) | |||
| Dim current, previous | |||
| current = Trim(CStr(rawPath)) | |||
| On Error Resume Next | |||
| Do | |||
| previous = current | |||
| current = Server.URLDecode(current) | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| current = previous | |||
| Exit Do | |||
| End If | |||
| Loop While current <> previous | |||
| On Error GoTo 0 | |||
| current = Replace(current, "%252D", "-") | |||
| current = Replace(current, "%252d", "-") | |||
| current = Replace(current, "%2D", "-") | |||
| current = Replace(current, "%2d", "-") | |||
| current = Replace(current, "%25", "%") | |||
| DecodeUrlPath = current | |||
| End Function | |||
| Sub Destroy(o) | |||
| if isobject(o) then | |||
| if not o is nothing then | |||
| @@ -252,7 +347,7 @@ Function CategoryDeleteUrl(ByVal categoryId) | |||
| End Function | |||
| Function PostUrl(ByVal slug) | |||
| PostUrl = "/posts/" & Server.URLEncode(CStr(slug)) | |||
| PostUrl = "/posts/" & Server.URLEncode(NormalizeSlug(slug)) | |||
| End Function | |||
| Function PostsUrl() | |||
| @@ -287,6 +382,14 @@ Function AdminPostAIUrl(ByVal postId) | |||
| AdminPostAIUrl = "/admin/posts/" & Server.URLEncode(CStr(postId)) & "/ai" | |||
| End Function | |||
| Function AdminAIPromptUrl() | |||
| AdminAIPromptUrl = "/admin/ai-prompt" | |||
| End Function | |||
| Function AdminAIPromptUpdateUrl() | |||
| AdminAIPromptUpdateUrl = "/admin/ai-prompt" | |||
| End Function | |||
| Function AdminUrl() | |||
| AdminUrl = "/admin" | |||
| End Function | |||
| @@ -295,6 +398,36 @@ Function CommentsUrl() | |||
| CommentsUrl = "/comments" | |||
| End Function | |||
| Function NormalizeSlug(ByVal slug) | |||
| Dim current, previous | |||
| current = Trim(CStr(slug)) | |||
| If Len(current) = 0 Then | |||
| NormalizeSlug = "" | |||
| Exit Function | |||
| End If | |||
| On Error Resume Next | |||
| Do | |||
| previous = current | |||
| current = Server.URLDecode(current) | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| current = previous | |||
| Exit Do | |||
| End If | |||
| Loop While current <> previous | |||
| On Error GoTo 0 | |||
| current = Replace(current, "%252D", "-") | |||
| current = Replace(current, "%252d", "-") | |||
| current = Replace(current, "%2D", "-") | |||
| current = Replace(current, "%2d", "-") | |||
| current = Replace(current, "%25", "%") | |||
| NormalizeSlug = current | |||
| End Function | |||
| '======================================================================================================================= | |||
| ' Adapted from Tolerable library | |||
| @@ -165,6 +165,19 @@ Private Function NormalizePath(path) | |||
| cleaned = Left(cleaned, Len(cleaned) - 1) | |||
| Loop | |||
| On Error Resume Next | |||
| Do | |||
| Dim decoded | |||
| decoded = Server.URLDecode(cleaned) | |||
| If Err.Number <> 0 Then | |||
| Err.Clear | |||
| Exit Do | |||
| End If | |||
| If decoded = cleaned Then Exit Do | |||
| cleaned = LCase(decoded) | |||
| Loop | |||
| On Error GoTo 0 | |||
| NormalizePath = cleaned | |||
| End Function | |||
| @@ -33,11 +33,13 @@ | |||
| router.AddRoute "GET", "/admin", "AdminController", "Index" | |||
| router.AddRoute "GET", "/admin/posts", "AdminController", "Posts" | |||
| router.AddRoute "GET", "/admin/categories", "AdminController", "Categories" | |||
| router.AddRoute "GET", "/admin/ai-prompt", "AdminController", "AIPrompt" | |||
| router.AddRoute "POST", "/admin/ai-prompt", "AdminController", "UpdateAIPrompt" | |||
| router.AddRoute "POST", "/admin/posts/{id}/publish", "AdminController", "PublishPost" | |||
| router.AddRoute "POST", "/admin/posts/{id}/unpublish", "AdminController", "UnpublishPost" | |||
| router.AddRoute "POST", "/admin/posts/{id}/ai", "AdminController", "GenerateAIContent" | |||
| ' Dispatch the request (resolves route and executes controller action) | |||
| MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | |||
| TrimQueryParams(Request.ServerVariables("HTTP_X_ORIGINAL_URL")) | |||
| MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | |||
| TrimQueryParams(DecodeUrlPath(Request.ServerVariables("HTTP_X_ORIGINAL_URL"))) | |||
| %> | |||
| @@ -48,6 +48,9 @@ | |||
| <!-- Model name to send to the Abacus RouteLLM API --> | |||
| <add key="AbacusModel" value="route-llm" /> | |||
| <!-- Editable prompt template used to generate AI post content --> | |||
| <add key="AbacusGenerationPrompt" value="You write clear, engaging blog post content for a classic ASP blog. Return only valid JSON with two keys: summary and body. Summary must be 1 to 2 sentences. Body must be 3 to 5 short paragraphs separated by blank lines. Do not use markdown fences, bullets, or code blocks. Create blog content for this post title: {TITLE} Existing summary: {SUMMARY} Existing body: {BODY} Keep the title unchanged. Make the content readable and helpful for a general audience." /> | |||
| </appSettings> | |||
| <system.webServer> | |||
Powered by TurnKey Linux.