Skip to content

Commit b728711

Browse files
Make code interpreter files available for download from a file shelf
1 parent 6e2c68f commit b728711

5 files changed

Lines changed: 580 additions & 3 deletions

File tree

routers/chat.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from openai.types.responses.response_output_item import McpApprovalRequest
3030
from openai.types.responses import ResponseFunctionToolCall
31+
from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall
3132
from openai._types import NOT_GIVEN
3233
from openai import AsyncOpenAI
3334
from utils.function_calling import Context, FunctionResult
@@ -361,7 +362,7 @@ async def iterate_stream(s, response_id: str = "") -> AsyncGenerator[str, None]:
361362
if filename.lower().endswith(image_extensions):
362363
img_html = (
363364
f'<div class="imageOutput">'
364-
f'<img src="{file_url_path}" alt="Code interpreter output" />'
365+
f'<img src="{file_url_path}" alt="Code interpreter output" onclick="openImagePreview(this.src)" style="cursor:pointer" />'
365366
f'</div>'
366367
)
367368
yield sse_format("imageOutput", img_html)
@@ -389,7 +390,34 @@ async def iterate_stream(s, response_id: str = "") -> AsyncGenerator[str, None]:
389390
yield sse_format("toolDelta", wrap_for_oob_swap(current_item_id, str(delta)))
390391

391392
case ResponseOutputItemDoneEvent():
392-
if isinstance(event.item, ResponseFunctionToolCall):
393+
if isinstance(event.item, ResponseCodeInterpreterToolCall):
394+
container_id = event.item.container_id
395+
if container_id:
396+
try:
397+
container_files = await client.containers.files.list(container_id=container_id)
398+
cards: list[str] = []
399+
for f in container_files.data:
400+
if f.source != "assistant":
401+
continue
402+
filename = f.path.split("/")[-1]
403+
file_url = files_router.url_path_for(
404+
"download_container_file",
405+
container_id=container_id,
406+
file_id=f.id,
407+
)
408+
is_image = filename.lower().endswith(
409+
(".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp")
410+
)
411+
cards.append(templates.get_template("components/file-card.html").render(
412+
file_url=file_url, filename=filename, is_image=is_image,
413+
))
414+
if cards:
415+
carousel_html = f'<div hx-swap-oob="innerHTML:#file-carousel">{"".join(cards)}</div>'
416+
yield sse_format("fileOutput", carousel_html)
417+
except Exception as e:
418+
logger.error(f"Error listing container files: {e}")
419+
420+
elif isinstance(event.item, ResponseFunctionToolCall):
393421
current_item_id = event.item.id
394422
function_name = event.item.name
395423
arguments_json = json.loads(event.item.arguments)

static/styles.css

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ pre {
591591
padding: 10px;
592592
display: flex;
593593
flex-direction: column;
594-
order: 2;
594+
order: 3;
595595
}
596596

597597
.userMessage,
@@ -1006,6 +1006,63 @@ pre {
10061006
.dots div:nth-child(2){animation-delay:-.16s}
10071007
@keyframes bounce{0%,80%,100%{transform:scale(0)}40%{transform:scale(1)}}
10081008

1009+
/* File carousel for code interpreter artifacts */
1010+
.fileCarousel {
1011+
display: flex;
1012+
flex-wrap: wrap;
1013+
gap: 8px;
1014+
padding: 8px 10px;
1015+
max-width: 600px;
1016+
width: 100%;
1017+
order: 2;
1018+
}
1019+
1020+
.fileCarousel:empty {
1021+
display: none;
1022+
padding: 0;
1023+
}
1024+
1025+
.fileCard {
1026+
display: flex;
1027+
flex-direction: column;
1028+
align-items: center;
1029+
gap: 4px;
1030+
padding: 8px;
1031+
border: 1px solid #e2e2e2;
1032+
border-radius: 8px;
1033+
background: #f9f9f9;
1034+
max-width: 140px;
1035+
text-decoration: none;
1036+
color: inherit;
1037+
transition: background 0.2s;
1038+
}
1039+
1040+
.fileCard:hover {
1041+
background: #efefef;
1042+
}
1043+
1044+
.fileCard img {
1045+
max-width: 120px;
1046+
max-height: 80px;
1047+
border-radius: 4px;
1048+
object-fit: cover;
1049+
cursor: pointer;
1050+
}
1051+
1052+
.fileCardIcon {
1053+
font-size: 2em;
1054+
line-height: 1;
1055+
color: #666;
1056+
}
1057+
1058+
.fileCardName {
1059+
font-size: 0.75em;
1060+
color: #555;
1061+
text-align: center;
1062+
word-break: break-all;
1063+
max-width: 120px;
1064+
}
1065+
10091066
/* Optional: Adjust button padding if loader makes it too wide/narrow */
10101067
/*
10111068
.button {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<a class="fileCard" href="{{ file_url }}" {% if not is_image %}download{% endif %} target="_blank" rel="noopener noreferrer">
2+
{% if is_image %}
3+
<img src="{{ file_url }}" alt="{{ filename }}" onclick="event.preventDefault(); openImagePreview(this.src)" />
4+
{% else %}
5+
<span class="fileCardIcon">&#128196;</span>
6+
{% endif %}
7+
<span class="fileCardName">{{ filename }}</span>
8+
</a>

templates/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
{% endif %}
2424
{% endfor %}
2525
</div>
26+
<div class="fileCarousel" id="file-carousel"></div>
2627
<form id="chatForm" class="inputForm clearfix"
2728
hx-on::after-request="this.reset(); clearImagePreview();"
2829
hx-on::before-request="removeNetworkError(); disableSendButton()"

0 commit comments

Comments
 (0)