Skip to content

Commit e7b5b80

Browse files
Render data visualizations in the chat
1 parent 5a1040e commit e7b5b80

4 files changed

Lines changed: 568 additions & 13 deletions

File tree

routers/chat.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -338,27 +338,42 @@ async def iterate_stream(s, response_id: str = "") -> AsyncGenerator[str, None]:
338338
yield sse_format("textDelta", wrap_for_oob_swap(current_item_id, event.delta))
339339

340340
case ResponseOutputTextAnnotationAddedEvent():
341-
if event.annotation and current_item_id:
341+
# Use event.item_id (not current_item_id) because annotations
342+
# may fire after other output items (e.g. code interpreter)
343+
# have changed current_item_id to a different element.
344+
annotation_target_id = event.item_id or current_item_id
345+
if event.annotation and annotation_target_id:
342346
if event.annotation["type"] == "file_citation":
343347
filename = event.annotation["filename"]
344348
# Emit a literal HTML anchor to avoid markdown parsing edge cases
345349
encoded_filename = url_quote(filename, safe="")
346350
file_url_path = files_router.url_path_for("download_stored_file", file_name=encoded_filename)
347351
citation = f"(<a href=\"{file_url_path}\">†</a>)"
348-
yield sse_format("textDelta", wrap_for_oob_swap(current_item_id, citation))
352+
yield sse_format("textDelta", wrap_for_oob_swap(annotation_target_id, citation))
349353
elif event.annotation["type"] == "container_file_citation":
350354
container_id = event.annotation["container_id"]
351355
file_id = event.annotation["file_id"]
352-
file = await client.containers.files.retrieve(file_id, container_id=container_id)
353-
container_file_path = file.path
356+
filename = event.annotation.get("filename", "")
354357
file_url_path = files_router.url_path_for("download_container_file", container_id=container_id, file_id=file_id)
355-
replacement_payload = f"sandbox:{container_file_path}|{file_url_path}"
356-
yield sse_format("textReplacement", wrap_for_oob_swap(current_item_id, replacement_payload))
358+
# Check if the file is an image by extension
359+
image_extensions = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp")
360+
if filename.lower().endswith(image_extensions):
361+
img_html = (
362+
f'<div class="imageOutput">'
363+
f'<img src="{file_url_path}" alt="Code interpreter output" />'
364+
f'</div>'
365+
)
366+
yield sse_format("imageOutput", img_html)
367+
else:
368+
file = await client.containers.files.retrieve(file_id, container_id=container_id)
369+
container_file_path = file.path
370+
replacement_payload = f"sandbox:{container_file_path}|{file_url_path}"
371+
yield sse_format("textReplacement", wrap_for_oob_swap(annotation_target_id, replacement_payload))
357372
elif event.annotation["type"] == "url_citation":
358373
url = event.annotation["url"]
359374
title = event.annotation.get("title", url)
360375
citation = f'(<a href="{escape(url)}" target="_blank" rel="noopener noreferrer">{escape(title)}</a>)'
361-
yield sse_format("textDelta", wrap_for_oob_swap(current_item_id, citation))
376+
yield sse_format("textDelta", wrap_for_oob_swap(annotation_target_id, citation))
362377
else:
363378
logger.error(f"Unhandled annotation type: {event.annotation['type']}")
364379

routers/files.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import mimetypes
23
from typing import Literal
34
from fastapi import (
45
APIRouter, Request, UploadFile, File, HTTPException, Depends, Path, Form
@@ -248,14 +249,23 @@ async def download_container_file(
248249
client.base_url = f"https://api.openai.com/v1/containers/{container_id}"
249250
file_content = await client.files.content(file_id)
250251
client.base_url = "https://api.openai.com/v1"
251-
252+
252253
if not hasattr(file_content, 'content'):
253254
raise HTTPException(status_code=500, detail="File content not available")
254-
255-
# Use stream_file_content helper
255+
256+
filename = file.path.split("/")[-1] or file_id
257+
# Serve images inline with correct Content-Type so <img> tags work
258+
mime_type, _ = mimetypes.guess_type(filename)
259+
if mime_type and mime_type.startswith("image/"):
260+
return StreamingResponse(
261+
stream_file_content(file_content.content),
262+
media_type=mime_type,
263+
headers={"Content-Disposition": f'inline; filename="{filename}"'}
264+
)
265+
256266
return StreamingResponse(
257-
stream_file_content(file_content.content), # Assuming stream_file_content handles bytes
258-
headers={"Content-Disposition": f'attachment; filename="{file.path.split("/")[-1] or file_id}"'}
267+
stream_file_content(file_content.content),
268+
headers={"Content-Disposition": f'attachment; filename="{filename}"'}
259269
)
260270
except Exception as e:
261271
logger.error(f"Error downloading file {file_id} from OpenAI: {e}")

static/styles.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,8 @@ pre {
542542
overflow-wrap: break-word;
543543
}
544544

545-
.assistantMessage img {
545+
.assistantMessage img,
546+
.imageOutput img {
546547
max-width: 100%;
547548
margin: 8px 0px 8px 0px;
548549
border-radius: 8px;

0 commit comments

Comments
 (0)