Skip to content

Commit 1a618f6

Browse files
committed
introduce keys.json
1 parent d1a9aa4 commit 1a618f6

File tree

12 files changed

+391
-42
lines changed

12 files changed

+391
-42
lines changed

apps/desktop/src/components/settings/ai/llm/health.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useEffect, useMemo } from "react";
55
import { useBillingAccess } from "../../../../billing";
66
import { useConfigValues } from "../../../../config/use-config";
77
import { useLanguageModel } from "../../../../hooks/useLLMConnection";
8-
import * as main from "../../../../store/tinybase/main";
8+
import * as keys from "../../../../store/tinybase/keys";
99
import { AvailabilityHealth, ConnectionHealth } from "../shared/health";
1010
import { llmProviderRequiresPro, PROVIDERS } from "./shared";
1111

@@ -104,9 +104,9 @@ function useAvailability() {
104104
] as const);
105105
const billing = useBillingAccess();
106106

107-
const configuredProviders = main.UI.useResultTable(
108-
main.QUERIES.llmProviders,
109-
main.STORE_ID,
107+
const configuredProviders = keys.UI.useResultTable(
108+
keys.QUERIES.llmProviders,
109+
keys.STORE_ID,
110110
);
111111

112112
const result = useMemo(() => {

apps/desktop/src/components/settings/ai/llm/select.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { cn } from "@hypr/utils";
1313
import { useAuth } from "../../../../auth";
1414
import { useBillingAccess } from "../../../../billing";
1515
import { useConfigValues } from "../../../../config/use-config";
16+
import * as keys from "../../../../store/tinybase/keys";
1617
import * as main from "../../../../store/tinybase/main";
1718
import { listAnthropicModels } from "../shared/list-anthropic";
1819
import {
@@ -183,9 +184,9 @@ function useConfiguredMapping(): Record<
183184
> {
184185
const auth = useAuth();
185186
const billing = useBillingAccess();
186-
const configuredProviders = main.UI.useResultTable(
187-
main.QUERIES.llmProviders,
188-
main.STORE_ID,
187+
const configuredProviders = keys.UI.useResultTable(
188+
keys.QUERIES.llmProviders,
189+
keys.STORE_ID,
189190
);
190191

191192
const mapping = useMemo(() => {

apps/desktop/src/components/settings/ai/shared/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "@hypr/ui/components/ui/input-group";
1313
import { cn } from "@hypr/utils";
1414

15-
import * as main from "../../../../store/tinybase/main";
15+
import * as keys from "../../../../store/tinybase/keys";
1616

1717
export * from "./model-combobox";
1818

@@ -58,13 +58,13 @@ export function StyledStreamdown({
5858
}
5959

6060
export function useProvider(id: string) {
61-
const providerRow = main.UI.useRow("ai_providers", id, main.STORE_ID);
62-
const setProvider = main.UI.useSetPartialRowCallback(
61+
const providerRow = keys.UI.useRow("ai_providers", id, keys.STORE_ID);
62+
const setProvider = keys.UI.useSetPartialRowCallback(
6363
"ai_providers",
6464
id,
6565
(row: Partial<AIProvider>) => row,
6666
[id],
67-
main.STORE_ID,
67+
keys.STORE_ID,
6868
) as (row: Partial<AIProvider>) => void;
6969

7070
const { data } = aiProviderSchema.safeParse(providerRow);

apps/desktop/src/components/settings/ai/stt/select.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { cn } from "@hypr/utils";
1515

1616
import { useBillingAccess } from "../../../../billing";
1717
import { useConfigValues } from "../../../../config/use-config";
18+
import * as keys from "../../../../store/tinybase/keys";
1819
import * as main from "../../../../store/tinybase/main";
1920
import { HealthCheckForConnection } from "./health";
2021
import {
@@ -216,9 +217,9 @@ function useConfiguredMapping(): Record<
216217
}
217218
> {
218219
const billing = useBillingAccess();
219-
const configuredProviders = main.UI.useResultTable(
220-
main.QUERIES.sttProviders,
221-
main.STORE_ID,
220+
const configuredProviders = keys.UI.useResultTable(
221+
keys.QUERIES.sttProviders,
222+
keys.STORE_ID,
222223
);
223224

224225
const targetArch = useQuery({

apps/desktop/src/hooks/useCurrentModelModalitySupport.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
listOpenAIModels,
1818
} from "../components/settings/ai/shared/list-openai";
1919
import { listOpenRouterModels } from "../components/settings/ai/shared/list-openrouter";
20+
import * as keys from "../store/tinybase/keys";
2021
import * as main from "../store/tinybase/main";
2122
import { useModelMetadata } from "./useModelMetadata";
2223

@@ -25,10 +26,10 @@ export function useCurrentModelModalitySupport(): InputModality[] | null {
2526
const { current_llm_provider, current_llm_model } = main.UI.useValues(
2627
main.STORE_ID,
2728
);
28-
const providerConfig = main.UI.useRow(
29+
const providerConfig = keys.UI.useRow(
2930
"ai_providers",
3031
current_llm_provider ?? "",
31-
main.STORE_ID,
32+
keys.STORE_ID,
3233
) as AIProviderStorage | undefined;
3334

3435
const providerId = current_llm_provider as ProviderId | null;

apps/desktop/src/hooks/useLLMConnection.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
PROVIDERS,
2121
} from "../components/settings/ai/llm/shared";
2222
import { env } from "../env";
23+
import * as keys from "../store/tinybase/keys";
2324
import * as main from "../store/tinybase/main";
2425
import { tracedFetch } from "../utils/traced-fetch";
2526

@@ -61,10 +62,10 @@ export const useLLMConnection = (): LLMConnectionResult => {
6162
const { current_llm_provider, current_llm_model } = main.UI.useValues(
6263
main.STORE_ID,
6364
);
64-
const providerConfig = main.UI.useRow(
65+
const providerConfig = keys.UI.useRow(
6566
"ai_providers",
6667
current_llm_provider ?? "",
67-
main.STORE_ID,
68+
keys.STORE_ID,
6869
) as AIProviderStorage | undefined;
6970

7071
return useMemo<LLMConnectionResult>(

apps/desktop/src/hooks/useSTTConnection.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useAuth } from "../auth";
88
import { useBillingAccess } from "../billing";
99
import { ProviderId } from "../components/settings/ai/stt/shared";
1010
import { env } from "../env";
11+
import * as keys from "../store/tinybase/keys";
1112
import * as main from "../store/tinybase/main";
1213

1314
export const useSTTConnection = () => {
@@ -20,10 +21,10 @@ export const useSTTConnection = () => {
2021
current_stt_model: string | undefined;
2122
};
2223

23-
const providerConfig = main.UI.useRow(
24+
const providerConfig = keys.UI.useRow(
2425
"ai_providers",
2526
current_stt_provider ?? "",
26-
main.STORE_ID,
27+
keys.STORE_ID,
2728
) as AIProviderStorage | undefined;
2829

2930
const isLocalModel =

apps/desktop/src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { createToolRegistry } from "./contexts/tool-registry/core";
2222
import { env } from "./env";
2323
import { initExtensionGlobals } from "./extension-globals";
2424
import { routeTree } from "./routeTree.gen";
25+
import { StoreComponent as KeysStoreComponent } from "./store/tinybase/keys";
2526
import { type Store, STORE_ID, StoreComponent } from "./store/tinybase/main";
2627
import { createAITaskStore } from "./store/zustand/ai-task";
2728
import { createListenerStore } from "./store/zustand/listener";
@@ -106,6 +107,7 @@ function AppWithTiny() {
106107
<TinyBaseProvider>
107108
<App />
108109
<StoreComponent persist={isMainWindow} />
110+
<KeysStoreComponent persist={isMainWindow} />
109111
{!isIframeContext && <TaskManager />}
110112
</TinyBaseProvider>
111113
</TinyTickProvider>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { createMergeableStore } from "tinybase/with-schemas";
2+
import { describe, expect, test } from "vitest";
3+
4+
import {
5+
fromSimplifiedFormat,
6+
SimplifiedFormat,
7+
toSimplifiedFormat,
8+
} from "./jsonPersister";
9+
10+
describe("jsonPersister transforms", () => {
11+
test("roundtrip: toSimplifiedFormat -> fromSimplifiedFormat", () => {
12+
const store = createMergeableStore();
13+
14+
store.setTable("ai_providers", {
15+
openai: {
16+
type: "llm",
17+
base_url: "https://api.openai.com",
18+
api_key: "sk-123",
19+
},
20+
anthropic: {
21+
type: "llm",
22+
base_url: "https://api.anthropic.com",
23+
api_key: "sk-456",
24+
},
25+
deepgram: {
26+
type: "stt",
27+
base_url: "https://api.deepgram.com",
28+
api_key: "dg-789",
29+
},
30+
});
31+
32+
const simplified = toSimplifiedFormat(store);
33+
const [tables] = fromSimplifiedFormat(simplified);
34+
35+
expect(tables).toEqual({
36+
ai_providers: {
37+
openai: {
38+
type: "llm",
39+
base_url: "https://api.openai.com",
40+
api_key: "sk-123",
41+
},
42+
anthropic: {
43+
type: "llm",
44+
base_url: "https://api.anthropic.com",
45+
api_key: "sk-456",
46+
},
47+
deepgram: {
48+
type: "stt",
49+
base_url: "https://api.deepgram.com",
50+
api_key: "dg-789",
51+
},
52+
},
53+
});
54+
});
55+
56+
test("toSimplifiedFormat groups by type", () => {
57+
const store = createMergeableStore();
58+
59+
store.setTable("ai_providers", {
60+
openai: {
61+
type: "llm",
62+
base_url: "https://api.openai.com",
63+
api_key: "sk-123",
64+
},
65+
deepgram: {
66+
type: "stt",
67+
base_url: "https://api.deepgram.com",
68+
api_key: "dg-789",
69+
},
70+
});
71+
72+
const result = toSimplifiedFormat(store);
73+
74+
expect(result).toEqual({
75+
llm: {
76+
openai: { base_url: "https://api.openai.com", api_key: "sk-123" },
77+
},
78+
stt: {
79+
deepgram: { base_url: "https://api.deepgram.com", api_key: "dg-789" },
80+
},
81+
});
82+
});
83+
84+
test("fromSimplifiedFormat flattens grouped data", () => {
85+
const simplified: SimplifiedFormat = {
86+
llm: {
87+
openai: { base_url: "https://api.openai.com", api_key: "sk-123" },
88+
},
89+
stt: {
90+
deepgram: { base_url: "https://api.deepgram.com", api_key: "dg-789" },
91+
},
92+
};
93+
94+
const [tables] = fromSimplifiedFormat(simplified);
95+
96+
expect(tables).toEqual({
97+
ai_providers: {
98+
openai: {
99+
type: "llm",
100+
base_url: "https://api.openai.com",
101+
api_key: "sk-123",
102+
},
103+
deepgram: {
104+
type: "stt",
105+
base_url: "https://api.deepgram.com",
106+
api_key: "dg-789",
107+
},
108+
},
109+
});
110+
});
111+
112+
test("handles empty data", () => {
113+
const store = createMergeableStore();
114+
115+
const simplified = toSimplifiedFormat(store);
116+
expect(simplified).toEqual({ llm: {}, stt: {} });
117+
118+
const [tables] = fromSimplifiedFormat(simplified);
119+
expect(tables).toEqual({ ai_providers: {} });
120+
});
121+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
BaseDirectory,
3+
exists,
4+
readTextFile,
5+
writeTextFile,
6+
} from "@tauri-apps/plugin-fs";
7+
import { createCustomPersister } from "tinybase/persisters/with-schemas";
8+
import type {
9+
Content,
10+
MergeableStore,
11+
OptionalSchemas,
12+
} from "tinybase/with-schemas";
13+
14+
import { StoreOrMergeableStore } from "./shared";
15+
16+
type ProviderType = "llm" | "stt";
17+
type ProviderData = { base_url: string; api_key: string };
18+
export type SimplifiedFormat = Record<
19+
ProviderType,
20+
Record<string, ProviderData>
21+
>;
22+
23+
export function toSimplifiedFormat<Schemas extends OptionalSchemas>(
24+
store: MergeableStore<Schemas>,
25+
): SimplifiedFormat {
26+
const result: SimplifiedFormat = { llm: {}, stt: {} };
27+
const rows = store.getTable("ai_providers") ?? {};
28+
29+
for (const [rowId, row] of Object.entries(rows)) {
30+
const { type, base_url, api_key } = row as unknown as {
31+
type: ProviderType;
32+
base_url: string;
33+
api_key: string;
34+
};
35+
if (type === "llm" || type === "stt") {
36+
result[type][rowId] = { base_url, api_key };
37+
}
38+
}
39+
40+
return result;
41+
}
42+
43+
export function fromSimplifiedFormat<Schemas extends OptionalSchemas>(
44+
data: SimplifiedFormat,
45+
): Content<Schemas> {
46+
const aiProviders: Record<
47+
string,
48+
{ type: string; base_url: string; api_key: string }
49+
> = {};
50+
51+
for (const providerType of ["llm", "stt"] as const) {
52+
const providers = data[providerType] ?? {};
53+
for (const [providerId, providerData] of Object.entries(providers)) {
54+
aiProviders[providerId] = {
55+
type: providerType,
56+
base_url: providerData.base_url ?? "",
57+
api_key: providerData.api_key ?? "",
58+
};
59+
}
60+
}
61+
62+
return [{ ai_providers: aiProviders }, {}] as unknown as Content<Schemas>;
63+
}
64+
65+
export function createJsonPersister<Schemas extends OptionalSchemas>(
66+
store: MergeableStore<Schemas>,
67+
filename: string,
68+
) {
69+
const path = `hyprnote/${filename}`;
70+
const options = { baseDir: BaseDirectory.Data };
71+
72+
return createCustomPersister(
73+
store,
74+
async (): Promise<Content<Schemas> | undefined> => {
75+
if (!(await exists(path, options))) {
76+
return undefined;
77+
}
78+
const content = await readTextFile(path, options);
79+
return fromSimplifiedFormat<Schemas>(JSON.parse(content));
80+
},
81+
async () => {
82+
const data = toSimplifiedFormat(store);
83+
await writeTextFile(path, JSON.stringify(data, null, 2), options);
84+
},
85+
(listener) => setInterval(listener, 1000),
86+
(handle) => clearInterval(handle),
87+
(error) => console.error(`[JsonPersister] ${filename}:`, error),
88+
StoreOrMergeableStore,
89+
);
90+
}

0 commit comments

Comments
 (0)