diff --git a/docker-compose.yml b/docker-compose.yml index 5a1db726..94acec50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: networks: - ten_agent_network ten_agent_playground: - image: ghcr.io/ten-framework/ten_agent_playground:0.5.0-56-g0536dbb + image: ghcr.io/ten-framework/ten_agent_playground:0.5.0-66-g830dd72 container_name: ten_agent_playground restart: always ports: diff --git a/playground/src/common/constant.ts b/playground/src/common/constant.ts index 14b837d2..97eb0a90 100644 --- a/playground/src/common/constant.ts +++ b/playground/src/common/constant.ts @@ -1,6 +1,7 @@ import { IOptions, ColorItem, LanguageOptionItem, VoiceOptionItem, GraphOptionItem } from "@/types" export const GITHUB_URL = "https://github.com/TEN-framework/TEN-Agent" export const OPTIONS_KEY = "__options__" +export const OVERRIDEN_PROPERTIES_KEY = "__overriden__" export const DEFAULT_OPTIONS: IOptions = { channel: "", userName: "", diff --git a/playground/src/common/hooks.ts b/playground/src/common/hooks.ts index ff1d8050..13a40930 100644 --- a/playground/src/common/hooks.ts +++ b/playground/src/common/hooks.ts @@ -1,7 +1,7 @@ "use client" import { IMicrophoneAudioTrack } from "agora-rtc-sdk-ng" -import { normalizeFrequencies } from "./utils" +import { deepMerge, normalizeFrequencies } from "./utils" import { useState, useEffect, useMemo, useRef } from "react" import type { AppDispatch, AppStore, RootState } from "../store" import { useDispatch, useSelector, useStore } from "react-redux" @@ -132,13 +132,29 @@ export const usePrevious = (value: any) => { export const useGraphExtensions = () => { const graphName = useAppSelector(state => state.global.graphName); const nodes = useAppSelector(state => state.global.extensions); + const overridenProperties = useAppSelector(state => state.global.overridenProperties); const [graphExtensions, setGraphExtensions] = useState>({}); useEffect(() => { if (nodes && nodes[graphName]) { - setGraphExtensions(nodes[graphName]); + let extensions:Record = {} + let extensionsByGraph = JSON.parse(JSON.stringify(nodes[graphName])); + let overriden = overridenProperties[graphName] || {}; + for (const key of Object.keys(extensionsByGraph)) { + if (!overriden[key]) { + extensions[key] = extensionsByGraph[key]; + continue; + } + extensions[key] = { + addon: extensionsByGraph[key].addon, + name: extensionsByGraph[key].name, + }; + extensions[key].property = deepMerge(extensionsByGraph[key].property, overriden[key]); + } + setGraphExtensions(extensions); } - }, [graphName, nodes]); + + }, [graphName, nodes, overridenProperties]); return graphExtensions; }; \ No newline at end of file diff --git a/playground/src/common/storage.ts b/playground/src/common/storage.ts index ed96083d..54c956c6 100644 --- a/playground/src/common/storage.ts +++ b/playground/src/common/storage.ts @@ -1,5 +1,5 @@ import { IOptions } from "@/types" -import { OPTIONS_KEY, DEFAULT_OPTIONS } from "./constant" +import { OPTIONS_KEY, DEFAULT_OPTIONS, OVERRIDEN_PROPERTIES_KEY } from "./constant" export const getOptionsFromLocal = () => { if (typeof window !== "undefined") { @@ -11,6 +11,15 @@ export const getOptionsFromLocal = () => { return DEFAULT_OPTIONS } +export const getOverridenPropertiesFromLocal = () => { + if (typeof window !== "undefined") { + const data = localStorage.getItem(OVERRIDEN_PROPERTIES_KEY) + if (data) { + return JSON.parse(data) + } + } + return {} +} export const setOptionsToLocal = (options: IOptions) => { if (typeof window !== "undefined") { @@ -18,4 +27,8 @@ export const setOptionsToLocal = (options: IOptions) => { } } - +export const setOverridenPropertiesToLocal = (properties: Record) => { + if (typeof window !== "undefined") { + localStorage.setItem(OVERRIDEN_PROPERTIES_KEY, JSON.stringify(properties)) + } +} diff --git a/playground/src/common/utils.ts b/playground/src/common/utils.ts index 1d6f0d00..e6f170de 100644 --- a/playground/src/common/utils.ts +++ b/playground/src/common/utils.ts @@ -56,4 +56,14 @@ export const genUUID = () => { export const isMobile = () => { return /Mobile|iPhone|iPad|Android|Windows Phone/i.test(navigator.userAgent) +} + +export const deepMerge = (target: Record, source: Record): Record => { + for (const key of Object.keys(source)) { + if (source[key] instanceof Object && key in target) { + Object.assign(source[key], deepMerge(target[key], source[key])); + } + } + // Merge source into target + return { ...target, ...source }; } \ No newline at end of file diff --git a/playground/src/components/authInitializer/index.tsx b/playground/src/components/authInitializer/index.tsx index 65336e21..c19e9fb8 100644 --- a/playground/src/components/authInitializer/index.tsx +++ b/playground/src/components/authInitializer/index.tsx @@ -1,8 +1,8 @@ "use client" import { ReactNode, useEffect } from "react" -import { useAppDispatch, getOptionsFromLocal, getRandomChannel, getRandomUserId } from "@/common" -import { setOptions, reset } from "@/store/reducers/global" +import { useAppDispatch, getOptionsFromLocal, getRandomChannel, getRandomUserId, getOverridenPropertiesFromLocal } from "@/common" +import { setOptions, reset, setOverridenProperties } from "@/store/reducers/global" interface AuthInitializerProps { children: ReactNode; @@ -15,16 +15,18 @@ const AuthInitializer = (props: AuthInitializerProps) => { useEffect(() => { if (typeof window !== "undefined") { const options = getOptionsFromLocal() + const overridenProperties = getOverridenPropertiesFromLocal() if (options && options.channel) { dispatch(reset()) dispatch(setOptions(options)) } else { dispatch(reset()) - dispatch(setOptions({ + dispatch(setOptions({ channel: getRandomChannel(), userId: getRandomUserId(), })) } + dispatch(setOverridenProperties(overridenProperties)) } }, [dispatch]) diff --git a/playground/src/platform/pc/chat/index.tsx b/playground/src/platform/pc/chat/index.tsx index 3ee02155..0e293a1c 100644 --- a/playground/src/platform/pc/chat/index.tsx +++ b/playground/src/platform/pc/chat/index.tsx @@ -12,7 +12,7 @@ import { useGraphExtensions, apiGetExtensionMetadata, } from "@/common" -import { setExtensionMetadata, setGraphName, setGraphs, setLanguage, setExtensions } from "@/store/reducers/global" +import { setExtensionMetadata, setGraphName, setGraphs, setLanguage, setExtensions, setOverridenPropertiesByGraph, setOverridenProperties } from "@/store/reducers/global" import { Button, Modal, Select, Tabs, TabsProps, } from 'antd'; import PdfSelect from "@/components/pdfSelect" @@ -31,6 +31,7 @@ const Chat = () => { const [modal2Open, setModal2Open] = useState(false) const graphExtensions = useGraphExtensions() const extensionMetadata = useAppSelector(state => state.global.extensionMetadata) + const overridenProperties = useAppSelector(state => state.global.overridenProperties) // const chatItems = genRandomChatList(10) @@ -93,9 +94,14 @@ const Chat = () => { open={modal2Open} onCancel={() => setModal2Open(false)} footer={ - + <> + + + } >

You can adjust extension properties here, the values will be overridden when the agent starts using "Connect." Note that this won't modify the property.json file.

@@ -109,9 +115,10 @@ const Chat = () => { initialData={node["property"] || {}} metadata={metadata ? metadata.api.property : {}} onUpdate={(data) => { - let nodesMap = JSON.parse(JSON.stringify(graphExtensions)) - nodesMap[key]["property"] = data - dispatch(setExtensions({ graphName, nodesMap })) + // clone the overridenProperties + let nodesMap = JSON.parse(JSON.stringify(overridenProperties[graphName] || {})) + nodesMap[key] = data + dispatch(setOverridenPropertiesByGraph({ graphName, nodesMap })) }} > } diff --git a/playground/src/platform/pc/chat/table/index.tsx b/playground/src/platform/pc/chat/table/index.tsx index 1b9f43db..88211b96 100644 --- a/playground/src/platform/pc/chat/table/index.tsx +++ b/playground/src/platform/pc/chat/table/index.tsx @@ -33,12 +33,18 @@ const convertToType = (value: any, type: string) => { }; const EditableTable: React.FC = ({ initialData, onUpdate, metadata }) => { - const [dataSource, setDataSource] = useState( - Object.entries(initialData).map(([key, value]) => ({ key, value })) - ); + const [dataSource, setDataSource] = useState([]); const [editingKey, setEditingKey] = useState(''); const [form] = Form.useForm(); const inputRef = useRef(null); // Ref to manage focus + const updatedValuesRef = useRef>({}); + + // Update dataSource whenever initialData changes + useEffect(() => { + setDataSource( + Object.entries(initialData).map(([key, value]) => ({ key, value })) + ); + }, [initialData]); // Function to check if the current row is being edited const isEditing = (record: DataType) => record.key === editingKey; @@ -65,16 +71,17 @@ const EditableTable: React.FC = ({ initialData, onUpdate, me setDataSource(newData); setEditingKey(''); - // Notify the parent component of the update - const updatedData = Object.fromEntries(newData.map(({ key, value }) => [key, value])); - onUpdate(updatedData); + // Store the updated value in the ref + updatedValuesRef.current[key] = updatedValue; + + // Notify the parent component of only the updated value + onUpdate({ [key]: updatedValue }); } } catch (errInfo) { console.log('Validation Failed:', errInfo); } }; - // Toggle the checkbox for boolean values directly in the table cell const handleCheckboxChange = (key: string, checked: boolean) => { const newData = [...dataSource]; @@ -83,9 +90,11 @@ const EditableTable: React.FC = ({ initialData, onUpdate, me newData[index].value = checked; // Update the boolean value setDataSource(newData); - // Notify the parent component of the update - const updatedData = Object.fromEntries(newData.map(({ key, value }) => [key, value])); - onUpdate(updatedData); + // Store the updated value in the ref + updatedValuesRef.current[key] = checked; + + // Notify the parent component of only the updated value + onUpdate({ [key]: checked }); } }; diff --git a/playground/src/platform/pc/description/index.tsx b/playground/src/platform/pc/description/index.tsx index 53d7d996..3a865d7c 100644 --- a/playground/src/platform/pc/description/index.tsx +++ b/playground/src/platform/pc/description/index.tsx @@ -1,8 +1,7 @@ import { setAgentConnected } from "@/store/reducers/global" import { useAppDispatch, useAppSelector, apiPing, genUUID, - apiStartService, apiStopService, - useGraphExtensions + apiStartService, apiStopService } from "@/common" import { Select, Button, message, Upload } from "antd" import { useEffect, useState, MouseEventHandler } from "react" @@ -20,7 +19,7 @@ const Description = () => { const voiceType = useAppSelector(state => state.global.voiceType) const [loading, setLoading] = useState(false) const graphName = useAppSelector(state => state.global.graphName) - const graphNodes = useGraphExtensions() + const overridenProperties = useAppSelector(state => state.global.overridenProperties) useEffect(() => { if (channel) { @@ -47,18 +46,14 @@ const Description = () => { message.success("Agent disconnected") stopPing() } else { - let properties: Record = {} - Object.keys(graphNodes).forEach(extensionName => { - properties[extensionName] = {} - properties[extensionName] = graphNodes[extensionName].property - }) + let properties: Record = overridenProperties[graphName] || {} const res = await apiStartService({ channel, userId, graphName, language, voiceType, - properties: properties + properties }) const { code, msg } = res || {} if (code != 0) { diff --git a/playground/src/store/reducers/global.ts b/playground/src/store/reducers/global.ts index eff3627e..0bb7f37c 100644 --- a/playground/src/store/reducers/global.ts +++ b/playground/src/store/reducers/global.ts @@ -1,6 +1,6 @@ import { IOptions, IChatItem, Language, VoiceType } from "@/types" import { createSlice, PayloadAction } from "@reduxjs/toolkit" -import { DEFAULT_OPTIONS, COLOR_LIST, setOptionsToLocal, genRandomChatList } from "@/common" +import { DEFAULT_OPTIONS, COLOR_LIST, setOptionsToLocal, genRandomChatList, setOverridenPropertiesToLocal, deepMerge } from "@/common" export interface InitialState { options: IOptions @@ -13,6 +13,7 @@ export interface InitialState { graphName: string, graphs: string[], extensions: Record, + overridenProperties: Record, extensionMetadata: Record } @@ -28,6 +29,7 @@ const getInitialState = (): InitialState => { graphName: "camera_va_openai_azure", graphs: [], extensions: {}, + overridenProperties: {}, extensionMetadata: {}, } } @@ -100,6 +102,15 @@ export const globalSlice = createSlice({ let { graphName, nodesMap } = action.payload state.extensions[graphName] = nodesMap }, + setOverridenProperties: (state, action: PayloadAction>) => { + state.overridenProperties = action.payload + setOverridenPropertiesToLocal(state.overridenProperties) + }, + setOverridenPropertiesByGraph: (state, action: PayloadAction>) => { + let { graphName, nodesMap } = action.payload + state.overridenProperties[graphName] = deepMerge(state.overridenProperties[graphName] || {}, nodesMap) + setOverridenPropertiesToLocal(state.overridenProperties) + }, setExtensionMetadata: (state, action: PayloadAction>) => { state.extensionMetadata = action.payload }, @@ -115,7 +126,7 @@ export const globalSlice = createSlice({ export const { reset, setOptions, setRoomConnected, setAgentConnected, setVoiceType, - addChatItem, setThemeColor, setLanguage, setGraphName, setGraphs, setExtensions, setExtensionMetadata } = + addChatItem, setThemeColor, setLanguage, setGraphName, setGraphs, setExtensions, setExtensionMetadata, setOverridenProperties, setOverridenPropertiesByGraph } = globalSlice.actions export default globalSlice.reducer