integrate all changes

This commit is contained in:
centdix
2025-06-02 12:05:17 +02:00
parent 6e4fdd6e18
commit af37a9332e
9 changed files with 97 additions and 121 deletions

View File

@@ -11,7 +11,7 @@
const { label, initialInput, onClick: onClickProp }: Props = $props()
const onClick = () => {
export function onClick() {
globalChatOpen.set(true)
if (initialInput) {
globalChatInitialInput.set(initialInput)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}
]}

View File

@@ -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>

View 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}

View File

@@ -189,7 +189,7 @@
color="dark"
size="xs2"
on:click={() => {
applyCode(code ?? '')
applyCode?.(code ?? '')
}}
>
Apply

View File

@@ -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={() => {