Skip to content

Commit 31863c3

Browse files
authored
feat: added optional code interpreter tool (#109)
1 parent eb1acf4 commit 31863c3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3316
-471
lines changed

.storybook/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ const config: StorybookConfig = {
1111
'@storybook/addon-onboarding',
1212
'@storybook/addon-a11y',
1313
'@storybook/addon-vitest',
14-
'storybook-dark-mode',
1514
],
15+
staticDirs: ['../public'],
1616
framework: {
1717
name: '@storybook/react-vite',
1818
options: {},

.storybook/preview.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react'
22
import type { Preview } from '@storybook/react-vite'
33
import '../src/styles.css'
4+
import { useEffect } from 'react'
45

56
const preview: Preview = {
67
parameters: {
@@ -30,3 +31,12 @@ const preview: Preview = {
3031
}
3132

3233
export default preview
34+
35+
// Add Pomerium favicon to Storybook preview head
36+
if (typeof window !== 'undefined') {
37+
const link = document.createElement('link')
38+
link.rel = 'icon'
39+
link.type = 'image/svg+xml'
40+
link.href = '/pomerium-favicon.svg'
41+
document.head.appendChild(link)
42+
}

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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"format": "prettier",
1414
"check": "prettier --write . && eslint --fix",
1515
"storybook": "storybook dev -p 6006",
16-
"build-storybook": "storybook build"
16+
"build-storybook": "npm ci && storybook build"
1717
},
1818
"dependencies": {
1919
"@pomerium/js-sdk": "^1.1.0",
@@ -40,6 +40,7 @@
4040
"class-variance-authority": "^0.7.1",
4141
"clsx": "^2.1.1",
4242
"lucide-react": "^0.476.0",
43+
"mime": "^4.0.7",
4344
"openai": "^4.103.0",
4445
"react": "^19.0.0",
4546
"react-dom": "^19.0.0",

public/pomerium-favicon.svg

Lines changed: 5 additions & 0 deletions
Loading

src/components/BotError.tsx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { AlertTriangle, ChevronRight } from 'lucide-react'
1+
import { AlertTriangle } from 'lucide-react'
2+
import { CollapsibleSection } from './ui/collapsible-section'
3+
import { MessageAvatar } from './MessageAvatar'
24

35
interface BotErrorProps {
46
message: string
@@ -7,20 +9,14 @@ interface BotErrorProps {
79
export function BotError({ message }: BotErrorProps) {
810
return (
911
<div className="flex w-full max-w-full gap-2 py-2 animate-in fade-in justify-start items-start text-red-900 dark:text-red-100">
10-
<div className="flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md bg-red-50 dark:bg-red-950">
11-
<AlertTriangle className="h-5 w-5" />
12-
</div>
12+
<MessageAvatar
13+
icon={<AlertTriangle className="h-5 w-5" />}
14+
variant="error"
15+
/>
1316
<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"
17-
>
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>
17+
<CollapsibleSection title="Error" variant="error" open={true}>
2218
<div>{message}</div>
23-
</details>
19+
</CollapsibleSection>
2420
</div>
2521
</div>
2622
)

src/components/BotMessage.tsx

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,61 @@
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 { MessageAvatar } from './MessageAvatar'
10+
import {
11+
isImageFile,
12+
createAnnotatedFileUrl,
13+
} from '@/lib/utils/code-interpreter'
914

1015
export interface BotMessageProps {
1116
message: Message
12-
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({ message, fileAnnotations = [] }: BotMessageProps) {
26+
const [imageErrors, setImageErrors] = useState<Set<string>>(new Set())
27+
1628
const handleCopy = async () => {
17-
const copied = await copyToClipboard(message.content)
29+
const copied = await copyToClipboard(processedContent)
1830

1931
if (copied) {
2032
toast.success('Copied message to clipboard')
2133
}
2234
}
2335

36+
const handleImageError = (fileId: string) => {
37+
setImageErrors((prev) => new Set(prev).add(fileId))
38+
}
39+
40+
// Use message content directly; sandbox URL replacement is not needed
41+
const processedContent = message.content
42+
43+
// Separate image and non-image files
44+
const imageFiles = fileAnnotations.filter((annotation) =>
45+
isImageFile(annotation.filename),
46+
)
47+
const otherFiles = fileAnnotations.filter(
48+
(annotation) => !isImageFile(annotation.filename),
49+
)
50+
2451
return (
2552
<div
2653
className={cn(
2754
'flex w-full max-w-full gap-2 py-2 animate-in fade-in',
2855
'justify-start',
2956
)}
3057
>
31-
<div
32-
className={cn(
33-
'flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100',
34-
isLoading && 'animate-[pulse_1.5s_ease-in-out_infinite] opacity-80',
35-
)}
36-
>
37-
<Bot className="h-5 w-5" />
38-
</div>
58+
<MessageAvatar icon={<Bot className="h-5 w-5" />} variant="default" />
3959
<div
4060
className={cn(
4161
'group grid gap-1 space-y-1 items-start',
@@ -50,6 +70,90 @@ export function BotMessage({ message, isLoading }: BotMessageProps) {
5070
<div data-raw-markdown={message.content}>
5171
<MarkdownContent content={message.content} />
5272
</div>
73+
74+
{/* Image Gallery */}
75+
{imageFiles.length > 0 && (
76+
<div className="mt-4 space-y-3">
77+
{imageFiles.map((annotation) => {
78+
const hasError = imageErrors.has(annotation.file_id)
79+
const imageUrl = createAnnotatedFileUrl(annotation)
80+
81+
// Generate accessible alt text based on filename
82+
const getAltText = (filename: string) => {
83+
const name = filename.replace(/\.[^/.]+$/, '') // Remove extension
84+
return `Generated visualization: ${name.replace(/_/g, ' ')}`
85+
}
86+
87+
return (
88+
<div key={annotation.file_id} className="relative group">
89+
{hasError ? (
90+
<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">
91+
<div className="text-center">
92+
<p className="text-sm text-gray-500 dark:text-gray-400">
93+
Failed to load image
94+
</p>
95+
<a
96+
href={createAnnotatedFileUrl(annotation)}
97+
download={annotation.filename}
98+
className="mt-2 text-xs text-blue-600 dark:text-blue-400 hover:underline"
99+
>
100+
Download file instead
101+
</a>
102+
</div>
103+
</div>
104+
) : (
105+
<>
106+
<img
107+
src={imageUrl}
108+
alt={getAltText(annotation.filename)}
109+
className="max-w-full h-auto rounded border border-gray-200 dark:border-gray-700 cursor-pointer"
110+
onError={() => handleImageError(annotation.file_id)}
111+
onClick={() => window.open(imageUrl, '_blank')}
112+
/>
113+
<a
114+
href={createAnnotatedFileUrl(annotation)}
115+
download={annotation.filename}
116+
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"
117+
title="Download image"
118+
>
119+
<Download className="h-4 w-4 text-gray-600 dark:text-gray-300" />
120+
</a>
121+
</>
122+
)}
123+
</div>
124+
)
125+
})}
126+
</div>
127+
)}
128+
129+
{/* Other Files */}
130+
{otherFiles.length > 0 && (
131+
<div className="mt-4 space-y-2">
132+
<p className="text-xs text-gray-500 dark:text-gray-400 font-medium">
133+
Attachments:
134+
</p>
135+
<div className="space-y-1">
136+
{otherFiles.map((annotation) => (
137+
<div
138+
key={annotation.file_id}
139+
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-900 rounded border border-gray-200 dark:border-gray-700"
140+
>
141+
<span className="text-sm text-gray-700 dark:text-gray-300 truncate">
142+
{annotation.filename}
143+
</span>
144+
<a
145+
href={createAnnotatedFileUrl(annotation)}
146+
download={annotation.filename}
147+
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"
148+
>
149+
<Download className="h-3 w-3" />
150+
Download
151+
</a>
152+
</div>
153+
))}
154+
</div>
155+
</div>
156+
)}
53157
</div>
54158
<div className="flex gap-1 items-center text-xs text-gray-500 dark:text-gray-400 space-x-1">
55159
<time dateTime={message.timestamp.toISOString()}>

0 commit comments

Comments
 (0)