feat: add settings management with username and tools context for agents

This commit is contained in:
Jonathan Atta
2026-03-04 11:32:39 +01:00
parent 21b669f96c
commit d77bdcf80e
8 changed files with 233 additions and 10 deletions

View File

@@ -17,6 +17,7 @@ import { ThemeProvider, useTheme } from './src/theme/ThemeProvider';
import { loadAgents } from './src/store/agentsSlice'; import { loadAgents } from './src/store/agentsSlice';
import { rehydrateChat } from './src/store/chatSlice'; import { rehydrateChat } from './src/store/chatSlice';
import { loadChatState } from './src/store/persistence'; import { loadChatState } from './src/store/persistence';
import { loadSettings } from './src/store/settingsSlice';
function InnerApp() { function InnerApp() {
const { scheme, colors } = useTheme(); const { scheme, colors } = useTheme();
@@ -30,6 +31,7 @@ function InnerApp() {
const [persisted] = await Promise.all([ const [persisted] = await Promise.all([
loadChatState(), loadChatState(),
dispatch(loadAgents()), dispatch(loadAgents()),
dispatch(loadSettings()),
]); ]);
if (persisted) { if (persisted) {
dispatch(rehydrateChat(persisted)); dispatch(rehydrateChat(persisted));

View File

@@ -40,6 +40,8 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
const [avatarUri, setAvatarUri] = useState<string | null>( const [avatarUri, setAvatarUri] = useState<string | null>(
agent?.avatarUri ?? null, agent?.avatarUri ?? null,
); );
const [toolNow, setToolNow] = useState(agent?.tools?.now ?? false);
const [toolUsername, setToolUsername] = useState(agent?.tools?.username ?? false);
const [pickingImage, setPickingImage] = useState(false); const [pickingImage, setPickingImage] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -49,6 +51,8 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
setName(agent?.name ?? ''); setName(agent?.name ?? '');
setPrompt(agent?.systemPrompt ?? 'Tu es un assistant utile et concis.'); setPrompt(agent?.systemPrompt ?? 'Tu es un assistant utile et concis.');
setAvatarUri(agent?.avatarUri ?? null); setAvatarUri(agent?.avatarUri ?? null);
setToolNow(agent?.tools?.now ?? false);
setToolUsername(agent?.tools?.username ?? false);
} }
}, [visible, agent]); }, [visible, agent]);
@@ -91,6 +95,7 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
name: name.trim(), name: name.trim(),
systemPrompt: prompt.trim(), systemPrompt: prompt.trim(),
avatarUri: avatarUri ?? null, avatarUri: avatarUri ?? null,
tools: { now: toolNow, username: toolUsername },
}; };
await dispatch(saveAgent(updated)); await dispatch(saveAgent(updated));
onClose(); onClose();
@@ -176,6 +181,32 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
numberOfLines={6} numberOfLines={6}
textAlignVertical="top" textAlignVertical="top"
/> />
{/* Tools */}
<Text style={styles.label}>Tools context</Text>
<View style={styles.toolsRow}>
<TouchableOpacity
style={[styles.toolChip, toolNow && styles.toolChipActive]}
onPress={() => setToolNow(v => !v)}
activeOpacity={0.75}
>
<Text style={[styles.toolChipText, toolNow && styles.toolChipTextActive]}>
🕒 now
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.toolChip, toolUsername && styles.toolChipActive]}
onPress={() => setToolUsername(v => !v)}
activeOpacity={0.75}
>
<Text style={[styles.toolChipText, toolUsername && styles.toolChipTextActive]}>
👤 username
</Text>
</TouchableOpacity>
</View>
<Text style={styles.toolsHint}>
Quand activés, ces données sont injectées automatiquement dans le contexte de lagent au moment de linférence.
</Text>
</ScrollView> </ScrollView>
{/* Actions */} {/* Actions */}
@@ -343,4 +374,39 @@ const styles = StyleSheet.create({
color: colors.surface, color: colors.surface,
fontWeight: typography.weights.semibold, fontWeight: typography.weights.semibold,
}, },
toolsRow: {
flexDirection: 'row',
gap: spacing.sm,
marginBottom: spacing.xs,
},
toolChip: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1.5,
borderColor: colors.border,
borderRadius: 20,
paddingHorizontal: spacing.md,
paddingVertical: 7,
backgroundColor: colors.background,
},
toolChipActive: {
borderColor: colors.primary,
backgroundColor: colors.agentBg ?? colors.primary + '18',
},
toolChipText: {
fontSize: typography.sizes.sm,
color: colors.textSecondary,
fontWeight: typography.weights.medium,
},
toolChipTextActive: {
color: colors.primary,
fontWeight: typography.weights.semibold,
},
toolsHint: {
fontSize: typography.sizes.xs,
color: colors.textTertiary,
lineHeight: 17,
marginTop: 2,
marginBottom: spacing.sm,
},
}); });

View File

@@ -1,10 +1,13 @@
import React, { useMemo } from 'react'; import React, { useMemo, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, TextInput } from 'react-native';
import type { CompositeScreenProps } from '@react-navigation/native'; import type { CompositeScreenProps } from '@react-navigation/native';
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import type { RootStackParamList, TabParamList } from '../navigation'; import type { RootStackParamList, TabParamList } from '../navigation';
import { useTheme } from '../theme/ThemeProvider'; import { useTheme } from '../theme/ThemeProvider';
import { useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '../store';
import { loadSettings, saveUsername } from '../store/settingsSlice';
type TColors = { type TColors = {
background: string; background: string;
@@ -21,9 +24,8 @@ function makeStyles(colors: TColors) {
return StyleSheet.create({ return StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20, paddingHorizontal: 20,
paddingTop: 32,
}, },
text: { text: {
fontSize: 20, fontSize: 20,
@@ -58,8 +60,47 @@ function makeStyles(colors: TColors) {
segmentTextActive: { segmentTextActive: {
color: colors.onPrimary ?? '#fff', color: colors.onPrimary ?? '#fff',
}, },
buttonWrap: { sectionLabel: {
marginTop: 20, fontSize: 13,
fontWeight: '600',
color: colors.text,
opacity: 0.5,
textTransform: 'uppercase',
letterSpacing: 0.6,
marginTop: 28,
marginBottom: 8,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
input: {
flex: 1,
backgroundColor: colors.card,
borderRadius: 10,
paddingHorizontal: 14,
paddingVertical: 10,
fontSize: 16,
color: colors.text,
borderWidth: 1,
borderColor: colors.border,
},
saveBtn: {
backgroundColor: colors.primary,
borderRadius: 10,
paddingHorizontal: 16,
paddingVertical: 10,
},
saveBtnText: {
color: colors.onPrimary ?? '#fff',
fontWeight: '600',
fontSize: 14,
},
savedHint: {
marginTop: 6,
fontSize: 12,
color: colors.primary,
}, },
}); });
} }
@@ -72,7 +113,26 @@ type Props = CompositeScreenProps<
export default function SettingsScreen({}: Props) { export default function SettingsScreen({}: Props) {
const { mode, setMode, colors } = useTheme(); const { mode, setMode, colors } = useTheme();
const styles = useMemo(() => makeStyles(colors), [colors]); const styles = useMemo(() => makeStyles(colors as unknown as TColors), [colors]);
const dispatch = useDispatch<AppDispatch>();
const storedUsername = useSelector((s: RootState) => s.settings.username);
const [localUsername, setLocalUsername] = useState(storedUsername);
const [saved, setSaved] = useState(false);
useEffect(() => {
dispatch(loadSettings());
}, [dispatch]);
useEffect(() => {
setLocalUsername(storedUsername);
}, [storedUsername]);
const handleSaveUsername = () => {
dispatch(saveUsername(localUsername.trim()));
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
const items: { key: 'light' | 'system' | 'dark' | 'neon'; label: string }[] = [ const items: { key: 'light' | 'system' | 'dark' | 'neon'; label: string }[] = [
{ key: 'light', label: 'Light' }, { key: 'light', label: 'Light' },
@@ -82,9 +142,28 @@ export default function SettingsScreen({}: Props) {
]; ];
return ( return (
<View style={[styles.container, { backgroundColor: colors.background }]}> <View style={[styles.container, { backgroundColor: colors.background }]}>
<Text style={[styles.text, { color: colors.text }]}>Settings</Text> {/* Username */}
<Text style={styles.sectionLabel}>Nom d'utilisateur</Text>
<View style={styles.inputRow}>
<TextInput
style={styles.input}
value={localUsername}
onChangeText={v => { setLocalUsername(v); setSaved(false); }}
placeholder="Ton prénom ou pseudo"
placeholderTextColor={`${colors.text}60`}
autoCorrect={false}
returnKeyType="done"
onSubmitEditing={handleSaveUsername}
/>
<TouchableOpacity style={styles.saveBtn} onPress={handleSaveUsername}>
<Text style={styles.saveBtnText}>Enregistrer</Text>
</TouchableOpacity>
</View>
{saved && <Text style={styles.savedHint}> Enregistré</Text>}
{/* Theme */}
<Text style={styles.sectionLabel}>Thème</Text>
<View style={styles.segmentWrap}> <View style={styles.segmentWrap}>
<View style={styles.segmentBar}> <View style={styles.segmentBar}>
{items.map((it) => { {items.map((it) => {

View File

@@ -83,10 +83,12 @@ export const generateResponse = createAsyncThunk(
} }
const agentsState = (getState() as RootState).agents; const agentsState = (getState() as RootState).agents;
const username = (getState() as RootState).settings?.username ?? '';
const systemPrompt = resolveSystemPrompt( const systemPrompt = resolveSystemPrompt(
effectiveCfg.systemPrompt, effectiveCfg.systemPrompt,
params.activeAgentId, params.activeAgentId,
agentsState?.agents ?? [], agentsState?.agents ?? [],
username,
); );
const oaiMessages = buildOAIMessages(params.messages, systemPrompt); const oaiMessages = buildOAIMessages(params.messages, systemPrompt);

View File

@@ -153,15 +153,36 @@ export function resolveActiveStops(
/** /**
* Compose the effective system prompt: inject the selected agent's name and * Compose the effective system prompt: inject the selected agent's name and
* system prompt in front of the model-level base prompt (if any). * system prompt in front of the model-level base prompt (if any).
* Active tools are injected as context lines BEFORE the agent prompt.
*/ */
export function resolveSystemPrompt( export function resolveSystemPrompt(
basePrompt: string | undefined, basePrompt: string | undefined,
activeAgentId: string | null | undefined, activeAgentId: string | null | undefined,
agents: Agent[], agents: Agent[],
username?: string,
): string | undefined { ): string | undefined {
if (!activeAgentId) { return basePrompt; } if (!activeAgentId) { return basePrompt; }
const agent = agents.find(a => a.id === activeAgentId); const agent = agents.find(a => a.id === activeAgentId);
if (!agent) { return basePrompt; } if (!agent) { return basePrompt; }
const agentBlock = `[Agent: ${agent.name}]\n${agent.systemPrompt}`;
// Build tools context block
const toolLines: string[] = [];
if (agent.tools?.now) {
const now = new Date();
const formatted = now.toLocaleString('fr-FR', {
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
toolLines.push(`Date et heure actuelles : ${formatted}`);
}
if (agent.tools?.username && username) {
toolLines.push(`Nom de l'utilisateur : ${username}`);
}
const toolsBlock = toolLines.length > 0
? `[Contexte]\n${toolLines.join('\n')}\n`
: '';
const agentBlock = `${toolsBlock}[Agent: ${agent.name}]\n${agent.systemPrompt}`;
return basePrompt ? `${agentBlock}\n\n${basePrompt}` : agentBlock; return basePrompt ? `${agentBlock}\n\n${basePrompt}` : agentBlock;
} }

View File

@@ -3,6 +3,7 @@ import modelsReducer from './modelsSlice';
import chatReducer from './chatSlice'; import chatReducer from './chatSlice';
import hardwareReducer from './hardwareSlice'; import hardwareReducer from './hardwareSlice';
import agentsReducer from './agentsSlice'; import agentsReducer from './agentsSlice';
import settingsReducer from './settingsSlice';
import { debouncedSaveChatState } from './persistence'; import { debouncedSaveChatState } from './persistence';
const store = configureStore({ const store = configureStore({
@@ -11,6 +12,7 @@ const store = configureStore({
chat: chatReducer, chat: chatReducer,
hardware: hardwareReducer, hardware: hardwareReducer,
agents: agentsReducer, agents: agentsReducer,
settings: settingsReducer,
}, },
}); });

View File

@@ -0,0 +1,46 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import AsyncStorage from '@react-native-async-storage/async-storage';
const STORAGE_KEY = '@settings_v1';
export interface SettingsState {
username: string;
}
const initialState: SettingsState = { username: '' };
export const loadSettings = createAsyncThunk('settings/load', async () => {
const raw = await AsyncStorage.getItem(STORAGE_KEY);
if (!raw) { return initialState; }
try {
return JSON.parse(raw) as SettingsState;
} catch {
return initialState;
}
});
export const saveUsername = createAsyncThunk(
'settings/saveUsername',
async (username: string) => {
const raw = await AsyncStorage.getItem(STORAGE_KEY);
const current = raw ? (JSON.parse(raw) as Partial<SettingsState>) : {};
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify({ ...current, username }));
return username;
},
);
const settingsSlice = createSlice({
name: 'settings',
initialState,
reducers: {},
extraReducers: builder => {
builder.addCase(loadSettings.fulfilled, (state, action) => {
state.username = action.payload.username ?? '';
});
builder.addCase(saveUsername.fulfilled, (state, action) => {
state.username = action.payload;
});
},
});
export default settingsSlice.reducer;

View File

@@ -139,6 +139,11 @@ export interface Agent {
systemPrompt: string; systemPrompt: string;
/** URI du fichier image local (dans DocumentDirectoryPath) ou null */ /** URI du fichier image local (dans DocumentDirectoryPath) ou null */
avatarUri?: string | null; avatarUri?: string | null;
/** Tools actifs pour cet agent — injectés automatiquement dans le prompt système */
tools?: {
now?: boolean;
username?: boolean;
};
} }
export interface AgentsState { export interface AgentsState {