From 65772d3545e972381ad2e7abf2f9e6cf37c16d55 Mon Sep 17 00:00:00 2001 From: GabrieleDeri <33870399+DGabri@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:05:01 +0100 Subject: [PATCH] Initial chatbot UI commit (#10232) * Initial chatbot UI commit * Update dist --- assets/third-party-npm.js | 2 +- http_src/vue/charts/line-chart.vue | 469 ++++++ http_src/vue/chatbot.vue | 1654 ++++++++++++++++++++ http_src/vue/llm_test.vue | 824 ---------- http_src/vue/ntop_vue.js | 6 +- httpdocs/dist | 2 +- httpdocs/misc/db_schema_clickhouse.sql | 18 + package.json | 5 + scripts/locales/en.lua | 19 +- scripts/lua/inc/menu.lua | 17 +- scripts/lua/modules/http_lint.lua | 3 + scripts/lua/modules/page_utils.lua | 20 +- scripts/lua/{llm_test.lua => nanalyst.lua} | 4 +- 13 files changed, 2192 insertions(+), 851 deletions(-) create mode 100644 http_src/vue/charts/line-chart.vue create mode 100644 http_src/vue/chatbot.vue delete mode 100644 http_src/vue/llm_test.vue rename scripts/lua/{llm_test.lua => nanalyst.lua} (93%) diff --git a/assets/third-party-npm.js b/assets/third-party-npm.js index 011cf51cc4..d3d9049ea0 100644 --- a/assets/third-party-npm.js +++ b/assets/third-party-npm.js @@ -10,7 +10,7 @@ window.$ = $ //import moment from 'moment' import moment from 'moment-timezone' import ApexCharts from 'apexcharts' - +import "bootstrap-icons/font/bootstrap-icons.css"; window.moment = moment window.ApexCharts = ApexCharts diff --git a/http_src/vue/charts/line-chart.vue b/http_src/vue/charts/line-chart.vue new file mode 100644 index 0000000000..9a9afd63ce --- /dev/null +++ b/http_src/vue/charts/line-chart.vue @@ -0,0 +1,469 @@ + + + + {{ chart.title }} + + + + + + {{ tooltip.series }} + · + {{ tooltip.value }} + + + + + + + + + {{ s.label }} + + + + + + + + \ No newline at end of file diff --git a/http_src/vue/chatbot.vue b/http_src/vue/chatbot.vue new file mode 100644 index 0000000000..2d911abbbe --- /dev/null +++ b/http_src/vue/chatbot.vue @@ -0,0 +1,1654 @@ + + + + + + + + + + + + {{ _i18n("llm.history") }} + + + + + + + + + + + {{ _i18n("llm.new_chat") }} + + + + + + + + {{ _i18n("loading") }} + + + + {{ _i18n("llm.no_conversations_yet") }} + + + + + + {{ chat.title }} + + + + + + + + + + {{ _i18n("llm.rename_chat") }} + + + {{ _i18n("save") }} + {{ _i18n("cancel") }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ _i18n('llm.loading_providers') }} + + + + {{ _i18n('llm.no_providers') }} + + + + + + + + {{ _i18n("prefs." + selectedProvider) }} + {{ selectedProviderInfo?.model }} + + + + + + + + + + + + {{ _i18n("prefs." + p.provider) }} + {{ p.model }} + + + + + + + + + + + + + + + {{ _i18n('llm.ask_a_question') }} + + + + + + + + + + + + + + + + + {{ _i18n('llm.error_label') }} + + + + + + + + + + + + {{ msg.content }} + + + + + {{ msg.time }} + + · + {{ msg.stats.completion_time_s }}s + + · + {{ msg.stats.generation_tokens_per_second }} tok/s + + + + + + + + + + {{ openSqlPanels.has(idx) ? _i18n('llm.hide_evidence') : _i18n('llm.show_evidence') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/http_src/vue/llm_test.vue b/http_src/vue/llm_test.vue deleted file mode 100644 index 645bc59acf..0000000000 --- a/http_src/vue/llm_test.vue +++ /dev/null @@ -1,824 +0,0 @@ - - - - - - - - {{ _i18n('llm.provider') }} - - - - - {{ _i18n('llm.loading_providers') }} - - - - - {{ _i18n('llm.no_providers') }} - - - - - {{ p.provider }} — {{ p.model }} - - - - - - - {{ providers.find(p => p.provider === selectedProvider)?.model ?? selectedProvider }} - - - - - - - {{ history.length / 2 }} turns - - - - - - {{ _i18n('llm.clear_chat') }} - - - - - - - - - - - - {{ _i18n('llm.empty_state_title') }} - - - - - - - - - - - - - - - - - {{ _i18n('llm.error_label') }} - - - - {{ msg.content }} - - - - - {{ msg.time }} - - · - {{ msg.stats.completion_time_s }}s - - · - {{ msg.stats.generation_tokens_per_second }} tok/s - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/http_src/vue/ntop_vue.js b/http_src/vue/ntop_vue.js index 3fed5d50b0..ce8ad45222 100644 --- a/http_src/vue/ntop_vue.js +++ b/http_src/vue/ntop_vue.js @@ -181,17 +181,19 @@ import { default as ModalDeleteAllACLRule } from "./modal-delete-all-acl-rules.v /* Charts */ import { default as MultiPieChart } from "./charts/multi-pie-chart.vue"; import { default as PieChart } from "./charts/pie-chart.vue"; +import { default as LineChart } from "./charts/line-chart.vue"; import { default as PeityChart } from "./charts/peity.vue"; -//import { default as LLMTest } from "./llm_test.vue"; +import { default as Chatbot } from "./chatbot.vue"; let ntopVue = { - //LLMTest: LLMTest, + Chatbot: Chatbot, // graphs MultiPieChart: MultiPieChart, PieChart: PieChart, PeityChart: PeityChart, + LineChart: LineChart, // pages PageAlertStats: PageAlertStats, diff --git a/httpdocs/dist b/httpdocs/dist index 3cf65c89ab..8ca9fab1ac 160000 --- a/httpdocs/dist +++ b/httpdocs/dist @@ -1 +1 @@ -Subproject commit 3cf65c89abb9d53b5e73dcc75bb4041dd3642e12 +Subproject commit 8ca9fab1acfbba2857cbb9bbf48d7d18f37d53d5 diff --git a/httpdocs/misc/db_schema_clickhouse.sql b/httpdocs/misc/db_schema_clickhouse.sql index 1496f2a85e..ce4bdfde33 100644 --- a/httpdocs/misc/db_schema_clickhouse.sql +++ b/httpdocs/misc/db_schema_clickhouse.sql @@ -1128,3 +1128,21 @@ CREATE TABLE IF NOT EXISTS `hourly_asn` ( COMMENT 'Hourly aggregated traffic statistics per source/destination ASN pair. Used for autonomous-system level traffic analysis and BGP peer analytics. Partitioned by day on FIRST_SEEN.'; @ ALTER TABLE `hourly_asn` ADD COLUMN IF NOT EXISTS TOTAL_BYTES UInt64; + +@ + +CREATE TABLE IF NOT EXISTS ai_chat_history ( + chat_id UUID COMMENT 'Unique identifier for a chat session', + sequence UInt32 COMMENT 'Seq number to preserve message order within a chat', + created_at DateTime COMMENT 'Message creation timestamp', + username String COMMENT 'Identifier of the user who created the chat', + message_role UInt8 COMMENT 'Role of message sender (user = 1 or assistant = 2 )', + message_content String COMMENT 'Raw message content (user input or assistant response)', + provider String COMMENT 'LLM provider used (local llm, anthropic, openAI)', + model String COMMENT 'Model name used for generation', + completion_time_sec UInt32 COMMENT 'Time taken to generate the assistant response (seconds)', + tokens_per_second UInt32 COMMENT 'Generation speed in tokens per second', + artifact_json String DEFAULT '' COMMENT 'JSON-encoded artifact spec (chart, ping, etc.) for assistant messages; empty for user messages', + evidence_json String DEFAULT '' COMMENT 'JSON audit trail of how the answer was produced: tool calls with inputs and result metadata', +) ENGINE = MergeTree() PARTITION BY toYYYYMMDD(created_at) ORDER BY (chat_id, sequence) +COMMENT 'Chat history table storing user and assistant messages for conversations'; diff --git a/package.json b/package.json index af14ad70f1..815cea822a 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@yaireo/tagify": "^4.35.3", "apexcharts": "^3.33.1", "bootstrap": "^5.3.0", + "bootstrap-icons": "^1.13.1", "d3": "^3.5.17", "d3-array": "^3.1.1", "d3-chord": "^3.0.1", @@ -72,13 +73,16 @@ "datatables.net-dt": "^1.11.5", "datatables.net-responsive-dt": "^2.4.0", "dc": "^4.2.7", + "dompurify": "^3.3.3", "dygraphs": "^2.2.1", "flatpickr": "^4.6.11", + "highlight.js": "^11.11.1", "jquery": "^3.6.0", "jquery-ui": "^1.13.2", "jquery.are-you-sure": "^1.9.0", "lucide": "^0.482.0", "madge": "^5.0.1", + "markdown-it": "^14.1.1", "moment": "^2.29.1", "moment-timezone": "^0.5.34", "peity": "^3.3.0", @@ -88,6 +92,7 @@ "sortablejs": "^1.15.1", "store-js": "^2.0.4", "topojson-client": "^2.0.1", + "uuid": "^11.1.0", "vis-network": "^9.1.6", "vite": "^6.3.5", "vue": "3.2.37", diff --git a/scripts/locales/en.lua b/scripts/locales/en.lua index e1005b6d1d..9bbe623171 100644 --- a/scripts/locales/en.lua +++ b/scripts/locales/en.lua @@ -16,6 +16,7 @@ local lang = { ["hop_exporter"] = "Hop / Exporter", ["return_path"] = "Return Path", ["historical_flows"] = "Historical Flows", + ["nanalyst"] = "nAnalyst", ["active_inactive"] = "Active/Inactive Hosts", ["active_monitoring"] = "Active Monitoring", ["current_hosts"] = "Current Hosts", @@ -614,7 +615,7 @@ local lang = { ["memory"] = "Memory", ["middle_endian"] = "Month/Day/Year", ["mirrored_traffic"] = "Mirrored Traffic", - ["missing_x_parameter"] = "Missing \"%{param}\" parameter", + ["missing_x_parameter"] = "Missing %{param} parameter", ["mitre_id"] = "Mitre Att&ck", ["model"] = "Model", ["modify_flowdev_alias"] = "Modify Flow Device Alias", @@ -7123,11 +7124,21 @@ local lang = { ["no_providers"] = "No LLM Provider Available", ["timeout"] = "Timeout", ["timeout_warning"] = "Request Timeout", - ["empty_state_title"] = "Ask us a question!", + ["ask_a_question"] = "Ask nAnalyst a question", ["error_label"] = "Error", ["input_placeholder"] = "Ask a question", - ["sending"] = "Generating...", - ["send"] = "Generate" + ["analyzing"] = "Analyzing...", + ["investigating"] = "Investigating...", + ["inspecting"] = "Inspecting...", + ["correlating"] = "Correlating...", + ["send"] = "Investigate", + ["history"] = "History", + ["new_chat"] = "New Chat", + ["no_conversations_yet"] = "No Conversations Yet", + ["rename_chat"] = "Rename Chat", + ["ai_can_make_mistakes"] = "nAnalyst can make mistakes. Always verify critical information independently", + ["show_evidence"] = "Show Evidence", + ["hide_evidence"] = "Hide Evidence", }, ["notification_endpoint"] = { ["discord"] = { diff --git a/scripts/lua/inc/menu.lua b/scripts/lua/inc/menu.lua index f443df3e83..ce4194a151 100644 --- a/scripts/lua/inc/menu.lua +++ b/scripts/lua/inc/menu.lua @@ -374,15 +374,16 @@ else }) -- ############################################## + -- chatbot, hide if viewed interface or system or not enterprise xl --[[ - page_utils.add_menubar_section({ - section = page_utils.menu_sections.chatbot, - hidden = is_system_interface or is_viewed, - entries = {{ - entry = page_utils.menu_entries.chatbot, - url = '/lua/chatbot.lua' - }}}) - ]] -- + page_utils.add_menubar_section({ + section = page_utils.menu_sections.nanalyst, + hidden = is_system_interface or is_viewed or not ntop.isEnterpriseXL() or not ntop.isClickHouseEnabled(), + entries = {{ + entry = page_utils.menu_entries.nanalyst, + url = '/lua/nanalyst.lua' + }}}) + ]] -- ############################################## -- Views menu entry for ASN Mode diff --git a/scripts/lua/modules/http_lint.lua b/scripts/lua/modules/http_lint.lua index 4a13186f6e..004d441e01 100644 --- a/scripts/lua/modules/http_lint.lua +++ b/scripts/lua/modules/http_lint.lua @@ -2259,6 +2259,9 @@ local known_parameters = { ["prompt"] = validateUnquoted, ["stream"] = validateBool, ["content"] = validateUnchecked, + ["chatId"] = validateUUID, + ["sequence"] = validateNumber, + ["title"] = validateUnquoted, -- VULNERABILITY SCAN ["scan_type"] = validateSingleWord, diff --git a/scripts/lua/modules/page_utils.lua b/scripts/lua/modules/page_utils.lua index 92baa24536..f2a1b76431 100644 --- a/scripts/lua/modules/page_utils.lua +++ b/scripts/lua/modules/page_utils.lua @@ -107,10 +107,12 @@ page_utils.menu_sections = { i18n_title = "help", icon = "fas fa-life-ring" }, - chatbot = { - key = "chatbot", - i18n_title = "chatbot", - icon = "fa-solid fa-headset" + nanalyst = { + key = "nanalyst", + i18n_title = "nanalyst", + -- icon = "fa-solid fa-headset" + icon = "fa-solid fa-magnifying-glass-chart" + -- icon = "fa-solid fa-brain" }, health = { key = "health", @@ -305,11 +307,11 @@ page_utils.menu_entries = { section = "hosts" }, - -- Chatbot - chatbot = { - key = "chatbot", - i18n_title = "chatbot", - section = "chatbot" + -- Chatbot (nAnalyst) + nanalyst = { + key = "nanalyst", + i18n_title = "nanalyst", + section = "nanalyst" }, -- Interface diff --git a/scripts/lua/llm_test.lua b/scripts/lua/nanalyst.lua similarity index 93% rename from scripts/lua/llm_test.lua rename to scripts/lua/nanalyst.lua index 868ee3be6a..38c8ae1574 100644 --- a/scripts/lua/llm_test.lua +++ b/scripts/lua/nanalyst.lua @@ -13,7 +13,7 @@ local template_utils = require("template_utils") sendHTTPContentTypeHeader('text/html') -page_utils.print_header_and_set_active_menu_entry(page_utils.menu_entries.geo_map) +page_utils.print_header_and_set_active_menu_entry(page_utils.menu_entries.nanalyst) dofile(dirs.installdir .. "/scripts/lua/inc/menu.lua") @@ -26,7 +26,7 @@ local context = { local json_context = json.encode(context) template_utils.render("pages/vue_page.template", { - vue_page_name = "LLMTest", + vue_page_name = "Chatbot", page_context = json_context })