From 9a3a44b0a8c70ffea6fe1f3cc1beccb4a8985701 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 31 Jul 2025 15:42:22 -0700 Subject: [PATCH 01/13] Add persona selection to AI qs --- .../components/Layout/LeftSidebar.module.css | 92 +++++++++++-------- .../src/components/Layout/LeftSidebar.tsx | 89 +++++++++++++++++- .../src/components/Layout/MainLayout.tsx | 2 + ai/ai-react-app/src/config/personas.ts | 42 +++++++++ 4 files changed, 183 insertions(+), 42 deletions(-) create mode 100644 ai/ai-react-app/src/config/personas.ts 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..2a7bf5805 100644 --- a/ai/ai-react-app/src/components/Layout/LeftSidebar.module.css +++ b/ai/ai-react-app/src/components/Layout/LeftSidebar.module.css @@ -1,73 +1,85 @@ +.sidebar { + background-color: #f8f9fa; + padding: 1rem; + height: 100%; + border-right: 1px solid #dee2e6; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + .sidebar ul { list-style: none; - padding: 0 12px; + padding: 0; margin: 0; } -.sidebar li { - margin-bottom: 4px; -} - .navButton { background-color: transparent; - border: none; - color: var(--color-text-secondary); - padding: 8px 12px; - text-align: left; + border: 1px solid #dee2e6; + border-radius: 0.375rem; + padding: 0.75rem 1rem; width: 100%; + text-align: left; cursor: pointer; - border-radius: 4px; - font-size: 0.875rem; + font-size: 1rem; + color: #212529; transition: - background-color 0.15s ease, - color 0.15s ease; - font-weight: 500; + background-color 0.15s ease-in-out, + border-color 0.15s ease-in-out; } .navButton:hover { - background-color: var(--color-surface-tertiary); - color: var(--color-text-primary); + background-color: #e9ecef; } .navButton.active { - background-color: var(--color-surface-tertiary); - color: var(--color-text-accent); - font-weight: 500; + background-color: #0d6efd; + color: white; + border-color: #0d6efd; } -.navButton.active:hover { - background-color: var(--brand-gray-40); -} - -.backendSelector { - margin-top: 24px; - padding: 0 12px; - border-top: 1px solid var(--color-border-primary); - padding-top: 16px; +.backendSelector, +.personaSelector { + border-top: 1px solid #dee2e6; + padding-top: 1rem; } .selectorTitle { - color: var(--color-text-secondary); - font-size: 0.75rem; - font-weight: 500; - margin: 0 0 8px 0; + font-size: 0.875rem; + font-weight: 600; + color: #6c757d; text-transform: uppercase; - letter-spacing: 0.5px; + margin-bottom: 0.75rem; } .radioGroup { - margin-bottom: 6px; + margin-bottom: 0.5rem; } .radioGroup label { display: flex; align-items: center; - font-size: 0.875rem; - color: var(--color-text-primary); + gap: 0.5rem; + font-size: 0.95rem; cursor: pointer; } -.radioGroup input[type="radio"] { - margin-right: 8px; - accent-color: var(--brand-google-blue); -} \ No newline at end of file +.personaDropdown { + width: 100%; + padding: 0.5rem; + border-radius: 0.25rem; + border: 1px solid #ced4da; + font-size: 0.9rem; + background-color: #fff; +} + +.customPersonaTextarea { + width: 100%; + margin-top: 0.5rem; + padding: 0.5rem; + border-radius: 0.25rem; + border: 1px solid #ced4da; + font-size: 0.9rem; + 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..e2f9328c7 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, useEffect } from "react"; import { AppMode } from "../../App"; import styles from "./LeftSidebar.module.css"; -import { BackendType } from "firebase/ai"; +import { BackendType, ModelParams } from "firebase/ai"; +import { PREDEFINED_PERSONAS } from "../../config/personas"; interface LeftSidebarProps { /** The currently active application mode (e.g., 'chat', 'imagenGen'). */ @@ -10,6 +11,10 @@ interface LeftSidebarProps { setActiveMode: (mode: AppMode) => void; activeBackend: BackendType; setActiveBackend: (backend: BackendType) => void; + generativeParams: ModelParams; + setGenerativeParams: ( + params: ModelParams | ((prevState: ModelParams) => ModelParams), + ) => void; } /** @@ -20,7 +25,50 @@ const LeftSidebar: React.FC = ({ setActiveMode, activeBackend, setActiveBackend, + generativeParams, + setGenerativeParams, }) => { + const [selectedPersonaId, setSelectedPersonaId] = useState("default"); + const [customPersona, setCustomPersona] = useState(""); + + // Effect to update systemInstruction when persona changes + useEffect(() => { + const selected = PREDEFINED_PERSONAS.find( + (p) => p.id === selectedPersonaId, + ); + if (!selected) return; + + const newInstructionText = + selected.id === "custom" ? customPersona : selected.systemInstruction; + + setGenerativeParams((prevParams) => { + const newSystemInstruction = newInstructionText + ? { parts: [{ text: newInstructionText }] } + : undefined; + + const currentInstruction = prevParams.systemInstruction; + const currentInstructionText = + currentInstruction && + typeof currentInstruction === "object" && + "parts" in currentInstruction && + Array.isArray(currentInstruction.parts) && + currentInstruction.parts.length > 0 && + "text" in currentInstruction.parts[0] + ? currentInstruction.parts[0].text + : undefined; + + // Only update if the text content has actually changed. + if ((newInstructionText || "") !== (currentInstructionText || "")) { + return { + ...prevParams, + systemInstruction: newSystemInstruction, + }; + } + // If no change, return the previous state to prevent re-render. + return prevParams; + }); + }, [selectedPersonaId, customPersona, setGenerativeParams]); + // Define the available modes and their display names const modes: { id: AppMode; label: string }[] = [ { id: "chat", label: "Chat" }, @@ -31,6 +79,16 @@ const LeftSidebar: React.FC = ({ setActiveBackend(event.target.value as BackendType); }; + const handlePersonaChange = (e: React.ChangeEvent) => { + setSelectedPersonaId(e.target.value); + }; + + const handleCustomPersonaChange = ( + e: React.ChangeEvent, + ) => { + setCustomPersona(e.target.value); + }; + return (