integrate all changes
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
const { label, initialInput, onClick: onClickProp }: Props = $props()
|
||||
|
||||
const onClick = () => {
|
||||
export function onClick() {
|
||||
globalChatOpen.set(true)
|
||||
if (initialInput) {
|
||||
globalChatInitialInput.set(initialInput)
|
||||
|
||||
@@ -1,112 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { Send, Loader2, Plus } from 'lucide-svelte'
|
||||
import { chatRequest, prepareSystemMessage } from './core'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
|
||||
import { globalChatInitialInput, userStore, copilotInfo } from '$lib/stores'
|
||||
import { userStore, copilotInfo } from '$lib/stores'
|
||||
import AiChat from '../copilot/chat/AIChat.svelte'
|
||||
import { base } from '$lib/base'
|
||||
|
||||
const isAdmin = $derived($userStore?.is_admin || $userStore?.is_super_admin)
|
||||
const hasCopilot = $derived($copilotInfo.enabled)
|
||||
|
||||
const firstMessage = $derived(
|
||||
const disabledMessage = $derived(
|
||||
hasCopilot
|
||||
? 'Hello! I am your global assistant. How can I help you today?'
|
||||
? ''
|
||||
: isAdmin
|
||||
? 'Enable Windmill AI in your workspace settings to use this chat'
|
||||
? `Enable Windmill AI in your [workspace settings](${base}/workspace_settings?tab=ai) to use this chat`
|
||||
: 'Ask an admin to enable Windmill AI in this workspace to use this chat'
|
||||
)
|
||||
|
||||
let inputValue = $state('')
|
||||
let isSubmitting = $state(false)
|
||||
let currentReply = $state('')
|
||||
let messages = $state<ChatCompletionMessageParam[]>([])
|
||||
|
||||
// Suggested questions for the user
|
||||
const suggestions = [
|
||||
'Where can i see my latest runs?',
|
||||
'How do i trigger a script with a webhook endpoint?',
|
||||
'How can I connect to a database?',
|
||||
'How do I schedule a recurring job?'
|
||||
]
|
||||
|
||||
// Check if there are any user messages
|
||||
const hasUserMessages = $derived(messages.some((msg) => msg.role === 'user'))
|
||||
|
||||
let abortController = new AbortController()
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!inputValue.trim()) return
|
||||
|
||||
isSubmitting = true
|
||||
currentReply = ''
|
||||
|
||||
const userMessage = inputValue
|
||||
const systemMessage = prepareSystemMessage()
|
||||
|
||||
messages = [...messages, { role: 'user', content: userMessage }]
|
||||
|
||||
const apiMessages = [systemMessage, ...messages]
|
||||
|
||||
inputValue = ''
|
||||
|
||||
await chatRequest(apiMessages, abortController, (token) => {
|
||||
currentReply = currentReply + token
|
||||
})
|
||||
|
||||
messages = [...messages, { role: 'assistant', content: currentReply }]
|
||||
|
||||
isSubmitting = false
|
||||
}
|
||||
|
||||
function submitSuggestion(suggestion: string) {
|
||||
inputValue = suggestion
|
||||
handleSubmit()
|
||||
}
|
||||
|
||||
function renderMarkdownLinks(content: string) {
|
||||
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
|
||||
return content.replace(
|
||||
linkRegex,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-400 dark:text-blue-300 hover:underline">$1</a>'
|
||||
)
|
||||
}
|
||||
|
||||
function resetChat() {
|
||||
console.log('resetChat')
|
||||
abortController.abort()
|
||||
abortController = new AbortController()
|
||||
|
||||
messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: firstMessage
|
||||
}
|
||||
]
|
||||
inputValue = ''
|
||||
currentReply = ''
|
||||
isSubmitting = false
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if ($globalChatInitialInput.length > 0) {
|
||||
inputValue = $globalChatInitialInput
|
||||
globalChatInitialInput.set('')
|
||||
handleSubmit()
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: firstMessage
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="relative flex flex-col h-full bg-surface z-20">
|
||||
<AiChat navigatorMode />
|
||||
<AiChat navigatorMode disabled={!hasCopilot} {disabledMessage} {suggestions} />
|
||||
</div>
|
||||
|
||||
@@ -30,15 +30,13 @@
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionSystemMessageParam
|
||||
} from 'openai/resources/index.mjs'
|
||||
<<<<<<< HEAD
|
||||
import { chatMode, copilotSessionModel, dbSchemas, workspaceStore } from '$lib/stores'
|
||||
=======
|
||||
import {
|
||||
navigatorTools,
|
||||
prepareNavigatorSystemMessage,
|
||||
prepareNavigatorUserMessage
|
||||
} from './navigator/core'
|
||||
>>>>>>> 332040744 (integrate navigator mode)
|
||||
import { globalChatInitialInput } from '$lib/stores'
|
||||
interface Props {
|
||||
scriptOptions?: ScriptOptions
|
||||
flowHelpers?: FlowAIChatHelpers & {
|
||||
@@ -49,6 +47,9 @@
|
||||
applyCode?: (code: string) => void
|
||||
headerLeft?: Snippet
|
||||
headerRight?: Snippet
|
||||
suggestions?: string[]
|
||||
disabled?: boolean
|
||||
disabledMessage?: string
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -58,7 +59,10 @@
|
||||
showDiffMode,
|
||||
headerLeft,
|
||||
headerRight,
|
||||
navigatorMode = false
|
||||
navigatorMode = false,
|
||||
disabled = false,
|
||||
disabledMessage = '',
|
||||
suggestions = []
|
||||
}: Props = $props()
|
||||
|
||||
let instructions = $state('')
|
||||
@@ -69,23 +73,14 @@
|
||||
flow: flowHelpers !== undefined,
|
||||
navigator: navigatorMode
|
||||
})
|
||||
<<<<<<< HEAD
|
||||
|
||||
async function updateMode(currentMode: 'script' | 'flow') {
|
||||
if (!allowedModes[currentMode]) {
|
||||
chatMode.set(currentMode === 'script' ? 'flow' : 'script')
|
||||
=======
|
||||
let mode: 'script' | 'flow' | 'navigator' = $state(
|
||||
flowHelpers ? 'flow' : scriptOptions ? 'script' : 'navigator'
|
||||
)
|
||||
|
||||
async function updateMode(currentMode: 'script' | 'flow' | 'navigator') {
|
||||
if (!allowedModes[currentMode] && Object.keys(allowedModes).length === 1) {
|
||||
const firstKey = Object.keys(allowedModes)[0]
|
||||
mode = firstKey as 'script' | 'flow' | 'navigator'
|
||||
>>>>>>> 332040744 (integrate navigator mode)
|
||||
chatMode.set(firstKey as 'script' | 'flow' | 'navigator')
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
updateMode(untrack(() => $chatMode))
|
||||
})
|
||||
@@ -94,6 +89,14 @@
|
||||
let abortController: AbortController | undefined = undefined
|
||||
let messages: ChatCompletionMessageParam[] = $state([])
|
||||
|
||||
$effect(() => {
|
||||
if ($globalChatInitialInput.length > 0) {
|
||||
instructions = $globalChatInitialInput
|
||||
globalChatInitialInput.set('')
|
||||
sendRequest()
|
||||
}
|
||||
})
|
||||
|
||||
setContext<AIChatContext>('AIChatContext', {
|
||||
loading,
|
||||
currentReply,
|
||||
@@ -141,15 +144,11 @@
|
||||
instructions = ''
|
||||
|
||||
const systemMessage =
|
||||
<<<<<<< HEAD
|
||||
$chatMode === 'script' ? prepareScriptSystemMessage() : prepareFlowSystemMessage()
|
||||
=======
|
||||
mode === 'script'
|
||||
$chatMode === 'script'
|
||||
? prepareScriptSystemMessage()
|
||||
: mode === 'flow'
|
||||
: $chatMode === 'flow'
|
||||
? prepareFlowSystemMessage()
|
||||
: prepareNavigatorSystemMessage()
|
||||
>>>>>>> 332040744 (integrate navigator mode)
|
||||
|
||||
if ($chatMode === 'flow' && !flowHelpers) {
|
||||
throw new Error('No flow helpers passed')
|
||||
@@ -402,4 +401,7 @@
|
||||
!!scriptOptions.lastDeployedCode &&
|
||||
scriptOptions.lastDeployedCode !== scriptOptions.code}
|
||||
diffMode={scriptOptions?.diffMode ?? false}
|
||||
{disabled}
|
||||
{disabledMessage}
|
||||
{suggestions}
|
||||
></AIChatDisplay>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import ProviderModelSelector from './ProviderModelSelector.svelte'
|
||||
import ChatMode from './ChatMode.svelte'
|
||||
import { chatMode } from '$lib/stores'
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
|
||||
let {
|
||||
allowedModes,
|
||||
@@ -32,7 +33,10 @@
|
||||
cancel,
|
||||
askAi = () => {}, // todo: remove default,
|
||||
headerLeft,
|
||||
headerRight
|
||||
headerRight,
|
||||
disabled = false,
|
||||
disabledMessage = '',
|
||||
suggestions = []
|
||||
}: {
|
||||
allowedModes: {
|
||||
script: boolean
|
||||
@@ -54,6 +58,9 @@
|
||||
askAi?: (instructions: string, options?: { withCode?: boolean; withDiff?: boolean }) => void
|
||||
headerLeft?: Snippet
|
||||
headerRight?: Snippet
|
||||
disabled?: boolean
|
||||
disabledMessage?: string
|
||||
suggestions?: string[]
|
||||
} = $props()
|
||||
|
||||
const { loading, currentReply } = getContext<AIChatContext>('AIChatContext')
|
||||
@@ -106,6 +113,11 @@
|
||||
return -1
|
||||
}
|
||||
|
||||
function submitSuggestion(suggestion: string) {
|
||||
instructions = suggestion
|
||||
sendRequest()
|
||||
}
|
||||
|
||||
const lastUserMessageIndex = $derived(findLastIndex(messages, (m) => m.role === 'user'))
|
||||
</script>
|
||||
|
||||
@@ -294,6 +306,7 @@
|
||||
}
|
||||
}}
|
||||
on:updateInstructions={(e) => (instructions = e.detail.value)}
|
||||
{disabled}
|
||||
/>
|
||||
{:else}
|
||||
<div class="relative w-full px-2 scroll-pb-2 pt-2">
|
||||
@@ -309,6 +322,7 @@
|
||||
rows={3}
|
||||
placeholder={messages.length === 0 ? 'Ask anything' : 'Ask followup'}
|
||||
class="resize-none"
|
||||
{disabled}
|
||||
></textarea>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -320,10 +334,32 @@
|
||||
{#if $chatMode === 'script' && hasDiff}
|
||||
<ChatQuickActions {askAi} {diffMode} />
|
||||
{/if}
|
||||
<div class="flex flex-row gap-2 min-w-0">
|
||||
<ChatMode {allowedModes} />
|
||||
<ProviderModelSelector />
|
||||
</div>
|
||||
{#if disabled}
|
||||
<div class="text-tertiary text-xs mt-2 px-2">
|
||||
<Markdown md={disabledMessage} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row gap-2 min-w-0">
|
||||
<ChatMode {allowedModes} />
|
||||
<ProviderModelSelector />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if suggestions.length > 0 && messages.filter((m) => m.role === 'user').length === 0 && !disabled}
|
||||
<div class="px-2 mt-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each suggestions as suggestion}
|
||||
<Button
|
||||
on:click={() => submitSuggestion(suggestion)}
|
||||
size="xs2"
|
||||
color="light"
|
||||
btnClasses="whitespace-normal text-center font-normal"
|
||||
>
|
||||
{suggestion}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { gfmPlugin } from 'svelte-exmarkdown/gfm'
|
||||
import type { DisplayMessage } from './shared'
|
||||
import CodeDisplay from './script/CodeDisplay.svelte'
|
||||
import LinkRenderer from './LinkRenderer.svelte'
|
||||
import { setContext } from 'svelte'
|
||||
|
||||
export let message: DisplayMessage
|
||||
@@ -21,7 +22,8 @@
|
||||
gfmPlugin(),
|
||||
{
|
||||
renderer: {
|
||||
pre: CodeDisplay
|
||||
pre: CodeDisplay,
|
||||
a: LinkRenderer
|
||||
}
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
export let availableContext: ContextElement[]
|
||||
export let selectedContext: ContextElement[]
|
||||
export let isFirstMessage: boolean
|
||||
export let disabled: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
updateInstructions: { value: string }
|
||||
@@ -319,6 +320,7 @@
|
||||
style={instructions.length > 0
|
||||
? 'color: transparent; -webkit-text-fill-color: transparent;'
|
||||
: ''}
|
||||
{disabled}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
15
frontend/src/lib/components/copilot/chat/LinkRenderer.svelte
Normal file
15
frontend/src/lib/components/copilot/chat/LinkRenderer.svelte
Normal file
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
type Props = {
|
||||
href?: string
|
||||
children?: Snippet
|
||||
}
|
||||
let { href, children }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {href} target="_blank" rel="noopener noreferrer">
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
@@ -189,7 +189,7 @@
|
||||
color="dark"
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
applyCode(code ?? '')
|
||||
applyCode?.(code ?? '')
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
|
||||
@@ -239,6 +239,7 @@
|
||||
let defaultMenuItemLabels = defaultMenuItems.map((item) => item.label)
|
||||
let defaultMenuItemAndHiddenLabels = defaultMenuItemsWithHidden.map((item) => item.label)
|
||||
let switchModeItemLabels = switchModeItems.map((item) => item.label)
|
||||
let askAiButton: AskAiButton | undefined
|
||||
|
||||
function fuzzyFilter(filter: string, items: any[], itemsPlainText: string[]) {
|
||||
if (filter === '') {
|
||||
@@ -370,6 +371,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((itemMap[tab] ?? []).length === 0 && searchTerm.length > 0 && event.key === 'Enter') {
|
||||
askAiButton?.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +621,7 @@
|
||||
</div>
|
||||
{#if (itemMap[tab] ?? []).length === 0 && searchTerm.length > 0}
|
||||
<AskAiButton
|
||||
bind:this={askAiButton}
|
||||
label="Ask AI"
|
||||
initialInput={searchTerm}
|
||||
onClick={() => {
|
||||
|
||||
Reference in New Issue
Block a user