From b5d0f23f4fe9ea05069cada517a47ebe1cf8267a Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Thu, 23 Oct 2025 10:35:51 +0200 Subject: [PATCH] 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 * 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 * 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 * 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 * 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 * simplify * nit * fix * fix --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: centdix --- frontend/src/lib/aiStore.ts | 29 ++++++++ .../src/lib/components/UserSettings.svelte | 2 + .../components/copilot/CustomAIPrompts.svelte | 69 +++++++++++++++++++ .../copilot/chat/AIChatManager.svelte.ts | 35 +++++----- .../settings/UserAIPromptsSettings.svelte | 50 ++++++++++++++ .../workspaceSettings/AISettings.svelte | 60 +--------------- 6 files changed, 169 insertions(+), 76 deletions(-) create mode 100644 frontend/src/lib/components/copilot/CustomAIPrompts.svelte create mode 100644 frontend/src/lib/components/settings/UserAIPromptsSettings.svelte diff --git a/frontend/src/lib/aiStore.ts b/frontend/src/lib/aiStore.ts index 60ade934ea..6b302d281f 100644 --- a/frontend/src/lib/aiStore.ts +++ b/frontend/src/lib/aiStore.ts @@ -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( @@ -90,3 +92,30 @@ export function getCurrentModel() { } return model } + +export function getUserCustomPrompts(): Record { + 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') +} diff --git a/frontend/src/lib/components/UserSettings.svelte b/frontend/src/lib/components/UserSettings.svelte index 489ee865a3..5471e21f10 100644 --- a/frontend/src/lib/components/UserSettings.svelte +++ b/frontend/src/lib/components/UserSettings.svelte @@ -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 @@
+
{/if} diff --git a/frontend/src/lib/components/copilot/CustomAIPrompts.svelte b/frontend/src/lib/components/copilot/CustomAIPrompts.svelte new file mode 100644 index 0000000000..eb4685a4d1 --- /dev/null +++ b/frontend/src/lib/components/copilot/CustomAIPrompts.svelte @@ -0,0 +1,69 @@ + + +
+

{title}

+ {#if description} +

{description}

+ {/if} +
+ + + +
+
diff --git a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts index b2a70b62dd..940ee5e6d6 100644 --- a/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts +++ b/frontend/src/lib/components/copilot/chat/AIChatManager.svelte.ts @@ -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 ?? {}, diff --git a/frontend/src/lib/components/settings/UserAIPromptsSettings.svelte b/frontend/src/lib/components/settings/UserAIPromptsSettings.svelte new file mode 100644 index 0000000000..f752a58b81 --- /dev/null +++ b/frontend/src/lib/components/settings/UserAIPromptsSettings.svelte @@ -0,0 +1,50 @@ + + +
+ + + {#if isExpanded} +
+ +
+ +
+
+ {/if} +
diff --git a/frontend/src/lib/components/workspaceSettings/AISettings.svelte b/frontend/src/lib/components/workspaceSettings/AISettings.svelte index 4fbbf8ca47..0c006b2c40 100644 --- a/frontend/src/lib/components/workspaceSettings/AISettings.svelte +++ b/frontend/src/lib/components/workspaceSettings/AISettings.svelte @@ -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 ) - // Custom system prompt settings - let selectedAiMode = $state(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} -
-

Custom system prompts

-
- - - -
-
+ {/if}