From d4e7f3679cd22a8083be037ba870af742cf5b069 Mon Sep 17 00:00:00 2001 From: Tat Dat Duong Date: Tue, 21 Nov 2023 01:51:16 +0100 Subject: [PATCH] Revert "Refactor schema loading to deal with race conditions, get rid of embedding for now" This reverts commit 099fc666e53f8bc562acce7accfbad591ba0113f. --- examples/configurable_chain/server.py | 23 +- langserve/playground/package.json | 1 - langserve/playground/src/App.tsx | 581 +++++++++++++----- .../src/components/CustomAnyOfRenderer.tsx | 2 +- .../playground/src/components/ShareDialog.tsx | 31 +- .../src/components/SubmitButton.tsx | 50 -- langserve/playground/src/main.tsx | 7 - langserve/playground/src/renderers.tsx | 111 ---- .../src/sections/SectionConfigure.tsx | 50 -- .../playground/src/sections/SectionInputs.tsx | 64 -- langserve/playground/src/useSchemas.tsx | 104 ++-- .../playground/src/useStreamCallback.tsx | 29 - langserve/playground/src/utils/url.ts | 32 +- langserve/playground/yarn.lock | 18 - 14 files changed, 512 insertions(+), 591 deletions(-) delete mode 100644 langserve/playground/src/components/SubmitButton.tsx delete mode 100644 langserve/playground/src/renderers.tsx delete mode 100644 langserve/playground/src/sections/SectionConfigure.tsx delete mode 100644 langserve/playground/src/sections/SectionInputs.tsx diff --git a/examples/configurable_chain/server.py b/examples/configurable_chain/server.py index a2fe2af8..1050fff1 100755 --- a/examples/configurable_chain/server.py +++ b/examples/configurable_chain/server.py @@ -63,28 +63,7 @@ ############################################################################### -# EXAMPLE 2: Configure prompt based on RunnableConfig # -############################################################################### -configurable_prompt = PromptTemplate.from_template( - "tell me a joke about {topic}." -).configurable_alternatives( - ConfigurableField( - id="prompt", - name="Prompt", - description="The prompt to use. Must contain {topic}.", - ), - default_key="joke", - fact=PromptTemplate.from_template( - "tell me a fact about {topic} in {language} language." - ), -) -prompt_chain = configurable_prompt | model | StrOutputParser() - -add_routes(app, prompt_chain, path="/configurable_prompt") - - -############################################################################### -# EXAMPLE 3: Configure fields based on Request metadata # +# EXAMPLE 2: Configure fields based on Request metadata # ############################################################################### diff --git a/langserve/playground/package.json b/langserve/playground/package.json index dc374164..30cf5485 100644 --- a/langserve/playground/package.json +++ b/langserve/playground/package.json @@ -28,7 +28,6 @@ "lz-string": "^1.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "swr": "^2.2.4", "tailwind-merge": "^1.14.0", "use-debounce": "^9.0.4", "vaul": "^0.7.3" diff --git a/langserve/playground/src/App.tsx b/langserve/playground/src/App.tsx index 5693d975..f78d03c8 100644 --- a/langserve/playground/src/App.tsx +++ b/langserve/playground/src/App.tsx @@ -1,54 +1,260 @@ import "./App.css"; -import { useEffect, useRef, useState } from "react"; - +import { useEffect, useMemo, useRef, useState } from "react"; +import defaults from "./utils/defaults"; +import { JsonForms } from "@jsonforms/react"; +import { + materialAllOfControlTester, + MaterialAllOfRenderer, + MaterialObjectRenderer, + materialOneOfControlTester, + MaterialOneOfRenderer, +} from "@jsonforms/material-renderers"; +import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; +import relativeDate from "dayjs/plugin/relativeTime"; +import SendIcon from "./assets/SendIcon.svg?react"; import ShareIcon from "./assets/ShareIcon.svg?react"; +import { compressToEncodedURIComponent } from "lz-string"; -import { useConfigSchema, useInputSchema } from "./useSchemas"; +import { + BooleanCell, + DateCell, + DateTimeCell, + EnumCell, + IntegerCell, + NumberCell, + SliderCell, + TimeCell, + booleanCellTester, + dateCellTester, + dateTimeCellTester, + enumCellTester, + integerCellTester, + numberCellTester, + sliderCellTester, + textAreaCellTester, + textCellTester, + timeCellTester, + vanillaRenderers, + InputControl, +} from "@jsonforms/vanilla-renderers"; +import { useSchemas } from "./useSchemas"; import { useStreamLog } from "./useStreamLog"; -import { AppCallbackContext, useAppStreamCallbacks } from "./useStreamCallback"; -import { JsonSchema } from "@jsonforms/core"; -import { ShareDialog } from "./components/ShareDialog"; +import { StreamCallback } from "./types"; +import { AppCallbackContext } from "./useStreamCallback"; +import { + JsonFormsCore, + RankedTester, + rankWith, + and, + uiTypeIs, + schemaMatches, + schemaTypeIs, +} from "@jsonforms/core"; +import CustomArrayControlRenderer, { + materialArrayControlTester, +} from "./components/CustomArrayControlRenderer"; +import CustomTextAreaCell from "./components/CustomTextAreaCell"; +import JsonTextAreaCell from "./components/JsonTextAreaCell"; +import { getStateFromUrl, ShareDialog } from "./components/ShareDialog"; +import { + chatMessagesTester, + ChatMessagesControlRenderer, +} from "./components/ChatMessagesControlRenderer"; +import { + ChatMessageTuplesControlRenderer, + chatMessagesTupleTester, +} from "./components/ChatMessageTuplesControlRenderer"; +import { + fileBase64Tester, + FileBase64ControlRenderer, +} from "./components/FileBase64Tester"; import { IntermediateSteps } from "./components/IntermediateSteps"; import { StreamOutput } from "./components/StreamOutput"; -import { ConfigValue, SectionConfigure } from "./sections/SectionConfigure"; -import { InputValue, SectionInputs } from "./sections/SectionInputs"; -import { SubmitButton } from "./components/SubmitButton"; -import { useDebounce } from "use-debounce"; +import { + customAnyOfTester, + CustomAnyOfRenderer, +} from "./components/CustomAnyOfRenderer"; import { cn } from "./utils/cn"; -import { getStateFromUrl } from "./utils/url"; -function InputPlayground(props: { - configSchema: { schema: JsonSchema; defaults: unknown }; - inputSchema: { schema: JsonSchema; defaults: unknown }; +dayjs.extend(relativeDate); +dayjs.extend(utc); + +const isObjectWithPropertiesControl = rankWith( + 2, + and( + uiTypeIs("Control"), + schemaTypeIs("object"), + schemaMatches((schema) => + Object.prototype.hasOwnProperty.call(schema, "properties") + ) + ) +); + +const isObject = rankWith(1, and(uiTypeIs("Control"), schemaTypeIs("object"))); +const isElse = rankWith(1, and(uiTypeIs("Control"))); + +export const renderers = [ + ...vanillaRenderers, + + // use material renderers to handle objects and json schema references + // they should yield the rendering to simpler cells + { tester: isObjectWithPropertiesControl, renderer: MaterialObjectRenderer }, + { tester: materialAllOfControlTester, renderer: MaterialAllOfRenderer }, + { tester: materialOneOfControlTester, renderer: MaterialOneOfRenderer }, + + { tester: customAnyOfTester, renderer: CustomAnyOfRenderer }, + + // custom renderers + { tester: materialArrayControlTester, renderer: CustomArrayControlRenderer }, + { tester: isObject, renderer: InputControl }, + { tester: chatMessagesTester, renderer: ChatMessagesControlRenderer }, + { + tester: chatMessagesTupleTester, + renderer: ChatMessageTuplesControlRenderer, + }, + { tester: fileBase64Tester, renderer: FileBase64ControlRenderer }, +]; + +const nestedArrayControlTester: RankedTester = rankWith(1, (_, jsonSchema) => { + return jsonSchema.type === "array"; +}); + +export const cells = [ + { tester: booleanCellTester, cell: BooleanCell }, + { tester: dateCellTester, cell: DateCell }, + { tester: dateTimeCellTester, cell: DateTimeCell }, + { tester: enumCellTester, cell: EnumCell }, + { tester: integerCellTester, cell: IntegerCell }, + { tester: numberCellTester, cell: NumberCell }, + { tester: sliderCellTester, cell: SliderCell }, + { tester: textAreaCellTester, cell: CustomTextAreaCell }, + { tester: textCellTester, cell: CustomTextAreaCell }, + { tester: timeCellTester, cell: TimeCell }, + { tester: nestedArrayControlTester, cell: CustomArrayControlRenderer }, + { tester: isElse, cell: JsonTextAreaCell }, +]; - configData: ConfigValue; +function App() { + const [isEmbedded] = useState(() => + window.location.search.includes("embeded=true") + ); + + // it is possible that defaults are being applied _after_ + // the initial update message has been sent from the parent window + // so we store the initial config data in a ref + const initConfigData = useRef(null); - startStream: (input: unknown, config: unknown) => void; - stopStream: (() => void) | undefined; + // store form state + const [configData, setConfigData] = useState< + Pick & { defaults: boolean } + >({ data: {}, errors: [], defaults: true }); - children?: React.ReactNode; -}) { - const [inputData, setInputData] = useState({ - data: props.inputSchema.defaults, - errors: [], + const [inputData, setInputData] = useState< + Pick + >({ data: null, errors: [] }); + // fetch input and config schemas from the server + const schemas = useSchemas(configData); + // apply defaults defined in each schema + useEffect(() => { + if (schemas.config) { + const state = getStateFromUrl(window.location.href); + setConfigData({ + data: + state.configFromUrl ?? + initConfigData.current ?? + defaults(schemas.config), + errors: [], + defaults: true, + }); + + setInputData({ data: defaults(schemas.input), errors: [] }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [schemas.config]); + + // callbacks handling + const callbacks = useRef<{ + onStart: Exclude[]; + onSuccess: Exclude[]; + onError: Exclude[]; + }>({ onStart: [], onSuccess: [], onError: [] }); + + // the runner + const { startStream, stopStream, latest } = useStreamLog({ + onStart(...args) { + for (const callback of callbacks.current.onStart) { + callback(...args); + } + }, + onSuccess(...args) { + for (const callback of callbacks.current.onSuccess) { + callback(...args); + } + }, + onError(...args) { + for (const callback of callbacks.current.onError) { + callback(...args); + } + }, }); - const submitRef = useRef<(() => void) | null>(null); - submitRef.current = () => { + useEffect(() => { + window.parent?.postMessage({ type: "init" }, "*"); + }, []); + + useEffect(() => { + function listener(event: MessageEvent) { + if (event.source === window.parent) { + const message = event.data; + if (typeof message === "object" && message != null) { + switch (message.type) { + case "update": { + const value: { config: JsonFormsCore["data"] } = message.value; + if (Object.keys(value.config).length > 0) { + initConfigData.current = value.config; + setConfigData({ + data: value.config, + errors: [], + defaults: false, + }); + break; + } + } + } + } + } + } + + window.addEventListener("message", listener); + return () => window.removeEventListener("message", listener); + }, []); + + const isInputResetable = useMemo(() => { + if (!schemas.input) return false; + return ( + JSON.stringify(defaults(schemas.input)) !== JSON.stringify(inputData.data) + ); + }, [schemas.input, inputData.data]); + + function onSubmit() { if ( - !props.stopStream && - (!!inputData.errors?.length || !!props.configData.errors?.length) + !stopStream && + (!!inputData.errors?.length || !!configData.errors?.length) ) { return; } - if (props.stopStream) { - props.stopStream(); + if (stopStream) { + stopStream(); } else { - props.startStream(inputData.data, props.configData.data); + startStream(inputData.data, configData.data); } - }; + } + + const submitRef = useRef<(() => void) | null>(null); + submitRef.current = onSubmit; useEffect(() => { window.addEventListener("keydown", (e) => { @@ -60,147 +266,206 @@ function InputPlayground(props: { }, []); const isSendDisabled = - !props.stopStream && - (!!inputData.errors?.length || !!props.configData.errors?.length); + !stopStream && (!!inputData.errors?.length || !!configData.errors?.length); + + if (!schemas.config || !schemas.input) { + return <>; + } return ( - <> - setInputData(input)} - /> - - {props.children} - -
- -
-
- - - - - - -
- - ); -} + +
+
+

+ 🦜 LangServe Playground +

-function ConfigPlayground(props: { - configSchema: { - schema: JsonSchema; - defaults: unknown; - }; -}) { - const urlState = getStateFromUrl(window.location.href); - const [configData, setConfigData] = useState({ - data: urlState.configFromUrl ?? props.configSchema.defaults, - errors: [], - defaults: true, - }); + {Object.keys(schemas.config).length > 0 && ( +
+ {!isEmbedded && ( +

Configure

+ )} - // input schema is derived from config data - const [debouncedConfigData, debounceState] = useDebounce( - configData.data, - 500 - ); +
+ + data + ? setConfigData({ data, errors, defaults: false }) + : undefined + } + /> - const inputSchema = useInputSchema( - debouncedConfigData !== props.configSchema.defaults - ? debouncedConfigData - : undefined - ); + {!!configData.errors?.length && configData.data && ( +
+
+ Validation Errors +
    + {configData.errors?.map((e, i) => ( +
  • {e.message}
  • + ))} +
+
+
+ )} +
+
+ )} - const { context, callbacks } = useAppStreamCallbacks(); - const { startStream, stopStream, latest } = useStreamLog(callbacks); + {!isEmbedded && ( +
+

Try it

- return ( - - - -
- {inputSchema.error != null ? ( -
-
- {inputSchema.error.toString()} -
-
- ) : ( - <> - {inputSchema.data != null ? ( - - {latest && ( -
-

Output

-
- -
- +
+
+

Inputs

+ {isInputResetable && ( + + )} +
+ + + setInputData({ data, errors }) + } + /> + {!!inputData.errors?.length && inputData.data && ( +
+ Validation Errors +
    + {inputData.errors?.map((e, i) => ( +
  • {e.message}
  • + ))} +
)} - - ) : null} - - )} -
- - ); -} +
-function Playground() { - const configSchema = useConfigSchema(); - if (configSchema.isLoading) return null; + {latest && ( +
+

Output

+
+ +
+ +
+ )} +
+ )} - if (configSchema.error != null) { - return ( -
-
- {configSchema.error.toString()} -
-
- ); - } - if (configSchema.data == null) return "No config schema found"; - return ; -} +
-export function App() { - return ( -
-
-

- 🦜 LangServe Playground -

- +
+
+ + {isEmbedded ? ( + <> + + + + ) : ( + <> + + + + + + )} +
+
-
+ ); } diff --git a/langserve/playground/src/components/CustomAnyOfRenderer.tsx b/langserve/playground/src/components/CustomAnyOfRenderer.tsx index 52da90b9..8dcbb657 100644 --- a/langserve/playground/src/components/CustomAnyOfRenderer.tsx +++ b/langserve/playground/src/components/CustomAnyOfRenderer.tsx @@ -5,7 +5,7 @@ import { JsonSchema, isAnyOfControl, } from "@jsonforms/core"; -import { renderers, cells } from "../renderers"; +import { renderers, cells } from "../App"; export const CustomAnyOfRenderer = withJsonFormsAnyOfProps((props) => { const anyOfRenderInfos = createCombinatorRenderInfos( diff --git a/langserve/playground/src/components/ShareDialog.tsx b/langserve/playground/src/components/ShareDialog.tsx index dcc6debb..494581d3 100644 --- a/langserve/playground/src/components/ShareDialog.tsx +++ b/langserve/playground/src/components/ShareDialog.tsx @@ -4,11 +4,37 @@ import CodeIcon from "../assets/CodeIcon.svg?react"; import PadlockIcon from "../assets/PadlockIcon.svg?react"; import CopyIcon from "../assets/CopyIcon.svg?react"; import CheckCircleIcon from "../assets/CheckCircleIcon.svg?react"; -import { compressToEncodedURIComponent } from "lz-string"; -import { getStateFromUrl } from "../utils/url"; +import { + compressToEncodedURIComponent, + decompressFromEncodedURIComponent, +} from "lz-string"; const URL_LENGTH_LIMIT = 2000; +export function getStateFromUrl(path: string) { + let configFromUrl = null; + let basePath = path; + if (basePath.endsWith("/")) { + basePath = basePath.slice(0, -1); + } + + if (basePath.endsWith("/playground")) { + basePath = basePath.slice(0, -"/playground".length); + } + + // check if we can omit the last segment + const [configHash, c, ...rest] = basePath.split("/").reverse(); + if (c === "c") { + basePath = rest.reverse().join("/"); + try { + configFromUrl = JSON.parse(decompressFromEncodedURIComponent(configHash)); + } catch (error) { + console.error(error); + } + } + return { basePath, configFromUrl }; +} + function CopyButton(props: { value: string }) { const [copied, setCopied] = useState(false); const cbRef = useRef(null); @@ -95,6 +121,7 @@ const result = await chain.invoke({ ... });
{playgroundUrl.split("://")[1]} + PadlockIcon
diff --git a/langserve/playground/src/components/SubmitButton.tsx b/langserve/playground/src/components/SubmitButton.tsx deleted file mode 100644 index 2dce60ea..00000000 --- a/langserve/playground/src/components/SubmitButton.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import SendIcon from "../assets/SendIcon.svg?react"; -import { cn } from "../utils/cn"; - -export function SubmitButton(props: { - disabled: boolean; - isLoading?: boolean; - onSubmit: () => void; -}) { - return ( - - ); -} diff --git a/langserve/playground/src/main.tsx b/langserve/playground/src/main.tsx index 7c3050b2..164cb889 100644 --- a/langserve/playground/src/main.tsx +++ b/langserve/playground/src/main.tsx @@ -1,11 +1,4 @@ import ReactDOM from "react-dom/client"; import App from "./App.tsx"; -import dayjs from "dayjs"; -import utc from "dayjs/plugin/utc"; -import relativeDate from "dayjs/plugin/relativeTime"; - -dayjs.extend(relativeDate); -dayjs.extend(utc); - ReactDOM.createRoot(document.getElementById("root")!).render(); diff --git a/langserve/playground/src/renderers.tsx b/langserve/playground/src/renderers.tsx deleted file mode 100644 index 630244b2..00000000 --- a/langserve/playground/src/renderers.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { - materialAllOfControlTester, - MaterialAllOfRenderer, - MaterialObjectRenderer, - materialOneOfControlTester, - MaterialOneOfRenderer, -} from "@jsonforms/material-renderers"; -import { - BooleanCell, - DateCell, - DateTimeCell, - EnumCell, - IntegerCell, - NumberCell, - SliderCell, - TimeCell, - booleanCellTester, - dateCellTester, - dateTimeCellTester, - enumCellTester, - integerCellTester, - numberCellTester, - sliderCellTester, - textAreaCellTester, - textCellTester, - timeCellTester, - vanillaRenderers, - InputControl, -} from "@jsonforms/vanilla-renderers"; -import { - RankedTester, - rankWith, - and, - uiTypeIs, - schemaMatches, - schemaTypeIs, -} from "@jsonforms/core"; -import CustomArrayControlRenderer, { - materialArrayControlTester, -} from "./components/CustomArrayControlRenderer"; -import CustomTextAreaCell from "./components/CustomTextAreaCell"; -import JsonTextAreaCell from "./components/JsonTextAreaCell"; -import { - chatMessagesTester, - ChatMessagesControlRenderer, -} from "./components/ChatMessagesControlRenderer"; -import { - ChatMessageTuplesControlRenderer, - chatMessagesTupleTester, -} from "./components/ChatMessageTuplesControlRenderer"; -import { - fileBase64Tester, - FileBase64ControlRenderer, -} from "./components/FileBase64Tester"; -import { - customAnyOfTester, - CustomAnyOfRenderer, -} from "./components/CustomAnyOfRenderer"; - -const isObjectWithPropertiesControl = rankWith( - 2, - and( - uiTypeIs("Control"), - schemaTypeIs("object"), - schemaMatches((schema) => - Object.prototype.hasOwnProperty.call(schema, "properties") - ) - ) -); -const isObject = rankWith(1, and(uiTypeIs("Control"), schemaTypeIs("object"))); -const isElse = rankWith(1, and(uiTypeIs("Control"))); - -export const renderers = [ - ...vanillaRenderers, - - // use material renderers to handle objects and json schema references - // they should yield the rendering to simpler cells - { tester: isObjectWithPropertiesControl, renderer: MaterialObjectRenderer }, - { tester: materialAllOfControlTester, renderer: MaterialAllOfRenderer }, - { tester: materialOneOfControlTester, renderer: MaterialOneOfRenderer }, - - { tester: customAnyOfTester, renderer: CustomAnyOfRenderer }, - - // custom renderers - { tester: materialArrayControlTester, renderer: CustomArrayControlRenderer }, - { tester: isObject, renderer: InputControl }, - { tester: chatMessagesTester, renderer: ChatMessagesControlRenderer }, - { - tester: chatMessagesTupleTester, - renderer: ChatMessageTuplesControlRenderer, - }, - { tester: fileBase64Tester, renderer: FileBase64ControlRenderer }, -]; -const nestedArrayControlTester: RankedTester = rankWith(1, (_, jsonSchema) => { - return jsonSchema.type === "array"; -}); - -export const cells = [ - { tester: booleanCellTester, cell: BooleanCell }, - { tester: dateCellTester, cell: DateCell }, - { tester: dateTimeCellTester, cell: DateTimeCell }, - { tester: enumCellTester, cell: EnumCell }, - { tester: integerCellTester, cell: IntegerCell }, - { tester: numberCellTester, cell: NumberCell }, - { tester: sliderCellTester, cell: SliderCell }, - { tester: textAreaCellTester, cell: CustomTextAreaCell }, - { tester: textCellTester, cell: CustomTextAreaCell }, - { tester: timeCellTester, cell: TimeCell }, - { tester: nestedArrayControlTester, cell: CustomArrayControlRenderer }, - { tester: isElse, cell: JsonTextAreaCell }, -]; diff --git a/langserve/playground/src/sections/SectionConfigure.tsx b/langserve/playground/src/sections/SectionConfigure.tsx deleted file mode 100644 index 2977bedb..00000000 --- a/langserve/playground/src/sections/SectionConfigure.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { JsonForms } from "@jsonforms/react"; -import { JsonFormsCore, JsonSchema } from "@jsonforms/core"; -import { renderers, cells } from "../renderers"; - -export type ConfigValue = Pick & { - defaults: boolean; -}; - -export function SectionConfigure(props: { - config: JsonSchema | undefined; - value: ConfigValue; - onChange: (value: ConfigValue) => void; -}) { - if (props.config == null || Object.keys(props.config).length === 0) { - return null; - } - - return ( -
-

Configure

- -
- { - if (data) { - props.onChange({ data, errors, defaults: false }); - } - }} - /> - - {!!props.value.errors?.length && props.value.data && ( -
-
- Validation Errors -
    - {props.value.errors?.map((e, i) => ( -
  • {e.message}
  • - ))} -
-
-
- )} -
-
- ); -} diff --git a/langserve/playground/src/sections/SectionInputs.tsx b/langserve/playground/src/sections/SectionInputs.tsx deleted file mode 100644 index da4668da..00000000 --- a/langserve/playground/src/sections/SectionInputs.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useMemo } from "react"; -import defaults from "../utils/defaults"; -import { JsonForms } from "@jsonforms/react"; -import { JsonFormsCore, JsonSchema } from "@jsonforms/core"; -import { renderers, cells } from "../renderers"; - -export type InputValue = Pick; - -export function SectionInputs(props: { - input: JsonSchema | undefined; - value: InputValue; - onChange: (value: InputValue) => void; -}) { - const isInputResetable = useMemo(() => { - if (!props.input) return false; - return ( - JSON.stringify(defaults(props.input)) !== JSON.stringify(props.value.data) - ); - }, [props.input, props.value.data]); - - return ( -
-

Try it

- -
-
-

Inputs

- {isInputResetable && ( - - )} -
- - props.onChange({ data, errors })} - /> - {!!props.value.errors?.length && props.value.data && ( -
- Validation Errors -
    - {props.value.errors?.map((e, i) => ( -
  • {e.message}
  • - ))} -
-
- )} -
-
- ); -} diff --git a/langserve/playground/src/useSchemas.tsx b/langserve/playground/src/useSchemas.tsx index a63a9e6f..901e4fe5 100644 --- a/langserve/playground/src/useSchemas.tsx +++ b/langserve/playground/src/useSchemas.tsx @@ -1,10 +1,9 @@ +import { useEffect, useState } from "react"; import { resolveApiUrl } from "./utils/url"; import { simplifySchema } from "./utils/simplifySchema"; -import { JsonSchema } from "@jsonforms/core"; +import { JsonFormsCore } from "@jsonforms/core"; import { compressToEncodedURIComponent } from "lz-string"; - -import useSWR from "swr"; -import defaults from "./utils/defaults"; +import { useDebounce } from "use-debounce"; declare global { interface Window { @@ -15,54 +14,63 @@ declare global { } } -export function useConfigSchema() { - return useSWR(["/config_schema"], async () => { - let schema: JsonSchema | null = null; - if (!import.meta.env.DEV && window.CONFIG_SCHEMA) { - schema = await simplifySchema(window.CONFIG_SCHEMA); - } else { - const response = await fetch(resolveApiUrl(`/config_schema`)); - if (!response.ok) throw new Error(await response.text()); - - const json = await response.json(); - schema = await simplifySchema(json); - } - - if (schema == null) return null; - return { - schema, - defaults: defaults(schema), - }; +export function useSchemas( + configData: Pick & { defaults: boolean } +) { + const [schemas, setSchemas] = useState<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: null | any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + input: null | any; + }>({ + config: null, + input: null, }); -} -export function useInputSchema(configData?: unknown) { - return useSWR( - ["/input_schema", configData], - async ([, configData]) => { - // TODO: this won't work if we're already seeing a prefixed URL - const prefix = configData - ? `/c/${compressToEncodedURIComponent(JSON.stringify(configData))}` - : ""; + useEffect(() => { + async function save() { + if (import.meta.env.DEV) { + const [config, input] = await Promise.all([ + fetch(resolveApiUrl("/config_schema")) + .then((r) => r.json()) + .then(simplifySchema), + fetch(resolveApiUrl("/input_schema")) + .then((r) => r.json()) + .then(simplifySchema), + ]); + setSchemas({ config, input }); + } else { + setSchemas({ + config: window.CONFIG_SCHEMA + ? await simplifySchema(window.CONFIG_SCHEMA) + : null, + input: window.INPUT_SCHEMA + ? await simplifySchema(window.INPUT_SCHEMA) + : null, + }); + } + } - let schema: JsonSchema | null = null; + save(); + }, []); - if (!prefix && !import.meta.env.DEV && window.INPUT_SCHEMA) { - schema = await simplifySchema(window.INPUT_SCHEMA); - } else { - const response = await fetch(resolveApiUrl(`${prefix}/input_schema`)); - if (!response.ok) throw new Error(await response.text()); + const [debouncedConfigData] = useDebounce(configData, 500); - const json = await response.json(); - schema = await simplifySchema(json); - } + useEffect(() => { + if (!debouncedConfigData.defaults) { + fetch( + resolveApiUrl( + `/c/${compressToEncodedURIComponent( + JSON.stringify(debouncedConfigData.data) + )}/input_schema` + ) + ) + .then((r) => r.json()) + .then(simplifySchema) + .then((input) => setSchemas((current) => ({ ...current, input }))) + .catch(() => {}); // ignore errors, eg. due to incomplete config + } + }, [debouncedConfigData]); - if (schema == null) return null; - return { - schema, - defaults: defaults(schema), - }; - }, - { keepPreviousData: true } - ); + return schemas; } diff --git a/langserve/playground/src/useStreamCallback.tsx b/langserve/playground/src/useStreamCallback.tsx index 31a3ca8b..137bfe07 100644 --- a/langserve/playground/src/useStreamCallback.tsx +++ b/langserve/playground/src/useStreamCallback.tsx @@ -14,35 +14,6 @@ export const AppCallbackContext = createContext[]; }> | null>(null); -export function useAppStreamCallbacks() { - // callbacks handling - const context = useRef<{ - onStart: Exclude[]; - onSuccess: Exclude[]; - onError: Exclude[]; - }>({ onStart: [], onSuccess: [], onError: [] }); - - const callbacks: StreamCallback = { - onStart(...args) { - for (const callback of context.current.onStart) { - callback(...args); - } - }, - onSuccess(...args) { - for (const callback of context.current.onSuccess) { - callback(...args); - } - }, - onError(...args) { - for (const callback of context.current.onError) { - callback(...args); - } - }, - }; - - return { context, callbacks }; -} - export function useStreamCallback< Type extends "onStart" | "onSuccess" | "onError" >(type: Type, callback: Exclude) { diff --git a/langserve/playground/src/utils/url.ts b/langserve/playground/src/utils/url.ts index 88f40a4d..6f560db1 100644 --- a/langserve/playground/src/utils/url.ts +++ b/langserve/playground/src/utils/url.ts @@ -1,33 +1,5 @@ -import { decompressFromEncodedURIComponent } from "lz-string"; - -export function getStateFromUrl(path: string) { - let configFromUrl = null; - let basePath = path; - if (basePath.endsWith("/")) { - basePath = basePath.slice(0, -1); - } - - if (basePath.endsWith("/playground")) { - basePath = basePath.slice(0, -"/playground".length); - } - - // check if we can omit the last segment - const [configHash, c, ...rest] = basePath.split("/").reverse(); - if (c === "c") { - basePath = rest.reverse().join("/"); - try { - configFromUrl = JSON.parse(decompressFromEncodedURIComponent(configHash)); - } catch (error) { - console.error(error); - } - } - return { basePath, configFromUrl }; -} - export function resolveApiUrl(path: string) { - const { basePath } = getStateFromUrl(window.location.href); - let prefix = new URL(basePath).pathname; + let prefix = window.location.pathname.split("/playground")[0]; if (prefix.endsWith("/")) prefix = prefix.slice(0, -1); - - return new URL(prefix + path, basePath); + return new URL(prefix + path, window.location.origin); } diff --git a/langserve/playground/yarn.lock b/langserve/playground/yarn.lock index b3f755da..39092cb6 100644 --- a/langserve/playground/yarn.lock +++ b/langserve/playground/yarn.lock @@ -1407,11 +1407,6 @@ chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" -client-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== - clsx@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" @@ -2625,14 +2620,6 @@ svg-parser@^2.0.4: resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -swr@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/swr/-/swr-2.2.4.tgz#03ec4c56019902fbdc904d78544bd7a9a6fa3f07" - integrity sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ== - dependencies: - client-only "^0.0.1" - use-sync-external-store "^1.2.0" - tailwind-merge@^1.14.0: version "1.14.0" resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b" @@ -2764,11 +2751,6 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" -use-sync-external-store@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" - integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== - util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"