Files
paseo/packages/app/src/screens/settings/metadata-generation-page.tsx
T
Mohamed Boudra 5ec26f0c09 feat(app): align interface typography to base size
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.
2026-08-17 22:55:46 +02:00

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,
},
}));