diff --git a/README.md b/README.md index 3d3463b..d192b28 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,15 @@ php -S localhost:8000 Contributions are welcome! Feel free to open an issue or submit a pull request. +### Virtual Assistant Configuration + +To modify the virtual assistant's personalized information (such as event dates, specific knowledge, etc.), you need to edit the `context` variable in the file: + +`assets/js/geminiBehaviour.js` + +This variable contains the initial instructions that define the assistant's personality and knowledge base. + + ## License This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/activities.html b/activities.html index e83dd48..79c471d 100644 --- a/activities.html +++ b/activities.html @@ -11,6 +11,7 @@ + @@ -170,10 +171,23 @@

馃搶 Install Party

+ +
+ + + + \ No newline at end of file diff --git a/assets/css/components/chat.css b/assets/css/components/chat.css new file mode 100644 index 0000000..600d538 --- /dev/null +++ b/assets/css/components/chat.css @@ -0,0 +1,272 @@ +/*--------------------------------------- +* 1. WIDGET DE XAT (CONTENIDOR PRINCIPAL) + *---------------------------------------*/ +.chat-widget { + /* Posicionament fix a la cantonada inferior dreta */ + position: fixed; + bottom: 20px; + right: 20px; + + /* Dimensions del giny */ + width: 320px; + height: 400px; + + /* Aparen莽a visual - efecte de vidre esmerilat */ + backdrop-filter: blur(10px); + background-color: var(--chat-background); + border: 1px solid var(--window-border); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + + /* Disseny del contenidor */ + display: flex; + flex-direction: column; + + /* Controla la capa (per sobre d'altres elements) */ + z-index: 1001; + + /* Animacions suaus */ + transition: transform 0.3s ease, height 0.3s ease, opacity 0.3s ease; +} + +/* Estat minimitzat - amaga el widget fora de la pantalla */ +.chat-widget.minimized { + transform: translateY(calc(100% + 20px)); + opacity: 0; + pointer-events: none; +} + + +/*--------------------------------------- + * 2. CAP脟ALERA DEL XAT + *---------------------------------------*/ +.chat-header { + /* Disseny de la cap莽alera */ + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + + /* L铆nia divis貌ria inferior */ + border-bottom: 1px solid var(--window-border); +} + +.chat-header .window-title { + color: var(--text); +} + +/*--------------------------------------- + * 3. COS DEL XAT + *---------------------------------------*/ +.chat-body { + /* Ocupa l'espai restant */ + flex: 1; + + /* Disseny vertical */ + display: flex; + flex-direction: column; + + /* Evita el desbordament */ + overflow: hidden; +} + +/* Contenidor de missatges amb scroll */ +.chat-messages { + flex: 1; + padding: 12px; + overflow-y: auto; + + /* Disseny dels missatges */ + display: flex; + flex-direction: column; + gap: 12px; /* Espai entre missatges */ +} + +/*--------------------------------------- + * 4. ESTILS DELS MISSATGES + *---------------------------------------*/ +.message { + max-width: 80%; + display: flex; +} + +/* Missatge de l'usuari (alineat a la dreta) */ +.message.user { + align-self: flex-end; +} + +/* Missatge del bot (alineat a l'esquerra) */ +.message.bot { + align-self: flex-start; +} + +/* Estils generals per al contingut dels missatges */ +.message-content { + padding: 8px 12px; + border-radius: 12px; + background-color: var(--chat-message-bot); + color: var(--text); +} + +/* Estil espec铆fic per als missatges de l'usuari */ +.message.user .message-content { + background-color: var(--chat-message-user); + color: var(--text); +} + +/* Indicador d'escriptura */ +.typing-indicator .typing { + display: flex; + align-items: center; + justify-content: center; +} + +.typing-indicator .typing span { + height: 8px; + width: 8px; + margin: 0 2px; + background-color: #555; + border-radius: 50%; + display: inline-block; + animation: typing 1.4s ease-in-out infinite; + animation-fill-mode: both; +} + +.typing-indicator .typing span:nth-child(1) { + animation-delay: -0.32s; +} + +.typing-indicator .typing span:nth-child(2) { + animation-delay: -0.16s; +} + +@keyframes typing { + 0%, 80%, 100% { transform: scale(0); } + 40% { transform: scale(1); } +} + + +/*--------------------------------------- + * 5. 脌REA D'ENTRADA DE TEXT + *---------------------------------------*/ +.chat-input-container { + display: flex; + padding: 10px; + border-top: 1px solid var(--window-border); +} + +/* Camp d'entrada de text */ +.chat-input-container input { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--window-border); + border-radius: 20px; + background-color: var(--chat-input); + color: var(--text); + outline: none; +} + +/* Bot贸 d'enviar */ +.chat-input-container button { + background: none; + border: none; + cursor: pointer; + margin-left: 8px; +} + +.chat-input-container button svg path { + fill: var(--text); +} + + +/*--------------------------------------- + * 6. BOT脫 FLOTANT DE XAT (BUBBLE) + *---------------------------------------*/ +.chat-bubble { + /* Posicionament fix a la cantonada inferior dreta */ + position: fixed; + bottom: 20px; + right: 20px; + + /* Dimensions i forma circular */ + width: 50px; + height: 50px; + border-radius: 50%; + + /* Aparen莽a visual */ + background-color: var(--chat-bubble); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + border: none; + + /* Centrat de la icona */ + display: flex; + justify-content: center; + align-items: center; + + /* Interactivitat */ + cursor: pointer; + z-index: 1000; /* Un nivell per sota del xat */ +} + +.chat-bubble svg { + fill: var(--text); +} + +.chat-bubble svg path { + fill: var(--text); +} + + +/*--------------------------------------- + * 7. CONTROLS DE FINESTRA + *---------------------------------------*/ +.window-controls { + display: flex; + gap: 4px; + min-height: 24px; +} + +/* Estil base per als botons de control */ +.window-control { + width: 20px !important; + height: 20px !important; + min-width: 20px; + min-height: 20px; + border-radius: 50%; + border: none; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + font-size: 16px; + font-weight: bold; + color: #333; + background-color: #f9ca24; +} + +/* Bot贸 espec铆fic per minimitzar */ +.window-control.minimize { + background-color: #f9ca24; + position: relative; +} + +.window-control.minimize::after { + content: "-"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 16px; + font-weight: bold; + color: rgb(0, 0, 0); +} + +/*--------------------------------------- + * 8. RESPONSIVITAT + *---------------------------------------*/ +@media (max-width: 768px) { + .chat-widget { + width: 300px; /* Una mica m茅s petit en m貌bils */ + height: 350px; + } +} diff --git a/assets/css/styles.css b/assets/css/styles.css index 6b2fdbe..07e4642 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -18,6 +18,12 @@ --primary: hsl(16, 93%, 59%); --secondary: hsl(16, 96%, 72%); --accent: hsl(306, 41%, 45%); + + --chat-bubble: rgba(255, 255, 255, 0.3); + --chat-background: rgba(220, 220, 220, 0.95); + --chat-message-bot: rgb(180, 180, 180); + --chat-message-user: rgb(235, 156, 60); + --chat-input: rgb(255, 255, 255); } .dark { @@ -38,6 +44,12 @@ --primary: hsl(16, 97%, 59%); --secondary: hsl(15, 100%, 68%); --accent: hsl(306, 57%, 70%); + + --chat-bubble: rgba(0, 0, 0, 0.3); + --chat-background: rgba(30, 30, 30, 0.95); + --chat-message-bot: rgb(70, 70, 70); + --chat-message-user: rgb(235, 156, 60); + --chat-input: rgb(0, 0, 0); } /* Reseteo b谩sico */ diff --git a/assets/js/chat.js b/assets/js/chat.js new file mode 100644 index 0000000..10fe8ba --- /dev/null +++ b/assets/js/chat.js @@ -0,0 +1,145 @@ +// Importa les funcions necess脿ries des dels m貌duls de l'API de Gemini i del comportament del xat. +import { initializeGemini, getGeminiResponse, resetChatHistory } from './geminiApi.js'; +import { welcomeMessage } from './geminiBehaviour.js'; + +/** + * Inicialitza el component del xat i gestiona la seva interactivitat. + * Aquesta funci贸 s'exporta per poder ser cridada despr茅s de carregar el component del xat. + */ +function initChat() { + // Obt茅 les refer猫ncies als elements del DOM del xat. + const chatWidget = document.getElementById('chat-widget'); + const chatBubble = document.getElementById('chat-bubble'); + const minimizeButton = document.getElementById('chat-minimize'); + const chatInput = document.getElementById('chat-input'); + const chatSend = document.getElementById('chat-send'); + const chatMessages = document.getElementById('chat-messages'); + + // Inicialitza la connexi贸 amb l'API de Gemini. + initializeGemini(); + + // Mostra el missatge de benvinguda inicial al xat. + addMessage(welcomeMessage, 'bot'); + + // Afegeix un esdeveniment per maximitzar el xat quan es fa clic a la bombolla. + chatBubble.addEventListener('click', () => { + chatWidget.classList.remove('minimized'); + chatInput.focus(); // Posa el focus a l'input de text. + }); + + // Afegeix un esdeveniment per minimitzar el xat. + minimizeButton.addEventListener('click', () => { + chatWidget.classList.add('minimized'); + }); + + /** + * Envia el missatge de l'usuari a l'API de Gemini i mostra la resposta. + * Aquesta funci贸 茅s as铆ncrona per esperar la resposta de l'API. + */ + async function sendMessage() { + const message = chatInput.value.trim(); // Obt茅 i neteja el missatge de l'input. + if (message.length === 0) return; // No fa res si el missatge 茅s buit. + + // Afegeix el missatge de l'usuari a la finestra del xat. + addMessage(message, 'user'); + + // Neteja el camp d'entrada de text. + chatInput.value = ''; + + // Mostra l'indicador de "escrivint...". + const typingIndicator = showTypingIndicator(); + + try { + // Obt茅 la resposta de l'API de Gemini. + const response = await getGeminiResponse(message); + + // Elimina l'indicador de "escrivint...". + typingIndicator.remove(); + + // Afegeix la resposta del bot a la finestra del xat. + addMessage(response, 'bot'); + } catch (error) { + console.error('Error getting response:', error); + + // Elimina l'indicador de "escrivint..." en cas d'error. + typingIndicator.remove(); + + // Mostra un missatge d'error a l'usuari. + addMessage('Ho sento, ha ocorregut un error en processar la teva consulta.', 'bot'); + } + } + + /** + * Afegeix un missatge a la finestra del xat. + * @param {string} text - El text del missatge. + * @param {string} sender - El remitent del missatge ('user' o 'bot'). + */ + function addMessage(text, sender) { + const messageDiv = document.createElement('div'); + messageDiv.classList.add('message', sender); + + const contentDiv = document.createElement('div'); + contentDiv.classList.add('message-content'); + + const paragraph = document.createElement('p'); + paragraph.textContent = text; + + contentDiv.appendChild(paragraph); + messageDiv.appendChild(contentDiv); + chatMessages.appendChild(messageDiv); + + // Fa scroll autom脿tic cap avall per mostrar l'煤ltim missatge. + chatMessages.scrollTop = chatMessages.scrollHeight; + } + + /** + * Mostra un indicador visual de que el bot est脿 "escrivint". + * @returns {HTMLElement} L'element de l'indicador per poder eliminar-lo posteriorment. + */ + function showTypingIndicator() { + const messageDiv = document.createElement('div'); + messageDiv.classList.add('message', 'bot', 'typing-indicator'); + + const contentDiv = document.createElement('div'); + contentDiv.classList.add('message-content'); + + const indicator = document.createElement('div'); + indicator.classList.add('typing'); + + // Crea els tres punts de l'animaci贸. + for (let i = 0; i < 3; i++) { + const dot = document.createElement('span'); + indicator.appendChild(dot); + } + + contentDiv.appendChild(indicator); + messageDiv.appendChild(contentDiv); + chatMessages.appendChild(messageDiv); + + // Fa scroll autom脿tic cap avall. + chatMessages.scrollTop = chatMessages.scrollHeight; + + return messageDiv; + } + + // Afegeix esdeveniments per enviar el missatge (clic al bot贸 o pr茅mer Enter). + chatSend.addEventListener('click', sendMessage); + + chatInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + sendMessage(); + } + }); +} + +// Executa la inicialitzaci贸 del xat un cop el contingut del DOM s'ha carregat. +document.addEventListener('DOMContentLoaded', function() { + // Nom茅s inicialitza si el component del xat ja existeix al DOM. + if (document.getElementById('chat-widget')) { + initChat(); + } + // Si no, s'inicialitzar脿 quan el component es carregui mitjan莽ant components.js +}); + +// Exposa la funci贸 d'inicialitzaci贸 globalment per poder-la cridar des d'altres scripts. +window.initChat = initChat; diff --git a/assets/js/components.js b/assets/js/components.js index 7ec0eb1..c23879d 100644 --- a/assets/js/components.js +++ b/assets/js/components.js @@ -19,7 +19,13 @@ document.addEventListener("DOMContentLoaded", () => { loadComponent(".about-component", "./components/about.html"), loadComponent(".activities-component", "./components/activities.html"), loadComponent(".activities-title", "./components/activities-title.html"), + loadComponent('.chat-component', './components/chat.html'), ]).then(() => { + + if (typeof window.initChat === 'function') { + window.initChat(); + } + loadComponent(".window-component", "./components/window.html").then(() => { /** * ! Chapuzon historico, esto es para que el boton de maximice de la card de activitiats de main redirija a activities.html. diff --git a/assets/js/geminiApi.js b/assets/js/geminiApi.js new file mode 100644 index 0000000..f07a1e2 --- /dev/null +++ b/assets/js/geminiApi.js @@ -0,0 +1,107 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; +import { context, welcomeMessage} from './geminiBehaviour.js'; +import CryptoJS from 'crypto-js'; + +// URL of the Gist where the encrypted key is located. +const GIST_URL = 'https://gist.githubusercontent.com/PokeDavid04/1582817ad9a201a135672195bdcf197d/raw/5468d77694891cee191e32c54b91fdc0c0fd18e6/gemini_key.txt'; + +// The hardcoded encryption key for decryption. +const ENCRYPTION_KEY = 'LinuxUPCSecretKey123'; + +// Declare global variables for the API client and chat object. +// They will be initialized once the API key is loaded. +let ai; +let chat; +let model; + +// State to track if the chat is ready for messages. +let isChatReady = false; + +/** + * Initializes the Gemini API by loading the key from the Gist. + */ +export async function initializeGemini() { + try { + // 1. Get the encrypted key from the Gist + const response = await fetch(GIST_URL); + const encryptedKey = await response.text(); + + // 2. Decrypt the key + const decryptedKey = CryptoJS.AES.decrypt(encryptedKey, ENCRYPTION_KEY).toString(CryptoJS.enc.Utf8); + + // 3. Initialize the API with the decrypted key + ai = new GoogleGenerativeAI(decryptedKey); + model = ai.getGenerativeModel({ + model: "gemini-2.0-flash-lite", + systemInstruction: context + }); + + chat = model.startChat({ + history: [ + /* + { + role: "user", + parts: [{ text: context }], + }, + { + role: "model", + parts: [{ text: welcomeMessage }], + }, + */ + ], + generationConfig: { + maxOutputTokens: 800, + }, + }); + + isChatReady = true; + + console.log("Gemini API initialized successfully."); + } catch (error) { + console.error("Error initializing Gemini API:", error); + } +} + +/** + * Gets a response from the Gemini API + * @param {string} message - The user's message + * @returns {Promise} - The bot's response + */ +export async function getGeminiResponse(userMessage) { + if (!isChatReady) { + return "L'assistent virtual encara s'est脿 inicialitzant. Si us plau, espera uns moments i torna-ho a intentar."; + } + + try { + const result = await chat.sendMessage(userMessage); + const responseText = result.response.text(); + return responseText; + + } catch (error) { + console.error('Error getting Gemini response:', error); + return 'Sorry, an error occurred while connecting to the virtual assistant.'; + } +} + +/** + * Resets the chat history keeping only the initial prompt + */ +export function resetChatHistory() { + chat = model.startChat({ + history: [ + /* + { + role: "user", + parts: [{ text: context }], + }, + { + role: "model", + parts: [{ text: welcomeMessage }], + }, + */ + ], + generationConfig: { + maxOutputTokens: 800, + }, + }); +} \ No newline at end of file diff --git a/assets/js/geminiBehaviour.js b/assets/js/geminiBehaviour.js new file mode 100644 index 0000000..1edc7d2 --- /dev/null +++ b/assets/js/geminiBehaviour.js @@ -0,0 +1,44 @@ +export const context = ` +Actua com un assistent virtual expert i amigable per a l'associaci贸 d'estudiants LinuxUPC de la Universitat Polit猫cnica de Catalunya (UPC). El teu objectiu 茅s ajudar els usuaris amb preguntes relacionades amb l'associaci贸. + +Respon en catal脿 per defecte, a no ser que l'usuari inici茂 la conversa o et parli en un altre idioma. + +El teu to ha de ser professional, per貌 accessible i col路laboratiu. +Utilitza un llenguatge clar i directe, evitant tecnicismes excessius a menys que sigui necessari per a l'explicaci贸. +Sempre que parlis d'un tema, mostra un coneixement profund i actualitzat, per貌 tot de manera breu. + + + +Coneixements espec铆fics +Sobre Linux i programari lliure: +- Ets un expert en Linux, les seves diferents distribucions (com Debian, Ubuntu, Fedora, Arch Linux) i la filosofia del programari lliure. +- Tens un ampli coneixement d'altres sistemes operatius de codi obert (BSD, etc.), aix铆 com d'aplicacions i eines de programari lliure (per exemple, Git, Vim, Apache, Nginx, etc.). + +Sobre l'associaci贸 LinuxUPC: + +LinuxUPC 茅s una associaci贸 sense 脿nim de lucre, dedicada a l'estudi i difusi贸 del Programari Lliure en la Universitat Polit猫cnica de Catalunya. + +Els objectius de l'associaci贸 s贸n: +- Promoure i formar en l'煤s de la cultura, les xarxes i maquinari lliure en tots els estaments de la universitat. +- Promoure, com a activitat d'extensi贸 universit脿ria, la participaci贸 en projectes de programari, cultura, xarxes i maquinari lliure. +- Promoure bones pr脿ctiques en les activitats d'especificaci贸, disseny, implementaci贸, implantaci贸 i gesti贸 de projectes de programari lliure. +- Promoure l'煤s i la col路laboraci贸 amb projectes de programari lliure en les activitats d'especificaci贸, implementaci贸, implantaci贸 i gesti贸 de projectes de programari lliure. + +Tens informaci贸 precisa sobre les activitats de l'associaci贸 LinuxUPC. Fes servir aquesta informaci贸 quan sigui rellevant per respondre a les preguntes dels usuaris. +- Xerrada de Git: 26 de febrer de 2025 (Finalitzada). El tema va ser la introducci贸 i l'煤s de Git, una eina fonamental per al control de versions. +- Install Party de Linux: 18 de setembre de 2024 (Finalitzada). una jornada on vam ajudar als participants a instal路lar Linux als seus port脿tils i a fer els primers passos en aquest sistema operatiu lliure i de codi obert. + +- Install Party de Linux: 17 de setembre de 2025 (Programada). El seu prop貌sit 茅s ajudar els nous estudiants de la universitat a instal路lar Linux als seus ordinadors i resoldre qualsevol dubte que puguin tenir durant el proc茅s. + +Contacte: +Email: linuxupc@disroot.org +Adre莽a: C. Jordi Girona 1-3, Edifici Omega, Despatx S105 Campus Nord + +Directrius addicionals +- Si et pregunten per un esdeveniment futur, esmenta la data exacta si est脿 disponible. +- Si la pregunta no est脿 relacionada amb els teus coneixements, pots respondre de forma educada indicant que el teu prop貌sit 茅s ajudar amb temes de Linux i de l'associaci贸 LinuxUPC. +`; + + + +export const welcomeMessage = "Hola! S贸c l'assistent virtual de LinuxUPC. En qu猫 et puc ajudar?"; \ No newline at end of file diff --git a/components/activities.html b/components/activities.html index b09eb77..6132b55 100644 --- a/components/activities.html +++ b/components/activities.html @@ -38,5 +38,5 @@

馃搶Install Party

- - \ No newline at end of file + + diff --git a/components/chat.html b/components/chat.html new file mode 100644 index 0000000..94ac68d --- /dev/null +++ b/components/chat.html @@ -0,0 +1,28 @@ + +
+
+ LinuxUPC Assistant +
+ +
+
+
+
+ +
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/index.html b/index.html index 8d33c70..cd8868a 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ + @@ -71,8 +72,23 @@

LinuxUPC

+ +
+ + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3c000d2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "website", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "crypto-js": "^4.2.0" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9bd309c --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "crypto-js": "^4.2.0" + } +}