diff --git a/api/oss/src/__init__.py b/api/oss/src/__init__.py index 487beef319..65fef7aadf 100644 --- a/api/oss/src/__init__.py +++ b/api/oss/src/__init__.py @@ -158,10 +158,19 @@ async def _get_blocked_emails() -> Set[str]: async def _is_blocked(email: str) -> bool: email = email.lower() - if email in await _get_blocked_emails(): + domain = email.split("@")[-1] if "@" in email else "" + allowed_domains = env.AGENTA_ALLOWED_DOMAINS + is_domain_allowed = allowed_domains and domain in allowed_domains + + if allowed_domains and not is_domain_allowed: return True - if "@" in email and email.split("@")[-1] in await _get_blocked_domains(): + + if email and email in await _get_blocked_emails(): return True + + if domain and domain in await _get_blocked_domains() and not is_domain_allowed: + return True + return False diff --git a/api/oss/src/utils/env.py b/api/oss/src/utils/env.py index d935bc9607..b2db70490f 100644 --- a/api/oss/src/utils/env.py +++ b/api/oss/src/utils/env.py @@ -89,6 +89,11 @@ class EnvironSettings(BaseModel): for e in (os.getenv("AGENTA_BLOCKED_DOMAINS") or "").split(",") if e.strip() } + AGENTA_ALLOWED_DOMAINS: set = { + e.strip().lower() + for e in (os.getenv("AGENTA_ALLOWED_DOMAINS") or "").split(",") + if e.strip() + } # AGENTA-SPECIFIC (INTERNAL INFRA) DOCKER_NETWORK_MODE: str = os.getenv("DOCKER_NETWORK_MODE") or "bridge" diff --git a/api/pyproject.toml b/api/pyproject.toml index e1578d57df..35b6013d1b 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "api" -version = "0.59.10" +version = "0.59.11" description = "Agenta API" authors = [ { name = "Mahmoud Mabrouk", email = "mahmoud@agenta.ai" }, diff --git a/docs/docs/self-host/02-configuration.mdx b/docs/docs/self-host/02-configuration.mdx index 4d46489d14..adde1c0333 100644 --- a/docs/docs/self-host/02-configuration.mdx +++ b/docs/docs/self-host/02-configuration.mdx @@ -55,6 +55,7 @@ Optional Agenta-specific configurations: | `AGENTA_SEND_EMAIL_FROM_ADDRESS` | From address for system emails | `mail@example.com` | | `AGENTA_API_INTERNAL_URL` | Internal API URL for services | _(empty)_ | | `AGENTA_SERVICE_MIDDLEWARE_CACHE_ENABLED` | Enable middleware caching in the chat and completion services | `true` | +| `AGENTA_ALLOWED_DOMAINS` | Comma-separated list of email domains allowed to authenticate; when set, all other domains are rejected | _(empty)_ | | `AGENTA_OTLP_MAX_BATCH_BYTES` | Max OTLP batch size before requests are rejected with 413 | `10485760` (10MB) | ### Third-party (Required) @@ -238,4 +239,3 @@ TRAEFIK_PORT=80 For configuration assistance: - Check the [GitHub issues](https://github.com/Agenta-AI/agenta/issues) - Join our [Slack community](https://join.slack.com/t/agenta-hq/shared_invite/zt-37pnbp5s6-mbBrPL863d_oLB61GSNFjw) -- Review the deployment documentation for your specific setup \ No newline at end of file diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 15047cc74f..513d95a1a3 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "agenta" -version = "0.59.10" +version = "0.59.11" description = "The SDK for agenta is an open-source LLMOps platform." readme = "README.md" authors = [ diff --git a/web/ee/package.json b/web/ee/package.json index 534779b1f0..f7599d7d1d 100644 --- a/web/ee/package.json +++ b/web/ee/package.json @@ -1,6 +1,6 @@ { "name": "@agenta/ee", - "version": "0.59.10", + "version": "0.59.11", "private": true, "engines": { "node": ">=18" diff --git a/web/ee/src/components/Evaluators/components/ConfigureEvaluator/index.tsx b/web/ee/src/components/Evaluators/components/ConfigureEvaluator/index.tsx index 7aeb94d820..5aa1fd4973 100644 --- a/web/ee/src/components/Evaluators/components/ConfigureEvaluator/index.tsx +++ b/web/ee/src/components/Evaluators/components/ConfigureEvaluator/index.tsx @@ -103,8 +103,7 @@ const ConfigureEvaluatorPage = ({evaluatorId}: {evaluatorId?: string | null}) => const handleSuccess = useCallback(async () => { message.success("Evaluator configuration saved") await refetchAll() - await navigateBack() - }, [navigateBack, refetchAll]) + }, [refetchAll]) if (!router.isReady || isLoading) { return diff --git a/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/DynamicFormField.tsx b/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/DynamicFormField.tsx index 3d909a7ff8..1d1a9d2761 100644 --- a/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/DynamicFormField.tsx +++ b/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/DynamicFormField.tsx @@ -1,11 +1,12 @@ +import {useCallback} from "react" + import {InfoCircleOutlined} from "@ant-design/icons" -import Editor from "@monaco-editor/react" import {theme, Form, Tooltip, InputNumber, Switch, Input, AutoComplete} from "antd" -import {Rule} from "antd/es/form" +import {FormInstance, Rule} from "antd/es/form" import Link from "next/link" import {createUseStyles} from "react-jss" -import {useAppTheme} from "@/oss/components/Layout/ThemeContextProvider" +import SharedEditor from "@/oss/components/Playground/Components/SharedEditor" import {isValidRegex} from "@/oss/lib/helpers/validators" import {generatePaths} from "@/oss/lib/transformers" import {EvaluationSettingsTemplate, JSSTheme} from "@/oss/lib/Types" @@ -15,15 +16,24 @@ import {Messages} from "./Messages" type DynamicFormFieldProps = EvaluationSettingsTemplate & { name: string | string[] traceTree: Record + form?: FormInstance } const useStyles = createUseStyles((theme: JSSTheme) => ({ - editor: { - border: `1px solid ${theme.colorBorder}`, - borderRadius: theme.borderRadius, - overflow: "hidden", - "& .monaco-editor": { - width: "0 !important", + codeEditor: { + "& .agenta-editor-wrapper": { + minHeight: 375, + }, + "&.agenta-shared-editor": { + borderColor: theme.colorBorder, + }, + }, + objectEditor: { + "& .agenta-editor-wrapper": { + minHeight: 120, + }, + "&.agenta-shared-editor": { + borderColor: theme.colorBorder, }, }, ExternalHelp: { @@ -45,6 +55,41 @@ const useStyles = createUseStyles((theme: JSSTheme) => ({ }, })) +interface ControlledSharedEditorProps { + value?: unknown + onChange?: (value: string) => void + className?: string + language?: "json" | "yaml" | "code" +} + +const ControlledSharedEditor = ({ + value, + onChange, + className, + language, +}: ControlledSharedEditorProps) => { + const handleValueChange = useCallback( + (next: string) => { + onChange?.(next) + }, + [onChange], + ) + + return ( + + ) +} + export const DynamicFormField: React.FC = ({ name, label, @@ -55,11 +100,24 @@ export const DynamicFormField: React.FC = ({ max, required, traceTree, + form, }) => { - const {appTheme} = useAppTheme() + const settingsValue = Form.useWatch(name, form) + const classes = useStyles() const {token} = theme.useToken() + const handleValueChange = useCallback( + (next: string) => { + if (form) { + form.setFieldsValue({ + [name as string]: next, + }) + } + }, + [form, name], + ) + const rules: Rule[] = [{required: required ?? true, message: "This field is required"}] if (type === "regex") rules.push({ @@ -100,7 +158,11 @@ export const DynamicFormField: React.FC = ({ )} } - initialValue={defaultVal} + initialValue={ + type === "object" && defaultVal && typeof defaultVal === "object" + ? JSON.stringify(defaultVal, null, 2) + : defaultVal + } rules={rules} hidden={type === "hidden"} > @@ -126,21 +188,18 @@ export const DynamicFormField: React.FC = ({ ) : type === "text" ? ( ) : type === "code" ? ( - ) : type === "object" ? ( - ) : null} diff --git a/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/index.tsx b/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/index.tsx index 1ec855d3ca..7c27c58fae 100644 --- a/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/index.tsx +++ b/web/ee/src/components/pages/evaluations/autoEvaluation/EvaluatorsModal/ConfigureEvaluator/index.tsx @@ -244,6 +244,7 @@ const ConfigureEvaluator = ({ {...field} key={field.key} traceTree={traceTree} + form={form} name={["settings_values", field.key]} /> ))} diff --git a/web/oss/package.json b/web/oss/package.json index 2d344c2980..ae1c659ae2 100644 --- a/web/oss/package.json +++ b/web/oss/package.json @@ -1,6 +1,6 @@ { "name": "@agenta/oss", - "version": "0.59.10", + "version": "0.59.11", "private": true, "engines": { "node": ">=18" diff --git a/web/oss/src/components/DynamicCodeBlock/CodeBlock.tsx b/web/oss/src/components/DynamicCodeBlock/CodeBlock.tsx index 425bc9e240..f6ad149f8e 100644 --- a/web/oss/src/components/DynamicCodeBlock/CodeBlock.tsx +++ b/web/oss/src/components/DynamicCodeBlock/CodeBlock.tsx @@ -46,7 +46,16 @@ const theme: EditorThemeClasses = { } // Normalize language ids using Lexical's helper -const normalizeShikiLang = (lang: string) => normalizeCodeLanguage((lang || "").toLowerCase()) +const LANGUAGE_FALLBACKS: Record = { + code: "python", +} + +const resolveLexicalLanguage = (language: string): string => { + const normalized = (language || "").toLowerCase() + const fallback = LANGUAGE_FALLBACKS[normalized] ?? normalized + const resolved = normalizeCodeLanguage(fallback) + return resolved || "plaintext" +} const ShikiHighlightPlugin: FC<{langs: string[]; themeName: string}> = ({langs, themeName}) => { const [editor] = useLexicalComposerContext() @@ -97,6 +106,8 @@ const InitializeContentPlugin: FC<{language: string; value: string}> = ({languag const CodeBlock: FC = ({language, value}) => { const classes = useStyles() + const lexicalLanguage = useMemo(() => resolveLexicalLanguage(language), [language]) + const editorConfig = useMemo( () => ({ namespace: "AgentaCodeBlock", @@ -109,7 +120,7 @@ const CodeBlock: FC = ({language, value}) => { ) const shikiTheme = "github-light" - const shikiLang = useMemo(() => normalizeShikiLang(language), [language]) + const shikiLang = lexicalLanguage const langs = useMemo(() => [shikiLang], [shikiLang]) return ( @@ -120,7 +131,7 @@ const CodeBlock: FC = ({language, value}) => { placeholder={null} ErrorBoundary={LexicalErrorBoundary} /> - + diff --git a/web/oss/src/components/Editor/commands/InitialContentCommand.ts b/web/oss/src/components/Editor/commands/InitialContentCommand.ts index d02c002009..4ce004b605 100644 --- a/web/oss/src/components/Editor/commands/InitialContentCommand.ts +++ b/web/oss/src/components/Editor/commands/InitialContentCommand.ts @@ -7,11 +7,13 @@ import {createCommand, LexicalCommand} from "lexical" +import type {CodeLanguage} from "../plugins/code/types" + export interface InitialContentPayload { /** The initial content to be processed */ content: string /** The language for syntax highlighting */ - language: "json" | "yaml" + language: CodeLanguage /** Whether this content should be handled by the default plugin */ preventDefault: () => void /** Whether default handling has been prevented */ diff --git a/web/oss/src/components/Editor/plugins/code/nodes/CodeBlockNode.ts b/web/oss/src/components/Editor/plugins/code/nodes/CodeBlockNode.ts index 0cd3ac6dd3..21e08654a4 100644 --- a/web/oss/src/components/Editor/plugins/code/nodes/CodeBlockNode.ts +++ b/web/oss/src/components/Editor/plugins/code/nodes/CodeBlockNode.ts @@ -11,13 +11,15 @@ import {ElementNode, LexicalNode, SerializedElementNode, Spread, EditorConfig} from "lexical" +import type {CodeLanguage} from "../types" + /** * Represents the serialized form of a CodeBlockNode. * Extends SerializedElementNode with a language property. */ export type SerializedCodeBlockNode = Spread< { - language: "json" | "yaml" + language: CodeLanguage hasValidationError: boolean }, SerializedElementNode @@ -29,7 +31,7 @@ export type SerializedCodeBlockNode = Spread< */ export class CodeBlockNode extends ElementNode { /** The programming language for syntax highlighting */ - __language: "json" | "yaml" + __language: CodeLanguage __hasValidationError: boolean /** @@ -54,7 +56,7 @@ export class CodeBlockNode extends ElementNode { * @param language - The programming language for the code block (defaults to "json") * @param key - Optional unique identifier for the node */ - constructor(language: "json" | "yaml" = "json", hasValidationError?: boolean, key?: string) { + constructor(language: CodeLanguage = "json", hasValidationError?: boolean, key?: string) { super(key) this.__language = language this.__hasValidationError = hasValidationError ?? false @@ -111,11 +113,11 @@ export class CodeBlockNode extends ElementNode { return false } - getLanguage(): "json" | "yaml" { + getLanguage(): CodeLanguage { return this.getLatest().__language } - setLanguage(language: "json" | "yaml") { + setLanguage(language: CodeLanguage) { const writable = this.getWritable() writable.__language = language } @@ -139,7 +141,7 @@ export class CodeBlockNode extends ElementNode { * @returns A new CodeBlockNode instance */ export function $createCodeBlockNode( - language: "json" | "yaml", + language: CodeLanguage, hasValidationError?: boolean, ): CodeBlockNode { return new CodeBlockNode(language, hasValidationError) diff --git a/web/oss/src/components/Editor/plugins/code/plugins/GlobalErrorIndicatorPlugin.tsx b/web/oss/src/components/Editor/plugins/code/plugins/GlobalErrorIndicatorPlugin.tsx index ecd80103e9..9efb57a58b 100644 --- a/web/oss/src/components/Editor/plugins/code/plugins/GlobalErrorIndicatorPlugin.tsx +++ b/web/oss/src/components/Editor/plugins/code/plugins/GlobalErrorIndicatorPlugin.tsx @@ -5,6 +5,7 @@ import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext" import {createPortal} from "react-dom" import {$getActiveLanguage} from "../utils/language" +import type {CodeLanguage} from "../types" import {validateAll} from "../utils/validationUtils" import {$getEditorCodeAsString} from "./RealTimeValidationPlugin" @@ -64,7 +65,7 @@ class ValidationManager { validateContent( content: string, schema?: any, - language: "json" | "yaml" = "json", + language: CodeLanguage = "json", ): ValidationState { // Skip if content hasn't changed if (content === this.state.lastValidatedContent) { diff --git a/web/oss/src/components/Editor/plugins/code/types.ts b/web/oss/src/components/Editor/plugins/code/types.ts new file mode 100644 index 0000000000..9163c23460 --- /dev/null +++ b/web/oss/src/components/Editor/plugins/code/types.ts @@ -0,0 +1,3 @@ +export type CodeLanguage = "json" | "yaml" | "code" + +export const DEFAULT_CODE_LANGUAGE: CodeLanguage = "json" diff --git a/web/oss/src/components/Editor/plugins/code/utils/indent.ts b/web/oss/src/components/Editor/plugins/code/utils/indent.ts index 35c16bb883..3818ec994c 100644 --- a/web/oss/src/components/Editor/plugins/code/utils/indent.ts +++ b/web/oss/src/components/Editor/plugins/code/utils/indent.ts @@ -24,7 +24,7 @@ export function calculateLineIndentation( lineText: string, previousLineText: string, baseIndentLevel: number, - language: "json" | "yaml", + language: "json" | "yaml" | "code", ): number { const trimmedLine = lineText.trim() const trimmedPrevious = previousLineText.trim() @@ -65,7 +65,7 @@ export function calculateLineIndentation( export function calculateMultiLineIndentation( lines: string[], baseIndentLevel: number, - language: "json" | "yaml", + language: "json" | "yaml" | "code", ): number[] { const indentLevels: number[] = [] @@ -93,7 +93,7 @@ export function calculateMultiLineIndentation( */ export function isFoldableLine(text: string, language: string): boolean { const trimmed = text.trim() - if (language === "json") { + if (language === "json" || language === "code") { return trimmed.endsWith("{") } if (language === "yaml") { diff --git a/web/oss/src/components/Editor/plugins/code/utils/language.ts b/web/oss/src/components/Editor/plugins/code/utils/language.ts index 9a136c2062..927438986b 100644 --- a/web/oss/src/components/Editor/plugins/code/utils/language.ts +++ b/web/oss/src/components/Editor/plugins/code/utils/language.ts @@ -1,6 +1,8 @@ import {$getRoot, LexicalEditor} from "lexical" import {$isCodeBlockNode} from "../nodes/CodeBlockNode" +import type {CodeLanguage} from "../types" +import {DEFAULT_CODE_LANGUAGE} from "../types" /** * Determines the active language mode of the code editor. @@ -12,12 +14,12 @@ import {$isCodeBlockNode} from "../nodes/CodeBlockNode" * @param editor - The Lexical editor instance to check * @returns The current language mode, defaults to 'json' if no code block found */ -export function $getActiveLanguage(editor: LexicalEditor): "json" | "yaml" { +export function $getActiveLanguage(editor: LexicalEditor): CodeLanguage { const root = $getRoot() for (const block of root.getChildren()) { if ($isCodeBlockNode(block)) { - return block.getLanguage() as "json" | "yaml" + return block.getLanguage() } } - return "json" + return DEFAULT_CODE_LANGUAGE } diff --git a/web/oss/src/components/Editor/plugins/code/utils/pasteUtils.ts b/web/oss/src/components/Editor/plugins/code/utils/pasteUtils.ts index b93f27006e..0ef838e064 100644 --- a/web/oss/src/components/Editor/plugins/code/utils/pasteUtils.ts +++ b/web/oss/src/components/Editor/plugins/code/utils/pasteUtils.ts @@ -3,6 +3,7 @@ import {$createRangeSelection, $setSelection} from "lexical" import {$createCodeHighlightNode} from "../nodes/CodeHighlightNode" import {$createCodeLineNode, CodeLineNode} from "../nodes/CodeLineNode" import {$createCodeTabNode} from "../nodes/CodeTabNode" +import type {CodeLanguage} from "../types" import {normalizePastedLinesIndentation} from "./indentationUtils" import {tokenizeCodeLine} from "./tokenizer" @@ -207,7 +208,7 @@ export function $insertLinesWithSelectionAndIndent({ } } -export function $createNodeForLineWithTabs(line: string, language: "json" | "yaml") { +export function $createNodeForLineWithTabs(line: string, language: CodeLanguage) { const codeLine = $createCodeLineNode() // Extract leading spaces/tabs const indentMatch = line.match(/^[ \t]+/) diff --git a/web/oss/src/components/Editor/plugins/code/utils/structuralValidators.ts b/web/oss/src/components/Editor/plugins/code/utils/structuralValidators.ts index d5e641efd2..a25c40ebf4 100644 --- a/web/oss/src/components/Editor/plugins/code/utils/structuralValidators.ts +++ b/web/oss/src/components/Editor/plugins/code/utils/structuralValidators.ts @@ -1,10 +1,13 @@ /** * Check if content appears to be incomplete (user is still typing) */ -export function isContentIncomplete(text: string, language: "json" | "yaml" = "json"): boolean { +export function isContentIncomplete( + text: string, + language: "json" | "yaml" | "code" = "json", +): boolean { const trimmed = text.trim() - if (language === "json") { + if (language === "json" || language === "code") { // JSON incomplete patterns return ( trimmed.endsWith(":") || // "key": diff --git a/web/oss/src/components/Editor/plugins/code/utils/tokenizer.ts b/web/oss/src/components/Editor/plugins/code/utils/tokenizer.ts index 9ee2a21075..c5426b6de6 100644 --- a/web/oss/src/components/Editor/plugins/code/utils/tokenizer.ts +++ b/web/oss/src/components/Editor/plugins/code/utils/tokenizer.ts @@ -2,8 +2,12 @@ import Prism from "prismjs" import "prismjs/components/prism-json" import "prismjs/components/prism-yaml" +import "prismjs/components/prism-python" +import "prismjs/components/prism-javascript" import type {Token as PrismToken} from "prismjs" +import type {CodeLanguage} from "../types" + /** * Represents a syntax token with content and type. * Used for syntax highlighting in the code editor. @@ -26,8 +30,19 @@ export interface Token { * @param language - The language to use for tokenization ('json' or 'yaml') * @returns Array of tokens with content and type */ -export function tokenizeCodeLine(line: string, language: "json" | "yaml"): Token[] { - const grammar = Prism.languages[language] +const LANGUAGE_GRAMMAR_MAP: Record = { + json: "json", + yaml: "yaml", + code: "python", +} + +export function tokenizeCodeLine(line: string, language: CodeLanguage): Token[] { + const targetGrammar = LANGUAGE_GRAMMAR_MAP[language] + const grammar = + Prism.languages[targetGrammar] ?? + Prism.languages.javascript ?? + Prism.languages.clike ?? + null if (!grammar) return [{content: line, type: "plain"}] const rawTokens = Prism.tokenize(line, grammar) diff --git a/web/oss/src/components/Editor/plugins/code/utils/validationUtils.ts b/web/oss/src/components/Editor/plugins/code/utils/validationUtils.ts index 3025690aae..7ce9a1ce31 100644 --- a/web/oss/src/components/Editor/plugins/code/utils/validationUtils.ts +++ b/web/oss/src/components/Editor/plugins/code/utils/validationUtils.ts @@ -1,6 +1,8 @@ import yaml from "js-yaml" import JSON5 from "json5" +import type {CodeLanguage} from "../types" + // Enhanced validation functions for irregular and chaotic input detection /** @@ -247,7 +249,7 @@ export interface ErrorInfo { export function validateAll( textContent: string, schema?: any, - language: "json" | "yaml" = "json", + language: CodeLanguage = "json", _editedLineContent?: string, cleanedToOriginalLineMap?: Map, ): { @@ -271,6 +273,16 @@ export function validateAll( } } + if (language === "code") { + return { + allErrors: [], + errorsByLine: new Map(), + structuralErrors: [], + bracketErrors: [], + schemaErrors: [], + } + } + // 1. Try native parsing first (fast path for valid content) try { if (language === "json") { @@ -1019,10 +1031,14 @@ function validateSchema( textContent: string, schema: any, lines: string[], - language: "json" | "yaml" = "json", + language: CodeLanguage = "json", ): ErrorInfo[] { const errors: ErrorInfo[] = [] + if (language === "code") { + return errors + } + try { // For schema validation, we'll use a simple approach: // Always check for missing required properties regardless of JSON validity @@ -1140,7 +1156,7 @@ function validateSchema( function findPropertyLine( lines: string[], propertyName: string, - language: "json" | "yaml" = "json", + language: CodeLanguage = "json", ): number { for (let i = 0; i < lines.length; i++) { const line = lines[i] @@ -1149,7 +1165,7 @@ function findPropertyLine( if (line.includes(`"${propertyName}"`)) { return i + 1 } - } else { + } else if (language === "yaml") { // YAML can use both quoted and unquoted property names if (line.includes(`"${propertyName}"`) || line.includes(`${propertyName}:`)) { return i + 1 diff --git a/web/oss/src/components/Editor/types.d.ts b/web/oss/src/components/Editor/types.d.ts index 2396135181..73f55399a6 100644 --- a/web/oss/src/components/Editor/types.d.ts +++ b/web/oss/src/components/Editor/types.d.ts @@ -26,7 +26,7 @@ export interface EditorProps extends React.HTMLProps { singleLine?: boolean autoFocus?: boolean codeOnly?: boolean - language?: "json" | "yaml" + language?: "json" | "yaml" | "code" showToolbar?: boolean enableTokens?: boolean tokens?: string[] @@ -58,7 +58,7 @@ export interface EditorPluginsProps { autoFocus?: boolean enableTokens: boolean debug: boolean - language?: "json" | "yaml" + language?: "json" | "yaml" | "code" placeholder?: string /** Initial text value for the editor */ initialValue: string diff --git a/web/oss/src/components/EnhancedUIs/Table/index.tsx b/web/oss/src/components/EnhancedUIs/Table/index.tsx index ce6c84d21d..821f61183c 100644 --- a/web/oss/src/components/EnhancedUIs/Table/index.tsx +++ b/web/oss/src/components/EnhancedUIs/Table/index.tsx @@ -238,7 +238,7 @@ const EnhancedTableInner = { - if (loading && (!dataSource || (Array.isArray(dataSource) && dataSource.length === 0))) { + if (loading) { return skeletonData } return dataSource diff --git a/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/assets/LoadTestsetModalContent/index.tsx b/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/assets/LoadTestsetModalContent/index.tsx index c42e969b78..8d82abbcea 100644 --- a/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/assets/LoadTestsetModalContent/index.tsx +++ b/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/assets/LoadTestsetModalContent/index.tsx @@ -1,4 +1,5 @@ -import NoResultsFound from "@/oss/components/NoResultsFound/NoResultsFound" +import dynamic from "next/dynamic" + import {Expandable} from "@/oss/components/Tables/ExpandableCell" import {getStringOrJson} from "@/oss/lib/helpers/utils" import {TestSet, testset} from "@/oss/lib/Types" @@ -7,7 +8,7 @@ import {useTestsetsData} from "@/oss/state/testset" import {urlAtom} from "@/oss/state/url" import {appUriInfoAtom} from "@/oss/state/variant/atoms/fetcher" import {useQueryClient} from "@tanstack/react-query" -import {Alert, Checkbox, Divider, Input, Menu, Table, Tooltip, Typography} from "antd" +import {Checkbox, Divider, Input, Menu, Tooltip, Typography} from "antd" import {ColumnsType} from "antd/es/table" import {useAtomValue} from "jotai" import {memo, useCallback, useEffect, useMemo, useState} from "react" @@ -15,6 +16,11 @@ import {useRouter} from "next/router" import clsx from "clsx" import {useTestsetInputsAnalysis} from "../../hooks/useTestsetInputsAnalysis" import {LoadTestsetModalContentProps} from "../types" +import EnhancedTable from "@/oss/components/EnhancedUIs/Table" + +const NoResultsFound = dynamic(() => import("@/oss/components/NoResultsFound/NoResultsFound"), { + ssr: false, +}) const LoadTestsetModalContent = ({ modalProps, @@ -26,7 +32,7 @@ const LoadTestsetModalContent = ({ isLoadingTestset, isChat, }: LoadTestsetModalContentProps) => { - const {testsets, columnsByTestsetId} = useTestsetsData({enabled: modalProps.open}) + const {testsets, columnsByTestsetId, isLoading} = useTestsetsData({enabled: modalProps.open}) const queryClient = useQueryClient() const router = useRouter() @@ -196,6 +202,7 @@ const LoadTestsetModalContent = ({ // Prefetch CSV data for the first N visible testsets to populate column cache useEffect(() => { if (!modalProps.open) return + const BATCH = 8 const list = (filteredTestset.length ? filteredTestset : testsets).slice(0, BATCH) list.forEach((ts: any) => { @@ -258,10 +265,17 @@ const LoadTestsetModalContent = ({ ) const columnDef = useMemo(() => { + if (!testsetCsvData.length) { + return [ + {title: "-", width: 300}, + {title: "-", width: 300}, + ] + } + const columns: ColumnsType = [] if (testsetCsvData.length > 0) { - const keys = Object.keys(testsetCsvData[0]) + const keys = Object.keys(testsetCsvData[0]).filter((key) => key !== "testcase_dedup_id") columns.push( ...keys.map((key, index) => ({ @@ -298,7 +312,14 @@ const LoadTestsetModalContent = ({ return columns }, [selectionWarningMessage, testsetCsvData]) + const dataSource = useMemo(() => { + if (!testsetCsvData.length) return [] + return testsetCsvData.map((data, index) => ({...data, id: index})) + }, [testsetCsvData]) + const menuItems = useMemo(() => { + if (!filteredTestset.length) return [] + const items = filteredTestset.map((ts: testset) => { const diagnostics = compatibilityByTestset[ts._id] const columnsKnown = diagnostics?.columnsKnown ?? false @@ -318,7 +339,7 @@ const LoadTestsetModalContent = ({ {ts.name} @@ -361,7 +382,7 @@ const LoadTestsetModalContent = ({ const menuSelectedKeys = selectedTestset ? [selectedTestset] : [] - if (!testsets.length) + if (!testsets.length && !testsetCsvData.length && !isLoadingTestset && !isLoading) return (
- + Select a testcase
- ({...data, id: index}))} + ({ className: "cursor-pointer", title: selectionWarningMessage, diff --git a/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/index.tsx b/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/index.tsx index a91b984801..94228a1003 100644 --- a/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/index.tsx +++ b/web/oss/src/components/Playground/Components/Modals/LoadTestsetModal/index.tsx @@ -55,7 +55,7 @@ const LoadTestsetModal: React.FC = ({ return ( { setSelectedRowKeys([]) afterClose?.() diff --git a/web/oss/src/components/Playground/Components/Shared/PromptMessageHeader.tsx b/web/oss/src/components/Playground/Components/Shared/PromptMessageHeader.tsx index 9fa83a7211..2b2bf93dec 100644 --- a/web/oss/src/components/Playground/Components/Shared/PromptMessageHeader.tsx +++ b/web/oss/src/components/Playground/Components/Shared/PromptMessageHeader.tsx @@ -98,7 +98,7 @@ const PromptMessageHeader: React.FC = ({ if (isFunction) { return ( -
+
{rolePropertyId ? ( = ({ ) : null} {!disabled && commonRight}
-
+
{functionNamePropertyId ? ( @@ -130,7 +130,7 @@ const PromptMessageHeader: React.FC = ({ variantId={variantId} rowId={rowId} as="SimpleInput" - className="message-user-select px-0 text-right" + className="message-user-select px-2 text-right" disabled={disabled} placeholder="Tool call id" /> diff --git a/web/oss/src/state/testset/index.tsx b/web/oss/src/state/testset/index.tsx index 1a018f9dd8..62d20f7a37 100644 --- a/web/oss/src/state/testset/index.tsx +++ b/web/oss/src/state/testset/index.tsx @@ -7,20 +7,24 @@ import axios from "@/oss/lib/api/assets/axiosConfig" import {getAgentaApiUrl} from "@/oss/lib/helpers/api" import {TestSet} from "@/oss/lib/Types" -import {previewTestsetsQueryAtom, testsetsQueryAtom} from "./atoms/fetcher" +import {previewTestsetsQueryAtom, testsetsQueryAtomFamily} from "./atoms/fetcher" import {useTestset} from "./hooks/useTestset" /** * Hook for regular/legacy testsets */ export const useTestsetsData = ({enabled = true} = {}) => { - const [{data: testsets, isPending, refetch, error, isError}] = useAtom(testsetsQueryAtom) + const [{data: testsets, isPending, isLoading, refetch, error, isError}] = useAtom( + testsetsQueryAtomFamily({enabled}), + ) const queryClient = useQueryClient() const [columnsFallback, setColumnsFallback] = useState>({}) const [csvVersion, setCsvVersion] = useState(0) + const [isValidating, setIsValidating] = useState(false) // Extract CSV columns from the TanStack Query cache for any testset const cachedColumnsByTestsetId = useMemo(() => { + if (!enabled) return {} const result: Record = {} ;(testsets ?? []).forEach((ts: any) => { const csv = queryClient.getQueryData(["testsetCsvData", ts?._id]) @@ -43,6 +47,7 @@ export const useTestsetsData = ({enabled = true} = {}) => { // Merge cache with fallback (from preview single testcase query) const columnsByTestsetId = useMemo(() => { + if (!enabled) return {} const merged: Record = {...cachedColumnsByTestsetId} Object.entries(columnsFallback).forEach(([id, cols]) => { if (!merged[id] || (merged[id]?.length ?? 0) === 0) { @@ -55,6 +60,9 @@ export const useTestsetsData = ({enabled = true} = {}) => { // Background fill: for testsets without cached columns, fetch a single testcase to infer columns const triedRef = useRef>(new Set()) useEffect(() => { + if (!enabled) return + if (isPending || isLoading) return + const controller = new AbortController() const tried = triedRef.current const run = async () => { @@ -74,6 +82,8 @@ export const useTestsetsData = ({enabled = true} = {}) => { await Promise.all( toFetch.map(async (ts: any) => { try { + setIsValidating(true) + const url = `${getAgentaApiUrl()}/preview/testcases/query` const {data} = await axios.post( url, @@ -97,7 +107,7 @@ export const useTestsetsData = ({enabled = true} = {}) => { if (cols.length) { setColumnsFallback((prev) => ({...prev, [ts._id]: cols})) // Also hydrate the primary cache so all consumers see columns immediately - queryClient.setQueryData(["testsetCsvData", ts._id], [dataObj]) + // queryClient.setQueryData(["testsetCsvData", ts._id], [dataObj]) } else { tried.add(ts._id) } @@ -105,13 +115,16 @@ export const useTestsetsData = ({enabled = true} = {}) => { // swallow; keep fallback empty for this id tried.add(ts._id) // console.warn("Failed to infer columns for testset", ts?._id, e) + } finally { + setIsValidating(false) } }), ) + } run() return () => controller.abort() - }, [testsets, columnsByTestsetId, columnsFallback]) + }, [testsets, columnsByTestsetId, columnsFallback, isPending, isLoading]) // When any testsetCsvData query updates, bump csvVersion useEffect(() => { @@ -130,7 +143,7 @@ export const useTestsetsData = ({enabled = true} = {}) => { testsets: testsets ?? [], isError, error, - isLoading: isPending, + isLoading: isPending || isLoading || isValidating, mutate: refetch, // New helpers (non-breaking): columnsByTestsetId, diff --git a/web/oss/src/styles/code-editor-styles.css b/web/oss/src/styles/code-editor-styles.css index 7f28485ac3..b35b1bf5bf 100644 --- a/web/oss/src/styles/code-editor-styles.css +++ b/web/oss/src/styles/code-editor-styles.css @@ -173,12 +173,15 @@ .editor-code.language-json .editor-code-line[data-old-line-number], .editor-code.language-json .editor-code-line[data-new-line-number], .editor-code.language-yaml .editor-code-line[data-old-line-number], - .editor-code.language-yaml .editor-code-line[data-new-line-number] { + .editor-code.language-yaml .editor-code-line[data-new-line-number], + .editor-code.language-code .editor-code-line[data-old-line-number], + .editor-code.language-code .editor-code-line[data-new-line-number] { padding-left: 64px; } .editor-code.language-json .editor-code-line[data-old-line-number]::before, - .editor-code.language-yaml .editor-code-line[data-old-line-number]::before { + .editor-code.language-yaml .editor-code-line[data-old-line-number]::before, + .editor-code.language-code .editor-code-line[data-old-line-number]::before { content: attr(data-old-line-number) !important; padding-right: 4px; box-sizing: border-box; @@ -199,7 +202,8 @@ } .editor-code.language-json .editor-code-line[data-new-line-number]::after, - .editor-code.language-yaml .editor-code-line[data-new-line-number]::after { + .editor-code.language-yaml .editor-code-line[data-new-line-number]::after, + .editor-code.language-code .editor-code-line[data-new-line-number]::after { content: attr(data-new-line-number) !important; padding-right: 0px; padding-left: 4px; @@ -458,4 +462,4 @@ margin-bottom: 0px; font-size: 10px; } -} \ No newline at end of file +} diff --git a/web/package.json b/web/package.json index 68d5ab27f4..ce1ef87cde 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "agenta-web", - "version": "0.59.10", + "version": "0.59.11", "workspaces": [ "ee", "oss",