ChatInputExtraContextItem.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import {
  2. DocumentTextIcon,
  3. SpeakerWaveIcon,
  4. XMarkIcon,
  5. } from '@heroicons/react/24/outline';
  6. import { MessageExtra } from '../utils/types';
  7. import { useState } from 'react';
  8. import { classNames } from '../utils/misc';
  9. export default function ChatInputExtraContextItem({
  10. items,
  11. removeItem,
  12. clickToShow,
  13. }: {
  14. items?: MessageExtra[];
  15. removeItem?: (index: number) => void;
  16. clickToShow?: boolean;
  17. }) {
  18. const [show, setShow] = useState(-1);
  19. const showingItem = show >= 0 ? items?.[show] : undefined;
  20. if (!items) return null;
  21. return (
  22. <div
  23. className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1"
  24. role="group"
  25. aria-description="Selected files"
  26. >
  27. {items.map((item, i) => (
  28. <div
  29. className="indicator"
  30. key={i}
  31. onClick={() => clickToShow && setShow(i)}
  32. tabIndex={0}
  33. aria-description={
  34. clickToShow ? `Click to show: ${item.name}` : undefined
  35. }
  36. role={clickToShow ? 'button' : 'menuitem'}
  37. >
  38. {removeItem && (
  39. <div className="indicator-item indicator-top">
  40. <button
  41. aria-label="Remove file"
  42. className="btn btn-neutral btn-sm w-4 h-4 p-0 rounded-full"
  43. onClick={() => removeItem(i)}
  44. >
  45. <XMarkIcon className="h-3 w-3" />
  46. </button>
  47. </div>
  48. )}
  49. <div
  50. className={classNames({
  51. 'flex flex-row rounded-md shadow-sm items-center m-0 p-0': true,
  52. 'cursor-pointer hover:shadow-md': !!clickToShow,
  53. })}
  54. >
  55. {item.type === 'imageFile' ? (
  56. <>
  57. <img
  58. src={item.base64Url}
  59. alt={`Preview image for ${item.name}`}
  60. className="w-14 h-14 object-cover rounded-md"
  61. />
  62. </>
  63. ) : (
  64. <>
  65. <div
  66. className="w-14 h-14 flex items-center justify-center"
  67. aria-description="Document icon"
  68. >
  69. {item.type === 'audioFile' ? (
  70. <SpeakerWaveIcon className="h-8 w-8 text-gray-500" />
  71. ) : (
  72. <DocumentTextIcon className="h-8 w-8 text-gray-500" />
  73. )}
  74. </div>
  75. <div className="text-xs pr-4">
  76. <b>{item.name ?? 'Extra content'}</b>
  77. </div>
  78. </>
  79. )}
  80. </div>
  81. </div>
  82. ))}
  83. {showingItem && (
  84. <dialog
  85. className="modal modal-open"
  86. aria-description={`Preview ${showingItem.name}`}
  87. >
  88. <div className="modal-box">
  89. <div className="flex justify-between items-center mb-4">
  90. <b>{showingItem.name ?? 'Extra content'}</b>
  91. <button
  92. className="btn btn-ghost btn-sm"
  93. aria-label="Close preview dialog"
  94. >
  95. <XMarkIcon className="h-5 w-5" onClick={() => setShow(-1)} />
  96. </button>
  97. </div>
  98. {showingItem.type === 'imageFile' ? (
  99. <img
  100. src={showingItem.base64Url}
  101. alt={`Preview image for ${showingItem.name}`}
  102. />
  103. ) : showingItem.type === 'audioFile' ? (
  104. <audio
  105. controls
  106. className="w-full"
  107. aria-description={`Audio file ${showingItem.name}`}
  108. >
  109. <source
  110. src={`data:${showingItem.mimeType};base64,${showingItem.base64Data}`}
  111. type={showingItem.mimeType}
  112. aria-description={`Audio file ${showingItem.name}`}
  113. />
  114. Your browser does not support the audio element.
  115. </audio>
  116. ) : (
  117. <div className="overflow-x-auto">
  118. <pre className="whitespace-pre-wrap break-words text-sm">
  119. {showingItem.content}
  120. </pre>
  121. </div>
  122. )}
  123. </div>
  124. <div className="modal-backdrop" onClick={() => setShow(-1)}></div>
  125. </dialog>
  126. )}
  127. </div>
  128. );
  129. }