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:
BIN
.loki/reference/chrome_laptop_Components_CodeEditor_Markdown.png
Normal file
BIN
.loki/reference/chrome_laptop_Components_CodeEditor_Markdown.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
5
bun.lock
5
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -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.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 || ""}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
};
|
||||
@@ -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({
|
||||
|
||||
@@ -2,6 +2,7 @@ export type CodeLanguage =
|
||||
| "clojure"
|
||||
| "html"
|
||||
| "json"
|
||||
| "markdown"
|
||||
| "mustache"
|
||||
| "pug"
|
||||
| "python"
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user