Skip to content

Commit 397bb4a

Browse files
authored
feat: added optional code interpreter tool (#102)
This pull request introduces support for OpenAI's Code Interpreter tool, enhancing the application's ability to execute Python code securely and process data dynamically as well as generate annoted files for download. Closes #78 ![CleanShot 2025-07-05 at 23 56 13](https://github.com/user-attachments/assets/ffae7551-1bb6-4b93-ab3a-c7bd2dc66f72)
1 parent 33c3dc3 commit 397bb4a

26 files changed

+2378
-155
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"class-variance-authority": "^0.7.1",
3838
"clsx": "^2.1.1",
3939
"lucide-react": "^0.476.0",
40+
"mime": "^4.0.7",
4041
"openai": "^4.103.0",
4142
"react": "^19.0.0",
4243
"react-dom": "^19.0.0",

src/components/BotError.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AlertTriangle, ChevronRight } from 'lucide-react'
2+
import { CollapsibleSection } from './ui/collapsible-section'
23

34
interface BotErrorProps {
45
message: string
@@ -11,16 +12,14 @@ export function BotError({ message }: BotErrorProps) {
1112
<AlertTriangle className="h-5 w-5" />
1213
</div>
1314
<div className="flex flex-col space-y-1 items-start w-full sm:w-[85%] md:w-[75%] lg:w-[65%]">
14-
<details
15-
open
16-
className="rounded-2xl px-4 py-2 text-sm w-full group [&:not([open])]:h-8 [&:not([open])]:flex [&:not([open])]:items-center [&:not([open])]:py-0 bg-red-50 dark:bg-red-950"
15+
<CollapsibleSection
16+
title="Error"
17+
icon={<AlertTriangle className="h-4 w-4" />}
18+
variant="red"
19+
open={true}
1720
>
18-
<summary className="font-medium mb-1 flex items-center gap-2 list-none [&::-webkit-details-marker]:hidden cursor-pointer group-[&:not([open])]:mb-0">
19-
<ChevronRight className="h-4 w-4 transition-transform group-open:rotate-90" />
20-
<span>Error</span>
21-
</summary>
2221
<div>{message}</div>
23-
</details>
22+
</CollapsibleSection>
2423
</div>
2524
</div>
2625
)

src/components/BotMessage.tsx

Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,57 @@
1-
import { cn } from '../lib/utils'
2-
import type { Message } from '../mcp/client'
3-
import { formatTimestamp } from '../lib/utils'
4-
import { Bot, Copy } from 'lucide-react'
5-
import { MarkdownContent } from './MarkdownContent'
6-
1+
import { Bot, Copy, Download } from 'lucide-react'
72
import { toast } from 'sonner'
8-
import { copyToClipboard } from '../lib/utils/clipboard'
3+
import { useState } from 'react'
4+
import { cn } from '@/lib/utils'
5+
import type { Message } from '@/mcp/client'
6+
import { formatTimestamp } from '@/lib/utils'
7+
import { MarkdownContent } from '@/components/MarkdownContent'
8+
import { copyToClipboard } from '@/lib/utils/clipboard'
9+
import {
10+
isImageFile,
11+
createAnnotatedFileUrl,
12+
} from '@/lib/utils/code-interpreter'
913

1014
export interface BotMessageProps {
1115
message: Message
1216
isLoading?: boolean
17+
fileAnnotations?: Array<{
18+
type: string
19+
container_id: string
20+
file_id: string
21+
filename: string
22+
}>
1323
}
1424

15-
export function BotMessage({ message, isLoading }: BotMessageProps) {
25+
export function BotMessage({
26+
message,
27+
isLoading,
28+
fileAnnotations = [],
29+
}: BotMessageProps) {
30+
const [imageErrors, setImageErrors] = useState<Set<string>>(new Set())
31+
1632
const handleCopy = async () => {
17-
const copied = await copyToClipboard(message.content)
33+
const copied = await copyToClipboard(processedContent)
1834

1935
if (copied) {
2036
toast.success('Copied message to clipboard')
2137
}
2238
}
2339

40+
const handleImageError = (fileId: string) => {
41+
setImageErrors((prev) => new Set(prev).add(fileId))
42+
}
43+
44+
// Use message content directly; sandbox URL replacement is not needed
45+
const processedContent = message.content
46+
47+
// Separate image and non-image files
48+
const imageFiles = fileAnnotations.filter((annotation) =>
49+
isImageFile(annotation.filename),
50+
)
51+
const otherFiles = fileAnnotations.filter(
52+
(annotation) => !isImageFile(annotation.filename),
53+
)
54+
2455
return (
2556
<div
2657
className={cn(
@@ -47,11 +78,95 @@ export function BotMessage({ message, isLoading }: BotMessageProps) {
4778
'rounded-2xl px-4 py-2 text-sm w-full bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100',
4879
)}
4980
>
50-
<div data-raw-markdown={message.content}>
81+
<div data-raw-markdown={processedContent}>
5182
<div className="prose prose-sm dark:prose-invert max-w-none break-words break-all whitespace-pre-wrap">
52-
<MarkdownContent content={message.content} />
83+
<MarkdownContent content={processedContent} />
5384
</div>
5485
</div>
86+
87+
{/* Image Gallery */}
88+
{imageFiles.length > 0 && (
89+
<div className="mt-4 space-y-3">
90+
{imageFiles.map((annotation) => {
91+
const hasError = imageErrors.has(annotation.file_id)
92+
const imageUrl = createAnnotatedFileUrl(annotation)
93+
94+
// Generate accessible alt text based on filename
95+
const getAltText = (filename: string) => {
96+
const name = filename.replace(/\.[^/.]+$/, '') // Remove extension
97+
return `Generated visualization: ${name.replace(/_/g, ' ')}`
98+
}
99+
100+
return (
101+
<div key={annotation.file_id} className="relative group">
102+
{hasError ? (
103+
<div className="flex items-center justify-center p-4 bg-gray-50 dark:bg-gray-900 rounded border border-dashed border-gray-300 dark:border-gray-600">
104+
<div className="text-center">
105+
<p className="text-sm text-gray-500 dark:text-gray-400">
106+
Failed to load image
107+
</p>
108+
<a
109+
href={createAnnotatedFileUrl(annotation)}
110+
download={annotation.filename}
111+
className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
112+
>
113+
Download file instead
114+
</a>
115+
</div>
116+
</div>
117+
) : (
118+
<>
119+
<img
120+
src={imageUrl}
121+
alt={getAltText(annotation.filename)}
122+
className="max-w-full h-auto rounded border border-gray-200 dark:border-gray-700 cursor-pointer"
123+
onError={() => handleImageError(annotation.file_id)}
124+
onClick={() => window.open(imageUrl, '_blank')}
125+
/>
126+
<a
127+
href={createAnnotatedFileUrl(annotation)}
128+
download={annotation.filename}
129+
className="absolute top-2 right-2 p-2 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-full shadow-sm opacity-0 group-hover:opacity-100 transition-opacity hover:bg-white dark:hover:bg-gray-800"
130+
title="Download image"
131+
>
132+
<Download className="h-4 w-4 text-gray-600 dark:text-gray-300" />
133+
</a>
134+
</>
135+
)}
136+
</div>
137+
)
138+
})}
139+
</div>
140+
)}
141+
142+
{/* Other Files */}
143+
{otherFiles.length > 0 && (
144+
<div className="mt-4 space-y-2">
145+
<p className="text-xs text-gray-500 dark:text-gray-400 font-medium">
146+
Attachments:
147+
</p>
148+
<div className="space-y-1">
149+
{otherFiles.map((annotation) => (
150+
<div
151+
key={annotation.file_id}
152+
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-700"
153+
>
154+
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
155+
{annotation.filename}
156+
</span>
157+
<a
158+
href={createAnnotatedFileUrl(annotation)}
159+
download={annotation.filename}
160+
className="flex items-center gap-1 px-2 py-1 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
161+
>
162+
<Download className="h-3 w-3" />
163+
Download
164+
</a>
165+
</div>
166+
))}
167+
</div>
168+
</div>
169+
)}
55170
</div>
56171
<div className="flex gap-1 items-center text-xs text-gray-500 dark:text-gray-400 space-x-1">
57172
<time dateTime={message.timestamp.toISOString()}>

0 commit comments

Comments
 (0)