Skip to content

Commit e772f6e

Browse files
authored
ChatInput Revamp (#574)
This PR revamps the design of `ChatInput` to better respond to error messages when sending the message or streaming a response. Error messages now appear right above the text input and there's logic to remove `optimisticMessage`s when the error occurs before the message is added to the thread.
1 parent f365336 commit e772f6e

15 files changed

Lines changed: 629 additions & 230 deletions

File tree

pingpong/ai.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,8 @@ async def run_thread(
320320
yield (
321321
orjson.dumps(
322322
{
323-
"type": "error",
324-
"detail": "OpenAI was unable to process your request. Please refresh the page and try again. If the issue persists, check https://pingpong-hks.statuspage.io/.",
323+
"type": "rate_limit_error",
324+
"detail": "OpenAI was unable to process your request. If the issue persists, check PingPong's status page for updates.",
325325
}
326326
)
327327
+ b"\n"
@@ -336,8 +336,8 @@ async def run_thread(
336336
# openai_error.message returns the entire error message in a string with all parameters. We can use the body to get the message if it exists, or we fall back to the whole thing.
337337
orjson.dumps(
338338
{
339-
"type": "error",
340-
"detail": "OpenAI was unable to process your request: "
339+
"type": "presend_error",
340+
"detail": "OpenAI was unable to process your request. "
341341
+ str(
342342
openai_error.body.get("message") or openai_error.message
343343
),
@@ -351,7 +351,7 @@ async def run_thread(
351351
except (ValueError, Exception) as e:
352352
try:
353353
logger.warning(f"Error adding new thread message: {e}")
354-
yield orjson.dumps({"type": "error", "detail": str(e)}) + b"\n"
354+
yield orjson.dumps({"type": "presend_error", "detail": str(e)}) + b"\n"
355355
except Exception as e_:
356356
logger.exception(f"Error writing to stream: {e_}")
357357
pass

pingpong/schemas.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,15 @@ class UserGroup(BaseModel):
460460
explanation: list[list[str]] | None
461461

462462

463+
class SupervisorUser(BaseModel):
464+
name: str | None = None
465+
email: str
466+
467+
468+
class ClassSupervisors(BaseModel):
469+
users: list[SupervisorUser]
470+
471+
463472
class ClassUsers(BaseModel):
464473
users: list[ClassUser]
465474
limit: int

pingpong/server.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,36 @@ async def remove_canvas_connection(
11121112
return {"status": "ok"}
11131113

11141114

1115+
@v1.get(
1116+
"/class/{class_id}/supervisors",
1117+
dependencies=[Depends(Authz("can_view", "class:{class_id}"))],
1118+
response_model=schemas.ClassSupervisors,
1119+
)
1120+
async def list_class_supervisors(class_id: str, request: Request):
1121+
supervisor_ids = await request.state.authz.list_entities(
1122+
f"class:{class_id}",
1123+
"supervisor",
1124+
"user",
1125+
)
1126+
supervisors = await models.User.get_all_by_id(request.state.db, supervisor_ids)
1127+
supervisors_users = []
1128+
for supervisor in supervisors:
1129+
supervisors_users.append(
1130+
schemas.SupervisorUser(
1131+
name=(
1132+
supervisor.display_name
1133+
if supervisor.display_name
1134+
else " ".join(
1135+
filter(None, [supervisor.first_name, supervisor.last_name])
1136+
)
1137+
or None
1138+
),
1139+
email=supervisor.email,
1140+
)
1141+
)
1142+
return {"users": supervisors_users}
1143+
1144+
11151145
@v1.get(
11161146
"/class/{class_id}/users",
11171147
dependencies=[Depends(Authz("can_view_users", "class:{class_id}"))],
@@ -2057,11 +2087,11 @@ async def create_run(
20572087
message=[],
20582088
file_names=file_names,
20592089
)
2060-
except Exception:
2090+
except Exception as e:
20612091
logger.exception("Error running thread")
20622092
raise HTTPException(
20632093
status_code=500,
2064-
detail="We faced an error while sending your message.",
2094+
detail="We faced an error while sending your message. " + str(e),
20652095
)
20662096

20672097
return StreamingResponse(stream, media_type="text/event-stream")
@@ -2209,7 +2239,7 @@ async def send_message(
22092239
logger.exception("Error running thread")
22102240
raise HTTPException(
22112241
status_code=500,
2212-
detail="We faced an error while sending your message.",
2242+
detail="We faced an error while sending your message. Please try again later.",
22132243
)
22142244
return StreamingResponse(stream, media_type="text/event-stream")
22152245

web/pingpong/src/lib/api.ts

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,20 @@ export type ValidationError = {
3939
}[];
4040
};
4141

42+
export class PresendError extends Error {
43+
constructor(message: string) {
44+
super(message);
45+
this.name = 'PresendError';
46+
}
47+
}
48+
49+
export class StreamError extends Error {
50+
constructor(message: string) {
51+
super(message);
52+
this.name = 'StreamError';
53+
}
54+
}
55+
4256
/**
4357
* Error response. The $status will be >= 400.
4458
*/
@@ -1099,9 +1113,15 @@ const _doUpload = (
10991113
resolve(info.response);
11001114
} else {
11011115
info.state = 'error';
1102-
info.response = {
1103-
error: xhr.responseText ? JSON.parse(xhr.responseText) : { detail: '' }
1104-
};
1116+
if (xhr.responseText) {
1117+
try {
1118+
info.response = { error: JSON.parse(xhr.responseText) };
1119+
} catch {
1120+
info.response = { error: { detail: xhr.responseText } };
1121+
}
1122+
} else {
1123+
info.response = { error: { detail: 'Unknown error.' } };
1124+
}
11051125
reject(info.response);
11061126
}
11071127
}
@@ -1216,6 +1236,24 @@ export const getClassUsers = async (f: Fetcher, classId: number, opts?: GetClass
12161236
};
12171237
};
12181238

1239+
export type ClassSupervisors = {
1240+
users: SupervisorUser[];
1241+
};
1242+
1243+
export type SupervisorUser = {
1244+
name: string | null;
1245+
email: string;
1246+
};
1247+
1248+
/**
1249+
* Fetch teachers in a class.
1250+
*
1251+
*/
1252+
export const getSupervisors = async (f: Fetcher, classId: number) => {
1253+
const url = `class/${classId}/supervisors`;
1254+
return await GET<never, ClassSupervisors>(f, url);
1255+
};
1256+
12191257
/**
12201258
* Response type for getClassUsers.
12211259
*/
@@ -1725,6 +1763,16 @@ export type ThreadStreamErrorChunk = {
17251763
detail: string;
17261764
};
17271765

1766+
export type ThreadPreSendErrorChunk = {
1767+
type: 'presend_error';
1768+
detail: string;
1769+
};
1770+
1771+
export type ThreadServerErrorChunk = {
1772+
type: 'server_error';
1773+
detail: string;
1774+
};
1775+
17281776
export type ThreadStreamDoneChunk = {
17291777
type: 'done';
17301778
};
@@ -1733,7 +1781,7 @@ export type ThreadHTTPErrorChunk = {
17331781
detail: string;
17341782
};
17351783

1736-
export type ThreadValidationErrorChunk = {
1784+
export type ThreadValidationError = {
17371785
detail: {
17381786
loc: string[];
17391787
msg: string;
@@ -1745,6 +1793,8 @@ export type ThreadStreamChunk =
17451793
| ThreadStreamMessageDeltaChunk
17461794
| ThreadStreamMessageCreatedChunk
17471795
| ThreadStreamErrorChunk
1796+
| ThreadPreSendErrorChunk
1797+
| ThreadServerErrorChunk
17481798
| ThreadStreamDoneChunk
17491799
| ThreadStreamToolCallCreatedChunk
17501800
| ThreadStreamToolCallDeltaChunk;
@@ -1761,33 +1811,36 @@ const streamThreadChunks = (res: Response) => {
17611811
.pipeThrough(new TextLineStream())
17621812
.pipeThrough(new JSONStream());
17631813
const reader = stream.getReader();
1764-
if (res.status !== 200 && res.status !== 422) {
1814+
if (res.status === 422) {
17651815
return {
17661816
stream,
17671817
reader,
17681818
async *[Symbol.asyncIterator]() {
17691819
const error = await reader.read();
1770-
const error_ = error.value as ThreadHTTPErrorChunk;
1771-
yield { type: 'error', detail: error_.detail } as ThreadStreamErrorChunk;
1820+
const error_ = error.value as ThreadValidationError;
1821+
const message = error_.detail
1822+
.map((error) => {
1823+
const location = error.loc.join(' -> ');
1824+
return `Error at ${location}: ${error.msg}`;
1825+
})
1826+
.join('\n');
1827+
yield {
1828+
type: 'presend_error',
1829+
detail: `We were unable to send your message, because it was not accepted by our server: ${message}`
1830+
} as ThreadPreSendErrorChunk;
17721831
}
17731832
};
1774-
} else if (res.status === 422) {
1833+
} else if (res.status !== 200) {
17751834
return {
17761835
stream,
17771836
reader,
17781837
async *[Symbol.asyncIterator]() {
17791838
const error = await reader.read();
1780-
const error_ = error.value as ThreadValidationErrorChunk;
1781-
const message = error_.detail
1782-
.map((error) => {
1783-
const location = error.loc.join(' -> ');
1784-
return `Error at ${location}: ${error.msg}`;
1785-
})
1786-
.join('\n');
1839+
const error_ = error.value as ThreadHTTPErrorChunk;
17871840
yield {
1788-
type: 'error',
1789-
detail: `Your request was invalid: ${message}.`
1790-
} as ThreadStreamErrorChunk;
1841+
type: 'presend_error',
1842+
detail: `We were unable to send your message: ${error_.detail}`
1843+
} as ThreadPreSendErrorChunk;
17911844
}
17921845
};
17931846
}

0 commit comments

Comments
 (0)