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:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user