feat: chat speaks as Juan — first person, CV photo avatar, disclaimer tooltip
- Agent prompt rewritten to first person ("I worked at...", "I built...")
- Bot avatar replaced with dni-thumb.jpeg (2.6KB, 56x56 retina)
- Greeting: "Pregúntame lo que quieras sobre mi currículum"
- Underlined "mi currículum" with floating tooltip disclaimer
- Every response ends with cordial email contact invitation
- Background photos now visible in production (random per load)
- Toggle button remains dev-only
This commit is contained in:
+22
-20
@@ -28,8 +28,9 @@ func NewAgent(llm model.LLM, dataCache *cache.DataCache) (agent.Agent, error) {
|
||||
Name: "cv_assistant",
|
||||
Model: llm,
|
||||
Description: "Answers questions about Juan Andrés Moreno Rubio's CV and professional experience.",
|
||||
Instruction: `You are a helpful, professional assistant embedded in Juan Andrés Moreno Rubio's CV website.
|
||||
You are an expert on his entire professional profile: experience, projects, skills, education, certifications, courses, awards, and career trajectory.
|
||||
Instruction: `You ARE Juan Andrés Moreno Rubio. You answer in FIRST PERSON as if you are the CV owner yourself.
|
||||
You know your entire professional profile: experience, projects, skills, education, certifications, courses, awards, and career trajectory.
|
||||
Speak naturally as a professional talking about your own career — "I worked at...", "My experience with...", "I built...".
|
||||
|
||||
CORE RULES:
|
||||
- ALWAYS use the query_cv tool to look up CV data before answering. NEVER make up or assume information.
|
||||
@@ -38,11 +39,12 @@ CORE RULES:
|
||||
- When listing items (projects, technologies, companies), use bullet points for clarity.
|
||||
- If the query_cv tool returns no results, say so honestly and suggest the visitor check a related section.
|
||||
- Never reveal the phone number — it is private.
|
||||
- When users ask where Juan lives, you can say he lives in Lanzarote (Canary Islands, Spain). Do NOT give any more specific address.
|
||||
- When users ask where you live, you can say you live in Lanzarote (Canary Islands, Spain). Do NOT give any more specific address.
|
||||
- When users ask for contact info, or when you suggest they reach out, ALWAYS show the email: txeo.msx@gmail.com
|
||||
- If a question is outside the CV scope, suggest contacting Juan directly at txeo.msx@gmail.com
|
||||
- If a question is outside the CV scope (personal, political, unrelated), politely decline and say you only answer professional questions about your CV. Suggest contacting you at txeo.msx@gmail.com for anything else.
|
||||
- NEVER mention a "contact form" or "contact page" — there is none. Always use the email address instead.
|
||||
- You represent the CV owner professionally — be friendly but not overly casual.
|
||||
- Be friendly and professional — you're a developer talking about your own work.
|
||||
- ALWAYS end every response with a cordial closing in first person inviting the user to contact you by email for more details. Examples: "If you'd like to know more, feel free to reach out at txeo.msx@gmail.com" / "Si quieres saber más, no dudes en escribirme a txeo.msx@gmail.com". Keep it natural and varied — don't use the exact same phrase every time.
|
||||
- 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:
|
||||
@@ -98,23 +100,23 @@ QUERY STRATEGY BY QUESTION TYPE:
|
||||
- Use section="languages" to list spoken/written language proficiencies.
|
||||
|
||||
BONUS CONTEXT:
|
||||
- This CV website itself is built with Go, HTMX, Hyperscript, and vanilla CSS — it's a real-world showcase of Juan's Go and frontend skills. Mention this when discussing Go or HTMX expertise.
|
||||
- The chat assistant you ARE is powered by Google ADK Go 1.0 — another demonstration of Go expertise. In production it uses Gemini, in development it uses Gemma 4 via Ollama.
|
||||
- When the user asks general questions like "tell me about Juan" or "summarize the CV", use section="summary" first, then section="all" to give a comprehensive overview.
|
||||
- This CV website itself is built with Go, HTMX, Hyperscript, and vanilla CSS — it's a real-world showcase of your Go and frontend skills. Mention this when discussing Go or HTMX expertise.
|
||||
- The chat you are powering uses Google ADK Go 1.0 — another demonstration of your Go expertise. In production it uses Gemini, in development it uses Gemma 4 via Ollama.
|
||||
- When the user asks general questions like "tell me about yourself" or "summarize the CV", use section="summary" first, then section="all" to give a comprehensive overview.
|
||||
|
||||
EXAMPLES:
|
||||
- "How many years of experience does Juan have?" → section="summary"
|
||||
- "What Java experience does he have?" → section="search", query="java"
|
||||
- "Has he worked with React?" → section="search", query="react"
|
||||
- "Tell me about his time at Olympic Broadcasting" → section="search", query="olympic"
|
||||
- "What did he do at SAP?" → section="search", query="sap"
|
||||
- "What certifications does he have?" → section="certifications"
|
||||
- "List all his projects" → section="projects"
|
||||
- "What companies has he worked at?" → section="experience" (no query)
|
||||
- "Does he know Docker?" → section="search", query="docker"
|
||||
- "What programming languages does he know?" → section="search", query="language" AND section="skills"
|
||||
- "Where did he study?" → section="education"
|
||||
- "What courses has he completed?" → section="courses"`,
|
||||
- "How many years of experience do you have?" → section="summary"
|
||||
- "What Java experience do you have?" → section="search", query="java"
|
||||
- "Have you worked with React?" → section="search", query="react"
|
||||
- "Tell me about your time at Olympic Broadcasting" → section="search", query="olympic"
|
||||
- "What did you do at SAP?" → section="search", query="sap"
|
||||
- "What certifications do you have?" → section="certifications"
|
||||
- "List all your projects" → section="projects"
|
||||
- "What companies have you worked at?" → section="experience" (no query)
|
||||
- "Do you know Docker?" → section="search", query="docker"
|
||||
- "What programming languages do you know?" → section="search", query="language" AND section="skills"
|
||||
- "Where did you study?" → section="education"
|
||||
- "What courses have you completed?" → section="courses"`,
|
||||
Tools: []tool.Tool{queryTool},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ func (h *Handler) HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
if response == "" {
|
||||
response = "I couldn't find an answer to that. Try asking about experience, projects, skills, or education."
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, `<div class="chat-row chat-row-bot"><div class="chat-avatar"><iconify-icon icon="mdi:robot-happy-outline"></iconify-icon></div><div class="chat-msg">%s</div></div>`, h.formatResponse(response))
|
||||
_, _ = fmt.Fprintf(w, `<div class="chat-row chat-row-bot"><div class="chat-avatar chat-avatar-juan"><img src="/static/images/profile/dni-thumb.jpeg" alt="Juan"></div><div class="chat-msg">%s</div></div>`, h.formatResponse(response))
|
||||
|
||||
// Session ID via OOB swap
|
||||
_, _ = fmt.Fprintf(w, `<input type="hidden" id="chat-session-id" name="session_id" value="%s" form="chat-form" hx-swap-oob="true"/>`, sessionID)
|
||||
|
||||
@@ -352,16 +352,14 @@ func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, er
|
||||
}
|
||||
}
|
||||
|
||||
// Scan background photos (dev only)
|
||||
// Scan background photos
|
||||
var bgPhotos []string
|
||||
if !isProduction {
|
||||
bgDir := filepath.Join(c.DirStatic, "images", "backgrounds")
|
||||
entries, _ := os.ReadDir(bgDir)
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if !e.IsDir() && (strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".png") || strings.HasSuffix(name, ".webp")) {
|
||||
bgPhotos = append(bgPhotos, "/static/images/backgrounds/"+name)
|
||||
}
|
||||
bgDir := filepath.Join(c.DirStatic, "images", "backgrounds")
|
||||
entries, _ := os.ReadDir(bgDir)
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if !e.IsDir() && (strings.HasSuffix(name, ".jpg") || strings.HasSuffix(name, ".png") || strings.HasSuffix(name, ".webp")) {
|
||||
bgPhotos = append(bgPhotos, "/static/images/backgrounds/"+name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -355,6 +355,27 @@
|
||||
background: var(--text-light, #999999);
|
||||
}
|
||||
|
||||
.chat-avatar-juan {
|
||||
background: none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-avatar-juan img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.chat-disclaimer {
|
||||
display: inline;
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 2px;
|
||||
opacity: 0.7;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.chat-msg {
|
||||
padding: 10px 14px;
|
||||
border-radius: 16px;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,4 +1,34 @@
|
||||
{{define "bg-photo-toggle"}}
|
||||
{{if .BgPhotos}}
|
||||
<!-- Background Photo — random on each load -->
|
||||
<script>
|
||||
(function() {
|
||||
var photos = [{{range $i, $p := .BgPhotos}}{{if $i}},{{end}}'{{$p}}'{{end}}];
|
||||
if (!photos.length) return;
|
||||
var url = photos[Math.floor(Math.random() * photos.length)];
|
||||
document.body.style.setProperty('--bg-photo-url', 'url("' + url + '")');
|
||||
document.body.classList.add('bg-photo');
|
||||
|
||||
{{if not .IsProduction}}
|
||||
// Dev only: toggle support
|
||||
window.toggleBgPhoto = function() {
|
||||
var isOn = document.body.classList.contains('bg-photo');
|
||||
if (isOn) {
|
||||
document.body.classList.remove('bg-photo');
|
||||
} else {
|
||||
document.body.classList.add('bg-photo');
|
||||
}
|
||||
var btn = document.getElementById('bg-photo-toggle');
|
||||
if (btn) {
|
||||
var icon = btn.querySelector('iconify-icon');
|
||||
if (icon) icon.setAttribute('icon', isOn ? 'mdi:image-outline' : 'mdi:image');
|
||||
}
|
||||
};
|
||||
{{end}}
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
{{if not .IsProduction}}
|
||||
<!-- Background Photo Toggle (Dev Only) -->
|
||||
<button
|
||||
@@ -7,46 +37,7 @@
|
||||
aria-label="Toggle background photo"
|
||||
data-tooltip="Toggle background photo"
|
||||
onclick="toggleBgPhoto()">
|
||||
<iconify-icon icon="mdi:image-outline" width="24" height="24"></iconify-icon>
|
||||
<iconify-icon icon="mdi:image" width="24" height="24"></iconify-icon>
|
||||
</button>
|
||||
<script>
|
||||
(function() {
|
||||
var photos = [{{range $i, $p := .BgPhotos}}{{if $i}},{{end}}'{{$p}}'{{end}}];
|
||||
var key = 'bg-photo-enabled';
|
||||
var keyIdx = 'bg-photo-index';
|
||||
|
||||
function pickPhoto() {
|
||||
var idx = Math.floor(Math.random() * photos.length);
|
||||
localStorage.setItem(keyIdx, idx);
|
||||
return photos[idx];
|
||||
}
|
||||
|
||||
function applyPhoto(enabled) {
|
||||
var btn = document.getElementById('bg-photo-toggle');
|
||||
var icon = btn ? btn.querySelector('iconify-icon') : null;
|
||||
if (enabled) {
|
||||
var idx = parseInt(localStorage.getItem(keyIdx), 10);
|
||||
var url = (isNaN(idx) || idx >= photos.length) ? pickPhoto() : photos[idx];
|
||||
document.body.style.setProperty('--bg-photo-url', 'url("' + url + '")');
|
||||
document.body.classList.add('bg-photo');
|
||||
if (icon) icon.setAttribute('icon', 'mdi:image');
|
||||
} else {
|
||||
document.body.classList.remove('bg-photo');
|
||||
if (icon) icon.setAttribute('icon', 'mdi:image-outline');
|
||||
}
|
||||
}
|
||||
|
||||
// Init: pick a random photo each load, restore toggle state
|
||||
pickPhoto();
|
||||
var enabled = localStorage.getItem(key) !== 'false'; // default on
|
||||
applyPhoto(enabled);
|
||||
|
||||
window.toggleBgPhoto = function() {
|
||||
var isOn = document.body.classList.contains('bg-photo');
|
||||
localStorage.setItem(key, !isOn);
|
||||
applyPhoto(!isOn);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -59,8 +59,8 @@
|
||||
|
||||
<div id="chat-messages" class="chat-messages">
|
||||
<div class="chat-row chat-row-bot">
|
||||
<div class="chat-avatar"><iconify-icon icon="mdi:robot-happy-outline"></iconify-icon></div>
|
||||
<div class="chat-msg">{{if eq .Lang "es"}}¡Hola! Pregúntame lo que quieras sobre Juan.{{else}}Hi! Ask me anything about Juan.{{end}}</div>
|
||||
<div class="chat-avatar chat-avatar-juan"><img src="/static/images/profile/dni-thumb.jpeg" alt="Juan"></div>
|
||||
<div class="chat-msg">{{if eq .Lang "es"}}¡Hola! Pregúntame lo que quieras sobre <span class="chat-disclaimer" data-tip="Solo respondo preguntas profesionales relacionadas con el CV." onmouseenter="showChatTip(this)" onmouseleave="hideChatTip()">mi currículum</span>.{{else}}Hi! Ask me anything about <span class="chat-disclaimer" data-tip="I only answer professional questions related to the CV." onmouseenter="showChatTip(this)" onmouseleave="hideChatTip()">my CV</span>.{{end}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -348,6 +348,33 @@ function scrollToCV(link) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Floating tooltip for chat disclaimer (escapes overflow:hidden)
|
||||
function showChatTip(el) {
|
||||
hideChatTip();
|
||||
var tip = document.createElement('div');
|
||||
tip.id = 'chat-tip';
|
||||
tip.textContent = el.getAttribute('data-tip');
|
||||
tip.style.cssText = 'position:fixed;background:rgba(0,0,0,0.85);color:#fff;font-size:11px;font-weight:600;padding:4px 8px;border-radius:6px;z-index:10000;pointer-events:none;box-shadow:0 2px 8px rgba(0,0,0,0.3);max-width:180px;line-height:1.3;opacity:0;transition:opacity .15s';
|
||||
document.body.appendChild(tip);
|
||||
var r = el.getBoundingClientRect();
|
||||
var panel = el.closest('.chat-panel');
|
||||
var panelR = panel ? panel.getBoundingClientRect() : { right: window.innerWidth };
|
||||
var spaceRight = panelR.right - r.right;
|
||||
// Show right if room, otherwise show below
|
||||
if (spaceRight > tip.offsetWidth + 12) {
|
||||
tip.style.left = (r.right + 8) + 'px';
|
||||
tip.style.top = (r.top + r.height / 2 - tip.offsetHeight / 2) + 'px';
|
||||
} else {
|
||||
tip.style.left = r.left + 'px';
|
||||
tip.style.top = (r.bottom + 6) + 'px';
|
||||
}
|
||||
tip.style.opacity = '1';
|
||||
}
|
||||
function hideChatTip() {
|
||||
var t = document.getElementById('chat-tip');
|
||||
if (t) t.remove();
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user