Skip to content

Commit d6e07cf

Browse files
authored
Merge pull request #55 from chrisglein/imageFix
Restore ability to opt-out of image responses
2 parents 55870be + cd35b4c commit d6e07cf

File tree

7 files changed

+133
-120
lines changed

7 files changed

+133
-120
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ There is a feedback dialog on each AI response. This is not yet implemented.
3636

3737
<img width="255" alt="image" src="https://user-images.githubusercontent.com/26607885/223870036-e8100f80-3360-4114-abb7-52e284039063.png">
3838

39-
As is the "Regenerate response" button.
39+
### Regenerate
40+
Pressing this button will result in a new OpenAI query for the previous response.
4041

4142
<img width="138" alt="image" src="https://user-images.githubusercontent.com/26607885/223875964-0a46490d-7d9a-4fe3-b57b-46ad7042e57d.png">
4243

@@ -61,9 +62,9 @@ The app's settings are handled by a `SettingsContext` object, which has dialog U
6162
| File | Type | Information |
6263
| --- | --- | --- |
6364
| App.tsx | `App` | Root of the app, publishes the `StylesContext` and `SettingsContext` |
64-
| AppContent.tsx | `ChatSession` | Owns the `ChatElement` list, and handles any writes to that list |
65-
| AppContent.tsx | `AutomatedChatSession` | Populates the `ChatSession` with either scripted responses or by creating components that query OpenAi |
66-
| Chat.tsx | `Chat` | The scrolling list of chat entries. Publishes the `FeedbackContext`, `ChatHistoryContext`, and `ChatScrollContext` services. Hosts a `ChatEntry` for the user input. Hosts the dialogs of the app (`FeedbackPopup` and `SettingsPopups`).
65+
| ChatSession.tsx | `ChatSession` | Owns the `ChatElement` list, publishes the `ChatHistoryContext`, and handles any writes to that list |
66+
| ChatSession.tsx | `AutomatedChatSession` | Populates the `ChatSession` with either scripted responses or by creating components that query OpenAi |
67+
| Chat.tsx | `Chat` | The scrolling list of chat entries. Publishes the `FeedbackContext`, and `ChatScrollContext` services. Hosts a `ChatEntry` for the user input. Hosts the dialogs of the app (`FeedbackPopup` and `SettingsPopups`).
6768
| Chat.tsx | `ChatEntry` | Takes in the user's text input |
6869
| Feedback.tsx | `FeedbackPopup` | Popup for giving feedback on AI responses |
6970
| Settings.tsx | `SettingsPopup` | Popup that shows controls for modifying the `SettingsContext` |

src/AiQuery.tsx

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import {
44
OpenAiApi,
55
CallOpenAi,
66
} from './OpenAI';
7-
import {
8-
AiSection,
9-
AiImageResponse,
10-
AiTextResponse,
11-
} from './AiResponse';
7+
import { AiSection } from './AiResponse';
128
import {
139
ChatSource,
1410
ChatContent,
@@ -20,34 +16,36 @@ import { SettingsContext } from './Settings';
2016
// Component that drives the queries to OpenAi to respond to a prompt
2117
type AiSectionWithQueryProps = {
2218
prompt: string;
19+
intent?: string;
2320
id: number;
24-
onResponse: ({prompt, response, contentType} : { prompt: string, response: string, contentType: ChatContent} ) => void;
21+
onResponse: ({prompt, intent, response, contentType} : { prompt: string, intent?: string, response: string, contentType: ChatContent} ) => void;
2522
};
26-
function AiSectionWithQuery({prompt, id, onResponse}: AiSectionWithQueryProps): JSX.Element {
23+
function AiSectionWithQuery({prompt, intent, id, onResponse}: AiSectionWithQueryProps): JSX.Element {
2724
const settingsContext = React.useContext(SettingsContext);
2825
const chatScroll = React.useContext(ChatScrollContext);
2926
const chatHistory = React.useContext(ChatHistoryContext);
3027
const [isLoading, setIsLoading] = React.useState(true);
31-
const [queryResult, setQueryResult] = React.useState<string | undefined>(undefined);
32-
const [error, setError] = React.useState<string | undefined>(undefined);
3328
const [isRequestForImage, setIsRequestForImage] = React.useState<boolean | undefined>(undefined);
3429
const [imagePrompt, setImagePrompt] = React.useState<string | undefined>(undefined);
3530

3631
// First determine the intent of the prompt
3732
const imageIntentSentinel = "[IMAGE]";
3833
React.useEffect(() => {
3934
setIsLoading(true);
40-
setError(undefined);
4135
setIsRequestForImage(undefined);
4236
setImagePrompt(undefined);
37+
if (intent === 'text') {
38+
setIsRequestForImage(false);
39+
return;
40+
}
4341
CallOpenAi({
4442
api: OpenAiApi.Completion,
4543
apiKey: settingsContext.apiKey,
4644
instructions: `You are an intuitive assistant helping the user with a project. Your only job is need to determine the primary intent of the user's last prompt.
4745
If and only if you are absolutely certain the user's primary intent is to see an image, respond with exactly the string "${imageIntentSentinel}". Otherwise, respond with your description of their intent.`,
4846
identifier: "INTENT:",
4947
prompt: prompt,
50-
onError: (error) => {
48+
onError: () => {
5149
setIsRequestForImage(false);
5250
},
5351
onResult: (result) => {
@@ -83,7 +81,7 @@ Where items enclosed in brackets ([]) would be replaced with an appropriate sugg
8381
Respond with the image prompt string in the required format. Do not respond conversationally.`,
8482
identifier: "KEYWORDS:",
8583
prompt: prompt,
86-
onError: (error) => {
84+
onError: () => {
8785
setIsRequestForImage(false);
8886
},
8987
onResult: (result) => {
@@ -107,14 +105,12 @@ Respond with the image prompt string in the required format. Do not respond conv
107105
filter((entry) => { return entry.text !== undefined && entry.id < id; }).
108106
map((entry) => { return {role: entry.type == ChatSource.Human ? "user" : "assistant", "content": entry.text ?? ""} }),
109107
onError: (error) => {
110-
setError(error);
111108
onResponse({
112109
prompt: prompt,
113110
response: error ?? "",
114111
contentType: ChatContent.Error});
115112
},
116113
onResult: (result) => {
117-
setQueryResult(result);
118114
onResponse({
119115
prompt: prompt,
120116
response: result ?? "",
@@ -133,14 +129,12 @@ Respond with the image prompt string in the required format. Do not respond conv
133129
identifier: "IMAGE-ANSWER:",
134130
prompt: imagePrompt,
135131
onError: (error) => {
136-
setError(error);
137132
onResponse({
138133
prompt: imagePrompt,
139134
response: error ?? "",
140135
contentType: ChatContent.Error});
141136
},
142137
onResult: (result) => {
143-
setQueryResult(result);
144138
onResponse({
145139
prompt: imagePrompt,
146140
response: result ?? "",
@@ -157,20 +151,16 @@ Respond with the image prompt string in the required format. Do not respond conv
157151
return (
158152
<AiSection isLoading={isLoading}>
159153
{
160-
// TODO: All of this can go away now, once images are working in new model
161-
(isLoading || error) ?
162-
<Text style={{color: 'crimson'}}>{error}</Text>
163-
: isRequestForImage ?
164-
<AiImageResponse
165-
imageUrl={queryResult}
166-
prompt={imagePrompt}
167-
rejectImage={() => {
168-
setIsRequestForImage(false);
169-
setQueryResult(undefined);
170-
}}/>
171-
: // Not an error, not an image
172-
<AiTextResponse
173-
text={queryResult}/>
154+
(isLoading ?
155+
isRequestForImage === undefined ?
156+
<Text>Identifying intent...</Text> :
157+
isRequestForImage === true ?
158+
imagePrompt === undefined ?
159+
<Text>Generating keywords for an image...</Text> :
160+
<Text>Generating image...</Text> :
161+
<Text>Generating text...</Text>
162+
:
163+
<Text>Done loading</Text>)
174164
}
175165
</AiSection>
176166
)

src/AiResponse.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
} from './Controls';
1515
import {
1616
ChatElement,
17-
ChatContent
17+
ChatContent,
18+
ChatHistoryContext
1819
} from './Chat';
1920
import { StylesContext } from './Styles';
2021
import { FeedbackContext } from './Feedback';
@@ -38,7 +39,7 @@ function AiImageResponse({imageUrl, prompt, rejectImage}: AiImageResponseProps):
3839
<View style={{alignSelf: 'flex-end', alignItems: 'flex-end'}}>
3940
<Button
4041
title="I didn't want to see an image"
41-
onPress={() => {rejectImage()}}/>
42+
onPress={() => rejectImage()}/>
4243
<Button
4344
title="Show me more"
4445
onPress={() => console.log("Not yet implemented")}/>
@@ -143,9 +144,11 @@ function AiSection({children, isLoading, contentShownOnHover}: AiSectionProps):
143144
}
144145

145146
type AiSectionContentProps = {
147+
id: number,
146148
content: ChatElement;
147149
}
148-
function AiSectionContent({content}: AiSectionContentProps): JSX.Element {
150+
function AiSectionContent({id, content}: AiSectionContentProps): JSX.Element {
151+
const chatHistory = React.useContext(ChatHistoryContext);
149152
return (
150153
<AiSection>
151154
{(() => {
@@ -156,7 +159,7 @@ function AiSectionContent({content}: AiSectionContentProps): JSX.Element {
156159
return <AiImageResponse
157160
imageUrl={content.text}
158161
prompt={content.prompt}
159-
rejectImage={() => console.log("Not yet implemented")}/>; // TODO: This would need to reset back to the text prompt
162+
rejectImage={() => chatHistory.modifyResponse(id, {intent: 'text', text: undefined})}/>;
160163
default:
161164
case ChatContent.Text:
162165
return <AiTextResponse text={content.text}/>

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import { Appearance } from 'react-native';
3-
import { ChatSession } from './AppContent';
3+
import { ChatSession } from './ChatSession';
44
import {
55
StylesContext,
66
CreateStyles,

src/Chat.tsx

Lines changed: 76 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React from 'react';
22
import {
33
Button,
44
ScrollView,
5-
Text,
65
TextInput,
76
View,
87
} from 'react-native';
@@ -30,6 +29,7 @@ type ChatElement = {
3029
id: number;
3130
type: ChatSource;
3231
contentType: ChatContent;
32+
intent?: string;
3333
prompt?: string;
3434
text?: string;
3535
content?: JSX.Element;
@@ -38,7 +38,11 @@ type ChatElement = {
3838
// Context for read-only access to the chat log
3939
const ChatHistoryContext = React.createContext<{
4040
entries: ChatElement[];
41-
}>({entries: []});
41+
modifyResponse: (id: number, delta?: any) => void;
42+
}>({
43+
entries: [],
44+
modifyResponse: () => {},
45+
});
4246

4347
// Context for being able to drive the chat scroller
4448
const ChatScrollContext = React.createContext<{
@@ -93,12 +97,11 @@ type ChatProps = {
9397
entries: ChatElement[];
9498
humanText? : string;
9599
onPrompt: (prompt: string) => void;
96-
onResponse: ({prompt, response, contentType, entryId} : { prompt: string, response: string, contentType: ChatContent, entryId: number} ) => void;
97-
regenerateResponse: () => void;
98100
clearConversation: () => void;
99101
};
100-
function Chat({entries, humanText, onPrompt, onResponse, regenerateResponse, clearConversation}: ChatProps): JSX.Element {
102+
function Chat({entries, humanText, onPrompt, clearConversation}: ChatProps): JSX.Element {
101103
const styles = React.useContext(StylesContext);
104+
const chatHistory = React.useContext(ChatHistoryContext);
102105
const [showFeedbackPopup, setShowFeedbackPopup] = React.useState(false);
103106
const [showSettingsPopup, setShowSettingsPopup] = React.useState(false);
104107
const [feedbackIsPositive, setFeedbackIsPositive] = React.useState(false);
@@ -120,70 +123,77 @@ function Chat({entries, humanText, onPrompt, onResponse, regenerateResponse, cle
120123

121124
return (
122125
<FeedbackContext.Provider value={feedbackContext}>
123-
<ChatHistoryContext.Provider value={{entries: entries}}>
124-
<ChatScrollContext.Provider value={{scrollToEnd: scrollToEnd}}>
125-
<View style={styles.appContent}>
126-
<ScrollView
127-
contentInsetAdjustmentBehavior="automatic"
128-
ref={scrollViewRef}
129-
style={{flexShrink: 1}}>
130-
<View
131-
style={{gap: 12}}>
132-
{// For each item in the chat log, render the appropriate component
133-
entries.map((entry) => (
134-
<View key={entry.id}>
135-
{
136-
entry.type === ChatSource.Human ?
137-
// Human inputs are always plain text
138-
<HumanSection content={entry.text}/> :
139-
entry.content ?
140-
// The element may have provided its own UI
141-
entry.content :
142-
// Otherwise, either render the completed query or start a query to get the resolved text
143-
entry.text ?
144-
<AiSectionContent content={entry}/> :
145-
<AiSectionWithQuery
146-
id={entry.id}
147-
prompt={entry.prompt ?? ""}
148-
onResponse={({prompt, response, contentType}) => onResponse({prompt: prompt, response: response, contentType: contentType, entryId: entry.id})}/>
149-
}
150-
</View>
151-
))}
152-
{(entries.length > 0) &&
153-
<View style={{alignSelf: 'center'}}>
154-
<Button title="🔁 Regenerate response" onPress={() => regenerateResponse()}/>
155-
</View>
156-
}
157-
</View>
158-
</ScrollView>
126+
<ChatScrollContext.Provider value={{scrollToEnd: scrollToEnd}}>
127+
<View style={styles.appContent}>
128+
<ScrollView
129+
contentInsetAdjustmentBehavior="automatic"
130+
ref={scrollViewRef}
131+
style={{flexShrink: 1}}>
159132
<View
160-
style={{flexShrink: 0, marginBottom: 12}}>
161-
<HumanSection
162-
disableEdit={true}
163-
disableCopy={true}
164-
contentShownOnHover={
165-
<HoverButton content="⚙️" onPress={() => setShowSettingsPopup(true)}/>
166-
}>
167-
<ChatEntry
168-
defaultText={humanText}
169-
submit={(newEntry) => {
170-
onPrompt(newEntry);
171-
scrollToEnd();
172-
}}
173-
clearConversation={clearConversation}/>
174-
</HumanSection>
133+
style={{gap: 12}}>
134+
{// For each item in the chat log, render the appropriate component
135+
entries.map((entry) => (
136+
<View key={entry.id}>
137+
{
138+
entry.type === ChatSource.Human ?
139+
// Human inputs are always plain text
140+
<HumanSection content={entry.text}/> :
141+
entry.content ?
142+
// The element may have provided its own UI
143+
entry.content :
144+
// Otherwise, either render the completed query or start a query to get the resolved text
145+
entry.text ?
146+
<AiSectionContent
147+
id={entry.id}
148+
content={entry}/> :
149+
<AiSectionWithQuery
150+
id={entry.id}
151+
prompt={entry.prompt ?? ""}
152+
intent={entry.intent}
153+
onResponse={({prompt, response, contentType}) =>
154+
chatHistory.modifyResponse(entry.id, {prompt: prompt, text: response, contentType: contentType})}/>
155+
}
156+
</View>
157+
))}
158+
{(entries.length > 0) &&
159+
<View style={{alignSelf: 'center'}}>
160+
<Button
161+
title="🔁 Regenerate response"
162+
onPress={() => {
163+
// Clear the response for the last entry
164+
chatHistory.modifyResponse(entries.length - 1, {text: undefined});
165+
}}/>
166+
</View>
167+
}
175168
</View>
176-
{ (showFeedbackPopup || showSettingsPopup) && <View style={styles.popupBackground}/> }
177-
<FeedbackPopup
178-
show={showFeedbackPopup}
179-
isPositive={feedbackIsPositive}
180-
close={() => setShowFeedbackPopup(false)}/>
181-
<SettingsPopup
182-
show={showSettingsPopup}
183-
close={() => setShowSettingsPopup(false)}/>
169+
</ScrollView>
170+
<View
171+
style={{flexShrink: 0, marginBottom: 12}}>
172+
<HumanSection
173+
disableEdit={true}
174+
disableCopy={true}
175+
contentShownOnHover={
176+
<HoverButton content="⚙️" onPress={() => setShowSettingsPopup(true)}/>
177+
}>
178+
<ChatEntry
179+
defaultText={humanText}
180+
submit={(newEntry) => {
181+
onPrompt(newEntry);
182+
scrollToEnd();
183+
}}
184+
clearConversation={clearConversation}/>
185+
</HumanSection>
184186
</View>
185-
</ChatScrollContext.Provider>
186-
</ChatHistoryContext.Provider>
187+
{ (showFeedbackPopup || showSettingsPopup) && <View style={styles.popupBackground}/> }
188+
<FeedbackPopup
189+
show={showFeedbackPopup}
190+
isPositive={feedbackIsPositive}
191+
close={() => setShowFeedbackPopup(false)}/>
192+
<SettingsPopup
193+
show={showSettingsPopup}
194+
close={() => setShowSettingsPopup(false)}/>
195+
</View>
196+
</ChatScrollContext.Provider>
187197
</FeedbackContext.Provider>
188198
);
189199
}

0 commit comments

Comments
 (0)