diff --git a/app/App.tsx b/app/App.tsx index b7f1138..64c0a97 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -17,6 +17,7 @@ import { ThemeProvider, useTheme } from './src/theme/ThemeProvider'; import { loadAgents } from './src/store/agentsSlice'; import { rehydrateChat } from './src/store/chatSlice'; import { loadChatState } from './src/store/persistence'; +import { loadSettings } from './src/store/settingsSlice'; function InnerApp() { const { scheme, colors } = useTheme(); @@ -30,6 +31,7 @@ function InnerApp() { const [persisted] = await Promise.all([ loadChatState(), dispatch(loadAgents()), + dispatch(loadSettings()), ]); if (persisted) { dispatch(rehydrateChat(persisted)); diff --git a/app/src/screens/AgentsScreen/AgentEditor.tsx b/app/src/screens/AgentsScreen/AgentEditor.tsx index d997210..b574490 100644 --- a/app/src/screens/AgentsScreen/AgentEditor.tsx +++ b/app/src/screens/AgentsScreen/AgentEditor.tsx @@ -40,6 +40,8 @@ export default function AgentEditor({ visible, agent, onClose }: Props) { const [avatarUri, setAvatarUri] = useState( 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 */} + Tools context + + setToolNow(v => !v)} + activeOpacity={0.75} + > + + 🕒️ now + + + setToolUsername(v => !v)} + activeOpacity={0.75} + > + + 👤 username + + + + + Quand activés, ces données sont injectées automatiquement dans le contexte de l’agent au moment de l’inférence. + {/* 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, + }, }); diff --git a/app/src/screens/SettingsScreen.tsx b/app/src/screens/SettingsScreen.tsx index 925ed14..0e71a83 100644 --- a/app/src/screens/SettingsScreen.tsx +++ b/app/src/screens/SettingsScreen.tsx @@ -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(); + 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 ( - - Settings + + {/* Username */} + Nom d'utilisateur + + { setLocalUsername(v); setSaved(false); }} + placeholder="Ton prénom ou pseudo" + placeholderTextColor={`${colors.text}60`} + autoCorrect={false} + returnKeyType="done" + onSubmitEditing={handleSaveUsername} + /> + + Enregistrer + + + {saved && ✓ Enregistré} + {/* Theme */} + Thème {items.map((it) => { diff --git a/app/src/store/chatThunks.ts b/app/src/store/chatThunks.ts index 8377cb5..e1bde1b 100644 --- a/app/src/store/chatThunks.ts +++ b/app/src/store/chatThunks.ts @@ -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); diff --git a/app/src/store/chatThunksHelpers.ts b/app/src/store/chatThunksHelpers.ts index 99b6dd3..46e6ff5 100644 --- a/app/src/store/chatThunksHelpers.ts +++ b/app/src/store/chatThunksHelpers.ts @@ -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; } diff --git a/app/src/store/index.ts b/app/src/store/index.ts index 144966d..82b10f9 100644 --- a/app/src/store/index.ts +++ b/app/src/store/index.ts @@ -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, }, }); diff --git a/app/src/store/settingsSlice.ts b/app/src/store/settingsSlice.ts new file mode 100644 index 0000000..e1cfd8f --- /dev/null +++ b/app/src/store/settingsSlice.ts @@ -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) : {}; + 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; diff --git a/app/src/store/types.ts b/app/src/store/types.ts index 962c07b..fbed0d5 100644 --- a/app/src/store/types.ts +++ b/app/src/store/types.ts @@ -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 {