UXW-3922 Add syntax highlight for AI system prompts (#79675)

* UXW-3922 Add syntax highlight for AI system prompts

* Review fixes

* Fix CI

* Update Loki Snapshots

* Update Loki Snapshots

* Fix e2e

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
This commit is contained in:
Denis Berezin
2026-10-02 22:18:58 +05:00
committed by GitHub
parent 1feea97a07
commit 24a52962de
13 changed files with 112 additions and 33 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -10,6 +10,7 @@
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/lang-python": "^6.1.7",
"@codemirror/lang-sql": "^6.8.0",
"@codemirror/language": "^6.12.1",
@@ -678,6 +679,8 @@
"@codemirror/lang-json": ["@codemirror/lang-json@6.0.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@lezer/json": "^1.0.0" } }, "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ=="],
"@codemirror/lang-markdown": ["@codemirror/lang-markdown@6.5.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.7.1", "@codemirror/lang-html": "^6.0.0", "@codemirror/language": "^6.3.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/markdown": "^1.0.0" } }, "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw=="],
"@codemirror/lang-python": ["@codemirror/lang-python@6.1.7", "", { "dependencies": { "@codemirror/autocomplete": "^6.3.2", "@codemirror/language": "^6.8.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.1", "@lezer/python": "^1.1.4" } }, "sha512-mZnFTsL4lW5p9ch8uKNKeRU3xGGxr1QpESLilfON2E3fQzOa/OygEMkaDvERvXDJWJA9U9oN/D4w0ZuUzNO4+g=="],
"@codemirror/lang-sql": ["@codemirror/lang-sql@6.8.0", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg=="],
@@ -1088,6 +1091,8 @@
"@lezer/lr": ["@lezer/lr@1.4.2", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA=="],
"@lezer/markdown": ["@lezer/markdown@1.7.2", "", { "dependencies": { "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0" } }, "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ=="],
"@lezer/python": ["@lezer/python@1.1.15", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-aVQ43m2zk4FZYedCqL0KHPEUsqZOrmAvRhkhHlVPnDD1HODDyyQv5BRIuod4DadkgBEZd53vQOtXTonNbEgjrQ=="],
"@locker/near-membrane-base": ["@locker/near-membrane-base@0.16.0", "", { "dependencies": { "@locker/near-membrane-shared": "0.16.0" } }, "sha512-Ds6FQb3UMdA2L4zoGBtFwux6xD3Vi/4u6tgI+nCaAhCg0LcA138llFp5HasRpRoRkNu4CGdQu4nbC5/7ARybZQ=="],

View File

@@ -264,7 +264,7 @@ describe("AI Controls > Metabot access and customization", () => {
cy.reload();
cy.findByRole("textbox", {
name: /AI chat prompt instructions/,
}).should("contain.value", "Be concise and helpful.");
}).should("have.text", "Be concise and helpful.");
});
it("should save a custom SQL generation system prompt", () => {
@@ -292,7 +292,7 @@ describe("AI Controls > Metabot access and customization", () => {
cy.reload();
cy.findByRole("textbox", {
name: /SQL generation prompt instructions/,
}).should("contain.value", "Always use uppercase SQL keywords.");
}).should("have.text", "Always use uppercase SQL keywords.");
});
});
});

View File

@@ -8,18 +8,11 @@
overflow: hidden;
}
.textareaRoot {
.editor {
flex: 1;
min-height: 0;
display: flex;
}
.textareaWrapper {
flex: 1;
display: flex;
}
.textareaInput {
height: 100%;
line-height: 1.25rem;
/* Keeps the editor's internal scroller inside the rounded border */
overflow: hidden;
border: 1px solid var(--mb-color-border);
border-radius: 0.5rem;
}

View File

@@ -1,9 +1,11 @@
import { EditorView } from "@codemirror/view";
import { useMemo } from "react";
import { t } from "ttag";
import { CodeEditor } from "metabase/common/components/CodeEditor";
import { useSelector } from "metabase/redux";
import { getApplicationName } from "metabase/selectors/whitelabel";
import { SettingsPageWrapper } from "metabase/settings-components";
import { Textarea } from "metabase/ui";
import { useAdminSettingWithBlurInput } from "metabase-enterprise/ai-controls/hooks";
import S from "./MetabotSystemPromptsPage.module.css";
@@ -24,22 +26,28 @@ function SystemPromptPage(props: SystemPromptPageProps) {
const { handleInputChange, handleBlur, inputValue } =
useAdminSettingWithBlurInput(settingKey);
const extensions = useMemo(
() => [
EditorView.lineWrapping,
EditorView.contentAttributes.of({ "aria-label": title }),
],
[title],
);
return (
<SettingsPageWrapper
title={title}
description={description}
className={S.wrapper}
>
<Textarea
<CodeEditor
aria-label={title}
autosize={false}
classNames={{
root: S.textareaRoot,
wrapper: S.textareaWrapper,
input: S.textareaInput,
}}
className={S.editor}
extensions={extensions}
language="markdown"
lineNumbers={false}
onBlur={handleBlur}
onChange={(e) => handleInputChange(e.target.value)}
onChange={handleInputChange}
placeholder={getPlaceholder()}
value={inputValue || ""}
/>

View File

@@ -0,0 +1,44 @@
import { CodeEditor } from "metabase/common/components/CodeEditor";
import { Box } from "metabase/ui";
export default {
title: "Components/CodeEditor",
component: CodeEditor,
};
const MARKDOWN_EXAMPLE = `# SQL Query Assistant
You are an expert SQL analyst specializing in business intelligence.
## Core Principles
**Clarity over cleverness.** Write SQL that a colleague can read at a
glance. Favor *explicit joins* and \`meaningful_aliases\`.
**Performance awareness.** Consider how queries will execute:
- Filter early using WHERE clauses before joins where possible
- Avoid SELECT * — always name columns explicitly
- Use CTEs (WITH clauses) to break complex logic into readable steps
---
## Query Structure
\`\`\`sql
SELECT column_name
FROM table_name
\`\`\`
`;
export const Markdown = {
render: () => (
<Box maw="40rem" p="md">
<CodeEditor
language="markdown"
lineNumbers={false}
value={MARKDOWN_EXAMPLE}
/>
</Box>
),
};

View File

@@ -1,23 +1,19 @@
import type { Extension } from "@uiw/react-codemirror";
import { useMemo } from "react";
import { CodeMirror } from "metabase/common/components/CodeMirror";
import {
CodeMirror,
type CodeMirrorProps,
} from "metabase/common/components/CodeMirror";
import type { CodeLanguage } from "./types";
import { useExtensions } from "./utils";
export type CodeEditorProps = {
className?: string;
highlightRanges?: { start: number; end: number }[];
id?: string;
export type CodeEditorProps = Omit<CodeMirrorProps, "onChange"> & {
language?: CodeLanguage | Extension;
lineNumbers?: boolean;
readOnly?: boolean;
value: string;
proposedValue?: string;
onChange?: (value: string) => void;
extensions?: Extension[];
"data-testid"?: string;
};
export function CodeEditor({

View File

@@ -2,6 +2,7 @@ export type CodeLanguage =
| "clojure"
| "html"
| "json"
| "markdown"
| "mustache"
| "pug"
| "python"

View File

@@ -1,9 +1,14 @@
import { html } from "@codemirror/lang-html";
import { javascript } from "@codemirror/lang-javascript";
import { json } from "@codemirror/lang-json";
import { markdown } from "@codemirror/lang-markdown";
import { python } from "@codemirror/lang-python";
import { sql } from "@codemirror/lang-sql";
import { StreamLanguage, indentUnit } from "@codemirror/language";
import {
LanguageDescription,
StreamLanguage,
indentUnit,
} from "@codemirror/language";
import { clojure } from "@codemirror/legacy-modes/mode/clojure";
import { pug } from "@codemirror/legacy-modes/mode/pug";
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
@@ -53,6 +58,15 @@ export function getLanguageExtension(language: CodeLanguage | Extension) {
return html();
case "json":
return json();
case "markdown":
return markdown({
codeLanguages: [
LanguageDescription.of({
name: "sql",
support: sql(),
}),
],
});
case "python":
return [python(), indentUnit.of(" ")];
case "mustache":

View File

@@ -68,3 +68,16 @@
.typeName {
color: var(--mb-color-core-purple-saturated);
}
.heading {
color: var(--mb-color-core-brand);
font-weight: bold;
}
.strong {
font-weight: bold;
}
.emphasis {
font-style: italic;
}

View File

@@ -12,8 +12,10 @@ const styledTags = [
{ tag: tags.comment, class: S.comment },
{ tag: tags.compareOperator, class: S.compareOperator },
{ tag: tags.constant(tags.variableName), class: S.constant },
{ tag: tags.emphasis, class: S.emphasis },
{ tag: tags.escape, class: S.escape },
{ tag: tags.function(tags.variableName), class: S.variableName },
{ tag: tags.heading, class: S.heading },
{ tag: tags.keyword, class: S.keyword },
{ tag: tags.lineComment, class: S.lineComment },
{ tag: tags.logicOperator, class: S.logicOperator },
@@ -23,6 +25,7 @@ const styledTags = [
{ tag: tags.special(tags.string), class: S.string },
{ tag: tags.squareBracket, class: S.squareBracket },
{ tag: tags.string, class: S.string },
{ tag: tags.strong, class: S.strong },
{ tag: tags.typeName, class: S.typeName },
{ tag: tags.variableName, class: S.variableName },
];

View File

@@ -27,6 +27,7 @@ module.exports = {
"^Components/Feedback/Alert",
"^Components/Feedback/Loader Overview",
"^Components/Ask Before Using/Chip Overview",
"^Components/CodeEditor Markdown",
"^Components/Data display/Badge Sizes and variants",
"^Components/Navigation/NavLink Overview",
"^Components/Navigation/Tabs/Overview",

View File

@@ -19,6 +19,7 @@
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/lang-python": "^6.1.7",
"@codemirror/lang-sql": "^6.8.0",
"@codemirror/language": "^6.12.1",