FilePart.tsx 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import { useCallback, type KeyboardEvent } from "react"
  2. import { useOpenFile } from "../../hooks/useOpenFile"
  3. interface FilePartProps {
  4. part: {
  5. id: string
  6. type: "file"
  7. mime: string
  8. filename?: string
  9. url: string
  10. source?: {
  11. type: "file" | "symbol"
  12. text: {
  13. value: string
  14. start: number
  15. end: number
  16. }
  17. path: string
  18. range?: {
  19. start: { line: number; character: number }
  20. end: { line: number; character: number }
  21. }
  22. name?: string
  23. kind?: number
  24. }
  25. }
  26. }
  27. export function FilePart({ part }: FilePartProps) {
  28. const openFile = useOpenFile()
  29. const isSymbol = part.source?.type === "symbol"
  30. const displayName = isSymbol && part.source?.name ? part.source.name : part.filename || part.source?.path || "file"
  31. const effectivePath = part.source?.path || part.filename || part.url
  32. const range = part.source?.range
  33. const handleOpen = useCallback(() => {
  34. if (!effectivePath) return
  35. openFile({
  36. path: effectivePath,
  37. display: displayName,
  38. range,
  39. })
  40. }, [displayName, effectivePath, openFile, range])
  41. const handleKeyDown = useCallback(
  42. (event: KeyboardEvent<HTMLSpanElement>) => {
  43. if (event.key === "Enter" || event.key === " ") {
  44. event.preventDefault()
  45. handleOpen()
  46. }
  47. },
  48. [handleOpen],
  49. )
  50. const getFileIcon = () => {
  51. if (isSymbol) {
  52. return (
  53. <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  54. <path
  55. strokeLinecap="round"
  56. strokeLinejoin="round"
  57. strokeWidth={2}
  58. d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"
  59. />
  60. </svg>
  61. )
  62. }
  63. return (
  64. <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  65. <path
  66. strokeLinecap="round"
  67. strokeLinejoin="round"
  68. strokeWidth={2}
  69. d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
  70. />
  71. </svg>
  72. )
  73. }
  74. return (
  75. <span
  76. className="inline-flex items-center gap-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded px-1.5 py-0.5 text-xs font-medium cursor-pointer hover:bg-blue-200/80 dark:hover:bg-blue-900/60"
  77. role="button"
  78. tabIndex={0}
  79. onClick={handleOpen}
  80. onKeyDown={handleKeyDown}
  81. title={part.source?.path || part.filename}
  82. data-tip={part.source?.path || part.filename}
  83. >
  84. {getFileIcon()}
  85. <span className="font-mono">{displayName}</span>
  86. {isSymbol && part.source?.range && (
  87. <span className="text-[10px] opacity-75">:{part.source.range.start.line + 1}</span>
  88. )}
  89. </span>
  90. )
  91. }