Initial functional version of the portfolio chatbot site
Some checks failed
Deploy to GitHub Pages / build-and-deploy (push) Has been cancelled
Some checks failed
Deploy to GitHub Pages / build-and-deploy (push) Has been cancelled
This commit is contained in:
44
src/components/AppFooter.vue
Normal file
44
src/components/AppFooter.vue
Normal 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>© {{ 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>
|
||||
57
src/components/AppHeader.vue
Normal file
57
src/components/AppHeader.vue
Normal 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>
|
||||
65
src/components/ChatInput.vue
Normal file
65
src/components/ChatInput.vue
Normal 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>
|
||||
92
src/components/ChatMessages.vue
Normal file
92
src/components/ChatMessages.vue
Normal 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>
|
||||
26
src/components/TechStack.vue
Normal file
26
src/components/TechStack.vue
Normal 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>
|
||||
64
src/components/WelcomeSection.vue
Normal file
64
src/components/WelcomeSection.vue
Normal 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>
|
||||
Reference in New Issue
Block a user