-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Rill Developer: generate dashboards with AI (#4064)
* Scaffolding * Add OpenAI client * Implement initial prompt * Fix lint * Add use_ai config option * Add fallback generator * Re-generate runtime client * Extend overlay with arbitrary component * Create UI action to generate a metrics view with AI * Use "Autogenerate dashboard" action in 4 places * Remove old code * Convey AI in CTAs * Only generate metrics view AI * Uncomment API call * Fix lint * Add request cancellation * Remove generated measures that fail validation * Use new action in Metrics Editor * Fix test util * Use new measure name in test * Clean up overlay code * Take `toggleMenu` out of action * Code cleanups * Nits * Bugfix * Remove old code * Avoid flash of disabled button * Fix metrics editor CTA with dedicated function * Fix trimming of invalid measures --------- Co-authored-by: Benjamin Egelund-Müller <[email protected]> Co-authored-by: Nishant Bangarwa <[email protected]>
- Loading branch information
1 parent
be2a101
commit c5c9e63
Showing
14 changed files
with
329 additions
and
582 deletions.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
web-common/src/features/metrics-views/ai-generation/OptionToCancelAIGeneration.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<script lang="ts"> | ||
export let onCancel: () => void; | ||
</script> | ||
|
||
<div class="flex flex-col gap-y-1"> | ||
<span> This could take up to 20 seconds. </span> | ||
<span> | ||
Prefer not to wait? | ||
<button on:click={onCancel} class="font-semibold text-primary-400" | ||
>Start simple</button | ||
> | ||
</span> | ||
</div> |
217 changes: 217 additions & 0 deletions
217
web-common/src/features/metrics-views/ai-generation/generateMetricsView.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { goto } from "$app/navigation"; | ||
import { get } from "svelte/store"; | ||
import { notifications } from "../../../components/notifications"; | ||
import { appScreen } from "../../../layout/app-store"; | ||
import { overlay } from "../../../layout/overlay-store"; | ||
import { behaviourEvent } from "../../../metrics/initMetrics"; | ||
import type { BehaviourEventMedium } from "../../../metrics/service/BehaviourEventTypes"; | ||
import { | ||
MetricsEventScreenName, | ||
type MetricsEventSpace, | ||
} from "../../../metrics/service/MetricsTypes"; | ||
import { | ||
RuntimeServiceGenerateMetricsViewFileBody, | ||
V1GenerateMetricsViewFileResponse, | ||
runtimeServiceGenerateMetricsViewFile, | ||
runtimeServiceGetFile, | ||
} from "../../../runtime-client"; | ||
import httpClient from "../../../runtime-client/http-client"; | ||
import { useDashboardFileNames } from "../../dashboards/selectors"; | ||
import { getFilePathFromNameAndType } from "../../entity-management/entity-mappers"; | ||
import { getName } from "../../entity-management/name-utils"; | ||
import { EntityType } from "../../entity-management/types"; | ||
import OptionToCancelAIGeneration from "./OptionToCancelAIGeneration.svelte"; | ||
|
||
/** | ||
* TanStack Query does not support mutation cancellation (at least as of v4). | ||
* Here, we create our own version of `runtimeServiceGenerateMetricsViewFile` that accepts an | ||
* AbortSignal, which we can use to cancel the request. | ||
*/ | ||
const runtimeServiceGenerateMetricsViewFileWithSignal = ( | ||
instanceId: string, | ||
runtimeServiceGenerateMetricsViewFileBody: RuntimeServiceGenerateMetricsViewFileBody, | ||
signal: AbortSignal, | ||
) => { | ||
return httpClient<V1GenerateMetricsViewFileResponse>({ | ||
url: `/v1/instances/${instanceId}/files/generate-metrics-view`, | ||
method: "post", | ||
headers: { "Content-Type": "application/json" }, | ||
data: runtimeServiceGenerateMetricsViewFileBody, | ||
signal, | ||
}); | ||
}; | ||
|
||
/** | ||
* Wrapper function that takes care of common UI side effects on top of creating a dashboard from a table. | ||
* | ||
* This function is to be called from all `Generate dashboard with AI` CTAs *outside* of the Metrics Editor. | ||
*/ | ||
export function useCreateDashboardFromTableUIAction( | ||
instanceId: string, | ||
tableName: string, | ||
behaviourEventMedium: BehaviourEventMedium, | ||
metricsEventSpace: MetricsEventSpace, | ||
) { | ||
// Get the list of existing dashboards to generate a unique name | ||
// We call here to avoid: `Error: Function called outside component initialization` | ||
const dashboardNames = useDashboardFileNames(instanceId); | ||
|
||
// Return a function that can be called to create a dashboard from a table | ||
return async () => { | ||
let isAICancelled = false; | ||
const abortController = new AbortController(); | ||
|
||
overlay.set({ | ||
title: "Hang tight! AI is personalizing your dashboard", | ||
detail: { | ||
component: OptionToCancelAIGeneration, | ||
props: { | ||
onCancel: () => { | ||
abortController.abort(); | ||
isAICancelled = true; | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
// Get a unique name | ||
const newDashboardName = getName( | ||
`${tableName}_dashboard`, | ||
get(dashboardNames).data ?? [], | ||
); | ||
const newFilePath = getFilePathFromNameAndType( | ||
newDashboardName, | ||
EntityType.MetricsDefinition, | ||
); | ||
|
||
try { | ||
// First, request an AI-generated dashboard | ||
void runtimeServiceGenerateMetricsViewFileWithSignal( | ||
instanceId, | ||
{ | ||
table: tableName, | ||
path: newFilePath, | ||
useAi: true, | ||
}, | ||
abortController.signal, | ||
); | ||
|
||
// Poll every second until the AI generation is complete or canceled | ||
while (!isAICancelled) { | ||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
|
||
try { | ||
await runtimeServiceGetFile(instanceId, newFilePath); | ||
// success, AI is done | ||
break; | ||
} catch (err) { | ||
// 404 error, AI is not done | ||
} | ||
} | ||
|
||
// If the user canceled the AI request, submit another request with `useAi=false` | ||
if (isAICancelled) { | ||
await runtimeServiceGenerateMetricsViewFile(instanceId, { | ||
table: tableName, | ||
path: newFilePath, | ||
useAi: false, | ||
}); | ||
} | ||
|
||
// Go to dashboard | ||
await goto(`/dashboard/${newDashboardName}`); | ||
void behaviourEvent.fireNavigationEvent( | ||
newDashboardName, | ||
behaviourEventMedium, | ||
metricsEventSpace, | ||
get(appScreen)?.type, | ||
MetricsEventScreenName.Dashboard, | ||
); | ||
} catch (err) { | ||
notifications.send({ | ||
message: "Failed to create a dashboard for " + tableName, | ||
detail: err.response?.data?.message ?? err.message, | ||
}); | ||
} | ||
|
||
// Done, remove the overlay | ||
overlay.set(null); | ||
}; | ||
} | ||
|
||
/** | ||
* Wrapper function that takes care of UI side effects on top of creating a dashboard from a model. | ||
* | ||
* This function is to be called from the `Generate dashboard with AI` CTA *inside* of the Metrics Editor. | ||
*/ | ||
export async function createDashboardFromTableInMetricsEditor( | ||
instanceId: string, | ||
modelName: string, | ||
metricsViewName: string, | ||
) { | ||
const tableName = modelName; | ||
let isAICancelled = false; | ||
const abortController = new AbortController(); | ||
|
||
overlay.set({ | ||
title: "Hang tight! AI is personalizing your dashboard", | ||
detail: { | ||
component: OptionToCancelAIGeneration, | ||
props: { | ||
onCancel: () => { | ||
abortController.abort(); | ||
isAICancelled = true; | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
const filePath = getFilePathFromNameAndType( | ||
metricsViewName, | ||
EntityType.MetricsDefinition, | ||
); | ||
try { | ||
// First, request an AI-generated dashboard | ||
void runtimeServiceGenerateMetricsViewFileWithSignal( | ||
instanceId, | ||
{ | ||
table: tableName, | ||
path: filePath, | ||
useAi: true, | ||
}, | ||
abortController.signal, | ||
); | ||
|
||
// Poll every second until the AI generation is complete or canceled | ||
while (!isAICancelled) { | ||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
|
||
try { | ||
const file = await runtimeServiceGetFile(instanceId, filePath); | ||
if (file.blob !== "") { | ||
// success, AI is done | ||
break; | ||
} | ||
} catch (err) { | ||
// 404 error, AI is not done | ||
} | ||
} | ||
|
||
// If the user canceled the AI request, submit another request with `useAi=false` | ||
if (isAICancelled) { | ||
await runtimeServiceGenerateMetricsViewFile(instanceId, { | ||
table: tableName, | ||
path: filePath, | ||
useAi: false, | ||
}); | ||
} | ||
} catch (err) { | ||
notifications.send({ | ||
message: "Failed to create a dashboard for " + tableName, | ||
detail: err.response?.data?.message ?? err.message, | ||
}); | ||
} | ||
|
||
// Done, remove the overlay | ||
overlay.set(null); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
c5c9e63
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 Published on https://ui.rilldata.in as production
🚀 Deployed on https://65d391560651200f6899e8c9--rill-ui-dev.netlify.app