diff --git a/nextgenie/nextgenie/doctype/genie_chat_message/__init__.py b/nextgenie/nextgenie/doctype/genie_chat_message/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.js b/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.js new file mode 100644 index 0000000..ab042b8 --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Codeventurers and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Genie Chat Message", { +// refresh(frm) { + +// }, +// }); diff --git a/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.json b/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.json new file mode 100644 index 0000000..255d806 --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.json @@ -0,0 +1,75 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-06-03 18:23:00.205412", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "session", + "column_break_acog", + "sender", + "section_break_xxcg", + "message" + ], + "fields": [ + { + "fieldname": "session", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Session", + "options": "Genie Chat Session", + "read_only": 1 + }, + { + "fieldname": "column_break_acog", + "fieldtype": "Column Break" + }, + { + "fieldname": "sender", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Sender", + "options": "\nUser\nGenie", + "read_only": 1 + }, + { + "fieldname": "section_break_xxcg", + "fieldtype": "Section Break" + }, + { + "fieldname": "message", + "fieldtype": "Long Text", + "label": "Message", + "read_only": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-06-03 18:31:59.241780", + "modified_by": "Administrator", + "module": "Nextgenie", + "name": "Genie Chat Message", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.py b/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.py new file mode 100644 index 0000000..1a2a58c --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_message/genie_chat_message.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Codeventurers and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class GenieChatMessage(Document): + pass diff --git a/nextgenie/nextgenie/doctype/genie_chat_message/test_genie_chat_message.py b/nextgenie/nextgenie/doctype/genie_chat_message/test_genie_chat_message.py new file mode 100644 index 0000000..cb361b5 --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_message/test_genie_chat_message.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Codeventurers and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestGenieChatMessage(FrappeTestCase): + pass diff --git a/nextgenie/nextgenie/doctype/genie_chat_session/__init__.py b/nextgenie/nextgenie/doctype/genie_chat_session/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.js b/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.js new file mode 100644 index 0000000..3eff7ce --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Codeventurers and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Genie Chat Session", { +// refresh(frm) { + +// }, +// }); diff --git a/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.json b/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.json new file mode 100644 index 0000000..4aa3632 --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-06-03 18:21:30.016357", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "title", + "column_break_kkld", + "user" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Title", + "read_only": 1 + }, + { + "fieldname": "column_break_kkld", + "fieldtype": "Column Break" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "read_only": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-06-03 18:32:13.887279", + "modified_by": "Administrator", + "module": "Nextgenie", + "name": "Genie Chat Session", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.py b/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.py new file mode 100644 index 0000000..fb04e3f --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_session/genie_chat_session.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Codeventurers and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class GenieChatSession(Document): + pass diff --git a/nextgenie/nextgenie/doctype/genie_chat_session/test_genie_chat_session.py b/nextgenie/nextgenie/doctype/genie_chat_session/test_genie_chat_session.py new file mode 100644 index 0000000..c87279b --- /dev/null +++ b/nextgenie/nextgenie/doctype/genie_chat_session/test_genie_chat_session.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Codeventurers and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestGenieChatSession(FrappeTestCase): + pass diff --git a/nextgenie/nextgenie/page/nextgenie/nextgenie.css b/nextgenie/nextgenie/page/nextgenie/nextgenie.css index 9d78e1d..6731fe9 100644 --- a/nextgenie/nextgenie/page/nextgenie/nextgenie.css +++ b/nextgenie/nextgenie/page/nextgenie/nextgenie.css @@ -1,123 +1,697 @@ -/* Copyright (c) 2025, Codeventurers and contributors -For license information, please see license.txt */ +/* Copyright (c) 2025, Codeventurers and contributors + For license information, please see license.txt */ +@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600&family=DM+Sans:wght@300;400;500&display=swap'); + +:root { + --ng-gold: #c9a84c; + --ng-gold-l: #e8c97a; + --ng-gold-dim: rgba(201, 168, 76, 0.13); + --ng-bg: #0d0f14; + --ng-panel: #13161e; + --ng-card: #1a1e2a; + --ng-input: #1f2333; + --ng-bdr: rgba(255, 255, 255, 0.07); + --ng-bdr-l: rgba(255, 255, 255, 0.12); + --ng-user-bg: #1e3a2f; + --ng-user-br: #2d5c44; + --ng-bot-bg: #1a1e2a; + --ng-bot-br: rgba(201, 168, 76, 0.25); + --ng-tp: #f0ece4; + --ng-ts: #8a8fa8; + --ng-tm: #4a4f63; +} + +/* ── Frappe page wrapper overrides ─────────────── */ +.page-head { + display: none !important; +} + +.page-content { + padding: 0 !important; +} + +.page-body { + padding: 0 !important; +} + +.container.page-body { + max-width: 100% !important; + padding: 0 !important; +} + +/* ── Root container ────────────────────────────── */ .chat-container { display: flex; - height: 80vh; - border: 1px solid #ddd; - border-radius: 12px; + flex-direction: row; + width: 100%; + height: calc(100vh - 56px); + background: var(--ng-bg); + overflow: hidden; + font-family: 'DM Sans', sans-serif; + color: var(--ng-tp); +} + +/* ════════════════════════════════════════ + HISTORY PANEL +════════════════════════════════════════ */ +.chat-history { + width: 220px; + min-width: 220px; + max-width: 220px; + height: 100%; + background: var(--ng-panel); + border-right: 1px solid var(--ng-bdr); + display: flex; + flex-direction: column; overflow: hidden; - font-family: 'Inter', sans-serif; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +.history-head { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + padding: 18px 16px 12px; + border-bottom: 1px solid var(--ng-bdr); + flex-shrink: 0; +} + +.brand-icon { + width: 32px; + height: 32px; + background: var(--ng-gold-dim); + border: 1px solid rgba(201, 168, 76, 0.3); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + color: var(--ng-gold); + flex-shrink: 0; +} - /* Glassmorphism effect */ - background: rgba(255, 255, 255, 0.15); - backdrop-filter: blur(12px); +.brand-name { + font-family: 'Playfair Display', serif; + font-size: 16px; + color: var(--ng-gold-l); + display: block; +} + +.new-chat-btn { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 6px; + margin: 10px 12px 4px; + padding: 8px 12px; + border-radius: 8px; + background: var(--ng-gold-dim); + border: 1px solid rgba(201, 168, 76, 0.25); + color: var(--ng-gold); + font-family: 'DM Sans', sans-serif; + font-size: 12px; + cursor: pointer; + flex-shrink: 0; + transition: background 0.2s; + text-align: center; +} + +.new-chat-btn:hover { + background: rgba(201, 168, 76, 0.22); +} + +.hist-section-label { + display: block; + font-size: 9px; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ng-tm); + padding: 8px 14px 4px; + flex-shrink: 0; +} + +.hist-list { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 4px 8px; + display: flex; + flex-direction: column; + gap: 2px; + min-height: 0; +} + +.hist-list::-webkit-scrollbar { + width: 3px; +} + +.hist-list::-webkit-scrollbar-thumb { + background: var(--ng-bdr-l); + border-radius: 3px; +} + +.hist-item { + display: block; + padding: 9px 10px; + border-radius: 8px; + cursor: pointer; + border: 1px solid transparent; + transition: background 0.2s, border-color 0.2s; + flex-shrink: 0; } -/* Sidebar */ +.hist-item:hover { + background: var(--ng-card); + border-color: var(--ng-bdr); +} + +.hist-item.active { + background: var(--ng-gold-dim); + border-color: rgba(201, 168, 76, 0.2); +} + +.hist-item-title { + display: block; + font-size: 12px; + color: var(--ng-tp); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: 'DM Sans', sans-serif; +} + +.hist-item-date { + display: block; + font-size: 10px; + color: var(--ng-tm); + margin-top: 2px; + font-family: 'DM Sans', sans-serif; +} + +.history-footer { + flex-shrink: 0; + padding: 14px; + border-top: 1px solid var(--ng-bdr); +} + +.history-footer small { + display: block; + font-size: 10px; + color: var(--ng-tm); + line-height: 1.6; + font-family: 'DM Sans', sans-serif; +} + +.ver-badge { + display: inline-block; + margin-top: 6px; + padding: 2px 8px; + background: var(--ng-gold-dim); + border: 1px solid rgba(201, 168, 76, 0.2); + border-radius: 20px; + font-size: 9px; + color: var(--ng-gold); + letter-spacing: .06em; + font-family: 'DM Sans', sans-serif; +} + +/* ════════════════════════════════════════ + CAPABILITIES SIDEBAR +════════════════════════════════════════ */ .chat-sidebar { - width: 25%; - background: linear-gradient(135deg, #4f46e5, #9333ea); - padding: 15px; - border-right: 1px solid rgba(255, 255, 255, 0.1); - color: #ffffff; + width: 190px; + min-width: 190px; + max-width: 190px; + height: 100%; + background: var(--ng-panel); + border-right: 1px solid var(--ng-bdr); + display: flex; + flex-direction: column; + padding: 20px 14px; + gap: 18px; + overflow-y: auto; + overflow-x: hidden; +} + +.chat-sidebar::-webkit-scrollbar { + width: 3px; +} + +.chat-sidebar::-webkit-scrollbar-thumb { + background: var(--ng-bdr-l); + border-radius: 3px; +} + +.sec-title { + display: block; + font-size: 9px; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--ng-tm); + margin-bottom: 8px; + font-family: 'DM Sans', sans-serif; +} + +.feats { + display: flex; + flex-direction: column; + gap: 5px; + list-style: none; + margin: 0; + padding: 0; +} + +.feats li { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + font-size: 11px; + color: var(--ng-ts); + padding: 7px 9px; + border-radius: 7px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid var(--ng-bdr); + cursor: pointer; + transition: background 0.2s, border-color 0.2s, color 0.2s; + font-family: 'DM Sans', sans-serif; + list-style: none; } -.chat-sidebar h3 { - margin-top: 0; - color: #ffffff; +.feats li:hover { + background: var(--ng-gold-dim); + border-color: rgba(201, 168, 76, 0.2); + color: var(--ng-tp); } -/* Main Chat Area */ +.feat-dot { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--ng-gold); + opacity: .5; + flex-shrink: 0; +} + +.feats li:hover .feat-dot { + opacity: 1; +} + +.sidebar-divider { + display: block; + height: 1px; + background: var(--ng-bdr); + flex-shrink: 0; + margin: 0; +} + +.sidebar-privacy { + margin-top: auto; + padding-top: 12px; + border-top: 1px solid var(--ng-bdr); + font-size: 10px; + color: var(--ng-tm); + line-height: 1.7; + font-family: 'DM Sans', sans-serif; + display: block; +} + +/* ════════════════════════════════════════ + CHAT MAIN +════════════════════════════════════════ */ .chat-main { - width: 75%; + flex: 1; + min-width: 0; + height: 100%; display: flex; flex-direction: column; - background: #f8fafc; + background: var(--ng-bg); + position: relative; + overflow: hidden; +} + +.chat-main::before { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(201, 168, 76, 0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(201, 168, 76, 0.025) 1px, transparent 1px); + background-size: 40px 40px; + pointer-events: none; + z-index: 0; +} + +/* Header */ +.chat-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 13px 20px; + border-bottom: 1px solid var(--ng-bdr); + background: rgba(13, 15, 20, 0.95); position: relative; + z-index: 2; + flex-shrink: 0; +} + +.chat-header-left { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +} + +.status-dot { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + background: #3ecf8e; + box-shadow: 0 0 8px rgba(62, 207, 142, .6); + animation: ng-pulse 2s ease-in-out infinite; + flex-shrink: 0; +} + +@keyframes ng-pulse { + + 0%, + 100% { + opacity: 1; + box-shadow: 0 0 8px rgba(62, 207, 142, .6); + } + + 50% { + opacity: .6; + box-shadow: 0 0 4px rgba(62, 207, 142, .3); + } +} + +.chat-header-title { + display: block; + font-family: 'Playfair Display', serif; + font-size: 14px; + color: var(--ng-tp); + font-weight: 600; } -/* Chat Messages */ +.chat-header-sub { + display: block; + font-size: 10px; + color: var(--ng-tm); + margin-top: 1px; + font-family: 'DM Sans', sans-serif; +} + +.chat-header-right { + display: flex; + flex-direction: row; + gap: 6px; +} + +.header-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + background: transparent; + border: 1px solid var(--ng-bdr); + color: var(--ng-tm); + cursor: pointer; + font-size: 12px; + transition: background 0.2s, border-color 0.2s, color 0.2s; +} + +.header-btn:hover { + background: var(--ng-gold-dim); + border-color: rgba(201, 168, 76, .3); + color: var(--ng-gold); +} + +/* Messages area */ .chat-messages { flex: 1; - padding: 15px; + min-height: 0; + padding: 18px 20px; overflow-y: auto; + overflow-x: hidden; display: flex; flex-direction: column; + gap: 14px; + position: relative; + z-index: 1; + scroll-behavior: smooth; } -/* Message bubbles */ -.bot-message, -.user-message { - margin: 8px 0; - padding: 10px 14px; - border-radius: 12px; - max-width: 70%; - font-size: 14px; - word-wrap: break-word; - color: #111827; +.chat-messages::-webkit-scrollbar { + width: 3px; +} + +.chat-messages::-webkit-scrollbar-thumb { + background: var(--ng-bdr-l); + border-radius: 3px; } -/* User → RIGHT */ -.user-message { - background: #bbf7d0; - align-self: flex-end; - border: 1px solid #86efac; +/* Message row */ +.ng-msg-row { + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 8px; + width: 100%; + animation: ng-msg-in .25s ease both; } -/* Bot → LEFT */ -.bot-message { - background: #e0e7ff; - align-self: flex-start; - border: 1px solid #c7d2fe; +.ng-msg-row.ng-user { + flex-direction: row-reverse; } -/* Input section */ -.chat-input { +@keyframes ng-msg-in { + from { + opacity: 0; + transform: translateY(6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Avatar */ +.ng-avatar { display: flex; - padding: 10px; - border-top: 1px solid #d1d5db; - background: #ffffff; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + min-width: 26px; + border-radius: 50%; + font-size: 11px; + border: 1px solid var(--ng-bdr-l); + flex-shrink: 0; +} + +.ng-avatar.ng-av-bot { + background: var(--ng-gold-dim); + border-color: rgba(201, 168, 76, .3); + color: var(--ng-gold); +} + +.ng-avatar.ng-av-user { + background: rgba(62, 207, 142, .1); + border-color: rgba(62, 207, 142, .25); + color: #3ecf8e; +} + +/* Bubble wrapper */ +.ng-bubble-wrap { + display: flex; + flex-direction: column; + max-width: 62%; + min-width: 0; } -.chat-input input { +.ng-user .ng-bubble-wrap { + align-items: flex-end; +} + +/* Bubble */ +.ng-bubble { + display: inline-block; + padding: 9px 13px; + border-radius: 13px; + font-size: 13px; + line-height: 1.6; + font-family: 'DM Sans', sans-serif; + color: var(--ng-tp); + word-break: break-word; + white-space: pre-wrap; + max-width: 100%; +} + +.ng-bubble.ng-bot { + background: var(--ng-bot-bg); + border: 1px solid var(--ng-bot-br); + border-bottom-left-radius: 3px; +} + +.ng-bubble.ng-usr { + background: var(--ng-user-bg); + border: 1px solid var(--ng-user-br); + border-bottom-right-radius: 3px; +} + +.ng-time { + display: block; + font-size: 10px; + color: var(--ng-tm); + margin-top: 3px; + padding: 0 3px; + font-family: 'DM Sans', sans-serif; +} + +.ng-user .ng-time { + text-align: right; +} + +/* Typing indicator */ +.ng-typing { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + padding: 9px 13px; + background: var(--ng-bot-bg); + border: 1px solid var(--ng-bot-br); + border-radius: 13px; + border-bottom-left-radius: 3px; + width: fit-content; +} + +.ng-typing span { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--ng-gold); + animation: ng-tb 1.2s ease-in-out infinite; +} + +.ng-typing span:nth-child(2) { + animation-delay: .2s; +} + +.ng-typing span:nth-child(3) { + animation-delay: .4s; +} + +@keyframes ng-tb { + + 0%, + 60%, + 100% { + transform: translateY(0); + opacity: .4; + } + + 30% { + transform: translateY(-4px); + opacity: 1; + } +} + +/* Input area */ +.chat-input-area { + display: block; + padding: 12px 18px 16px; + border-top: 1px solid var(--ng-bdr); + background: rgba(13, 15, 20, 0.95); + position: relative; + z-index: 2; + flex-shrink: 0; +} + +.chat-input-wrapper { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + background: var(--ng-input); + border: 1px solid var(--ng-bdr-l); + border-radius: 11px; + padding: 7px 7px 7px 13px; + transition: border-color .2s, box-shadow .2s; +} + +.chat-input-wrapper:focus-within { + border-color: rgba(201, 168, 76, .4); + box-shadow: 0 0 0 3px rgba(201, 168, 76, .07); +} + +#chat-input-box { flex: 1; - padding: 10px; - border: 1px solid #d1d5db; - border-radius: 20px; + min-width: 0; + background: transparent; + border: none; outline: none; - font-size: 14px; - background: #ffffff; - color: #111827; + font-family: 'DM Sans', sans-serif; + font-size: 13px; + color: var(--ng-tp); + display: block; + padding: 0; + margin: 0; + box-shadow: none; + height: auto; +} + +#chat-input-box::placeholder { + color: var(--ng-tm); } -/* Send Button (Floating Circle) */ -.chat-input button { - margin-left: 8px; - width: 42px; - height: 42px; +#send-btn { + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + min-width: 34px; + border-radius: 8px; + background: var(--ng-gold); border: none; - background: #4f46e5; - color: #ffffff; - border-radius: 50%; - font-size: 18px; cursor: pointer; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25); - transition: transform 0.2s, background 0.3s; + transition: background .2s, transform .15s; + flex-shrink: 0; + padding: 0; } -.chat-input button:hover { - transform: scale(1.1); - background: #4338ca; +#send-btn:hover { + background: var(--ng-gold-l); + transform: scale(1.06); } -/* Footer License */ -.chat-footer { +#send-btn:active { + transform: scale(.96); +} + +#send-btn svg { + width: 14px; + height: 14px; + fill: #0d0f14; + display: block; +} + +.input-hint { + display: block; + margin-top: 6px; + font-size: 10px; + color: var(--ng-tm); text-align: center; - padding: 6px; - border-top: 1px solid #eee; - font-size: 12px; - color: #777; - background: #f8fafc; + font-family: 'DM Sans', sans-serif; } \ No newline at end of file diff --git a/nextgenie/nextgenie/page/nextgenie/nextgenie.html b/nextgenie/nextgenie/page/nextgenie/nextgenie.html index 9155910..03339a8 100644 --- a/nextgenie/nextgenie/page/nextgenie/nextgenie.html +++ b/nextgenie/nextgenie/page/nextgenie/nextgenie.html @@ -1,36 +1,85 @@ + For license information, please see license.txt -->
- + + +
+
+
+ NextGenie +
+ + + + +
+ + +
+ +
-

🤖 NextGenie

-

This is a demo chatbot UI built on Frappe.

- +
+

HR Modules

+
    +
  • Leave Management
  • +
  • Payroll Queries
  • +
  • Onboarding
  • +
  • Policy Lookup
  • +
+
+ + + +
+

Quick Actions

+
    +
  • Apply Leave
  • +
  • Salary Slip
  • +
  • Attendance
  • +
  • HR Contacts
  • +
+
+ +
- +
-
-
👋 Hi! I’m NextGenie, how can I help?
-
Hello! Just testing UI 😎
-
- -
- - +
+
+
+
+
NextGenie Assistant
+
Online · HR Module
+
+
+
+ + +
- -
\ No newline at end of file diff --git a/nextgenie/nextgenie/page/nextgenie/nextgenie.js b/nextgenie/nextgenie/page/nextgenie/nextgenie.js index 82386e7..830d3b7 100644 --- a/nextgenie/nextgenie/page/nextgenie/nextgenie.js +++ b/nextgenie/nextgenie/page/nextgenie/nextgenie.js @@ -1,74 +1,252 @@ -// Copyright(c) 2025, Codeventurers and contributors +// Copyright (c) 2025, Codeventurers and contributors // For license information, please see license.txt frappe.pages['nextgenie'].on_page_load = function (wrapper) { + var page = frappe.ui.make_app_page({ parent: wrapper, - title: 'NextGenie Chat', + title: 'NextGenie', single_column: true }); - $(frappe.render_template("nextgenie")).appendTo(page.body); + // Hide Frappe page title bar + setTimeout(function () { + $(wrapper).find('.page-head').hide(); + $(wrapper).closest('.page-wrapper').find('.page-head').hide(); + }, 0); - frappe.call({ - method: "nextgenie.nextgenie.page.nextgenie.nextgenie.get_chat_history", - args: { limit: 50 }, - callback: function (r) { - if (r.message) { - r.message.forEach(msg => { - let css_class = msg.sender === "User" ? "user-message" : "bot-message"; - $("#chat-messages").append(`
${msg.message}
`); - }); - $("#chat-messages").scrollTop($("#chat-messages")[0].scrollHeight); + page.main.css({ 'padding': '0', 'margin': '0' }); + $(page.main).addClass('ng-page-main'); + + $(frappe.render_template("nextgenie")).appendTo(page.main); + + // ── State ──────────────────────────────────────── + var sessions = []; + var activeId = null; + + // ── Helpers ────────────────────────────────────── + + function getTime() { + return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + + function scrollBottom() { + var el = document.getElementById('chat-messages'); + if (el) el.scrollTop = el.scrollHeight; + } + + // ── Render history list ─────────────────────────── + + function renderHistory() { + var $list = $('#hist-list').empty(); + sessions.forEach(function (s) { + var $item = $('
') + .addClass('hist-item') + .toggleClass('active', s.id === activeId); + + $('
').addClass('hist-item-title').text(s.title).appendTo($item); + $('
').addClass('hist-item-date').text(s.date || '').appendTo($item); + + $item.on('click', function () { loadSession(s.id); }); + $list.append($item); + }); + } + + // ── Load session messages ───────────────────────── + + function loadSession(id) { + activeId = id; + var s = sessions.find(function (x) { return x.id === id; }); + if (s) $('#chat-header-title').text(s.title); + $('#chat-messages').empty(); + renderHistory(); + + frappe.call({ + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.get_session_messages', + args: { session_id: id }, + callback: function (r) { + if (r.message && r.message.length) { + r.message.forEach(function (m) { + appendMessage(m.sender, m.message, false); + }); + } } - } - }); + }); + } - // Send message - $(document).on("click", "#send-btn", function () { - sendMessage(); - }); + // ── Append message bubble ───────────────────────── - $(document).on("keypress", "#chat-input-box", function (e) { - if (e.which === 13) { - sendMessage(); - } - }); + function appendMessage(sender, text, animate) { + if (animate === undefined) animate = true; + var isUser = (sender === 'User'); + + // Row + var $row = $('
').addClass('ng-msg-row'); + if (isUser) $row.addClass('ng-user'); + if (!animate) $row.css('animation', 'none'); + + // Avatar + var $av = $('
') + .addClass('ng-avatar') + .addClass(isUser ? 'ng-av-user' : 'ng-av-bot') + .text(isUser ? '👤' : '✦'); + + // Bubble wrapper + var $wrap = $('
').addClass('ng-bubble-wrap'); + + // Bubble — use .text() so no HTML injection and no char-wrap + var $bub = $('
') + .addClass('ng-bubble') + .addClass(isUser ? 'ng-usr' : 'ng-bot') + .text(text); + + // Timestamp + var $time = $('
').addClass('ng-time').text(getTime()); + + $wrap.append($bub).append($time); + $row.append($av).append($wrap); + $('#chat-messages').append($row); + scrollBottom(); + } + + function showTyping() { + var $row = $('
').addClass('ng-msg-row').attr('id', 'ng-typing-row'); + var $av = $('
').addClass('ng-avatar ng-av-bot').text('✦'); + var $typ = $('
').addClass('ng-typing') + .append($('')).append($('')).append($('')); + $row.append($av).append($typ); + $('#chat-messages').append($row); + scrollBottom(); + } + + function hideTyping() { + $('#ng-typing-row').remove(); + } + + // ── Load all sessions ───────────────────────────── + + function loadSessions() { + frappe.call({ + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.get_sessions', + callback: function (r) { + if (r.message && r.message.length) { + sessions = r.message; + renderHistory(); + loadSession(sessions[0].id); + } else { + createNewSession(); + } + } + }); + } + + // ── Create new session ──────────────────────────── + + function createNewSession() { + frappe.call({ + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.create_session', + callback: function (r) { + if (r.message) { + sessions.unshift(r.message); + activeId = r.message.id; + $('#chat-messages').empty(); + $('#chat-header-title').text('New Chat'); + renderHistory(); + + var welcome = "👋 Hello! I'm NextGenie, your private HR assistant. How can I help you today?"; + appendMessage('Genie', welcome); + frappe.call({ + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.add_chat_message', + args: { session_id: activeId, sender: 'Genie', message: welcome } + }); + } + } + }); + } + + // ── Send message ────────────────────────────────── function sendMessage() { - let input = $("#chat-input-box"); - let message = input.val().trim(); + var $input = $('#chat-input-box'); + var message = $input.val().trim(); if (!message) return; - // Append immediately - $("#chat-messages").append(`
${message}
`); - $("#chat-messages").scrollTop($("#chat-messages")[0].scrollHeight); + appendMessage('User', message); + $input.val(''); + + // Auto-rename on first message + var s = sessions.find(function (x) { return x.id === activeId; }); + if (s && s.title === 'New Chat') { + s.title = message.length > 28 ? message.slice(0, 28) + '…' : message; + $('#chat-header-title').text(s.title); + renderHistory(); + frappe.call({ + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.update_session_title', + args: { session_id: activeId, title: s.title } + }); + } + + // Save user message + frappe.call({ + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.add_chat_message', + args: { session_id: activeId, sender: 'User', message: message } + }); + + showTyping(); - // Save to backend + // Get AI reply frappe.call({ - method: "nextgenie.nextgenie.page.nextgenie.nextgenie.add_chat_message", - args: { - sender: "User", - message: message + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.get_ai_reply', + args: { session_id: activeId, message: message }, + callback: function (r) { + hideTyping(); + var reply = (r.message && r.message.reply) + ? r.message.reply + : "I'm here to help! Could you clarify your question?"; + appendMessage('Genie', reply); + frappe.call({ + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.add_chat_message', + args: { session_id: activeId, sender: 'Genie', message: reply } + }); + }, + error: function () { + hideTyping(); + appendMessage('Genie', '⚠️ Something went wrong. Please try again.'); } }); + } - input.val(""); + // ── Events ──────────────────────────────────────── - // For now, add dummy Genie reply to history - let genie_reply = "🤖 Thanks for your message!"; - setTimeout(() => { - $("#chat-messages").append(`
${genie_reply}
`); - $("#chat-messages").scrollTop($("#chat-messages")[0].scrollHeight); + $(document).on('click', '#send-btn', sendMessage); + + $(document).on('keypress', '#chat-input-box', function (e) { + if (e.which === 13) sendMessage(); + }); - // Save bot reply + $(document).on('click', '#new-chat-btn', createNewSession); + + $(document).on('click', '#clear-btn', function () { + if (confirm('Clear this chat?')) { frappe.call({ - method: "nextgenie.nextgenie.page.nextgenie.nextgenie.add_chat_message", - args: { - sender: "Genie", - message: genie_reply + method: 'nextgenie.nextgenie.page.nextgenie.nextgenie.clear_session_messages', + args: { session_id: activeId }, + callback: function () { + $('#chat-messages').empty(); + appendMessage('Genie', 'Chat cleared. How can I assist you?'); } }); - }, 500); - } -}; + } + }); + + $(document).on('click', '#quick-actions li', function () { + var msg = $(this).data('msg'); + if (msg) { + $('#chat-input-box').val(msg); + sendMessage(); + } + }); + + // ── Boot ───────────────────────────────────────── + loadSessions(); +}; \ No newline at end of file diff --git a/nextgenie/nextgenie/page/nextgenie/nextgenie.py b/nextgenie/nextgenie/page/nextgenie/nextgenie.py index fb34b60..edf54d3 100644 --- a/nextgenie/nextgenie/page/nextgenie/nextgenie.py +++ b/nextgenie/nextgenie/page/nextgenie/nextgenie.py @@ -2,17 +2,177 @@ # For license information, please see license.txt import frappe +import requests +from datetime import datetime + + +# ── Session Management ──────────────────────────────────────── + + +@frappe.whitelist() +def get_sessions(): + """Return all chat sessions for the current user, newest first.""" + sessions = frappe.get_all( + "Genie Chat Session", + filters={"owner": frappe.session.user}, + fields=["name as id", "title", "creation"], + order_by="creation desc", + limit=30 + ) + for s in sessions: + s["date"] = _friendly_date(s["creation"]) + return sessions + + +@frappe.whitelist() +def create_session(): + """Create a new chat session and return it.""" + doc = frappe.get_doc({ + "doctype": "Genie Chat Session", + "title": "New Chat", + "owner": frappe.session.user + }) + doc.insert(ignore_permissions=True) + frappe.db.commit() + return { + "id": doc.name, + "title": doc.title, + "date": "Just now" + } + + +@frappe.whitelist() +def update_session_title(session_id, title): + """Rename a session.""" + frappe.db.set_value("Genie Chat Session", session_id, "title", title) + frappe.db.commit() + return {"status": "ok"} + + +# ── Message Management ──────────────────────────────────────── + + +@frappe.whitelist() +def get_session_messages(session_id): + """Return all messages for a session.""" + return frappe.get_all( + "Genie Chat Message", + filters={"session": session_id}, + fields=["sender", "message", "creation"], + order_by="creation asc" + ) + + +@frappe.whitelist() +def add_chat_message(session_id, sender, message): + """Save a single chat message.""" + doc = frappe.get_doc({ + "doctype": "Genie Chat Message", + "session": session_id, + "sender": sender, + "message": message + }) + doc.insert(ignore_permissions=True) + frappe.db.commit() + return {"status": "ok"} @frappe.whitelist() -def get_chat_history(): - return [ - {"sender": "Genie", "message": "👋 Hi, I’m NextGenie!", "creation": "2025-01-01 10:00:00"}, - {"sender": "User", "message": "Hello Genie 😎", "creation": "2025-01-01 10:01:00"}, - {"sender": "Genie", "message": "How can I help you today?", "creation": "2025-01-01 10:02:00"} - ] +def clear_session_messages(session_id): + """Delete all messages in a session.""" + frappe.db.delete("Genie Chat Message", {"session": session_id}) + frappe.db.commit() + return {"status": "ok"} + + +# ── AI Reply ────────────────────────────────────────────────── @frappe.whitelist() -def add_chat_message(sender, message): - return {"status": "ok", "note": "Message saved"} \ No newline at end of file +def get_ai_reply(session_id, message): + """ + Get AI reply from local Ollama model. + Ollama must be running: ollama serve + Model must be pulled: ollama pull phi3:mini + """ + + # Build conversation history for context + history = frappe.get_all( + "Genie Chat Message", + filters={"session": session_id}, + fields=["sender", "message"], + order_by="creation asc", + limit=20 # last 20 messages as context + ) + + # Format history as prompt context + context = "" + for msg in history: + role = "User" if msg["sender"] == "User" else "Assistant" + context += f"{role}: {msg['message']}\n" + context += f"User: {message}\nAssistant:" + + system_prompt = """You are NextGenie, a helpful and professional HR assistant. +You help employees with leave management, payroll queries, attendance, +onboarding, company policies, and HR-related questions. +Always be polite, concise, and accurate. If you don't know something, +say so and suggest contacting the HR team directly.""" + + try: + response = requests.post( + "http://localhost:11434/api/generate", + json={ + "model": "phi3:mini", # change to your pulled model + "system": system_prompt, + "prompt": context, + "stream": False, + "options": { + "temperature": 0.7, + "num_predict": 300 + } + }, + timeout=60 + ) + data = response.json() + reply = data.get("response", "").strip() + if not reply: + reply = "I'm here to help. Could you clarify your question?" + return {"reply": reply} + + except requests.exceptions.ConnectionError: + return {"reply": "⚠️ Local AI is offline. Please start Ollama: `ollama serve`"} + except Exception as e: + frappe.log_error(str(e), "NextGenie AI Error") + return {"reply": "⚠️ Something went wrong. Please try again."} + + +# ── Utils ───────────────────────────────────────────────────── + + +def _friendly_date(creation): + """Convert creation datetime to a friendly label.""" + if not creation: + return "" + # creation may be a string from frappe — parse it + if isinstance(creation, str): + try: + creation = datetime.strptime(creation, "%Y-%m-%d %H:%M:%S.%f") + except ValueError: + creation = datetime.strptime(creation, "%Y-%m-%d %H:%M:%S") + now = datetime.now() + delta = now - creation + total_seconds = int(delta.total_seconds()) + if total_seconds < 60: + return "Just now" + elif total_seconds < 3600: + mins = total_seconds // 60 + return f"{mins}m ago" + elif total_seconds < 86400: + hrs = total_seconds // 3600 + return f"{hrs}h ago" + elif total_seconds < 172800: + return "Yesterday" + elif delta.days < 7: + return f"{delta.days} days ago" + else: + return creation.strftime("%d %b %Y") \ No newline at end of file