From d3fbfdb7a8a5f14ddf564df406ec263fa4e55f32 Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary Date: Tue, 17 Mar 2026 17:57:49 +0530 Subject: [PATCH 1/7] Add support for variables in prompt messages --- .../prompts/components/promptsViewHeader.tsx | 7 +- .../prompts/components/variablesTableView.tsx | 4 +- ui/components/prompts/context.tsx | 12 +++ .../prompts/fragments/settingsPanel.tsx | 4 +- ui/lib/message/constant.ts | 2 +- ui/lib/types/prompts.ts | 78 ++++++++++--------- 6 files changed, 63 insertions(+), 44 deletions(-) diff --git a/ui/components/prompts/components/promptsViewHeader.tsx b/ui/components/prompts/components/promptsViewHeader.tsx index 4df3002e1c..5dbe7e3836 100644 --- a/ui/components/prompts/components/promptsViewHeader.tsx +++ b/ui/components/prompts/components/promptsViewHeader.tsx @@ -26,6 +26,7 @@ export default function PromptsViewHeader() { modelParams, provider, model, + variables, hasChanges, hasVersionChanges, hasSessionChanges, @@ -87,6 +88,7 @@ export default function PromptsViewHeader() { model_params: buildSaveParams(), provider, model, + variables: Object.keys(variables).length > 0 ? variables : undefined, }, }).unwrap(); setUrlState({ sessionId: result.session.id, versionId: null }); @@ -94,7 +96,7 @@ export default function PromptsViewHeader() { } catch (err) { toast.error("Failed to save session", { description: getErrorMessage(err) }); } - }, [selectedPrompt?.id, messages, buildSaveParams, provider, model, createSession, setUrlState, hasChanges, isStreaming]); + }, [selectedPrompt?.id, messages, buildSaveParams, provider, model, variables, createSession, setUrlState, hasChanges, isStreaming]); // Cmd+S / Ctrl+S to save session useHotkeys( @@ -126,6 +128,7 @@ export default function PromptsViewHeader() { model_params: buildSaveParams(), provider, model, + variables: Object.keys(variables).length > 0 ? variables : undefined, }, }).unwrap(); setUrlState({ sessionId: result.session.id, versionId: null }); @@ -133,7 +136,7 @@ export default function PromptsViewHeader() { } catch (err) { toast.error("Failed to save session", { description: getErrorMessage(err) }); } - }, [selectedPrompt?.id, messages, buildSaveParams, provider, model, createSession, setUrlState, onSessionSaved, hasChanges]); + }, [selectedPrompt?.id, messages, buildSaveParams, provider, model, variables, createSession, setUrlState, onSessionSaved, hasChanges]); const handleRenameSession = useCallback( async (sessionId: number, name: string) => { diff --git a/ui/components/prompts/components/variablesTableView.tsx b/ui/components/prompts/components/variablesTableView.tsx index 2b9c6db297..8843235bf2 100644 --- a/ui/components/prompts/components/variablesTableView.tsx +++ b/ui/components/prompts/components/variablesTableView.tsx @@ -10,7 +10,7 @@ export function VariablesTableView({ variables: VariableMap; onChange: React.Dispatch>; }) { - const entries = useMemo(() => Object.entries(variables), [variables]); + const entries = useMemo(() => Object.entries(variables).sort(([a], [b]) => a.localeCompare(b)), [variables]); const handleValueChange = useCallback( (name: string, value: string) => { @@ -25,7 +25,7 @@ export function VariablesTableView({

Detected from {"{{ }}"} syntax in messages. Values are substituted at runtime.

-
+
diff --git a/ui/components/prompts/context.tsx b/ui/components/prompts/context.tsx index b9e5b11544..3d9929738f 100644 --- a/ui/components/prompts/context.tsx +++ b/ui/components/prompts/context.tsx @@ -220,6 +220,10 @@ export function PromptProvider({ children }: { children: ReactNode }) { const loaded = Message.fromLegacyAll(raw); loadMessages(loaded.length > 0 ? loaded : [Message.system("")]); loadFromParams(selectedSession.model_params, selectedSession.provider, selectedSession.model); + // Restore variables (key:value) from session + if (selectedSession.variables && Object.keys(selectedSession.variables).length > 0) { + setVariables(selectedSession.variables); + } } else if (selectedVersion) { // If sessions are still loading and no session is explicitly selected, // wait — a session may auto-select and take priority @@ -228,6 +232,10 @@ export function PromptProvider({ children }: { children: ReactNode }) { const loaded = Message.fromLegacyAll(raw); loadMessages(loaded.length > 0 ? loaded : [Message.system("")]); loadFromParams(selectedVersion.model_params, selectedVersion.provider, selectedVersion.model); + // Initialize variables from version (keys with empty values) + if (selectedVersion.variables && Object.keys(selectedVersion.variables).length > 0) { + setVariables((prev) => mergeVariables(prev, Object.keys(selectedVersion.variables!))); + } } else if (selectedPrompt?.latest_version) { // Only fall back to latest_version after sessions have settled // to avoid racing with the session auto-select effect @@ -237,6 +245,10 @@ export function PromptProvider({ children }: { children: ReactNode }) { const loaded = Message.fromLegacyAll(raw); loadMessages(loaded.length > 0 ? loaded : [Message.system("")]); loadFromParams(version.model_params, version.provider, version.model); + // Initialize variables from version (keys with empty values) + if (version.variables && Object.keys(version.variables).length > 0) { + setVariables((prev) => mergeVariables(prev, Object.keys(version.variables!))); + } if (sessions.length === 0) { setUrlState({ versionId: version.id }); } diff --git a/ui/components/prompts/fragments/settingsPanel.tsx b/ui/components/prompts/fragments/settingsPanel.tsx index 3a21f03178..449af20e1d 100644 --- a/ui/components/prompts/fragments/settingsPanel.tsx +++ b/ui/components/prompts/fragments/settingsPanel.tsx @@ -168,12 +168,12 @@ export function SettingsPanel() { /> )} - {/* {Object.keys(variables).length > 0 && ( + {Object.keys(variables).length > 0 && ( <> - )} */} + )} {model && ( <> diff --git a/ui/lib/message/constant.ts b/ui/lib/message/constant.ts index 5803a0658a..fe1185dff4 100644 --- a/ui/lib/message/constant.ts +++ b/ui/lib/message/constant.ts @@ -19,7 +19,7 @@ export const JINJA_VAR_REGEX = /\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/g; */ export const JINJA_VAR_HIGHLIGHT_PATTERNS = [ { - pattern: /{{.*?}}/g, + pattern: /\{\{\s*[a-zA-Z_][a-zA-Z0-9_.]*\s*\}\}/g, className: "outline-content-brand-light text-sm cursor-pointer bg-green-500/20", validate: (part: string) => { return ( diff --git a/ui/lib/types/prompts.ts b/ui/lib/types/prompts.ts index 0458bbcee4..d1fa8295a5 100644 --- a/ui/lib/types/prompts.ts +++ b/ui/lib/types/prompts.ts @@ -43,18 +43,19 @@ export interface ModelParams { } export interface PromptVersion { - id: number; - prompt_id: string; - version_number: number; - commit_message: string; - messages: PromptVersionMessage[]; - model_params: ModelParams; - provider: string; - model: string; - is_latest: boolean; - created_by_id?: number; - created_by?: PromptUser; - created_at: string; // No updated_at - versions are immutable + id: number + prompt_id: string + version_number: number + commit_message: string + messages: PromptVersionMessage[] + model_params: ModelParams + provider: string + model: string + variables?: Record + is_latest: boolean + created_by_id?: number + created_by?: PromptUser + created_at: string // No updated_at - versions are immutable } export interface PromptVersionMessage { @@ -66,20 +67,21 @@ export interface PromptVersionMessage { } export interface PromptSession { - id: number; - prompt_id: string; - prompt?: Prompt; - version_id?: number; - version?: PromptVersion; - name: string; - messages: PromptSessionMessage[]; - model_params: ModelParams; - provider: string; - model: string; - created_by_id?: number; - created_by?: PromptUser; - created_at: string; - updated_at: string; + id: number + prompt_id: string + prompt?: Prompt + version_id?: number + version?: PromptVersion + name: string + messages: PromptSessionMessage[] + model_params: ModelParams + provider: string + model: string + variables?: Record + created_by_id?: number + created_by?: PromptUser + created_at: string + updated_at: string } export interface PromptSessionMessage { @@ -205,12 +207,13 @@ export interface GetSessionResponse { } export interface CreateSessionRequest { - name?: string; - version_id?: number; - messages?: PromptMessage[]; - model_params: ModelParams; - provider: string; - model: string; + name?: string + version_id?: number + messages?: PromptMessage[] + model_params: ModelParams + provider: string + model: string + variables?: Record } export interface CreateSessionResponse { @@ -218,11 +221,12 @@ export interface CreateSessionResponse { } export interface UpdateSessionRequest { - name?: string; - messages: PromptMessage[]; - model_params: ModelParams; - provider: string; - model: string; + name?: string + messages: PromptMessage[] + model_params: ModelParams + provider: string + model: string + variables?: Record } export interface UpdateSessionResponse { From a1f892241c403b9246f09dd54735cf5651e63d00 Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary Date: Wed, 18 Mar 2026 14:20:37 +0530 Subject: [PATCH 2/7] Add support for variables in the prompt messages backend --- framework/configstore/migrations.go | 47 +++++++++++++++++++ .../configstore/tables/promptSessions.go | 22 +++++++++ .../configstore/tables/promptVersions.go | 41 +++++++++++----- transports/bifrost-http/handlers/prompts.go | 47 ++++++++++++++----- ui/lib/message/variables.test.ts | 20 +++++--- 5 files changed, 149 insertions(+), 28 deletions(-) diff --git a/framework/configstore/migrations.go b/framework/configstore/migrations.go index bced2ec3f5..42ca3f3c8d 100644 --- a/framework/configstore/migrations.go +++ b/framework/configstore/migrations.go @@ -359,6 +359,9 @@ func triggerMigrations(ctx context.Context, db *gorm.DB) error { if err := migrationAddRoutingChainMaxDepthColumn(ctx, db); err != nil { return err } + if err := migrationAddPromptVariablesColumns(ctx, db); err != nil { + return err + } if err := migrationAddModelCapabilityColumns(ctx, db); err != nil { return err } @@ -5466,6 +5469,50 @@ func migrationAddOpenAIConfigJSONColumn(ctx context.Context, db *gorm.DB) error return nil } +// migrationAddPromptVariablesColumns adds variables_json column to prompt_sessions and prompt_versions +func migrationAddPromptVariablesColumns(ctx context.Context, db *gorm.DB) error { + m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{ + ID: "add_prompt_variables_columns", + Migrate: func(tx *gorm.DB) error { + tx = tx.WithContext(ctx) + migrator := tx.Migrator() + + if !migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") { + if err := migrator.AddColumn(&tables.TablePromptSession{}, "VariablesJSON"); err != nil { + return fmt.Errorf("failed to add variables_json column to prompt_sessions: %w", err) + } + } + + if !migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") { + if err := migrator.AddColumn(&tables.TablePromptVersion{}, "VariablesJSON"); err != nil { + return fmt.Errorf("failed to add variables_json column to prompt_versions: %w", err) + } + } + + return nil + }, + Rollback: func(tx *gorm.DB) error { + tx = tx.WithContext(ctx) + migrator := tx.Migrator() + if migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") { + if err := migrator.DropColumn(&tables.TablePromptSession{}, "variables_json"); err != nil { + return err + } + } + if migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") { + if err := migrator.DropColumn(&tables.TablePromptVersion{}, "variables_json"); err != nil { + return err + } + } + return nil + }, + }}) + if err := m.Migrate(); err != nil { + return fmt.Errorf("error while running add_prompt_variables_columns migration: %s", err.Error()) + } + return nil +} + // migrationAddKeyBlacklistedModelsJSONColumn adds blacklisted_models_json to config_keys // for per-key model deny lists (JSON array of model ids, default []). func migrationAddKeyBlacklistedModelsJSONColumn(ctx context.Context, db *gorm.DB) error { diff --git a/framework/configstore/tables/promptSessions.go b/framework/configstore/tables/promptSessions.go index df96db09aa..4618704ebf 100644 --- a/framework/configstore/tables/promptSessions.go +++ b/framework/configstore/tables/promptSessions.go @@ -22,6 +22,8 @@ type TablePromptSession struct { ModelParams ModelParams `gorm:"-" json:"model_params"` Provider string `gorm:"type:varchar(100)" json:"provider"` Model string `gorm:"type:varchar(100)" json:"model"` + VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"` + Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables CreatedAt time.Time `gorm:"not null" json:"created_at"` UpdatedAt time.Time `gorm:"not null" json:"updated_at"` @@ -40,6 +42,17 @@ func (s *TablePromptSession) BeforeSave(tx *gorm.DB) error { } paramsStr := string(data) s.ModelParamsJSON = ¶msStr + + if s.Variables != nil { + varsData, err := json.Marshal(s.Variables) + if err != nil { + return err + } + varsStr := string(varsData) + s.VariablesJSON = &varsStr + } else { + s.VariablesJSON = nil + } return nil } @@ -52,6 +65,15 @@ func (s *TablePromptSession) AfterFind(tx *gorm.DB) error { return err } } + if s.VariablesJSON != nil && *s.VariablesJSON != "" { + var vars PromptVariables + if err := json.Unmarshal([]byte(*s.VariablesJSON), &vars); err != nil { + return err + } + s.Variables = vars + } else { + s.Variables = nil + } return nil } diff --git a/framework/configstore/tables/promptVersions.go b/framework/configstore/tables/promptVersions.go index 1703be58f3..ca9e41e039 100644 --- a/framework/configstore/tables/promptVersions.go +++ b/framework/configstore/tables/promptVersions.go @@ -12,17 +12,19 @@ import ( // TablePromptVersion represents an immutable version of a prompt // Once created, a version cannot be modified - to make changes, create a new version type TablePromptVersion struct { - ID uint `gorm:"primaryKey;autoIncrement" json:"id"` - PromptID string `gorm:"type:varchar(36);not null;index;uniqueIndex:idx_prompt_version" json:"prompt_id"` - Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"` - VersionNumber int `gorm:"not null;uniqueIndex:idx_prompt_version" json:"version_number"` - CommitMessage string `gorm:"type:text" json:"commit_message"` - ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"` - ModelParams ModelParams `gorm:"-" json:"model_params"` - Provider string `gorm:"type:varchar(100)" json:"provider"` - Model string `gorm:"type:varchar(100)" json:"model"` - IsLatest bool `gorm:"not null;default:false" json:"is_latest"` - CreatedAt time.Time `gorm:"not null" json:"created_at"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + PromptID string `gorm:"type:varchar(36);not null;index;uniqueIndex:idx_prompt_version" json:"prompt_id"` + Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"` + VersionNumber int `gorm:"not null;uniqueIndex:idx_prompt_version" json:"version_number"` + CommitMessage string `gorm:"type:text" json:"commit_message"` + ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"` + ModelParams ModelParams `gorm:"-" json:"model_params"` + Provider string `gorm:"type:varchar(100)" json:"provider"` + Model string `gorm:"type:varchar(100)" json:"model"` + VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"` + Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables + IsLatest bool `gorm:"not null;default:false" json:"is_latest"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` // No UpdatedAt - versions are immutable // Relationships @@ -36,6 +38,10 @@ func (TablePromptVersion) TableName() string { return "prompt_versions" } // so that any provider-specific params (response_format, seed, logprobs, etc.) are preserved. type ModelParams map[string]interface{} +// PromptVariables represents a map of Jinja2 variable names to their values. +// Sessions store full {key: value} pairs; versions store {key: ""} (keys only). +type PromptVariables map[string]string + // BeforeSave GORM hook to serialize JSON fields func (v *TablePromptVersion) BeforeSave(tx *gorm.DB) error { if v.ModelParams != nil { @@ -46,6 +52,14 @@ func (v *TablePromptVersion) BeforeSave(tx *gorm.DB) error { paramsStr := string(data) v.ModelParamsJSON = ¶msStr } + if v.Variables != nil { + varsData, err := json.Marshal(v.Variables) + if err != nil { + return err + } + varsStr := string(varsData) + v.VariablesJSON = &varsStr + } return nil } @@ -58,6 +72,11 @@ func (v *TablePromptVersion) AfterFind(tx *gorm.DB) error { return err } } + if v.VariablesJSON != nil && *v.VariablesJSON != "" { + if err := json.Unmarshal([]byte(*v.VariablesJSON), &v.Variables); err != nil { + return err + } + } return nil } diff --git a/transports/bifrost-http/handlers/prompts.go b/transports/bifrost-http/handlers/prompts.go index e5b96f0c38..c5e5737f0f 100644 --- a/transports/bifrost-http/handlers/prompts.go +++ b/transports/bifrost-http/handlers/prompts.go @@ -137,25 +137,28 @@ type CreateVersionRequest struct { ModelParams tables.ModelParams `json:"model_params"` Provider string `json:"provider"` Model string `json:"model"` + Variables tables.PromptVariables `json:"variables,omitempty"` } // CreateSessionRequest represents the request body for creating a session type CreateSessionRequest struct { - Name string `json:"name"` - VersionID *uint `json:"version_id,omitempty"` - Messages []tables.PromptMessage `json:"messages,omitempty"` - ModelParams tables.ModelParams `json:"model_params"` - Provider string `json:"provider"` - Model string `json:"model"` + Name string `json:"name"` + VersionID *uint `json:"version_id,omitempty"` + Messages []tables.PromptMessage `json:"messages,omitempty"` + ModelParams tables.ModelParams `json:"model_params"` + Provider string `json:"provider"` + Model string `json:"model"` + Variables tables.PromptVariables `json:"variables,omitempty"` } // UpdateSessionRequest represents the request body for updating a session type UpdateSessionRequest struct { - Name string `json:"name"` - Messages []tables.PromptMessage `json:"messages"` - ModelParams tables.ModelParams `json:"model_params"` - Provider string `json:"provider"` - Model string `json:"model"` + Name string `json:"name"` + Messages []tables.PromptMessage `json:"messages"` + ModelParams tables.ModelParams `json:"model_params"` + Provider string `json:"provider"` + Model string `json:"model"` + Variables tables.PromptVariables `json:"variables,omitempty"` } // RenameSessionRequest represents the request body for renaming a session @@ -631,12 +634,22 @@ func (h *PromptsHandler) createVersion(ctx *fasthttp.RequestCtx) { }) } + // Strip variable values — versions store keys only; values live in sessions + var versionVars tables.PromptVariables + if len(req.Variables) > 0 { + versionVars = make(tables.PromptVariables, len(req.Variables)) + for key := range req.Variables { + versionVars[key] = "" + } + } + version := &tables.TablePromptVersion{ PromptID: promptID, CommitMessage: req.CommitMessage, ModelParams: req.ModelParams, Provider: req.Provider, Model: req.Model, + Variables: versionVars, Messages: messages, } @@ -835,6 +848,7 @@ func (h *PromptsHandler) createSession(ctx *fasthttp.RequestCtx) { ModelParams: req.ModelParams, Provider: req.Provider, Model: req.Model, + Variables: req.Variables, Messages: messages, } @@ -890,6 +904,7 @@ func (h *PromptsHandler) updateSession(ctx *fasthttp.RequestCtx) { session.ModelParams = req.ModelParams session.Provider = req.Provider session.Model = req.Model + session.Variables = req.Variables // Update messages var messages []tables.TablePromptSessionMessage @@ -1066,12 +1081,22 @@ func (h *PromptsHandler) commitSession(ctx *fasthttp.RequestCtx) { return } + // Copy variable keys from session with empty values for the version + var versionVars tables.PromptVariables + if len(session.Variables) > 0 { + versionVars = make(tables.PromptVariables, len(session.Variables)) + for key := range session.Variables { + versionVars[key] = "" + } + } + version := &tables.TablePromptVersion{ PromptID: session.PromptID, CommitMessage: req.CommitMessage, ModelParams: session.ModelParams, Provider: session.Provider, Model: session.Model, + Variables: versionVars, Messages: messages, } diff --git a/ui/lib/message/variables.test.ts b/ui/lib/message/variables.test.ts index 4212822c53..e6a3df4dea 100644 --- a/ui/lib/message/variables.test.ts +++ b/ui/lib/message/variables.test.ts @@ -1,11 +1,11 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { Message } from "./message"; import { - extractVariablesFromText, extractVariablesFromMessages, - replaceVariablesInText, - replaceVariablesInMessages, + extractVariablesFromText, mergeVariables, + replaceVariablesInMessages, + replaceVariablesInText, } from "./variables"; // ============================================================================= @@ -122,7 +122,7 @@ describe("extractVariablesFromMessages", () => { it("extracts variables from an assistant message", () => { const messages = [Message.response("Hello {{ name }}")]; - expect(extractVariablesFromMessages(messages)).toEqual(["name"]); + expect(extractVariablesFromMessages(messages)).toEqual([]); }); it("extracts variables across multiple messages", () => { @@ -180,6 +180,14 @@ describe("replaceVariablesInText", () => { expect(replaceVariablesInText("{{ x }} and {{ x }}", { x: "yes" })).toBe("yes and yes"); }); + it("preserves leading curly braces that are not part of variables", () => { + expect(replaceVariablesInText("{{{ x }} and {{ x }}", { x: "yes" })).toBe("{yes and yes"); + }); + + it("preserves trailing curly braces that are not part of variables", () => { + expect(replaceVariablesInText("{{ x }} and {{ x }}}}", { x: "yes" })).toBe("yes and yes}}"); + }); + it("leaves variable untouched when not in map", () => { expect(replaceVariablesInText("{{ unknown }}", {})).toBe("{{ unknown }}"); }); @@ -352,4 +360,4 @@ describe("mergeVariables", () => { const result = mergeVariables(current, ["name"]); expect(result).toEqual({ name: "" }); }); -}); \ No newline at end of file +}); From f538e6e8f58f445a7aa37f4bab82b560b47b4842 Mon Sep 17 00:00:00 2001 From: Suresh Chaudhary Date: Mon, 23 Mar 2026 15:25:05 +0530 Subject: [PATCH 3/7] refactor moved CELRuleBuilder to lib for reusability --- .../mcp-logs/views/bifrost.code-workspace | 11 ++ .../components/celBuilder/celRuleBuilder.tsx | 130 ++---------- ui/components/ui/asyncMultiselect.tsx | 8 +- .../ui/custom}/celBuilder/actionButton.tsx | 0 .../ui/custom/celBuilder/celRuleBuilder.tsx | 187 ++++++++++++++++++ .../custom}/celBuilder/combinatorSelector.tsx | 0 .../ui/custom}/celBuilder/fieldSelector.tsx | 0 ui/components/ui/custom/celBuilder/index.ts | 2 + .../custom}/celBuilder/operatorSelector.tsx | 0 .../celBuilder/queryBuilderWrapper.tsx | 0 .../ui/custom}/celBuilder/valueEditor.tsx | 14 +- ui/components/ui/modelMultiselect.tsx | 6 + 12 files changed, 243 insertions(+), 115 deletions(-) create mode 100644 ui/app/workspace/mcp-logs/views/bifrost.code-workspace rename ui/{app/workspace/routing-rules/components => components/ui/custom}/celBuilder/actionButton.tsx (100%) create mode 100644 ui/components/ui/custom/celBuilder/celRuleBuilder.tsx rename ui/{app/workspace/routing-rules/components => components/ui/custom}/celBuilder/combinatorSelector.tsx (100%) rename ui/{app/workspace/routing-rules/components => components/ui/custom}/celBuilder/fieldSelector.tsx (100%) create mode 100644 ui/components/ui/custom/celBuilder/index.ts rename ui/{app/workspace/routing-rules/components => components/ui/custom}/celBuilder/operatorSelector.tsx (100%) rename ui/{app/workspace/routing-rules/components => components/ui/custom}/celBuilder/queryBuilderWrapper.tsx (100%) rename ui/{app/workspace/routing-rules/components => components/ui/custom}/celBuilder/valueEditor.tsx (94%) diff --git a/ui/app/workspace/mcp-logs/views/bifrost.code-workspace b/ui/app/workspace/mcp-logs/views/bifrost.code-workspace new file mode 100644 index 0000000000..8c5511061b --- /dev/null +++ b/ui/app/workspace/mcp-logs/views/bifrost.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../../.." + }, + { + "path": "../../../../../../bifrost-enterprise" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx index 4660699352..29f034d5d9 100644 --- a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx +++ b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx @@ -1,25 +1,16 @@ /** - * CEL Rule Builder Component for Routing Rules - * Visual query builder for creating CEL expressions + * CEL Rule Builder for Routing Rules + * Thin wrapper around the reusable CELRuleBuilder with routing-specific config */ -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; +"use client"; + +import { CELRuleBuilder as BaseCELRuleBuilder } from "@/components/ui/custom/celBuilder"; import { getRoutingFields } from "@/lib/config/celFieldsRouting"; import { celOperatorsRouting } from "@/lib/config/celOperatorsRouting"; -import { convertRuleGroupToCEL } from "@/lib/utils/celConverterRouting"; -import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; -import { Check, Copy, Loader2 } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Field, QueryBuilder, RuleGroupType } from "react-querybuilder"; -import "react-querybuilder/dist/query-builder.css"; -import { ActionButton } from "./actionButton"; -import { CombinatorSelector } from "./combinatorSelector"; -import { FieldSelector } from "./fieldSelector"; -import { OperatorSelector } from "./operatorSelector"; -import { QueryBuilderWrapper } from "./queryBuilderWrapper"; -import { ValueEditor } from "./valueEditor"; +import { convertRuleGroupToCEL, validateRegexPattern } from "@/lib/utils/celConverterRouting"; +import { useMemo } from "react"; +import { RuleGroupType } from "react-querybuilder"; interface CELRuleBuilderProps { onChange?: (celExpression: string, query: RuleGroupType) => void; @@ -30,11 +21,6 @@ interface CELRuleBuilderProps { isLoading?: boolean; } -const defaultQuery: RuleGroupType = { - combinator: "and", - rules: [], -}; - export function CELRuleBuilder({ onChange, initialQuery, @@ -43,96 +29,18 @@ export function CELRuleBuilder({ isLoading = false, allowCustomModels = false, }: CELRuleBuilderProps) { - const [query, setQuery] = useState(initialQuery || defaultQuery); - const [celExpression, setCelExpression] = useState(""); - const { copy, copied } = useCopyToClipboard(); - const onChangeRef = useRef(onChange); - - // Keep ref updated so the query effect always invokes the latest callback - useEffect(() => { - onChangeRef.current = onChange; - }, [onChange]); - - // Generate fields with dynamic providers and models - const fields = useMemo(() => { - const celFields = getRoutingFields(providers, models); - return celFields.map((field) => ({ - ...field, - value: field.name, - })) as Field[]; - }, [providers, models]); - - useEffect(() => { - const expression = convertRuleGroupToCEL(query); - setCelExpression(expression); - onChangeRef.current?.(expression, query); - }, [query]); - - const handleCopy = () => copy(celExpression); - - // Show loading state - if (isLoading) { - return ( -
- - Loading CEL builder... -
- ); - } + const fields = useMemo(() => getRoutingFields(providers, models), [providers, models]); return ( -
-
-
- - ({ - name: op.name, - label: op.label, - }))} - controlElements={{ - fieldSelector: FieldSelector, - operatorSelector: OperatorSelector, - valueEditor: ValueEditor, - addRuleAction: ActionButton, - addGroupAction: ActionButton, - removeRuleAction: ActionButton, - removeGroupAction: ActionButton, - combinatorSelector: CombinatorSelector, - }} - translations={{ - addRule: { label: "Add Rule" }, - addGroup: { label: "Add Rule Group" }, - }} - /> - -
-
- -
-
- - -
-