feat: CV navigation links in chat responses (GPS for the CV)

Agent instruction now requires markdown links to CV anchors:
- Companies: [Olympic Broadcasting](#exp-olympic-broadcasting)
- Projects: [Immich Photo Manager](#proj-immich-photo-manager)
- Sections: [Skills](#skills), [Experience](#experience)

formatResponse converts [text](#anchor) → clickable green links
that close the chat panel, smooth-scroll to the target, and
pulse a green highlight for 2 seconds.

All existing CV anchor IDs used: exp-{companyID}, proj-{projectID},
course-{courseID}, plus section IDs (experience, projects, skills, etc.)
This commit is contained in:
juanatsap
2026-04-08 17:11:22 +01:00
parent 160be31b31
commit c44e9e8c67
4 changed files with 81 additions and 0 deletions
+9
View File
@@ -39,6 +39,15 @@ CORE RULES:
- If the query_cv tool returns no results, say so honestly and suggest the visitor check a related section.
- Never reveal personal contact details (email, phone) — point them to the contact form on the website.
- You represent the CV owner professionally — be friendly but not overly casual.
- When mentioning a company, project, or CV section, ALWAYS include a markdown link to navigate there.
Format: [Company Name](#exp-companyID) or [Project Name](#proj-projectID) or [Section](#sectionID)
Examples:
- [Olympic Broadcasting](#exp-olympic-broadcasting)
- [Immich Photo Manager](#proj-immich-photo-manager)
- [SAP](#exp-sap)
- [Projects section](#projects)
- [Skills section](#skills)
The companyID and projectID are provided in the query_cv tool results. Always use them.
QUERY STRATEGY BY QUESTION TYPE:
+9
View File
@@ -7,6 +7,7 @@ import (
"log"
"net/http"
"os"
"regexp"
"strings"
"time"
@@ -270,6 +271,9 @@ func (h *Handler) runAgent(cr *chatRunner, message string) (string, string, erro
return response.String(), sessionID, nil
}
// mdLinkRe matches markdown links like [text](#anchor)
var mdLinkRe = regexp.MustCompile(`\[([^\]]+)\]\((#[a-zA-Z0-9_-]+)\)`)
// formatResponse converts basic markdown to HTML for the chat bubble.
func formatResponse(text string) string {
text = html.EscapeString(text)
@@ -279,6 +283,11 @@ func formatResponse(text string) string {
text = strings.Replace(text, "**", "</strong>", 1)
}
// Links: [text](#anchor) → clickable navigation link
// After html.EscapeString, the parens and brackets are unchanged but # stays.
// The regex matches the escaped form since []()# are not escaped by html.EscapeString.
text = mdLinkRe.ReplaceAllString(text, `<a href="$2" class="chat-nav-link" onclick="return scrollToCV(this)">$1</a>`)
lines := strings.Split(text, "\n")
var result []string
inList := false
+46
View File
@@ -182,6 +182,33 @@
font-size: 0.75rem;
}
/* ==========================================================================
Navigation Links in Chat Messages
========================================================================== */
.chat-nav-link {
color: var(--accent-green, #27ae60);
text-decoration: none;
font-weight: 600;
cursor: pointer;
border-bottom: 1px dotted var(--accent-green, #27ae60);
}
.chat-nav-link:hover {
color: #1e8c4c;
border-bottom-style: solid;
}
/* Highlight animation when scrolled to from chat */
.chat-highlight {
animation: chatHighlight 2s ease;
}
@keyframes chatHighlight {
0%, 100% { box-shadow: none; }
20%, 80% { box-shadow: 0 0 0 3px var(--accent-green, #27ae60); border-radius: 4px; }
}
/* ==========================================================================
Typing Indicator
========================================================================== */
@@ -532,3 +559,22 @@
.theme-clean .chat-typing-dot {
background: #555555;
}
.theme-clean .chat-nav-link {
color: #2ecc71;
border-bottom-color: #2ecc71;
}
.theme-clean .chat-nav-link:hover {
color: #27ae60;
border-bottom-color: #27ae60;
}
.theme-clean .chat-highlight {
animation: chatHighlightDark 2s ease;
}
@keyframes chatHighlightDark {
0%, 100% { box-shadow: none; }
20%, 80% { box-shadow: 0 0 0 3px #2ecc71; border-radius: 4px; }
}
@@ -112,6 +112,23 @@ function closeChatHelpAndAsk(question) {
sendChatQuestion(question);
}
// Navigate from chat link to CV section, then highlight
function scrollToCV(link) {
var anchor = link.getAttribute('href');
var target = document.querySelector(anchor);
if (target) {
// Close chat panel
document.getElementById('chat-panel').classList.remove('chat-open');
document.getElementById('chat-toggle-btn').classList.remove('mascot-active');
// Scroll to target
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Highlight briefly
target.classList.add('chat-highlight');
setTimeout(function() { target.classList.remove('chat-highlight'); }, 2000);
}
return false; // prevent default anchor navigation
}
// Clear input after HTMX request completes
document.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.elt && event.detail.elt.id === 'chat-form') {