Integrate backend into storywriter
This commit is contained in:
parent
64268b42c9
commit
d2d3ad42d8
|
|
@ -2,6 +2,16 @@ import { db } from "./db.ts";
|
||||||
|
|
||||||
export const TOKEN_TTL_DAYS = parseInt(process.env.TOKEN_TTL_DAYS ?? "30");
|
export const TOKEN_TTL_DAYS = parseInt(process.env.TOKEN_TTL_DAYS ?? "30");
|
||||||
|
|
||||||
|
export const issueToken = async (userId: number): Promise<string> => {
|
||||||
|
const token = crypto.randomUUID();
|
||||||
|
const expiresAt = new Date(Date.now() + TOKEN_TTL_DAYS * 86_400_000)
|
||||||
|
.toISOString()
|
||||||
|
.replace("T", " ")
|
||||||
|
.slice(0, 19);
|
||||||
|
await db`INSERT INTO auth_tokens (user_id, token, expires_at) VALUES (${userId}, ${token}, ${expiresAt})`;
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
export const validateToken = async (token: string): Promise<number | null> => {
|
export const validateToken = async (token: string): Promise<number | null> => {
|
||||||
const [row] = await db`
|
const [row] = await db`
|
||||||
SELECT user_id FROM auth_tokens
|
SELECT user_id FROM auth_tokens
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { db } from "./db.ts";
|
||||||
|
import { issueToken } from "./auth.ts";
|
||||||
|
|
||||||
|
const [username, password] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
console.error("Usage: bun run login <username> <password>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [user] = await db`SELECT id, password_hash FROM users WHERE username = ${username}`;
|
||||||
|
|
||||||
|
if (!user || !await Bun.password.verify(password, user.password_hash as string)) {
|
||||||
|
console.error("Invalid credentials");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(await issueToken(user.id as number));
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { db } from "../db.ts";
|
import { db } from "../db.ts";
|
||||||
import { TOKEN_TTL_DAYS } from "../auth.ts";
|
import { issueToken } from "../auth.ts";
|
||||||
|
|
||||||
export const handleLogin = async (req: Request): Promise<Response> => {
|
export const handleLogin = async (req: Request): Promise<Response> => {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
|
|
@ -30,16 +30,6 @@ export const handleLogin = async (req: Request): Promise<Response> => {
|
||||||
return Response.json({ error: "Invalid credentials" }, { status: 401 });
|
return Response.json({ error: "Invalid credentials" }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = crypto.randomUUID();
|
const token = await issueToken(user.id as number);
|
||||||
const expiresAt = new Date(Date.now() + TOKEN_TTL_DAYS * 86_400_000)
|
|
||||||
.toISOString()
|
|
||||||
.replace("T", " ")
|
|
||||||
.slice(0, 19);
|
|
||||||
|
|
||||||
await db`
|
|
||||||
INSERT INTO auth_tokens (user_id, token, expires_at)
|
|
||||||
VALUES (${user.id}, ${token}, ${expiresAt})
|
|
||||||
`;
|
|
||||||
|
|
||||||
return Response.json({ token });
|
return Response.json({ token });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
--border: #3e3d32;
|
--border: #3e3d32;
|
||||||
--accent: #f92672;
|
--accent: #f92672;
|
||||||
--accent-alt: #a6e22e;
|
--accent-alt: #a6e22e;
|
||||||
|
--danger: #c4265e;
|
||||||
|
--danger-alt: #f92672;
|
||||||
--text: #f8f8f2;
|
--text: #f8f8f2;
|
||||||
--text-muted: #75715e;
|
--text-muted: #75715e;
|
||||||
--text-dim: #cfcfc2;
|
--text-dim: #cfcfc2;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
"backend": "bun backend/src/index.ts",
|
"backend": "bun backend/src/index.ts",
|
||||||
"backend:dev": "bun --hot backend/src/index.ts",
|
"backend:dev": "bun --hot backend/src/index.ts",
|
||||||
"register": "bun backend/src/register.ts",
|
"register": "bun backend/src/register.ts",
|
||||||
|
"login": "bun backend/src/login.ts",
|
||||||
"docker:build": "docker build -t git.pabloader.ru/pabloid/tsgames:latest backend/",
|
"docker:build": "docker build -t git.pabloader.ru/pabloid/tsgames:latest backend/",
|
||||||
"docker:push": "docker push git.pabloader.ru/pabloid/tsgames:latest",
|
"docker:push": "docker push git.pabloader.ru/pabloid/tsgames:latest",
|
||||||
"docker:update": "docker service update --force --image git.pabloader.ru/pabloid/tsgames:latest tsgames_backend",
|
"docker:update": "docker service update --force --image git.pabloader.ru/pabloid/tsgames:latest tsgames_backend",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
.error {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ button {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
transition: all var(--transition);
|
transition: all var(--transition);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonPrimary {
|
.buttonPrimary {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { login } from "@common/storage";
|
||||||
|
import { useInputState } from "@common/hooks/useInputState";
|
||||||
|
import ui from "../assets/ui.module.css";
|
||||||
|
import styles from "../assets/login.module.css";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onLogin?: () => void;
|
||||||
|
class?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoginForm = ({ onLogin, ['class']: cls, className }: IProps) => {
|
||||||
|
const [username, handleUsername] = useInputState();
|
||||||
|
const [password, handlePassword] = useInputState();
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await login(username, password);
|
||||||
|
onLogin?.();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Login failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form class={clsx(ui.form, cls, className)} onSubmit={handleSubmit}>
|
||||||
|
<div class={ui.formGroup}>
|
||||||
|
<label class={ui.label}>Username</label>
|
||||||
|
<input
|
||||||
|
class={ui.input}
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onInput={handleUsername}
|
||||||
|
autocomplete="username"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class={ui.formGroup}>
|
||||||
|
<label class={ui.label}>Password</label>
|
||||||
|
<input
|
||||||
|
class={ui.input}
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onInput={handlePassword}
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <span class={styles.error}>{error}</span>}
|
||||||
|
<button class={ui.buttonPrimary} type="submit" disabled={loading}>
|
||||||
|
{loading ? 'Logging in…' : 'Log in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { useEffect, useReducer, useState, type Dispatch, type Reducer, type StateUpdater } from "preact/hooks";
|
import { useEffect, useReducer, useState, type Dispatch, type Reducer, type StateUpdater } from "preact/hooks";
|
||||||
import { loadObject, saveObject } from "@common/storage";
|
import { loadObject, saveObject } from "@common/storage";
|
||||||
|
import { storedStateKey, storedReducerKey } from "@common/hooks/useStored";
|
||||||
|
|
||||||
declare const GAME: string;
|
export const remoteStateKey = (key: string) => `useRemoteState.${GAME}.${key}`;
|
||||||
|
export const remoteReducerKey = (key: string) => `useRemoteReducer.${GAME}.${key}`;
|
||||||
|
|
||||||
export const useRemoteState = <T>(key: string, initialValue: T) => {
|
export const useRemoteState = <T>(key: string, initialValue: T) => {
|
||||||
const storedKey = `useRemoteState.${GAME}.${key}`;
|
const storedKey = remoteStateKey(key);
|
||||||
|
|
||||||
const [value, setValue] = useState<T>(initialValue);
|
const [value, setValue] = useState<T>(initialValue);
|
||||||
|
|
||||||
|
|
@ -34,10 +36,35 @@ const wrapReducer = <T, A>(reducer: Reducer<T, A>): Reducer<T, A | HydrateAction
|
||||||
return reducer(state, action);
|
return reducer(state, action);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRemoteReducer = <T, A>(key: string, reducer: Reducer<T, A>, initialValue: T) => {
|
/** Copies a useStoredState value into the useRemoteState slot and syncs it remotely. */
|
||||||
const storedKey = `useRemoteReducer.${GAME}.${key}`;
|
export const importStoredState = <T>(key: string): boolean => {
|
||||||
|
const raw = localStorage.getItem(storedStateKey(key));
|
||||||
|
if (!raw) return false;
|
||||||
|
saveObject(remoteStateKey(key), JSON.parse(raw) as T);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(wrapReducer(reducer), initialValue);
|
/** Copies a useStoredReducer value into the useRemoteReducer slot and syncs it remotely. */
|
||||||
|
export const importStoredReducer = <T>(key: string): boolean => {
|
||||||
|
const raw = localStorage.getItem(storedReducerKey(key));
|
||||||
|
if (!raw) return false;
|
||||||
|
saveObject(remoteReducerKey(key), JSON.parse(raw) as T);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRemoteReducer = <T, A>(key: string, reducer: Reducer<T, A>, initialValue: T) => {
|
||||||
|
const storedKey = remoteReducerKey(key);
|
||||||
|
|
||||||
|
const getInitialValue = (): T => {
|
||||||
|
try {
|
||||||
|
const storedValue = localStorage.getItem(storedKey);
|
||||||
|
return storedValue ? JSON.parse(storedValue) : initialValue;
|
||||||
|
} catch {
|
||||||
|
return initialValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(wrapReducer(reducer), undefined, getInitialValue);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadObject<T>(storedKey, initialValue).then((loaded) => {
|
loadObject<T>(storedKey, initialValue).then((loaded) => {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { useEffect, useReducer, useState, type Dispatch, type Reducer, type StateUpdater } from "preact/hooks";
|
import { useEffect, useReducer, useState, type Dispatch, type Reducer, type StateUpdater } from "preact/hooks";
|
||||||
|
|
||||||
declare const GAME: string;
|
export const storedStateKey = (key: string) => `useStoredState.${GAME}.${key}`;
|
||||||
|
export const storedReducerKey = (key: string) => `useStoredReducer.${GAME}.${key}`;
|
||||||
|
|
||||||
export const useStoredState = <T>(key: string, initialValue: T) => {
|
export const useStoredState = <T>(key: string, initialValue: T) => {
|
||||||
const storedKey = `useStoredState.${GAME}.${key}`;
|
const storedKey = storedStateKey(key);
|
||||||
|
|
||||||
const [value, setValue] = useState<T>(() => {
|
const [value, setValue] = useState<T>(() => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -22,7 +23,7 @@ export const useStoredState = <T>(key: string, initialValue: T) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useStoredReducer = <T, A>(key: string, reducer: Reducer<T, A>, initialValue: T) => {
|
export const useStoredReducer = <T, A>(key: string, reducer: Reducer<T, A>, initialValue: T) => {
|
||||||
const storedKey = `useStoredReducer.${GAME}.${key}`;
|
const storedKey = storedReducerKey(key);
|
||||||
|
|
||||||
const getInitialValue = (): T => {
|
const getInitialValue = (): T => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,93 @@
|
||||||
const saveThrottleDelay = 2000;
|
const SAVE_THROTTLE_MS = 2000;
|
||||||
|
const TOKEN_KEY = '__storage_token__';
|
||||||
|
const BASE_URL = '/storage/api';
|
||||||
|
|
||||||
const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
|
const pendingSaves = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
// ─── Auth ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let token: string | null = localStorage.getItem(TOKEN_KEY);
|
||||||
|
|
||||||
|
export const isLoggedIn = () => token !== null;
|
||||||
|
|
||||||
|
export const login = async (username: string, password: string): Promise<void> => {
|
||||||
|
const res = await fetch(`${BASE_URL}/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (!res.ok) throw new Error(json.error ?? 'Login failed');
|
||||||
|
token = json.token;
|
||||||
|
localStorage.setItem(TOKEN_KEY, json.token);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
token = null;
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Storage ─────────────────────────────────────────────────
|
||||||
|
|
||||||
export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> => {
|
export const loadObject = async <T>(key: string, defaultObject: T): Promise<T> => {
|
||||||
let localObject: Partial<T> = {};
|
let localObject: Partial<T> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const json = localStorage.getItem(key);
|
const json = localStorage.getItem(key);
|
||||||
if (json) {
|
if (json) localObject = JSON.parse(json);
|
||||||
localObject = JSON.parse(json);
|
|
||||||
}
|
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
let remoteObject: Partial<T> = {};
|
let remoteObject: Partial<T> = {};
|
||||||
try {
|
if (token) {
|
||||||
// TODO loading from a remote source
|
try {
|
||||||
const compressedData = new Blob([]);
|
const res = await fetch(`${BASE_URL}/storage/${key}`, {
|
||||||
const decompressedData = await decompressBlob(compressedData);
|
headers: { 'Authorization': `Bearer ${token}` },
|
||||||
remoteObject = JSON.parse(await decompressedData.text());
|
});
|
||||||
} catch { }
|
if (res.ok) remoteObject = await res.json();
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
return { ...defaultObject, ...localObject, ...remoteObject };
|
return { ...defaultObject, ...localObject, ...remoteObject };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveObject = <T>(key: string, obj: T) => {
|
export const saveObject = <T>(key: string, obj: T) => {
|
||||||
const existing = pendingSaves.get(key);
|
|
||||||
if (existing) clearTimeout(existing);
|
|
||||||
pendingSaves.set(key, setTimeout(() => {
|
|
||||||
pendingSaves.delete(key);
|
|
||||||
doSaveObject(key, obj);
|
|
||||||
}, saveThrottleDelay));
|
|
||||||
};
|
|
||||||
|
|
||||||
const doSaveObject = async <T>(key: string, obj: T) => {
|
|
||||||
const saveData = JSON.stringify(obj);
|
const saveData = JSON.stringify(obj);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, saveData);
|
localStorage.setItem(key, saveData);
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
try {
|
if (token) {
|
||||||
const compressedData = await compressBlob(saveData);
|
const existing = pendingSaves.get(key);
|
||||||
// TODO saving to remote storage
|
if (existing) clearTimeout(existing);
|
||||||
void compressedData;
|
pendingSaves.set(key, setTimeout(() => {
|
||||||
} catch {
|
pendingSaves.delete(key);
|
||||||
|
saveRemote(key, saveData);
|
||||||
|
}, SAVE_THROTTLE_MS));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const saveRemote = async (key: string, saveData: string) => {
|
||||||
|
try {
|
||||||
|
await fetch(`${BASE_URL}/storage/${key}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: saveData,
|
||||||
|
});
|
||||||
|
} catch { }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Compression utils ───────────────────────────────────────
|
||||||
|
|
||||||
export const compressBlob = async (blob: Blob | string): Promise<Blob> => {
|
export const compressBlob = async (blob: Blob | string): Promise<Blob> => {
|
||||||
if (typeof blob === 'string') {
|
if (typeof blob === 'string') blob = new Blob([blob]);
|
||||||
blob = new Blob([blob]);
|
|
||||||
}
|
|
||||||
const cs = new CompressionStream("gzip");
|
const cs = new CompressionStream("gzip");
|
||||||
const compressedStream = blob.stream().pipeThrough(cs);
|
return await new Response(blob.stream().pipeThrough(cs)).blob();
|
||||||
return await new Response(compressedStream).blob();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const decompressBlob = async (blob: Blob): Promise<Blob> => {
|
export const decompressBlob = async (blob: Blob): Promise<Blob> => {
|
||||||
const ds = new DecompressionStream("gzip");
|
const ds = new DecompressionStream("gzip");
|
||||||
const decompressedStream = blob.stream().pipeThrough(ds);
|
return await new Response(blob.stream().pipeThrough(ds)).blob();
|
||||||
return await new Response(decompressedStream).blob();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@ import { render } from "preact";
|
||||||
import styles from "./assets/game.module.css";
|
import styles from "./assets/game.module.css";
|
||||||
import type { Game } from "../../../build/isGame";
|
import type { Game } from "../../../build/isGame";
|
||||||
|
|
||||||
declare const GAMES: Game[];
|
|
||||||
|
|
||||||
function GameList({ games }: { games: Game[] }) {
|
function GameList({ games }: { games: Game[] }) {
|
||||||
return (
|
return (
|
||||||
<div class={styles.games}>
|
<div class={styles.games}>
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,6 @@
|
||||||
min-height: unset;
|
min-height: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
|
||||||
composes: button from '@common/assets/ui.module.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonSecondary {
|
.buttonSecondary {
|
||||||
composes: buttonSecondary from '@common/assets/ui.module.css';
|
composes: buttonSecondary from '@common/assets/ui.module.css';
|
||||||
}
|
}
|
||||||
|
|
@ -84,6 +80,10 @@
|
||||||
color: var(--accent-text);
|
color: var(--accent-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttonDanger {
|
||||||
|
composes: buttonDanger from '@common/assets/ui.module.css';
|
||||||
|
}
|
||||||
|
|
||||||
.inputRow {
|
.inputRow {
|
||||||
composes: inputRow from '@common/assets/ui.module.css';
|
composes: inputRow from '@common/assets/ui.module.css';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ import { SamplingSettings } from "./settings/sampling";
|
||||||
import { SystemInstructionSettings } from "./settings/system-instruction";
|
import { SystemInstructionSettings } from "./settings/system-instruction";
|
||||||
import { ImageSettings } from "./settings/image";
|
import { ImageSettings } from "./settings/image";
|
||||||
import { UserSettings } from "./settings/user";
|
import { UserSettings } from "./settings/user";
|
||||||
|
import { LoginSettings } from "./settings/login";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = "banned-tokens" | "system-instruction" | "chat-system-instruction" | "continue-prompt" | "connection" | "user" | "sampling" | "image";
|
type Tab = "banned-tokens" | "system-instruction" | "chat-system-instruction" | "continue-prompt" | "connection" | "user" | "sampling" | "image" | "login";
|
||||||
|
|
||||||
const TABS: { id: Tab; label: string }[] = [
|
const TABS: { id: Tab; label: string }[] = [
|
||||||
{ id: "connection", label: "Connection" },
|
{ id: "connection", label: "Connection" },
|
||||||
|
|
@ -27,6 +28,7 @@ const TABS: { id: Tab; label: string }[] = [
|
||||||
{ id: "system-instruction", label: "System Instruction" },
|
{ id: "system-instruction", label: "System Instruction" },
|
||||||
{ id: "continue-prompt", label: "Continue Prompt" },
|
{ id: "continue-prompt", label: "Continue Prompt" },
|
||||||
{ id: "chat-system-instruction", label: "Chat System Instruction" },
|
{ id: "chat-system-instruction", label: "Chat System Instruction" },
|
||||||
|
{ id: "login", label: "Login" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SettingsModal = ({ open, onClose }: Props) => {
|
export const SettingsModal = ({ open, onClose }: Props) => {
|
||||||
|
|
@ -60,6 +62,7 @@ export const SettingsModal = ({ open, onClose }: Props) => {
|
||||||
{activeTab === "connection" && <ConnectionSettings />}
|
{activeTab === "connection" && <ConnectionSettings />}
|
||||||
{activeTab === "sampling" && <SamplingSettings />}
|
{activeTab === "sampling" && <SamplingSettings />}
|
||||||
{activeTab === "image" && <ImageSettings />}
|
{activeTab === "image" && <ImageSettings />}
|
||||||
|
{activeTab === "login" && <LoginSettings />}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useInputState } from "@common/hooks/useInputState";
|
||||||
import { useUpdate } from "@common/hooks/useUpdate";
|
import { useUpdate } from "@common/hooks/useUpdate";
|
||||||
import { fuzzyMatch } from "@common/utils";
|
import { fuzzyMatch } from "@common/utils";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useMemo, useRef } from "preact/hooks";
|
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||||
import styles from "../../assets/settings-modal.module.css";
|
import styles from "../../assets/settings-modal.module.css";
|
||||||
import { useAppState } from "../../contexts/state";
|
import { useAppState } from "../../contexts/state";
|
||||||
import LLM from "../../utils/llm";
|
import LLM from "../../utils/llm";
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export const ImageSettings = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class={styles.button} onClick={handleReset}>
|
<button class={styles.buttonSecondary} onClick={handleReset}>
|
||||||
Reset to defaults
|
Reset to defaults
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { LoginForm } from "@common/components/LoginForm";
|
||||||
|
import { isLoggedIn, logout } from "@common/storage";
|
||||||
|
import { importStoredReducer } from "@common/hooks/useRemote";
|
||||||
|
import { STATE_KEY } from "../../contexts/state";
|
||||||
|
import styles from "../../assets/settings-modal.module.css";
|
||||||
|
|
||||||
|
export const LoginSettings = () => {
|
||||||
|
const [loggedIn, setLoggedIn] = useState(isLoggedIn);
|
||||||
|
const [imported, setImported] = useState(false);
|
||||||
|
|
||||||
|
const handleLogin = () => setLoggedIn(true);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
setLoggedIn(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = () => {
|
||||||
|
if (importStoredReducer(STATE_KEY)) setImported(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loggedIn) {
|
||||||
|
return (
|
||||||
|
<div class={styles.form}>
|
||||||
|
<span class={styles.label}>Logged in. Save data is synced to the server.</span>
|
||||||
|
<button class={styles.buttonSecondary} onClick={handleImport} disabled={imported}>
|
||||||
|
{imported ? 'Imported' : 'Import settings from local storage'}
|
||||||
|
</button>
|
||||||
|
<button class={styles.buttonDanger} onClick={handleLogout}>Log out</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <LoginForm onLogin={handleLogin} class={styles.form} />;
|
||||||
|
};
|
||||||
|
|
@ -14,12 +14,12 @@ interface ParamConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PARAMS: ParamConfig[] = [
|
const PARAMS: ParamConfig[] = [
|
||||||
{ key: "temperature", label: "Temperature", min: 0, max: 4, step: 0.01 },
|
{ key: "temperature", label: "Temperature", min: 0, max: 4, step: 0.01 },
|
||||||
{ key: "top_p", label: "Top P", min: 0, max: 1, step: 0.01 },
|
{ key: "top_p", label: "Top P", min: 0, max: 1, step: 0.01 },
|
||||||
{ key: "top_k", label: "Top K", min: 0, max: 200, step: 1 },
|
{ key: "top_k", label: "Top K", min: 0, max: 200, step: 1 },
|
||||||
{ key: "min_p", label: "Min P", min: 0, max: 1, step: 0.01 },
|
{ key: "min_p", label: "Min P", min: 0, max: 1, step: 0.01 },
|
||||||
{ key: "repetition_penalty", label: "Repetition Penalty", min: 1, max: 3, step: 0.01 },
|
{ key: "repetition_penalty", label: "Repetition Penalty", min: 1, max: 3, step: 0.01 },
|
||||||
{ key: "frequency_penalty", label: "Frequency Penalty", min: -2, max: 2, step: 0.01 },
|
{ key: "frequency_penalty", label: "Frequency Penalty", min: -2, max: 2, step: 0.01 },
|
||||||
];
|
];
|
||||||
|
|
||||||
function ParamRow({ config, initialValue, onChange }: {
|
function ParamRow({ config, initialValue, onChange }: {
|
||||||
|
|
@ -83,7 +83,7 @@ export const SamplingSettings = () => {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div>
|
<div>
|
||||||
<button class={styles.button} onClick={handleReset}>
|
<button class={styles.buttonSecondary} onClick={handleReset}>
|
||||||
Reset to defaults
|
Reset to defaults
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useStoredReducer } from "@common/hooks/useStored";
|
import { useRemoteReducer } from "@common/hooks/useRemote";
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext, useMemo } from "preact/hooks";
|
import { useContext, useMemo } from "preact/hooks";
|
||||||
import Chapters from "../utils/chapters";
|
import Chapters from "../utils/chapters";
|
||||||
|
|
@ -695,8 +695,10 @@ export const useAppState = () => useContext(StateContext);
|
||||||
|
|
||||||
// ─── Provider ────────────────────────────────────────────────────────────────
|
// ─── Provider ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const STATE_KEY = 'state';
|
||||||
|
|
||||||
export const StateContextProvider = ({ children }: { children?: any }) => {
|
export const StateContextProvider = ({ children }: { children?: any }) => {
|
||||||
const [state, dispatch] = useStoredReducer('state', reducer, DEFAULT_STATE);
|
const [state, dispatch] = useRemoteReducer(STATE_KEY, reducer, DEFAULT_STATE);
|
||||||
|
|
||||||
const value = useMemo<AppState>(() => {
|
const value = useMemo<AppState>(() => {
|
||||||
const currentWorld = state.worlds.find(w => w.id === state.currentWorldId) ?? null;
|
const currentWorld = state.worlds.find(w => w.id === state.currentWorldId) ?? null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
declare const GAME: string;
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const { default: runGame }: { default: RunGame } = await import(`./games/${GAME}`);
|
const { default: runGame }: { default: RunGame } = await import(`./games/${GAME}`);
|
||||||
await runGame();
|
await runGame();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ type Rect = [number, number, number, number];
|
||||||
|
|
||||||
type RunGame = () => Promise<void>;
|
type RunGame = () => Promise<void>;
|
||||||
|
|
||||||
|
declare const GAME: string;
|
||||||
|
declare const GAMES: Game[];
|
||||||
|
|
||||||
declare namespace WebAssembly {
|
declare namespace WebAssembly {
|
||||||
type tc = 'i32' | 'i64' | 'f32' | 'f64';
|
type tc = 'i32' | 'i64' | 'f32' | 'f64';
|
||||||
export class Function {
|
export class Function {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue