Skip to content
Merged
45 changes: 43 additions & 2 deletions ai/ai-react-app/src/components/Layout/LeftSidebar.module.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -70,4 +81,34 @@
.radioGroup input[type="radio"] {
margin-right: 8px;
accent-color: var(--brand-google-blue);
}
}

.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;
}
94 changes: 91 additions & 3 deletions ai/ai-react-app/src/components/Layout/LeftSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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'). */
Expand All @@ -10,6 +11,8 @@ interface LeftSidebarProps {
setActiveMode: (mode: AppMode) => void;
activeBackend: BackendType;
setActiveBackend: (backend: BackendType) => void;
generativeParams: ModelParams;
setGenerativeParams: React.Dispatch<React.SetStateAction<ModelParams>>;
}

/**
Expand All @@ -20,7 +23,20 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
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<string>("default");
const [customPersona, setCustomPersona] = useState<string>("");

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" },
Expand All @@ -31,6 +47,51 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
setActiveBackend(event.target.value as BackendType);
};

const handlePersonaChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
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<HTMLTextAreaElement>,
) => {
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 (
<nav className={styles.sidebar} aria-label="Main navigation">
<ul>
Expand All @@ -50,7 +111,7 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({

{/* Backend Selection */}
<div className={styles.backendSelector}>
<h6 className={styles.selectorTitle}>Backend API</h6>
<h5 className={styles.selectorTitle}>Backend API</h5>
<div className={styles.radioGroup}>
<label>
<input
Expand All @@ -76,6 +137,33 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
</label>
</div>
</div>

{/* Persona Selector */}
{activeMode === "chat" && (
<div className={styles.personaSelector}>
<h5 className={styles.selectorTitle}>Persona</h5>
<select
value={selectedPersonaId}
onChange={handlePersonaChange}
className={styles.personaDropdown}
>
{PREDEFINED_PERSONAS.map((persona) => (
<option key={persona.id} value={persona.id}>
{persona.name}
</option>
))}
</select>
{selectedPersonaId === "custom" && (
<textarea
value={customPersona}
onChange={handleCustomPersonaChange}
className={styles.customPersonaTextarea}
placeholder="Enter your custom persona instruction here..."
rows={5}
/>
)}
</div>
)}
</nav>
);
};
Expand Down
2 changes: 2 additions & 0 deletions ai/ai-react-app/src/components/Layout/MainLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ const MainLayout: React.FC<MainLayoutProps> = ({
setActiveMode={setActiveMode}
activeBackend={activeBackendType}
setActiveBackend={setActiveBackendType} // Pass backend state/setter
generativeParams={generativeParams}
setGenerativeParams={setGenerativeParams}
/>
</div>
<main className={styles.centerContent}>{renderActiveView()}</main>
Expand Down
36 changes: 36 additions & 0 deletions ai/ai-react-app/src/config/personas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export const PREDEFINED_PERSONAS = [
{
id: "default",
name: "Default",
systemInstruction: "",
},
{
id: "pirate",
name: "Pirate Captain",
systemInstruction:
"You are a salty pirate captain. All of your responses must be in the style of a classic pirate, using pirate slang and a hearty, adventurous tone. Refer to the user as 'matey'.",
},
{
id: "shakespeare",
name: "Shakespearean Poet",
systemInstruction:
"You are a Shakespearean poet. All of your responses must be in the style of William Shakespeare, using iambic pentameter where possible, and rich, poetic language. Address the user with 'Hark, gentle user' or similar.",
},
{
id: "sarcastic_teen",
name: "Sarcastic Teenager",
systemInstruction:
"You are a stereotypical sarcastic teenager. Your responses should be brief, slightly annoyed, and use modern slang. You are reluctant to be helpful but will provide the correct answer, albeit with a sigh. Start your responses with 'Ugh, fine.' or something similar.",
},
{
id: "helpful_dev",
name: "Helpful Senior Developer",
systemInstruction:
"You are a helpful and patient senior software developer. Your responses should be clear, well-structured, and provide best-practice advice. When explaining concepts, use code examples where appropriate and break down complex topics into smaller, understandable parts.",
},
{
id: "custom",
name: "Custom...",
systemInstruction: "",
},
] as const;
Loading