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()"]
```



<details> <summary><h3> File Walkthrough</h3></summary>

<table><thead><tr><th></th><th align="left">Relevant
files</th></tr></thead><tbody><tr><td><strong>Enhancement</strong></td><td><details><summary>10
files</summary><table>
<tr>
<td><strong>O2AIChat.vue</strong><dd><code>Add `appendMode` prop and
scroll input enhancements</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-a6a125719a7b32b31b53fcfe05c12081d82c139a401699affdda0db8e73887e3">+47/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>ScheduledPipeline.vue</strong><dd><code>Propagate
`appendMode` and update sendToAiChat signature</code>&nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-63d8f838bb880356e7804b4a57fd9223d4f2c189de7aadc7cf33cba723b29aaf">+10/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>MainLayout.vue</strong><dd><code>Bind `appendMode` and clear
context after send</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-91e4134141264c4624d4eb4cfa987b33ed31eb6f551e089c0b26f2610dd54362">+13/-3</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>DetailTable.vue</strong><dd><code>Update sendToAiChat to
accept `append` parameter</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-80552b5d25fa6cef9fbbe430b1865d761c4b132284c2d2175afceea8cbabc8c7">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>Index.vue</strong><dd><code>Adjust splitter when AI chat
opens or closes</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-0323c3da69b369843c5d72bf4df5b00ca6b5147192a213baf331659ecf92af62">+21/-2</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>JsonPreview.vue</strong><dd><code>Extend sendToAiChat
signature with `append`</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-3985073134d35ff074742944b114d0905524038b42e6d2ac887fef2c8436fb16">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>SearchBar.vue</strong><dd><code>Add toggle CSS class and
dynamic width style</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-81893de4edf186b0556f610acb280c87baa2ffe67d3b48a60a866094c7635baf">+10/-1</a>&nbsp;
&nbsp; </td>

</tr>

<tr>
<td><strong>SearchResult.vue</strong><dd><code>Add `append` flag to
sendToAiChat emit</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-5e65533aa4754bb6d0074fda18ffc1ecacded3b9ca6bc28a161c3f6b12f20195">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>TenstackTable.vue</strong><dd><code>Tweak AI button position
and append flag support</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-bc3c669f5ab65a42ae378df1f7d6b35014c9c220790bdcc72b47f55d1385dc22">+5/-5</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>

<tr>
<td><strong>Functions.vue</strong><dd><code>Modify sendToAiChat to
include `append` argument</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;
&nbsp; &nbsp; &nbsp; &nbsp; </dd></td>
<td><a
href="https://github.com/openobserve/openobserve/pull/10291/files#diff-ad825ec1b8d58aec9bf5d2553ea09394a30b22ac59d708bcf9e56168ca1daaf6">+2/-2</a>&nbsp;
&nbsp; &nbsp; </td>

</tr>
</table></details></td></tr></tr></tbody></table>

</details>

___
This commit is contained in:
Sai Nikhil
2026-01-30 20:06:25 +05:30
committed by GitHub
parent 2c4ae4ea66
commit ec2a9cc771
14 changed files with 120 additions and 27 deletions
+47 -1
View File
@@ -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<HTMLElement | null>(null);
const chatInput = ref<HTMLElement | null>(null);
const scrollTimeoutId = ref<ReturnType<typeof setTimeout> | 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 {
@@ -1247,6 +1247,7 @@ size="md" />
:is-open="store.state.isAiChatEnabled"
@close="store.state.isAiChatEnabled = false"
:aiChatInputContext="aiChatInputContext"
:appendMode="aiChatAppendMode"
/>
</div>
</div>
@@ -1453,6 +1454,7 @@ const isHovered = ref(false);
const loading = ref(false);
const aiChatInputContext = ref("");
const aiChatAppendMode = ref(true);
const userDefinedFields = ref<any[]>([]);
@@ -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,
});
</script>
+13 -3
View File
@@ -104,6 +104,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:is-open="store.state.isAiChatEnabled"
@close="closeChat"
:aiChatInputContext="aiChatInputContext"
:appendMode="aiChatAppendMode"
/>
</div>
</div>
@@ -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,
+2 -2
View File
@@ -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();
});
+2 -2
View File
@@ -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");
};
+21 -2
View File
@@ -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 = () => {
+1 -1
View File
@@ -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();
});
});
+2 -2
View File
@@ -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(() => {
+10 -1
View File
@@ -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
/>
</div>
@@ -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"
/>
</div>
@@ -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 {
+1 -1
View File
@@ -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
});
});
+2 -2
View File
@@ -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 = () => {
+5 -5
View File
@@ -382,7 +382,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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);
}
};
+2 -2
View File
@@ -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
});
});
+2 -2
View File
@@ -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 {