Initial functional version of the portfolio chatbot site
Some checks failed
Deploy to GitHub Pages / build-and-deploy (push) Has been cancelled

This commit is contained in:
2025-07-22 08:06:10 +02:00
commit 9eca12ebca
23 changed files with 5069 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
<template>
<footer class="mt-12 pt-8 border-t border-white/10 text-center text-gray-400">
<div class="flex flex-col md:flex-row justify-between items-center space-y-4 md:space-y-0">
<div class="flex items-center space-x-4">
<a
href="https://github.com/tu-usuario"
target="_blank"
class="hover:text-white transition-colors"
>
<Github class="w-5 h-5" />
</a>
<a
href="https://linkedin.com/in/tu-perfil"
target="_blank"
class="hover:text-white transition-colors"
>
<Linkedin class="w-5 h-5" />
</a>
<a
href="mailto:tu.email@ejemplo.com"
class="hover:text-white transition-colors"
>
<Mail class="w-5 h-5" />
</a>
</div>
<div class="text-sm">
<p>&copy; {{ currentYear }} Tu Nombre. Hecho con y Vue.js</p>
</div>
<div class="text-xs">
<p>Versión 1.0.0 Node.js {{ nodeVersion }}</p>
</div>
</div>
</footer>
</template>
<script setup>
import { computed } from 'vue'
import { Github, Linkedin, Mail } from 'lucide-vue-next'
const currentYear = computed(() => new Date().getFullYear())
const nodeVersion = '18+'
</script>

View File

@@ -0,0 +1,57 @@
<template>
<header class="border-b border-white/10 backdrop-blur-sm bg-black/20 sticky top-0 z-50">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full flex items-center justify-center">
<img src="/src/assets/avatar-bot.png" alt="Avatar" class=" rounded-full object-cover" />
</div>
<div>
<h1 class="text-xl font-bold">Pablo de la Torre Jamardo</h1>
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-1">
<div
class="w-2 h-2 rounded-full"
:class="isOnline ? 'bg-green-400' : 'bg-red-400'"
></div>
<span class="text-xs text-gray-300">
{{ isOnline ? 'Online' : 'Offline' }}
</span>
</div>
<span class="text-xs text-gray-400"></span>
<span class="text-xs text-gray-300">Virtual Me, Powered by Code</span>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<button
@click="$emit('toggle-theme')"
class="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
:title="isDark ? 'Cambiar a tema claro' : 'Cambiar a tema oscuro'"
>
<component :is="isDark ? 'Sun' : 'Moon'" class="w-5 h-5" />
</button>
<a
href="https://github.com/tu-usuario/ai-portfolio-chat"
target="_blank"
class="p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
title="Ver código en GitHub"
>
<Github class="w-5 h-5" />
</a>
</div>
</div>
</header>
</template>
<script setup>
import { Bot, Sun, Moon, Github } from 'lucide-vue-next'
defineProps({
isDark: Boolean,
isOnline: Boolean
})
defineEmits(['toggle-theme'])
</script>

View File

@@ -0,0 +1,65 @@
<template>
<form @submit.prevent="handleSubmit" class="relative">
<div class="flex space-x-2">
<div class="flex-1 relative">
<input
:value="input"
@input="$emit('update:input', $event.target.value)"
:disabled="isLoading"
placeholder="Pregúntame sobre mi experiencia, habilidades, proyectos..."
class="w-full px-4 py-3 pr-12 glass-effect rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent placeholder-gray-400 transition-all"
@keydown.enter.prevent="handleSubmit"
/>
<!-- Character counter -->
<div class="absolute right-3 top-1/2 transform -translate-y-1/2 text-xs text-gray-500">
{{ input.length }}/500
</div>
</div>
<button
type="submit"
:disabled="isLoading || !input.trim() || input.length > 500"
class="px-6 py-3 bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl font-semibold disabled:opacity-50 disabled:cursor-not-allowed hover:from-purple-600 hover:to-pink-600 transition-all transform hover:scale-105 active:scale-95"
>
<Send v-if="!isLoading" class="w-5 h-5" />
<div v-else class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
</button>
</div>
<!-- Quick suggestions -->
<div v-if="!input && quickSuggestions.length > 0" class="flex flex-wrap gap-2 mt-3">
<button
v-for="suggestion in quickSuggestions.slice(0, 3)"
:key="suggestion"
@click="$emit('update:input', suggestion)"
class="px-3 py-1 text-sm bg-white/5 hover:bg-white/10 rounded-full border border-white/10 transition-colors"
>
{{ suggestion }}
</button>
</div>
</form>
</template>
<script setup>
import { Send } from 'lucide-vue-next'
const props = defineProps({
input: String,
isLoading: Boolean
})
const emit = defineEmits(['update:input', 'send-message'])
const quickSuggestions = [
'¿Cuál es tu experiencia?',
'¿Qué tecnologías usas?',
'Háblame de tus proyectos'
]
function handleSubmit() {
if (props.input.trim() && !props.isLoading && props.input.length <= 500) {
emit('send-message')
}
}
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div class="space-y-4 mb-6 max-h-96 overflow-y-auto" ref="messagesContainer">
<TransitionGroup name="chat-message" tag="div">
<div
v-for="message in messages"
:key="message.id"
class="flex items-start space-x-3"
:class="message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''"
>
<div class="flex-shrink-0">
<div
class="w-8 h-8 rounded-full flex items-center justify-center"
:class="message.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-cyan-500'
: 'bg-gradient-to-r from-purple-500 to-pink-500'"
>
<component :is="message.role === 'user' ? 'User' : 'Bot'" class="w-4 h-4" />
<img
:src="message.role === 'user' ? avatarUser : avatarBot"
alt="avatar"
class="w-8 h-8 rounded-full object-cover"
/>
</div>
</div>
<div
class="max-w-xs lg:max-w-md px-4 py-3 rounded-2xl"
:class="message.role === 'user'
? 'bg-gradient-to-r from-blue-500 to-cyan-500 text-white'
: 'glass-effect'"
>
<!-- Typing indicator -->
<div v-if="message.role === 'assistant' && message.typing" class="flex space-x-1">
<div class="w-2 h-2 bg-purple-400 rounded-full typing-indicator"></div>
<div class="w-2 h-2 bg-purple-400 rounded-full typing-indicator"></div>
<div class="w-2 h-2 bg-purple-400 rounded-full typing-indicator"></div>
</div>
<!-- Message content -->
<div v-else>
<div v-html="formatMessage(message.content)" class="prose prose-invert max-w-none"></div>
<div v-if="message.role === 'assistant'" class="text-xs text-gray-400 mt-2">
{{ formatTime(message.timestamp) }}
</div>
</div>
</div>
</div>
</TransitionGroup>
</div>
</template>
<script setup>
import { ref, nextTick, watch } from 'vue'
import avatarUser from './../assets/avatar-user.jpg'
import avatarBot from './../assets/avatar-bot.png'
const props = defineProps({
messages: Array,
isLoading: Boolean
})
const messagesContainer = ref(null)
function formatMessage(content) {
return content.replace(/\n/g, '<br>')
}
function formatTime(timestamp) {
if (!timestamp) return ''
return new Date(timestamp).toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit'
})
}
function scrollToBottom() {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
// Watch for new messages and scroll to bottom
watch(() => props.messages.length, () => {
nextTick(() => scrollToBottom())
})
// Expose scrollToBottom method
defineExpose({
scrollToBottom
})
</script>

View File

@@ -0,0 +1,26 @@
<template>
<div class="mt-8 p-6 glass-effect rounded-xl">
<h3 class="text-lg font-semibold mb-4 flex items-center">
<Code class="w-5 h-5 mr-2 text-purple-400" />
Stack Tecnológico Principal
</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="(tech, index) in techStack"
:key="tech"
class="px-3 py-1 bg-purple-500/20 text-purple-300 rounded-full text-sm border border-purple-500/30 hover:bg-purple-500/30 transition-colors cursor-default"
:style="{ animationDelay: `${index * 50}ms` }"
>
{{ tech }}
</span>
</div>
</div>
</template>
<script setup>
import { Code } from 'lucide-vue-next'
defineProps({
techStack: Array
})
</script>

View File

@@ -0,0 +1,64 @@
<template>
<div class="text-center mb-8 animate-fade-in">
<div class="mb-6">
<div class="w-24 h-24 bg-gradient-to-r from-purple-500 to-pink-500 rounded-full mx-auto mb-4 flex items-center justify-center animate-pulse-slow">
<img src="/src/assets/avatar-bot.png" alt="Avatar" class="rounded-full object-cover" />
</div>
<h2 class="text-3xl font-bold mb-2 gradient-text">
¡Hola! Soy tu asistente personal de portfolio
</h2>
<p class="text-gray-300 text-lg max-w-2xl mx-auto">
Pregúntame sobre mi experiencia, habilidades, proyectos o cualquier detalle técnico.
¡Estoy listo para charlar y ayudarte a descubrir mi perfil profesional!
</p>
</div>
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<button
v-for="(suggestion, index) in quickSuggestions"
:key="suggestion.text"
@click="$emit('send-message', suggestion.text)"
class="p-4 glass-effect rounded-xl transition-all hover:scale-105 hover:bg-white/15 text-left group"
:style="{ animationDelay: `${index * 100}ms` }"
>
<component
:is="suggestion.icon"
class="w-6 h-6 mb-2 text-purple-400 group-hover:text-purple-300 transition-colors"
/>
<h3 class="font-semibold mb-1 text-white group-hover:text-purple-100 transition-colors">
{{ suggestion.title }}
</h3>
<p class="text-sm text-gray-400 group-hover:text-gray-300 transition-colors">
{{ suggestion.text }}
</p>
</button>
</div>
<!-- Stats -->
<div class="flex justify-center space-x-8 text-sm text-gray-400">
<div class="text-center">
<div class="text-2xl font-bold text-purple-400">5+</div>
<div>Años Experiencia</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-400">50+</div>
<div>Proyectos</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-purple-400">15+</div>
<div>Tecnologías</div>
</div>
</div>
</div>
</template>
<script setup>
import { Bot, Briefcase, Code, Rocket, GraduationCap, Mail, DollarSign } from 'lucide-vue-next'
defineProps({
quickSuggestions: Array
})
defineEmits(['send-message'])
</script>