diff --git a/ai/ai-react-app/src/components/Layout/LeftSidebar.module.css b/ai/ai-react-app/src/components/Layout/LeftSidebar.module.css index 893a214ee..3fa4b89c4 100644 --- a/ai/ai-react-app/src/components/Layout/LeftSidebar.module.css +++ b/ai/ai-react-app/src/components/Layout/LeftSidebar.module.css @@ -1,3 +1,13 @@ +.sidebar { + background-color: var(--color-surface-primary); + padding: 24px 0; + height: 100%; + border-right: 1px solid var(--color-border-primary); + display: flex; + flex-direction: column; + gap: 16px; +} + .sidebar ul { list-style: none; padding: 0 12px; @@ -39,7 +49,8 @@ background-color: var(--brand-gray-40); } -.backendSelector { +.backendSelector, +.personaSelector { margin-top: 24px; padding: 0 12px; border-top: 1px solid var(--color-border-primary); @@ -70,4 +81,34 @@ .radioGroup input[type="radio"] { margin-right: 8px; accent-color: var(--brand-google-blue); -} \ No newline at end of file +} + +.personaDropdown { + width: 100%; + padding: 8px 12px; + background-color: var(--color-surface-primary); + border: 1px solid var(--color-border-secondary); + color: var(--color-text-primary); + border-radius: 4px; + box-sizing: border-box; + font-size: 0.875rem; + transition: + border-color 0.15s ease, + background-color 0.15s ease; +} + +.customPersonaTextarea { + width: 100%; + margin-top: 8px; + padding: 8px 12px; + background-color: var(--color-surface-primary); + border: 1px solid var(--color-border-secondary); + color: var(--color-text-primary); + border-radius: 4px; + box-sizing: border-box; + font-size: 0.875rem; + transition: + border-color 0.15s ease, + background-color 0.15s ease; + resize: vertical; +} diff --git a/ai/ai-react-app/src/components/Layout/LeftSidebar.tsx b/ai/ai-react-app/src/components/Layout/LeftSidebar.tsx index 75e23d500..879a3f13b 100644 --- a/ai/ai-react-app/src/components/Layout/LeftSidebar.tsx +++ b/ai/ai-react-app/src/components/Layout/LeftSidebar.tsx @@ -1,7 +1,8 @@ -import React from "react"; +import React, { useState } from "react"; import { AppMode } from "../../App"; import styles from "./LeftSidebar.module.css"; -import { BackendType } from "firebase/ai"; +import { BackendType, Content, ModelParams } from "firebase/ai"; +import { PREDEFINED_PERSONAS } from "../../config/personas"; interface LeftSidebarProps { /** The currently active application mode (e.g., 'chat', 'imagenGen'). */ @@ -10,6 +11,8 @@ interface LeftSidebarProps { setActiveMode: (mode: AppMode) => void; activeBackend: BackendType; setActiveBackend: (backend: BackendType) => void; + generativeParams: ModelParams; + setGenerativeParams: React.Dispatch>; } /** @@ -20,7 +23,20 @@ const LeftSidebar: React.FC = ({ setActiveMode, activeBackend, setActiveBackend, + setGenerativeParams, }) => { + // This component now manages its own UI state and pushes updates upwards. + // It does not rely on a useEffect to sync systemInstruction from the parent, + // following the pattern in RightSidebar.tsx to prevent state-reversion bugs. + const [selectedPersonaId, setSelectedPersonaId] = useState("default"); + const [customPersona, setCustomPersona] = useState(""); + + const handleModelParamsUpdate = ( + updateFn: (prevState: ModelParams) => ModelParams, + ) => { + setGenerativeParams((prevState) => updateFn(prevState)); + }; + // Define the available modes and their display names const modes: { id: AppMode; label: string }[] = [ { id: "chat", label: "Chat" }, @@ -31,6 +47,51 @@ const LeftSidebar: React.FC = ({ setActiveBackend(event.target.value as BackendType); }; + const handlePersonaChange = (e: React.ChangeEvent) => { + const newPersonaId = e.target.value; + setSelectedPersonaId(newPersonaId); // 1. Update UI state + + let newSystemInstructionText: string; + + if (newPersonaId === "custom") { + // When switching to custom, the instruction is whatever is in the textarea. + newSystemInstructionText = customPersona; + } else { + // When switching to a predefined persona, find its instruction text. + const selected = PREDEFINED_PERSONAS.find((p) => p.id === newPersonaId); + newSystemInstructionText = selected?.systemInstruction ?? ""; + // We are no longer in 'custom', but we don't clear the customPersona state + // in case the user wants to switch back and forth. + } + + const newSystemInstruction: Content | undefined = newSystemInstructionText + ? { parts: [{ text: newSystemInstructionText }], role: "system" } + : undefined; + + // 2. Update model state upwards + handleModelParamsUpdate((prev: ModelParams) => ({ + ...prev, + systemInstruction: newSystemInstruction, + })); + }; + + const handleCustomPersonaChange = ( + e: React.ChangeEvent, + ) => { + const newSystemInstructionText = e.target.value; + setCustomPersona(newSystemInstructionText); // 1. Update UI state + + const newSystemInstruction: Content | undefined = newSystemInstructionText + ? { parts: [{ text: newSystemInstructionText }], role: "system" } + : undefined; + + // 2. Update model state upwards + handleModelParamsUpdate((prev: ModelParams) => ({ + ...prev, + systemInstruction: newSystemInstruction, + })); + }; + return (