Skip to content

Commit 4618e50

Browse files
authored
Merge pull request #898 from firebase/pb-personas
Add persona selection to AI qs
2 parents 4b98596 + 6cbeb5d commit 4618e50

File tree

4 files changed

+172
-5
lines changed

4 files changed

+172
-5
lines changed

ai/ai-react-app/src/components/Layout/LeftSidebar.module.css

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
.sidebar {
2+
background-color: var(--color-surface-primary);
3+
padding: 24px 0;
4+
height: 100%;
5+
border-right: 1px solid var(--color-border-primary);
6+
display: flex;
7+
flex-direction: column;
8+
gap: 16px;
9+
}
10+
111
.sidebar ul {
212
list-style: none;
313
padding: 0 12px;
@@ -39,7 +49,8 @@
3949
background-color: var(--brand-gray-40);
4050
}
4151

42-
.backendSelector {
52+
.backendSelector,
53+
.personaSelector {
4354
margin-top: 24px;
4455
padding: 0 12px;
4556
border-top: 1px solid var(--color-border-primary);
@@ -70,4 +81,34 @@
7081
.radioGroup input[type="radio"] {
7182
margin-right: 8px;
7283
accent-color: var(--brand-google-blue);
73-
}
84+
}
85+
86+
.personaDropdown {
87+
width: 100%;
88+
padding: 8px 12px;
89+
background-color: var(--color-surface-primary);
90+
border: 1px solid var(--color-border-secondary);
91+
color: var(--color-text-primary);
92+
border-radius: 4px;
93+
box-sizing: border-box;
94+
font-size: 0.875rem;
95+
transition:
96+
border-color 0.15s ease,
97+
background-color 0.15s ease;
98+
}
99+
100+
.customPersonaTextarea {
101+
width: 100%;
102+
margin-top: 8px;
103+
padding: 8px 12px;
104+
background-color: var(--color-surface-primary);
105+
border: 1px solid var(--color-border-secondary);
106+
color: var(--color-text-primary);
107+
border-radius: 4px;
108+
box-sizing: border-box;
109+
font-size: 0.875rem;
110+
transition:
111+
border-color 0.15s ease,
112+
background-color 0.15s ease;
113+
resize: vertical;
114+
}

ai/ai-react-app/src/components/Layout/LeftSidebar.tsx

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React from "react";
1+
import React, { useState } from "react";
22
import { AppMode } from "../../App";
33
import styles from "./LeftSidebar.module.css";
4-
import { BackendType } from "firebase/ai";
4+
import { BackendType, Content, ModelParams } from "firebase/ai";
5+
import { PREDEFINED_PERSONAS } from "../../config/personas";
56

67
interface LeftSidebarProps {
78
/** The currently active application mode (e.g., 'chat', 'imagenGen'). */
@@ -10,6 +11,8 @@ interface LeftSidebarProps {
1011
setActiveMode: (mode: AppMode) => void;
1112
activeBackend: BackendType;
1213
setActiveBackend: (backend: BackendType) => void;
14+
generativeParams: ModelParams;
15+
setGenerativeParams: React.Dispatch<React.SetStateAction<ModelParams>>;
1316
}
1417

1518
/**
@@ -20,7 +23,20 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
2023
setActiveMode,
2124
activeBackend,
2225
setActiveBackend,
26+
setGenerativeParams,
2327
}) => {
28+
// This component now manages its own UI state and pushes updates upwards.
29+
// It does not rely on a useEffect to sync systemInstruction from the parent,
30+
// following the pattern in RightSidebar.tsx to prevent state-reversion bugs.
31+
const [selectedPersonaId, setSelectedPersonaId] = useState<string>("default");
32+
const [customPersona, setCustomPersona] = useState<string>("");
33+
34+
const handleModelParamsUpdate = (
35+
updateFn: (prevState: ModelParams) => ModelParams,
36+
) => {
37+
setGenerativeParams((prevState) => updateFn(prevState));
38+
};
39+
2440
// Define the available modes and their display names
2541
const modes: { id: AppMode; label: string }[] = [
2642
{ id: "chat", label: "Chat" },
@@ -31,6 +47,51 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
3147
setActiveBackend(event.target.value as BackendType);
3248
};
3349

50+
const handlePersonaChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
51+
const newPersonaId = e.target.value;
52+
setSelectedPersonaId(newPersonaId); // 1. Update UI state
53+
54+
let newSystemInstructionText: string;
55+
56+
if (newPersonaId === "custom") {
57+
// When switching to custom, the instruction is whatever is in the textarea.
58+
newSystemInstructionText = customPersona;
59+
} else {
60+
// When switching to a predefined persona, find its instruction text.
61+
const selected = PREDEFINED_PERSONAS.find((p) => p.id === newPersonaId);
62+
newSystemInstructionText = selected?.systemInstruction ?? "";
63+
// We are no longer in 'custom', but we don't clear the customPersona state
64+
// in case the user wants to switch back and forth.
65+
}
66+
67+
const newSystemInstruction: Content | undefined = newSystemInstructionText
68+
? { parts: [{ text: newSystemInstructionText }], role: "system" }
69+
: undefined;
70+
71+
// 2. Update model state upwards
72+
handleModelParamsUpdate((prev: ModelParams) => ({
73+
...prev,
74+
systemInstruction: newSystemInstruction,
75+
}));
76+
};
77+
78+
const handleCustomPersonaChange = (
79+
e: React.ChangeEvent<HTMLTextAreaElement>,
80+
) => {
81+
const newSystemInstructionText = e.target.value;
82+
setCustomPersona(newSystemInstructionText); // 1. Update UI state
83+
84+
const newSystemInstruction: Content | undefined = newSystemInstructionText
85+
? { parts: [{ text: newSystemInstructionText }], role: "system" }
86+
: undefined;
87+
88+
// 2. Update model state upwards
89+
handleModelParamsUpdate((prev: ModelParams) => ({
90+
...prev,
91+
systemInstruction: newSystemInstruction,
92+
}));
93+
};
94+
3495
return (
3596
<nav className={styles.sidebar} aria-label="Main navigation">
3697
<ul>
@@ -50,7 +111,7 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
50111

51112
{/* Backend Selection */}
52113
<div className={styles.backendSelector}>
53-
<h6 className={styles.selectorTitle}>Backend API</h6>
114+
<h5 className={styles.selectorTitle}>Backend API</h5>
54115
<div className={styles.radioGroup}>
55116
<label>
56117
<input
@@ -76,6 +137,33 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
76137
</label>
77138
</div>
78139
</div>
140+
141+
{/* Persona Selector */}
142+
{activeMode === "chat" && (
143+
<div className={styles.personaSelector}>
144+
<h5 className={styles.selectorTitle}>Persona</h5>
145+
<select
146+
value={selectedPersonaId}
147+
onChange={handlePersonaChange}
148+
className={styles.personaDropdown}
149+
>
150+
{PREDEFINED_PERSONAS.map((persona) => (
151+
<option key={persona.id} value={persona.id}>
152+
{persona.name}
153+
</option>
154+
))}
155+
</select>
156+
{selectedPersonaId === "custom" && (
157+
<textarea
158+
value={customPersona}
159+
onChange={handleCustomPersonaChange}
160+
className={styles.customPersonaTextarea}
161+
placeholder="Enter your custom persona instruction here..."
162+
rows={5}
163+
/>
164+
)}
165+
</div>
166+
)}
79167
</nav>
80168
);
81169
};

ai/ai-react-app/src/components/Layout/MainLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ const MainLayout: React.FC<MainLayoutProps> = ({
135135
setActiveMode={setActiveMode}
136136
activeBackend={activeBackendType}
137137
setActiveBackend={setActiveBackendType} // Pass backend state/setter
138+
generativeParams={generativeParams}
139+
setGenerativeParams={setGenerativeParams}
138140
/>
139141
</div>
140142
<main className={styles.centerContent}>{renderActiveView()}</main>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
export const PREDEFINED_PERSONAS = [
2+
{
3+
id: "default",
4+
name: "Default",
5+
systemInstruction: "",
6+
},
7+
{
8+
id: "pirate",
9+
name: "Pirate Captain",
10+
systemInstruction:
11+
"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'.",
12+
},
13+
{
14+
id: "shakespeare",
15+
name: "Shakespearean Poet",
16+
systemInstruction:
17+
"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.",
18+
},
19+
{
20+
id: "sarcastic_teen",
21+
name: "Sarcastic Teenager",
22+
systemInstruction:
23+
"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.",
24+
},
25+
{
26+
id: "helpful_dev",
27+
name: "Helpful Senior Developer",
28+
systemInstruction:
29+
"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.",
30+
},
31+
{
32+
id: "custom",
33+
name: "Custom...",
34+
systemInstruction: "",
35+
},
36+
] as const;

0 commit comments

Comments
 (0)