Treat the appearance setting as the rendered base size so the type ramp scales predictably. Native starts one pixel larger while web and desktop preserve the authored 14px base, with legacy values migrated on load.
182 lines
6.2 KiB
TypeScript
182 lines
6.2 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { Alert, Text, View } from "react-native";
|
|
import { useTranslation } from "react-i18next";
|
|
import { StyleSheet } from "react-native-unistyles";
|
|
import type { AgentProvider } from "@getpaseo/protocol/agent-types";
|
|
import { CombinedModelSelector } from "@/components/combined-model-selector";
|
|
import { ExternalLink } from "@/components/ui/external-link";
|
|
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
|
import { SegmentedControl } from "@/components/ui/segmented-control";
|
|
import { useDaemonConfig } from "@/hooks/use-daemon-config";
|
|
import { useProvidersSnapshot } from "@/hooks/use-providers-snapshot";
|
|
import { buildSelectableProviderSelectorProviders } from "@/provider-selection/provider-selection";
|
|
import { SettingsSection } from "@/screens/settings/settings-section";
|
|
import { settingsStyles } from "@/styles/settings";
|
|
|
|
const METADATA_GENERATION_DOCS_URL = "https://paseo.sh/docs/metadata-generation";
|
|
type SelectionMode = "automatic" | "preferred";
|
|
|
|
export function MetadataGenerationPage({ serverId }: { serverId: string }) {
|
|
const { t } = useTranslation();
|
|
const { config, isLoading: isConfigLoading, patchConfig } = useDaemonConfig(serverId);
|
|
const snapshot = useProvidersSnapshot(serverId);
|
|
const providers = useMemo(
|
|
() => buildSelectableProviderSelectorProviders(snapshot.entries),
|
|
[snapshot.entries],
|
|
);
|
|
const configuredProviders = config?.metadataGeneration.providers;
|
|
const configuredProvider = configuredProviders?.[0] ?? null;
|
|
const savedMode: SelectionMode = configuredProvider ? "preferred" : "automatic";
|
|
const [draftMode, setDraftMode] = useState<SelectionMode | null>(null);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const mode = draftMode ?? savedMode;
|
|
|
|
useEffect(() => {
|
|
setDraftMode(null);
|
|
}, [configuredProvider?.model, configuredProvider?.provider]);
|
|
|
|
const modeOptions = useMemo(
|
|
() => [
|
|
{ value: "automatic" as const, label: t("settings.metadataGeneration.automatic") },
|
|
{ value: "preferred" as const, label: t("settings.metadataGeneration.preferred") },
|
|
],
|
|
[t],
|
|
);
|
|
|
|
const saveProviders = useCallback(
|
|
async (providersPatch: { provider: string; model?: string }[]) => {
|
|
setIsSaving(true);
|
|
try {
|
|
await patchConfig({ metadataGeneration: { providers: providersPatch } });
|
|
} catch (error) {
|
|
setDraftMode(null);
|
|
Alert.alert(
|
|
t("settings.metadataGeneration.saveError"),
|
|
error instanceof Error ? error.message : String(error),
|
|
);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
},
|
|
[patchConfig, t],
|
|
);
|
|
|
|
const handleModeChange = useCallback(
|
|
(next: SelectionMode) => {
|
|
setDraftMode(next);
|
|
if (next === "automatic") {
|
|
void saveProviders([]);
|
|
}
|
|
},
|
|
[saveProviders],
|
|
);
|
|
|
|
const handleModelSelect = useCallback(
|
|
(provider: AgentProvider, model: string) => {
|
|
setDraftMode("preferred");
|
|
void saveProviders([
|
|
{ provider, ...(model ? { model } : {}) },
|
|
...(configuredProviders?.slice(1) ?? []),
|
|
]);
|
|
},
|
|
[configuredProviders, saveProviders],
|
|
);
|
|
|
|
const handleSelectorOpen = useCallback(() => {
|
|
snapshot.refetchIfStale(configuredProvider?.provider);
|
|
}, [configuredProvider?.provider, snapshot]);
|
|
const handleRetryProvider = useCallback(
|
|
(provider: AgentProvider) => snapshot.refresh([provider]),
|
|
[snapshot],
|
|
);
|
|
const docsLink = useMemo(
|
|
() => (
|
|
<ExternalLink
|
|
href={METADATA_GENERATION_DOCS_URL}
|
|
label={t("settings.metadataGeneration.docs")}
|
|
/>
|
|
),
|
|
[t],
|
|
);
|
|
|
|
if (isConfigLoading || !config) {
|
|
return (
|
|
<View style={styles.loading}>
|
|
<LoadingSpinner size="large" color={styles.spinnerColor.color} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SettingsSection
|
|
title={t("settings.metadataGeneration.title")}
|
|
trailing={docsLink}
|
|
testID="metadata-generation-settings"
|
|
>
|
|
<Text style={styles.description}>{t("settings.metadataGeneration.description")}</Text>
|
|
<View style={settingsStyles.card}>
|
|
<View style={settingsStyles.row}>
|
|
<View style={settingsStyles.rowContent}>
|
|
<Text style={settingsStyles.rowTitle}>
|
|
{t("settings.metadataGeneration.selection")}
|
|
</Text>
|
|
<Text style={settingsStyles.rowHint}>
|
|
{mode === "automatic"
|
|
? t("settings.metadataGeneration.automaticHint")
|
|
: t("settings.metadataGeneration.preferredHint")}
|
|
</Text>
|
|
</View>
|
|
<SegmentedControl
|
|
options={modeOptions}
|
|
value={mode}
|
|
onValueChange={handleModeChange}
|
|
size="sm"
|
|
testID="metadata-generation-mode"
|
|
/>
|
|
</View>
|
|
{mode === "preferred" ? (
|
|
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
|
<View style={settingsStyles.rowContent}>
|
|
<Text style={settingsStyles.rowTitle}>{t("settings.metadataGeneration.model")}</Text>
|
|
<Text style={settingsStyles.rowHint}>
|
|
{t("settings.metadataGeneration.fallbackHint")}
|
|
</Text>
|
|
</View>
|
|
<CombinedModelSelector
|
|
providers={providers}
|
|
selectedProvider={configuredProvider?.provider ?? ""}
|
|
selectedModel={configuredProvider?.model ?? ""}
|
|
onSelect={handleModelSelect}
|
|
isLoading={snapshot.isLoading || snapshot.isFetching}
|
|
onOpen={handleSelectorOpen}
|
|
onRetryProvider={handleRetryProvider}
|
|
isRetryingProvider={snapshot.isRefreshing}
|
|
disabled={isSaving}
|
|
serverId={serverId}
|
|
desktopPlacement="bottom-start"
|
|
desktopMinWidth={360}
|
|
/>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
</SettingsSection>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
description: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
lineHeight: theme.fontSize.sm * 1.45,
|
|
marginHorizontal: theme.spacing[1],
|
|
},
|
|
loading: {
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
minHeight: 180,
|
|
},
|
|
spinnerColor: {
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
}));
|