diff options
| author | Daniel Polito <[email protected]> | 2025-12-29 14:22:48 -0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-12-29 11:22:48 -0600 |
| commit | b7ce46f7a12e68283d6588c33aaf972426ddd65e (patch) | |
| tree | b1599ff88021779f969c646a1e154b8871d46129 /packages/ui/src | |
| parent | 82b8d8fa5dd9206607b60de6130a6115cee68830 (diff) | |
| download | opencode-b7ce46f7a12e68283d6588c33aaf972426ddd65e.tar.gz opencode-b7ce46f7a12e68283d6588c33aaf972426ddd65e.zip | |
Desktop: Image Preview and Dedupe File Upload (#6372)
Diffstat (limited to 'packages/ui/src')
| -rw-r--r-- | packages/ui/src/components/image-preview.css | 63 | ||||
| -rw-r--r-- | packages/ui/src/components/image-preview.tsx | 24 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.css | 4 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 19 | ||||
| -rw-r--r-- | packages/ui/src/styles/index.css | 1 |
5 files changed, 110 insertions, 1 deletions
diff --git a/packages/ui/src/components/image-preview.css b/packages/ui/src/components/image-preview.css new file mode 100644 index 000000000..3c47f7a25 --- /dev/null +++ b/packages/ui/src/components/image-preview.css @@ -0,0 +1,63 @@ +[data-component="image-preview"] { + position: fixed; + inset: 0; + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + + [data-slot="image-preview-container"] { + position: relative; + z-index: 50; + width: min(calc(100vw - 32px), 90vw); + max-width: 1200px; + height: min(calc(100vh - 32px), 90vh); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + [data-slot="image-preview-content"] { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-height: 100%; + border-radius: var(--radius-lg); + background: var(--surface-raised-stronger-non-alpha); + box-shadow: + 0 15px 45px 0 rgba(19, 16, 16, 0.35), + 0 3.35px 10.051px 0 rgba(19, 16, 16, 0.25), + 0 0.998px 2.993px 0 rgba(19, 16, 16, 0.2); + overflow: hidden; + + &:focus-visible { + outline: none; + } + + [data-slot="image-preview-header"] { + display: flex; + padding: 8px 8px 0; + justify-content: flex-end; + align-items: center; + align-self: stretch; + } + + [data-slot="image-preview-body"] { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + overflow: auto; + } + + [data-slot="image-preview-image"] { + max-width: 100%; + max-height: calc(90vh - 100px); + object-fit: contain; + border-radius: var(--radius-md); + } + } + } +} diff --git a/packages/ui/src/components/image-preview.tsx b/packages/ui/src/components/image-preview.tsx new file mode 100644 index 000000000..900abc725 --- /dev/null +++ b/packages/ui/src/components/image-preview.tsx @@ -0,0 +1,24 @@ +import { Dialog as Kobalte } from "@kobalte/core/dialog" +import { IconButton } from "./icon-button" + +export interface ImagePreviewProps { + src: string + alt?: string +} + +export function ImagePreview(props: ImagePreviewProps) { + return ( + <div data-component="image-preview"> + <div data-slot="image-preview-container"> + <Kobalte.Content data-slot="image-preview-content"> + <div data-slot="image-preview-header"> + <Kobalte.CloseButton data-slot="image-preview-close" as={IconButton} icon="close" variant="ghost" /> + </div> + <div data-slot="image-preview-body"> + <img src={props.src} alt={props.alt ?? "Image preview"} data-slot="image-preview-image" /> + </div> + </Kobalte.Content> + </div> + </div> + ) +} diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index ce479d8c1..4338940cb 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -40,6 +40,10 @@ border-color: var(--border-strong-base); } + &[data-clickable="true"] { + cursor: pointer; + } + &[data-type="image"] { width: 48px; height: 48px; diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 31103b35c..8590b31cb 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -25,6 +25,7 @@ import { import { useData } from "../context" import { useDiffComponent } from "../context/diff" import { useCodeComponent } from "../context/code" +import { useDialog } from "../context/dialog" import { BasicTool } from "./basic-tool" import { GenericTool } from "./basic-tool" import { Button } from "./button" @@ -33,6 +34,7 @@ import { Icon } from "./icon" import { Checkbox } from "./checkbox" import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" +import { ImagePreview } from "./image-preview" import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path" import { checksum } from "@opencode-ai/util/encode" import { createAutoScroll } from "../hooks" @@ -264,6 +266,8 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part } export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) { + const dialog = useDialog() + const textPart = createMemo( () => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined, ) @@ -286,13 +290,26 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp }), ) + const openImagePreview = (url: string, alt?: string) => { + dialog.show(() => <ImagePreview src={url} alt={alt} />) + } + return ( <div data-component="user-message"> <Show when={attachments().length > 0}> <div data-slot="user-message-attachments"> <For each={attachments()}> {(file) => ( - <div data-slot="user-message-attachment" data-type={file.mime.startsWith("image/") ? "image" : "file"}> + <div + data-slot="user-message-attachment" + data-type={file.mime.startsWith("image/") ? "image" : "file"} + data-clickable={file.mime.startsWith("image/") && !!file.url} + onClick={() => { + if (file.mime.startsWith("image/") && file.url) { + openImagePreview(file.url, file.filename) + } + }} + > <Show when={file.mime.startsWith("image/") && file.url} fallback={ diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index 9bf3e4f69..554bdeb89 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -22,6 +22,7 @@ @import "../components/provider-icon.css" layer(components); @import "../components/icon.css" layer(components); @import "../components/icon-button.css" layer(components); +@import "../components/image-preview.css" layer(components); @import "../components/text-field.css" layer(components); @import "../components/list.css" layer(components); @import "../components/logo.css" layer(components); |
