1
0
Fork 0
tsgames/src/games/storywriter/components/settings-modal.tsx

183 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useMemo, useRef } from "preact/hooks";
import { useQuery } from "@common/hooks/useAsyncState";
import { useInputState } from "@common/hooks/useInputState";
import { useUpdate } from "@common/hooks/useUpdate";
import { useAppState } from "../contexts/state";
import LLM from "../utils/llm";
import styles from "../assets/settings-modal.module.css";
interface Props {
onClose: () => void;
}
export const SettingsModal = ({ onClose }: Props) => {
const { connection, model, dispatch } = useAppState();
const [url, setUrl] = useInputState(connection?.url ?? "");
const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? "");
const [selectedModel, setSelectedModel] = useInputState(model ?? "");
const [update, triggerFetch] = useUpdate();
const urlRef = useRef(url);
const apiKeyRef = useRef(apiKey);
urlRef.current = url;
apiKeyRef.current = apiKey;
const connectionToFetch = useMemo<LLM.Connection | null>(() => {
const currentUrl = urlRef.current;
const currentApiKey = apiKeyRef.current;
if (!currentUrl || !currentApiKey) return null;
return { url: currentUrl, apiKey: currentApiKey };
}, [update]);
const fetchModels = useMemo(() => async (conn: LLM.Connection | null) => {
if (!conn) return [];
const r = await LLM.getModels(conn);
return r.data;
}, []);
const modelsData = useQuery(fetchModels, connectionToFetch);
const isLoadingModels = connectionToFetch != null && modelsData == undefined;
const groupedModels = useMemo(() => {
const sorted = (modelsData ?? []).sort((a, b) => {
// Sort by tool support first (true before false)
if (a.support_tools !== b.support_tools) {
return a.support_tools ? -1 : 1;
}
// Then by max context (bigger first, undefined treated as 0)
const aContext = a.max_context ?? 0;
const bContext = b.max_context ?? 0;
if (aContext !== bContext) {
return bContext - aContext;
}
// Then by name (alphabetically)
return a.id.localeCompare(b.id);
});
// Group by context size
const groups = Map.groupBy(sorted, m => m.max_context ?? 0);
// Convert to array sorted by context size (bigger first)
return Array.from(groups.entries())
.sort((a, b) => b[0] - a[0])
.map(([context, models]) => ({ context, models }));
}, [modelsData]);
const handleBlur = () => {
if (url && apiKey) {
triggerFetch();
}
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && url && apiKey) {
triggerFetch();
}
};
const handleConfirm = () => {
dispatch({
type: 'SET_CONNECTION',
connection: connectionToFetch,
});
dispatch({
type: 'SET_MODEL',
model: selectedModel || null,
});
onClose();
};
const connectionToTest = url && apiKey ? { url, apiKey } : null;
return (
<div class={styles.overlay} onClick={onClose}>
<div class={styles.modal} onClick={(e) => e.stopPropagation()}>
<div class={styles.header}>
<h2 class={styles.title}>Settings</h2>
<button class={styles.closeButton} onClick={onClose}>
×
</button>
</div>
<div class={styles.content}>
<div class={styles.form} autocomplete="off">
<div class={styles.formGroup}>
<label class={styles.label}>
API URL
</label>
<input
type="text"
value={url}
onInput={setUrl}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder="http://localhost:1234"
class={styles.input}
autocomplete="off"
name="api-url-random"
/>
</div>
<div class={styles.formGroup}>
<label class={styles.label}>
API Key
</label>
<input
type="password"
value={apiKey}
onInput={setApiKey}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
placeholder="your-api-key"
class={styles.input}
autocomplete="new-password"
name="api-key-random"
/>
</div>
<div class={styles.formGroup}>
<label class={styles.label}>
Model
</label>
{connectionToTest ? (
isLoadingModels ? (
<p>Loading models...</p>
) : groupedModels.length > 0 ? (
<select
value={selectedModel}
onChange={setSelectedModel}
class={styles.select}
>
<option value="">Select a model</option>
{groupedModels.map(({ context, models }) => (
<optgroup key={context} label={`${context} context`}>
{models.map(m => (
<option key={m.id} value={m.id}>
{m.support_tools ? '🔧 ' : ''}{m.id} {m.max_length ? `(len: ${m.max_length})` : ''}
</option>
))}
</optgroup>
))}
</select>
) : (
<p>No models available</p>
)
) : (
<p>Enter connection details to load models</p>
)}
</div>
</div>
</div>
<div class={styles.footer}>
<button onClick={onClose} class={`${styles.button} ${styles.buttonSecondary}`}>
Cancel
</button>
<button onClick={handleConfirm} class={`${styles.button} ${styles.buttonPrimary}`}>
Confirm
</button>
</div>
</div>
</div>
);
};