@meistrari/chat-nuxt 4.10.0-rc.1 → 4.10.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/files/components/{audio-preview.d.vue.ts → chat-file-audio.d.vue.ts} +8 -7
  3. package/dist/runtime/files/components/chat-file-audio.vue +94 -0
  4. package/dist/runtime/files/components/{image-preview.d.vue.ts → chat-file-audio.vue.d.ts} +8 -7
  5. package/dist/runtime/files/components/chat-file-card.d.vue.ts +31 -0
  6. package/dist/runtime/files/components/chat-file-card.vue +44 -0
  7. package/dist/runtime/files/components/chat-file-card.vue.d.ts +31 -0
  8. package/dist/runtime/files/components/chat-file-preview.vue +90 -196
  9. package/dist/runtime/files/composables/pdf.d.ts +5 -842
  10. package/dist/runtime/files/content-detection.js +7 -3
  11. package/dist/runtime/files/formatting.d.ts +1 -0
  12. package/dist/runtime/files/formatting.js +4 -0
  13. package/dist/runtime/files/presentation.d.ts +3 -0
  14. package/dist/runtime/files/presentation.js +24 -0
  15. package/dist/runtime/messages/answer.d.ts +0 -5
  16. package/dist/runtime/messages/answer.js +0 -18
  17. package/dist/runtime/messages/components/chat-answer.vue +4 -3
  18. package/dist/runtime/messages/components/chat-message-bubble.vue +12 -0
  19. package/dist/runtime/messages/components/widgets/chat-thinking-widget.vue +4 -0
  20. package/dist/runtime/rendering/components/openui/bar-chart.d.vue.ts +10 -0
  21. package/dist/runtime/rendering/components/openui/bar-chart.vue +39 -0
  22. package/dist/runtime/rendering/components/openui/bar-chart.vue.d.ts +10 -0
  23. package/dist/runtime/rendering/components/openui/bar.d.vue.ts +10 -0
  24. package/dist/runtime/rendering/components/openui/bar.vue +12 -0
  25. package/dist/runtime/rendering/components/openui/bar.vue.d.ts +10 -0
  26. package/dist/runtime/rendering/components/openui/button.d.vue.ts +11 -0
  27. package/dist/runtime/rendering/components/openui/button.vue +30 -0
  28. package/dist/runtime/rendering/components/openui/button.vue.d.ts +11 -0
  29. package/dist/runtime/rendering/components/openui/buttons.d.vue.ts +8 -0
  30. package/dist/runtime/rendering/components/openui/buttons.vue +14 -0
  31. package/dist/runtime/rendering/components/openui/buttons.vue.d.ts +8 -0
  32. package/dist/runtime/rendering/components/openui/callout.d.vue.ts +11 -0
  33. package/dist/runtime/rendering/components/openui/callout.vue +27 -0
  34. package/dist/runtime/rendering/components/openui/callout.vue.d.ts +11 -0
  35. package/dist/runtime/rendering/components/openui/card.d.vue.ts +9 -0
  36. package/dist/runtime/rendering/components/openui/card.vue +19 -0
  37. package/dist/runtime/rendering/components/openui/card.vue.d.ts +9 -0
  38. package/dist/runtime/rendering/components/openui/checkbox.d.vue.ts +9 -0
  39. package/dist/runtime/rendering/components/openui/checkbox.vue +26 -0
  40. package/dist/runtime/rendering/components/openui/checkbox.vue.d.ts +9 -0
  41. package/dist/runtime/rendering/components/openui/citation.d.vue.ts +9 -0
  42. package/dist/runtime/rendering/components/openui/citation.vue +13 -0
  43. package/dist/runtime/rendering/components/openui/citation.vue.d.ts +9 -0
  44. package/dist/runtime/rendering/components/openui/col.d.vue.ts +10 -0
  45. package/dist/runtime/rendering/components/openui/col.vue +16 -0
  46. package/dist/runtime/rendering/components/openui/col.vue.d.ts +10 -0
  47. package/dist/runtime/rendering/components/openui/file-card.d.vue.ts +9 -0
  48. package/dist/runtime/rendering/components/openui/file-card.vue +15 -0
  49. package/dist/runtime/rendering/components/openui/file-card.vue.d.ts +9 -0
  50. package/dist/runtime/rendering/components/openui/form.d.vue.ts +10 -0
  51. package/dist/runtime/rendering/components/openui/form.vue +60 -0
  52. package/dist/runtime/rendering/components/openui/form.vue.d.ts +10 -0
  53. package/dist/runtime/rendering/components/openui/image.d.vue.ts +10 -0
  54. package/dist/runtime/rendering/components/openui/image.vue +40 -0
  55. package/dist/runtime/rendering/components/openui/image.vue.d.ts +10 -0
  56. package/dist/runtime/rendering/components/openui/input.d.vue.ts +10 -0
  57. package/dist/runtime/rendering/components/openui/input.vue +23 -0
  58. package/dist/runtime/rendering/components/openui/input.vue.d.ts +10 -0
  59. package/dist/runtime/rendering/components/openui/markdown.d.vue.ts +9 -0
  60. package/dist/runtime/rendering/components/openui/markdown.vue +14 -0
  61. package/dist/runtime/rendering/components/openui/markdown.vue.d.ts +9 -0
  62. package/dist/runtime/rendering/components/openui/metric.d.vue.ts +11 -0
  63. package/dist/runtime/rendering/components/openui/metric.vue +20 -0
  64. package/dist/runtime/rendering/components/openui/metric.vue.d.ts +11 -0
  65. package/dist/runtime/rendering/components/openui/option.d.vue.ts +10 -0
  66. package/dist/runtime/rendering/components/openui/option.vue +12 -0
  67. package/dist/runtime/rendering/components/openui/option.vue.d.ts +10 -0
  68. package/dist/runtime/rendering/components/openui/question.d.vue.ts +11 -0
  69. package/dist/runtime/rendering/components/openui/question.vue +15 -0
  70. package/dist/runtime/rendering/components/openui/question.vue.d.ts +11 -0
  71. package/dist/runtime/rendering/components/openui/questions.d.vue.ts +9 -0
  72. package/dist/runtime/rendering/components/openui/questions.vue +91 -0
  73. package/dist/runtime/rendering/components/openui/questions.vue.d.ts +9 -0
  74. package/dist/runtime/rendering/components/openui/select.d.vue.ts +10 -0
  75. package/dist/runtime/rendering/components/openui/select.vue +27 -0
  76. package/dist/runtime/rendering/components/openui/select.vue.d.ts +10 -0
  77. package/dist/runtime/rendering/components/openui/stack.d.vue.ts +9 -0
  78. package/dist/runtime/rendering/components/openui/stack.vue +18 -0
  79. package/dist/runtime/rendering/components/openui/stack.vue.d.ts +9 -0
  80. package/dist/runtime/rendering/components/openui/table.d.vue.ts +9 -0
  81. package/dist/runtime/rendering/components/openui/table.vue +41 -0
  82. package/dist/runtime/rendering/components/openui/table.vue.d.ts +9 -0
  83. package/dist/runtime/rendering/components/openui-block.d.vue.ts +12 -0
  84. package/dist/runtime/rendering/components/openui-block.vue +73 -0
  85. package/dist/runtime/rendering/components/openui-block.vue.d.ts +12 -0
  86. package/dist/runtime/rendering/components/openui-reasoning-placeholder.d.vue.ts +3 -0
  87. package/dist/runtime/rendering/components/openui-reasoning-placeholder.vue +10 -0
  88. package/dist/runtime/rendering/components/openui-reasoning-placeholder.vue.d.ts +3 -0
  89. package/dist/runtime/rendering/components/renderers/artifact-display.vue +1 -1
  90. package/dist/runtime/rendering/openui/actions.d.ts +15 -0
  91. package/dist/runtime/rendering/openui/actions.js +40 -0
  92. package/dist/runtime/rendering/openui/context.d.ts +22 -0
  93. package/dist/runtime/rendering/openui/context.js +4 -0
  94. package/dist/runtime/rendering/openui/form-fields.d.ts +10 -0
  95. package/dist/runtime/rendering/openui/form-fields.js +30 -0
  96. package/dist/runtime/rendering/openui/markdown-link.d.ts +2 -0
  97. package/dist/runtime/rendering/openui/markdown-link.js +5 -0
  98. package/dist/runtime/rendering/openui/program.d.ts +15 -0
  99. package/dist/runtime/rendering/openui/program.js +74 -0
  100. package/dist/runtime/rendering/openui/registry.d.ts +5 -0
  101. package/dist/runtime/rendering/openui/registry.js +44 -0
  102. package/dist/runtime/rendering/openui/render.d.ts +21 -0
  103. package/dist/runtime/rendering/openui/render.js +45 -0
  104. package/dist/runtime/rendering/openui/use-openui-context.d.ts +2 -0
  105. package/dist/runtime/rendering/openui/use-openui-context.js +8 -0
  106. package/dist/runtime/rendering/openui/use-widget-prose.d.ts +2 -0
  107. package/dist/runtime/rendering/openui/use-widget-prose.js +7 -0
  108. package/dist/runtime/shared/composables/polling.d.ts +1 -1
  109. package/dist/runtime/types/schemas/chat/conversation-runtime.d.ts +12 -0
  110. package/dist/runtime/types/schemas/chat/conversations.d.ts +12 -0
  111. package/dist/runtime/types/schemas/chat/conversations.js +3 -0
  112. package/dist/runtime/types/schemas/chat/messages.d.ts +18 -0
  113. package/dist/runtime/types/schemas/chat/ui-language.d.ts +14 -0
  114. package/dist/runtime/types/schemas/chat/ui-language.js +8 -0
  115. package/package.json +3 -1
  116. package/dist/runtime/files/components/audio-preview.vue +0 -52
  117. package/dist/runtime/files/components/audio-preview.vue.d.ts +0 -14
  118. package/dist/runtime/files/components/csv-preview.d.vue.ts +0 -20
  119. package/dist/runtime/files/components/csv-preview.vue +0 -157
  120. package/dist/runtime/files/components/csv-preview.vue.d.ts +0 -20
  121. package/dist/runtime/files/components/image-preview.vue +0 -54
  122. package/dist/runtime/files/components/image-preview.vue.d.ts +0 -14
  123. package/dist/runtime/files/components/pdf-preview.client.d.vue.ts +0 -20
  124. package/dist/runtime/files/components/pdf-preview.client.vue +0 -90
  125. package/dist/runtime/files/components/pdf-preview.client.vue.d.ts +0 -20
  126. package/dist/runtime/files/components/text-preview.d.vue.ts +0 -19
  127. package/dist/runtime/files/components/text-preview.vue +0 -105
  128. package/dist/runtime/files/components/text-preview.vue.d.ts +0 -19
  129. package/dist/runtime/files/components/video-preview.d.vue.ts +0 -14
  130. package/dist/runtime/files/components/video-preview.vue +0 -55
  131. package/dist/runtime/files/components/video-preview.vue.d.ts +0 -14
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meistrari/chat-nuxt",
3
3
  "configKey": "chatNuxt",
4
- "version": "4.10.0-rc.1",
4
+ "version": "4.10.0-rc.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1,14 +1,15 @@
1
+ import type { ChatFileMenuItem } from './chat-file-card.vue.js';
1
2
  type __VLS_Props = {
2
- url: string;
3
- name?: string;
4
- clickable?: boolean;
3
+ url?: string;
4
+ iconSrc: string;
5
+ name: string;
6
+ meta: string;
7
+ menuItems: ChatFileMenuItem[];
5
8
  };
6
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
- click: () => any;
8
- loaded: () => any;
10
+ open: () => any;
9
11
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
- onClick?: (() => any) | undefined;
11
- onLoaded?: (() => any) | undefined;
12
+ onOpen?: (() => any) | undefined;
12
13
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
14
  declare const _default: typeof __VLS_export;
14
15
  export default _default;
@@ -0,0 +1,94 @@
1
+ <script setup>
2
+ import ChatFileCard from "./chat-file-card.vue";
3
+ import { formatPlaybackTime } from "../formatting";
4
+ const props = defineProps({
5
+ url: { type: String, required: false },
6
+ iconSrc: { type: String, required: true },
7
+ name: { type: String, required: true },
8
+ meta: { type: String, required: true },
9
+ menuItems: { type: Array, required: true }
10
+ });
11
+ const emit = defineEmits(["open"]);
12
+ const player = ref();
13
+ const playing = ref(false);
14
+ const currentTime = ref(0);
15
+ const duration = ref(0);
16
+ const failed = ref(false);
17
+ const progress = computed(() => duration.value > 0 ? currentTime.value / duration.value * 100 : 0);
18
+ function togglePlayback() {
19
+ const audio = player.value;
20
+ if (!audio)
21
+ return;
22
+ if (audio.paused)
23
+ void audio.play();
24
+ else
25
+ audio.pause();
26
+ }
27
+ function seek(event) {
28
+ const audio = player.value;
29
+ if (!audio || duration.value <= 0)
30
+ return;
31
+ audio.currentTime = Number(event.target.value) / 100 * duration.value;
32
+ }
33
+ watch(() => props.url, () => {
34
+ playing.value = false;
35
+ currentTime.value = 0;
36
+ duration.value = 0;
37
+ failed.value = false;
38
+ });
39
+ </script>
40
+
41
+ <template>
42
+ <ChatFileCard
43
+ :icon-src="iconSrc"
44
+ :name="name"
45
+ :meta="failed ? 'Falha ao carregar \xE1udio' : meta"
46
+ :menu-items="menuItems"
47
+ :disabled="!url"
48
+ @open="emit('open')"
49
+ >
50
+ <template #actions>
51
+ <TelaIconButton
52
+ :icon="playing ? 'i-ph-pause-fill' : 'i-ph-play-fill'"
53
+ color="secondary"
54
+ size="xs"
55
+ :aria-label="playing ? 'Pausar \xE1udio' : 'Reproduzir \xE1udio'"
56
+ :disabled="!url || failed"
57
+ @click="togglePlayback"
58
+ />
59
+ </template>
60
+ <div flex items-center gap-12px px-12px py-10px border-t-0.5px border>
61
+ <input
62
+ type="range"
63
+ min="0"
64
+ max="100"
65
+ step="0.1"
66
+ :value="progress"
67
+ :disabled="!url || failed || duration <= 0"
68
+ aria-label="Posição do áudio"
69
+ flex-1 cursor-pointer text-primary
70
+ class="chat-file-audio__track"
71
+ @input="seek"
72
+ >
73
+ <span body-12-regular text-secondary tabular-nums shrink-0>
74
+ {{ formatPlaybackTime(currentTime) }} / {{ formatPlaybackTime(duration) }}
75
+ </span>
76
+ </div>
77
+ <audio
78
+ v-if="url"
79
+ ref="player"
80
+ :src="url"
81
+ preload="metadata"
82
+ @play="playing = true"
83
+ @pause="playing = false"
84
+ @ended="playing = false"
85
+ @timeupdate="currentTime = player?.currentTime ?? 0"
86
+ @loadedmetadata="duration = player?.duration ?? 0"
87
+ @error="failed = true"
88
+ />
89
+ </ChatFileCard>
90
+ </template>
91
+
92
+ <style scoped>
93
+ .chat-file-audio__track{accent-color:currentColor;height:4px}
94
+ </style>
@@ -1,14 +1,15 @@
1
+ import type { ChatFileMenuItem } from './chat-file-card.vue.js';
1
2
  type __VLS_Props = {
2
- url: string;
3
- maxHeight?: number;
4
- clickable?: boolean;
3
+ url?: string;
4
+ iconSrc: string;
5
+ name: string;
6
+ meta: string;
7
+ menuItems: ChatFileMenuItem[];
5
8
  };
6
9
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
- click: () => any;
8
- loaded: () => any;
10
+ open: () => any;
9
11
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
- onClick?: (() => any) | undefined;
11
- onLoaded?: (() => any) | undefined;
12
+ onOpen?: (() => any) | undefined;
12
13
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
14
  declare const _default: typeof __VLS_export;
14
15
  export default _default;
@@ -0,0 +1,31 @@
1
+ export interface ChatFileMenuItem {
2
+ label: string;
3
+ icon: string;
4
+ click: () => void;
5
+ }
6
+ type __VLS_Props = {
7
+ iconSrc: string;
8
+ name: string;
9
+ meta: string;
10
+ menuItems: ChatFileMenuItem[];
11
+ disabled?: boolean;
12
+ };
13
+ declare var __VLS_1: {}, __VLS_14: {};
14
+ type __VLS_Slots = {} & {
15
+ actions?: (props: typeof __VLS_1) => any;
16
+ } & {
17
+ default?: (props: typeof __VLS_14) => any;
18
+ };
19
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
+ open: () => any;
21
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
22
+ onOpen?: (() => any) | undefined;
23
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
25
+ declare const _default: typeof __VLS_export;
26
+ export default _default;
27
+ type __VLS_WithSlots<T, S> = T & {
28
+ new (): {
29
+ $slots: S;
30
+ };
31
+ };
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ defineProps({
3
+ iconSrc: { type: String, required: true },
4
+ name: { type: String, required: true },
5
+ meta: { type: String, required: true },
6
+ menuItems: { type: Array, required: true },
7
+ disabled: { type: Boolean, required: false }
8
+ });
9
+ const emit = defineEmits(["open"]);
10
+ </script>
11
+
12
+ <template>
13
+ <div w-full max-w-480px rounded-12px border-0.5px border bg overflow-hidden data-chat-file="card">
14
+ <div flex items-center gap-12px p-12px>
15
+ <button
16
+ type="button"
17
+ flex items-center gap-12px flex-1 min-w-0 text-left
18
+ :class="disabled ? 'cursor-default' : 'cursor-pointer'"
19
+ :disabled="disabled"
20
+ :aria-label="`Abrir ${name}`"
21
+ @click="emit('open')"
22
+ >
23
+ <span size-40px rounded-8px bg-subtle flex items-center justify-center shrink-0>
24
+ <img :src="iconSrc" alt="" size-20px>
25
+ </span>
26
+ <span flex flex-col min-w-0>
27
+ <span body-14-medium text-primary truncate :title="name">{{ name }}</span>
28
+ <span body-12-regular text-secondary truncate>{{ meta }}</span>
29
+ </span>
30
+ </button>
31
+ <slot name="actions" />
32
+ <TelaDropdownMenu :items="menuItems" align="end">
33
+ <TelaIconButton
34
+ icon="i-ph-dots-three"
35
+ color="secondary"
36
+ size="3xs"
37
+ aria-label="Ações do arquivo"
38
+ :disabled="disabled"
39
+ />
40
+ </TelaDropdownMenu>
41
+ </div>
42
+ <slot />
43
+ </div>
44
+ </template>
@@ -0,0 +1,31 @@
1
+ export interface ChatFileMenuItem {
2
+ label: string;
3
+ icon: string;
4
+ click: () => void;
5
+ }
6
+ type __VLS_Props = {
7
+ iconSrc: string;
8
+ name: string;
9
+ meta: string;
10
+ menuItems: ChatFileMenuItem[];
11
+ disabled?: boolean;
12
+ };
13
+ declare var __VLS_1: {}, __VLS_14: {};
14
+ type __VLS_Slots = {} & {
15
+ actions?: (props: typeof __VLS_1) => any;
16
+ } & {
17
+ default?: (props: typeof __VLS_14) => any;
18
+ };
19
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
+ open: () => any;
21
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
22
+ onOpen?: (() => any) | undefined;
23
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
25
+ declare const _default: typeof __VLS_export;
26
+ export default _default;
27
+ type __VLS_WithSlots<T, S> = T & {
28
+ new (): {
29
+ $slots: S;
30
+ };
31
+ };
@@ -2,8 +2,11 @@
2
2
  import { useFilePreviewPanel } from "../composables/file-preview-panel";
3
3
  import { useMediaCache } from "../composables/media-cache";
4
4
  import { useVault } from "../composables/vault";
5
+ import ChatFileAudio from "./chat-file-audio.vue";
6
+ import ChatFileCard from "./chat-file-card.vue";
5
7
  import { FILE_ICONS } from "../icons";
6
8
  import { MIME_TO_EXTENSION, normalizeMimeType } from "../mime";
9
+ import { describeFile, resolveFilePresentation } from "../presentation";
7
10
  import { isVaultPermalink } from "../vault-links";
8
11
  const props = defineProps({
9
12
  file: { type: Object, required: true }
@@ -11,32 +14,17 @@ const props = defineProps({
11
14
  const { getFileInfo, isVaultReference } = useVault();
12
15
  const { getCached, setCache, fetchUrlWithMetadata, base64ToDataUrl } = useMediaCache();
13
16
  const { openPreview } = useFilePreviewPanel();
14
- const FILE_COLORS = {
15
- default: { bg: "bg-gray-100", text: "text-gray-600", border: "border-gray-200" }
16
- };
17
17
  const resolvedUrl = ref();
18
18
  const cachedBase64 = ref();
19
19
  const originalFileName = ref();
20
20
  const resolvedMimeType = ref();
21
21
  const isLoading = ref(false);
22
- const isHovered = ref(false);
23
22
  const fileSize = ref();
24
- const pdfTotalPages = ref();
25
- const csvInfo = ref();
26
- const textInfo = ref();
23
+ const mediaFailed = ref(false);
27
24
  const effectiveMimeType = computed(() => resolvedMimeType.value ?? props.file.mimeType);
28
25
  const normalizedMimeType = computed(() => normalizeMimeType(effectiveMimeType.value));
26
+ const presentation = computed(() => resolveFilePresentation(normalizedMimeType.value));
29
27
  const iconSrc = computed(() => FILE_ICONS[normalizedMimeType.value] ?? FILE_ICONS.unknown);
30
- const fileColors = computed(() => FILE_COLORS.default ?? { bg: "bg-gray-100", text: "text-gray-600", border: "border-gray-200" });
31
- const isPdf = computed(() => normalizedMimeType.value === "application/pdf");
32
- const isImage = computed(() => normalizedMimeType.value.startsWith("image/"));
33
- const isVideo = computed(() => normalizedMimeType.value.startsWith("video/"));
34
- const isAudio = computed(() => normalizedMimeType.value.startsWith("audio/"));
35
- const isCsv = computed(() => normalizedMimeType.value === "text/csv");
36
- const isJson = computed(() => normalizedMimeType.value === "application/json");
37
- const isText = computed(() => normalizedMimeType.value.startsWith("text/") && !isCsv.value || isJson.value);
38
- const initialPage = computed(() => props.file.hashParams?.pageNumber);
39
- const hasInlinePreview = computed(() => isPdf.value || isImage.value || isVideo.value || isAudio.value || isCsv.value || isText.value);
40
28
  const effectiveUrl = computed(() => {
41
29
  if (cachedBase64.value && normalizedMimeType.value)
42
30
  return base64ToDataUrl(cachedBase64.value, normalizedMimeType.value);
@@ -50,32 +38,17 @@ const displayName = computed(() => {
50
38
  const ext = MIME_TO_EXTENSION[normalizedMimeType.value];
51
39
  return ext ? `${name}.${ext}` : name;
52
40
  });
53
- const shortName = computed(() => {
54
- const name = displayName.value;
55
- if (name.length <= 24)
56
- return name;
57
- const ext = name.split(".").pop() ?? "";
58
- const baseName = name.slice(0, name.length - ext.length - 1);
59
- return `${baseName.slice(0, 16)}...${ext ? `.${ext}` : ""}`;
60
- });
61
- const fileExtension = computed(() => {
62
- const parts = displayName.value.split(".");
63
- return parts.length > 1 ? parts.pop()?.toUpperCase() : "ARQUIVO";
64
- });
65
- const formattedSize = computed(() => {
66
- if (!fileSize.value)
67
- return null;
68
- const bytes = fileSize.value;
69
- if (bytes < 1024)
70
- return `${bytes} B`;
71
- if (bytes < 1024 * 1024)
72
- return `${(bytes / 1024).toFixed(1)} KB`;
73
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
74
- });
41
+ const mediaUnavailable = computed(() => mediaFailed.value || (presentation.value === "image" || presentation.value === "video") && !effectiveUrl.value);
42
+ const meta = computed(() => describeFile(normalizedMimeType.value, displayName.value));
43
+ const menuItems = computed(() => [
44
+ { label: "Abrir", icon: "i-ph-arrow-square-out", click: openSidePanel },
45
+ { label: "Baixar", icon: "i-ph-download-simple", click: () => void download() }
46
+ ]);
75
47
  async function resolveVaultUrl() {
76
48
  const reference = props.file.reference;
77
49
  const cleanReference = reference.split("#")[0] ?? reference;
78
50
  isLoading.value = true;
51
+ mediaFailed.value = false;
79
52
  const cacheKey = cleanReference;
80
53
  try {
81
54
  const cached = await getCached(cacheKey);
@@ -142,177 +115,98 @@ function openSidePanel() {
142
115
  cachedBase64: cachedBase64.value,
143
116
  originalFileName: originalFileName.value,
144
117
  resolvedMimeType: resolvedMimeType.value,
145
- fileSize: fileSize.value,
146
- pdfTotalPages: pdfTotalPages.value,
147
- csvInfo: csvInfo.value,
148
- textInfo: textInfo.value
118
+ fileSize: fileSize.value
149
119
  });
150
120
  }
151
- function onPdfLoaded(data) {
152
- pdfTotalPages.value = data.totalPages;
153
- }
154
- function onCsvLoaded(data) {
155
- csvInfo.value = data;
156
- }
157
- function onTextLoaded(data) {
158
- textInfo.value = data;
121
+ async function download() {
122
+ const url = effectiveUrl.value;
123
+ if (!url)
124
+ return;
125
+ try {
126
+ const response = await fetch(url);
127
+ if (!response.ok)
128
+ throw new Error(`Download failed with status ${response.status}`);
129
+ const blob = await response.blob();
130
+ const blobUrl = URL.createObjectURL(blob);
131
+ const link = document.createElement("a");
132
+ link.href = blobUrl;
133
+ link.download = displayName.value;
134
+ document.body.appendChild(link);
135
+ link.click();
136
+ document.body.removeChild(link);
137
+ URL.revokeObjectURL(blobUrl);
138
+ } catch (error) {
139
+ console.error("[FilePreview] Failed to download file", error);
140
+ window.open(url, "_blank", "noopener");
141
+ }
159
142
  }
160
143
  </script>
161
144
 
162
145
  <template>
163
- <div>
164
- <!-- Wide Layout for files with inline preview -->
165
- <div v-if="hasInlinePreview" class="w-full">
166
- <!-- Header -->
167
- <div class="flex items-center gap-12px p-12px rounded-t-16px" style="background-color: #F5F5F5;">
168
- <!-- File Icon -->
169
- <img :src="iconSrc" :alt="displayName" class="w-16px h-16px flex-shrink-0">
170
-
171
- <!-- File Info -->
172
- <div class="flex-1 min-w-0">
173
- <p class="text-14px font-medium text-gray-900 truncate">
174
- {{ displayName }}
175
- </p>
176
- <p class="text-12px text-gray-500">
177
- <span v-if="isPdf && pdfTotalPages">{{ pdfTotalPages }} páginas</span>
178
- <span v-else-if="isCsv && csvInfo">{{ csvInfo.totalRows }} linhas · {{ csvInfo.totalColumns }} colunas</span>
179
- <span v-else-if="isText && textInfo">{{ textInfo.totalLines }} linhas</span>
180
- <span v-if="(isPdf && pdfTotalPages || isCsv && csvInfo || isText && textInfo) && formattedSize"> · </span>
181
- <span v-if="formattedSize">{{ formattedSize }}</span>
182
- <span v-if="isLoading">Carregando...</span>
183
- </p>
184
- </div>
185
-
186
- <!-- Visualizar Button -->
187
- <TelaButton
188
- variant="secondary"
189
- :disabled="isLoading || !effectiveUrl"
190
- @click="openSidePanel"
191
- >
192
- Visualizar
193
- </TelaButton>
194
- </div>
195
-
196
- <!-- Preview Content -->
197
- <div v-if="effectiveUrl" class="rounded-b-16px overflow-hidden" style="background-color: #F5F5F5;">
198
- <!-- PDF Preview -->
199
- <PdfPreview
200
- v-if="isPdf"
201
- :url="effectiveUrl"
202
- :initial-page="initialPage"
203
- :max-height="300"
204
- :clickable="false"
205
- @loaded="onPdfLoaded"
206
- @click="openSidePanel"
207
- />
208
-
209
- <!-- Image Preview -->
210
- <ImagePreview
211
- v-else-if="isImage"
212
- :url="effectiveUrl"
213
- :max-height="300"
214
- :clickable="false"
215
- @click="openSidePanel"
216
- />
217
-
218
- <!-- Video Preview -->
219
- <VideoPreview
220
- v-else-if="isVideo"
221
- :url="effectiveUrl"
222
- :max-height="300"
223
- :clickable="false"
224
- @click="openSidePanel"
225
- />
226
-
227
- <!-- Audio Preview -->
228
- <AudioPreview
229
- v-else-if="isAudio"
230
- :url="effectiveUrl"
231
- :name="displayName"
232
- :clickable="false"
233
- @click="openSidePanel"
234
- />
235
-
236
- <!-- CSV Preview -->
237
- <CsvPreview
238
- v-else-if="isCsv"
239
- :url="effectiveUrl"
240
- :max-height="300"
241
- :clickable="false"
242
- @loaded="onCsvLoaded"
243
- @click="openSidePanel"
244
- />
245
-
246
- <!-- Text Preview -->
247
- <TextPreview
248
- v-else-if="isText"
249
- :url="effectiveUrl"
250
- :mime-type="normalizedMimeType"
251
- :max-height="300"
252
- :clickable="false"
253
- @loaded="onTextLoaded"
254
- @click="openSidePanel"
255
- />
256
- </div>
257
- </div>
258
-
259
- <!-- Regular File Card (for unsupported types) -->
146
+ <TelaSkeleton v-if="isLoading" class="rounded-12px" w-full max-w-480px h-64px data-chat-file="loading" />
147
+ <div v-else-if="presentation === 'image' && !mediaUnavailable" data-chat-file="media">
260
148
  <button
261
- v-else
262
149
  type="button"
263
- class="group relative w-140px rounded-12px border overflow-hidden transition-all duration-200"
264
- :class="[
265
- fileColors.border,
266
- isLoading || !effectiveUrl ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:shadow-md hover:scale-102'
267
- ]"
268
- :disabled="isLoading || !effectiveUrl"
269
- @mouseenter="isHovered = true"
270
- @mouseleave="isHovered = false"
150
+ block p-0 border-0 bg-transparent cursor-zoom-in
151
+ aria-label="Abrir imagem"
271
152
  @click="openSidePanel"
272
153
  >
273
- <!-- Thumbnail Area -->
274
- <div
275
- class="relative h-80px flex items-center justify-center overflow-hidden bg-gray-100"
154
+ <img
155
+ :src="effectiveUrl"
156
+ :alt="displayName"
157
+ block max-w-full max-h-480px rounded-12px object-contain
158
+ @error="mediaFailed = true"
276
159
  >
277
- <!-- Loading State -->
278
- <div v-if="isLoading" class="flex items-center justify-center w-full h-full">
279
- <TelaSkeleton class="rounded-8px" style="width: 120px; height: 60px;" />
280
- </div>
281
-
282
- <!-- File Icon -->
283
- <div v-else class="flex flex-col items-center gap-4px">
284
- <img :src="iconSrc" :alt="displayName" class="w-32px h-32px">
285
- <span class="text-10px font-bold uppercase tracking-wide px-6px py-2px rounded-4px bg-white/80 text-gray-600">
286
- {{ fileExtension }}
287
- </span>
288
- </div>
289
-
290
- <!-- Hover Overlay -->
291
- <Transition name="fade">
292
- <div
293
- v-if="isHovered && !isLoading && effectiveUrl"
294
- class="absolute inset-0 bg-black/50 flex items-center justify-center"
295
- >
296
- <div class="w-36px h-36px rounded-full bg-white flex items-center justify-center shadow-lg">
297
- <span class="i-ph-eye text-18px text-gray-700" />
298
- </div>
299
- </div>
300
- </Transition>
301
- </div>
302
-
303
- <!-- File Info -->
304
- <div class="px-10px py-8px bg-white">
305
- <p class="text-12px font-medium text-gray-800 truncate text-left">
306
- {{ shortName }}
307
- </p>
308
- <p v-if="formattedSize" class="text-10px text-gray-400 text-left mt-2px">
309
- {{ formattedSize }}
310
- </p>
311
- </div>
312
160
  </button>
313
161
  </div>
162
+ <ChatFileCard
163
+ v-else-if="presentation === 'video' && !mediaUnavailable"
164
+ :icon-src="iconSrc"
165
+ :name="displayName"
166
+ :meta="meta"
167
+ :menu-items="menuItems"
168
+ @open="openSidePanel"
169
+ >
170
+ <video
171
+ :src="effectiveUrl"
172
+ controls
173
+ preload="metadata"
174
+ block w-full max-h-480px bg-black border-t-0.5px border
175
+ data-chat-file="media"
176
+ @error="mediaFailed = true"
177
+ />
178
+ </ChatFileCard>
179
+ <ChatFileAudio
180
+ v-else-if="presentation === 'audio'"
181
+ :url="effectiveUrl"
182
+ :icon-src="iconSrc"
183
+ :name="displayName"
184
+ :meta="meta"
185
+ :menu-items="menuItems"
186
+ @open="openSidePanel"
187
+ />
188
+ <ChatFileCard
189
+ v-else-if="presentation === 'card' || mediaUnavailable"
190
+ :icon-src="iconSrc"
191
+ :name="displayName"
192
+ :meta="mediaUnavailable ? 'Falha ao carregar a pr\xE9via' : meta"
193
+ :menu-items="menuItems"
194
+ :disabled="!effectiveUrl"
195
+ @open="openSidePanel"
196
+ />
197
+ <button
198
+ v-else
199
+ type="button"
200
+ class="chat-file-link"
201
+ inline-flex items-center gap-6px max-w-full text-left
202
+ :disabled="!effectiveUrl"
203
+ @click="openSidePanel"
204
+ >
205
+ <img :src="iconSrc" alt="" size-14px shrink-0>
206
+ <span truncate>{{ displayName }}</span>
207
+ </button>
314
208
  </template>
315
209
 
316
210
  <style scoped>
317
- .fade-enter-active,.fade-leave-active{transition:opacity .15s ease}.fade-enter-from,.fade-leave-to{opacity:0}.hover\:scale-102:hover{transform:scale(1.02)}
211
+ .chat-file-link{color:inherit;cursor:pointer;text-decoration:underline;text-decoration-color:#a3a3a3;text-decoration-style:dotted;text-decoration-thickness:1px;text-underline-offset:3px;transition:text-decoration-color .15s ease}.chat-file-link:hover{text-decoration-color:#525252}.chat-file-link:disabled{cursor:default}
318
212
  </style>