From ec2a9cc7719dc4dab3d543dc6845ff6db4ad09cd Mon Sep 17 00:00:00 2001 From: Sai Nikhil Date: Fri, 30 Jan 2026 20:06:25 +0530 Subject: [PATCH] fix: o2 ai issues when enabled (#10291) ### **User description** This PR fixes the issue in E: 1216 (1 & 3) , E: 1218 , along with append the fields when sending to ai chat ___ ### **PR Type** Enhancement ___ ### **Description** - Add `appendMode` prop for AI Chat - Auto-scroll input on append - Propagate and clear chat context - UI tweaks: widths, toggle styles, splitter ___ ### Diagram Walkthrough ```mermaid flowchart LR SP["ScheduledPipeline.vue"] -- "sendToAiChat(value,append)" --> O2AIChat ML["MainLayout.vue"] -- "sendToAiChat(value,append)" --> O2AIChat O2AIChat["O2AIChat.vue"] --> AM["appendMode prop"] AM -- "true" --> APPEND["append input with separator"] AM -- "false" --> REPLACE["replace input"] APPEND --> SCROLL["scrollInputToBottom()"] ```

File Walkthrough

Relevant files
Enhancement
10 files
O2AIChat.vue
Add `appendMode` prop and scroll input enhancements           
+47/-1   
ScheduledPipeline.vue
Propagate `appendMode` and update sendToAiChat signature 
+10/-1   
MainLayout.vue
Bind `appendMode` and clear context after send                     
+13/-3   
DetailTable.vue
Update sendToAiChat to accept `append` parameter                 
+2/-2     
Index.vue
Adjust splitter when AI chat opens or closes                         
+21/-2   
JsonPreview.vue
Extend sendToAiChat signature with `append`                           
+2/-2     
SearchBar.vue
Add toggle CSS class and dynamic width style                         
+10/-1   
SearchResult.vue
Add `append` flag to sendToAiChat emit                                     
+2/-2     
TenstackTable.vue
Tweak AI button position and append flag support                 
+5/-5     
Functions.vue
Modify sendToAiChat to include `append` argument                 
+2/-2     
___ --- web/src/components/O2AIChat.vue | 48 ++++++++++++++++++- .../pipeline/NodeForm/ScheduledPipeline.vue | 11 ++++- web/src/layouts/MainLayout.vue | 16 +++++-- web/src/plugins/logs/DetailTable.spec.ts | 4 +- web/src/plugins/logs/DetailTable.vue | 4 +- web/src/plugins/logs/Index.vue | 23 ++++++++- web/src/plugins/logs/JsonPreview.spec.ts | 2 +- web/src/plugins/logs/JsonPreview.vue | 4 +- web/src/plugins/logs/SearchBar.vue | 11 ++++- web/src/plugins/logs/SearchResult.spec.ts | 2 +- web/src/plugins/logs/SearchResult.vue | 4 +- web/src/plugins/logs/TenstackTable.vue | 10 ++-- web/src/views/Functions.spec.ts | 4 +- web/src/views/Functions.vue | 4 +- 14 files changed, 120 insertions(+), 27 deletions(-) diff --git a/web/src/components/O2AIChat.vue b/web/src/components/O2AIChat.vue index 5cfe3da949..f5171bf05c 100644 --- a/web/src/components/O2AIChat.vue +++ b/web/src/components/O2AIChat.vue @@ -479,6 +479,10 @@ export default defineComponent({ aiChatInputContext: { type: String, default: '' + }, + appendMode: { + type: Boolean, + default: true } }, setup(props) { @@ -488,6 +492,7 @@ export default defineComponent({ const isLoading = ref(false); const messagesContainer = ref(null); const chatInput = ref(null); + const scrollTimeoutId = ref | null>(null); const currentStreamingMessage = ref(''); const currentTextSegment = ref(''); // Track current text segment (resets after each tool call) const showHistory = ref(false); @@ -709,7 +714,20 @@ export default defineComponent({ watch(() => props.aiChatInputContext, (newAiChatInputContext: string) => { if(newAiChatInputContext) { - inputMessage.value = newAiChatInputContext; + if (props.appendMode) { + // Append mode: add to existing input with separator if needed + const currentValue = inputMessage.value?.trim(); + if (currentValue) { + inputMessage.value = currentValue + "\n\n" + newAiChatInputContext; + } else { + inputMessage.value = newAiChatInputContext; + } + // Scroll to show the newly appended content + scrollInputToBottom(); + } else { + // Replace mode: replace the input + inputMessage.value = newAiChatInputContext; + } } }); @@ -1408,6 +1426,34 @@ export default defineComponent({ } }; + // Scroll input textarea to bottom to show latest appended content + const scrollInputToBottom = () => { + // Clear any pending scroll timeout + if (scrollTimeoutId.value !== null) { + clearTimeout(scrollTimeoutId.value); + } + + // Set new timeout for scroll + scrollTimeoutId.value = setTimeout(() => { + const textarea = chatInput.value?.$el?.querySelector('textarea'); + if (!textarea) return; + + textarea.focus(); + textarea.setSelectionRange(textarea.value.length, textarea.value.length); + + // Scroll all scrollable parent elements + let element = textarea; + while (element && element !== document.body) { + if (element.scrollHeight > element.clientHeight) { + element.scrollTop = element.scrollHeight; + } + element = element.parentElement; + } + + scrollTimeoutId.value = null; + }, 50); + }; + // Load query history from localStorage const loadQueryHistory = () => { try { diff --git a/web/src/components/pipeline/NodeForm/ScheduledPipeline.vue b/web/src/components/pipeline/NodeForm/ScheduledPipeline.vue index 8e4ed4a95f..07669899af 100644 --- a/web/src/components/pipeline/NodeForm/ScheduledPipeline.vue +++ b/web/src/components/pipeline/NodeForm/ScheduledPipeline.vue @@ -1247,6 +1247,7 @@ size="md" /> :is-open="store.state.isAiChatEnabled" @close="store.state.isAiChatEnabled = false" :aiChatInputContext="aiChatInputContext" + :appendMode="aiChatAppendMode" /> @@ -1453,6 +1454,7 @@ const isHovered = ref(false); const loading = ref(false); const aiChatInputContext = ref(""); +const aiChatAppendMode = ref(true); const userDefinedFields = ref([]); @@ -2388,9 +2390,15 @@ const removeAiContextHandler = () => { // [END] O2 AI Context Handler -const sendToAiChat = (value: any) => { +const sendToAiChat = (value: any, append: boolean = true) => { + aiChatAppendMode.value = append; aiChatInputContext.value = value; store.dispatch("setIsAiChatEnabled", true); + + // Clear context after setting to prevent accumulation + nextTick(() => { + aiChatInputContext.value = ""; + }); }; defineExpose({ @@ -2429,6 +2437,7 @@ defineExpose({ getBtnLogo, sendToAiChat, aiChatInputContext, + aiChatAppendMode, }); diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index 868cc354d9..e5a10e4bbf 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -104,6 +104,7 @@ along with this program. If not, see . :is-open="store.state.isAiChatEnabled" @close="closeChat" :aiChatInputContext="aiChatInputContext" + :appendMode="aiChatAppendMode" /> @@ -315,6 +316,7 @@ export default defineComponent({ ); const isHovered = ref(false); const aiChatInputContext = ref(""); + const aiChatAppendMode = ref(true); const rowsPerPage = ref(10); const searchQuery = ref(""); @@ -1062,15 +1064,22 @@ export default defineComponent({ localStorage.removeItem("isFirstTimeLogin"); }; - const sendToAiChat = (value: any) => { + const sendToAiChat = (value: any, append: boolean = true) => { if (!store.state.isAiChatEnabled) { store.dispatch("setIsAiChatEnabled", true); } - //here we reset the value befoere setting it because if user clears the input then again click on the same value it wont trigger the watcher that is there in the child component - //so to force trigger we do this + + // Set the append mode + aiChatAppendMode.value = append; + + // Always clear and set to trigger the watcher in O2AIChat aiChatInputContext.value = ""; nextTick(() => { aiChatInputContext.value = value; + // Clear it after another tick so it doesn't accumulate in parent + nextTick(() => { + aiChatInputContext.value = ""; + }); }); }; @@ -1134,6 +1143,7 @@ export default defineComponent({ removeFirstTimeLogin, sendToAiChat, aiChatInputContext, + aiChatAppendMode, userClickedOrg, searchQuery, filteredOrganizations, diff --git a/web/src/plugins/logs/DetailTable.spec.ts b/web/src/plugins/logs/DetailTable.spec.ts index 3cced0cb9a..cd82e40649 100644 --- a/web/src/plugins/logs/DetailTable.spec.ts +++ b/web/src/plugins/logs/DetailTable.spec.ts @@ -631,9 +631,9 @@ describe("DetailTable Component", () => { it("should emit sendToAiChat and closeTable when sendToAiChat called", () => { const testValue = "test message"; wrapper.vm.sendToAiChat(testValue); - + expect(wrapper.emitted().sendToAiChat).toBeTruthy(); - expect(wrapper.emitted().sendToAiChat[0]).toEqual([testValue]); + expect(wrapper.emitted().sendToAiChat[0]).toEqual([testValue, true]); // includes append parameter expect(wrapper.emitted().closeTable).toBeTruthy(); }); diff --git a/web/src/plugins/logs/DetailTable.vue b/web/src/plugins/logs/DetailTable.vue index ecc4cea56b..16a0dd1b60 100644 --- a/web/src/plugins/logs/DetailTable.vue +++ b/web/src/plugins/logs/DetailTable.vue @@ -740,8 +740,8 @@ export default defineComponent({ emit("view-trace"); }; - const sendToAiChat = (value: any) => { - emit("sendToAiChat", value); + const sendToAiChat = (value: any, append: boolean = true) => { + emit("sendToAiChat", value, append); emit("closeTable"); }; diff --git a/web/src/plugins/logs/Index.vue b/web/src/plugins/logs/Index.vue index 2d11facf0c..383dc4a44e 100644 --- a/web/src/plugins/logs/Index.vue +++ b/web/src/plugins/logs/Index.vue @@ -1992,6 +1992,25 @@ export default defineComponent({ { deep: true }, ); + // Watch AI chat state and adjust splitter to give more space when chat is open + const originalSplitterValue = ref(searchObj.config.splitterModel); + watch( + () => store.state.isAiChatEnabled, + (isEnabled) => { + // Only adjust splitter if field list is shown + if (searchObj.meta.showFields) { + if (isEnabled) { + // AI chat opened - save current value and set splitter to 25 + originalSplitterValue.value = searchObj.config.splitterModel; + searchObj.config.splitterModel = 25; + } else { + // AI chat closed - restore original splitter value + searchObj.config.splitterModel = originalSplitterValue.value; + } + } + } + ); + const handleRunQueryFn = async (clear_cache = false) => { if (searchObj.meta.logsVisualizeToggle == "visualize") { // Set the shouldRefreshWithoutCache flag @@ -2519,8 +2538,8 @@ export default defineComponent({ // [END] Context Provider Setup - const sendToAiChat = (value: any) => { - emit("sendToAiChat", value); + const sendToAiChat = (value: any, append: boolean = true) => { + emit("sendToAiChat", value, append); }; const clearAllTimeouts = () => { diff --git a/web/src/plugins/logs/JsonPreview.spec.ts b/web/src/plugins/logs/JsonPreview.spec.ts index c5a5d183ef..9d1f48647e 100644 --- a/web/src/plugins/logs/JsonPreview.spec.ts +++ b/web/src/plugins/logs/JsonPreview.spec.ts @@ -362,7 +362,7 @@ describe("JsonPreview Component", () => { it("should emit sendToAiChat event", () => { wrapper.vm.sendToAiChat("test data"); expect(wrapper.emitted('sendToAiChat')).toBeTruthy(); - expect(wrapper.emitted('sendToAiChat')[0]).toEqual(["test data"]); + expect(wrapper.emitted('sendToAiChat')[0]).toEqual(["test data", true]); // includes append parameter expect(wrapper.emitted('closeTable')).toBeTruthy(); }); }); diff --git a/web/src/plugins/logs/JsonPreview.vue b/web/src/plugins/logs/JsonPreview.vue index 51d39b832a..9f9b7da28b 100644 --- a/web/src/plugins/logs/JsonPreview.vue +++ b/web/src/plugins/logs/JsonPreview.vue @@ -790,9 +790,9 @@ export default { return t("common.addFieldToTable"); }; - const sendToAiChat = (value: string) => { + const sendToAiChat = (value: string, append: boolean = true) => { emit("closeTable"); - emit("sendToAiChat", value); + emit("sendToAiChat", value, append); }; const getBtnLogo = computed(() => { diff --git a/web/src/plugins/logs/SearchBar.vue b/web/src/plugins/logs/SearchBar.vue index d2c54729ec..465cf3b43f 100644 --- a/web/src/plugins/logs/SearchBar.vue +++ b/web/src/plugins/logs/SearchBar.vue @@ -479,6 +479,7 @@ class="field_list" no-hover> v-model="searchObj.meta.showHistogram" size="xs" flat + class="o2-toggle-button-xs" :class=" store.state.theme === 'dark' ? 'o2-toggle-button-xs-dark' @@ -523,6 +524,7 @@ class="field_list" no-hover> ? 'o2-toggle-button-xs-dark' : 'o2-toggle-button-xs-light' " + class="o2-toggle-button-xs" @click.stop /> @@ -562,6 +564,7 @@ class="field_list" no-hover> ? 'o2-toggle-button-xs-dark' : 'o2-toggle-button-xs-light' " + class="o2-toggle-button-xs" @click.stop="handleQuickMode" /> @@ -4020,6 +4023,12 @@ export default defineComponent({ ? "0.375rem solid var(--o2-card-bg)" : "0.375rem solid var(--o2-card-bg)" : "none", + // Conditional width when focused (expand-on-focus active) + width: isFocused.value + ? store.state.isAiChatEnabled + ? "calc(75% - 104px)" // AI chat enabled: 75% minus nav width + : "calc(100% - 104px)" // AI chat disabled: full width minus nav + : undefined, }; }); const editorWidthToggleFunction = computed(() => { @@ -4606,7 +4615,7 @@ export default defineComponent({ position: fixed !important; height: calc(100vh - 12.5rem) !important; z-index: 20 !important; - width: calc(100% - 104px); + /* Width is now handled dynamically via backgroundColorStyle computed property */ } .file-type label { diff --git a/web/src/plugins/logs/SearchResult.spec.ts b/web/src/plugins/logs/SearchResult.spec.ts index fd40e23af7..05c0baf910 100644 --- a/web/src/plugins/logs/SearchResult.spec.ts +++ b/web/src/plugins/logs/SearchResult.spec.ts @@ -191,7 +191,7 @@ describe("SearchResult Component", () => { await wrapper.vm.sendToAiChat(testValue); - expect(wrapper.emitted()["sendToAiChat"]).toEqual([[testValue]]); + expect(wrapper.emitted()["sendToAiChat"]).toEqual([[testValue, true]]); // includes append parameter }); }); diff --git a/web/src/plugins/logs/SearchResult.vue b/web/src/plugins/logs/SearchResult.vue index 4dd3ddfb3b..94d45b139a 100644 --- a/web/src/plugins/logs/SearchResult.vue +++ b/web/src/plugins/logs/SearchResult.vue @@ -1394,8 +1394,8 @@ export default defineComponent({ return searchObj.meta.showHistogram && searchObj.loadingHistogram == true; }); - const sendToAiChat = (value: any) => { - emit("sendToAiChat", value); + const sendToAiChat = (value: any, append: boolean = true) => { + emit("sendToAiChat", value, append); }; const closeTable = () => { diff --git a/web/src/plugins/logs/TenstackTable.vue b/web/src/plugins/logs/TenstackTable.vue index 0b9e515083..5ca957be72 100644 --- a/web/src/plugins/logs/TenstackTable.vue +++ b/web/src/plugins/logs/TenstackTable.vue @@ -382,7 +382,7 @@ along with this program. If not, see . cell.column.columnDef.id === store.state.zoConfig.timestamp_column " - class="tw:absolute tw:top-1/2 tw:transform tw:invisible tw:-translate-y-1/2 ai-btn" + class="tw:absolute tw:top-[14px] tw:left-[18px] tw:transform tw:invisible tw:-translate-y-1/2 ai-btn" @send-to-ai-chat=" sendToAiChat(JSON.stringify(cell.row.original), true) " @@ -1034,7 +1034,7 @@ const showCorrelation = (row: any) => { emits("show-correlation", row); }; -const sendToAiChat = (value: any, isEntireRow: boolean = false) => { +const sendToAiChat = (value: any, isEntireRow: boolean = false, append: boolean = true) => { if (isEntireRow) { //here we will get the original value of the row //and we need to filter the row if props.columns have any filtered cols that user applied @@ -1046,14 +1046,14 @@ const sendToAiChat = (value: any, isEntireRow: boolean = false) => { //lets filter based on props.columns so lets ignore _timestamp column as it is always present and now we want to check if source is present we can directly send the row //otherwise we need to filter the row based on the columns that user have applied if (checkIfSourceColumnPresent(props.columns)) { - emits("sendToAiChat", JSON.stringify(row)); + emits("sendToAiChat", JSON.stringify(row), append); } else { //we need to filter the row based on the columns that user have applied const filteredRow = filterRowBasedOnColumns(row, props.columns); - emits("sendToAiChat", JSON.stringify(filteredRow)); + emits("sendToAiChat", JSON.stringify(filteredRow), append); } } else { - emits("sendToAiChat", value); + emits("sendToAiChat", value, append); } }; diff --git a/web/src/views/Functions.spec.ts b/web/src/views/Functions.spec.ts index 79e63f2d46..f5970ccdd3 100644 --- a/web/src/views/Functions.spec.ts +++ b/web/src/views/Functions.spec.ts @@ -304,9 +304,9 @@ describe('Functions.vue', () => { const testValue = { message: 'test message' }; (wrapper.vm as any).sendToAiChat(testValue); - + expect(wrapper.emitted('sendToAiChat')).toBeTruthy(); - expect(wrapper.emitted('sendToAiChat')?.[0]).toEqual([testValue]); + expect(wrapper.emitted('sendToAiChat')?.[0]).toEqual([testValue, false]); // includes append parameter }); }); diff --git a/web/src/views/Functions.vue b/web/src/views/Functions.vue index 396a7ac9c0..9662679973 100644 --- a/web/src/views/Functions.vue +++ b/web/src/views/Functions.vue @@ -172,8 +172,8 @@ export default defineComponent({ } }; - const sendToAiChat = (value: any) => { - emit("sendToAiChat", value); + const sendToAiChat = (value: any, append: boolean = false) => { + emit("sendToAiChat", value, append); }; return {