sliftutils 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.cursorrules CHANGED
@@ -79,6 +79,12 @@ Coding Styles
79
79
 
80
80
  Do not try catch for no reason. If you can't actually handle the exception, just let it throw.
81
81
 
82
+ For input events, always use event.currentTarget.
83
+
84
+ Use ref={elem => } callbacks. NEVER use .createRef.
85
+
86
+ NEVER render images with a fixed width+height. This will cause them to be stretched or cut off. This is terrible. Only set the width or height.
87
+
82
88
 
83
89
 
84
90
  General Styling
@@ -107,8 +113,28 @@ API Calls
107
113
  APIController(getExtNodeId()).getModels()
108
114
 
109
115
 
116
+ DiskCollection
117
+ If read an object from a collection, and then mutate it, and want to set it in the collection, you have to shallow copy it, so the DiskCollection picks up the change.
118
+ let x = collection.get("x");
119
+ x.y = Math.random();
120
+ // Not asynchronous, as sets are synchronous
121
+ collection.set("x", { ...x });
122
+ The non-async functions should be called in render functions, the async functions should be called in event handlers, etc. Except for .set, which should be called in all both cases.
123
+ get(key: string): T | undefined;
124
+ async getPromise(key: string): Promise<T | undefined>;
125
+ set(key: string, value: T): void;
126
+ remove(key: string): void;
127
+ getKeys(): string[];
128
+ getKeysPromise(): Promise<string[]>;
129
+ getEntries(): [string, T][];
130
+ getValues(): T[];
131
+ async getValuesPromise(): Promise<T[]>;
132
+ getInfo(key: string);
133
+ async reset();
110
134
 
111
135
  css
136
+ Add very little styling, such as colours, rounding, etc, unless asked to add more styling.
137
+
112
138
  css should be set using className={css.cssPropertyName(cssPropertyValue).anotherPropertyName...}
113
139
  Always use the "css" helper for styling. For example:
114
140
  <div className={css.width(100).height(50)}>My width is 100px, my height is 50px</div>
package/index.d.ts CHANGED
@@ -1,6 +1,25 @@
1
1
  // Auto-generated file. Do not edit manually.
2
2
  // Generated by: yarn generate-index-dts
3
3
 
4
+ declare module "sliftutils/misc/apiKeys" {
5
+ import preact from "preact";
6
+ export declare class APIKeysControl extends preact.Component {
7
+ render(): preact.JSX.Element;
8
+ }
9
+ export declare const getAPIKey: {
10
+ (key: string): Promise<string>;
11
+ clear(key: string): void;
12
+ clearAll(): void;
13
+ forceSet(key: string, value: Promise<string>): void;
14
+ getAllKeys(): string[];
15
+ get(key: string): Promise<string> | undefined;
16
+ };
17
+ export declare class ManageAPIKeys extends preact.Component {
18
+ render(): string;
19
+ }
20
+
21
+ }
22
+
4
23
  declare module "sliftutils/misc/environment" {
5
24
  export declare function isInChromeExtension(): string | false;
6
25
  export declare function isInChromeExtensionBackground(): boolean;
@@ -37,6 +56,55 @@ declare module "sliftutils/misc/matchFilter" {
37
56
 
38
57
  }
39
58
 
59
+ declare module "sliftutils/misc/openrouter" {
60
+ export type MessageHistory = {
61
+ role: "system" | "user" | "assistant";
62
+ content: string;
63
+ }[];
64
+ export type MessageHistory2 = {
65
+ role: "system" | "user" | "assistant";
66
+ content: string | {
67
+ type: "image_url";
68
+ image_url: {
69
+ url: string;
70
+ };
71
+ }[];
72
+ }[];
73
+ export declare function getTotalCost(): number;
74
+ type OpenRouterOptions = {
75
+ provider?: {
76
+ sort?: "throughput" | "price" | "latency";
77
+ order?: string[];
78
+ };
79
+ reasoningEffort?: "low" | "medium" | "high";
80
+ };
81
+ /** IMPORTANT! Make sure to tell the AI to return yaml. */
82
+ export declare function yamlOpenRouterCall<T>(config: {
83
+ model: string;
84
+ messages: MessageHistory;
85
+ retries?: number;
86
+ options?: OpenRouterOptions;
87
+ onCost?: (cost: number) => void;
88
+ validate?: (response: T) => void;
89
+ }): Promise<T>;
90
+ export declare function openRouterCall(config: {
91
+ model: string;
92
+ messages: MessageHistory;
93
+ options?: OpenRouterOptions;
94
+ onCost?: (cost: number) => void;
95
+ retries?: number;
96
+ }): Promise<string>;
97
+ export declare function openRouterCallBase(config: {
98
+ model: string;
99
+ messages: MessageHistory2;
100
+ options?: OpenRouterOptions;
101
+ onCost?: (cost: number) => void;
102
+ retries?: number;
103
+ }): Promise<string>;
104
+ export {};
105
+
106
+ }
107
+
40
108
  declare module "sliftutils/misc/random" {
41
109
  export declare function getSeededRandom(seed: number): () => number;
42
110
  export declare function shuffle<T>(array: T[], seed: number): T[];
@@ -384,6 +452,7 @@ declare module "sliftutils/render-utils/modal" {
384
452
  }): {
385
453
  close: () => void;
386
454
  };
455
+ export declare function closeAllModals(): void;
387
456
 
388
457
  }
389
458
 
@@ -0,0 +1,15 @@
1
+ import preact from "preact";
2
+ export declare class APIKeysControl extends preact.Component {
3
+ render(): preact.JSX.Element;
4
+ }
5
+ export declare const getAPIKey: {
6
+ (key: string): Promise<string>;
7
+ clear(key: string): void;
8
+ clearAll(): void;
9
+ forceSet(key: string, value: Promise<string>): void;
10
+ getAllKeys(): string[];
11
+ get(key: string): Promise<string> | undefined;
12
+ };
13
+ export declare class ManageAPIKeys extends preact.Component {
14
+ render(): string;
15
+ }
@@ -0,0 +1,136 @@
1
+ import preact from "preact";
2
+ import { cache, lazy } from "socket-function/src/caching";
3
+ import { css, isNode } from "typesafecss";
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import { observable } from "mobx";
7
+ import { InputLabel } from "../render-utils/InputLabel";
8
+ import { showModal } from "../render-utils/modal";
9
+ import { observer } from "../render-utils/observer";
10
+
11
+ @observer
12
+ class DirectoryPrompter extends preact.Component<{
13
+ valueKey: string;
14
+ onDone: (value: string) => void;
15
+ }> {
16
+ synced = observable({
17
+ value: "",
18
+ });
19
+ render() {
20
+ const { valueKey, onDone } = this.props;
21
+ const { value } = this.synced;
22
+ return (
23
+ <div className={
24
+ css.position("fixed").pos(0, 0).size("100vw", "100vh")
25
+ .zIndex(1)
26
+ .hsla(0, 0, 20, 0.5)
27
+ .center
28
+ .fontSize(40)
29
+ }>
30
+ <InputLabel
31
+ label={valueKey}
32
+ value={value}
33
+ onChangeValue={value => this.synced.value = value}
34
+ />
35
+ <button onClick={() => onDone(value)}>
36
+ Done
37
+ </button>
38
+ </div>
39
+ );
40
+ }
41
+ }
42
+
43
+
44
+ function getStorageKey(key: string) {
45
+ if (isNode()) return os.homedir() + "/" + key;
46
+ return `apiKeys.ts_${key}`;
47
+ }
48
+
49
+ let state = observable({
50
+ keys: [] as string[],
51
+ });
52
+ if (!isNode()) {
53
+ try {
54
+ let newKeys = JSON.parse(localStorage.getItem(getStorageKey("__keys"))!);
55
+ if (Array.isArray(newKeys)) {
56
+ state.keys = newKeys;
57
+ }
58
+ } catch { }
59
+ }
60
+
61
+ @observer
62
+ export class APIKeysControl extends preact.Component {
63
+ render() {
64
+ return (
65
+ <label className={css.hbox(4)}>
66
+ <span>Keys:</span>
67
+ <select>
68
+ {state.keys.map(key =>
69
+ <option>{key}</option>
70
+ )}
71
+ </select>
72
+ <button onClick={() => {
73
+ let key = state.keys[0];
74
+ state.keys = state.keys.filter(k => k !== key);
75
+ localStorage.setItem(getStorageKey("__keys"), JSON.stringify(state.keys));
76
+ localStorage.removeItem(getStorageKey(key));
77
+ }}>
78
+ Delete
79
+ </button>
80
+ </label>
81
+ );
82
+ }
83
+ }
84
+
85
+ let getStorage = lazy((): {
86
+ getItem: (key: string) => string | null;
87
+ setItem: (key: string, value: string) => void;
88
+ } => {
89
+ if (!isNode()) return localStorage;
90
+ return {
91
+ getItem: (key) => {
92
+ if (!fs.existsSync(key)) return null;
93
+ return fs.readFileSync(key, "utf-8");
94
+ },
95
+ setItem: (key, value) => {
96
+ fs.writeFileSync(key, value);
97
+ },
98
+ };
99
+ });
100
+
101
+ export const getAPIKey = cache(async function getAPIKey(key: string): Promise<string> {
102
+ let keyKey = getStorageKey(key);
103
+ let localStorageValue = getStorage().getItem(keyKey);
104
+ if (localStorageValue?.startsWith("{")) {
105
+ try {
106
+ localStorageValue = JSON.parse(localStorageValue).key;
107
+ } catch { }
108
+ }
109
+ if (localStorageValue) {
110
+ return localStorageValue;
111
+ }
112
+ if (isNode()) throw new Error(`Add an api key to ${keyKey}`);
113
+
114
+ let onDone = (value: string) => { };
115
+ let promise = new Promise<string>(resolve => {
116
+ onDone = resolve;
117
+ });
118
+ let obj = showModal({ contents: <DirectoryPrompter valueKey={key} onDone={onDone} /> });
119
+ try {
120
+ let value = await promise;
121
+
122
+ getStorage().setItem(keyKey, value);
123
+ state.keys.push(key);
124
+ getStorage().setItem(getStorageKey("__keys"), JSON.stringify(state.keys));
125
+ return value;
126
+ } finally {
127
+ obj.close();
128
+ }
129
+ });
130
+
131
+ @observer
132
+ export class ManageAPIKeys extends preact.Component {
133
+ render() {
134
+ return "ManageAPIKeys";
135
+ }
136
+ }
@@ -0,0 +1,45 @@
1
+ export type MessageHistory = {
2
+ role: "system" | "user" | "assistant";
3
+ content: string;
4
+ }[];
5
+ export type MessageHistory2 = {
6
+ role: "system" | "user" | "assistant";
7
+ content: string | {
8
+ type: "image_url";
9
+ image_url: {
10
+ url: string;
11
+ };
12
+ }[];
13
+ }[];
14
+ export declare function getTotalCost(): number;
15
+ type OpenRouterOptions = {
16
+ provider?: {
17
+ sort?: "throughput" | "price" | "latency";
18
+ order?: string[];
19
+ };
20
+ reasoningEffort?: "low" | "medium" | "high";
21
+ };
22
+ /** IMPORTANT! Make sure to tell the AI to return yaml. */
23
+ export declare function yamlOpenRouterCall<T>(config: {
24
+ model: string;
25
+ messages: MessageHistory;
26
+ retries?: number;
27
+ options?: OpenRouterOptions;
28
+ onCost?: (cost: number) => void;
29
+ validate?: (response: T) => void;
30
+ }): Promise<T>;
31
+ export declare function openRouterCall(config: {
32
+ model: string;
33
+ messages: MessageHistory;
34
+ options?: OpenRouterOptions;
35
+ onCost?: (cost: number) => void;
36
+ retries?: number;
37
+ }): Promise<string>;
38
+ export declare function openRouterCallBase(config: {
39
+ model: string;
40
+ messages: MessageHistory2;
41
+ options?: OpenRouterOptions;
42
+ onCost?: (cost: number) => void;
43
+ retries?: number;
44
+ }): Promise<string>;
45
+ export {};
@@ -0,0 +1,152 @@
1
+ import { parseYAML } from "sliftutils/misc/yaml";
2
+ import { retryFunctional } from "socket-function/src/batching";
3
+ import { formatNumber } from "socket-function/src/formatting/format";
4
+ import { getAPIKey } from "./apiKeys";
5
+
6
+ export type MessageHistory = {
7
+ role: "system" | "user" | "assistant";
8
+ content: string;
9
+ }[];
10
+ export type MessageHistory2 = {
11
+ role: "system" | "user" | "assistant";
12
+ content: string | {
13
+ type: "image_url";
14
+ image_url: {
15
+ url: string;
16
+ };
17
+ }[];
18
+ }[];
19
+
20
+ let totalCost = 0;
21
+ export function getTotalCost() {
22
+ return totalCost;
23
+ }
24
+ type OpenRouterOptions = {
25
+ provider?: {
26
+ sort?: "throughput" | "price" | "latency",
27
+ // https://openrouter.ai/docs/features/provider-routing#ordering-specific-providers
28
+ order?: string[],
29
+ }
30
+ reasoningEffort?: "low" | "medium" | "high",
31
+ };
32
+ /** IMPORTANT! Make sure to tell the AI to return yaml. */
33
+ export async function yamlOpenRouterCall<T>(config: {
34
+ model: string;
35
+ messages: MessageHistory;
36
+ retries?: number;
37
+ options?: OpenRouterOptions;
38
+ onCost?: (cost: number) => void;
39
+ validate?: (response: T) => void;
40
+ }): Promise<T> {
41
+ let { model, messages, retries = 3, options, onCost } = config;
42
+ try {
43
+ let response = await openRouterCall({ model, messages, options, onCost, retries: 0 });
44
+ let result = parseYAML(response) as T;
45
+ config.validate?.(result);
46
+ return result;
47
+ } catch (error) {
48
+ if (retries > 0) {
49
+ return yamlOpenRouterCall<T>({ model, messages, retries: retries - 1, onCost });
50
+ }
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ let pendingLog: {
56
+ count: number;
57
+ duration: number;
58
+ cost: number;
59
+ } | undefined = undefined;
60
+
61
+ export async function openRouterCall(config: {
62
+ model: string;
63
+ messages: MessageHistory;
64
+ options?: OpenRouterOptions;
65
+ onCost?: (cost: number) => void;
66
+ retries?: number;
67
+ }): Promise<string> {
68
+ return await openRouterCallBase(config);
69
+ }
70
+
71
+ export async function openRouterCallBase(config: {
72
+ model: string;
73
+ messages: MessageHistory2;
74
+ options?: OpenRouterOptions;
75
+ onCost?: (cost: number) => void;
76
+ retries?: number;
77
+ }): Promise<string> {
78
+ let { model, messages, options, onCost } = config;
79
+ let openrouterKey = await getAPIKey("openrouter.json");
80
+ console.log(`Calling ${model} with ${messages.length} messages`);
81
+ let time = Date.now();
82
+ let stillRunning = true;
83
+
84
+ // Spawn monitoring loop
85
+ void (async () => {
86
+ while (stillRunning) {
87
+ await new Promise(resolve => setTimeout(resolve, 5000));
88
+ if (stillRunning) {
89
+ console.log("OpenRouter call still running...");
90
+ }
91
+ }
92
+ })();
93
+
94
+ try {
95
+ return await retryFunctional(async () => {
96
+ let response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
97
+ method: "POST",
98
+ headers: {
99
+ "Authorization": `Bearer ${openrouterKey}`,
100
+ "Content-Type": "application/json",
101
+ },
102
+ body: JSON.stringify({
103
+ model,
104
+ messages,
105
+ provider: {
106
+ sort: "throughput",
107
+ },
108
+ usage: { include: true },
109
+ ...options,
110
+ }),
111
+ });
112
+
113
+ // If it failed, throw
114
+ if (response.status !== 200) {
115
+ let responseText = await response.text();
116
+ throw new Error(`Failed to call OpenRouter: ${response.status} ${response.statusText} ${responseText}`);
117
+ }
118
+ let responseObj = await response.json() as {
119
+ usage: {
120
+ cost: number;
121
+ };
122
+ choices: {
123
+ message: {
124
+ content: string;
125
+ };
126
+ }[];
127
+ };
128
+ let newCost = responseObj.usage.cost;
129
+ totalCost += newCost;
130
+ onCost?.(newCost);
131
+ if (!pendingLog) {
132
+ pendingLog = {
133
+ count: 0,
134
+ duration: 0,
135
+ cost: 0,
136
+ };
137
+ setTimeout(() => {
138
+ let log = pendingLog;
139
+ if (!log) return;
140
+ console.log(`Ran: ${log.count} calls at a total summed cost of ${formatNumber(1 / log.cost)}/USD`);
141
+ pendingLog = undefined;
142
+ }, 10_000);
143
+ }
144
+ pendingLog.count++;
145
+ pendingLog.duration += Date.now() - time;
146
+ pendingLog.cost += newCost;
147
+ return responseObj.choices[0].message.content as string;
148
+ }, { maxRetries: config.retries || 3 })();
149
+ } finally {
150
+ stillRunning = false;
151
+ }
152
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -15,7 +15,13 @@ export class Anchor extends preact.Component<{
15
15
  const { params, button, className, ...remaining } = this.props;
16
16
  let resolvedParams = params.map(getResolvedParam);
17
17
  let searchObj = new URLSearchParams(window.location.search);
18
- let selected = resolvedParams.every(([param, value]) => searchObj.get(param) === value);
18
+ let selected = resolvedParams.every(([param, value], index) => {
19
+ let originalValue = params[index][0];
20
+ if (!(typeof originalValue === "string") && originalValue.value === value) {
21
+ return true;
22
+ }
23
+ return searchObj.get(param) === value;
24
+ });
19
25
  let link = (
20
26
  <a
21
27
  {...remaining}
@@ -4,3 +4,4 @@ export declare function showModal(config: {
4
4
  }): {
5
5
  close: () => void;
6
6
  };
7
+ export declare function closeAllModals(): void;
@@ -1,18 +1,51 @@
1
1
  import preact from "preact";
2
+ import { observable } from "mobx";
3
+ import * as mobx from "mobx";
4
+ import { observer } from "./observer";
5
+ import { lazy } from "socket-function/src/caching";
6
+
7
+ // Global observable map to store all active modals
8
+ const activeModals = new mobx.ObservableMap<string, preact.ComponentChildren>();
9
+
10
+ @observer
11
+ class ModalRoot extends preact.Component {
12
+ render() {
13
+ const modals: Array<[string, preact.ComponentChildren]> = Array.from(activeModals.entries());
14
+ return <>
15
+ {modals.map(([id, contents]) => (
16
+ <div key={id}>
17
+ {contents}
18
+ </div>
19
+ ))}
20
+ </>;
21
+ }
22
+ }
23
+
24
+ const ensureRootMounted = lazy(() => {
25
+ const root = document.createElement("div");
26
+ document.body.appendChild(root);
27
+ preact.render(<ModalRoot />, root);
28
+ });
29
+
30
+ let modalIdCounter = 0;
2
31
 
3
32
  export function showModal(config: {
4
33
  contents: preact.ComponentChildren;
5
34
  }): {
6
35
  close: () => void;
7
36
  } {
8
- let root = document.createElement("div");
9
- document.body.appendChild(root);
10
- preact.render(config.contents, root);
37
+ ensureRootMounted();
38
+
39
+ const id = `modal-${modalIdCounter++}`;
40
+ activeModals.set(id, config.contents);
11
41
 
12
42
  return {
13
43
  close() {
14
- preact.render(undefined, root);
15
- document.body.removeChild(root);
44
+ activeModals.delete(id);
16
45
  }
17
46
  };
47
+ }
48
+
49
+ export function closeAllModals() {
50
+ activeModals.clear();
18
51
  }
package/web/Page.tsx CHANGED
@@ -31,6 +31,12 @@ export class Page extends preact.Component {
31
31
  el.click();
32
32
  }
33
33
  };
34
+ componentDidMount() {
35
+ document.addEventListener("keydown", this.onKeyDown);
36
+ }
37
+ componentWillUnmount() {
38
+ document.removeEventListener("keydown", this.onKeyDown);
39
+ }
34
40
  render() {
35
41
  let pages = [
36
42
  {
@@ -43,7 +49,7 @@ export class Page extends preact.Component {
43
49
 
44
50
  return (
45
51
  <div className={css.size("100vw", "100vh").vbox(0)}>
46
- <div className={css.hbox(12).pad2(20)}>
52
+ <div className={css.hbox(12).pad2(20, 0)}>
47
53
  {pages.map(p => (
48
54
  <Anchor key={p.key} params={[[pageURL, p.key]]}>
49
55
  {p.key}