feat: add settings management with username and tools context for agents
This commit is contained in:
@@ -40,6 +40,8 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
|
||||
const [avatarUri, setAvatarUri] = useState<string | 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 [saving, setSaving] = useState(false);
|
||||
|
||||
@@ -49,6 +51,8 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
|
||||
setName(agent?.name ?? '');
|
||||
setPrompt(agent?.systemPrompt ?? 'Tu es un assistant utile et concis.');
|
||||
setAvatarUri(agent?.avatarUri ?? null);
|
||||
setToolNow(agent?.tools?.now ?? false);
|
||||
setToolUsername(agent?.tools?.username ?? false);
|
||||
}
|
||||
}, [visible, agent]);
|
||||
|
||||
@@ -91,6 +95,7 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
|
||||
name: name.trim(),
|
||||
systemPrompt: prompt.trim(),
|
||||
avatarUri: avatarUri ?? null,
|
||||
tools: { now: toolNow, username: toolUsername },
|
||||
};
|
||||
await dispatch(saveAgent(updated));
|
||||
onClose();
|
||||
@@ -176,6 +181,32 @@ export default function AgentEditor({ visible, agent, onClose }: Props) {
|
||||
numberOfLines={6}
|
||||
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 l’agent au moment de l’inférence.
|
||||
</Text>
|
||||
</ScrollView>
|
||||
|
||||
{/* Actions */}
|
||||
@@ -343,4 +374,39 @@ const styles = StyleSheet.create({
|
||||
color: colors.surface,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import React, { useMemo, useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, TextInput } from 'react-native';
|
||||
import type { CompositeScreenProps } from '@react-navigation/native';
|
||||
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { RootStackParamList, TabParamList } from '../navigation';
|
||||
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 = {
|
||||
background: string;
|
||||
@@ -21,9 +24,8 @@ function makeStyles(colors: TColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 32,
|
||||
},
|
||||
text: {
|
||||
fontSize: 20,
|
||||
@@ -58,8 +60,47 @@ function makeStyles(colors: TColors) {
|
||||
segmentTextActive: {
|
||||
color: colors.onPrimary ?? '#fff',
|
||||
},
|
||||
buttonWrap: {
|
||||
marginTop: 20,
|
||||
sectionLabel: {
|
||||
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) {
|
||||
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 }[] = [
|
||||
{ key: 'light', label: 'Light' },
|
||||
@@ -82,9 +142,28 @@ export default function SettingsScreen({}: Props) {
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
<Text style={[styles.text, { color: colors.text }]}>Settings</Text>
|
||||
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
||||
{/* 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.segmentBar}>
|
||||
{items.map((it) => {
|
||||
|
||||
@@ -83,10 +83,12 @@ export const generateResponse = createAsyncThunk(
|
||||
}
|
||||
|
||||
const agentsState = (getState() as RootState).agents;
|
||||
const username = (getState() as RootState).settings?.username ?? '';
|
||||
const systemPrompt = resolveSystemPrompt(
|
||||
effectiveCfg.systemPrompt,
|
||||
params.activeAgentId,
|
||||
agentsState?.agents ?? [],
|
||||
username,
|
||||
);
|
||||
|
||||
const oaiMessages = buildOAIMessages(params.messages, systemPrompt);
|
||||
|
||||
@@ -153,15 +153,36 @@ export function resolveActiveStops(
|
||||
/**
|
||||
* Compose the effective system prompt: inject the selected agent's name and
|
||||
* 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(
|
||||
basePrompt: string | undefined,
|
||||
activeAgentId: string | null | undefined,
|
||||
agents: Agent[],
|
||||
username?: string,
|
||||
): string | undefined {
|
||||
if (!activeAgentId) { return basePrompt; }
|
||||
const agent = agents.find(a => a.id === activeAgentId);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import modelsReducer from './modelsSlice';
|
||||
import chatReducer from './chatSlice';
|
||||
import hardwareReducer from './hardwareSlice';
|
||||
import agentsReducer from './agentsSlice';
|
||||
import settingsReducer from './settingsSlice';
|
||||
import { debouncedSaveChatState } from './persistence';
|
||||
|
||||
const store = configureStore({
|
||||
@@ -11,6 +12,7 @@ const store = configureStore({
|
||||
chat: chatReducer,
|
||||
hardware: hardwareReducer,
|
||||
agents: agentsReducer,
|
||||
settings: settingsReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
46
app/src/store/settingsSlice.ts
Normal file
46
app/src/store/settingsSlice.ts
Normal 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;
|
||||
@@ -139,6 +139,11 @@ export interface Agent {
|
||||
systemPrompt: string;
|
||||
/** URI du fichier image local (dans DocumentDirectoryPath) ou 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 {
|
||||
|
||||
Reference in New Issue
Block a user