diff --git a/app/_assets/entrypoints/hub.js b/app/_assets/entrypoints/hub.js index a9da8d984f..101e1410bb 100644 --- a/app/_assets/entrypoints/hub.js +++ b/app/_assets/entrypoints/hub.js @@ -11,18 +11,19 @@ class Hub { this.areFiltersOpen = false; this.deploymentTopologies = this.filters.querySelectorAll( - 'input[name="deployment-topology"]' + 'input[name="deployment-topology"]', ); this.tiers = this.filters.querySelectorAll('input[name="tier"]'); this.categories = this.filters.querySelectorAll('input[name="category"]'); this.support = this.filters.querySelectorAll('input[name="support"]'); this.trustedContent = this.filters.querySelectorAll( - 'input[name="trusted-content"]' + 'input[name="trusted-content"]', ); this.phases = this.filters.querySelectorAll('input[name="phase"]'); this.policyTargets = this.filters.querySelectorAll( - 'input[name="policy-target"]' + 'input[name="policy-target"]', ); + this.products = this.filters.querySelectorAll('input[name="product"]'); this.deploymentValues = []; this.categoryValues = []; @@ -31,6 +32,7 @@ class Hub { this.tierValues = []; this.phaseValues = []; this.policyTargetValues = []; + this.productValues = []; this.typingTimer; this.typeInterval = 400; @@ -48,6 +50,7 @@ class Hub { ...this.tiers, ...this.phases, ...this.policyTargets, + ...this.products, ]; checkboxes.forEach((checkbox) => { checkbox.addEventListener("change", () => this.onChange()); @@ -77,7 +80,7 @@ class Hub { this.seeResults.addEventListener("click", () => this.toggleDrawer()); this.toggleFiltersDrawer.addEventListener("click", () => - this.toggleDrawer() + this.toggleDrawer(), ); } @@ -95,6 +98,7 @@ class Hub { this.trustedContentValues = this.getValues(this.trustedContent); this.phaseValues = this.getValues(this.phases); this.policyTargetValues = this.getValues(this.policyTargets); + this.productValues = this.getValues(this.products); this.updateURL(); this.scrollCardsIntoView(); @@ -119,24 +123,24 @@ class Hub { const matchesDeploymentTopology = this.matchesFilter( plugin, this.deploymentTopologies, - "deploymentTopology" + "deploymentTopology", ); const matchesCategory = this.matchesFilter( plugin, this.categories, - "category" + "category", ); const matchesSupport = this.matchesFilter( plugin, this.support, - "support" + "support", ); const matchesTrustedContent = this.matchesFilter( plugin, this.trustedContent, - "trustedContent" + "trustedContent", ); const matchesPhases = this.matchesFilter(plugin, this.phases, "phases"); @@ -144,11 +148,17 @@ class Hub { const matchesPolicyTarget = this.matchesFilter( plugin, this.policyTargets, - "policyTarget" + "policyTarget", ); const matchesTier = this.matchesFilter(plugin, this.tiers, "tier"); + const matchesProducts = this.matchesFilter( + plugin, + this.products, + "products", + ); + const matchesText = this.matchesQuery(plugin); const showPlugin = @@ -159,6 +169,7 @@ class Hub { matchesTier && matchesPhases && matchesPolicyTarget && + matchesProducts && matchesText; plugin.classList.toggle("hidden", !showPlugin); @@ -172,7 +183,7 @@ class Hub { this.categories.forEach((cat) => { const category = document.getElementById(cat.value); const showCategory = category.querySelectorAll( - '[data-card="plugin"]:not(.hidden)' + '[data-card="plugin"]:not(.hidden)', ).length; category.classList.toggle("hidden", !showCategory); @@ -183,7 +194,7 @@ class Hub { const thirdParty = document.getElementById("third-party"); if (thirdParty) { const showThirdParty = thirdParty.querySelectorAll( - '[data-card="plugin"]:not(.hidden)' + '[data-card="plugin"]:not(.hidden)', ).length; thirdParty.classList.toggle("hidden", !showThirdParty); @@ -220,7 +231,7 @@ class Hub { params.delete("deployment-topology"); if (this.deploymentValues.length > 0) { this.deploymentValues.forEach((value) => - params.append("deployment-topology", value) + params.append("deployment-topology", value), ); } @@ -237,7 +248,7 @@ class Hub { params.delete("trusted-content"); if (this.trustedContentValues.length > 0) { this.trustedContentValues.forEach((value) => - params.append("trusted-content", value) + params.append("trusted-content", value), ); } @@ -260,10 +271,15 @@ class Hub { params.delete("policy-target"); if (this.policyTargetValues.length > 0) { this.policyTargetValues.forEach((value) => - params.append("policy-target", value) + params.append("policy-target", value), ); } + params.delete("product"); + if (this.productValues.length > 0) { + this.productValues.forEach((value) => params.append("product", value)); + } + let newUrl = window.location.pathname; if (params.size > 0) { newUrl += "?" + params.toString(); @@ -309,6 +325,11 @@ class Hub { checkbox.checked = policyTargetValues.includes(checkbox.value); }); + const productValues = params.getAll("product") || []; + this.products.forEach((checkbox) => { + checkbox.checked = productValues.includes(checkbox.value); + }); + const termsValue = params.get("terms") || ""; this.textInput.value = decodeURIComponent(termsValue); @@ -320,6 +341,7 @@ class Hub { tierValues.length || phaseValues.length || policyTargetValues.length || + productValues.length || termsValue ) { this.onChange(); diff --git a/app/_assets/stylesheets/index.css b/app/_assets/stylesheets/index.css index 6533c757ae..5245b006d3 100644 --- a/app/_assets/stylesheets/index.css +++ b/app/_assets/stylesheets/index.css @@ -834,6 +834,12 @@ pre { @apply whitespace-pre overflow-auto !bg-code-block; } + + &[data-ask-kai="true"] { + pre { + @apply whitespace-pre-wrap break-normal; + } + } } clipboard-copy { diff --git a/app/_data/schemas/frontmatter/base.json b/app/_data/schemas/frontmatter/base.json index 2e9e2f68a7..795113ed2d 100644 --- a/app/_data/schemas/frontmatter/base.json +++ b/app/_data/schemas/frontmatter/base.json @@ -18,7 +18,7 @@ }, "content_type": { "type": "string", - "enum": ["landing_page", "how_to", "reference", "concept", "plugin", "plugin_example", "api", "policy", "support"] + "enum": ["landing_page", "how_to", "reference", "concept", "plugin", "plugin_example", "api", "policy", "support", "prompt"] }, "description": { "type": "string" diff --git a/app/_includes/cards/prompt.html b/app/_includes/cards/prompt.html new file mode 100644 index 0000000000..99c5e44213 --- /dev/null +++ b/app/_includes/cards/prompt.html @@ -0,0 +1,22 @@ +{% assign prompt = include.prompt %} +
+ +
+

{{ prompt.title | liquify }}

+ +

+ {{ prompt.description | liquify }} +

+ +
+ {% for product in prompt.products %} + {% include_cached product_icon.html product=product %} + {% endfor %} +
+
+
+
\ No newline at end of file diff --git a/app/_includes/checkbox.html b/app/_includes/checkbox.html index 1dbe68f8a6..581a819580 100644 --- a/app/_includes/checkbox.html +++ b/app/_includes/checkbox.html @@ -1,6 +1,9 @@
\ No newline at end of file diff --git a/app/_layouts/prompts/overview.html b/app/_layouts/prompts/overview.html new file mode 100644 index 0000000000..e0e24a50c6 --- /dev/null +++ b/app/_layouts/prompts/overview.html @@ -0,0 +1,5 @@ +--- +layout: without_aside +--- + +{{content}} diff --git a/app/_plugins/converters/syntax_highlight.rb b/app/_plugins/converters/syntax_highlight.rb index e5d63646ca..db66015168 100644 --- a/app/_plugins/converters/syntax_highlight.rb +++ b/app/_plugins/converters/syntax_highlight.rb @@ -17,13 +17,15 @@ def render_shiki(el, _indent) # rubocop:disable Metrics/AbcSize,Metrics/MethodLe id = SecureRandom.uuid snippet = CodeHighlighter.new.highlight(code, language, id) + Liquid::Template.parse(template, { line_numbers: true }).render( { 'codeblock' => { 'copy' => copy, 'css_classes' => el.attr['class'], 'collapsible' => el.attr.fetch('class', '').include?('collapsible'), - 'render_header' => !data['data-file'].nil?, + 'ask_kai' => ask_kai(data, code), + 'render_header' => !data['data-file'].nil? || !ask_kai(data, code).nil?, 'id' => id, 'data' => data, 'snippet' => snippet @@ -54,6 +56,12 @@ def data_attributes(attr) data[key] = value if key.start_with?('data-') end end + + def ask_kai(data, code) + return nil if data['data-ask-kai'].nil? + + "https://cloud.konghq.com/?agent=true&agent-prompt=#{URI.encode_www_form_component(code)}" + end end end end diff --git a/app/_plugins/generators/data/search_tags/base.rb b/app/_plugins/generators/data/search_tags/base.rb index 96d1c628fc..7fd6578fb7 100644 --- a/app/_plugins/generators/data/search_tags/base.rb +++ b/app/_plugins/generators/data/search_tags/base.rb @@ -13,7 +13,8 @@ class Base # rubocop:disable Style/Documentation 'plugin_example' => 'PluginExample', 'reference' => 'Reference', 'policy' => 'Policy', - 'support' => 'Support' + 'support' => 'Support', + 'prompt' => 'Prompt' }.freeze def self.make_for(site:, page:) diff --git a/app/_plugins/generators/data/search_tags/prompt.rb b/app/_plugins/generators/data/search_tags/prompt.rb new file mode 100644 index 0000000000..528af14b8e --- /dev/null +++ b/app/_plugins/generators/data/search_tags/prompt.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require_relative 'base' + +module Jekyll + module Data + module SearchTags + class Prompt < Base + end + end + end +end diff --git a/app/_plugins/generators/data/title/base.rb b/app/_plugins/generators/data/title/base.rb index 7c7a8dd93e..f63b845d06 100644 --- a/app/_plugins/generators/data/title/base.rb +++ b/app/_plugins/generators/data/title/base.rb @@ -9,6 +9,8 @@ def self.make_for(page:, site:) # rubocop:disable Metrics/AbcSize,Metrics/Cyclom APIPage.new(page:, site:) elsif page.url.start_with?('/plugins/') Plugin.new(page:, site:) + elsif page.url.start_with?('/prompts/') + Prompt.new(page:, site:) elsif page.url.start_with?('/mesh/policies/') || page.url.start_with?('/event-gateway/policies/') Policy.new(page:, site:) elsif page.data['content_type'] && page.data['content_type'] == 'reference' diff --git a/app/_plugins/generators/data/title/prompt.rb b/app/_plugins/generators/data/title/prompt.rb new file mode 100644 index 0000000000..14636b5e4b --- /dev/null +++ b/app/_plugins/generators/data/title/prompt.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative 'base' + +module Jekyll + module Data + module Title + class Prompt < Base + def title_sections + [page_title, 'Prompts'] + end + + def llm_title + page_title + end + end + end + end +end diff --git a/app/_plugins/generators/prompts.rb b/app/_plugins/generators/prompts.rb new file mode 100644 index 0000000000..52a9b19487 --- /dev/null +++ b/app/_plugins/generators/prompts.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Jekyll + class PromptsGenerator < Jekyll::Generator + priority :high + + def generate(site) + site.data['prompts'] ||= [] + Jekyll::PromptPages::Generator.run(site) + end + end +end diff --git a/app/_plugins/generators/prompts/generator.rb b/app/_plugins/generators/prompts/generator.rb new file mode 100644 index 0000000000..42b19da5e4 --- /dev/null +++ b/app/_plugins/generators/prompts/generator.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Jekyll + module PromptPages + class Generator + PROMPTS_FOLDER = '_prompts' + + def self.run(site) + new(site).run + end + + attr_reader :site + + def initialize(site) + @site = site + end + + def run + return if site.config.dig('skip', 'prompts') + + Dir.glob(File.join(site.source, "#{PROMPTS_FOLDER}/*.{yaml,yml}")).each do |file| + slug = File.basename(file, File.extname(file)) + prompt = Jekyll::PromptPages::Prompt.new(file:, slug:) + + generate_overview_page(prompt) + end + end + + private + + def generate_overview_page(prompt) + overview = Jekyll::PromptPages::Pages::Overview + .new(prompt:) + .to_jekyll_page + + site.data['prompts'] << overview + site.pages << overview + end + end + end +end diff --git a/app/_plugins/generators/prompts/pages/base.rb b/app/_plugins/generators/prompts/pages/base.rb new file mode 100644 index 0000000000..43203d87df --- /dev/null +++ b/app/_plugins/generators/prompts/pages/base.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require_relative '../../../lib/site_accessor' + +module Jekyll + module PromptPages + module Pages + class Base + include Jekyll::SiteAccessor + + attr_reader :prompt + + def initialize(prompt:) + @prompt = prompt + end + + def to_jekyll_page + CustomJekyllPage.new(site:, page: self) + end + + def dir + url + end + + def relative_path + @relative_path ||= prompt.file.gsub("#{site.source}/", '') + end + + def data + { + 'title' => prompt.title, + 'description' => prompt.description, + 'extended_description' => prompt.extended_description, + 'products' => prompt.products, + 'prompts' => prompt.prompts, + 'slug' => prompt.slug, + 'content_type' => 'prompt', + 'layout' => layout, + 'breadcrumbs' => ['/prompts/'] + } + end + + def url + @url ||= self.class.url(prompt) + end + end + end + end +end diff --git a/app/_plugins/generators/prompts/pages/overview.rb b/app/_plugins/generators/prompts/pages/overview.rb new file mode 100644 index 0000000000..993a675708 --- /dev/null +++ b/app/_plugins/generators/prompts/pages/overview.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Jekyll + module PromptPages + module Pages + class Overview < Base + def self.url(prompt) + "/prompts/#{prompt.slug}/" + end + + def content + @content ||= File.read('app/_includes/prompts/overview.md') + end + + def layout + 'prompts/overview' + end + + def data + super.merge('overview?' => true, 'content_type' => 'prompt') + end + end + end + end +end diff --git a/app/_plugins/generators/prompts/prompt.rb b/app/_plugins/generators/prompts/prompt.rb new file mode 100644 index 0000000000..4fa523b9dd --- /dev/null +++ b/app/_plugins/generators/prompts/prompt.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'yaml' +require_relative '../../lib/site_accessor' + +module Jekyll + module PromptPages + class Prompt + include Jekyll::SiteAccessor + + attr_reader :file, :slug + + def initialize(file:, slug:) + @file = file + @slug = slug + end + + def metadata + @metadata ||= YAML.load_file(@file) + end + + def title + @title ||= metadata.fetch('title') + end + + def description + @description ||= metadata.fetch('description') + end + + def extended_description + @extended_description ||= metadata.fetch('extended_description', description) + end + + def products + @products ||= metadata.fetch('products', []) + end + + def prompts + @prompts ||= metadata.fetch('prompts', []) + end + end + end +end diff --git a/app/_prompts/debugging.yaml b/app/_prompts/debugging.yaml new file mode 100644 index 0000000000..18a0a1d70b --- /dev/null +++ b/app/_prompts/debugging.yaml @@ -0,0 +1,11 @@ +title: Debugging with KAi +description: Help diagnose and fix issues in your Kong configuration with KAi's debugging capabilities. +extended_description: Help diagnose and fix issues in your Kong configuration with KAi's debugging capabilities. Learn how to use KAi to identify and resolve common problems effectively. +products: + - gateway + +prompts: + - Why is my `/api/checkout` endpoint slow? Create a debugging session, focusing on high-latency requests, and summarize where time is being spent. + - Help me initiate a debugging session to troubleshoot the recent latency spikes and give me an analysis of the results with recommendations. + - Help me diagnose intermittent 502s on my [ROUTE_NAME] Route. + - I'm seeing 429 errors on my API Gateway route. Walk me through diagnosing a rate limiting issue, including how to check plugin config, consumer credentials, and request headers. diff --git a/app/_prompts/security.yaml b/app/_prompts/security.yaml new file mode 100644 index 0000000000..facf19c5d8 --- /dev/null +++ b/app/_prompts/security.yaml @@ -0,0 +1,12 @@ + +title: Security with KAi +description: Enhance the security of your Kong configuration with KAi's security auditing capabilities. +extended_description: Enhance the security of your Kong configuration with KAi's security auditing capabilities. Learn how to use KAi to identify and fix common security misconfigurations effectively. +products: + - gateway + - event-gateway + +prompts: + - "Audit my [CONTROL_PLANE] control plane for common security misconfigurations (for example: auth missing, weak TLS, or overly broad CORS). Suggest prioritized fixes." + - "Suggest a safe baseline policy set for a public API (rate limiting, authentication, and logging) and show how to apply it to the [SERVICE_NAME] Gateway Service." + - "Check if any Routes in my [CONTROL_PLANE] control plane expose endpoints without authentication, and list them." diff --git a/app/prompts.html b/app/prompts.html new file mode 100644 index 0000000000..674aebcb22 --- /dev/null +++ b/app/prompts.html @@ -0,0 +1,89 @@ +--- +title: Prompt Hub +layout: default +hub: true +no_edit_link: true +edit_and_issue_links: false +description: A curated collection of AI prompts for use with KAi, Konnect's AI assistant. +--- +{% if page.output_format == 'markdown'%} +{%- for category in site.data.prompts %} +- [{{category.title | liquify }}]({{ category.url }}): {{ category.description | liquify }}{% for prompt in category.prompts %} + - {{ prompt | liquify }}{%- endfor -%}{%- endfor %} +{% else %} +
+
+
+
+

Prompt Hub

{% include components/llm_dropdown.html url=page.url %}
+ A curated collection of AI prompts for KAi, Konnect's built-in AI assistant. +
Browse, filter, and run prompts directly in your Konnect environment.
+
+
+
+ +
+
+ + +
+ +
+
+
+
+ + + + +
+
+ +
+ +
+
+ +
+
+
+ {% for prompt in site.data.prompts %} + {% include cards/prompt.html prompt=prompt %} + {% endfor %} +
+
+
+
+
+
+{% endif %} \ No newline at end of file diff --git a/jekyll-dev.yml b/jekyll-dev.yml index 173923f61c..21ad5ff278 100644 --- a/jekyll-dev.yml +++ b/jekyll-dev.yml @@ -12,6 +12,7 @@ skip: auto_generated: true # skip auto_generated references, i.e. app/_referneces mesh: true # skip kuma to mesh generation llm_pages: true # skip markdown pages generation + prompts: true # exclude app/_references # even though we set skip.auto_generated: true and the collection has output:false