mirror of
https://github.com/alexankitty/Myrient-Search-Engine.git
synced 2026-01-15 16:33:15 -03:00
AI support
This commit is contained in:
6
.env
6
.env
@@ -18,6 +18,12 @@ EMULATOR_ENABLED=true
|
|||||||
# Set the hostname
|
# Set the hostname
|
||||||
HOSTNAME=myrient.mahou.one
|
HOSTNAME=myrient.mahou.one
|
||||||
|
|
||||||
|
# OpenAI-compatible API settings
|
||||||
|
AI_ENABLED=true
|
||||||
|
AI_API_KEY=gsk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
AI_API_URL=https://api.groq.com/openai/v1/chat/completions
|
||||||
|
AI_MODEL=openai/gpt-oss-120b
|
||||||
|
|
||||||
# Run docker-compose.dev.yml for running locally
|
# Run docker-compose.dev.yml for running locally
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
POSTGRES_HOST=localhost
|
POSTGRES_HOST=localhost
|
||||||
|
|||||||
@@ -78,6 +78,12 @@ To ensure OpenGraph metadata embed for chat apps works correctly, please be sure
|
|||||||
# Metadata
|
# Metadata
|
||||||
To enable metadata synchronize and matching, you will need to create a developer application in the [Twitch TV Developer Console](https://dev.twitch.tv/console) and then add your client id to `TWITCH_CLIENT_ID` in `.env` or `docker-compose.yml` along with adding your client secret to `TWITCH_CLIENT_SECRET`. Metadata takes about half an hour to synchronize from IGDB to your database, and about another half an hour to match via Postgres Full Text Search. Once all other database maintenance operations are done, the database will attempt to match anything that still isn't matched using a much slower fuzzy trigram search that can take up to a day to complete. These processes won't run again until a new crawl of myrient has been performed and the file count has increased.
|
To enable metadata synchronize and matching, you will need to create a developer application in the [Twitch TV Developer Console](https://dev.twitch.tv/console) and then add your client id to `TWITCH_CLIENT_ID` in `.env` or `docker-compose.yml` along with adding your client secret to `TWITCH_CLIENT_SECRET`. Metadata takes about half an hour to synchronize from IGDB to your database, and about another half an hour to match via Postgres Full Text Search. Once all other database maintenance operations are done, the database will attempt to match anything that still isn't matched using a much slower fuzzy trigram search that can take up to a day to complete. These processes won't run again until a new crawl of myrient has been performed and the file count has increased.
|
||||||
|
|
||||||
|
# AI Chat Assistant
|
||||||
|
The search engine includes an optional AI chat assistant that can help users find games and answer questions. The AI assistant appears as a floating button in the bottom-right corner of all pages. You will have to fill API configuration in `.env` or `docker-compose.yml` to enable the AI chat assistant.
|
||||||
|
|
||||||
|
The AI chat is OpenAI compatible, so you can use any compatible providers, like ChatGPT, Gemini, Ollama etc. We recommend using [Groq](https://groq.com) with `openai/gpt-oss-120b`, as it has excellent performance, great reasoning and generous limits for free usage.
|
||||||
|
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
You know the usual fluff.
|
You know the usual fluff.
|
||||||
Is there a missing category or string association? `lib/categories.json` and any of the files under `lib/json/relatedkeywords` can both updated to include these. If you do update/improve these, please put in a pull request so that it can be added to the public hosted server, as well.
|
Is there a missing category or string association? `lib/categories.json` and any of the files under `lib/json/relatedkeywords` can both updated to include these. If you do update/improve these, please put in a pull request so that it can be added to the public hosted server, as well.
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "العودة إلى الصفحة الرئيسية",
|
"back_home": "العودة إلى الصفحة الرئيسية",
|
||||||
"go_back": "رجوع"
|
"go_back": "رجوع"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "مساعد الذكاء الاصطناعي",
|
||||||
|
"button_tooltip": "مساعد الذكاء الاصطناعي",
|
||||||
|
"welcome": {
|
||||||
|
"title": "مساعد الذكاء الاصطناعي",
|
||||||
|
"description": "يمكنني مساعدتك في العثور على ألعاب الريترو، وتقديم توصيات الألعاب، والإجابة على الأسئلة حول استخدام محرك البحث."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "ما هي الألعاب التي توصي بها لجهاز Nintendo 64؟",
|
||||||
|
"search_tips": "كيف أبحث عن مناطق معينة؟",
|
||||||
|
"gaming_history": "أخبرني عن أجهزة الألعاب الريترو"
|
||||||
|
},
|
||||||
|
"input_placeholder": "اسألني عن أي شيء...",
|
||||||
|
"typing_indicator": "الذكاء الاصطناعي يفكر",
|
||||||
|
"error": {
|
||||||
|
"network": "تعذر الاتصال بخدمة الذكاء الاصطناعي. يرجى التحقق من اتصال الإنترنت والمحاولة مرة أخرى.",
|
||||||
|
"generic": "آسف، أواجه مشاكل في الاتصال الآن. يرجى المحاولة مرة أخرى لاحقاً."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "হোম পেজে ফিরে যান",
|
"back_home": "হোম পেজে ফিরে যান",
|
||||||
"go_back": "পেছনে যান"
|
"go_back": "পেছনে যান"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "এআই সহায়ক",
|
||||||
|
"button_tooltip": "এআই সহায়ক",
|
||||||
|
"welcome": {
|
||||||
|
"title": "এআই সহায়ক",
|
||||||
|
"description": "আমি আপনাকে রেট্রো গেম খুঁজতে, গেমের সুপারিশ দিতে এবং সার্চ ইঞ্জিন ব্যবহার সম্পর্কে প্রশ্নের উত্তর দিতে সাহায্য করতে পারি।"
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Nintendo 64-এর জন্য আপনি কোন গেমগুলি সুপারিশ করেন?",
|
||||||
|
"search_tips": "আমি কীভাবে নির্দিষ্ট অঞ্চলের জন্য সার্চ করব?",
|
||||||
|
"gaming_history": "রেট্রো গেমিং কনসোল সম্পর্কে আমাকে বলুন"
|
||||||
|
},
|
||||||
|
"input_placeholder": "আমাকে যেকোনো কিছু জিজ্ঞাসা করুন...",
|
||||||
|
"typing_indicator": "এআই চিন্তা করছে",
|
||||||
|
"error": {
|
||||||
|
"network": "এআই সেবার সাথে সংযোগ করতে পারছি না। অনুগ্রহ করে আপনার ইন্টারনেট সংযোগ পরীক্ষা করুন এবং আবার চেষ্টা করুন।",
|
||||||
|
"generic": "দুঃখিত, এখন আমার সংযোগে সমস্যা হচ্ছে। অনুগ্রহ করে পরে আবার চেষ্টা করুন।"
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
@@ -169,7 +188,7 @@
|
|||||||
"tr": "Türkçe",
|
"tr": "Türkçe",
|
||||||
"it": "Italiano",
|
"it": "Italiano",
|
||||||
"romaji": "Romaji",
|
"romaji": "Romaji",
|
||||||
"hi": "हिन्दी",
|
"hi": "हिন्दी",
|
||||||
"ar": "العربية",
|
"ar": "العربية",
|
||||||
"bn": "বাংলা",
|
"bn": "বাংলা",
|
||||||
"ru": "Русский"
|
"ru": "Русский"
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Zurück zur Startseite",
|
"back_home": "Zurück zur Startseite",
|
||||||
"go_back": "Zurück"
|
"go_back": "Zurück"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "KI-Assistent",
|
||||||
|
"button_tooltip": "KI-Assistent",
|
||||||
|
"welcome": {
|
||||||
|
"title": "KI-Assistent",
|
||||||
|
"description": "Ich kann Ihnen helfen, Retro-Spiele zu finden, Spiel-Empfehlungen zu geben und Fragen zur Nutzung der Suchmaschine zu beantworten."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Welche Spiele empfehlen Sie für Nintendo 64?",
|
||||||
|
"search_tips": "Wie suche ich nach bestimmten Regionen?",
|
||||||
|
"gaming_history": "Erzählen Sie mir von Retro-Spielkonsolen"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Fragen Sie mich etwas...",
|
||||||
|
"typing_indicator": "KI denkt nach",
|
||||||
|
"error": {
|
||||||
|
"network": "Verbindung zum KI-Service nicht möglich. Überprüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.",
|
||||||
|
"generic": "Entschuldigung, ich habe gerade Verbindungsprobleme. Bitte versuchen Sie es später erneut."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Back to Home",
|
"back_home": "Back to Home",
|
||||||
"go_back": "Go Back"
|
"go_back": "Go Back"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "AI Assistant",
|
||||||
|
"button_tooltip": "AI Assistant",
|
||||||
|
"welcome": {
|
||||||
|
"title": "AI Assistant",
|
||||||
|
"description": "I can help you find retro games, provide gaming recommendations, and answer questions about using the search engine."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "What games do you recommend for Nintendo 64?",
|
||||||
|
"search_tips": "How do I search for specific regions?",
|
||||||
|
"gaming_history": "Tell me about retro gaming consoles"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Ask me anything...",
|
||||||
|
"typing_indicator": "AI is thinking",
|
||||||
|
"error": {
|
||||||
|
"network": "Unable to connect to the AI service. Please check your internet connection and try again.",
|
||||||
|
"generic": "Sorry, I'm having trouble connecting right now. Please try again later."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Volver al Inicio",
|
"back_home": "Volver al Inicio",
|
||||||
"go_back": "Volver atrás"
|
"go_back": "Volver atrás"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "Asistente de IA",
|
||||||
|
"button_tooltip": "Asistente de IA",
|
||||||
|
"welcome": {
|
||||||
|
"title": "Asistente de IA",
|
||||||
|
"description": "Puedo ayudarte a encontrar juegos retro, proporcionar recomendaciones de juegos y responder preguntas sobre el uso del motor de búsqueda."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "¿Qué juegos recomiendas para Nintendo 64?",
|
||||||
|
"search_tips": "¿Cómo busco regiones específicas?",
|
||||||
|
"gaming_history": "Cuéntame sobre consolas de videojuegos retro"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Pregúntame cualquier cosa...",
|
||||||
|
"typing_indicator": "La IA está pensando",
|
||||||
|
"error": {
|
||||||
|
"network": "No se puede conectar con el servicio de IA. Revisa tu conexión a internet e inténtalo de nuevo.",
|
||||||
|
"generic": "Lo siento, tengo problemas para conectarme ahora mismo. Inténtalo más tarde."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Retour à l'accueil",
|
"back_home": "Retour à l'accueil",
|
||||||
"go_back": "Retour"
|
"go_back": "Retour"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "Assistant IA",
|
||||||
|
"button_tooltip": "Assistant IA",
|
||||||
|
"welcome": {
|
||||||
|
"title": "Assistant IA",
|
||||||
|
"description": "Je peux vous aider à trouver des jeux rétro, fournir des recommandations de jeux et répondre aux questions sur l'utilisation du moteur de recherche."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Quels jeux recommandez-vous pour Nintendo 64 ?",
|
||||||
|
"search_tips": "Comment rechercher des régions spécifiques ?",
|
||||||
|
"gaming_history": "Parlez-moi des consoles de jeux rétro"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Demandez-moi n'importe quoi...",
|
||||||
|
"typing_indicator": "L'IA réfléchit",
|
||||||
|
"error": {
|
||||||
|
"network": "Impossible de se connecter au service IA. Vérifiez votre connexion internet et réessayez.",
|
||||||
|
"generic": "Désolé, j'ai du mal à me connecter en ce moment. Veuillez réessayer plus tard."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
@@ -169,7 +188,7 @@
|
|||||||
"tr": "Türkçe",
|
"tr": "Türkçe",
|
||||||
"it": "Italiano",
|
"it": "Italiano",
|
||||||
"romaji": "Romaji",
|
"romaji": "Romaji",
|
||||||
"hi": "हिन्दी",
|
"hi": "हiन्दी",
|
||||||
"ar": "العربية",
|
"ar": "العربية",
|
||||||
"bn": "বাংলা",
|
"bn": "বাংলা",
|
||||||
"ru": "Русский"
|
"ru": "Русский"
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "होम पर वापस जाएं",
|
"back_home": "होम पर वापस जाएं",
|
||||||
"go_back": "वापस जाएं"
|
"go_back": "वापस जाएं"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "एआई सहायक",
|
||||||
|
"button_tooltip": "एआई सहायक",
|
||||||
|
"welcome": {
|
||||||
|
"title": "एआई सहायक",
|
||||||
|
"description": "मैं आपको रेट्रो गेम्स खोजने, गेम सुझाव देने और सर्च इंजन के उपयोग के बारे में प्रश्नों के उत्तर देने में मदद कर सकता हूं।"
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Nintendo 64 के लिए आप कौन से गेम्स सुझाते हैं?",
|
||||||
|
"search_tips": "मैं विशिष्ट क्षेत्रों की खोज कैसे करूं?",
|
||||||
|
"gaming_history": "रेट्रो गेमिंग कंसोल के बारे में बताइए"
|
||||||
|
},
|
||||||
|
"input_placeholder": "मुझसे कुछ भी पूछें...",
|
||||||
|
"typing_indicator": "एआई सोच रहा है",
|
||||||
|
"error": {
|
||||||
|
"network": "एआई सेवा से कनेक्ट नहीं हो सकता। कृपया अपना इंटरनेट कनेक्शन जांचें और फिर कोशिश करें।",
|
||||||
|
"generic": "खुशी है, मुझे अभी कनेक्शन में समस्या हो रही है। कृपया बाद में कोशिश करें।"
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Torna alla Home",
|
"back_home": "Torna alla Home",
|
||||||
"go_back": "Indietro"
|
"go_back": "Indietro"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "Assistente AI",
|
||||||
|
"button_tooltip": "Assistente AI",
|
||||||
|
"welcome": {
|
||||||
|
"title": "Assistente AI",
|
||||||
|
"description": "Posso aiutarti a trovare giochi retrò, fornire raccomandazioni sui giochi e rispondere a domande sull'utilizzo del motore di ricerca."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Che giochi consigli per Nintendo 64?",
|
||||||
|
"search_tips": "Come cerco regioni specifiche?",
|
||||||
|
"gaming_history": "Dimmi delle console retro"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Chiedimi qualsiasi cosa...",
|
||||||
|
"typing_indicator": "L'AI sta pensando",
|
||||||
|
"error": {
|
||||||
|
"network": "Impossibile connettersi al servizio AI. Controlla la tua connessione internet e riprova.",
|
||||||
|
"generic": "Mi dispiace, sto avendo problemi di connessione ora. Riprova più tardi."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
@@ -169,7 +188,7 @@
|
|||||||
"tr": "Türkçe",
|
"tr": "Türkçe",
|
||||||
"it": "Italiano",
|
"it": "Italiano",
|
||||||
"romaji": "Romaji",
|
"romaji": "Romaji",
|
||||||
"hi": "हिन्दी",
|
"hi": "हिन्दŀ",
|
||||||
"ar": "العربية",
|
"ar": "العربية",
|
||||||
"bn": "বাংলা",
|
"bn": "বাংলা",
|
||||||
"ru": "Русский"
|
"ru": "Русский"
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "ホームに戻る",
|
"back_home": "ホームに戻る",
|
||||||
"go_back": "戻る"
|
"go_back": "戻る"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "AIアシスタント",
|
||||||
|
"button_tooltip": "AIアシスタント",
|
||||||
|
"welcome": {
|
||||||
|
"title": "AIアシスタント",
|
||||||
|
"description": "レトロゲームの検索、ゲームの推薦、検索エンジンの使用方法に関する質問にお答えします。"
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Nintendo 64におすすめのゲームはありますか?",
|
||||||
|
"search_tips": "特定の地域で検索するにはどうすればいいですか?",
|
||||||
|
"gaming_history": "レトロゲーム機について教えてください"
|
||||||
|
},
|
||||||
|
"input_placeholder": "何でもお聞きください...",
|
||||||
|
"typing_indicator": "AIが考えています",
|
||||||
|
"error": {
|
||||||
|
"network": "AIサービスに接続できません。インターネット接続を確認して、もう一度お試しください。",
|
||||||
|
"generic": "申し訳ございませんが、現在接続に問題があります。後ほど再度お試しください。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "홈으로 돌아가기",
|
"back_home": "홈으로 돌아가기",
|
||||||
"go_back": "뒤로 가기"
|
"go_back": "뒤로 가기"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "AI 어시스턴트",
|
||||||
|
"button_tooltip": "AI 어시스턴트",
|
||||||
|
"welcome": {
|
||||||
|
"title": "AI 어시스턴트",
|
||||||
|
"description": "레트로 게임 찾기, 게임 추천 제공, 검색 엔진 사용에 대한 질문 답변을 도와드릴 수 있습니다."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Nintendo 64에 어떤 게임을 추천하시나요?",
|
||||||
|
"search_tips": "특정 지역을 어떻게 검색하나요?",
|
||||||
|
"gaming_history": "레트로 게임 콘솔에 대해 알려주세요"
|
||||||
|
},
|
||||||
|
"input_placeholder": "무엇이든 물어보세요...",
|
||||||
|
"typing_indicator": "AI가 생각 중입니다",
|
||||||
|
"error": {
|
||||||
|
"network": "AI 서비스에 연결할 수 없습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.",
|
||||||
|
"generic": "죄송합니다. 지금 연결에 문제가 있습니다. 나중에 다시 시도해 주세요."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Powrót do strony głównej",
|
"back_home": "Powrót do strony głównej",
|
||||||
"go_back": "Wróć"
|
"go_back": "Wróć"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "Asystent AI",
|
||||||
|
"button_tooltip": "Asystent AI",
|
||||||
|
"welcome": {
|
||||||
|
"title": "Asystent AI",
|
||||||
|
"description": "Mogę pomóc znaleźć retro gry, udzielić rekomendacji gier i odpowiedzieć na pytania dotyczące używania wyszukiwarki."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Jakie gry polecasz na Nintendo 64?",
|
||||||
|
"search_tips": "Jak szukać określonych regionów?",
|
||||||
|
"gaming_history": "Opowiedz mi o retro konsolach do gier"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Zapytaj mnie o cokolwiek...",
|
||||||
|
"typing_indicator": "AI myśli",
|
||||||
|
"error": {
|
||||||
|
"network": "Nie można połączyć się z usługą AI. Sprawdź połączenie internetowe i spróbuj ponownie.",
|
||||||
|
"generic": "Przepraszam, mam teraz problemy z połączeniem. Spróbuj ponownie później."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
@@ -169,7 +188,7 @@
|
|||||||
"tr": "Türkçe",
|
"tr": "Türkçe",
|
||||||
"it": "Italiano",
|
"it": "Italiano",
|
||||||
"romaji": "Romaji",
|
"romaji": "Romaji",
|
||||||
"hi": "हिन्दी",
|
"hi": "हिन्दŀ",
|
||||||
"ar": "العربية",
|
"ar": "العربية",
|
||||||
"bn": "বাংলা",
|
"bn": "বাংলা",
|
||||||
"ru": "Русский"
|
"ru": "Русский"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
"title": "Configurações",
|
"title": "Configurações",
|
||||||
"search_columns": {
|
"search_columns": {
|
||||||
"title": "Colunas de Busca",
|
"title": "Colunas de Busca",
|
||||||
"tooltip": "Seleciona em quais colunas o buscador vai pesquisar."
|
"tooltip": "Seleciona em quais colunas o mecanismo de busca vai pesquisar."
|
||||||
},
|
},
|
||||||
"score_multiplier": {
|
"score_multiplier": {
|
||||||
"title": "Multiplicador de Relevância",
|
"title": "Multiplicador de Relevância",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"title": "Opções avançadas",
|
"title": "Opções avançadas",
|
||||||
"fuzzy": {
|
"fuzzy": {
|
||||||
"label": "Busca aproximada",
|
"label": "Busca aproximada",
|
||||||
"tooltip": "Valor entre 0,00 e 1,00 que determina o quão similar uma palavra precisa ser para ser considerada uma correspondência (distância Levenshtein). Valor mais alto permite correspondências menos exatas. Valor 0 desativa esta função."
|
"tooltip": "Valor entre 0,00 e 1,00 que determina o quão similar uma palavra precisa ser para ser considerada uma correspondência (distância Levenshtein). Valor mais alto permite correspondências menos exatas. Valor 0 desabilita esta função."
|
||||||
},
|
},
|
||||||
"prefix": {
|
"prefix": {
|
||||||
"label": "Permitir Prefixos",
|
"label": "Permitir Prefixos",
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
},
|
},
|
||||||
"match_all": {
|
"match_all": {
|
||||||
"label": "Corresponder Todas as Palavras",
|
"label": "Corresponder Todas as Palavras",
|
||||||
"tooltip": "Exige que todas as palavras da busca sejam encontradas nos resultados."
|
"tooltip": "Requer que todas as palavras da busca sejam encontradas nos resultados."
|
||||||
},
|
},
|
||||||
"hide_non_game": {
|
"hide_non_game": {
|
||||||
"label": "Ocultar Conteúdo Não-Jogo",
|
"label": "Ocultar Conteúdo Não-Jogo",
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
"see_about": "Veja a página {{link}} para mais informações.",
|
"see_about": "Veja a página {{link}} para mais informações.",
|
||||||
"no_data": "Não há dados de emulador disponíveis.",
|
"no_data": "Não há dados de emulador disponíveis.",
|
||||||
"https": "Conexão insegura: Alguns emuladores precisam de HTTPS para funcionar corretamente. Esta página não está configurada corretamente.",
|
"https": "Conexão insegura: Alguns emuladores precisam de HTTPS para funcionar corretamente. Esta página não está configurada corretamente.",
|
||||||
"not_available_tooltip": "A emulação web não está disponível para este título pois não é um jogo ou a plataforma não é suportada."
|
"not_available_tooltip": "A emulação web não está disponível para este título porque não é um jogo ou a plataforma não é suportada."
|
||||||
},
|
},
|
||||||
"console": {
|
"console": {
|
||||||
"about": "Este é um emulador online que executa jogos diretamente do arquivo público do Myrient.",
|
"about": "Este é um emulador online que executa jogos diretamente do arquivo público do Myrient.",
|
||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Voltar para a Página Inicial",
|
"back_home": "Voltar para a Página Inicial",
|
||||||
"go_back": "Voltar"
|
"go_back": "Voltar"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "Assistente IA",
|
||||||
|
"button_tooltip": "Assistente IA",
|
||||||
|
"welcome": {
|
||||||
|
"title": "Assistente IA",
|
||||||
|
"description": "Posso ajudar a encontrar jogos retrô, fornecer recomendações de jogos e responder perguntas sobre o uso da busca."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Que jogos recomenda para Nintendo 64?",
|
||||||
|
"search_tips": "Como pesquisar por regiões específicas?",
|
||||||
|
"gaming_history": "Conte-me sobre consoles de videogames antigos"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Pergunte-me qualquer coisa...",
|
||||||
|
"typing_indicator": "A IA está pensando",
|
||||||
|
"error": {
|
||||||
|
"network": "Não foi possível conectar ao serviço de IA. Tente novamente mais tarde.",
|
||||||
|
"generic": "Desculpe, estou com problemas de conexão agora. Tente novamente mais tarde."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Hōmu ni modoru",
|
"back_home": "Hōmu ni modoru",
|
||||||
"go_back": "Modoru"
|
"go_back": "Modoru"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "AI Ashisutanto",
|
||||||
|
"button_tooltip": "AI Ashisutanto",
|
||||||
|
"welcome": {
|
||||||
|
"title": "AI Ashisutanto",
|
||||||
|
"description": "Retoro geemu no kensaku, geemu no suisen, kensaku enjin no shiyouhouhou ni kansuru shitsumon ni otae dekimasu."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Nintendo 64 ni osusume no geemu wa arimasu ka?",
|
||||||
|
"search_tips": "Tokutei no chiiki de kensaku suru ni wa dou sureba ii desu ka?",
|
||||||
|
"gaming_history": "Retoro geemu ki ni tsuite oshiete kudasai"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Nandemo okiki kudasai...",
|
||||||
|
"typing_indicator": "AI ga kangaete imasu",
|
||||||
|
"error": {
|
||||||
|
"network": "AI saabisu ni setsuzoku dekimasen. Intaanetto setsuzoku wo kakunin shite, mou ichido otameshi kudasai.",
|
||||||
|
"generic": "Moushiwake gozaimasen ga, genzai setsuzoku ni mondai ga arimasu. Nochihodo saido otameshi kudasai."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -155,6 +155,25 @@
|
|||||||
"back_home": "Вернуться на главную",
|
"back_home": "Вернуться на главную",
|
||||||
"go_back": "Назад"
|
"go_back": "Назад"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "ИИ Помощник",
|
||||||
|
"button_tooltip": "ИИ Помощник",
|
||||||
|
"welcome": {
|
||||||
|
"title": "ИИ Помощник",
|
||||||
|
"description": "Я могу помочь найти ретро-игры, предоставить игровые рекомендации и ответить на вопросы об использовании поисковой системы."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Какие игры вы рекомендуете для Nintendo 64?",
|
||||||
|
"search_tips": "Как искать определённые регионы?",
|
||||||
|
"gaming_history": "Расскажите о ретро игровых консолях"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Спросите меня о чём угодно...",
|
||||||
|
"typing_indicator": "ИИ думает",
|
||||||
|
"error": {
|
||||||
|
"network": "Не удаётся подключиться к службе ИИ. Проверьте подключение к интернету и попробуйте снова.",
|
||||||
|
"generic": "Извините, у меня проблемы с подключением прямо сейчас. Попробуйте позже."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "Ana Sayfaya Dön",
|
"back_home": "Ana Sayfaya Dön",
|
||||||
"go_back": "Geri Dön"
|
"go_back": "Geri Dön"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "AI Asistanı",
|
||||||
|
"button_tooltip": "AI Asistanı",
|
||||||
|
"welcome": {
|
||||||
|
"title": "AI Asistanı",
|
||||||
|
"description": "Retro oyunlar bulmanıza, oyun önerileri sunmama ve arama motoru kullanımıyla ilgili sorularınızı yanıtlamama yardımcı olabilirim."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "Nintendo 64 için hangi oyunları önerirsiniz?",
|
||||||
|
"search_tips": "Belirli bölgeleri nasıl ararım?",
|
||||||
|
"gaming_history": "Retro oyun konsolları hakkında bilgi verin"
|
||||||
|
},
|
||||||
|
"input_placeholder": "Bana her şeyi sorabilirsiniz...",
|
||||||
|
"typing_indicator": "AI düşünüyor",
|
||||||
|
"error": {
|
||||||
|
"network": "AI hizmetine bağlanılamıyor. İnternet bağlantınızı kontrol edin ve tekrar deneyin.",
|
||||||
|
"generic": "Üzgünüm, şu anda bağlantı sorunum var. Lütfen daha sonra tekrar deneyin."
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
@@ -156,6 +156,25 @@
|
|||||||
"back_home": "返回首页",
|
"back_home": "返回首页",
|
||||||
"go_back": "返回"
|
"go_back": "返回"
|
||||||
},
|
},
|
||||||
|
"ai_chat": {
|
||||||
|
"title": "AI助手",
|
||||||
|
"button_tooltip": "AI助手",
|
||||||
|
"welcome": {
|
||||||
|
"title": "AI助手",
|
||||||
|
"description": "我可以帮助您找到复古游戏,提供游戏推荐,并回答有关使用搜索引擎的问题。"
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"game_recommendations": "您推荐Nintendo 64有什么游戏?",
|
||||||
|
"search_tips": "如何搜索特定地区?",
|
||||||
|
"gaming_history": "告诉我关于复古游戏机的信息"
|
||||||
|
},
|
||||||
|
"input_placeholder": "问我任何问题...",
|
||||||
|
"typing_indicator": "AI正在思考",
|
||||||
|
"error": {
|
||||||
|
"network": "无法连接到AI服务。请检查您的互联网连接并重试。",
|
||||||
|
"generic": "抱歉,我现在连接有问题。请稍后再试。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"en": "English",
|
"en": "English",
|
||||||
"es": "Español",
|
"es": "Español",
|
||||||
|
|||||||
104
server.js
104
server.js
@@ -532,6 +532,110 @@ app.get("/api/emulators", function (req, res) {
|
|||||||
res.json(emulatorsData);
|
res.json(emulatorsData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post("/api/ai-chat", async function (req, res) {
|
||||||
|
try {
|
||||||
|
const { message } = req.body;
|
||||||
|
|
||||||
|
if (!message || typeof message !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Message is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if AI is enabled and configured
|
||||||
|
const aiEnabled = process.env.AI_ENABLED === 'true';
|
||||||
|
const apiKey = process.env.AI_API_KEY;
|
||||||
|
const apiUrl = process.env.AI_API_URL || 'https://api.openai.com/v1/chat/completions';
|
||||||
|
const model = process.env.AI_MODEL || 'gpt-3.5-turbo';
|
||||||
|
|
||||||
|
if (!aiEnabled) {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: 'AI chat is currently disabled. Please contact the administrator.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: 'AI service is not configured. Please contact the administrator.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create system prompt with context about Myrient
|
||||||
|
const systemPrompt = `You are a helpful AI assistant for the Myrient Search Engine, a website that helps users find and search through retro games and ROMs.
|
||||||
|
|
||||||
|
About Myrient:
|
||||||
|
- Myrient is a preservation project that offers a comprehensive collection of retro games
|
||||||
|
- Users can search for games by filename, category, type, and region
|
||||||
|
- The site includes an emulator feature for playing games directly in the browser
|
||||||
|
- The search engine indexes thousands of games from various gaming systems and regions
|
||||||
|
|
||||||
|
Your role:
|
||||||
|
- Help users find games they're looking for
|
||||||
|
- Provide information about gaming history, consoles, and game recommendations
|
||||||
|
- Answer questions about how to use the search features
|
||||||
|
- Be knowledgeable about retro gaming but stay focused on being helpful
|
||||||
|
- Keep responses concise and friendly
|
||||||
|
- If users ask about downloading or legal issues, remind them that Myrient focuses on preservation
|
||||||
|
|
||||||
|
Current search context: The user is on a retro gaming search website and may be looking for specific games or gaming information.`;
|
||||||
|
|
||||||
|
const aiResponse = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'User-Agent': 'Myrient-Search-Engine/1.0'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: model,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: systemPrompt },
|
||||||
|
{ role: 'user', content: message }
|
||||||
|
],
|
||||||
|
max_tokens: 500,
|
||||||
|
temperature: 0.7,
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!aiResponse.ok) {
|
||||||
|
const errorData = await aiResponse.json().catch(() => ({}));
|
||||||
|
console.error('AI API Error:', aiResponse.status, errorData);
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (aiResponse.status === 401) {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: 'AI service authentication failed. Please contact the administrator.'
|
||||||
|
});
|
||||||
|
} else if (aiResponse.status === 429) {
|
||||||
|
return res.status(429).json({
|
||||||
|
error: 'AI service is currently busy. Please try again in a moment.'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: 'AI service is temporarily unavailable. Please try again later.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const aiData = await aiResponse.json();
|
||||||
|
|
||||||
|
if (!aiData.choices || aiData.choices.length === 0) {
|
||||||
|
return res.status(503).json({
|
||||||
|
error: 'AI service returned an unexpected response.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = aiData.choices[0].message.content.trim();
|
||||||
|
|
||||||
|
res.json({ response });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI Chat Error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'An unexpected error occurred. Please try again later.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get("/proxy-image", async function (req, res, next) {
|
app.get("/proxy-image", async function (req, res, next) {
|
||||||
const imageUrl = req.query.url;
|
const imageUrl = req.query.url;
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,57 @@
|
|||||||
<%- include('../partials/footer'); %>
|
<%- include('../partials/footer'); %>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<!-- AI Chat Button and Modal -->
|
||||||
|
<button class="ai-chat-button" id="aiChatButton" title="<%= __('ai_chat.button_tooltip') %>">
|
||||||
|
<i class="fas fa-robot"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="ai-chat-modal" id="aiChatModal">
|
||||||
|
<div class="ai-chat-header">
|
||||||
|
<h5 class="ai-chat-title"><%= __('ai_chat.title') %></h5>
|
||||||
|
<button class="ai-chat-close" id="aiChatClose">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="ai-chat-messages" id="aiChatMessages">
|
||||||
|
<div class="ai-chat-empty-state" id="aiChatEmptyState">
|
||||||
|
<div class="ai-chat-welcome">
|
||||||
|
<i class="fas fa-robot ai-chat-welcome-icon"></i>
|
||||||
|
<h6 class="ai-chat-welcome-title"><%= __('ai_chat.welcome.title') %></h6>
|
||||||
|
<p class="ai-chat-welcome-text">
|
||||||
|
<%= __('ai_chat.welcome.description') %>
|
||||||
|
</p>
|
||||||
|
<div class="ai-chat-suggestions">
|
||||||
|
<button class="ai-suggestion-pill" data-suggestion="<%= __('ai_chat.suggestions.game_recommendations') %>">🎮 <%= __('ai_chat.suggestions.game_recommendations') %></button>
|
||||||
|
<button class="ai-suggestion-pill" data-suggestion="<%= __('ai_chat.suggestions.search_tips') %>">🔍 <%= __('ai_chat.suggestions.search_tips') %></button>
|
||||||
|
<button class="ai-suggestion-pill" data-suggestion="<%= __('ai_chat.suggestions.gaming_history') %>">🕹️ <%= __('ai_chat.suggestions.gaming_history') %></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ai-chat-input-container">
|
||||||
|
<textarea
|
||||||
|
class="ai-chat-input"
|
||||||
|
id="aiChatInput"
|
||||||
|
placeholder="<%= __('ai_chat.input_placeholder') %>"
|
||||||
|
rows="1"
|
||||||
|
maxlength="1000"
|
||||||
|
></textarea>
|
||||||
|
<button class="ai-chat-send" id="aiChatSend" title="<%= __('search.button') %>">
|
||||||
|
➤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
|
// Pass AI Chat translations to the client
|
||||||
|
window.aiChatTranslations = {
|
||||||
|
typing_indicator: '<%= __('ai_chat.typing_indicator') %>',
|
||||||
|
error: {
|
||||||
|
network: '<%= __('ai_chat.error.network') %>',
|
||||||
|
generic: '<%= __('ai_chat.error.generic') %>'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if(window.location.pathname != '/settings'){ //don't load on the settings page
|
if(window.location.pathname != '/settings'){ //don't load on the settings page
|
||||||
settingStore = localStorage.getItem('settings')
|
settingStore = localStorage.getItem('settings')
|
||||||
if(typeof settingStore == 'string' && window.location.href){
|
if(typeof settingStore == 'string' && window.location.href){
|
||||||
@@ -39,7 +88,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js'></script>
|
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js'></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
|
||||||
|
<link rel="stylesheet" href="/public/css/ai.css">
|
||||||
|
<script src="/public/js/ai-chat.js"></script>
|
||||||
</html>
|
</html>
|
||||||
560
views/public/css/ai.css
Normal file
560
views/public/css/ai.css
Normal file
@@ -0,0 +1,560 @@
|
|||||||
|
/* AI Chat Styles */
|
||||||
|
.ai-chat-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: linear-gradient(135deg, #ffbd33, #f0a400);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 189, 51, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #1c2020;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-button:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 189, 51, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-modal {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 90px;
|
||||||
|
right: 20px;
|
||||||
|
width: 350px;
|
||||||
|
height: 500px;
|
||||||
|
background: #262c2c;
|
||||||
|
border: 1px solid rgba(255, 189, 51, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 999;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-modal.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-header {
|
||||||
|
background: #343a40;
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid rgba(255, 189, 51, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-title {
|
||||||
|
color: #ffbd33;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-close:hover {
|
||||||
|
color: #ffbd33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 15px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #ffbd33 #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-messages::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-messages::-webkit-scrollbar-track {
|
||||||
|
background: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-messages::-webkit-scrollbar-thumb {
|
||||||
|
background: #ffbd33;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-message {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-message.user {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-message.ai {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome {
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #ffbd33;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome-title {
|
||||||
|
color: #ffbd33;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome-text {
|
||||||
|
color: #b0b8c1;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-suggestions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-pill {
|
||||||
|
background: rgba(255, 189, 51, 0.1);
|
||||||
|
border: 1px solid rgba(255, 189, 51, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #ffbd33;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-pill:hover {
|
||||||
|
background: rgba(255, 189, 51, 0.15);
|
||||||
|
border-color: rgba(255, 189, 51, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-pill:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble {
|
||||||
|
max-width: 85%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
position: relative;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user {
|
||||||
|
background: #ffbd33;
|
||||||
|
color: #1c2020;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.ai {
|
||||||
|
background: #484f60;
|
||||||
|
color: #ffffff;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown Styling for AI Messages */
|
||||||
|
.ai-message-bubble h1,
|
||||||
|
.ai-message-bubble h2,
|
||||||
|
.ai-message-bubble h3,
|
||||||
|
.ai-message-bubble h4,
|
||||||
|
.ai-message-bubble h5,
|
||||||
|
.ai-message-bubble h6 {
|
||||||
|
margin: 0.5em 0 0.3em 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble h1 { font-size: 1.4em; }
|
||||||
|
.ai-message-bubble h2 { font-size: 1.3em; }
|
||||||
|
.ai-message-bubble h3 { font-size: 1.2em; }
|
||||||
|
.ai-message-bubble h4 { font-size: 1.1em; }
|
||||||
|
.ai-message-bubble h5 { font-size: 1em; }
|
||||||
|
.ai-message-bubble h6 { font-size: 0.9em; }
|
||||||
|
|
||||||
|
.ai-message-bubble p {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble code {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
color: #ffbd33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user code {
|
||||||
|
background: rgba(0, 0, 0, 0.15);
|
||||||
|
color: #1c2020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble pre {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
border-left: 3px solid #ffbd33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user pre {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-left-color: #1c2020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble ul,
|
||||||
|
.ai-message-bubble ol {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding-left: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble li {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble a {
|
||||||
|
color: #ffbd33;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: rgba(255, 189, 51, 0.5);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user a {
|
||||||
|
color: #1c2020;
|
||||||
|
text-decoration-color: rgba(28, 32, 32, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble a:hover {
|
||||||
|
text-decoration-color: currentColor;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble blockquote {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-left: 3px solid #ffbd33;
|
||||||
|
background: rgba(255, 189, 51, 0.1);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user blockquote {
|
||||||
|
border-left-color: #1c2020;
|
||||||
|
background: rgba(28, 32, 32, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble th,
|
||||||
|
.ai-message-bubble td {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
padding: 4px 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user th,
|
||||||
|
.ai-message-bubble.user td {
|
||||||
|
border-color: rgba(28, 32, 32, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble th {
|
||||||
|
background: rgba(255, 189, 51, 0.2);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user th {
|
||||||
|
background: rgba(28, 32, 32, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user hr {
|
||||||
|
border-top-color: rgba(28, 32, 32, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble em {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble del {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Task Lists */
|
||||||
|
.ai-message-bubble .task-list-item {
|
||||||
|
list-style: none;
|
||||||
|
margin-left: -1.2em;
|
||||||
|
padding-left: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble .task-list-item input[type="checkbox"] {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced code blocks with syntax highlighting placeholder */
|
||||||
|
.ai-message-bubble pre[class*="language-"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble pre[class*="language-"]::before {
|
||||||
|
content: attr(class);
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 6px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: rgba(255, 189, 51, 0.6);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user pre[class*="language-"]::before {
|
||||||
|
color: rgba(28, 32, 32, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better spacing for nested lists */
|
||||||
|
.ai-message-bubble li ul,
|
||||||
|
.ai-message-bubble li ol {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special styling for inline math (if using MathJax later) */
|
||||||
|
.ai-message-bubble .math-inline {
|
||||||
|
background: rgba(255, 189, 51, 0.1);
|
||||||
|
padding: 1px 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-bubble.user .math-inline {
|
||||||
|
background: rgba(28, 32, 32, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6c757d;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-input-container {
|
||||||
|
padding: 15px;
|
||||||
|
border-top: 1px solid rgba(255, 189, 51, 0.2);
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-input {
|
||||||
|
flex: 1;
|
||||||
|
background: #343a40;
|
||||||
|
border: 1px solid rgba(255, 189, 51, 0.3);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: none;
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 80px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-input:focus {
|
||||||
|
border-color: #ffbd33;
|
||||||
|
box-shadow: 0 0 0 0.1rem rgba(255, 189, 51, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-input::placeholder {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-send {
|
||||||
|
background: #ffbd33;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #1c2020;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-send:hover:not(:disabled) {
|
||||||
|
background: #f0a400;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-send:disabled {
|
||||||
|
background: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-typing-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-typing-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #6c757d;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: typing-pulse 1.4s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-typing-dot:nth-child(1) {
|
||||||
|
animation-delay: -0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-typing-dot:nth-child(2) {
|
||||||
|
animation-delay: -0.16s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing-pulse {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsiveness */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.ai-chat-modal {
|
||||||
|
width: calc(100vw - 40px);
|
||||||
|
height: 60vh;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-button {
|
||||||
|
bottom: 15px;
|
||||||
|
right: 15px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-empty-state {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-chat-welcome-text {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-suggestion-pill {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ a:hover {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
td {
|
td {
|
||||||
webkit-transition: background 300ms ease-in;
|
|
||||||
transition-behavior: normal;
|
transition-behavior: normal;
|
||||||
transition-duration: 300ms;
|
transition-duration: 300ms;
|
||||||
transition-timing-function: ease-in;
|
transition-timing-function: ease-in;
|
||||||
|
|||||||
308
views/public/js/ai-chat.js
Normal file
308
views/public/js/ai-chat.js
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// AI Chat functionality
|
||||||
|
class AIChat {
|
||||||
|
constructor() {
|
||||||
|
this.modal = document.getElementById('aiChatModal');
|
||||||
|
this.button = document.getElementById('aiChatButton');
|
||||||
|
this.closeBtn = document.getElementById('aiChatClose');
|
||||||
|
this.messagesContainer = document.getElementById('aiChatMessages');
|
||||||
|
this.input = document.getElementById('aiChatInput');
|
||||||
|
this.sendBtn = document.getElementById('aiChatSend');
|
||||||
|
this.emptyState = document.getElementById('aiChatEmptyState');
|
||||||
|
|
||||||
|
this.isOpen = false;
|
||||||
|
this.isTyping = false;
|
||||||
|
this.hasMessages = false;
|
||||||
|
|
||||||
|
// Initialize translations
|
||||||
|
this.translations = window.aiChatTranslations || {
|
||||||
|
typing_indicator: 'AI is thinking',
|
||||||
|
error: {
|
||||||
|
network: 'Unable to connect to the AI service. Please check your internet connection and try again.',
|
||||||
|
generic: 'Sorry, I\'m having trouble connecting right now. Please try again later.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.initializeEventListeners();
|
||||||
|
this.adjustInputHeight();
|
||||||
|
this.configureMarkdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
configureMarkdown() {
|
||||||
|
// Configure marked.js options for security and rendering
|
||||||
|
if (typeof marked !== 'undefined') {
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true, // Convert \n to <br>
|
||||||
|
gfm: true, // GitHub Flavored Markdown
|
||||||
|
sanitize: false, // We'll handle sanitization manually if needed
|
||||||
|
smartypants: true, // Use smart quotes
|
||||||
|
xhtml: false,
|
||||||
|
headerIds: false, // Disable header IDs for security
|
||||||
|
mangle: false // Don't mangle email addresses
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom renderer for better styling
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
|
||||||
|
// Custom code rendering
|
||||||
|
renderer.code = function(code, language) {
|
||||||
|
const validLang = language && language.match(/^[a-zA-Z0-9_+-]*$/);
|
||||||
|
const langClass = validLang ? ` class="language-${language}"` : '';
|
||||||
|
const escapedCode = code.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
return `<pre><code${langClass}>${escapedCode}</code></pre>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom link rendering with security
|
||||||
|
renderer.link = function(href, title, text) {
|
||||||
|
const escapedHref = href.replace(/"/g, '"');
|
||||||
|
const titleAttr = title ? ` title="${title.replace(/"/g, '"')}"` : '';
|
||||||
|
return `<a href="${escapedHref}" target="_blank" rel="noopener noreferrer"${titleAttr}>${text}</a>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom list item rendering for task lists
|
||||||
|
renderer.listitem = function(text) {
|
||||||
|
// Check if this is a task list item
|
||||||
|
const taskListMatch = text.match(/^<p>\s*\[([ xX])\]\s*(.*)<\/p>$/);
|
||||||
|
if (taskListMatch) {
|
||||||
|
const checked = taskListMatch[1] !== ' ' ? 'checked' : '';
|
||||||
|
const content = taskListMatch[2];
|
||||||
|
return `<li class="task-list-item"><input type="checkbox" disabled ${checked}> ${content}</li>`;
|
||||||
|
}
|
||||||
|
// Regular task list without <p> wrapper
|
||||||
|
const simpleTaskMatch = text.match(/^\[([ xX])\]\s*(.*)$/);
|
||||||
|
if (simpleTaskMatch) {
|
||||||
|
const checked = simpleTaskMatch[1] !== ' ' ? 'checked' : '';
|
||||||
|
const content = simpleTaskMatch[2];
|
||||||
|
return `<li class="task-list-item"><input type="checkbox" disabled ${checked}> ${content}</li>`;
|
||||||
|
}
|
||||||
|
return `<li>${text}</li>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
marked.use({ renderer });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeEventListeners() {
|
||||||
|
// Toggle modal
|
||||||
|
this.button.addEventListener('click', () => this.toggleModal());
|
||||||
|
this.closeBtn.addEventListener('click', () => this.closeModal());
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
this.sendBtn.addEventListener('click', () => this.sendMessage());
|
||||||
|
|
||||||
|
// Handle input events
|
||||||
|
this.input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.input.addEventListener('input', () => this.adjustInputHeight());
|
||||||
|
|
||||||
|
// Handle suggestion pills
|
||||||
|
this.initializeSuggestionPills();
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!this.modal.contains(e.target) && !this.button.contains(e.target)) {
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleModal() {
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.closeModal();
|
||||||
|
} else {
|
||||||
|
this.openModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal() {
|
||||||
|
this.modal.classList.add('show');
|
||||||
|
this.isOpen = true;
|
||||||
|
// Focus input when modal opens
|
||||||
|
setTimeout(() => this.input.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.modal.classList.remove('show');
|
||||||
|
this.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustInputHeight() {
|
||||||
|
this.input.style.height = 'auto';
|
||||||
|
this.input.style.height = Math.min(this.input.scrollHeight, 80) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSuggestionPills() {
|
||||||
|
const suggestionPills = document.querySelectorAll('.ai-suggestion-pill');
|
||||||
|
suggestionPills.forEach(pill => {
|
||||||
|
pill.addEventListener('click', () => {
|
||||||
|
const suggestion = pill.getAttribute('data-suggestion');
|
||||||
|
if (suggestion) {
|
||||||
|
this.input.value = suggestion;
|
||||||
|
this.adjustInputHeight();
|
||||||
|
this.sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hideEmptyState() {
|
||||||
|
if (this.emptyState) {
|
||||||
|
this.emptyState.style.display = 'none';
|
||||||
|
this.hasMessages = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendMessage() {
|
||||||
|
const message = this.input.value.trim();
|
||||||
|
if (!message || this.isTyping) return;
|
||||||
|
|
||||||
|
// Hide empty state on first message
|
||||||
|
if (!this.hasMessages) {
|
||||||
|
this.hideEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user message
|
||||||
|
this.addMessage(message, 'user');
|
||||||
|
this.input.value = '';
|
||||||
|
this.adjustInputHeight();
|
||||||
|
|
||||||
|
// Disable send button and show typing indicator
|
||||||
|
this.setSending(true);
|
||||||
|
this.showTypingIndicator();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send to backend
|
||||||
|
const response = await fetch('/api/ai-chat', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ message })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Remove typing indicator and add AI response
|
||||||
|
this.hideTypingIndicator();
|
||||||
|
this.addMessage(data.response, 'ai');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI Chat Error:', error);
|
||||||
|
this.hideTypingIndicator();
|
||||||
|
|
||||||
|
let errorMessage = error.message;
|
||||||
|
|
||||||
|
// Handle different types of errors
|
||||||
|
if (error.message.includes('Failed to fetch')) {
|
||||||
|
errorMessage = this.translations.error.network;
|
||||||
|
} else if (!error.message.includes('HTTP error')) {
|
||||||
|
// Use backend error message if available, otherwise use generic message
|
||||||
|
errorMessage = error.message || this.translations.error.generic;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addMessage(errorMessage, 'ai');
|
||||||
|
} finally {
|
||||||
|
this.setSending(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage(content, sender) {
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = `ai-chat-message ${sender}`;
|
||||||
|
|
||||||
|
const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
|
||||||
|
messageDiv.innerHTML = `
|
||||||
|
<div class="ai-message-bubble ${sender}">${this.formatMessage(content)}</div>
|
||||||
|
<small class="ai-message-time">${time}</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.messagesContainer.appendChild(messageDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatMessage(message) {
|
||||||
|
// Use marked.js for full markdown parsing if available
|
||||||
|
if (typeof marked !== 'undefined') {
|
||||||
|
try {
|
||||||
|
return marked.parse(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Markdown parsing error:', error);
|
||||||
|
// Fallback to basic formatting
|
||||||
|
return this.formatMessageBasic(message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to basic formatting if marked.js not available
|
||||||
|
return this.formatMessageBasic(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatMessageBasic(message) {
|
||||||
|
// Basic formatting fallback
|
||||||
|
return message
|
||||||
|
.replace(/\n/g, '<br>')
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||||
|
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||||
|
.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||||||
|
}
|
||||||
|
|
||||||
|
showTypingIndicator() {
|
||||||
|
if (this.isTyping) return;
|
||||||
|
|
||||||
|
this.isTyping = true;
|
||||||
|
const typingDiv = document.createElement('div');
|
||||||
|
typingDiv.className = 'ai-chat-message ai';
|
||||||
|
typingDiv.id = 'typingIndicator';
|
||||||
|
|
||||||
|
typingDiv.innerHTML = `
|
||||||
|
<div class="ai-typing-indicator">
|
||||||
|
<span>${this.translations.typing_indicator}</span>
|
||||||
|
<div class="ai-typing-dots">
|
||||||
|
<div class="ai-typing-dot"></div>
|
||||||
|
<div class="ai-typing-dot"></div>
|
||||||
|
<div class="ai-typing-dot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.messagesContainer.appendChild(typingDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideTypingIndicator() {
|
||||||
|
this.isTyping = false;
|
||||||
|
const typingIndicator = document.getElementById('typingIndicator');
|
||||||
|
if (typingIndicator) {
|
||||||
|
typingIndicator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSending(sending) {
|
||||||
|
this.sendBtn.disabled = sending;
|
||||||
|
this.input.disabled = sending;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize AI Chat when DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Only initialize if AI chat elements exist
|
||||||
|
if (document.getElementById('aiChatModal')) {
|
||||||
|
new AIChat();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user