mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
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> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-a6a125719a7b32b31b53fcfe05c12081d82c139a401699affdda0db8e73887e3">+47/-1</a> </td> </tr> <tr> <td><strong>ScheduledPipeline.vue</strong><dd><code>Propagate `appendMode` and update sendToAiChat signature</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-63d8f838bb880356e7804b4a57fd9223d4f2c189de7aadc7cf33cba723b29aaf">+10/-1</a> </td> </tr> <tr> <td><strong>MainLayout.vue</strong><dd><code>Bind `appendMode` and clear context after send</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-91e4134141264c4624d4eb4cfa987b33ed31eb6f551e089c0b26f2610dd54362">+13/-3</a> </td> </tr> <tr> <td><strong>DetailTable.vue</strong><dd><code>Update sendToAiChat to accept `append` parameter</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-80552b5d25fa6cef9fbbe430b1865d761c4b132284c2d2175afceea8cbabc8c7">+2/-2</a> </td> </tr> <tr> <td><strong>Index.vue</strong><dd><code>Adjust splitter when AI chat opens or closes</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-0323c3da69b369843c5d72bf4df5b00ca6b5147192a213baf331659ecf92af62">+21/-2</a> </td> </tr> <tr> <td><strong>JsonPreview.vue</strong><dd><code>Extend sendToAiChat signature with `append`</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-3985073134d35ff074742944b114d0905524038b42e6d2ac887fef2c8436fb16">+2/-2</a> </td> </tr> <tr> <td><strong>SearchBar.vue</strong><dd><code>Add toggle CSS class and dynamic width style</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-81893de4edf186b0556f610acb280c87baa2ffe67d3b48a60a866094c7635baf">+10/-1</a> </td> </tr> <tr> <td><strong>SearchResult.vue</strong><dd><code>Add `append` flag to sendToAiChat emit</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-5e65533aa4754bb6d0074fda18ffc1ecacded3b9ca6bc28a161c3f6b12f20195">+2/-2</a> </td> </tr> <tr> <td><strong>TenstackTable.vue</strong><dd><code>Tweak AI button position and append flag support</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-bc3c669f5ab65a42ae378df1f7d6b35014c9c220790bdcc72b47f55d1385dc22">+5/-5</a> </td> </tr> <tr> <td><strong>Functions.vue</strong><dd><code>Modify sendToAiChat to include `append` argument</code> </dd></td> <td><a href="https://github.com/openobserve/openobserve/pull/10291/files#diff-ad825ec1b8d58aec9bf5d2553ea09394a30b22ac59d708bcf9e56168ca1daaf6">+2/-2</a> </td> </tr> </table></details></td></tr></tr></tbody></table> </details> ___
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user