diff --git a/shared/schemaGraph/index.ts b/shared/schemaGraph/index.ts index 9fb9c72b..732112e0 100644 --- a/shared/schemaGraph/index.ts +++ b/shared/schemaGraph/index.ts @@ -1,7 +1,7 @@ export * from "./state/query"; export * from "./state/interfaces"; -export {Schema} from "./state"; +export {Schema, type SchemaState} from "./state"; export {schemaContext} from "./state/provider"; diff --git a/shared/studio/components/lazyTabs/lazyTabs.module.scss b/shared/studio/components/lazyTabs/lazyTabs.module.scss new file mode 100644 index 00000000..8c1898b0 --- /dev/null +++ b/shared/studio/components/lazyTabs/lazyTabs.module.scss @@ -0,0 +1,83 @@ +@import "@edgedb/common/mixins.scss"; + +.tabWrapper { + position: relative; + flex-grow: 1; + background-color: var(--app_panel_background); + border-radius: 12px; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.04), 0 0 4px rgba(0, 0, 0, 0.06); + display: flex; + min-width: 0; + min-height: 0; + font-family: "Roboto Flex Variable", sans-serif; + + @include isMobile { + flex-direction: column; + border-radius: 0; + } + + @include darkTheme { + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2), + 0px 0px 4px 0px rgba(0, 0, 0, 0.3); + } +} + +.loadingSchema { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + font-style: italic; +} + +.fallbackSpinner { + margin: auto; +} + +.extDisabled { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + margin: auto; + font-family: "Roboto Flex Variable", sans-serif; + color: #4d4d4d; + + h2 { + color: inherit; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 26px; + margin: 0; + } + + p { + margin: 8px 0; + } + + pre { + background: #eee; + margin: 12px; + padding: 12px 16px; + border-radius: 6px; + user-select: text; + + @include darkTheme { + background: #383838; + } + } + + a { + color: #b044c2; + + @include darkTheme { + color: #ac86f6; + } + } + + @include darkTheme { + color: #ccc; + } +} diff --git a/shared/studio/state/database.ts b/shared/studio/state/database.ts index 583f4731..b7707d2d 100644 --- a/shared/studio/state/database.ts +++ b/shared/studio/state/database.ts @@ -16,6 +16,7 @@ import { _async, _await, frozen, + modelAction, } from "mobx-keystone"; import { @@ -82,6 +83,19 @@ export class DatabaseState extends Model({ @observable currentRole: string | null = null; + _getTabState(modelType: string, stateClass: ModelClass) { + if (!this.tabStates.has(modelType)) { + this._initTabState(stateClass); + } + return this.tabStates.get(modelType)!; + } + + @modelAction + private _initTabState(stateClass: ModelClass) { + const state = new stateClass({}); + this.tabStates.set(state.$modelType, state); + } + @observable loadingTabs = new Map(); diff --git a/shared/studio/state/index.ts b/shared/studio/state/index.ts index f713ae0a..1bd2f2d0 100644 --- a/shared/studio/state/index.ts +++ b/shared/studio/state/index.ts @@ -9,5 +9,5 @@ export {useDatabaseState}; export function useTabState(stateClass: ModelClass): T { const modelType = (getTypeInfo(stateClass) as ModelTypeInfo).modelType; - return useDatabaseState().tabStates.get(modelType) as T; + return useDatabaseState()._getTabState(modelType, stateClass) as T; } diff --git a/shared/studio/tabs/ai/ai.tsx b/shared/studio/tabs/ai/ai.tsx new file mode 100644 index 00000000..fe5f250c --- /dev/null +++ b/shared/studio/tabs/ai/ai.tsx @@ -0,0 +1,96 @@ +import {useEffect, useLayoutEffect} from "react"; +import {Observer, observer} from "mobx-react-lite"; + +import cn from "@edgedb/common/utils/classNames"; + +import {useTabState} from "../../state"; +import {useDBRouter} from "../../hooks/dbRoute"; + +import {AIAdminState} from "./state"; +import {ProvidersTab} from "./providers"; +import {PlaygroundTab} from "./playground"; +import {PromptsTab} from "./prompts"; + +import styles from "./aiAdmin.module.scss"; +import {WarningIcon} from "@edgedb/common/newui"; + +const aiAdminTabs = [ + { + path: "", + label: "Playground", + element: , + }, + { + path: "prompts", + label: "Prompts", + element: , + }, + { + path: "providers", + label: "Providers", + element: , + warning: ( + { + const state = useTabState(AIAdminState); + return state.indexesWithoutProviders?.length ? ( + + ) : null; + }} + /> + ), + }, +]; + +const AIAdminPage = observer(function AIAdminPage() { + const state = useTabState(AIAdminState); + + useEffect(() => { + state.refreshConfig(); + }, []); + + const {navigate, currentPath} = useDBRouter(); + + const activePath = currentPath.slice(2).join("/"); + + useEffect(() => { + state.setLastSelectedTab(currentPath.slice(2).join("/")); + }, [currentPath]); + + useLayoutEffect(() => { + if (currentPath.length == 2 && state.lastSelectedTab) { + navigate(`${currentPath.join("/")}/${state.lastSelectedTab}`, true); + } + }, []); + + return ( +
+
+ {aiAdminTabs.map((tab) => ( +
{ + navigate( + [...currentPath.slice(0, 2), tab.path] + .join("/") + .replace(/\/$/, "") + ); + }} + > + {tab.label} + {tab.warning} +
+ ))} +
+ +
+ {aiAdminTabs.find((tab) => tab.path === activePath)?.element} +
+
+ ); +}); + +export default AIAdminPage; diff --git a/shared/studio/tabs/ai/aiAdmin.module.scss b/shared/studio/tabs/ai/aiAdmin.module.scss index 33c4c4bc..5d5a252c 100644 --- a/shared/studio/tabs/ai/aiAdmin.module.scss +++ b/shared/studio/tabs/ai/aiAdmin.module.scss @@ -1,81 +1,5 @@ @import "@edgedb/common/mixins.scss"; -.aiAdmin { - position: relative; - flex-grow: 1; - border-radius: 12px; - background: #fbfbfb; - color: #4d4d4d; - box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.04), - 0px 0px 4px 0px rgba(0, 0, 0, 0.06); - display: flex; - justify-content: center; - gap: 32px; - min-width: 0; - min-height: 0; - font-family: "Roboto Flex Variable", sans-serif; - - h2 { - color: inherit; - font-size: 18px; - font-style: normal; - font-weight: 500; - line-height: 26px; - margin: 0; - } - - @include darkTheme { - color: #ccc; - background: var(--Grey14, #242424); - box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.2), - 0px 0px 4px 0px rgba(0, 0, 0, 0.3); - } - - @include isMobile { - border-radius: 0; - } -} - -.loadingSchema { - flex-grow: 1; - display: flex; - align-items: center; - justify-content: center; - font-style: italic; -} - -.extDisabled { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - - p { - margin: 8px 0; - } - - pre { - background: #eee; - margin: 12px; - padding: 12px 16px; - border-radius: 6px; - user-select: text; - - @include darkTheme { - background: #383838; - } - } - - a { - color: #b044c2; - - @include darkTheme { - color: #ac86f6; - } - } -} - .mainLayout { flex-grow: 1; display: grid; @@ -94,6 +18,15 @@ min-height: 0; min-width: 0; + h2 { + color: inherit; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 26px; + margin: 0; + } + @include isMobile { grid-column: 1; } @@ -168,8 +101,10 @@ justify-self: stretch; flex-direction: row; justify-content: center; + background: var(--header_background); padding: 4px 8px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04), 0 2px 3px rgba(0, 0, 0, 0.03); + z-index: 2; .tab { display: flex; @@ -1079,7 +1014,7 @@ &:before { content: ""; position: absolute; - background: #fbfbfb; + background: var(--app_panel_background); top: -16px; left: -1px; right: -1px; diff --git a/shared/studio/tabs/ai/index.tsx b/shared/studio/tabs/ai/index.tsx index 4311522a..c6da1866 100644 --- a/shared/studio/tabs/ai/index.tsx +++ b/shared/studio/tabs/ai/index.tsx @@ -1,37 +1,34 @@ -import {useEffect, useLayoutEffect} from "react"; -import {Observer, observer} from "mobx-react-lite"; +import {lazy, Suspense} from "react"; +import {observer} from "mobx-react-lite"; -import cn from "@edgedb/common/utils/classNames"; import CodeBlock from "@edgedb/common/ui/codeBlock"; +import Spinner from "@edgedb/common/ui/spinner"; + +import {useDatabaseState} from "../../state"; -import {useTabState} from "../../state"; -import {useDBRouter} from "../../hooks/dbRoute"; import {DatabaseTabSpec} from "../../components/databasePage"; import {TabAIIcon} from "../../icons"; -import {AIAdminState} from "./state"; -import {ProvidersTab} from "./providers"; -import {PlaygroundTab} from "./playground"; -import {PromptsTab} from "./prompts"; +import styles from "../../components/lazyTabs/lazyTabs.module.scss"; -import styles from "./aiAdmin.module.scss"; -import {WarningIcon} from "@edgedb/common/newui"; +const AIAdminPage = lazy(() => import("./ai")); -const AIAdminPage = observer(function AIAdminPage() { - const state = useTabState(AIAdminState); +const AuthAdminLoader = observer(function AuthAdminLoader() { + const db = useDatabaseState(); - useEffect(() => { - if (state.extEnabled) { - state.refreshConfig(); - } - }, [state.extEnabled]); + const extEnabled = + db.schemaData?.extensions.some((ext) => ext.name === "ai") ?? null; return ( -
- {state.extEnabled === null ? ( +
+ {extEnabled === null ? (
Loading schema...
- ) : state.extEnabled ? ( - + ) : extEnabled ? ( + } + > + + ) : (

The AI extension is not enabled

@@ -55,81 +52,6 @@ export const aiTabSpec: DatabaseTabSpec = { label: "AI", icon: (active) => , usesSessionState: false, - element: , + element: , allowNested: true, - state: AIAdminState, }; - -const aiAdminTabs = [ - { - path: "", - label: "Playground", - element: , - }, - { - path: "prompts", - label: "Prompts", - element: , - }, - { - path: "providers", - label: "Providers", - element: , - warning: ( - { - const state = useTabState(AIAdminState); - return state.indexesWithoutProviders?.length ? ( - - ) : null; - }} - /> - ), - }, -]; - -function AIAdminLayout() { - const state = useTabState(AIAdminState); - const {navigate, currentPath} = useDBRouter(); - - const activePath = currentPath.slice(2).join("/"); - - useEffect(() => { - state.setLastSelectedTab(currentPath.slice(2).join("/")); - }, [currentPath]); - - useLayoutEffect(() => { - if (currentPath.length == 2 && state.lastSelectedTab) { - navigate(`${currentPath.join("/")}/${state.lastSelectedTab}`, true); - } - }, []); - - return ( -
-
- {aiAdminTabs.map((tab) => ( -
{ - navigate( - [...currentPath.slice(0, 2), tab.path] - .join("/") - .replace(/\/$/, "") - ); - }} - > - {tab.label} - {tab.warning} -
- ))} -
- -
- {aiAdminTabs.find((tab) => tab.path === activePath)?.element} -
-
- ); -} diff --git a/shared/studio/tabs/ai/prompts.tsx b/shared/studio/tabs/ai/prompts.tsx index a88724fb..3c9f97b6 100644 --- a/shared/studio/tabs/ai/prompts.tsx +++ b/shared/studio/tabs/ai/prompts.tsx @@ -16,6 +16,7 @@ import {useTabState} from "../../state"; import {AIAdminState, AIPromptDraft, PromptChatParticipantRole} from "./state"; import textStyles from "@edgedb/common/newui/textInput/textInput.module.scss"; +import fieldHeaderStyles from "@edgedb/common/newui/fieldHeader/fieldHeader.module.scss"; import styles from "./aiAdmin.module.scss"; export const PromptsTab = observer(function PromptTab() { @@ -210,7 +211,7 @@ const PromptMessageCard = observer(function PromptMessageCard({ />