Spaces:
Running
Running
Juan Jimenez Carrero
feat: sync robot preferences to HF, thinking movement + wait/announce on plan
b5352be | import { createRepo, uploadFiles, downloadFile, whoAmI } from "@huggingface/hub"; | |
| import type { AppData, SyncableSettings } from "./store"; | |
| import { validateData } from "./store"; | |
| /** Optional sync of the app data to a private HF dataset owned by the user. | |
| * Requires a user-pasted fine-grained token (write access to the dataset); | |
| * the host shell's OAuth token is short-lived and read-only, so it can't be | |
| * used for this. The token never leaves the browser except toward hf.co. */ | |
| const DATASET_NAME = "cookaiware-data"; | |
| const DATA_FILE = "cookaiware-data.json"; | |
| const SETTINGS_FILE = "cookaiware-settings.json"; | |
| export interface HfSyncIdentity { | |
| userName: string; | |
| repoId: string; | |
| } | |
| export async function resolveIdentity(token: string): Promise<HfSyncIdentity> { | |
| const me = await whoAmI({ accessToken: token }); | |
| return { userName: me.name, repoId: `datasets/${me.name}/${DATASET_NAME}` }; | |
| } | |
| async function ensureRepo(token: string, repoId: string): Promise<void> { | |
| try { | |
| await createRepo({ repo: repoId, accessToken: token, private: true }); | |
| } catch (err) { | |
| // The repo usually already exists (every save after the first), and the | |
| // Hub wording for that error varies — so never fail the save here. If the | |
| // repo is genuinely missing/unreachable, uploadFiles will raise the real | |
| // error right after and that one is surfaced. | |
| console.debug("[hf-sync] createRepo skipped:", err instanceof Error ? err.message : err); | |
| } | |
| } | |
| export async function uploadData( | |
| token: string, | |
| data: AppData, | |
| settings?: SyncableSettings, | |
| ): Promise<HfSyncIdentity> { | |
| const identity = await resolveIdentity(token); | |
| await ensureRepo(token, identity.repoId); | |
| const files = [ | |
| { path: DATA_FILE, content: new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }) }, | |
| ]; | |
| if (settings) { | |
| files.push({ | |
| path: SETTINGS_FILE, | |
| content: new Blob([JSON.stringify(settings, null, 2)], { type: "application/json" }), | |
| }); | |
| } | |
| await uploadFiles({ | |
| repo: identity.repoId, | |
| accessToken: token, | |
| files, | |
| commitTitle: "Update CookAIware data", | |
| }); | |
| return identity; | |
| } | |
| export async function downloadData(token: string): Promise<AppData | null> { | |
| const identity = await resolveIdentity(token); | |
| const res = await downloadFile({ repo: identity.repoId, path: DATA_FILE, accessToken: token }); | |
| if (!res) return null; | |
| return validateData(JSON.parse(await res.text())); | |
| } | |
| /** Fetch the synced settings subset, or null if none has been uploaded yet | |
| * (older datasets, or a device that never enabled sync). */ | |
| export async function downloadSettings(token: string): Promise<unknown | null> { | |
| try { | |
| const identity = await resolveIdentity(token); | |
| const res = await downloadFile({ repo: identity.repoId, path: SETTINGS_FILE, accessToken: token }); | |
| if (!res) return null; | |
| return JSON.parse(await res.text()); | |
| } catch (err) { | |
| console.debug("[hf-sync] no settings file:", err instanceof Error ? err.message : err); | |
| return null; | |
| } | |
| } | |