feat: test openai key + improve AI UI (#2465)

This commit is contained in:
HugoCasa
2023-10-19 16:15:18 +02:00
committed by GitHub
parent b91b133e0e
commit f7ecbb9de7
12 changed files with 232 additions and 95 deletions

View File

@@ -150,21 +150,24 @@
<div class="flex gap-2 flex-wrap">
<div class="flex justify-start">
<ToggleButtonGroup bind:selected={searchKind} class="h-10">
<ToggleButton small value="all" label={'All' + counts.all} />
<ToggleButton small light value="all" label={'All' + counts.all} />
<ToggleButton
small
light
value="scripts"
icon={Code2}
label={'Scripts' + counts.scripts}
/>
<ToggleButton
small
light
value="resources"
icon={Boxes}
label={'Resources' + counts.resources}
/>
<ToggleButton
small
light
value="flows"
label={'Flows' + counts.flows}
icon={FlowIcon}
@@ -172,6 +175,7 @@
/>
<ToggleButton
small
light
value="apps"
label={'Apps' + counts.apps}
icon={LayoutDashboard}

View File

@@ -220,7 +220,7 @@
label={'${}'}
/>
{:else}
<ToggleButton small label="Static" value="static" />
<ToggleButton small light label="Static" value="static" />
{/if}
<ToggleButton

View File

@@ -36,7 +36,7 @@
{/if}
</div>
<div class="ml-3 flex-1 w-0">
<p class="text-sm text-secondary">{message}</p>
<p class="text-sm text-secondary break-words">{message}</p>
{#if errorMessage}
<p
class="text-sm text-secondary border bg-surface-secondary p-2 w-full overflow-auto mt-2"

View File

@@ -24,6 +24,7 @@
export let element: ButtonType.Element | undefined = undefined
export let id: string = ''
export let nonCaptureEvent: boolean = false
export let propagateEvent: boolean = false
export let loading = false
export let title: string | undefined = undefined
export let style: string = ''
@@ -99,7 +100,10 @@
async function onClick(event: MouseEvent) {
if (!nonCaptureEvent) {
event.preventDefault()
event.stopPropagation()
if (!propagateEvent) {
// by default events are not propagated, added this prop so that we can
event.stopPropagation()
}
dispatch('click', event)
}
}

View File

@@ -11,6 +11,8 @@
export let containerClasses: string = 'rounded-lg shadow-md border p-4 bg-surface'
const [floatingRef, floatingContent] = createFloatingActions(floatingConfig)
export let blockOpen = false
</script>
<Popover on:close>
@@ -22,6 +24,7 @@
<Portal>
<div use:floatingContent class="z5000">
<Transition
show={blockOpen || undefined}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
@@ -29,7 +32,7 @@
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel let:close>
<PopoverPanel let:close static={blockOpen}>
<div class={containerClasses}>
<slot {close} />
</div>

View File

@@ -14,7 +14,8 @@
export let icon: any | undefined = undefined
export let disabled: boolean = false
export let selectedColor: string = '#3b82f6'
export let small: boolean = false
export let small = false
export let light = false
export let iconProps: Record<string, any> = {}
export let showTooltipIcon: boolean = false
export let documentationLink: string | undefined = undefined
@@ -35,7 +36,8 @@
{disabled}
class={twMerge(
' rounded-md transition-all text-xs flex gap-1 flex-row items-center',
small ? 'px-1 py-0.5' : 'px-2 py-1',
small ? 'px-1.5 py-0.5 text-2xs' : 'px-2 py-1',
light ? 'font-medium' : '',
$selected === value
? 'bg-surface shadow-md'
: 'bg-surface-secondary hover:bg-surface-hover',

View File

@@ -11,6 +11,7 @@
import type { FlowModule } from '$lib/gen'
import type { FlowEditorContext } from '../flows/types'
import { ExternalLink } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
export let copilotLoading: boolean
export let copilotStatus: string
@@ -34,7 +35,7 @@
<ManualPopover bind:this={copilotPopover}>
<Button
size="xs"
btnClasses={'mr-2' + ($currentStepStore !== undefined ? 'z-[901]' : '')}
btnClasses={twMerge('mr-2', $currentStepStore !== undefined ? 'z-[901]' : '')}
on:click={() => {
if (copilotLoading || ($currentStepStore !== undefined && $currentStepStore !== 'Input')) {
abortController?.abort()

View File

@@ -16,7 +16,6 @@
import { dbSchemas, copilotInfo, type DBSchema } from '$lib/stores'
import type DiffEditor from '../DiffEditor.svelte'
import { scriptLangToEditorLang } from '$lib/scripts'
import type { Selection } from 'monaco-editor/esm/vs/editor/editor.api'
import type SimpleEditor from '../SimpleEditor.svelte'
import Tooltip from '../Tooltip.svelte'
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
@@ -28,6 +27,8 @@
import { sleep } from '$lib/utils'
import { autoPlacement } from '@floating-ui/core'
import { ExternalLink } from 'lucide-svelte'
import { fade } from 'svelte/transition'
import { isInitialCode } from '$lib/script_helpers'
// props
export let iconOnly: boolean = false
@@ -42,10 +43,10 @@
let genLoading: boolean = false
let input: HTMLInputElement | undefined
let generatedCode = writable<string>('')
let selection: Selection | undefined
let isEdit = false
let dbSchema: DBSchema | undefined = undefined
let abortController: AbortController | undefined = undefined
let blockPopupOpen = false
let mode: 'gen' | 'edit' = 'gen'
let button: HTMLButtonElement | undefined
@@ -55,23 +56,20 @@
}
try {
genLoading = true
blockPopupOpen = true
abortController = new AbortController()
if (isEdit && selection) {
const selectedCode = editor?.getSelectedLines() || ''
const originalCode = editor?.getCode() || ''
if (mode === 'edit') {
await copilot(
{
language: lang,
description: funcDesc,
code: selectedCode,
code: editor?.getCode() || '',
dbSchema: dbSchema,
type: 'edit'
},
generatedCode,
abortController
)
setupDiff()
diffEditor?.setModified(originalCode.replace(selectedCode, $generatedCode + '\n'))
} else {
await copilot(
{
@@ -83,9 +81,10 @@
generatedCode,
abortController
)
setupDiff()
diffEditor?.setModified($generatedCode)
}
setupDiff()
diffEditor?.setModified($generatedCode)
blockPopupOpen = false
await sleep(500)
closePopup()
await sleep(300)
@@ -128,12 +127,6 @@
diffEditor?.hide()
}
function setSelectionHandler() {
editor?.onDidChangeCursorSelection((e) => {
selection = e.selection
})
}
$: input?.focus()
function clear() {
@@ -143,18 +136,28 @@
$: lang && clear()
$: !$generatedCode && hideDiff()
$: editor && setSelectionHandler()
$: selection && (isEdit = !selection.isEmpty())
function updateSchema(lang, args) {
function updateSchema(lang, args, dbSchemas) {
const schemaRes = lang === 'graphql' ? args.api : args.database
if (typeof schemaRes === 'string') {
dbSchema = $dbSchemas[schemaRes.replace('$res:', '')]
const schemaPath = schemaRes.replace('$res:', '')
if (schemaPath in dbSchemas && dbSchemas[schemaPath].lang === lang) {
dbSchema = dbSchemas[schemaPath]
} else {
dbSchema = undefined
}
} else {
dbSchema = undefined
}
}
$: updateSchema(lang, args)
$: updateSchema(lang, args, $dbSchemas)
</script>
{#if genLoading}
<div transition:fade class="fixed z-[4999] inset-0 bg-gray-500/75" />
{/if}
{#if $generatedCode.length > 0 && !genLoading}
{#if inlineScript}
<div class="flex gap-1">
@@ -214,6 +217,7 @@
]
}}
let:close
blockOpen={blockPopupOpen}
>
<svelte:fragment slot="button">
{#if inlineScript}
@@ -234,13 +238,23 @@
{:else}
<Button
title="Generate code from prompt"
btnClasses="!font-medium"
btnClasses={'!font-medium ' + (genLoading ? 'z-[5000]' : '')}
size="xs"
color={genLoading ? 'red' : 'light'}
spacingSize="md"
startIcon={genLoading ? undefined : { icon: faMagicWandSparkles }}
nonCaptureEvent={!genLoading}
on:click={genLoading ? () => abortController?.abort() : undefined}
propagateEvent
on:click={genLoading
? () => abortController?.abort()
: () => {
if (editor) {
if (isInitialCode(editor.getCode())) {
mode = 'gen'
} else {
mode = 'edit'
}
}
}}
bind:element={button}
>
{#if genLoading}
@@ -253,7 +267,7 @@
/>
Stop
{:else}
{isEdit ? 'AI Edit' : 'AI Gen'}
AI
{/if}
</Button>
{/if}
@@ -270,50 +284,59 @@
{/if}
</div>
{:else if $copilotInfo.exists_openai_resource_path}
<div class="flex w-96">
<input
type="text"
bind:this={input}
bind:value={funcDesc}
on:keypress={({ key }) => {
if (key === 'Enter' && funcDesc.length > 0) {
<div class="flex flex-col gap-4">
<ToggleButtonGroup class="w-auto shrink-0" bind:selected={mode}>
<ToggleButton value={'gen'} label="Generate from scratch" small light />
<ToggleButton value={'edit'} label="Edit existing code" small light />
</ToggleButtonGroup>
<div class="flex w-96">
<input
type="text"
bind:this={input}
bind:value={funcDesc}
on:keypress={({ key }) => {
if (key === 'Enter' && funcDesc.length > 0) {
onGenerate(() => close(input || null))
}
}}
placeholder={mode === 'edit'
? 'Describe the changes you want'
: 'Describe what the script should do'}
/>
<Button
size="xs"
color="blue"
buttonType="button"
btnClasses="!p-1 !w-[38px] !ml-2"
aria-label="Generate"
on:click={() => {
onGenerate(() => close(input || null))
}
}}
placeholder={isEdit
? 'Describe the changes you want'
: 'Describe what the script should do'}
/>
<Button
size="xs"
color="blue"
buttonType="button"
btnClasses="!p-1 !w-[38px] !ml-2"
aria-label="Generate"
on:click={() => {
onGenerate(() => close(input || null))
}}
disabled={funcDesc.length <= 0}
>
<Icon data={faMagicWandSparkles} />
</Button>
</div>
{#if ['postgresql', 'mysql', 'snowflake', 'bigquery', 'graphql'].includes(lang) && dbSchema?.lang === lang}
<div class="flex flex-row items-center justify-between w-96 mt-2">
<p class="text-sm">
Will take into account the DB schema
<Tooltip>
In order to better generate the script, we pass the selected DB schema to GPT-4.
</Tooltip>
</p>
{#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC)}
<ToggleButtonGroup class="w-auto shrink-0" bind:selected={dbSchema.publicOnly}>
<ToggleButton value={true} label="Public schema" />
<ToggleButton value={false} label="All schemas" />
</ToggleButtonGroup>
{/if}
}}
disabled={funcDesc.length <= 0}
>
<Icon data={faMagicWandSparkles} />
</Button>
</div>
{/if}
{#if ['postgresql', 'mysql', 'snowflake', 'bigquery', 'graphql'].includes(lang) && dbSchema?.lang === lang}
<div class="flex flex-row items-center justify-between gap-2 w-96">
<div class="flex flex-row items-center">
<p class="text-xs text-secondary">
Context: {lang === 'graphql' ? 'GraphQL' : 'DB'} schema
</p>
<Tooltip>
In order to better generate the script, we pass the selected schema to GPT-4.
</Tooltip>
</div>
{#if dbSchema.lang !== 'graphql' && (dbSchema.schema?.public || dbSchema.schema?.PUBLIC)}
<ToggleButtonGroup class="w-auto shrink-0" bind:selected={dbSchema.publicOnly}>
<ToggleButton value={true} label="Public schema" small light />
<ToggleButton value={false} label="All schemas" small light />
</ToggleButtonGroup>
{/if}
</div>
{/if}</div
>
{:else}
<p class="text-sm"
>Enable Windmill AI in the <a

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { sendUserToast } from '$lib/toast'
import Button from '../common/button/Button.svelte'
import { testKey } from './lib'
export let disabled = false
export let apiKey: string | undefined = undefined
let loading = false
</script>
<Button
size="sm"
variant="contained"
color="dark"
{disabled}
{loading}
on:click={async () => {
loading = true
try {
const abortController = new AbortController()
setTimeout(() => {
abortController.abort()
}, 10000)
await testKey({
apiKey,
messages: [
{
role: 'user',
content: "this is a test, simply reply with 'ok'"
}
],
abortController
})
sendUserToast('Valid key')
} catch (err) {
if (err.message === 'Request was aborted.') {
sendUserToast('Could not validate key within 10s', true)
} else {
sendUserToast(`Invalid key: ${err}`, true)
}
} finally {
loading = false
}
}}>Test key</Button
>

View File

@@ -26,6 +26,35 @@ const openaiConfig: CompletionCreateParamsStreaming = {
let workspace: string | undefined = undefined
let openai: OpenAI | undefined = undefined
export async function testKey({
apiKey,
abortController,
messages
}: {
apiKey?: string
messages: CreateChatCompletionRequestMessage[]
abortController: AbortController
}) {
if (apiKey) {
const openai = new OpenAI({
apiKey,
dangerouslyAllowBrowser: true
})
await openai.chat.completions.create(
{
...openaiConfig,
messages,
stream: false
},
{
signal: abortController.signal
}
)
} else {
await getNonStreamingCompletion(messages, abortController)
}
}
workspaceStore.subscribe(async (value) => {
workspace = value
const baseURL = `${location.origin}${OpenAPI.BASE}/w/${workspace}/openai/proxy`

View File

@@ -11,6 +11,7 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import { onMount } from 'svelte'
import { sendUserToast } from '$lib/toast'
import TestOpenaiKey from '$lib/components/copilot/TestOpenaiKey.svelte'
const rd = $page.url.searchParams.get('rd')
@@ -21,6 +22,7 @@
let errorId = ''
let errorUser = ''
let openAiKey = ''
let codeCompletionEnabled = true
let checking = false
$: id = name.toLowerCase().replace(/\s/gi, '-')
@@ -69,7 +71,7 @@
})
await WorkspaceService.editCopilotConfig({
workspace: id,
requestBody: { openai_resource_path: path, code_completion_enabled: false }
requestBody: { openai_resource_path: path, code_completion_enabled: codeCompletionEnabled }
})
}
@@ -150,12 +152,29 @@
{/if}
</label>
<label class="block pb-4">
<span class="text-secondary text-sm"
>OpenAI key for codegen<span class="text-2xs text-tertiary ml-2"
>(optional but recommended)</span
></span
>
<input type="password" bind:value={openAiKey} on:keyup={handleKeyUp} />
<span class="text-secondary text-sm">
OpenAI key for Windmill AI
<Tooltip>
Find out how it can help you <a
href="https://www.windmill.dev/docs/core_concepts/ai_generation"
target="_blank"
rel="noopener noreferrer">in the docs</a
>
</Tooltip>
<span class="text-2xs text-tertiary ml-2">(optional but recommended)</span>
</span>
<div class="flex flex-row gap-1">
<input type="password" bind:value={openAiKey} on:keyup={handleKeyUp} />
<TestOpenaiKey apiKey={openAiKey} disabled={!openAiKey} />
</div>
{#if openAiKey}
<Toggle
disabled={!openAiKey}
size="xs"
bind:checked={codeCompletionEnabled}
options={{ right: 'Enable code completion' }}
/>
{/if}
</label>
<Toggle
disabled={!isDomainAllowed}

View File

@@ -31,6 +31,7 @@
import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import TestOpenaiKey from '$lib/components/copilot/TestOpenaiKey.svelte'
const slackErrorHandler = 'hub/2431/slack/schedule-error-handler-slack'
@@ -168,7 +169,10 @@
if (emptyString($enterpriseLicense)) {
errorHandlerSelected = 'custom'
} else {
errorHandlerSelected = emptyString(errorHandlerScriptPath) || errorHandlerScriptPath === slackErrorHandler ? 'slack' : 'custom'
errorHandlerSelected =
emptyString(errorHandlerScriptPath) || errorHandlerScriptPath === slackErrorHandler
? 'slack'
: 'custom'
}
errorHandlerExtraArgs = settings.error_handler_extra_args ?? {}
codeCompletionEnabled = settings.code_completion_enabled
@@ -184,18 +188,18 @@
if (errorHandlerScriptPath) {
await WorkspaceService.editErrorHandler({
workspace: $workspaceStore!,
requestBody: {
requestBody: {
error_handler: `${errorHandlerItemKind}/${errorHandlerScriptPath}`,
error_handler_extra_args: errorHandlerExtraArgs,
error_handler_extra_args: errorHandlerExtraArgs
}
})
sendUserToast(`workspace error handler set to ${errorHandlerScriptPath}`)
} else {
await WorkspaceService.editErrorHandler({
workspace: $workspaceStore!,
requestBody: {
requestBody: {
error_handler: undefined,
error_handler_extra_args: undefined,
error_handler_extra_args: undefined
}
})
sendUserToast(`workspace error handler removed`)
@@ -438,7 +442,7 @@
</div>
{:else if tab == 'error_handler'}
<PageHeader title="Script to run as error handler" primary={false} />
<ErrorOrRecoveryHandler
isEditable={true}
handlersOnlyForEe={['slack']}
@@ -458,15 +462,17 @@
The following args will be passed to the error handler:
<ul class="mt-1 ml-2">
<li><b>path</b>: The path of the script or flow that errored.</li>
<li><b>email</b>: The email of the user who ran the script or flow that errored.</li>
<li>
<b>email</b>: The email of the user who ran the script or flow that errored.
</li>
<li><b>error</b>: The error details.</li>
<li><b>job_id</b>: The job id.</li>
<li><b>is_flow</b>: Whether the error comes from a flow.</li>
<li><b>workspace_id</b>: The workspace id of the failed script or flow.</li>
</ul>
<br />
The error handler will be executed by the automatically created group g/error_handler. If
your error handler requires variables or resources, you need to add them to the group.
The error handler will be executed by the automatically created group g/error_handler.
If your error handler requires variables or resources, you need to add them to the group.
</div>
</div>
</Tooltip>
@@ -475,14 +481,15 @@
<div class="flex mt-5 justify-start">
<Button
disabled={(errorHandlerSelected === 'slack' && !emptyString(errorHandlerScriptPath) && emptyString(errorHandlerExtraArgs['channel']))}
disabled={errorHandlerSelected === 'slack' &&
!emptyString(errorHandlerScriptPath) &&
emptyString(errorHandlerExtraArgs['channel'])}
size="sm"
on:click={editErrorHandler}
>
Save
</Button>
</div>
{:else if tab == 'openai'}
<PageHeader title="Windmill AI" primary={false} />
<div class="mt-2">
@@ -491,7 +498,7 @@
features.
</Alert>
</div>
<div class="mt-5">
<div class="mt-5 flex gap-1">
{#key openaiResourceInitialPath}
<ResourcePicker
resourceType="openai"
@@ -501,6 +508,7 @@
}}
/>
{/key}
<TestOpenaiKey disabled={!openaiResourceInitialPath} />
</div>
<div class="mt-3">
<Toggle