@meistrari/chat-nuxt 1.7.0 → 1.8.0

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 (90) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +2 -0
  3. package/dist/runtime/components/MeistrariChatEmbed.vue +5 -1
  4. package/dist/runtime/components/chat/bash-widget.vue +3 -3
  5. package/dist/runtime/components/chat/doc-sdk-widget.vue +3 -3
  6. package/dist/runtime/components/chat/files-view.vue +3 -64
  7. package/dist/runtime/components/chat/generic-tool-widget.vue +3 -3
  8. package/dist/runtime/components/chat/grep-widget.vue +3 -3
  9. package/dist/runtime/components/chat/message-bubble.vue +1 -1
  10. package/dist/runtime/components/chat/message-input.d.vue.ts +2 -0
  11. package/dist/runtime/components/chat/message-input.vue +21 -5
  12. package/dist/runtime/components/chat/message-input.vue.d.ts +2 -0
  13. package/dist/runtime/components/chat/message-list.vue +1 -1
  14. package/dist/runtime/components/chat/mobile/files/file-preview.d.vue.ts +11 -0
  15. package/dist/runtime/components/chat/mobile/files/file-preview.vue +112 -0
  16. package/dist/runtime/components/chat/mobile/files/file-preview.vue.d.ts +11 -0
  17. package/dist/runtime/components/chat/mobile/files/files-view.d.vue.ts +10 -0
  18. package/dist/runtime/components/chat/mobile/files/files-view.vue +384 -0
  19. package/dist/runtime/components/chat/mobile/files/files-view.vue.d.ts +10 -0
  20. package/dist/runtime/components/chat/mobile/home/header.d.vue.ts +19 -0
  21. package/dist/runtime/components/chat/mobile/home/header.vue +55 -0
  22. package/dist/runtime/components/chat/mobile/home/header.vue.d.ts +19 -0
  23. package/dist/runtime/components/chat/mobile/home/recents.d.vue.ts +14 -0
  24. package/dist/runtime/components/chat/mobile/home/recents.vue +48 -0
  25. package/dist/runtime/components/chat/mobile/home/recents.vue.d.ts +14 -0
  26. package/dist/runtime/components/chat/mobile/home/suggestions.d.vue.ts +7 -0
  27. package/dist/runtime/components/chat/mobile/home/suggestions.vue +31 -0
  28. package/dist/runtime/components/chat/mobile/home/suggestions.vue.d.ts +7 -0
  29. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.d.vue.ts +21 -0
  30. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.vue +48 -0
  31. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.vue.d.ts +21 -0
  32. package/dist/runtime/components/chat/mobile/primitives/info-bubble.d.vue.ts +6 -0
  33. package/dist/runtime/components/chat/mobile/primitives/info-bubble.vue +50 -0
  34. package/dist/runtime/components/chat/mobile/primitives/info-bubble.vue.d.ts +6 -0
  35. package/dist/runtime/components/chat/mobile/shell/drawer.d.vue.ts +22 -0
  36. package/dist/runtime/components/chat/mobile/shell/drawer.vue +143 -0
  37. package/dist/runtime/components/chat/mobile/shell/drawer.vue.d.ts +22 -0
  38. package/dist/runtime/components/chat/mobile/shell/header.d.vue.ts +40 -0
  39. package/dist/runtime/components/chat/mobile/shell/header.vue +139 -0
  40. package/dist/runtime/components/chat/mobile/shell/header.vue.d.ts +40 -0
  41. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.d.vue.ts +14 -0
  42. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.vue +47 -0
  43. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.vue.d.ts +14 -0
  44. package/dist/runtime/components/chat/mobile/shell/shell.d.vue.ts +122 -0
  45. package/dist/runtime/components/chat/mobile/shell/shell.vue +283 -0
  46. package/dist/runtime/components/chat/mobile/shell/shell.vue.d.ts +122 -0
  47. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.d.vue.ts +19 -0
  48. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.vue +51 -0
  49. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.vue.d.ts +19 -0
  50. package/dist/runtime/components/chat/mobile/usage/usage-view.d.vue.ts +8 -0
  51. package/dist/runtime/components/chat/mobile/usage/usage-view.vue +243 -0
  52. package/dist/runtime/components/chat/mobile/usage/usage-view.vue.d.ts +8 -0
  53. package/dist/runtime/components/chat/model-selector.vue +6 -0
  54. package/dist/runtime/components/chat/read-widget.vue +3 -3
  55. package/dist/runtime/components/chat/reasoning-step-item.vue +1 -1
  56. package/dist/runtime/components/chat/reasoning-steps.vue +1 -1
  57. package/dist/runtime/components/chat/reasoning-thought.vue +2 -3
  58. package/dist/runtime/components/chat/tela-skill-widget.vue +3 -3
  59. package/dist/runtime/components/chat/thinking-widget.vue +3 -3
  60. package/dist/runtime/components/chat/usage-view.vue +20 -101
  61. package/dist/runtime/components/chat/web-search-widget.vue +3 -3
  62. package/dist/runtime/components/chat/workstation-query-widget.vue +6 -6
  63. package/dist/runtime/components/chat/write-widget.vue +3 -3
  64. package/dist/runtime/composables/useConversationFileDownloads.d.ts +6 -0
  65. package/dist/runtime/composables/useConversationFileDownloads.js +50 -0
  66. package/dist/runtime/composables/useConversationUsage.d.ts +13 -0
  67. package/dist/runtime/composables/useConversationUsage.js +121 -0
  68. package/dist/runtime/composables/useEmbedConfig.d.ts +2 -0
  69. package/dist/runtime/composables/useEmbedConfig.js +4 -0
  70. package/dist/runtime/composables/useMentionAutocomplete.d.ts +6 -0
  71. package/dist/runtime/composables/useMentionAutocomplete.js +71 -23
  72. package/dist/runtime/composables/useWorkspaceSwitchModal.d.ts +5 -0
  73. package/dist/runtime/composables/useWorkspaceSwitchModal.js +14 -0
  74. package/dist/runtime/embed/components/ChatConfigurationModal.vue +119 -13
  75. package/dist/runtime/embed/components/ChatEmbed.vue +4 -0
  76. package/dist/runtime/embed/components/ChatEmbedInner.d.vue.ts +2 -0
  77. package/dist/runtime/embed/components/ChatEmbedInner.vue +201 -11
  78. package/dist/runtime/embed/components/ChatEmbedInner.vue.d.ts +2 -0
  79. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.d.vue.ts +34 -0
  80. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue +95 -0
  81. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue.d.ts +34 -0
  82. package/dist/runtime/server/api/conversations/[id]/messages/[messageId]/retry.post.js +3 -1
  83. package/dist/runtime/server/api/conversations/[id]/messages/index.get.js +29 -5
  84. package/dist/runtime/server/utils/conversation-message-sync.d.ts +8 -2
  85. package/dist/runtime/server/utils/conversation-message-sync.js +52 -6
  86. package/dist/runtime/types/chat-auth.d.ts +1 -0
  87. package/dist/runtime/types/embed.d.ts +2 -0
  88. package/dist/runtime/utils/breakpoints.d.ts +1 -0
  89. package/dist/runtime/utils/breakpoints.js +1 -0
  90. package/package.json +1 -1
@@ -0,0 +1,243 @@
1
+ <script setup>
2
+ import ChatMobileInfoBubble from "../primitives/info-bubble.vue";
3
+ import { formatTokenCount } from "../../../../utils/calculate-usage";
4
+ import { formatDateWithTime } from "../../../../utils/format-time";
5
+ const props = defineProps({
6
+ conversationId: { type: String, required: true },
7
+ createdAt: { type: [String, Date], required: false },
8
+ updatedAt: { type: [String, Date], required: false }
9
+ });
10
+ const conversationIdRef = computed(() => props.conversationId);
11
+ const {
12
+ usageData,
13
+ loading,
14
+ error,
15
+ cacheInsight,
16
+ costBreakdown,
17
+ modelRows,
18
+ costBarSegments,
19
+ formatBillingCost,
20
+ fetchUsage
21
+ } = useConversationUsage(conversationIdRef);
22
+ const COLOR_INPUT = "#6469E8";
23
+ const COLOR_OUTPUT = "#F5B86C";
24
+ const COLOR_REUSED = "#A57BE0";
25
+ const COLOR_NEW = "#7BD9A6";
26
+ const segmentColor = {
27
+ input: COLOR_INPUT,
28
+ output: COLOR_OUTPUT
29
+ };
30
+ const breakdownTooltip = {
31
+ input: "Custo dos tokens enviados (prompt + contexto).",
32
+ output: "Custo dos tokens gerados pelo modelo."
33
+ };
34
+ onMounted(fetchUsage);
35
+ watch(() => props.conversationId, fetchUsage);
36
+ </script>
37
+
38
+ <template>
39
+ <div class="usage-screen" pt-54px h-full overflow-y-auto>
40
+ <div v-if="loading" flex="~ col" items-center justify-center gap-16px py-64px>
41
+ <span i-ph-spinner animate-spin text-32px text-neutral-400 />
42
+ <p text-14px text-neutral-500>
43
+ Carregando dados de uso...
44
+ </p>
45
+ </div>
46
+
47
+ <div v-else-if="error" flex="~ col" items-center justify-center gap-16px py-64px px-24px>
48
+ <span i-ph-warning text-32px text-amber-500 />
49
+ <p text-14px text-neutral-500 text-center>
50
+ {{ error }}
51
+ </p>
52
+ <button
53
+ px-16px py-8px rounded-8px
54
+ bg-neutral-100 text-neutral-900 text-14px
55
+ @click="fetchUsage"
56
+ >
57
+ Tentar novamente
58
+ </button>
59
+ </div>
60
+
61
+ <div
62
+ v-else-if="!usageData"
63
+ flex="~ col" items-center justify-center
64
+ gap-14px px-28px py-40px text-center
65
+ >
66
+ <div
67
+ w-48px h-48px rounded-14px
68
+ bg-neutral-100 text-neutral-500
69
+ flex items-center justify-center
70
+ >
71
+ <span i-ph-chart-bar text-22px />
72
+ </div>
73
+ <div flex="~ col" gap-4px>
74
+ <p text-16px font-500 text-neutral-900 leading-20px tracking="-.2px">
75
+ Sem dados de uso
76
+ </p>
77
+ <p text-14px text-neutral-500 leading-18px max-w-240px>
78
+ Métricas de tokens e custo desta conversa aparecerão aqui.
79
+ </p>
80
+ </div>
81
+ </div>
82
+
83
+ <div v-else flex="~ col" gap-12px px-14px pt-12px pb-24px>
84
+ <!-- Hero card -->
85
+ <div class="card card--hero">
86
+ <div flex="~ col" gap-4px>
87
+ <span class="eyebrow">
88
+ Custo total
89
+ <ChatMobileInfoBubble text="Soma de todos os custos de processamento desta conversa." />
90
+ </span>
91
+ <span class="hero-number">
92
+ {{ formatBillingCost(usageData.cost.total) }}
93
+ </span>
94
+ </div>
95
+
96
+ <div class="hero-divider" />
97
+
98
+ <div flex="~ col">
99
+ <div class="stat-row stat-row--bordered">
100
+ <span class="stat-label">
101
+ Tokens utilizados
102
+ <ChatMobileInfoBubble text="Total de tokens consumidos por todos os modelos." />
103
+ </span>
104
+ <span class="stat-value">
105
+ {{ formatTokenCount(usageData.tokens.total) }}
106
+ </span>
107
+ </div>
108
+ <div class="stat-row stat-row--bordered">
109
+ <span class="stat-label">Data de criação</span>
110
+ <span class="stat-value">{{ formatDateWithTime(createdAt) }}</span>
111
+ </div>
112
+ <div class="stat-row">
113
+ <span class="stat-label">Último uso</span>
114
+ <span class="stat-value">{{ formatDateWithTime(updatedAt) }}</span>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Cost breakdown card -->
120
+ <div class="card">
121
+ <span class="card-title">Detalhamento de custos</span>
122
+
123
+ <div flex="~ col" mt-12px>
124
+ <div
125
+ v-for="(item, idx) in costBreakdown"
126
+ :key="item.key"
127
+ class="breakdown-row"
128
+ :class="{ 'breakdown-row--bordered': idx < costBreakdown.length - 1 }"
129
+ >
130
+ <span class="dot" :style="{ background: segmentColor[item.key] }" />
131
+ <span class="stat-label">
132
+ {{ item.label }}
133
+ <ChatMobileInfoBubble :text="breakdownTooltip[item.key]" />
134
+ </span>
135
+ <span flex-1 />
136
+ <span class="breakdown-tokens">
137
+ {{ formatTokenCount(item.tokens) }} tokens
138
+ </span>
139
+ <span class="breakdown-cost">{{ formatBillingCost(item.cost) }}</span>
140
+ <span class="breakdown-pct">({{ item.percentage }}%)</span>
141
+ </div>
142
+ </div>
143
+
144
+ <div v-if="costBarSegments.length > 0" mt-14px>
145
+ <div class="bar bar--track" aria-hidden="true">
146
+ <div
147
+ v-for="seg in costBarSegments"
148
+ :key="seg.key"
149
+ class="bar__fill"
150
+ :style="{ width: `${Math.max(seg.percentage, 1)}%`, background: segmentColor[seg.key] }"
151
+ />
152
+ </div>
153
+
154
+ <div flex items-center gap-14px mt-10px>
155
+ <div
156
+ v-for="seg in costBarSegments"
157
+ :key="seg.key"
158
+ flex items-center gap-4px
159
+ >
160
+ <span class="dot dot--sm" :style="{ background: segmentColor[seg.key] }" />
161
+ <span class="legend-label">{{ seg.label }}</span>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+
167
+ <!-- Context reuse card -->
168
+ <div v-if="cacheInsight.hasData" class="card">
169
+ <div flex items-baseline flex-wrap gap-8px>
170
+ <span class="reuse-number">{{ cacheInsight.percentage }}%</span>
171
+ <span class="reuse-tail">do contexto foi reutilizado</span>
172
+ </div>
173
+
174
+ <p class="reuse-caption">
175
+ Contexto reutilizado reduz o custo de processamento.
176
+ </p>
177
+
178
+ <div mt-14px>
179
+ <div class="bar bar--track" aria-hidden="true">
180
+ <div
181
+ class="bar__fill"
182
+ :style="{ width: `${cacheInsight.percentage}%`, background: COLOR_REUSED }"
183
+ />
184
+ <div
185
+ class="bar__fill"
186
+ :style="{ width: `${100 - cacheInsight.percentage}%`, background: COLOR_NEW }"
187
+ />
188
+ </div>
189
+ </div>
190
+
191
+ <div flex items-center justify-between mt-10px gap-8px>
192
+ <div flex items-center gap-4px>
193
+ <span class="dot dot--sm" :style="{ background: COLOR_REUSED }" />
194
+ <span class="reuse-token-bold">{{ formatTokenCount(cacheInsight.reusedTokens) }}</span>
195
+ <span class="legend-label">reutilizado</span>
196
+ </div>
197
+ <div flex items-center gap-4px>
198
+ <span class="dot dot--sm" :style="{ background: COLOR_NEW }" />
199
+ <span class="reuse-token-bold">{{ formatTokenCount(cacheInsight.storedTokens) }}</span>
200
+ <span class="legend-label">novo</span>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <!-- Models section -->
206
+ <div v-if="modelRows.length > 0" flex="~ col" gap-8px mt-4px>
207
+ <div class="models-header">
208
+ <span class="card-title">Modelos</span>
209
+ <ChatMobileInfoBubble text="Modelos auxiliares podem ser utilizados internamente para auxiliar o processamento." />
210
+ </div>
211
+
212
+ <div
213
+ v-for="row in modelRows"
214
+ :key="row.modelId + row.tokens"
215
+ class="card card--model"
216
+ >
217
+ <div flex items-center gap-10px>
218
+ <div class="provider-badge">
219
+ <IconsAnthropic w-14px h-14px text-current />
220
+ </div>
221
+ <span class="model-name">{{ row.modelName }}</span>
222
+ <span class="model-cost">{{ row.cost }}</span>
223
+ </div>
224
+
225
+ <div flex items-center gap-8px mt-10px>
226
+ <span class="model-tokens">{{ row.tokens }} tokens</span>
227
+ <div class="bar bar--track bar--thin" flex-1 aria-hidden="true">
228
+ <div
229
+ class="bar__fill"
230
+ :style="{ width: `${Math.max(row.costShare, 1)}%`, background: COLOR_INPUT }"
231
+ />
232
+ </div>
233
+ <span class="model-pct">{{ Math.round(row.costShare) }}%</span>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </template>
240
+
241
+ <style scoped>
242
+ .usage-screen{background:#fafafa;font-variant-numeric:tabular-nums}.card{background:#fff;border:.5px solid #e8e8e8;border-radius:12px;box-shadow:0 1px 4px rgba(103,127,148,.03);padding:16px}.card--hero{padding:18px 16px}.card--model{padding:14px}.eyebrow{align-items:center;color:#737373;display:inline-flex;font-size:12px;font-weight:500;gap:4px;letter-spacing:.1px;white-space:nowrap}.hero-number{color:#171717;font-size:40px;font-variant-numeric:tabular-nums;font-weight:580;letter-spacing:-.8px;line-height:1.05;margin-top:4px}.hero-divider{background:#f5f5f5;height:1px;margin:14px -16px 12px}.stat-row{align-items:center;display:flex;gap:8px;justify-content:space-between;padding:8px 0}.stat-row--bordered{border-bottom:.5px solid #f5f5f5}.stat-label{align-items:center;color:#737373;display:inline-flex;font-size:13px;font-weight:460}.stat-value{color:#171717;font-size:13px;font-variant-numeric:tabular-nums;font-weight:500;text-align:right}.card-title{color:#171717;font-size:14px;font-weight:580}.breakdown-row{align-items:center;display:flex;font-size:13px;gap:8px;padding:6px 0}.breakdown-row--bordered{border-bottom:.5px solid #f5f5f5}.dot{border-radius:999px;display:inline-block;flex-shrink:0;height:8px;width:8px}.dot--sm{height:7px;width:7px}.breakdown-tokens{color:#737373;font-size:12px;font-variant-numeric:tabular-nums;white-space:nowrap}.breakdown-cost{color:#171717;font-variant-numeric:tabular-nums;font-weight:500;min-width:52px;text-align:right}.breakdown-pct{color:#a3a3a3;font-size:11px;font-variant-numeric:tabular-nums;min-width:28px;text-align:right}.bar{display:flex;overflow:hidden}.bar--track{background:#f5f5f5;border-radius:999px;height:8px}.bar--thin{height:4px}.bar__fill{border-radius:999px;height:100%;transition:width .2s ease}.legend-label{color:#737373;font-size:11.5px}.reuse-number{color:#171717;font-size:32px;font-variant-numeric:tabular-nums;font-weight:580;letter-spacing:-.5px;line-height:1}.reuse-tail{color:#737373;font-size:14px;font-weight:460}.reuse-caption{color:#a3a3a3;font-size:12.5px;line-height:1.45;margin-top:6px}.reuse-token-bold{color:#171717;font-size:11.5px;font-variant-numeric:tabular-nums;font-weight:500}.models-header{align-items:center;display:flex;gap:6px;padding:0 4px}.provider-badge{align-items:center;background:#f5f5f5;border-radius:8px;color:#c2410c;display:inline-flex;flex-shrink:0;height:28px;justify-content:center;width:28px}.model-name{flex:1;font-size:13.5px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.model-cost,.model-name{color:#171717;font-weight:500}.model-cost{font-size:13px;font-variant-numeric:tabular-nums}.model-tokens{color:#737373;white-space:nowrap}.model-pct,.model-tokens{font-size:12px;font-variant-numeric:tabular-nums}.model-pct{color:#a3a3a3;min-width:30px;text-align:right}
243
+ </style>
@@ -0,0 +1,8 @@
1
+ type __VLS_Props = {
2
+ conversationId: string;
3
+ createdAt?: string | Date;
4
+ updatedAt?: string | Date;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: typeof __VLS_export;
8
+ export default _default;
@@ -1,6 +1,8 @@
1
1
  <script setup>
2
+ import { useBreakpoints } from "@vueuse/core";
2
3
  import { AVAILABLE_MODELS, DEFAULT_MODEL } from "../../types/chat";
3
4
  import AnthropicLogo from "../../components/icons/anthropic.vue";
5
+ import { CHAT_MOBILE_BREAKPOINT } from "../../utils/breakpoints";
4
6
  defineProps({
5
7
  disabled: { type: Boolean, required: false }
6
8
  });
@@ -11,6 +13,9 @@ const options = AVAILABLE_MODELS.map((m) => ({
11
13
  description: m.description,
12
14
  icon: AnthropicLogo
13
15
  }));
16
+ const breakpoints = useBreakpoints({ mobile: CHAT_MOBILE_BREAKPOINT });
17
+ const isMobile = breakpoints.smaller("mobile");
18
+ const contentClass = computed(() => isMobile.value ? "w-280px!" : "");
14
19
  </script>
15
20
 
16
21
  <template>
@@ -24,6 +29,7 @@ const options = AVAILABLE_MODELS.map((m) => ({
24
29
  :has-searchbar="false"
25
30
  :has-grouping-labels="false"
26
31
  :trigger-class="`border-none! ${disabled ? 'bg-[#F3F4F6]!' : ''}`"
32
+ :content-class="contentClass"
27
33
  label-class="!text-14px !leading-18px !font-normal -tracking-0.01em! !text-gray-500"
28
34
  icon-class="w-16px h-16px scale-110 mr-1px !text-gray-500"
29
35
  label-item-class="!text-gray-500"
@@ -113,9 +113,9 @@ const isContentCode = computed(() => {
113
113
  <template>
114
114
  <div class="bg-subtle rounded-16px border border-gray-200">
115
115
  <!-- Header -->
116
- <div class="px-24px py-24px flex items-center justify-between">
116
+ <div class="px-16px py-16px flex items-center justify-between">
117
117
  <div class="flex-1 min-w-0">
118
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
118
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
119
119
  Read
120
120
  </h3>
121
121
  <p class="text-12px text-secondary truncate mt-2px">
@@ -127,7 +127,7 @@ const isContentCode = computed(() => {
127
127
  </div>
128
128
 
129
129
  <!-- Content -->
130
- <div v-if="toolResult" class="px-24px pb-24px space-y-16px">
130
+ <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
131
131
  <!-- File path section -->
132
132
  <div>
133
133
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -28,7 +28,7 @@ function toggle() {
28
28
  <div class="relative flex gap-12px group/step">
29
29
  <!-- Status Icon -->
30
30
  <div class="flex flex-col items-center flex-shrink-0" :class="{ 'pb-8px': !isLast || !completed }">
31
- <div class="z-10 flex items-center justify-center w-24px h-24px group-hover/step:text-gray-700 transition-colors">
31
+ <div class="flex items-center justify-center w-24px h-24px group-hover/step:text-gray-700 transition-colors">
32
32
  <!-- COMPLETED STATE: Show specific type icons -->
33
33
  <template v-if="completed">
34
34
  <!-- Show error icon if there's an error -->
@@ -24,7 +24,7 @@ defineExpose({
24
24
  </script>
25
25
 
26
26
  <template>
27
- <div v-if="groupedReasoning.length > 0" flex="~ col" w-full p-16px>
27
+ <div v-if="groupedReasoning.length > 0" flex="~ col" w-full p-12px>
28
28
  <ChatReasoningMessage
29
29
  v-for="(step, index) in groupedReasoning"
30
30
  :key="getStepKey(step, index)"
@@ -176,7 +176,7 @@ onUnmounted(() => {
176
176
  flex
177
177
  items-center
178
178
  gap-4px
179
- px-16px
179
+ px-4px
180
180
  pt-16px
181
181
  pb-8px
182
182
  w-full
@@ -237,7 +237,7 @@ onUnmounted(() => {
237
237
  <div
238
238
  v-if="!completed && !isExpanded"
239
239
  border="~ 0.5 default rounded-16px"
240
- m-16px
240
+ mt-8px mb-16px
241
241
  cursor-pointer
242
242
  @click="toggleExpanded"
243
243
  >
@@ -268,7 +268,6 @@ onUnmounted(() => {
268
268
  v-if="isExpanded"
269
269
  overflow-hidden
270
270
  border="~ 0.5 default rounded-16px"
271
- mx-16px
272
271
  mb-16px
273
272
  >
274
273
  <ChatReasoningSteps
@@ -39,15 +39,15 @@ const argsValue = computed(() => {
39
39
  <template>
40
40
  <div class="bg-subtle rounded-16px border border-gray-200">
41
41
  <!-- Header -->
42
- <div class="px-24px py-24px flex items-center justify-between">
43
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
42
+ <div class="px-16px py-16px flex items-center justify-between">
43
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
44
44
  {{ skillName }}
45
45
  </h3>
46
46
  <TelaStatus :variant="statusVariant" :label="statusLabel" />
47
47
  </div>
48
48
 
49
49
  <!-- Content -->
50
- <div v-if="toolResult" class="px-24px pb-24px space-y-16px">
50
+ <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
51
51
  <!-- Arguments section -->
52
52
  <div v-if="hasArgs">
53
53
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -24,15 +24,15 @@ const title = computed(() => {
24
24
  <template>
25
25
  <div class="bg-subtle rounded-16px border border-gray-200">
26
26
  <!-- Header -->
27
- <div class="px-24px py-24px flex items-center justify-between">
28
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
27
+ <div class="px-16px py-16px flex items-center justify-between">
28
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
29
29
  {{ title }}
30
30
  </h3>
31
31
  <TelaStatus :variant="statusVariant" :label="statusLabel" />
32
32
  </div>
33
33
 
34
34
  <!-- Content -->
35
- <div class="px-24px pb-24px">
35
+ <div class="px-16px pb-16px">
36
36
  <div class="bg-white rounded-xl border-0.5px border-gray-200 px-3 py-2">
37
37
  <div class="reasoning-markdown text-14px text-primary">
38
38
  <ClientOnly>
@@ -1,111 +1,30 @@
1
1
  <script setup>
2
- import { formatTokenCount, formatCost, formatCredits, formatModelName } from "../../utils/calculate-usage";
2
+ import { formatTokenCount } from "../../utils/calculate-usage";
3
3
  import { formatDateWithTime } from "../../utils/format-time";
4
4
  const props = defineProps({
5
5
  conversationId: { type: String, required: true },
6
6
  createdAt: { type: [String, Date], required: false },
7
7
  updatedAt: { type: [String, Date], required: false }
8
8
  });
9
- const chatApi = useChatApi();
10
- const usageData = ref(null);
11
- const billing = ref(null);
12
- const loading = ref(false);
13
- const error = ref(null);
14
- function formatBillingCost(cost) {
15
- if (!billing.value || billing.value.mode === "credits") {
16
- return formatCredits(cost);
17
- }
18
- return formatCost(cost);
19
- }
20
- async function fetchUsage() {
21
- if (!props.conversationId)
22
- return;
23
- loading.value = true;
24
- error.value = null;
25
- try {
26
- const response = await $fetch(
27
- chatApi.path(`/conversations/${props.conversationId}/usage`),
28
- chatApi.withChatHeaders()
29
- );
30
- if (response.error) {
31
- error.value = response.error;
32
- } else {
33
- usageData.value = response.usage;
34
- billing.value = response.billing;
35
- }
36
- } catch (err) {
37
- console.error("Failed to fetch usage data:", err);
38
- error.value = "Falha ao carregar dados de uso";
39
- } finally {
40
- loading.value = false;
41
- }
42
- }
9
+ const conversationIdRef = computed(() => props.conversationId);
10
+ const {
11
+ usageData,
12
+ billing,
13
+ loading,
14
+ error,
15
+ cacheInsight,
16
+ costBreakdown,
17
+ modelRows,
18
+ costBarSegments,
19
+ formatBillingCost,
20
+ fetchUsage
21
+ } = useConversationUsage(conversationIdRef);
22
+ const segmentColorClass = {
23
+ input: "bg-indigo-400",
24
+ output: "bg-amber-400"
25
+ };
43
26
  onMounted(fetchUsage);
44
27
  watch(() => props.conversationId, fetchUsage);
45
- const cacheInsight = computed(() => {
46
- if (!usageData.value)
47
- return { percentage: 0, reusedTokens: 0, storedTokens: 0, hasData: false };
48
- const hasData = usageData.value.tokens.cacheRead > 0 || usageData.value.tokens.cacheCreation > 0;
49
- return {
50
- percentage: Math.round(usageData.value.metrics.cacheHitRate * 100),
51
- reusedTokens: usageData.value.tokens.cacheRead,
52
- storedTokens: usageData.value.tokens.cacheCreation,
53
- hasData
54
- };
55
- });
56
- const costBreakdown = computed(() => {
57
- if (!usageData.value)
58
- return [];
59
- const total = usageData.value.cost.total;
60
- return [
61
- {
62
- label: "Envio",
63
- tooltip: "Texto enviado ao modelo: perguntas, contexto e instru\xE7\xF5es",
64
- tokens: usageData.value.tokens.prompt,
65
- cost: usageData.value.cost.prompt,
66
- percentage: total > 0 ? Math.round(usageData.value.cost.prompt / total * 100) : 0
67
- },
68
- {
69
- label: "Respostas",
70
- tooltip: "Texto gerado pelo modelo",
71
- tokens: usageData.value.tokens.completion,
72
- cost: usageData.value.cost.completion,
73
- percentage: total > 0 ? Math.round(usageData.value.cost.completion / total * 100) : 0
74
- }
75
- ];
76
- });
77
- const modelRows = computed(() => {
78
- if (!usageData.value)
79
- return [];
80
- const totalCost = usageData.value.cost.total;
81
- return usageData.value.models.map((m) => {
82
- const totalTokens = m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheCreationTokens;
83
- return {
84
- modelId: m.model,
85
- modelName: formatModelName(m.model),
86
- tokens: formatTokenCount(totalTokens),
87
- cost: formatBillingCost(m.cost),
88
- costShare: totalCost > 0 ? m.cost / totalCost * 100 : 0
89
- };
90
- });
91
- });
92
- const costBarSegments = computed(() => {
93
- if (!usageData.value || usageData.value.cost.total === 0)
94
- return [];
95
- const total = usageData.value.cost.total;
96
- return [
97
- {
98
- label: "Envio",
99
- percentage: usageData.value.cost.prompt / total * 100,
100
- colorClass: "bg-indigo-400"
101
- },
102
- {
103
- label: "Respostas",
104
- percentage: usageData.value.cost.completion / total * 100,
105
- colorClass: "bg-amber-400"
106
- }
107
- ];
108
- });
109
28
  </script>
110
29
 
111
30
  <template>
@@ -216,7 +135,7 @@ const costBarSegments = computed(() => {
216
135
  <div
217
136
  v-for="seg in costBarSegments"
218
137
  :key="seg.label"
219
- :class="seg.colorClass"
138
+ :class="segmentColorClass[seg.key]"
220
139
  :style="{ width: `${Math.max(seg.percentage, 1)}%` }"
221
140
  h-full transition-all duration-300
222
141
  />
@@ -227,7 +146,7 @@ const costBarSegments = computed(() => {
227
146
  :key="seg.label"
228
147
  flex gap-4px items-center
229
148
  >
230
- <div w-6px h-6px rounded-full :class="seg.colorClass" />
149
+ <div w-6px h-6px rounded-full :class="segmentColorClass[seg.key]" />
231
150
  <span body-11-regular text-tertiary>{{ seg.label }}</span>
232
151
  </div>
233
152
  </div>
@@ -44,15 +44,15 @@ const sources = computed(() => {
44
44
  <template>
45
45
  <div class="bg-subtle rounded-16px border border-gray-200">
46
46
  <!-- Header -->
47
- <div class="px-24px py-24px flex items-center justify-between">
48
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
47
+ <div class="px-16px py-16px flex items-center justify-between">
48
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
49
49
  WebSearch
50
50
  </h3>
51
51
  <TelaStatus :variant="statusVariant" :label="statusLabel" />
52
52
  </div>
53
53
 
54
54
  <!-- Content -->
55
- <div v-if="toolResult" class="px-24px pb-24px space-y-16px">
55
+ <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
56
56
  <!-- Sources list -->
57
57
  <div v-if="sources.length > 0">
58
58
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -336,9 +336,9 @@ const displayName = computed(() => {
336
336
  <template>
337
337
  <div class="bg-subtle rounded-16px border border-gray-200">
338
338
  <!-- Header -->
339
- <div class="px-24px py-24px flex items-center justify-between">
339
+ <div class="px-16px py-16px flex items-center justify-between">
340
340
  <div class="flex-1 min-w-0">
341
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
341
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
342
342
  Consulta de Dados
343
343
  </h3>
344
344
  <p v-if="displayName !== 'Workstation'" class="text-12px text-secondary truncate mt-2px">
@@ -355,7 +355,7 @@ const displayName = computed(() => {
355
355
  <!-- Filters preview (when collapsed) -->
356
356
  <div
357
357
  v-if="!expanded && canExpand && (formattedFilters || formattedOutputQuery)"
358
- class="px-24px pb-12px"
358
+ class="px-16px pb-12px"
359
359
  >
360
360
  <div class="flex flex-wrap items-center gap-6px">
361
361
  <template v-if="formattedFilters">
@@ -386,7 +386,7 @@ const displayName = computed(() => {
386
386
  </div>
387
387
 
388
388
  <!-- Launch message (when only have launching output) -->
389
- <div v-if="toolResult && isLaunchMessage" class="px-24px pb-16px">
389
+ <div v-if="toolResult && isLaunchMessage" class="px-16px pb-16px">
390
390
  <div class="bg-blue-50 rounded-xl border-0.5px border-blue-200 px-16px py-12px">
391
391
  <div class="flex items-center gap-8px">
392
392
  <div class="i-ph-clock text-16px text-blue-600 animate-pulse" />
@@ -398,7 +398,7 @@ const displayName = computed(() => {
398
398
  </div>
399
399
 
400
400
  <!-- Expand/Collapse button -->
401
- <div v-if="toolResult && !isLaunchMessage" class="px-24px pb-12px">
401
+ <div v-if="toolResult && !isLaunchMessage" class="px-16px pb-12px">
402
402
  <button
403
403
  type="button"
404
404
  class="w-full flex items-center justify-center gap-6px px-12px py-8px text-13px font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-8px transition-colors"
@@ -413,7 +413,7 @@ const displayName = computed(() => {
413
413
  </div>
414
414
 
415
415
  <!-- Expanded content -->
416
- <div v-if="expanded && toolResult && !isLaunchMessage" class="px-24px pb-24px space-y-16px">
416
+ <div v-if="expanded && toolResult && !isLaunchMessage" class="px-16px pb-16px space-y-16px">
417
417
  <!-- Filters section -->
418
418
  <div v-if="formattedFilters || formattedOutputQuery">
419
419
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -92,9 +92,9 @@ const hasMoreLines = computed(() => {
92
92
  <template>
93
93
  <div class="bg-subtle rounded-16px border border-gray-200">
94
94
  <!-- Header -->
95
- <div class="px-24px py-24px flex items-center justify-between">
95
+ <div class="px-16px py-16px flex items-center justify-between">
96
96
  <div class="flex-1 min-w-0">
97
- <h3 class="text-20px font-580 leading-24px tracking-[-0.5px] text-primary truncate">
97
+ <h3 class="text-16px font-580 leading-20px tracking-[-0.3px] text-primary truncate">
98
98
  Write
99
99
  </h3>
100
100
  <p class="text-12px text-secondary truncate mt-2px">
@@ -106,7 +106,7 @@ const hasMoreLines = computed(() => {
106
106
  </div>
107
107
 
108
108
  <!-- Content -->
109
- <div v-if="toolResult" class="px-24px pb-24px space-y-16px">
109
+ <div v-if="toolResult" class="px-16px pb-16px space-y-16px">
110
110
  <!-- File path section -->
111
111
  <div>
112
112
  <div class="text-14px font-580 leading-16px tracking-[-0.15px] text-gray-700 mb-2">
@@ -0,0 +1,6 @@
1
+ import type { ConversationFile } from '../types/chat.js';
2
+ export declare function useConversationFileDownloads(): {
3
+ resolveDownloadUrl: (file: ConversationFile) => Promise<string>;
4
+ downloadFile: (file: ConversationFile) => Promise<void>;
5
+ copyFileLink: (file: ConversationFile) => Promise<void>;
6
+ };