Remote storage for saves
This commit is contained in:
parent
a0b73eb306
commit
21a26859da
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks';
|
||||||
import styles from './header.module.css';
|
import styles from './header.module.css';
|
||||||
import { Connection, HORDE_ANON_KEY, isHordeConnection, isKoboldConnection, type IConnection, type IHordeModel } from '../../tools/connection';
|
import { Connection, HORDE_ANON_KEY, type IConnection, type IHordeModel } from '../../tools/connection';
|
||||||
import { Instruct } from '../../contexts/state';
|
import { Instruct } from '../../contexts/state';
|
||||||
import { useInputState } from '@common/hooks/useInputState';
|
import { useInputState } from '@common/hooks/useInputState';
|
||||||
import { useInputCallback } from '@common/hooks/useInputCallback';
|
import { useInputCallback } from '@common/hooks/useInputCallback';
|
||||||
|
|
@ -23,24 +23,17 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => {
|
||||||
const [hordeModels, setHordeModels] = useState<IHordeModel[]>([]);
|
const [hordeModels, setHordeModels] = useState<IHordeModel[]>([]);
|
||||||
const [contextLength, setContextLength] = useState<number>(0);
|
const [contextLength, setContextLength] = useState<number>(0);
|
||||||
|
|
||||||
const backendType = useMemo(() => {
|
|
||||||
if (isKoboldConnection(connection)) return 'kobold';
|
|
||||||
if (isHordeConnection(connection)) return 'horde';
|
|
||||||
return 'unknown';
|
|
||||||
}, [connection]);
|
|
||||||
|
|
||||||
const isOnline = useMemo(() => contextLength > 0, [contextLength]);
|
const isOnline = useMemo(() => contextLength > 0, [contextLength]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInstruct(connection.instruct);
|
setInstruct(connection.instruct);
|
||||||
|
connection.url && setConnectionUrl(connection.url);
|
||||||
if (isKoboldConnection(connection)) {
|
connection.model && setModelName(connection.model);
|
||||||
setConnectionUrl(connection.url);
|
|
||||||
Connection.getContextLength(connection).then(setContextLength);
|
|
||||||
} else if (isHordeConnection(connection)) {
|
|
||||||
setModelName(connection.model);
|
|
||||||
setApiKey(connection.apiKey || HORDE_ANON_KEY);
|
setApiKey(connection.apiKey || HORDE_ANON_KEY);
|
||||||
|
|
||||||
|
if (connection.type === 'kobold') {
|
||||||
|
Connection.getContextLength(connection).then(setContextLength);
|
||||||
|
} else if (connection.type === 'horde') {
|
||||||
Connection.getHordeModels()
|
Connection.getHordeModels()
|
||||||
.then(m => setHordeModels(Array.from(m.values()).sort((a, b) => a.name.localeCompare(b.name))));
|
.then(m => setHordeModels(Array.from(m.values()).sort((a, b) => a.name.localeCompare(b.name))));
|
||||||
}
|
}
|
||||||
|
|
@ -59,22 +52,22 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => {
|
||||||
}, [modelName]);
|
}, [modelName]);
|
||||||
|
|
||||||
const setBackendType = useInputCallback((type) => {
|
const setBackendType = useInputCallback((type) => {
|
||||||
if (type === 'kobold') {
|
switch (type) {
|
||||||
|
case 'kobold':
|
||||||
|
case 'horde':
|
||||||
setConnection({
|
setConnection({
|
||||||
|
type,
|
||||||
instruct,
|
instruct,
|
||||||
url: connectionUrl,
|
url: connectionUrl,
|
||||||
});
|
|
||||||
} else if (type === 'horde') {
|
|
||||||
setConnection({
|
|
||||||
instruct,
|
|
||||||
apiKey,
|
apiKey,
|
||||||
model: modelName,
|
model: modelName,
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, [setConnection, connectionUrl, apiKey, modelName, instruct]);
|
}, [setConnection, connectionUrl, apiKey, modelName, instruct]);
|
||||||
|
|
||||||
const handleSetInstruct = useInputCallback((instruct: string) => {
|
const handleSetInstruct = useInputCallback((instruct: string) => {
|
||||||
setConnection({...connection, instruct});
|
setConnection({ ...connection, instruct });
|
||||||
}, [setConnection, connection]);
|
}, [setConnection, connection]);
|
||||||
|
|
||||||
const handleBlurUrl = useCallback(() => {
|
const handleBlurUrl = useCallback(() => {
|
||||||
|
|
@ -82,14 +75,19 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => {
|
||||||
const url = connectionUrl.replace(regex, 'http$1://$2');
|
const url = connectionUrl.replace(regex, 'http$1://$2');
|
||||||
|
|
||||||
setConnection({
|
setConnection({
|
||||||
|
type: 'kobold',
|
||||||
instruct,
|
instruct,
|
||||||
url,
|
url,
|
||||||
|
apiKey,
|
||||||
|
model: modelName,
|
||||||
});
|
});
|
||||||
}, [connectionUrl, instruct, setConnection]);
|
}, [connectionUrl, instruct, setConnection]);
|
||||||
|
|
||||||
const handleBlurHorde = useCallback(() => {
|
const handleBlurHorde = useCallback(() => {
|
||||||
setConnection({
|
setConnection({
|
||||||
|
type: 'horde',
|
||||||
instruct,
|
instruct,
|
||||||
|
url: connectionUrl,
|
||||||
apiKey,
|
apiKey,
|
||||||
model: modelName,
|
model: modelName,
|
||||||
});
|
});
|
||||||
|
|
@ -97,7 +95,7 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.connectionEditor}>
|
<div class={styles.connectionEditor}>
|
||||||
<select value={backendType} onChange={setBackendType}>
|
<select value={connection.type} onChange={setBackendType}>
|
||||||
<option value='kobold'>Kobold CPP</option>
|
<option value='kobold'>Kobold CPP</option>
|
||||||
<option value='horde'>Horde</option>
|
<option value='horde'>Horde</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -116,13 +114,13 @@ export const ConnectionEditor = ({ connection, setConnection }: IProps) => {
|
||||||
<option value={connection.instruct}>Custom</option>
|
<option value={connection.instruct}>Custom</option>
|
||||||
</optgroup>}
|
</optgroup>}
|
||||||
</select>
|
</select>
|
||||||
{isKoboldConnection(connection) && <input
|
{connection.type === 'kobold' && <input
|
||||||
value={connectionUrl}
|
value={connectionUrl}
|
||||||
onInput={setConnectionUrl}
|
onInput={setConnectionUrl}
|
||||||
onBlur={handleBlurUrl}
|
onBlur={handleBlurUrl}
|
||||||
class={isOnline ? styles.valid : styles.invalid}
|
class={isOnline ? styles.valid : styles.invalid}
|
||||||
/>}
|
/>}
|
||||||
{isHordeConnection(connection) && <>
|
{connection.type === 'horde' && <>
|
||||||
<input
|
<input
|
||||||
placeholder='Horde API key'
|
placeholder='Horde API key'
|
||||||
title='Horde API key'
|
title='Horde API key'
|
||||||
|
|
|
||||||
|
|
@ -57,15 +57,17 @@ export const Header = () => {
|
||||||
<div class={styles.header}>
|
<div class={styles.header}>
|
||||||
<div class={styles.inputs}>
|
<div class={styles.inputs}>
|
||||||
<div class={styles.buttons}>
|
<div class={styles.buttons}>
|
||||||
<button class={`icon ${isOnline ? styles.online: styles.offline}`} onClick={connectionsOpen.setTrue} title='Connection settings'>
|
<button class={`icon ${isOnline ? styles.online : styles.offline}`} onClick={connectionsOpen.setTrue} title='Connection settings'>
|
||||||
🔌
|
🔌
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.info}>
|
<div class={styles.info}>
|
||||||
<span>{modelName}</span>
|
<span>{modelName}</span>
|
||||||
<span>📃{promptTokens}/{contextLength}</span>
|
<span>📃{promptTokens}/{contextLength}</span>
|
||||||
|
{connection.type === 'horde' ? <>
|
||||||
<span>💲{spentKudos}</span>
|
<span>💲{spentKudos}</span>
|
||||||
<span>💰{totalSpentKudos}</span>
|
<span>💰{totalSpentKudos}</span>
|
||||||
|
</> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.buttons}>
|
<div class={styles.buttons}>
|
||||||
|
|
|
||||||
|
|
@ -195,9 +195,16 @@ export const LLMContextProvider = ({ children }: { children?: any }) => {
|
||||||
const prompt = Huggingface.applyChatTemplate(connection.instruct, [{ role: 'user', content }]);
|
const prompt = Huggingface.applyChatTemplate(connection.instruct, [{ role: 'user', content }]);
|
||||||
console.log('[LLM.summarize]', prompt);
|
console.log('[LLM.summarize]', prompt);
|
||||||
|
|
||||||
const tokens = await Array.fromAsync(Connection.generate(connection, prompt, {}));
|
const tokens = await Array.fromAsync(Connection.generate(connection, prompt));
|
||||||
|
const summary = tokens.reduce((sum, token) => ({
|
||||||
|
text: sum.text + token.text,
|
||||||
|
cost: sum.cost + token.cost,
|
||||||
|
}), { text: '', cost: 0 });
|
||||||
|
|
||||||
return MessageTools.trimSentence(tokens.join(''));
|
setSpentKudos(sk => sk + summary.cost);
|
||||||
|
setTotalSpentKudos(sk => sk + summary.cost);
|
||||||
|
|
||||||
|
return MessageTools.trimSentence(summary.text);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error summarizing:', e);
|
console.error('Error summarizing:', e);
|
||||||
return '';
|
return '';
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState, type Dispatch, type StateUpd
|
||||||
import { MessageTools, type IMessage } from "../tools/messages";
|
import { MessageTools, type IMessage } from "../tools/messages";
|
||||||
import { useInputState } from "@common/hooks/useInputState";
|
import { useInputState } from "@common/hooks/useInputState";
|
||||||
import { type IConnection } from "../tools/connection";
|
import { type IConnection } from "../tools/connection";
|
||||||
|
import { loadObject, saveObject } from "../tools/storage";
|
||||||
|
|
||||||
interface IContext {
|
interface IContext {
|
||||||
currentConnection: number;
|
currentConnection: number;
|
||||||
|
|
@ -72,8 +73,9 @@ export enum Instruct {
|
||||||
const DEFAULT_CONTEXT: IContext = {
|
const DEFAULT_CONTEXT: IContext = {
|
||||||
currentConnection: 0,
|
currentConnection: 0,
|
||||||
availableConnections: [{
|
availableConnections: [{
|
||||||
|
type: 'kobold',
|
||||||
url: 'http://localhost:5001',
|
url: 'http://localhost:5001',
|
||||||
instruct: Instruct.CHATML,
|
instruct: Instruct.MISTRAL,
|
||||||
}],
|
}],
|
||||||
input: '',
|
input: '',
|
||||||
systemPrompt: 'You are a creative writer. Write a story based on the world description below. Story should be adult and mature; and could include swearing, violence and unfairness. Portray characters realistically and stay in the lore.',
|
systemPrompt: 'You are a creative writer. Write a story based on the world description below. Story should be adult and mature; and could include swearing, violence and unfairness. Portray characters realistically and stay in the lore.',
|
||||||
|
|
@ -98,33 +100,21 @@ Make sure to follow the world description and rules exactly. Avoid cliffhangers
|
||||||
continueLast: false,
|
continueLast: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveContext = (context: IContext) => {
|
export const saveContext = async (context: IContext) => {
|
||||||
const contextToSave: Partial<IContext> = { ...context };
|
const contextToSave: Partial<IContext> = { ...context };
|
||||||
delete contextToSave.triggerNext;
|
delete contextToSave.triggerNext;
|
||||||
delete contextToSave.continueLast;
|
delete contextToSave.continueLast;
|
||||||
|
|
||||||
localStorage.setItem(SAVE_KEY, JSON.stringify(contextToSave));
|
return saveObject(SAVE_KEY, contextToSave);
|
||||||
}
|
|
||||||
|
|
||||||
export const loadContext = (): IContext => {
|
|
||||||
let loadedContext: Partial<IContext> = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const json = localStorage.getItem(SAVE_KEY);
|
|
||||||
if (json) {
|
|
||||||
loadedContext = JSON.parse(json);
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
return { ...DEFAULT_CONTEXT, ...loadedContext };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IStateContext = IContext & IActions & IComputableContext;
|
export type IStateContext = IContext & IActions & IComputableContext;
|
||||||
|
|
||||||
export const StateContext = createContext<IStateContext>({} as IStateContext);
|
export const StateContext = createContext<IStateContext>({} as IStateContext);
|
||||||
|
|
||||||
|
const loadedContext = await loadObject(SAVE_KEY, DEFAULT_CONTEXT);
|
||||||
|
|
||||||
export const StateContextProvider = ({ children }: { children?: any }) => {
|
export const StateContextProvider = ({ children }: { children?: any }) => {
|
||||||
const loadedContext = useMemo(() => loadContext(), []);
|
|
||||||
const [currentConnection, setCurrentConnection] = useState<number>(loadedContext.currentConnection);
|
const [currentConnection, setCurrentConnection] = useState<number>(loadedContext.currentConnection);
|
||||||
const [availableConnections, setAvailableConnections] = useState<IConnection[]>(loadedContext.availableConnections);
|
const [availableConnections, setAvailableConnections] = useState<IConnection[]>(loadedContext.availableConnections);
|
||||||
const [input, setInput] = useInputState(loadedContext.input);
|
const [input, setInput] = useInputState(loadedContext.input);
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,24 @@ import { Huggingface } from "./huggingface";
|
||||||
import { approximateTokens, normalizeModel } from "./model";
|
import { approximateTokens, normalizeModel } from "./model";
|
||||||
|
|
||||||
interface IBaseConnection {
|
interface IBaseConnection {
|
||||||
|
type: 'kobold' | 'horde';
|
||||||
instruct: string;
|
instruct: string;
|
||||||
|
url?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
model?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IKoboldConnection extends IBaseConnection {
|
interface IKoboldConnection extends IBaseConnection {
|
||||||
|
type: 'kobold';
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHordeConnection extends IBaseConnection {
|
interface IHordeConnection extends IBaseConnection {
|
||||||
|
type: 'horde';
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model: string;
|
model: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isKoboldConnection = (obj: unknown): obj is IKoboldConnection => (
|
|
||||||
obj != null && typeof obj === 'object' && 'url' in obj && typeof obj.url === 'string'
|
|
||||||
);
|
|
||||||
|
|
||||||
export const isHordeConnection = (obj: unknown): obj is IHordeConnection => (
|
|
||||||
obj != null && typeof obj === 'object' && 'model' in obj && typeof obj.model === 'string'
|
|
||||||
);
|
|
||||||
|
|
||||||
export type IConnection = IKoboldConnection | IHordeConnection;
|
export type IConnection = IKoboldConnection | IHordeConnection;
|
||||||
|
|
||||||
interface IHordeWorker {
|
interface IHordeWorker {
|
||||||
|
|
@ -264,9 +262,9 @@ export namespace Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function* generate(connection: IConnection, prompt: string, extraSettings: IGenerationSettings = {}) {
|
export async function* generate(connection: IConnection, prompt: string, extraSettings: IGenerationSettings = {}) {
|
||||||
if (isKoboldConnection(connection)) {
|
if (connection.type === 'kobold') {
|
||||||
yield* generateKobold(connection.url, prompt, extraSettings);
|
yield* generateKobold(connection.url, prompt, extraSettings);
|
||||||
} else if (isHordeConnection(connection)) {
|
} else if (connection.type === 'horde') {
|
||||||
yield* generateHorde(connection, prompt, extraSettings);
|
yield* generateHorde(connection, prompt, extraSettings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -331,7 +329,7 @@ export namespace Connection {
|
||||||
export const getHordeModels = throttle(requestHordeModels, 10000);
|
export const getHordeModels = throttle(requestHordeModels, 10000);
|
||||||
|
|
||||||
export async function getModelName(connection: IConnection): Promise<string> {
|
export async function getModelName(connection: IConnection): Promise<string> {
|
||||||
if (isKoboldConnection(connection)) {
|
if (connection.type === 'kobold') {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${connection.url}/api/v1/model`);
|
const response = await fetch(`${connection.url}/api/v1/model`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
@ -341,7 +339,7 @@ export namespace Connection {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error getting max tokens', e);
|
console.error('Error getting max tokens', e);
|
||||||
}
|
}
|
||||||
} else if (isHordeConnection(connection)) {
|
} else if (connection.type === 'horde') {
|
||||||
return connection.model;
|
return connection.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -349,7 +347,7 @@ export namespace Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getContextLength(connection: IConnection): Promise<number> {
|
export async function getContextLength(connection: IConnection): Promise<number> {
|
||||||
if (isKoboldConnection(connection)) {
|
if (connection.type === 'kobold') {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${connection.url}/api/extra/true_max_context_length`);
|
const response = await fetch(`${connection.url}/api/extra/true_max_context_length`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
|
@ -359,7 +357,7 @@ export namespace Connection {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error getting max tokens', e);
|
console.error('Error getting max tokens', e);
|
||||||
}
|
}
|
||||||
} else if (isHordeConnection(connection) && connection.model) {
|
} else if (connection.type === 'horde' && connection.model) {
|
||||||
const models = await getHordeModels();
|
const models = await getHordeModels();
|
||||||
const model = models.get(connection.model);
|
const model = models.get(connection.model);
|
||||||
if (model) {
|
if (model) {
|
||||||
|
|
@ -371,7 +369,7 @@ export namespace Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countTokens(connection: IConnection, prompt: string) {
|
export async function countTokens(connection: IConnection, prompt: string) {
|
||||||
if (isKoboldConnection(connection)) {
|
if (connection.type === 'kobold') {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${connection.url}/api/extra/tokencount`, {
|
const response = await fetch(`${connection.url}/api/extra/tokencount`, {
|
||||||
body: JSON.stringify({ prompt }),
|
body: JSON.stringify({ prompt }),
|
||||||
|
|
@ -385,7 +383,7 @@ export namespace Connection {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error counting tokens:', e);
|
console.error('Error counting tokens:', e);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (connection.type === 'horde') {
|
||||||
const model = await getModelName(connection);
|
const model = await getModelName(connection);
|
||||||
const tokenizer = await Huggingface.findTokenizer(model);
|
const tokenizer = await Huggingface.findTokenizer(model);
|
||||||
if (tokenizer) {
|
if (tokenizer) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import * as hub from '@huggingface/hub';
|
||||||
import { Template } from '@huggingface/jinja';
|
import { Template } from '@huggingface/jinja';
|
||||||
import { AutoTokenizer, PreTrainedTokenizer } from '@huggingface/transformers';
|
import { AutoTokenizer, PreTrainedTokenizer } from '@huggingface/transformers';
|
||||||
import { normalizeModel } from './model';
|
import { normalizeModel } from './model';
|
||||||
|
import { loadObject, saveObject } from './storage';
|
||||||
|
|
||||||
export namespace Huggingface {
|
export namespace Huggingface {
|
||||||
export interface ITemplateMessage {
|
export interface ITemplateMessage {
|
||||||
|
|
@ -60,27 +61,9 @@ export namespace Huggingface {
|
||||||
|
|
||||||
const TEMPLATE_CACHE_KEY = 'ai_game_template_cache';
|
const TEMPLATE_CACHE_KEY = 'ai_game_template_cache';
|
||||||
|
|
||||||
const loadCache = (): Record<string, string> => {
|
const templateCache: Record<string, string> = {};
|
||||||
const json = localStorage.getItem(TEMPLATE_CACHE_KEY);
|
loadObject(TEMPLATE_CACHE_KEY, {}).then(c => Object.assign(templateCache, c));
|
||||||
|
|
||||||
try {
|
|
||||||
if (json) {
|
|
||||||
const cache = JSON.parse(json);
|
|
||||||
if (cache && typeof cache === 'object') {
|
|
||||||
return cache
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveCache = (cache: Record<string, string>) => {
|
|
||||||
const json = JSON.stringify(cache);
|
|
||||||
localStorage.setItem(TEMPLATE_CACHE_KEY, json);
|
|
||||||
};
|
|
||||||
|
|
||||||
const templateCache: Record<string, string> = loadCache();
|
|
||||||
const compiledTemplates = new Map<string, Template>();
|
const compiledTemplates = new Map<string, Template>();
|
||||||
const tokenizerCache = new Map<string, PreTrainedTokenizer | null>();
|
const tokenizerCache = new Map<string, PreTrainedTokenizer | null>();
|
||||||
|
|
||||||
|
|
@ -261,7 +244,7 @@ export namespace Huggingface {
|
||||||
}
|
}
|
||||||
|
|
||||||
templateCache[modelName] = template;
|
templateCache[modelName] = template;
|
||||||
saveCache(templateCache);
|
saveObject(TEMPLATE_CACHE_KEY, templateCache);
|
||||||
|
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
const API_KEY = 'awoorwa32';
|
||||||
|
|
||||||
|
export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> => {
|
||||||
|
let localObject: Partial<T> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = localStorage.getItem(key);
|
||||||
|
if (json) {
|
||||||
|
localObject = JSON.parse(json);
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
let remoteObject: Partial<T> = {};
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://demo.pabloader.ru/storage/${key}`);
|
||||||
|
if (response.ok) {
|
||||||
|
remoteObject = await response.json();
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
return { ...defaultObject, ...localObject, ...remoteObject };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveObject = async <T>(key: string, obj: T) => {
|
||||||
|
const saveData = JSON.stringify(obj);
|
||||||
|
|
||||||
|
localStorage.setItem(key, saveData);
|
||||||
|
try {
|
||||||
|
const url = new URL('https://demo.pabloader.ru/storage/index.php');
|
||||||
|
url.searchParams.set('filename', key);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${API_KEY}`,
|
||||||
|
},
|
||||||
|
body: saveData,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save context');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue