SettingsScreen CC 23 -> below 10. Form parsing, change detection and the dark-theme check moved to utils/ with unit tests; the dark switch now reads the OS scheme instead of comparing the background colour. Adds characterization tests for the screen.
195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
import { ConnectionBadge } from '@/components/ui/ConnectionBadge';
|
|
import { useLMStudio } from '@/hooks/useLMStudio';
|
|
import { useThemeColor } from '@/hooks/useThemeColor';
|
|
import * as api from '@/services/lmStudioApi';
|
|
import { useAppStore } from '@/store/useAppStore';
|
|
import { buildServerConfig, isConfigChanged } from '@/utils/serverConfig';
|
|
import { Network } from 'lucide-react-native';
|
|
import React, { useState } from 'react';
|
|
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
import { LabeledInput } from './LabeledInput';
|
|
import { SettingsSection } from './SettingsSection';
|
|
|
|
function TestResultBox({ ok }: { ok: boolean }) {
|
|
const colors = useThemeColor();
|
|
|
|
return (
|
|
<View style={[styles.resultBox, { backgroundColor: ok ? colors.successBg : colors.errorBg }]}>
|
|
<Text style={[styles.resultText, { color: ok ? colors.successText : colors.errorText }]}>
|
|
{ok
|
|
? 'Connection successful. API is ready.'
|
|
: 'Connection failed. Verify host and port.'}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export function ServerConnectionCard() {
|
|
const colors = useThemeColor();
|
|
const serverConfig = useAppStore(state => state.serverConfig);
|
|
const setServerConfig = useAppStore(state => state.setServerConfig);
|
|
const isConnected = useAppStore(state => state.isConnected);
|
|
const setIsConnected = useAppStore(state => state.setIsConnected);
|
|
const { fetchModels } = useLMStudio();
|
|
|
|
const [host, setHost] = useState(serverConfig.host);
|
|
const [port, setPort] = useState(serverConfig.port.toString());
|
|
const [apiKey, setApiKey] = useState(serverConfig.apiKey ?? '');
|
|
const [isTesting, setIsTesting] = useState(false);
|
|
const [testResult, setTestResult] = useState<boolean | null>(null);
|
|
|
|
const form = { host, port, apiKey };
|
|
const currentConfig = buildServerConfig(form);
|
|
const configChanged = isConfigChanged(form, serverConfig);
|
|
|
|
const handleSaveConfig = () => {
|
|
setServerConfig(currentConfig);
|
|
setTestResult(null);
|
|
};
|
|
|
|
const handleTestConnection = async () => {
|
|
handleSaveConfig();
|
|
setIsTesting(true);
|
|
setTestResult(null);
|
|
|
|
const ok = await api.testConnection(currentConfig);
|
|
setTestResult(ok);
|
|
setIsConnected(ok);
|
|
if (ok) {
|
|
await fetchModels();
|
|
}
|
|
setIsTesting(false);
|
|
};
|
|
|
|
return (
|
|
<SettingsSection title="SERVER CONNECTION">
|
|
<View style={styles.cardHeader}>
|
|
<View style={styles.cardHeaderTitle}>
|
|
<Network size={20} color={colors.text} style={styles.headerIcon} />
|
|
<Text style={[styles.cardTitle, { color: colors.text }]}>LM Studio API</Text>
|
|
</View>
|
|
<ConnectionBadge isConnected={isConnected} />
|
|
</View>
|
|
|
|
<View style={[styles.divider, { backgroundColor: colors.border }]} />
|
|
|
|
<LabeledInput
|
|
label="Host IP / Address"
|
|
value={host}
|
|
onChangeText={setHost}
|
|
placeholder="localhost"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
/>
|
|
<LabeledInput
|
|
label="Port Number"
|
|
value={port}
|
|
onChangeText={setPort}
|
|
placeholder="1234"
|
|
keyboardType="numeric"
|
|
/>
|
|
<LabeledInput
|
|
label="API Key (optional)"
|
|
value={apiKey}
|
|
onChangeText={setApiKey}
|
|
placeholder="Leave empty for local"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
secureTextEntry
|
|
/>
|
|
|
|
<View style={styles.buttonRow}>
|
|
{configChanged && (
|
|
<TouchableOpacity
|
|
style={[styles.saveButton, { borderColor: colors.border, backgroundColor: colors.cardBackground }]}
|
|
onPress={handleSaveConfig}
|
|
>
|
|
<Text style={[styles.saveButtonText, { color: colors.text }]}>Save Changes</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.testButton,
|
|
{ backgroundColor: colors.tint },
|
|
isTesting && styles.testButtonDisabled
|
|
]}
|
|
onPress={handleTestConnection}
|
|
disabled={isTesting}
|
|
>
|
|
{isTesting ? (
|
|
<ActivityIndicator size="small" color={colors.backgroundColor} />
|
|
) : (
|
|
<Text style={[styles.testButtonText, { color: colors.backgroundColor }]}>Test Connection</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{testResult !== null && <TestResultBox ok={testResult} />}
|
|
</SettingsSection>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 16,
|
|
},
|
|
cardHeaderTitle: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
headerIcon: {
|
|
marginRight: 8,
|
|
},
|
|
cardTitle: {
|
|
fontSize: 18,
|
|
fontWeight: '700',
|
|
},
|
|
divider: {
|
|
height: 1,
|
|
width: '100%',
|
|
marginBottom: 16,
|
|
},
|
|
buttonRow: {
|
|
flexDirection: 'row',
|
|
gap: 12,
|
|
marginTop: 8,
|
|
},
|
|
saveButton: {
|
|
flex: 1,
|
|
paddingVertical: 14,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
},
|
|
saveButtonText: {
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
},
|
|
testButton: {
|
|
flex: 2,
|
|
paddingVertical: 14,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
},
|
|
testButtonDisabled: {
|
|
opacity: 0.7,
|
|
},
|
|
testButtonText: {
|
|
fontSize: 15,
|
|
fontWeight: '600',
|
|
},
|
|
resultBox: {
|
|
marginTop: 16,
|
|
borderRadius: 12,
|
|
padding: 14,
|
|
},
|
|
resultText: {
|
|
fontSize: 14,
|
|
textAlign: 'center',
|
|
fontWeight: '500',
|
|
},
|
|
});
|