Repository: nat/openplayground Branch: main Commit: 7f3f79035b32 Files: 74 Total size: 367.5 KB Directory structure: gitextract_ydrkwoag/ ├── .dockerignore ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .parcelrc │ ├── .postcssrc │ ├── .prettierrc │ ├── package.json │ ├── src/ │ │ ├── app.tsx │ │ ├── components/ │ │ │ ├── inputarea.tsx │ │ │ ├── multi-select.tsx │ │ │ ├── navbar.tsx │ │ │ ├── parameter-slider.tsx │ │ │ ├── parameters-side-panel.tsx │ │ │ └── ui/ │ │ │ ├── alert-dialog.tsx │ │ │ ├── button.tsx │ │ │ ├── checkbox.tsx │ │ │ ├── dialog-sheet-base.tsx │ │ │ ├── dialog.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── navigation-menu.tsx │ │ │ ├── popover.tsx │ │ │ ├── right-sheet.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── select.tsx │ │ │ ├── separator.tsx │ │ │ ├── sheet.tsx │ │ │ ├── slider.tsx │ │ │ ├── switch.tsx │ │ │ ├── textarea.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ └── tooltip.tsx │ │ ├── error-page.tsx │ │ ├── hooks/ │ │ │ ├── ui/ │ │ │ │ └── use-toast.tsx │ │ │ └── use-breakpoint.ts │ │ ├── index.css │ │ ├── index.html │ │ ├── index.tsx │ │ ├── lib/ │ │ │ ├── ctrl-meta-keypress.tsx │ │ │ ├── editor-styles.tsx │ │ │ ├── keypress.tsx │ │ │ ├── meta-keypress.tsx │ │ │ └── utils.ts │ │ └── pages/ │ │ ├── compare.tsx │ │ ├── index.tsx │ │ ├── playground.tsx │ │ └── settings.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsconfig.tsbuildinfo ├── build.py ├── dockerfile ├── pyproject.toml └── server/ ├── __init__.py ├── app.py ├── lib/ │ ├── __init__.py │ ├── api/ │ │ ├── __init__.py │ │ ├── inference.py │ │ ├── provider.py │ │ └── response_utils.py │ ├── entities.py │ ├── event_emitter.py │ ├── inference/ │ │ ├── __init__.py │ │ └── huggingface/ │ │ ├── __init__.py │ │ ├── generator.py │ │ ├── helpers.py │ │ └── hf.py │ ├── sse.py │ ├── sseserver.py │ └── storage.py ├── models.json └── requirements.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .env ================================================ FILE: .gitignore ================================================ # These files should be generated during dev/publishing server/static/* #mISC .DS_Store .vscode/ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* yarn.lock* package-lock.json Cargo.lock # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # Parcel related files dist/ .cache/ .parcel-cache/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging bin/ # build/ develop-eggs/ dist/ eggs/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject # Django stuff: *.log *.pot # Sphinx documentation docs/_build/ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) [2023] [OpenPlayground] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # openplayground An LLM playground you can run on your laptop. https://user-images.githubusercontent.com/111631/227399583-39b23f48-9823-4571-a906-985dbe282b20.mp4 #### Features - Use any model from [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com), [Forefront](https://forefront.ai), [HuggingFace](https://huggingface.co), [Aleph Alpha](https://aleph-alpha.com), [Replicate](https://replicate.com), [Banana](https://banana.dev) and [llama.cpp](https://github.com/ggerganov/llama.cpp). - Full playground UI, including history, parameter tuning, keyboard shortcuts, and logprops. - Compare models side-by-side with the same prompt, individually tune model parameters, and retry with different parameters. - Automatically detects local models in your HuggingFace cache, and lets you install new ones. - Works OK on your phone. - Probably won't kill everyone. ## Try on nat.dev Try the hosted version: [nat.dev](https://nat.dev). ## How to install and run ```sh pip install openplayground openplayground run ``` Alternatively, run it as a docker container: ```sh docker run --name openplayground -p 5432:5432 -d --volume openplayground:/web/config natorg/openplayground ``` This runs a Flask process, so you can add the typical flags such as setting a different port `openplayground run -p 1235` and others. ## How to run for development ```sh git clone https://github.com/nat/openplayground cd app && npm install && npx parcel watch src/index.html --no-cache cd server && pip3 install -r requirements.txt && cd .. && python3 -m server.app ``` ## Docker ```sh docker build . --tag "openplayground" docker run --name openplayground -p 5432:5432 -d --volume openplayground:/web/config openplayground ``` First volume is optional. It's used to store API keys, models settings. ## Ideas for contributions - Add a token counter to the playground - Add a cost counter to the playground and the compare page - Measure and display time to first token - Setup automatic builds with GitHub Actions - The default parameters for each model are configured in the `server/models.json` file. If you find better default parameters for a model, please submit a pull request! - Someone can help us make a homebrew package, and a dockerfile - Easier way to install open source models directly from openplayground, with `openplayground install ` or in the UI. - Find and fix bugs - ChatGPT UI, with turn-by-turn, markdown rendering, chatgpt plugin support, etc. - We will probably need multimodal inputs and outputs at some point in 2023 ### llama.cpp ## Adding models to openplayground Models and providers have three types in openplayground: - Searchable - Local inference - API You can add models in `server/models.json` with the following schema: #### Local inference For models running locally on your device you can add them to openplayground like the following (a minimal example): ```json "llama": { "api_key" : false, "models" : { "llama-70b": { "parameters": { "temperature": { "value": 0.5, "range": [ 0.1, 1.0 ] }, } } } } ``` Keep in mind you will need to add a generation method for your model in `server/app.py`. Take a look at `local_text_generation()` as an example. #### API Provider Inference This is for model providers like OpenAI, cohere, forefront, and more. You can connect them easily into openplayground (a minimal example): ```json "cohere": { "api_key" : true, "models" : { "xlarge": { "parameters": { "temperature": { "value": 0.5, "range": [ 0.1, 1.0 ] }, } } } } ``` Keep in mind you will need to add a generation method for your model in `server/app.py`. Take a look at `openai_text_generation()` or `cohere_text_generation()` as an example. #### Searchable models We use this for Huggingface Remote Inference models, the search endpoint is useful for scaling to N models in the settings page. ```json "provider_name": { "api_key": true, "search": { "endpoint": "ENDPOINT_URL" }, "parameters": { "parameter": { "value": 1.0, "range": [ 0.1, 1.0 ] }, } } ``` #### Credits Instigated by Nat Friedman. Initial implementation by [Zain Huda](https://github.com/zainhuda) as a repl.it bounty. Many features and extensive refactoring by [Alex Lourenco](https://github.com/AlexanderLourenco). ================================================ FILE: app/.parcelrc ================================================ { "extends": "@parcel/config-default", "compressors": { "*.{html,css,js,svg,map}": [ "...", "@parcel/compressor-gzip", "@parcel/compressor-brotli" ] } } ================================================ FILE: app/.postcssrc ================================================ { "plugins": { "tailwindcss": {} } } ================================================ FILE: app/.prettierrc ================================================ { "trailingComma": "es5", "semi": false } ================================================ FILE: app/package.json ================================================ { "devDependencies": { "@parcel/compressor-brotli": "^2.8.3", "@parcel/compressor-gzip": "^2.8.3", "@parcel/config-default": "^2.8.3", "@types/chroma-js": "^2.4.0", "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "autoprefixer": "^10.4.13", "buffer": "^5.7.1", "events": "^3.3.0", "https-browserify": "^1.0.0", "parcel": "^2.8.3", "postcss": "^8.4.21", "process": "^0.11.10", "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "stream-http": "^3.2.0", "tailwindcss": "^3.2.4", "url": "^0.11.0", "util": "^0.12.5" }, "dependencies": { "@radix-ui/react-alert-dialog": "^1.0.2", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.3", "@radix-ui/react-dropdown-menu": "^2.0.2", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-navigation-menu": "^1.1.1", "@radix-ui/react-popover": "^1.0.4", "@radix-ui/react-scroll-area": "^1.0.2", "@radix-ui/react-select": "^1.2.0", "@radix-ui/react-separator": "^1.0.1", "@radix-ui/react-slider": "^1.1.0", "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-toast": "^1.1.2", "@radix-ui/react-tooltip": "^1.0.3", "@types/draft-js": "^0.11.10", "chroma-js": "^2.4.2", "class-variance-authority": "^0.4.0", "clsx": "^1.2.1", "draft-js": "^0.11.7", "event-source-polyfill": "^1.0.31", "eventsource": "^2.0.2", "immutable": "^4.2.3", "localforage": "^1.10.0", "lucide-react": "^0.108.0", "match-sorter": "^6.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-responsive": "^9.0.2", "react-router-dom": "^6.8.1", "react-scroll": "^1.8.9", "react-select": "^5.7.0", "react-tiny-popover": "^7.2.3", "react-transition-group": "^4.4.5", "sort-by": "^1.2.0", "sse.js": "^0.6.1", "tailwind-merge": "^1.9.0", "tailwindcss-animate": "^1.0.5", "uuidv4": "^6.2.13" } } ================================================ FILE: app/src/app.tsx ================================================ import React, { useEffect } from "react" import {Playground, Compare, Settings} from "./pages" import {SSE} from "sse.js" import { EditorState, convertFromRaw, } from "draft-js" import { BrowserRouter, Route, Routes, } from "react-router-dom" import { Toaster } from "./components/ui/toaster" import { useToast } from "./hooks/ui/use-toast" const DEFAULT_PARAMETERS_STATE = { temperature: 1.0, maximumLength: 200, topP: 0.9, topK: 0, repetitionPenalty: 1.0, frequencyPenalty: 0.0, presencePenalty: 0.0, stopSequences: [], highlightModels: true, showProbabilities: false } const DEFAULT_EDITOR_STATE = { prompt: "", prePrompt: "", internalState: null } const DEFAULT_HISTORY_STATE = { show: false, entries: [], current: null } const DEFAULT_CONTEXTS = { PAGES: { playground:{ history: DEFAULT_HISTORY_STATE, editor: {...DEFAULT_EDITOR_STATE, previousInternalState: null }, modelsState: [], parameters: DEFAULT_PARAMETERS_STATE }, compare:{ history: DEFAULT_HISTORY_STATE, editor: DEFAULT_EDITOR_STATE, modelsState: [], parameters: { ...DEFAULT_PARAMETERS_STATE, selectAllModels: false, showParametersTable: false } }, }, MODELS: [], } let SETTINGS = null; try { SETTINGS = JSON.parse(localStorage.getItem("openplayground_settings")); if (!SETTINGS) throw new Error("no settings") } catch (e) { localStorage.clear(); SETTINGS = {}; } finally { if (!SETTINGS.pages) { SETTINGS.pages = DEFAULT_CONTEXTS.PAGES; } if (!SETTINGS.models) { SETTINGS.models = DEFAULT_CONTEXTS.MODELS; } } DEFAULT_CONTEXTS.PAGES = SETTINGS.pages; DEFAULT_CONTEXTS.MODELS = SETTINGS.models; export const APIContext = React.createContext({}); export const EditorContext = React.createContext({}); export const ModelsStateContext = React.createContext([]); export const ParametersContext = React.createContext({}); export const HistoryContext = React.createContext({}); export const ModelsContext = React.createContext(DEFAULT_CONTEXTS.MODELS); const saveSettings = () => { let _settings = JSON.stringify(SETTINGS) let SETTINGS_SIZE = _settings.length * 2 / 1024 / 1024; if (SETTINGS_SIZE >= 5) { const shouldDownloadHistory = confirm("Local Storage is full. Do you wish to download your history prior to clearing storage?"); const first_entry = SETTINGS.pages["playground"].history.entries.shift() if (shouldDownloadHistory) { const element = document.createElement("a") const history_json = SETTINGS.pages["playground"].history.entries.map((entry: any) => { const model = entry.modelsState.find(({selected}) => selected) const text = EditorState.createWithContent(convertFromRaw(entry.editor.internalState)).getCurrentContent().getPlainText() return { model: model.name, date: entry.date, timestamp: entry.timestamp, text: text, parameters: entry.parameters } }) const file = new Blob([JSON.stringify(history_json)], { type: "application/json", }) element.href = URL.createObjectURL(file) element.download = "history.json" document.body.appendChild(element) element.click() } SETTINGS.pages["playground"].history.entries = [first_entry] SETTINGS.pages["playground"].history.current = first_entry _settings = JSON.stringify(SETTINGS) } localStorage.setItem("openplayground_settings", _settings) } function useDebounce(func, delay) { const timeoutRef = React.useRef(null); useEffect(() => { return () => { clearTimeout(timeoutRef.current); }; }, []); function debouncedFunction(...args) { clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => { func(...args); }, delay); } return debouncedFunction; } const APIContextWrapper = ({children}) => { const pendingCompletionRequest = React.useRef(false); const textCompletionSubscribers = React.useRef([]); const chatCompletionSubscribers = React.useRef([]); const notificationSubscribers = React.useRef([]); useEffect(() => { const sse_request = new SSE("/api/notifications") sse_request.addEventListener("notification", (event: any) => { const parsedEvent = JSON.parse(event.data); notificationSubscribers.current.forEach((callback) => { callback(parsedEvent.message); }) }); sse_request.stream(); }, []) const Model = { getAll: async () => (await fetch("/api/models")).json(), getAllEnabled: async () => (await fetch("/api/models-enabled")).json(), toggle: async (provider, model) => (await fetch(`/api/provider/${provider}/model/${encodeURIComponent(model)}/toggle-status`)).json(), search: async (provider, query) => (await fetch(`/api/provider/${provider}/models/search?query=${query}`)).json(), }; const Notifications = { subscribe: (callback) => { notificationSubscribers.current.push(callback); }, unsubscribe: (callback) => { notificationSubscribers.current = notificationSubscribers.current.filter((cb) => cb !== callback); }, }; const Provider = { setAPIKey: async (provider, apiKey) => (await fetch(`/api/provider/${provider}/api-key`, {method: "PUT", headers: {"Content-Type": "application/json"}, body: JSON.stringify({apiKey: apiKey})} )).json(), getAll: async () => (await fetch("/api/providers")).json(), getAllWithModels: async () => (await fetch("/api/providers-with-key-and-models")).json(), }; const Inference = { subscribeTextCompletion: (callback) => { textCompletionSubscribers.current.push(callback); }, unsubscribeTextCompletion: (callback) => { textCompletionSubscribers.current = textCompletionSubscribers.current.filter((cb) => cb !== callback); }, textCompletionRequest: createTextCompletionRequest, subscribeChatCompletion: (callback) => { chatCompletionSubscribers.current.push(callback); }, unsubscribeChatCompletion: (callback) => { chatCompletionSubscribers.current = chatCompletionSubscribers.current.filter((cb) => cb !== callback); }, chatCompletion: createChatCompletionRequest, }; const [apiContext, _] = React.useState({ Model, Notifications, Provider, Inference, }); function createTextCompletionRequest({prompt, models}) { const url = "/api/inference/text/stream"; const payload = { prompt: prompt, models: models, }; return createCompletionRequest(url, payload, textCompletionSubscribers); } function createChatCompletionRequest(prompt, model) { const url = "/api/inference/chat/stream"; const payload = {prompt, model}; return createCompletionRequest(url, payload, chatCompletionSubscribers); } function createCompletionRequest(url, payload, subscribers) { pendingCompletionRequest.current = true; let sse_request = null; function beforeUnloadHandler() { if (sse_request) sse_request.close(); } window.addEventListener("beforeunload", beforeUnloadHandler); const completionsBuffer = createCompletionsBuffer(payload.models); let error_occured = false; let request_complete = false; sse_request = new SSE(url, {payload: JSON.stringify(payload)}); bindSSEEvents(sse_request, completionsBuffer, {error_occured, request_complete}, beforeUnloadHandler, subscribers); return () => { if (sse_request) sse_request.close(); }; } function createCompletionsBuffer(models) { const buffer = {}; models.forEach((model) => { buffer[model.tag] = []; }); return buffer; } function bindSSEEvents(sse_request, completionsBuffer, requestState, beforeUnloadHandler, subscribers) { sse_request.onopen = async () => { bulkWrite(completionsBuffer, requestState, subscribers); }; sse_request.addEventListener("infer", (event) => { let resp = JSON.parse(event.data); completionsBuffer[resp.modelTag].push(resp); }); sse_request.addEventListener("status", (event) => { subscribers.current.forEach((callback) => callback({ event: "status", data: JSON.parse(event.data) })); }); sse_request.addEventListener("error", (event) => { requestState.error_occured = true; try { const message = JSON.parse(event.data); subscribers.current.forEach((callback) => callback({ "event": "error", "data": message.status })); } catch (e) { subscribers.current.forEach((callback) => callback({ "event": "error", "data": "Unknown error" })); } close_sse(sse_request, requestState, beforeUnloadHandler, subscribers); }); sse_request.addEventListener("abort", () => { requestState.error_occured = true; close_sse(sse_request, requestState, beforeUnloadHandler, subscribers); }); sse_request.addEventListener("readystatechange", (event) => { if (event.readyState === 2) close_sse(sse_request, requestState, beforeUnloadHandler, subscribers); }); sse_request.stream(); } function close_sse(sse_request, requestState, beforeUnloadHandler, subscribers) { requestState.request_complete = true; subscribers.current.forEach((callback) => callback({ "event": "close", "meta": {error: requestState.error_occured}, })); window.removeEventListener("beforeunload", beforeUnloadHandler); } function bulkWrite(completionsBuffer, requestState, subscribers) { setTimeout(() => { let newTokens = false; let batchUpdate = {}; for (let modelTag in completionsBuffer) { if (completionsBuffer[modelTag].length > 0) { newTokens = true; batchUpdate[modelTag] = completionsBuffer[modelTag].splice(0, completionsBuffer[modelTag].length); } } if (newTokens) { subscribers.current.forEach((callback) => callback({ event: "completion", data: batchUpdate, })); } if (!requestState.request_complete) bulkWrite(completionsBuffer, requestState, subscribers); }, 20); } return ( {children} ) } const PlaygroundContextWrapper = ({page, children}) => { const apiContext = React.useContext(APIContext) const [editorContext, _setEditorContext] = React.useState(DEFAULT_CONTEXTS.PAGES[page].editor); const [parametersContext, _setParametersContext] = React.useState(DEFAULT_CONTEXTS.PAGES[page].parameters); let [modelsStateContext, _setModelsStateContext] = React.useState(DEFAULT_CONTEXTS.PAGES[page].modelsState); const [modelsContext, _setModelsContext] = React.useState(DEFAULT_CONTEXTS.MODELS); const [historyContext, _setHistoryContext] = React.useState(DEFAULT_CONTEXTS.PAGES[page].history); /* Temporary fix for models that have been purged remotely but are still cached locally */ for(const {name} of modelsStateContext) { if (!modelsContext[name]) { modelsStateContext = modelsStateContext.filter(({name: _name}) => _name !== name) } } const editorContextRef = React.useRef(editorContext); const historyContextRef = React.useRef(historyContext); React.useEffect(() => { historyContextRef.current = historyContext; editorContextRef.current = editorContext; }, [historyContext, editorContext]); const {toast} = useToast() useEffect(() => { const notificationCallback = ({event, data, meta}) => { console.warn("NOTIFICATION CALLBACK", event, data) switch (event) { case "modelAdded": toast({ title: "New Model is available!", description: `${data.provider}'s model ${data.model} has been added to the playground!`, }) updateModelsData().catch(console.error) break; case "modelRemoved": toast({ title: "Model removed!", description: `${data.provider}'s model ${data.model} has been removed from the playground!`, }) updateModelsData().catch(console.error) break; default: console.log("Unknown event????", event, data); break; } } apiContext.Notifications.subscribe(notificationCallback) return () => { apiContext.Notifications.unsubscribe(notificationCallback); }; }, []); const updateModelsData = async () => { const json_params = await apiContext.Model.getAllEnabled() const models = {}; const PAGE_MODELS_STATE = SETTINGS.pages[page].modelsState; for (const [model_key, modelDetails] of Object.entries(json_params)) { const existingModelEntry = (PAGE_MODELS_STATE.find((model) => model.name === model_key)); if (!existingModelEntry) { PAGE_MODELS_STATE.push({ name: model_key, tag: model_key, capabilities: modelDetails.capabilities, provider: modelDetails.provider, parameters: Object.entries(modelDetails.parameters).reduce((acc, [key, fields]) => { acc[key] = fields.value; return acc; }, {}), enabled: (page === "compare") ? false : true, selected: false }) } else { if (!existingModelEntry.parameters) { existingModelEntry.capabilites = modelDetails.capabilities, existingModelEntry.provider = modelDetails.provider, existingModelEntry.tag = model_key; existingModelEntry.parameters = Object.entries(modelDetails.parameters).reduce((acc, [key, fields]) => { acc[key] = fields.value; return acc; }, {}); } } models[model_key] = { name: model_key, capabilities: modelDetails.capabilities, defaultParameters: modelDetails.parameters, provider: modelDetails.provider, } } const SERVER_SIDE_MODELS = Object.keys(json_params); for (const {name} of PAGE_MODELS_STATE) { if (!SERVER_SIDE_MODELS.includes(name)) { PAGE_MODELS_STATE.splice(PAGE_MODELS_STATE.findIndex((model) => model.name === name), 1) } } setModelsContext(models) setModelsStateContext(PAGE_MODELS_STATE) } const debouncedSettingsSave = useDebounce(saveSettings, 3000); const setEditorContext = (newEditorContext, immediate=false) => { SETTINGS.pages[page].editor = {...SETTINGS.pages[page].editor, ...newEditorContext}; const _editor = {...SETTINGS.pages[page].editor, internalState: null }; _setEditorContext(_editor); if (immediate) { saveSettings() } else { debouncedSettingsSave() } } const setParametersContext = (newParameters) => { const parameters = { ...DEFAULT_PARAMETERS_STATE, ...newParameters} SETTINGS.pages[page].parameters = parameters; debouncedSettingsSave() _setParametersContext(parameters); } const setModelsContext = (newModels) => { SETTINGS.models = newModels; debouncedSettingsSave() _setModelsContext(newModels); } const setModelsStateContext = (newModelsState) => { SETTINGS.pages[page].modelsState = newModelsState; debouncedSettingsSave() _setModelsStateContext(newModelsState); } const toggleShowHistory = (value) => { const _newHistory = { ...SETTINGS.pages[page].history, show: (value === undefined || value === null) ? !SETTINGS.pages[page].history.show : value } _setHistoryContext(_newHistory); SETTINGS.pages[page].history = _newHistory; debouncedSettingsSave() } const addHistoryEntry = (editorState) => { //check if device is mobile by navigator const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); if (isMobile) return; const currentDate = new Date(); const year = currentDate.getFullYear(); const month = String(currentDate.getMonth() + 1).padStart(2, '0'); const day = String(currentDate.getDate()).padStart(2, '0'); const newEntry = { timestamp: currentDate.getTime(), date: `${year}-${month}-${day}`, editor: { ...editorContextRef.current, internalState: editorState }, parameters: SETTINGS.pages[page].parameters, modelsState: SETTINGS.pages[page].modelsState, } const _newHistory = { ...SETTINGS.pages[page].history, entries: [newEntry, ...SETTINGS.pages[page].history.entries], current: newEntry } _setHistoryContext(_newHistory); //console.warn("Adding to history", _newHistory) SETTINGS.pages[page].history = _newHistory; debouncedSettingsSave() } const removeHistoryEntry = (entry) => { const _newHistory = { ...SETTINGS.pages[page].history, entries: SETTINGS.pages[page].history.entries.filter((historyEntry) => historyEntry !== entry) } _setHistoryContext(_newHistory); SETTINGS.pages[page].history = _newHistory; debouncedSettingsSave() } const clearHistory = () => { const _newHistory = { entries: [], show: false, current: null } _setHistoryContext(_newHistory); SETTINGS.pages[page].history = _newHistory; debouncedSettingsSave() } const selectHistoryItem = (entry) => { SETTINGS.pages[page].history.current = entry; _setEditorContext(entry.editor); _setHistoryContext(SETTINGS.pages[page].history); setParametersContext(entry.parameters); setModelsStateContext(entry.modelsState); } React.useEffect(() => { updateModelsData().catch(console.error) }, []) return ( {children} ) } function ProviderWithRoutes() { return ( } /> } /> } /> ); } export default function App() { return ( ) } ================================================ FILE: app/src/components/inputarea.tsx ================================================ import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" export function InputArea() { return (