mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
fix: O2A title and tool call improvements (#10385)
This commit is contained in:
+729
-95
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,9 @@ export const createAlertsContextProvider = (
|
||||
alertName: formData?.value?.name || '',
|
||||
isRealTime: formData?.value?.type === 'realtime',
|
||||
organization_identifier: store?.state?.selectedOrganization?.identifier || '',
|
||||
user_intent: isUpdated ? 'edit existing alert' : 'create new alert'
|
||||
user_intent: isUpdated ? 'edit existing alert' : 'create new alert',
|
||||
// Current timestamp when request is fired (microseconds) for AI agent time calculations
|
||||
request_timestamp: Date.now() * 1000,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -47,7 +47,9 @@ export const createDashboardsContextProvider = (
|
||||
const context: PageContext = {
|
||||
currentPage: 'Dashboards',
|
||||
dashboardId: route?.query?.dashboard || '',
|
||||
organization_identifier: store?.state?.selectedOrganization?.identifier || ''
|
||||
organization_identifier: store?.state?.selectedOrganization?.identifier || '',
|
||||
// Current timestamp when request is fired (microseconds) for AI agent time calculations
|
||||
request_timestamp: Date.now() * 1000,
|
||||
};
|
||||
|
||||
// Determine user intent based on route and context
|
||||
|
||||
@@ -42,16 +42,19 @@ export const createDefaultContextProvider = (
|
||||
|
||||
return {
|
||||
currentPage: currentRoute?.meta?.title || 'unknown',
|
||||
|
||||
|
||||
// Current route information
|
||||
routeName: currentRoute?.name || 'unknown',
|
||||
routePath: currentRoute?.path || '/',
|
||||
routeFullPath: currentRoute?.fullPath || '/',
|
||||
routeParams: currentRoute?.params || {},
|
||||
routeQuery: currentRoute?.query || {},
|
||||
|
||||
|
||||
// Organization context
|
||||
organization_identifier: store?.state?.selectedOrganization?.identifier || ''
|
||||
organization_identifier: store?.state?.selectedOrganization?.identifier || '',
|
||||
|
||||
// Current timestamp when request is fired (microseconds) for AI agent time calculations
|
||||
request_timestamp: Date.now() * 1000,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,6 +50,8 @@ export const createIncidentsContextProvider = (
|
||||
alert_count: incidentData?.alert_count,
|
||||
first_alert_at: incidentData?.first_alert_at,
|
||||
last_alert_at: incidentData?.last_alert_at,
|
||||
// Current timestamp when request is fired (microseconds) for AI agent time calculations
|
||||
request_timestamp: Date.now() * 1000,
|
||||
};
|
||||
|
||||
return context;
|
||||
|
||||
@@ -181,19 +181,21 @@ export const createLogsContextProvider = (
|
||||
// Stream information
|
||||
selectedStreams: streams || [],
|
||||
streamType: streamType,
|
||||
|
||||
|
||||
// Interesting fields organized by stream name (using actual structure)
|
||||
interestingFields: extractInterestingFieldsByStream(
|
||||
searchObj?.data?.stream?.selectedInterestingStreamFields || []
|
||||
),
|
||||
|
||||
|
||||
// Time range (conditional based on type)
|
||||
timeRange: buildTimeRangeContext(searchObj.data.datetime),
|
||||
|
||||
|
||||
// Current organization
|
||||
organization_identifier: store?.state?.selectedOrganization?.identifier || '',
|
||||
quickMode: searchObj?.meta?.quickMode || false,
|
||||
|
||||
|
||||
// Current timestamp when request is fired (microseconds) for AI agent time calculations
|
||||
request_timestamp: Date.now() * 1000,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error generating logs context:', error);
|
||||
|
||||
@@ -44,7 +44,9 @@ export const createPipelinesContextProvider = (
|
||||
pipelineId: pipeline?.id || '',
|
||||
pipelineName: pipeline?.name || '',
|
||||
organization_identifier: store?.state?.selectedOrganization?.identifier || '',
|
||||
user_intent: pipelineObj?.isEditPipeline ? 'edit existing pipeline' : 'create new pipeline'
|
||||
user_intent: pipelineObj?.isEditPipeline ? 'edit existing pipeline' : 'create new pipeline',
|
||||
// Current timestamp when request is fired (microseconds) for AI agent time calculations
|
||||
request_timestamp: Date.now() * 1000,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -368,12 +368,12 @@ describe('useAiChat', () => {
|
||||
it('should make API call with correct URL and body when model is provided', async () => {
|
||||
const mockResponse = { ok: true, json: vi.fn() };
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
|
||||
const model = 'gpt-4';
|
||||
const orgId = 'test-org';
|
||||
|
||||
|
||||
const result = await aiChatComposable.fetchAiChat(mockMessages, model, orgId);
|
||||
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`http://localhost:5080/api/test-org/ai/chat_stream`,
|
||||
expect.objectContaining({
|
||||
@@ -383,27 +383,34 @@ describe('useAiChat', () => {
|
||||
'Content-Type': 'application/json',
|
||||
'traceparent': expect.stringMatching(/^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/)
|
||||
}),
|
||||
body: JSON.stringify({ model, messages: mockMessages })
|
||||
})
|
||||
);
|
||||
// Verify body includes model, messages, and context with user_timezone
|
||||
const callArgs = mockFetch.mock.calls[0][1];
|
||||
const requestBody = JSON.parse(callArgs.body);
|
||||
expect(requestBody.model).toBe(model);
|
||||
expect(requestBody.messages).toEqual(mockMessages);
|
||||
expect(requestBody.context).toBeDefined();
|
||||
expect(requestBody.context.user_timezone).toBeDefined();
|
||||
expect(result).toBe(mockResponse);
|
||||
});
|
||||
|
||||
it('should make API call without model when model is empty string', async () => {
|
||||
const mockResponse = { ok: true, json: vi.fn() };
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
|
||||
const model = '';
|
||||
const orgId = 'test-org';
|
||||
|
||||
|
||||
await aiChatComposable.fetchAiChat(mockMessages, model, orgId);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`http://localhost:5080/api/test-org/ai/chat_stream`,
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({ messages: mockMessages })
|
||||
})
|
||||
);
|
||||
|
||||
// Verify body includes messages and context with user_timezone but no model
|
||||
const callArgs = mockFetch.mock.calls[0][1];
|
||||
const requestBody = JSON.parse(callArgs.body);
|
||||
expect(requestBody.model).toBeUndefined();
|
||||
expect(requestBody.messages).toEqual(mockMessages);
|
||||
expect(requestBody.context).toBeDefined();
|
||||
expect(requestBody.context.user_timezone).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not mutate original messages array', async () => {
|
||||
|
||||
@@ -80,10 +80,16 @@ const useAiChat = () => {
|
||||
// Extract agent_type from context if present (for SRE agent routing)
|
||||
const { agent_type, ...contextWithoutAgentType } = contextToUse;
|
||||
|
||||
// Add user's timezone to context for time display formatting
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// Build payload with agent_type at root level if present
|
||||
const payload: any = {
|
||||
messages: _messages,
|
||||
context: contextWithoutAgentType
|
||||
context: {
|
||||
...contextWithoutAgentType,
|
||||
user_timezone: userTimezone,
|
||||
}
|
||||
};
|
||||
|
||||
body = JSON.stringify(payload);
|
||||
@@ -91,14 +97,17 @@ const useAiChat = () => {
|
||||
// Fallback to legacy approach - inject context into message content
|
||||
const currentMessage = _messages[_messages.length - 1];
|
||||
currentMessage.content = getFormattedContext(currentMessage, legacyContext);
|
||||
body = model.length > 0
|
||||
? JSON.stringify({ model, messages: _messages })
|
||||
: JSON.stringify({ messages: _messages });
|
||||
// Add user's timezone to context
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
body = model.length > 0
|
||||
? JSON.stringify({ model, messages: _messages, context: { user_timezone: userTimezone } })
|
||||
: JSON.stringify({ messages: _messages, context: { user_timezone: userTimezone } });
|
||||
} else {
|
||||
// No context available
|
||||
body = model.length > 0
|
||||
? JSON.stringify({ model, messages: _messages })
|
||||
: JSON.stringify({ messages: _messages });
|
||||
// No context available - still include timezone
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
body = model.length > 0
|
||||
? JSON.stringify({ model, messages: _messages, context: { user_timezone: userTimezone } })
|
||||
: JSON.stringify({ messages: _messages, context: { user_timezone: userTimezone } });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -134,7 +134,7 @@ export default defineComponent({
|
||||
{ name: 'Sensitive Data Redaction', price: '$0.15 / GB' , is_parent: true},
|
||||
|
||||
{ name: 'Incident Management', price: '$0 during preview' , is_parent: true},
|
||||
{ name: 'AI Assistant', price: '$0 during preview' , is_parent: true},
|
||||
{ name: 'O2 Assistant', price: '$0 during preview' , is_parent: true},
|
||||
{ name: 'AI SRE Agent', price: '$0 during preview' , is_parent: true},
|
||||
{ name: 'Audit Trail', price: '2% of monthly spend' , is_parent: true},
|
||||
{ name: 'Retention', price: '' , is_parent: true},
|
||||
|
||||
@@ -92,7 +92,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<div
|
||||
class="col-auto"
|
||||
v-show="store.state.isAiChatEnabled && isLoading"
|
||||
style="width: 25%; max-width: 100%; min-width: 75px; z-index: 10"
|
||||
style="width: 25%; max-width: 100%; min-width: 75px; z-index: 10; padding-top: 44px; padding-right: 0.625rem;"
|
||||
:class="
|
||||
store.state.theme == 'dark'
|
||||
? 'dark-mode-chat-container'
|
||||
@@ -100,7 +100,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"
|
||||
>
|
||||
<O2AIChat
|
||||
:header-height="82.5"
|
||||
:header-height="42.5"
|
||||
:is-open="store.state.isAiChatEnabled"
|
||||
@close="closeChat"
|
||||
:aiChatInputContext="aiChatInputContext"
|
||||
@@ -1602,12 +1602,8 @@ body.ai-chat-open {
|
||||
color: white;
|
||||
}
|
||||
.dark-mode-chat-container {
|
||||
border-left: 1.5px solid #232323ff;
|
||||
box-shadow: -0rem 0.1rem 0.3rem var(--hover-shadow);
|
||||
}
|
||||
.light-mode-chat-container {
|
||||
border-left: 1.5px solid #f7f7f7;
|
||||
box-shadow: -0rem 0.1rem 0.3rem var(--hover-shadow);
|
||||
}
|
||||
|
||||
.ai-btn-active {
|
||||
|
||||
@@ -661,7 +661,7 @@
|
||||
"note": "Automated SRE operations and insights"
|
||||
},
|
||||
"ai_assistant": {
|
||||
"name": "AI Assistant",
|
||||
"name": "O2 Assistant",
|
||||
"note": "Intelligent query and analysis assistance"
|
||||
},
|
||||
"anomaly_detection": {
|
||||
|
||||
@@ -647,7 +647,7 @@
|
||||
"note": "Automated SRE operations and insights"
|
||||
},
|
||||
"ai_assistant": {
|
||||
"name": "AI Assistant",
|
||||
"name": "O2 Assistant",
|
||||
"note": "Intelligent query and analysis assistance"
|
||||
},
|
||||
"anomaly_detection": {
|
||||
|
||||
@@ -12,47 +12,54 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { getAlertPayload } from './alertPayload';
|
||||
|
||||
describe('alertPayload', () => {
|
||||
describe('getAlertPayload', () => {
|
||||
it('should transform form data to API payload', () => {
|
||||
const formData = {
|
||||
name: 'Test Alert',
|
||||
description: 'Test Description',
|
||||
is_real_time: 'false',
|
||||
trigger_condition: {
|
||||
threshold: 100,
|
||||
period: 5,
|
||||
frequency: 60,
|
||||
silence: 300
|
||||
},
|
||||
context_attributes: [
|
||||
{ key: 'env', value: 'production' },
|
||||
{ key: 'service', value: 'api' }
|
||||
],
|
||||
query_condition: {
|
||||
type: 'sql',
|
||||
conditions: [],
|
||||
sql: 'SELECT * FROM logs'
|
||||
},
|
||||
stream_name: 'default',
|
||||
stream_type: 'logs',
|
||||
uuid: 'temp-uuid-123'
|
||||
};
|
||||
const createBaseFormData = () => ({
|
||||
name: 'Test Alert',
|
||||
description: ' Test Description ',
|
||||
is_real_time: 'false',
|
||||
trigger_condition: {
|
||||
threshold: '100',
|
||||
period: '5',
|
||||
frequency: '60',
|
||||
silence: '300'
|
||||
},
|
||||
context_attributes: [
|
||||
{ key: 'env', value: 'production' },
|
||||
{ key: 'service', value: 'api' }
|
||||
],
|
||||
query_condition: {
|
||||
type: 'sql',
|
||||
conditions: [],
|
||||
sql: 'SELECT * FROM logs',
|
||||
aggregation: { field: 'count' },
|
||||
promql_condition: { query: 'up' },
|
||||
vrl_function: ''
|
||||
},
|
||||
stream_name: 'default',
|
||||
stream_type: 'logs',
|
||||
uuid: 'temp-uuid-123'
|
||||
});
|
||||
|
||||
const context = {
|
||||
store: {
|
||||
state: {
|
||||
selectedOrganization: { identifier: 'test-org' },
|
||||
userInfo: { email: 'test@example.com' }
|
||||
}
|
||||
},
|
||||
isAggregationEnabled: { value: false },
|
||||
getSelectedTab: { value: 'sql' },
|
||||
beingUpdated: false
|
||||
};
|
||||
const createBaseContext = (overrides = {}) => ({
|
||||
store: {
|
||||
state: {
|
||||
selectedOrganization: { identifier: 'test-org' },
|
||||
userInfo: { email: 'test@example.com' }
|
||||
}
|
||||
},
|
||||
isAggregationEnabled: { value: false },
|
||||
getSelectedTab: { value: 'sql' },
|
||||
beingUpdated: false,
|
||||
...overrides
|
||||
});
|
||||
|
||||
it('should transform form data to API payload', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext();
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
@@ -64,34 +71,188 @@ describe('alertPayload', () => {
|
||||
expect(payload.context_attributes.env).toBe('production');
|
||||
});
|
||||
|
||||
it('should handle real-time alerts', () => {
|
||||
const formData = {
|
||||
name: 'RT Alert',
|
||||
description: '',
|
||||
is_real_time: 'true',
|
||||
trigger_condition: { threshold: 10, period: 1, frequency: 10, silence: 60 },
|
||||
context_attributes: [],
|
||||
query_condition: { type: 'sql', conditions: [], sql: 'SELECT count(*) FROM logs' },
|
||||
stream_name: 'default',
|
||||
stream_type: 'logs'
|
||||
};
|
||||
it('should convert string trigger conditions to integers', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext();
|
||||
|
||||
const context = {
|
||||
store: {
|
||||
state: {
|
||||
selectedOrganization: { identifier: 'test-org' },
|
||||
userInfo: { email: 'test@example.com' }
|
||||
}
|
||||
},
|
||||
isAggregationEnabled: { value: true },
|
||||
getSelectedTab: { value: 'promql' },
|
||||
beingUpdated: false
|
||||
};
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.trigger_condition.threshold).toBe(100);
|
||||
expect(payload.trigger_condition.period).toBe(5);
|
||||
expect(payload.trigger_condition.frequency).toBe(60);
|
||||
expect(payload.trigger_condition.silence).toBe(300);
|
||||
expect(typeof payload.trigger_condition.threshold).toBe('number');
|
||||
});
|
||||
|
||||
it('should trim the description', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext();
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.description).toBe('Test Description');
|
||||
});
|
||||
|
||||
it('should handle real-time alerts and set type to custom', () => {
|
||||
const formData = createBaseFormData();
|
||||
formData.is_real_time = 'true';
|
||||
const context = createBaseContext();
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.is_real_time).toBe(true);
|
||||
expect(payload.query_condition.type).toBe('custom');
|
||||
});
|
||||
|
||||
it('should clear aggregation when not enabled', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({ isAggregationEnabled: { value: false } });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.aggregation).toBeNull();
|
||||
});
|
||||
|
||||
it('should keep aggregation when enabled and tab is custom', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({
|
||||
isAggregationEnabled: { value: true },
|
||||
getSelectedTab: { value: 'custom' }
|
||||
});
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.aggregation).toEqual({ field: 'count' });
|
||||
});
|
||||
|
||||
it('should clear aggregation when tab is not custom even if enabled', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({
|
||||
isAggregationEnabled: { value: true },
|
||||
getSelectedTab: { value: 'sql' }
|
||||
});
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.aggregation).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear conditions for sql tab', () => {
|
||||
const formData = createBaseFormData();
|
||||
formData.query_condition.conditions = [{ field: 'level', operator: '=', value: 'error' }];
|
||||
const context = createBaseContext({ getSelectedTab: { value: 'sql' } });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.conditions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should clear conditions for promql tab', () => {
|
||||
const formData = createBaseFormData();
|
||||
formData.query_condition.conditions = [{ field: 'level', operator: '=', value: 'error' }];
|
||||
const context = createBaseContext({ getSelectedTab: { value: 'promql' } });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.conditions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should clear promql_condition for sql tab', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({ getSelectedTab: { value: 'sql' } });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.promql_condition).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear promql_condition for custom tab', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({ getSelectedTab: { value: 'custom' } });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.promql_condition).toBeNull();
|
||||
});
|
||||
|
||||
it('should clear sql for promql tab', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({ getSelectedTab: { value: 'promql' } });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.query_condition.sql).toBe('');
|
||||
});
|
||||
|
||||
it('should base64 encode vrl_function when present', () => {
|
||||
const formData = createBaseFormData();
|
||||
formData.query_condition.vrl_function = ' .message = "test" ';
|
||||
const context = createBaseContext();
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
// b64EncodeUnicode uses URL-safe base64 encoding with . instead of =
|
||||
// So btoa('.message = "test"') = "Lm1lc3NhZ2UgPSAidGVzdCI=" becomes "Lm1lc3NhZ2UgPSAidGVzdCI."
|
||||
expect(payload.query_condition.vrl_function).toBe('Lm1lc3NhZ2UgPSAidGVzdCI.');
|
||||
});
|
||||
|
||||
it('should set createdAt and owner for new alerts', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({ beingUpdated: false });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.createdAt).toBeDefined();
|
||||
expect(payload.owner).toBe('test@example.com');
|
||||
expect(payload.lastTriggeredAt).toBeDefined();
|
||||
expect(payload.lastEditedBy).toBe('test@example.com');
|
||||
});
|
||||
|
||||
it('should set updatedAt for existing alerts', () => {
|
||||
const formData = createBaseFormData();
|
||||
const context = createBaseContext({ beingUpdated: true });
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.updatedAt).toBeDefined();
|
||||
expect(payload.lastEditedBy).toBe('test@example.com');
|
||||
expect(payload.createdAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should filter out empty context attributes', () => {
|
||||
const formData = createBaseFormData();
|
||||
formData.context_attributes = [
|
||||
{ key: 'valid', value: 'value' },
|
||||
{ key: '', value: 'no-key' },
|
||||
{ key: 'no-value', value: '' },
|
||||
{ key: ' ', value: ' ' }
|
||||
];
|
||||
const context = createBaseContext();
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(Object.keys(payload.context_attributes)).toEqual(['valid']);
|
||||
});
|
||||
|
||||
it('should handle empty context attributes array', () => {
|
||||
const formData = createBaseFormData();
|
||||
formData.context_attributes = [];
|
||||
const context = createBaseContext();
|
||||
|
||||
const payload = getAlertPayload(formData, context);
|
||||
|
||||
expect(payload.context_attributes).toEqual({});
|
||||
});
|
||||
|
||||
it('should preserve original formData by using deep clone', () => {
|
||||
const formData = createBaseFormData();
|
||||
const originalUuid = formData.uuid;
|
||||
const context = createBaseContext();
|
||||
|
||||
getAlertPayload(formData, context);
|
||||
|
||||
// Original formData should not be modified
|
||||
expect(formData.uuid).toBe(originalUuid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<template>
|
||||
<q-page class="tw:px-[0.625rem] q-pt-xs" style="overflow-y: auto; min-height: inherit; height: calc(100vh - 40px);" :class="store.state.zoConfig.ai_enabled ? 'ai-enabled-home-view q-pb-sm' : ''">
|
||||
<div v-if="!no_data_ingest && !isLoadingSummary" class="tw:w-full tw:h-full tw:px-[0.625rem] tw:py-[0.625rem] card-container" style="display: flex; flex-direction: column; ">
|
||||
<q-page class="tw:px-[0.625rem] q-pt-xs" style="overflow-y: auto; min-height: inherit; height: calc(100vh - 40px);" :class="store.state.isAiChatEnabled ? 'ai-enabled-home-view q-pb-sm' : ''">
|
||||
<div v-if="!no_data_ingest && !isLoadingSummary" class="tw:w-full tw:px-[0.625rem] tw:py-[0.625rem] card-container" :class="store.state.isAiChatEnabled ? 'tw:h-[calc(100% - 40px)]' : 'tw:h-full'" style="display: flex; flex-direction: column; ">
|
||||
<!-- 1st section -->
|
||||
<div>
|
||||
<TrialPeriod></TrialPeriod>
|
||||
@@ -1232,11 +1232,23 @@ export default defineComponent({
|
||||
margin-top: 1rem;
|
||||
align-items: stretch;
|
||||
|
||||
// Responsive: stack on smaller screens
|
||||
@media (max-width: 1280px) {
|
||||
// Stack on smaller screens
|
||||
@media (max-width: 1400px) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
|
||||
// Also stack on mobile
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
}
|
||||
|
||||
// When AI chat is enabled, force vertical stacking (overrides all breakpoints)
|
||||
.ai-enabled-home-view .charts-main-container {
|
||||
grid-template-columns: 1fr !important;
|
||||
grid-template-rows: auto auto auto !important;
|
||||
}
|
||||
|
||||
.functions-dashboards-column {
|
||||
@@ -1245,17 +1257,26 @@ export default defineComponent({
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
|
||||
// On smaller screens, display as a row
|
||||
@media (max-width: 1280px) {
|
||||
// Display as row when stacked
|
||||
@media (max-width: 1400px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
// On mobile, stack vertically again
|
||||
// Stack on mobile
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// When AI chat is enabled, display functions/dashboards in a row (overrides breakpoints)
|
||||
.ai-enabled-home-view .functions-dashboards-column {
|
||||
flex-direction: row !important;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tile-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user