Skip to content

Commit 9a3a44b

Browse files
committed
Add persona selection to AI qs
1 parent 4b98596 commit 9a3a44b

File tree

4 files changed

+183
-42
lines changed

4 files changed

+183
-42
lines changed
Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,85 @@
1+
.sidebar {
2+
background-color: #f8f9fa;
3+
padding: 1rem;
4+
height: 100%;
5+
border-right: 1px solid #dee2e6;
6+
display: flex;
7+
flex-direction: column;
8+
gap: 1.5rem;
9+
}
10+
111
.sidebar ul {
212
list-style: none;
3-
padding: 0 12px;
13+
padding: 0;
414
margin: 0;
515
}
616

7-
.sidebar li {
8-
margin-bottom: 4px;
9-
}
10-
1117
.navButton {
1218
background-color: transparent;
13-
border: none;
14-
color: var(--color-text-secondary);
15-
padding: 8px 12px;
16-
text-align: left;
19+
border: 1px solid #dee2e6;
20+
border-radius: 0.375rem;
21+
padding: 0.75rem 1rem;
1722
width: 100%;
23+
text-align: left;
1824
cursor: pointer;
19-
border-radius: 4px;
20-
font-size: 0.875rem;
25+
font-size: 1rem;
26+
color: #212529;
2127
transition:
22-
background-color 0.15s ease,
23-
color 0.15s ease;
24-
font-weight: 500;
28+
background-color 0.15s ease-in-out,
29+
border-color 0.15s ease-in-out;
2530
}
2631

2732
.navButton:hover {
28-
background-color: var(--color-surface-tertiary);
29-
color: var(--color-text-primary);
33+
background-color: #e9ecef;
3034
}
3135

3236
.navButton.active {
33-
background-color: var(--color-surface-tertiary);
34-
color: var(--color-text-accent);
35-
font-weight: 500;
37+
background-color: #0d6efd;
38+
color: white;
39+
border-color: #0d6efd;
3640
}
3741

38-
.navButton.active:hover {
39-
background-color: var(--brand-gray-40);
40-
}
41-
42-
.backendSelector {
43-
margin-top: 24px;
44-
padding: 0 12px;
45-
border-top: 1px solid var(--color-border-primary);
46-
padding-top: 16px;
42+
.backendSelector,
43+
.personaSelector {
44+
border-top: 1px solid #dee2e6;
45+
padding-top: 1rem;
4746
}
4847

4948
.selectorTitle {
50-
color: var(--color-text-secondary);
51-
font-size: 0.75rem;
52-
font-weight: 500;
53-
margin: 0 0 8px 0;
49+
font-size: 0.875rem;
50+
font-weight: 600;
51+
color: #6c757d;
5452
text-transform: uppercase;
55-
letter-spacing: 0.5px;
53+
margin-bottom: 0.75rem;
5654
}
5755

5856
.radioGroup {
59-
margin-bottom: 6px;
57+
margin-bottom: 0.5rem;
6058
}
6159

6260
.radioGroup label {
6361
display: flex;
6462
align-items: center;
65-
font-size: 0.875rem;
66-
color: var(--color-text-primary);
63+
gap: 0.5rem;
64+
font-size: 0.95rem;
6765
cursor: pointer;
6866
}
6967

70-
.radioGroup input[type="radio"] {
71-
margin-right: 8px;
72-
accent-color: var(--brand-google-blue);
73-
}
68+
.personaDropdown {
69+
width: 100%;
70+
padding: 0.5rem;
71+
border-radius: 0.25rem;
72+
border: 1px solid #ced4da;
73+
font-size: 0.9rem;
74+
background-color: #fff;
75+
}
76+
77+
.customPersonaTextarea {
78+
width: 100%;
79+
margin-top: 0.5rem;
80+
padding: 0.5rem;
81+
border-radius: 0.25rem;
82+
border: 1px solid #ced4da;
83+
font-size: 0.9rem;
84+
resize: vertical;
85+
}

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

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React from "react";
1+
import React, { useState, useEffect } from "react";
22
import { AppMode } from "../../App";
33
import styles from "./LeftSidebar.module.css";
4-
import { BackendType } from "firebase/ai";
4+
import { BackendType, 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,10 @@ interface LeftSidebarProps {
1011
setActiveMode: (mode: AppMode) => void;
1112
activeBackend: BackendType;
1213
setActiveBackend: (backend: BackendType) => void;
14+
generativeParams: ModelParams;
15+
setGenerativeParams: (
16+
params: ModelParams | ((prevState: ModelParams) => ModelParams),
17+
) => void;
1318
}
1419

1520
/**
@@ -20,7 +25,50 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
2025
setActiveMode,
2126
activeBackend,
2227
setActiveBackend,
28+
generativeParams,
29+
setGenerativeParams,
2330
}) => {
31+
const [selectedPersonaId, setSelectedPersonaId] = useState<string>("default");
32+
const [customPersona, setCustomPersona] = useState<string>("");
33+
34+
// Effect to update systemInstruction when persona changes
35+
useEffect(() => {
36+
const selected = PREDEFINED_PERSONAS.find(
37+
(p) => p.id === selectedPersonaId,
38+
);
39+
if (!selected) return;
40+
41+
const newInstructionText =
42+
selected.id === "custom" ? customPersona : selected.systemInstruction;
43+
44+
setGenerativeParams((prevParams) => {
45+
const newSystemInstruction = newInstructionText
46+
? { parts: [{ text: newInstructionText }] }
47+
: undefined;
48+
49+
const currentInstruction = prevParams.systemInstruction;
50+
const currentInstructionText =
51+
currentInstruction &&
52+
typeof currentInstruction === "object" &&
53+
"parts" in currentInstruction &&
54+
Array.isArray(currentInstruction.parts) &&
55+
currentInstruction.parts.length > 0 &&
56+
"text" in currentInstruction.parts[0]
57+
? currentInstruction.parts[0].text
58+
: undefined;
59+
60+
// Only update if the text content has actually changed.
61+
if ((newInstructionText || "") !== (currentInstructionText || "")) {
62+
return {
63+
...prevParams,
64+
systemInstruction: newSystemInstruction,
65+
};
66+
}
67+
// If no change, return the previous state to prevent re-render.
68+
return prevParams;
69+
});
70+
}, [selectedPersonaId, customPersona, setGenerativeParams]);
71+
2472
// Define the available modes and their display names
2573
const modes: { id: AppMode; label: string }[] = [
2674
{ id: "chat", label: "Chat" },
@@ -31,6 +79,16 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
3179
setActiveBackend(event.target.value as BackendType);
3280
};
3381

82+
const handlePersonaChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
83+
setSelectedPersonaId(e.target.value);
84+
};
85+
86+
const handleCustomPersonaChange = (
87+
e: React.ChangeEvent<HTMLTextAreaElement>,
88+
) => {
89+
setCustomPersona(e.target.value);
90+
};
91+
3492
return (
3593
<nav className={styles.sidebar} aria-label="Main navigation">
3694
<ul>
@@ -76,6 +134,33 @@ const LeftSidebar: React.FC<LeftSidebarProps> = ({
76134
</label>
77135
</div>
78136
</div>
137+
138+
{/* Persona Selector */}
139+
{activeMode === "chat" && (
140+
<div className={styles.personaSelector}>
141+
<h6 className={styles.selectorTitle}>Persona</h6>
142+
<select
143+
value={selectedPersonaId}
144+
onChange={handlePersonaChange}
145+
className={styles.personaDropdown}
146+
>
147+
{PREDEFINED_PERSONAS.map((persona) => (
148+
<option key={persona.id} value={persona.id}>
149+
{persona.name}
150+
</option>
151+
))}
152+
</select>
153+
{selectedPersonaId === "custom" && (
154+
<textarea
155+
value={customPersona}
156+
onChange={handleCustomPersonaChange}
157+
className={styles.customPersonaTextarea}
158+
placeholder="Enter your custom persona instruction here..."
159+
rows={5}
160+
/>
161+
)}
162+
</div>
163+
)}
79164
</nav>
80165
);
81166
};

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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export interface Persona {
2+
id: string;
3+
name: string;
4+
systemInstruction: string;
5+
}
6+
7+
export const PREDEFINED_PERSONAS: Persona[] = [
8+
{
9+
id: "default",
10+
name: "Default",
11+
systemInstruction: "",
12+
},
13+
{
14+
id: "pirate",
15+
name: "Pirate Captain",
16+
systemInstruction:
17+
"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'.",
18+
},
19+
{
20+
id: "shakespeare",
21+
name: "Shakespearean Poet",
22+
systemInstruction:
23+
"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.",
24+
},
25+
{
26+
id: "sarcastic_teen",
27+
name: "Sarcastic Teenager",
28+
systemInstruction:
29+
"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.",
30+
},
31+
{
32+
id: "helpful_dev",
33+
name: "Helpful Senior Developer",
34+
systemInstruction:
35+
"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.",
36+
},
37+
{
38+
id: "custom",
39+
name: "Custom...",
40+
systemInstruction: "",
41+
},
42+
];

0 commit comments

Comments
 (0)