mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 00:06:06 +00:00
feat(aichat): add user-level custom system prompts (#6884)
* feat(aichat): create reusable CustomAIPrompts component Extract custom AI prompts UI into a reusable component that can be used in both workspace settings and user settings. Component includes: - AI mode selector with visual indicators for set prompts - Textarea with character limit - Customizable title, description, and hint messages Co-authored-by: centdix <centdix@users.noreply.github.com> * refactor(aichat): update workspace AISettings to use reusable component Replace inline custom prompts UI with the reusable CustomAIPrompts component. Add hint about user-level custom prompts being available in account settings and how they combine with workspace prompts. Co-authored-by: centdix <centdix@users.noreply.github.com> * feat(aichat): add user-level custom AI prompts in account settings Add collapsible section in user settings for custom AI prompts: - Stored in localStorage (key: userCustomAIPrompts) - Collapsible UI to save space - Visual indicator when prompts are configured - Hint about prompt combination with workspace settings - Prompts apply across all workspaces for the user Co-authored-by: centdix <centdix@users.noreply.github.com> * feat(aichat): combine workspace and user custom prompts Update AIChatManager to combine workspace-level and user-level custom prompts. Prompts are combined in order: workspace first, then user. Add helper functions in aiStore.ts: - getUserCustomPrompts(): retrieves user prompts from localStorage - getCombinedCustomPrompt(mode): combines workspace + user prompts All AI modes (script, flow, navigator, ask, API) now use combined prompts, allowing users to append their own instructions to workspace settings across all workspaces. Co-authored-by: centdix <centdix@users.noreply.github.com> * fix: remove unused imports Remove unused imports to fix svelte-check errors: - Remove unused 'get' from svelte/store in AIChatManager - Remove unused 'copilotInfo' from aiStore in AIChatManager - Remove unused 'AIMode' from AISettings Co-authored-by: centdix <centdix@users.noreply.github.com> * simplify * nit * fix * fix --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: centdix <centdix@users.noreply.github.com>
This commit is contained in:
co-authored by
centdix
claude[bot]
parent
877af9c845
commit
b5d0f23f4f
@@ -4,6 +4,8 @@ import { type AIProviderModel, type AIProvider, WorkspaceService, type AIConfig
|
||||
import { COPILOT_SESSION_MODEL_SETTING_NAME, COPILOT_SESSION_PROVIDER_SETTING_NAME } from './stores'
|
||||
import { getLocalSetting } from './utils'
|
||||
|
||||
const USER_CUSTOM_PROMPTS_KEY = 'userCustomAIPrompts'
|
||||
|
||||
const sessionModel = getLocalSetting(COPILOT_SESSION_MODEL_SETTING_NAME)
|
||||
const sessionProvider = getLocalSetting(COPILOT_SESSION_PROVIDER_SETTING_NAME)
|
||||
export const copilotSessionModel = writable<AIProviderModel | undefined>(
|
||||
@@ -90,3 +92,30 @@ export function getCurrentModel() {
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
export function getUserCustomPrompts(): Record<string, string> {
|
||||
const stored = getLocalSetting(USER_CUSTOM_PROMPTS_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user custom prompts', e)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
export function getCombinedCustomPrompt(mode: string): string | undefined {
|
||||
const workspacePrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const userPrompts = getUserCustomPrompts()
|
||||
const userPrompt = userPrompts[mode]
|
||||
|
||||
const prompts = [workspacePrompt, userPrompt].filter((p) => p?.trim())
|
||||
|
||||
if (prompts.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return prompts.join('\n\n')
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import UserInfoSettings from './settings/UserInfoSettings.svelte'
|
||||
import AIUserSettings from './settings/AIUserSettings.svelte'
|
||||
import UserAIPromptsSettings from './settings/UserAIPromptsSettings.svelte'
|
||||
|
||||
interface Props {
|
||||
scopes?: string[] | undefined
|
||||
@@ -69,6 +70,7 @@
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<AIUserSettings />
|
||||
<UserAIPromptsSettings />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { AIMode } from './chat/AIChatManager.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Label from '../Label.svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
|
||||
const MAX_CUSTOM_PROMPT_LENGTH = 5000
|
||||
|
||||
let {
|
||||
customPrompts = $bindable(),
|
||||
title,
|
||||
description
|
||||
}: {
|
||||
customPrompts: Record<string, string>
|
||||
title?: string
|
||||
description?: string
|
||||
} = $props()
|
||||
|
||||
let selectedAiMode = $state<AIMode>(AIMode.ASK)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold">{title}</p>
|
||||
{#if description}
|
||||
<p class="text-xs text-secondary">{description}</p>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="AI Mode">
|
||||
<ToggleButtonGroup bind:selected={selectedAiMode}>
|
||||
{#snippet children({ item })}
|
||||
{#each Object.values(AIMode) as mode}
|
||||
<div class="relative">
|
||||
<ToggleButton
|
||||
value={mode}
|
||||
label={mode.charAt(0).toUpperCase() + mode.slice(1)}
|
||||
{item}
|
||||
/>
|
||||
{#if customPrompts[mode]?.length > 0}
|
||||
<div
|
||||
class="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full border border-surface"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
label="Custom system prompt for {selectedAiMode.charAt(0).toUpperCase() +
|
||||
selectedAiMode.slice(1)} Mode"
|
||||
>
|
||||
<textarea
|
||||
bind:value={customPrompts[selectedAiMode]}
|
||||
placeholder="Enter a custom system prompt for {selectedAiMode} mode."
|
||||
class="w-full min-h-24 p-2 border border-gray-200 dark:border-gray-700 rounded-md bg-surface text-primary resize-y"
|
||||
rows="4"
|
||||
maxlength={MAX_CUSTOM_PROMPT_LENGTH}
|
||||
use:autosize
|
||||
></textarea>
|
||||
<div class="flex justify-end mt-1">
|
||||
<span class="text-xs text-secondary">
|
||||
{(customPrompts[selectedAiMode] ?? '').length}/{MAX_CUSTOM_PROMPT_LENGTH} characters
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,6 @@ import { getStringError } from './utils'
|
||||
import type { FlowModuleState, FlowState } from '$lib/components/flows/flowState'
|
||||
import type { CurrentEditor, ExtendedOpenFlow } from '$lib/components/flows/types'
|
||||
import { untrack } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import { type DBSchemas } from '$lib/stores'
|
||||
import { askTools, prepareAskSystemMessage, prepareAskUserMessage } from './ask/core'
|
||||
import { chatState, DEFAULT_SIZE, triggerablesByAi } from './sharedChatState.svelte'
|
||||
@@ -45,7 +44,7 @@ import type AIChatInput from './AIChatInput.svelte'
|
||||
import { prepareApiSystemMessage, prepareApiUserMessage } from './api/core'
|
||||
import { getAnthropicCompletion, parseAnthropicCompletion } from './anthropic'
|
||||
import type { ReviewChangesOpts } from './monaco-adapter'
|
||||
import { copilotInfo, getCurrentModel } from '$lib/aiStore'
|
||||
import { getCurrentModel, getCombinedCustomPrompt } from '$lib/aiStore'
|
||||
|
||||
// If the estimated token usage is greater than the model context window - the threshold, we delete the oldest message
|
||||
const MAX_TOKENS_THRESHOLD_PERCENTAGE = 0.05
|
||||
@@ -127,7 +126,7 @@ class AIChatManager {
|
||||
return (
|
||||
estimatedTokens >
|
||||
modelContextWindow -
|
||||
Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT)
|
||||
Math.max(modelContextWindow * MAX_TOKENS_THRESHOLD_PERCENTAGE, MAX_TOKENS_HARD_LIMIT)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -209,7 +208,7 @@ class AIChatManager {
|
||||
this.mode = mode
|
||||
this.pendingPrompt = pendingPrompt ?? ''
|
||||
if (mode === AIMode.SCRIPT) {
|
||||
const customPrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
const currentModel = getCurrentModel()
|
||||
this.systemMessage = prepareScriptSystemMessage(currentModel, customPrompt)
|
||||
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
|
||||
@@ -236,23 +235,23 @@ class AIChatManager {
|
||||
}
|
||||
}
|
||||
} else if (mode === AIMode.FLOW) {
|
||||
const customPrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
this.systemMessage = prepareFlowSystemMessage(customPrompt)
|
||||
this.systemMessage.content = this.NAVIGATION_SYSTEM_PROMPT + this.systemMessage.content
|
||||
this.tools = [this.changeModeTool, ...flowTools]
|
||||
this.helpers = this.flowAiChatHelpers
|
||||
} else if (mode === AIMode.NAVIGATOR) {
|
||||
const customPrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
this.systemMessage = prepareNavigatorSystemMessage(customPrompt)
|
||||
this.tools = [this.changeModeTool, ...navigatorTools]
|
||||
this.helpers = {}
|
||||
} else if (mode === AIMode.ASK) {
|
||||
const customPrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
this.systemMessage = prepareAskSystemMessage(customPrompt)
|
||||
this.tools = [...askTools]
|
||||
this.helpers = {}
|
||||
} else if (mode === AIMode.API) {
|
||||
const customPrompt = get(copilotInfo).customPrompts?.[mode]
|
||||
const customPrompt = getCombinedCustomPrompt(mode)
|
||||
this.systemMessage = prepareApiSystemMessage(customPrompt)
|
||||
this.tools = [...this.apiTools]
|
||||
this.helpers = {}
|
||||
@@ -480,8 +479,8 @@ class AIChatManager {
|
||||
onNewToken: (token: string) => {
|
||||
reply += token
|
||||
},
|
||||
onMessageEnd: () => { },
|
||||
setToolStatus: () => { }
|
||||
onMessageEnd: () => {},
|
||||
setToolStatus: () => {}
|
||||
},
|
||||
systemMessage
|
||||
}
|
||||
@@ -887,15 +886,15 @@ class AIChatManager {
|
||||
const editorRelated =
|
||||
currentEditor && currentEditor.type === 'script' && currentEditor.stepId === module.id
|
||||
? {
|
||||
diffMode: currentEditor.diffMode,
|
||||
lastDeployedCode: currentEditor.lastDeployedCode,
|
||||
lastSavedCode: undefined
|
||||
}
|
||||
diffMode: currentEditor.diffMode,
|
||||
lastDeployedCode: currentEditor.lastDeployedCode,
|
||||
lastSavedCode: undefined
|
||||
}
|
||||
: {
|
||||
diffMode: false,
|
||||
lastDeployedCode: undefined,
|
||||
lastSavedCode: undefined
|
||||
}
|
||||
diffMode: false,
|
||||
lastDeployedCode: undefined,
|
||||
lastSavedCode: undefined
|
||||
}
|
||||
|
||||
return {
|
||||
args: moduleState?.previewArgs ?? {},
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import { storeLocalSetting } from '$lib/utils'
|
||||
import CustomAIPrompts from '../copilot/CustomAIPrompts.svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getUserCustomPrompts } from '$lib/aiStore'
|
||||
|
||||
const USER_CUSTOM_PROMPTS_KEY = 'userCustomAIPrompts'
|
||||
|
||||
let customPrompts = $state<Record<string, string>>(getUserCustomPrompts())
|
||||
let isExpanded = $state(false)
|
||||
|
||||
function save() {
|
||||
storeLocalSetting(USER_CUSTOM_PROMPTS_KEY, JSON.stringify(customPrompts))
|
||||
sendUserToast('User AI prompts saved')
|
||||
}
|
||||
|
||||
let hasPrompts = $derived(Object.values(customPrompts).some((p) => p?.trim().length > 0))
|
||||
</script>
|
||||
|
||||
<div class="mt-4">
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center border-b cursor-pointer hover:bg-surface-hover w-full transition-colors"
|
||||
onclick={() => (isExpanded = !isExpanded)}
|
||||
>
|
||||
{#if isExpanded}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
<ChevronRight size={16} />
|
||||
{/if}
|
||||
<h2>Custom system prompts</h2>
|
||||
{#if hasPrompts}
|
||||
<div class="w-2 h-2 bg-blue-500 rounded-full ml-2"></div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isExpanded}
|
||||
<div>
|
||||
<CustomAIPrompts
|
||||
bind:customPrompts
|
||||
description="These prompts are stored locally in your browser and apply in addition to the workspace-level prompts."
|
||||
/>
|
||||
<div class="flex flex-row justify-end mt-2">
|
||||
<Button onclick={save}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -14,14 +14,9 @@
|
||||
import { safeSelectItems } from '../select/utils.svelte'
|
||||
import Badge from '../common/badge/Badge.svelte'
|
||||
import Tooltip from '../Tooltip.svelte'
|
||||
import { AIMode } from '../copilot/chat/AIChatManager.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import autosize from '$lib/autosize'
|
||||
import ModelTokenLimits from './ModelTokenLimits.svelte'
|
||||
import { setCopilotInfo } from '$lib/aiStore'
|
||||
|
||||
const MAX_CUSTOM_PROMPT_LENGTH = 5000
|
||||
import CustomAIPrompts from '../copilot/CustomAIPrompts.svelte'
|
||||
|
||||
let {
|
||||
aiProviders = $bindable(),
|
||||
@@ -48,9 +43,6 @@
|
||||
) as Record<AIProvider, string[]>
|
||||
)
|
||||
|
||||
// Custom system prompt settings
|
||||
let selectedAiMode = $state<AIMode>(AIMode.ASK)
|
||||
|
||||
let selectedAiModels = $derived(Object.values(aiProviders).flatMap((p) => p.models))
|
||||
let modelProviderMap = $derived(
|
||||
Object.fromEntries(
|
||||
@@ -331,55 +323,7 @@
|
||||
{/if}
|
||||
|
||||
{#if Object.keys(aiProviders).length > 0}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold">Custom system prompts</p>
|
||||
<div class="flex flex-col gap-4">
|
||||
<Label label="AI Mode">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={selectedAiMode}
|
||||
on:selected={({ detail }) => {
|
||||
selectedAiMode = detail
|
||||
}}
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
{#each Object.values(AIMode) as mode}
|
||||
<div class="relative">
|
||||
<ToggleButton
|
||||
value={mode}
|
||||
label={mode.charAt(0).toUpperCase() + mode.slice(1)}
|
||||
{item}
|
||||
/>
|
||||
{#if customPrompts[mode]?.length > 0}
|
||||
<div
|
||||
class="absolute -top-1 -right-1 w-2 h-2 bg-blue-500 rounded-full border border-surface"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
label="Custom system prompt for {selectedAiMode.charAt(0).toUpperCase() +
|
||||
selectedAiMode.slice(1)} Mode"
|
||||
>
|
||||
<textarea
|
||||
bind:value={customPrompts[selectedAiMode]}
|
||||
placeholder="Enter a custom system prompt for {selectedAiMode} mode."
|
||||
class="w-full min-h-24 p-2 border border-gray-200 dark:border-gray-700 rounded-md bg-surface text-primary resize-y"
|
||||
rows="4"
|
||||
maxlength={MAX_CUSTOM_PROMPT_LENGTH}
|
||||
use:autosize
|
||||
></textarea>
|
||||
<div class="flex justify-end mt-1">
|
||||
<span class="text-xs text-secondary">
|
||||
{(customPrompts[selectedAiMode] ?? '').length}/{MAX_CUSTOM_PROMPT_LENGTH} characters
|
||||
</span>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<CustomAIPrompts bind:customPrompts title="Custom system prompts" />
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user