diff --git a/packages/ui/src/components/session-review.css b/packages/ui/src/components/session-review.css index e682c19b4..a53289b9a 100644 --- a/packages/ui/src/components/session-review.css +++ b/packages/ui/src/components/session-review.css @@ -138,6 +138,20 @@ justify-content: flex-end; } + [data-slot="session-review-change"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-weight: var(--font-weight-medium); + } + + [data-slot="session-review-change"][data-type="added"] { + color: var(--icon-diff-add-base); + } + + [data-slot="session-review-change"][data-type="removed"] { + color: var(--icon-diff-delete-base); + } + [data-slot="session-review-file-container"] { padding: 0; } @@ -163,4 +177,22 @@ font-size: var(--font-size-small); color: var(--text-weak); } + + [data-slot="session-review-audio-container"] { + padding: 12px; + display: flex; + justify-content: center; + background: var(--background-stronger); + } + + [data-slot="session-review-audio"] { + width: 100%; + max-width: 560px; + } + + [data-slot="session-review-audio-placeholder"] { + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + color: var(--text-weak); + } } diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 4d2123ff0..a19a8741d 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -36,6 +36,7 @@ export interface SessionReviewProps { } const imageExtensions = new Set(["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "ico", "tif", "tiff", "heic"]) +const audioExtensions = new Set(["mp3", "wav", "ogg", "m4a", "aac", "flac", "opus"]) function getExtension(file: string): string { const idx = file.lastIndexOf(".") @@ -47,17 +48,21 @@ function isImageFile(file: string): boolean { return imageExtensions.has(getExtension(file)) } +function isAudioFile(file: string): boolean { + return audioExtensions.has(getExtension(file)) +} + function dataUrl(content: FileContent | undefined): string | undefined { if (!content) return if (content.encoding !== "base64") return const mime = content.mimeType ?? "" - if (!mime.startsWith("image/")) return + if (!mime.startsWith("image/") && !mime.startsWith("audio/")) return return `data:${mime};base64,${content.content}` } function dataUrlFromValue(value: unknown): string | undefined { if (typeof value === "string") { - if (value.startsWith("data:image/")) return value + if (value.startsWith("data:image/") || value.startsWith("data:audio/")) return value return } if (!value || typeof value !== "object") return @@ -69,7 +74,7 @@ function dataUrlFromValue(value: unknown): string | undefined { if (typeof content !== "string") return if (encoding !== "base64") return if (typeof mimeType !== "string") return - if (!mimeType.startsWith("image/")) return + if (!mimeType.startsWith("image/") && !mimeType.startsWith("audio/")) return return `data:${mimeType};base64,${content}` } @@ -150,11 +155,16 @@ export const SessionReview = (props: SessionReviewProps) => { const isAdded = () => beforeText().length === 0 && afterText().length > 0 const isDeleted = () => afterText().length === 0 && beforeText().length > 0 const isImage = () => isImageFile(diff.file) + const isAudio = () => isAudioFile(diff.file) const diffImageSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) const [imageSrc, setImageSrc] = createSignal(diffImageSrc) const [imageStatus, setImageStatus] = createSignal<"idle" | "loading" | "error">("idle") + const diffAudioSrc = dataUrlFromValue(diff.after) ?? dataUrlFromValue(diff.before) + const [audioSrc, setAudioSrc] = createSignal(diffAudioSrc) + const [audioStatus, setAudioStatus] = createSignal<"idle" | "loading" | "error">("idle") + createEffect(() => { if (!open().includes(diff.file)) return if (!isImage()) return @@ -180,6 +190,31 @@ export const SessionReview = (props: SessionReviewProps) => { }) }) + createEffect(() => { + if (!open().includes(diff.file)) return + if (!isAudio()) return + if (audioSrc()) return + if (audioStatus() !== "idle") return + + const reader = props.readFile + if (!reader) return + + setAudioStatus("loading") + reader(diff.file) + .then((result) => { + const src = dataUrl(result) + if (!src) { + setAudioStatus("error") + return + } + setAudioSrc(src) + setAudioStatus("idle") + }) + .catch(() => { + setAudioStatus("error") + }) + }) + const fileForCode = () => { const contents = afterText() || beforeText() return { @@ -216,7 +251,21 @@ export const SessionReview = (props: SessionReviewProps) => {
- + + + + Added + + + + + Removed + + + + + +
@@ -241,6 +290,23 @@ export const SessionReview = (props: SessionReviewProps) => { + +
+ + + Loading audio... + Audio preview unavailable + +
+ } + > +