From f19ca55d674d300d35c82de35624a9092c1f68ca Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Mon, 10 Nov 2025 08:56:12 +0100 Subject: [PATCH 01/11] site: use gordon for ask ai Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> --- assets/css/style.css | 1 + .../images/ask-ai-logo.svg | 0 assets/js/src/alpine.js | 80 ++- hugo.yaml | 3 + layouts/_default/baseof.html | 1 + layouts/index.html | 1 + layouts/partials/gordon-chat.html | 558 ++++++++++++++++++ layouts/partials/head.html | 53 -- layouts/partials/header.html | 3 +- layouts/partials/search-bar.html | 7 +- package-lock.json | 23 + package.json | 2 + 12 files changed, 675 insertions(+), 57 deletions(-) rename static/assets/images/logo-icon-white.svg => assets/images/ask-ai-logo.svg (100%) create mode 100644 layouts/partials/gordon-chat.html diff --git a/assets/css/style.css b/assets/css/style.css index d8469b419a5f..8315621f425d 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -41,5 +41,6 @@ @import "syntax-dark.css"; @import "syntax-light.css"; @import "components.css"; +@import "highlight-github-dark.css"; @variant dark (&:where(.dark, .dark *)); diff --git a/static/assets/images/logo-icon-white.svg b/assets/images/ask-ai-logo.svg similarity index 100% rename from static/assets/images/logo-icon-white.svg rename to assets/images/ask-ai-logo.svg diff --git a/assets/js/src/alpine.js b/assets/js/src/alpine.js index cdcdfeb93055..d023fb11d5b2 100644 --- a/assets/js/src/alpine.js +++ b/assets/js/src/alpine.js @@ -2,12 +2,90 @@ import Alpine from 'alpinejs' import collapse from '@alpinejs/collapse' import persist from '@alpinejs/persist' import focus from '@alpinejs/focus' - +import { marked } from 'marked' +import hljs from 'highlight.js/lib/core' +// Import languages relevant to Docker docs +import bash from 'highlight.js/lib/languages/bash' +import dockerfile from 'highlight.js/lib/languages/dockerfile' +import yaml from 'highlight.js/lib/languages/yaml' +import json from 'highlight.js/lib/languages/json' +import javascript from 'highlight.js/lib/languages/javascript' +import python from 'highlight.js/lib/languages/python' +import go from 'highlight.js/lib/languages/go' + window.Alpine = Alpine Alpine.plugin(collapse) Alpine.plugin(persist) Alpine.plugin(focus) + +// Register highlight.js languages +hljs.registerLanguage('bash', bash) +hljs.registerLanguage('sh', bash) +hljs.registerLanguage('shell', bash) +hljs.registerLanguage('console', bash) +hljs.registerLanguage('dockerfile', dockerfile) +hljs.registerLanguage('yaml', yaml) +hljs.registerLanguage('yml', yaml) +hljs.registerLanguage('json', json) +hljs.registerLanguage('javascript', javascript) +hljs.registerLanguage('js', javascript) +hljs.registerLanguage('python', python) +hljs.registerLanguage('py', python) +hljs.registerLanguage('go', go) +hljs.registerLanguage('golang', go) + +// Add $markdown magic for rendering markdown with syntax highlighting +Alpine.magic('markdown', () => { + return (content) => { + if (!content) return '' + const html = marked(content) + + // Parse and highlight code blocks + const div = document.createElement('div') + div.innerHTML = html + + // Handle code blocks (pre > code) + div.querySelectorAll('pre').forEach((pre) => { + // Add not-prose to prevent Tailwind Typography styling + pre.classList.add('not-prose') + const code = pre.querySelector('code') + if (code) { + // Preserve the original text with newlines + const codeText = code.textContent + + // Clear and set as plain text first to preserve structure + code.textContent = codeText + + // Now apply highlight.js which will work with the text nodes + hljs.highlightElement(code) + } + }) + + // Handle inline code elements (not in pre blocks) + div.querySelectorAll('code:not(pre code)').forEach((code) => { + code.classList.add('not-prose') + }) + + return div.innerHTML + } +}) + +// Stores Alpine.store("showSidebar", false) +Alpine.store('gordon', { + isOpen: Alpine.$persist(false).using(sessionStorage).as('gordon-isOpen'), + query: '', + toggle() { + this.isOpen = !this.isOpen + }, + open(query) { + this.isOpen = true + if (query) this.query = query + }, + close() { + this.isOpen = false + } +}) Alpine.start() diff --git a/hugo.yaml b/hugo.yaml index bc4bde92e1d8..e1143ea0518f 100644 --- a/hugo.yaml +++ b/hugo.yaml @@ -271,6 +271,9 @@ module: # Mount the icon files to assets so we can access them with resources.Get - source: node_modules/@material-symbols/svg-400/rounded target: assets/icons + # Mount highlight.js theme for Gordon chat syntax highlighting + - source: node_modules/highlight.js/styles/github-dark.css + target: assets/css/highlight-github-dark.css imports: diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html index 7b06c790c819..946147a21e9c 100644 --- a/layouts/_default/baseof.html +++ b/layouts/_default/baseof.html @@ -8,6 +8,7 @@ class="dark:bg-navbar-bg-dark bg-navbar-bg flex flex-col items-center text-base text-black dark:text-white" > {{ partial "header.html" . }} + {{ partial "gordon-chat.html" . }}
{{ partial "header.html" . }} + {{ partial "gordon-chat.html" . }}
diff --git a/layouts/partials/gordon-chat.html b/layouts/partials/gordon-chat.html new file mode 100644 index 000000000000..e62b895f0588 --- /dev/null +++ b/layouts/partials/gordon-chat.html @@ -0,0 +1,558 @@ + +
+ +
+ + +
+ +
+
+ {{ partial "utils/svg.html" "images/ask-ai-logo.svg" }} +
+
+ + +
+
+ + +
+ + + + + + + + +
+ + +
+
+
+ + +
+ + + +
+
+ +
+
+ + +
+ This is a custom LLM for answering questions about Docker. Answers are + based on the documentation. +
+
+
+ + + diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 4c1455512c37..af9892537126 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -51,59 +51,6 @@ })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv='); {{ end }} -{{/* kapa.ai widget */}} - - {{/* preload Roboto Flex as it's a critical font: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preload */}}
. + .
` }} {{- $emptyState | safe.HTML }} diff --git a/package-lock.json b/package-lock.json index 4aefc775b6d7..3b7316c8b938 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@tailwindcss/cli": "^4.1.6", "@tailwindcss/typography": "^0.5.15", "alpinejs": "^3.14.3", + "highlight.js": "^11.11.1", + "marked": "^17.0.0", "tailwindcss": "^4.1.6" }, "devDependencies": { @@ -976,6 +978,15 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -1367,6 +1378,18 @@ "url": "https://github.com/sponsors/DavidAnson" } }, + "node_modules/marked": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.0.tgz", + "integrity": "sha512-KkDYEWEEiYJw/KC+DVm1zzlpMQSMIu6YRltkcCvwheCp8HWPXCk9JwOmHJKBlGfzcpzcIt6x3sMnTsRm/51oDg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", diff --git a/package.json b/package.json index de8098b00b20..795ed12561b4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@tailwindcss/cli": "^4.1.6", "@tailwindcss/typography": "^0.5.15", "alpinejs": "^3.14.3", + "highlight.js": "^11.11.1", + "marked": "^17.0.0", "tailwindcss": "^4.1.6" }, "devDependencies": { From 482ea6d3ed2ece97bdb30bee2875c164b41eb13a Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:36:43 +0100 Subject: [PATCH 02/11] site(gordon): show error message when rate limited Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> --- layouts/partials/gordon-chat.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/layouts/partials/gordon-chat.html b/layouts/partials/gordon-chat.html index e62b895f0588..69882299e4a4 100644 --- a/layouts/partials/gordon-chat.html +++ b/layouts/partials/gordon-chat.html @@ -79,7 +79,11 @@ } catch (err) { // Only set error if messages weren't cleared if (this.messages.length > 0) { - this.error = 'Failed to get response. Please try again.' + if (err.message === 'RATE_LIMIT_EXCEEDED') { + this.error = 'You\'ve exceeded your question quota for the day. Please come back tomorrow.' + } else { + this.error = 'Failed to get response. Please try again.' + } } console.error('Gordon API error:', err) // Only try to remove message if it still exists @@ -160,6 +164,9 @@ }) if (!response.ok) { + if (response.status === 429) { + throw new Error('RATE_LIMIT_EXCEEDED') + } throw new Error(`HTTP ${response.status}: ${response.statusText}`) } From 7c95a40893c28831d3d1ca9bf9d1efc84ad3389b Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:47:26 +0100 Subject: [PATCH 03/11] site(gordon): configure hostname with HUGO_USE_LOCAL_GORDON Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> --- layouts/partials/gordon-chat.html | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/layouts/partials/gordon-chat.html b/layouts/partials/gordon-chat.html index 69882299e4a4..5fc5c6378440 100644 --- a/layouts/partials/gordon-chat.html +++ b/layouts/partials/gordon-chat.html @@ -1,4 +1,13 @@ +
+ class="fixed top-0 right-0 z-50 flex h-screen w-full flex-col overflow-hidden rounded-lg bg-white shadow-2xl transition-all duration-200 md:w-[min(80ch,90vw)] md:h-[calc(100vh-1rem)] md:top-2 md:right-2 dark:bg-gray-900">
@@ -355,8 +355,8 @@

- + Context @@ -474,30 +491,16 @@

x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" - class="absolute bottom-full right-0 mb-2 w-56 rounded-lg bg-gray-900 p-2.5 text-xs text-white shadow-lg dark:bg-gray-700" + class="absolute bottom-full left-0 mb-2 w-56 rounded-lg bg-gray-900 p-2.5 text-xs text-white shadow-lg dark:bg-gray-700" style="display: none;">

When enabled, Gordon considers the current page you're viewing to provide more relevant answers.

-
+

-
From 0b2d4d96fe5ac6a8bfdad8e77ebf02fe767b1d6a Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:54:26 +0100 Subject: [PATCH 07/11] site(gordon): add y padding for response in loading state Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> --- layouts/partials/gordon-chat.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layouts/partials/gordon-chat.html b/layouts/partials/gordon-chat.html index daefbd78afc7..d641c9b41c2d 100644 --- a/layouts/partials/gordon-chat.html +++ b/layouts/partials/gordon-chat.html @@ -358,7 +358,7 @@

-
From 6ccced9872a21f1f25ccd5351c0c8174b52d52f9 Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:03:28 +0100 Subject: [PATCH 09/11] site(gordon): get qa ID from stream chunk Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> --- layouts/partials/gordon-chat.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/layouts/partials/gordon-chat.html b/layouts/partials/gordon-chat.html index 35df300f1bb1..4117c2bdad72 100644 --- a/layouts/partials/gordon-chat.html +++ b/layouts/partials/gordon-chat.html @@ -163,12 +163,6 @@ throw new Error(`HTTP ${response.status}: ${response.statusText}`) } - // Capture question_answer_id from response headers - const questionAnswerId = response.headers.get('DOCKER-AI-QUESTION-ANSWER-ID') - if (questionAnswerId) { - this.messages[responseIndex].questionAnswerId = questionAnswerId - } - const reader = response.body.getReader() const decoder = new TextDecoder() @@ -198,6 +192,12 @@ continue } + // Capture question_answer_id for feedback + if (parsed.question_answer_id) { + this.messages[responseIndex].questionAnswerId = parsed.question_answer_id + continue + } + if (parsed.choices && parsed.choices[0]?.delta?.content) { const content = parsed.choices[0].delta.content this.messages[responseIndex].content += content From 25def314e7de70b30882ecf5fcd8263e39ba86b4 Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:26:26 +0100 Subject: [PATCH 10/11] site(gordon): do not persist open state Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> --- assets/js/src/alpine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/src/alpine.js b/assets/js/src/alpine.js index f9d2ec796a2a..7249a31def29 100644 --- a/assets/js/src/alpine.js +++ b/assets/js/src/alpine.js @@ -90,7 +90,7 @@ Alpine.magic('markdown', () => { // Stores Alpine.store("showSidebar", false) Alpine.store('gordon', { - isOpen: Alpine.$persist(false).using(sessionStorage).as('gordon-isOpen'), + isOpen: false, query: '', toggle() { this.isOpen = !this.isOpen From c00b2b5ee765241c2cc843f5d21f939d9453b272 Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:57:48 +0100 Subject: [PATCH 11/11] site(gordon): add turn limit (10 messages) Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com> --- layouts/partials/gordon-chat.html | 669 ++++++++++++++++++------------ 1 file changed, 400 insertions(+), 269 deletions(-) diff --git a/layouts/partials/gordon-chat.html b/layouts/partials/gordon-chat.html index 4117c2bdad72..f44dd86e2fc2 100644 --- a/layouts/partials/gordon-chat.html +++ b/layouts/partials/gordon-chat.html @@ -8,13 +8,15 @@ 'https://ai-backend-service-stage.docker.com' {{- end }}; -
m.role === 'user').length + }, + + getRemainingTurns() { + return this.maxTurnsPerThread - this.getTurnCount() + }, + + isThreadLimitReached() { + return this.getTurnCount() >= this.maxTurnsPerThread + }, + + shouldShowCountdown() { + const remaining = this.getRemainingTurns() + return remaining > 0 && remaining <= 3 + }, + async askQuestion() { const question = this.currentQuestion.trim() - if (!question || this.isLoading) { + if (!question || this.isLoading || this.isThreadLimitReached()) { return } @@ -287,281 +306,393 @@ console.error('Failed to copy:', err) } } - }" x-cloak @keydown.escape.window="$store.gordon.close()"> - -
- - -
- -
-
- {{ partial "utils/svg.html" "images/ask-ai-logo.svg" }} -
-
- - -
-
- - -
- - - - - + + + + + + + + + +
+ + +
+
+
+
+ +
+ +
+
+
+ - - -
-
-
-
- - -
- This is a custom LLM for answering questions about Docker. Answers are - based on the documentation. -
-
+ class="cursor-pointer rounded-md px-2 py-1 text-xs font-medium transition-colors hover:opacity-80" + > + + + + Context + + + + +
+ + + + + +
+ This is a custom LLM for answering questions about Docker. Answers are + based on the documentation. +
+