mediaSequence.ts 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
  1. import type { UiMessage } from '@/components/messages/types'
  2. import { normalizeAssistantMediaMarkdown } from '@/utils/mediaMarkdown'
  3. import type { LibraryKind, UiLibraryItem } from './types'
  4. const imageExtensions = new Set(['apng', 'avif', 'bmp', 'gif', 'heic', 'heif', 'ico', 'jfif', 'jpeg', 'jpg', 'jxl', 'png', 'svg', 'tif', 'tiff', 'webp'])
  5. const videoExtensions = new Set(['avi', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'webm'])
  6. export function previewItemFromUrl(name: string, url: string, id = `local:${url}`, kind?: LibraryKind): UiLibraryItem {
  7. const extension = name.split('.').at(-1)?.toLocaleLowerCase() || ''
  8. const inferred = imageExtensions.has(extension) ? 'image'
  9. : videoExtensions.has(extension) ? 'video'
  10. : ['mp3', 'wav', 'm4a', 'aac', 'ogg'].includes(extension) ? 'audio'
  11. : extension === 'pdf' ? 'pdf'
  12. : ['md', 'markdown', 'txt', 'json', 'yaml', 'yml', 'csv', 'js', 'ts', 'py', 'sh'].includes(extension) ? 'text' : 'file'
  13. return { id, name, kind: kind || inferred, previewUrl: url, downloadUrl: url }
  14. }
  15. export function mediaUrlIdentity(url: string): string {
  16. try {
  17. return new URL(url, window.location.origin).href
  18. } catch {
  19. return url
  20. }
  21. }
  22. function nameFromUrl(url: string): string {
  23. try { return decodeURIComponent(new URL(url, window.location.origin).pathname.split('/').at(-1) || '媒体') } catch { return '媒体' }
  24. }
  25. /** The ordered image/video sequence visible in one normal or group conversation. */
  26. export function mediaItemsFromMessages(messages: UiMessage[]): UiLibraryItem[] {
  27. const result: UiLibraryItem[] = []
  28. const seen = new Set<string>()
  29. const append = (item: UiLibraryItem) => {
  30. if (!item.previewUrl || !['image', 'video'].includes(item.kind)) return
  31. const identity = mediaUrlIdentity(item.previewUrl)
  32. if (seen.has(identity)) return
  33. seen.add(identity)
  34. result.push(item)
  35. }
  36. for (const message of messages) {
  37. for (const attachment of message.attachments ?? []) {
  38. if (!attachment.url || !['image', 'video'].includes(attachment.kind || 'file')) continue
  39. append(previewItemFromUrl(attachment.name, attachment.url, `${message.id}:${attachment.id}`, attachment.kind))
  40. }
  41. const content = message.role === 'assistant' ? normalizeAssistantMediaMarkdown(message.content) : message.content
  42. const markdownMedia = /!\[[^\]]*\]\(([^)\s]+)\)|\[[^\]]+\]\(([^)\s]+)\)/g
  43. for (let match = markdownMedia.exec(content); match; match = markdownMedia.exec(content)) {
  44. const url = match[1] || match[2]
  45. if (!url) continue
  46. const item = previewItemFromUrl(nameFromUrl(url), url, `${message.id}:${url}`)
  47. append(item)
  48. }
  49. }
  50. return result
  51. }