Skip to content

Commit 555c958

Browse files
Copilotchrisglein
andcommitted
Merge main branch and resolve conflicts
Co-authored-by: chrisglein <26607885+chrisglein@users.noreply.github.com>
1 parent b9ae00b commit 555c958

File tree

10 files changed

+239
-19
lines changed

10 files changed

+239
-19
lines changed

src/AiQuery.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Respond with the image prompt string in the required format. Do not respond conv
133133
CallOpenAi({
134134
api: OpenAiApi.ChatCompletion,
135135
apiKey: settingsContext.apiKey,
136-
instructions: 'The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly. If the response involves code, use markdown format for that with ```(language) blocks.',
136+
instructions: settingsContext.systemInstructions,
137137
identifier: 'TEXT-ANSWER:',
138138
prompt: prompt,
139139
options: {

src/AiResponse.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ type AiSectionProps = PropsWithChildren<{
7979
isLoading?: boolean;
8080
copyValue?: string;
8181
moreMenu?: FlyoutMenuButtonType[];
82+
pinned?: boolean;
8283
}>;
8384
function AiSection({
8485
children,
8586
id,
8687
isLoading,
8788
copyValue,
8889
moreMenu,
90+
pinned = false,
8991
}: AiSectionProps): JSX.Element {
9092
const feedbackContext = React.useContext(FeedbackContext);
9193
const styles = React.useContext(StylesContext);
@@ -102,6 +104,12 @@ function AiSection({
102104
menuItems.push(...moreMenu);
103105
}
104106
if (id !== undefined) {
107+
// Add pin/unpin option
108+
menuItems.push({
109+
title: pinned ? 'Unpin message' : 'Pin message',
110+
icon: 0xE718, // Pin icon
111+
onPress: () => chatHistory.togglePin(id)
112+
});
105113
menuItems.push(
106114
{title: 'Delete this response', icon: 0xE74D, onPress: () => chatHistory.deleteResponse(id)}
107115
);
@@ -128,7 +136,7 @@ function AiSection({
128136
<Text
129137
accessibilityRole="header"
130138
style={[styles.sectionTitle, {flexGrow: 1}]}>
131-
OpenAI
139+
{pinned ? '📌 ' : ''}OpenAI
132140
</Text>
133141
<FlyoutMenu items={menuItems} maxWidth={300} maxHeight={400}/>
134142
</View>
@@ -150,7 +158,7 @@ function AiSectionContent({id, content}: AiSectionContentProps): JSX.Element {
150158
const chatHistory = React.useContext(ChatHistoryContext);
151159
const firstResult = content.responses ? content.responses[0] : '';
152160
return (
153-
<AiSection copyValue={firstResult} id={id}>
161+
<AiSection copyValue={firstResult} id={id} pinned={content.pinned}>
154162
{(() => {
155163
switch (content.contentType) {
156164
case ChatContent.Error:

src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function App(): JSX.Element {
2626
const [showSettingsPopup, setShowSettingsPopup] = React.useState(false);
2727
const [showAboutPopup, setShowAboutPopup] = React.useState(false);
2828
const [readToMeVoice, setReadToMeVoice] = React.useState<string>('');
29+
const [systemInstructions, setSystemInstructions] = React.useState<string>('The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly. You may use markdown syntax in the response as appropriate.');
2930

3031
const isDarkMode = currentTheme === 'dark';
3132
const isHighContrast = false;
@@ -46,6 +47,8 @@ function App(): JSX.Element {
4647
setChatModel: setChatModel,
4748
readToMeVoice: readToMeVoice,
4849
setReadToMeVoice: setReadToMeVoice,
50+
systemInstructions: systemInstructions,
51+
setSystemInstructions: setSystemInstructions,
4952
};
5053

5154
const popups = {

src/Chat.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { SettingsContext } from './Settings';
1717
import { FluentButton as Button } from './FluentControls';
1818
import { Speak } from './Speech';
1919
import Clipboard from '@react-native-clipboard/clipboard';
20+
import { WelcomeMessage } from './WelcomeMessage';
2021

2122
enum ChatSource {
2223
Human,
@@ -35,6 +36,7 @@ type ChatElement = {
3536
prompt?: string;
3637
responses?: string[];
3738
content?: JSX.Element;
39+
pinned?: boolean;
3840
};
3941

4042
// Context for read-only access to the chat log
@@ -43,11 +45,13 @@ const ChatHistoryContext = React.createContext<{
4345
modifyResponse: (id: number, delta?: any) => void;
4446
deleteResponse: (id: number) => void;
4547
add: (response: ChatElement) => void;
48+
togglePin: (id: number) => void;
4649
}>({
4750
entries: [],
4851
modifyResponse: () => {},
4952
deleteResponse: () => {},
5053
add: () => {},
54+
togglePin: () => {},
5155
});
5256

5357
// Context for being able to drive the chat scroller
@@ -75,8 +79,6 @@ function ChatEntry({
7579
// If the user hits submit but the text is empty, don't carry that forward
7680
if (value !== '') {
7781
submit(value);
78-
// Reset to a blank prompt
79-
setValue('');
8082
}
8183
};
8284

@@ -89,6 +91,7 @@ function ChatEntry({
8991
onChangeText={newValue => setValue(newValue)}
9092
submitKeyEvents={[{code: 'Enter', shiftKey: false}]}
9193
onSubmitEditing={submitValue}
94+
clearTextOnSubmit={true}
9295
value={defaultText ?? value}
9396
/>
9497
<Button
@@ -158,11 +161,16 @@ function Chat({
158161

159162
const copyEntireChatLog = () => {
160163
const chatLogText = entries.map((entry) => {
161-
const content = entry.responses ? entry.responses[0] : '';
162164
if (entry.type === ChatSource.Human) {
165+
// Human prompt
166+
const content = entry.responses ? entry.responses[0] : '';
163167
return `Prompt: ${content}`;
164168
} else {
165-
return `OpenAI: ${content}`;
169+
// AI response
170+
if (entry.responses?.length > 0) {
171+
return `OpenAI: ${entry.responses[0]}`;
172+
}
173+
return 'OpenAI: [Loading...]';
166174
}
167175
}).join('\n\n');
168176

@@ -179,6 +187,7 @@ function Chat({
179187
style={{flexShrink: 1}}>
180188
<View
181189
style={{gap: 12}}>
190+
{entries.length === 0 && <WelcomeMessage />}
182191
{// For each item in the chat log, render the appropriate component
183192
entries.map((entry) => (
184193
<View key={entry.id}>
@@ -187,7 +196,8 @@ function Chat({
187196
// Human inputs are always plain text
188197
<HumanSection
189198
id={entry.id}
190-
content={entry.responses ? entry.responses[0] : ''}/> :
199+
content={entry.responses ? entry.responses[0] : ''}
200+
pinned={entry.pinned}/> :
191201
entry.content ?
192202
// The element may have provided its own UI
193203
entry.content :
@@ -228,7 +238,7 @@ function Chat({
228238
onPress: () => popups.setShowAbout(true),
229239
},
230240
{
231-
title: 'Copy all',
241+
title: 'Copy entire chat log',
232242
icon: 0xE8C8,
233243
onPress: () => copyEntireChatLog(),
234244
},

src/ChatSession.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ function ChatSession(): JSX.Element {
128128
if (delta.hasOwnProperty('contentType')) {entry.contentType = delta.contentType;}
129129
if (delta.hasOwnProperty('prompt')) {entry.prompt = delta.prompt;}
130130
if (delta.hasOwnProperty('intent')) {entry.intent = delta.intent;}
131+
if (delta.hasOwnProperty('pinned')) {entry.pinned = delta.pinned;}
131132

132133
modifiedEntries[index] = entry;
133134
setEntries(modifiedEntries);
@@ -143,18 +144,43 @@ function ChatSession(): JSX.Element {
143144
console.error(`Index ${index} is out of bounds`);
144145
} else {
145146
modifiedEntries.splice(index, 1);
146-
setEntries(modifiedEntries);
147+
// Re-index the remaining entries to maintain ID-to-index consistency
148+
const reindexedEntries = modifiedEntries.map((entry, newIndex) => ({
149+
...entry,
150+
id: newIndex
151+
}));
152+
setEntries(reindexedEntries);
147153
}
148154
},
149155
[entries],
150156
);
151157

158+
const togglePin = React.useCallback(
159+
(index: number) => {
160+
modifyEntry(index, { pinned: !entries[index]?.pinned });
161+
},
162+
[entries, modifyEntry],
163+
);
164+
152165
const clearConversation = async () => {
153-
setEntries([]);
154-
// Clear the stored chat data as well
166+
// Keep only pinned messages when clearing
167+
const pinnedEntries = entries.filter(entry => entry.pinned);
168+
// Re-index the pinned entries to maintain ID-to-index consistency
169+
const reindexedEntries = pinnedEntries.map((entry, index) => ({
170+
...entry,
171+
id: index
172+
}));
173+
setEntries(reindexedEntries);
174+
175+
// Update stored chat data with only pinned entries
155176
try {
156-
await AsyncStorage.removeItem(chatLogKey);
157-
console.debug('Cleared stored chat data');
177+
if (reindexedEntries.length > 0) {
178+
await SaveChatData(reindexedEntries);
179+
console.debug('Cleared non-pinned chat data');
180+
} else {
181+
await AsyncStorage.removeItem(chatLogKey);
182+
console.debug('Cleared stored chat data');
183+
}
158184
} catch (e) {
159185
console.error('Error clearing chat data:', e);
160186
}
@@ -166,6 +192,7 @@ function ChatSession(): JSX.Element {
166192
entries: entries,
167193
modifyResponse: modifyEntry,
168194
deleteResponse: deleteEntry,
195+
togglePin: togglePin,
169196
add: element => {
170197
element.id = entries.length;
171198
appendEntry(element);

src/FluentControls.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ type LinkProps = {
244244
}
245245

246246
const Link = (props: LinkProps) => {
247+
const styles = React.useContext(StylesContext);
248+
247249
const handlePress = () => {
248250
Linking.openURL(props.url).catch(err => {
249251
console.error('Failed to open URL:', err);
@@ -257,10 +259,10 @@ const Link = (props: LinkProps) => {
257259
style={({ pressed }) => ({
258260
opacity: pressed ? 0.8 : 1,
259261
})}>
260-
<Text style={{
262+
<Text style={[styles.text, {
261263
color: '#0078d4',
262264
textDecorationLine: 'underline',
263-
}}>
265+
}]}>
264266
{props.content}
265267
</Text>
266268
</Pressable>

src/HumanQuery.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ type HumanSectionProps = PropsWithChildren<{
1212
content?: string;
1313
disableCopy?: boolean;
1414
moreMenu?: FlyoutMenuButtonType[];
15+
pinned?: boolean;
1516
}>;
1617
function HumanSection({
1718
children,
1819
id,
1920
content,
2021
disableCopy,
2122
moreMenu,
23+
pinned = false,
2224
}: HumanSectionProps): JSX.Element {
2325
const styles = React.useContext(StylesContext);
2426
const chatHistory = React.useContext(ChatHistoryContext);
@@ -28,6 +30,12 @@ function HumanSection({
2830
menuItems.push(...moreMenu);
2931
}
3032
if (id !== undefined) {
33+
// Add pin/unpin option
34+
menuItems.push({
35+
title: pinned ? 'Unpin message' : 'Pin message',
36+
icon: 0xE718, // Pin icon
37+
onPress: () => chatHistory.togglePin(id)
38+
});
3139
menuItems.push(
3240
{title: 'Delete this response', icon: 0xE74D, onPress: () => chatHistory.deleteResponse(id)}
3341
);
@@ -47,7 +55,7 @@ function HumanSection({
4755
<Text
4856
accessibilityRole="header"
4957
style={[styles.sectionTitle, {flexGrow: 1}]}>
50-
Prompt
58+
{pinned ? '📌 ' : ''}Prompt
5159
</Text>
5260
<FlyoutMenu items={menuItems} maxWidth={300} maxHeight={400}/>
5361
</View>

src/OpenAI.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const OpenAiHandler = ({api, options, instructions}: OpenAiHandlerType) => {
2828

2929
let actualInstructions =
3030
instructions ??
31-
'The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly.';
31+
'The following is a conversation with an AI assistant. The assistant is helpful, creative, clever, and very friendly. You may use markdown syntax in the response as appropriate.';
3232

3333
switch (api) {
3434
case OpenAiApi.Completion:
@@ -128,7 +128,7 @@ const CallOpenAi = async ({
128128
let effectiveApiKey = await getEffectiveApiKey(apiKey);
129129

130130
if (!effectiveApiKey) {
131-
const errorMessage = 'To use this app, you need an OpenAI API key. Get one at https://platform.openai.com/account/api-keys and enter it in Settings.';
131+
const errorMessage = 'To use this app, you need an OpenAI API key. Get one at https://platform.openai.com/account/api-keys and enter it in the settings menu below (...).';
132132
onError(errorMessage);
133133
onComplete();
134134
return;

0 commit comments

Comments
 (0)