@meistrari/chat-nuxt 1.1.3 → 1.2.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 (67) hide show
  1. package/README.md +70 -4
  2. package/dist/module.d.mts +5 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +58 -19
  5. package/dist/runtime/components/chat/conversation-list.d.vue.ts +1 -0
  6. package/dist/runtime/components/chat/conversation-list.vue +22 -2
  7. package/dist/runtime/components/chat/conversation-list.vue.d.ts +1 -0
  8. package/dist/runtime/components/chat/message-list.d.vue.ts +1 -0
  9. package/dist/runtime/components/chat/message-list.vue +23 -2
  10. package/dist/runtime/components/chat/message-list.vue.d.ts +1 -0
  11. package/dist/runtime/composables/useChat.d.ts +2 -1
  12. package/dist/runtime/composables/useChat.js +32 -3
  13. package/dist/runtime/composables/useChatApi.js +2 -1
  14. package/dist/runtime/composables/useConversations.d.ts +3 -2
  15. package/dist/runtime/composables/useConversations.js +99 -11
  16. package/dist/runtime/composables/useEmbedConfig.d.ts +11 -6
  17. package/dist/runtime/composables/useEmbedConfig.js +35 -7
  18. package/dist/runtime/composables/useIDB.js +4 -0
  19. package/dist/runtime/composables/usePdf.d.ts +840 -1
  20. package/dist/runtime/composables/usePdf.js +21 -3
  21. package/dist/runtime/composables/useWorkspaceContextFiles.d.ts +3 -3
  22. package/dist/runtime/composables/useWorkspaceContextFiles.js +19 -10
  23. package/dist/runtime/composables/useWorkspaceExternalSkills.d.ts +1 -1
  24. package/dist/runtime/composables/useWorkspaceExternalSkills.js +5 -6
  25. package/dist/runtime/composables/useWorkspacePersonalization.d.ts +1 -1
  26. package/dist/runtime/composables/useWorkspacePersonalization.js +4 -5
  27. package/dist/runtime/composables/useWorkspaceSettings.js +11 -8
  28. package/dist/runtime/embed/components/ChatConfigurationModal.d.vue.ts +25 -0
  29. package/dist/runtime/embed/components/ChatConfigurationModal.vue +373 -0
  30. package/dist/runtime/embed/components/ChatConfigurationModal.vue.d.ts +25 -0
  31. package/dist/runtime/embed/components/ChatEmbed.vue +12 -1
  32. package/dist/runtime/embed/components/ChatEmbedInner.vue +31 -16
  33. package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.d.vue.ts +22 -0
  34. package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.vue +133 -0
  35. package/dist/runtime/embed/components/configuration/ChatConfigurationContextFilesTab.vue.d.ts +22 -0
  36. package/dist/runtime/embed/components/configuration/ChatConfigurationCredentialsTab.d.vue.ts +10 -0
  37. package/dist/runtime/embed/components/configuration/ChatConfigurationCredentialsTab.vue +220 -0
  38. package/dist/runtime/embed/components/configuration/ChatConfigurationCredentialsTab.vue.d.ts +10 -0
  39. package/dist/runtime/embed/components/configuration/ChatConfigurationExternalSkillsTab.d.vue.ts +32 -0
  40. package/dist/runtime/embed/components/configuration/ChatConfigurationExternalSkillsTab.vue +222 -0
  41. package/dist/runtime/embed/components/configuration/ChatConfigurationExternalSkillsTab.vue.d.ts +32 -0
  42. package/dist/runtime/embed/components/configuration/ChatConfigurationKnowledgeSourcesTab.d.vue.ts +18 -0
  43. package/dist/runtime/embed/components/configuration/ChatConfigurationKnowledgeSourcesTab.vue +142 -0
  44. package/dist/runtime/embed/components/configuration/ChatConfigurationKnowledgeSourcesTab.vue.d.ts +18 -0
  45. package/dist/runtime/embed/components/configuration/ChatConfigurationPersonalizationTab.d.vue.ts +10 -0
  46. package/dist/runtime/embed/components/configuration/ChatConfigurationPersonalizationTab.vue +37 -0
  47. package/dist/runtime/embed/components/configuration/ChatConfigurationPersonalizationTab.vue.d.ts +10 -0
  48. package/dist/runtime/embed/components/configuration/ChatConfigurationTelaToolsTab.d.vue.ts +20 -0
  49. package/dist/runtime/embed/components/configuration/ChatConfigurationTelaToolsTab.vue +146 -0
  50. package/dist/runtime/embed/components/configuration/ChatConfigurationTelaToolsTab.vue.d.ts +20 -0
  51. package/dist/runtime/server/api/conversations/[id]/duplicate.post.js +45 -12
  52. package/dist/runtime/types/chat.d.ts +2 -1
  53. package/dist/runtime/types/workspace-settings.d.ts +3 -0
  54. package/dist/runtime/types/workspace-settings.js +0 -0
  55. package/dist/runtime/utils/conversation-list-skeleton.d.ts +31 -0
  56. package/dist/runtime/utils/conversation-list-skeleton.js +12 -0
  57. package/dist/runtime/utils/file.js +42 -42
  58. package/dist/types.d.mts +2 -0
  59. package/package.json +23 -27
  60. package/dist/runtime/composables/PageNavigation.d.ts +0 -13
  61. package/dist/runtime/composables/PageNavigation.js +0 -18
  62. /package/dist/runtime/components/{pdf-preview.d.vue.ts → pdf-preview.client.d.vue.ts} +0 -0
  63. /package/dist/runtime/components/{pdf-preview.vue → pdf-preview.client.vue} +0 -0
  64. /package/dist/runtime/components/{pdf-preview.vue.d.ts → pdf-preview.client.vue.d.ts} +0 -0
  65. /package/dist/runtime/components/{pdf-viewer.d.vue.ts → pdf-viewer.client.d.vue.ts} +0 -0
  66. /package/dist/runtime/components/{pdf-viewer.vue → pdf-viewer.client.vue} +0 -0
  67. /package/dist/runtime/components/{pdf-viewer.vue.d.ts → pdf-viewer.client.vue.d.ts} +0 -0
@@ -0,0 +1,25 @@
1
+ import type { DeepReadonly } from 'vue';
2
+ import type { WorkspaceSettings, WorkspaceSettingsUpdate } from '../../composables/useWorkspaceSettings.js';
3
+ export type ChatConfigurationTab = 'personalization' | 'context-files' | 'knowledge-sources' | 'tela-tools' | 'external-skills' | 'credentials';
4
+ export type ChatConfigurationSavePayload = {
5
+ updates: WorkspaceSettingsUpdate;
6
+ onSaved: () => void;
7
+ };
8
+ type __VLS_Props = {
9
+ open: boolean;
10
+ settings?: DeepReadonly<WorkspaceSettings> | WorkspaceSettings | null;
11
+ initialTab?: ChatConfigurationTab;
12
+ saving?: boolean;
13
+ };
14
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ save: (payload: ChatConfigurationSavePayload) => any;
16
+ "update:open": (value: boolean) => any;
17
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
18
+ onSave?: ((payload: ChatConfigurationSavePayload) => any) | undefined;
19
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
20
+ }>, {
21
+ initialTab: ChatConfigurationTab;
22
+ saving: boolean;
23
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
24
+ declare const _default: typeof __VLS_export;
25
+ export default _default;
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { provideEmbedConfig, useEmbedConfig } from "../../composables/useEmbedConfig";
2
3
  import ChatEmbedInner from "./ChatEmbedInner.vue";
3
4
  const props = defineProps({
4
5
  workspaceId: { type: String, required: true },
@@ -10,7 +11,17 @@ const props = defineProps({
10
11
  features: { type: Object, required: false }
11
12
  });
12
13
  defineEmits(["update:conversationId"]);
13
- provideEmbedConfig(props);
14
+ const parentEmbedConfig = useEmbedConfig();
15
+ if (import.meta.dev && parentEmbedConfig) {
16
+ watchEffect(() => {
17
+ const parentWorkspaceId = parentEmbedConfig.workspaceId.value.trim();
18
+ const propWorkspaceId = props.workspaceId.trim();
19
+ if (parentWorkspaceId && propWorkspaceId && parentWorkspaceId !== propWorkspaceId) {
20
+ console.warn(`[MeistrariChatEmbed] workspaceId prop (${propWorkspaceId}) differs from parent embed config (${parentWorkspaceId}); using prop value for this embed.`);
21
+ }
22
+ });
23
+ }
24
+ provideEmbedConfig(props, { syncGlobal: !parentEmbedConfig });
14
25
  </script>
15
26
 
16
27
  <template>
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { exportConversationToMarkdown } from "../../utils/export-conversation";
3
+ import { conversationListSkeletonRows } from "../../utils/conversation-list-skeleton";
3
4
  import {
4
5
  normalizeConversationId,
5
6
  resolveChatFeatures,
@@ -18,9 +19,9 @@ const embedConfig = useEmbedConfig();
18
19
  const { user: authUser } = useTelaApplicationAuth();
19
20
  const { getMember } = useWorkspaceMembers();
20
21
  const statusToast = useStatusToast();
21
- const { conversations, fetchConversations, createConversation, deleteConversation, renameConversation, duplicateConversation } = useConversations();
22
+ const { conversations, loading: conversationsLoading, fetchConversations, createConversation, deleteConversation, renameConversation, duplicateConversation, updateConversationInList } = useConversations();
22
23
  const { settings: workspaceSettingsState, fetchSettings } = useWorkspaceSettings();
23
- const effectiveWorkspaceSettings = computed(() => embedConfig?.workspaceSettings.value ?? workspaceSettingsState.value);
24
+ const effectiveWorkspaceSettings = computed(() => embedConfig?.workspaceSettings.value !== void 0 ? embedConfig.workspaceSettings.value : workspaceSettingsState.value);
24
25
  const { closePanel: closeFilesPanel } = useConversationFilesPanel();
25
26
  const activeConversationId = ref(
26
27
  normalizeConversationId(props.conversationId) ?? normalizeConversationId(props.initialConversationId)
@@ -45,6 +46,7 @@ const {
45
46
  animatedTitle,
46
47
  hasPendingMessage,
47
48
  loadConversation,
49
+ setConversation,
48
50
  sendMessage,
49
51
  retryMessage,
50
52
  cancelConversation,
@@ -200,7 +202,7 @@ const displayTitle = computed(() => {
200
202
  });
201
203
  watch(
202
204
  () => [currentConversation.value?.id ?? null, currentConversation.value?.title ?? null],
203
- ([conversationId, conversationTitle], [previousConversationId]) => {
205
+ ([conversationId, conversationTitle], [previousConversationId, previousTitle]) => {
204
206
  if (conversationId !== previousConversationId) {
205
207
  optimisticTitle.value = null;
206
208
  return;
@@ -208,6 +210,9 @@ watch(
208
210
  if (optimisticTitle.value !== null && conversationTitle === optimisticTitle.value) {
209
211
  optimisticTitle.value = null;
210
212
  }
213
+ if (conversationTitle && conversationTitle !== previousTitle && currentConversation.value) {
214
+ updateConversationInList(currentConversation.value);
215
+ }
211
216
  }
212
217
  );
213
218
  const currentUser = computed(() => props.user ?? authUser.value ?? null);
@@ -218,22 +223,23 @@ const conversationCreator = computed(
218
223
  })
219
224
  );
220
225
  async function handleSend(content, files, model) {
226
+ let createdConversationId = null;
221
227
  if (!activeConversationId.value || !currentConversation.value) {
222
228
  const conversation = await createConversation({
223
229
  model: model ?? selectedModel.value
224
230
  });
225
231
  if (!conversation)
226
232
  return;
227
- updateConversationId(conversation.id);
228
- await ensureConversationLoaded(conversation.id);
229
- if (!currentConversation.value) {
230
- statusToast.update({ text: "Erro ao carregar conversa", icon: "i-ph-warning" });
231
- return;
232
- }
233
+ setConversation(conversation);
234
+ createdConversationId = conversation.id;
233
235
  }
234
236
  if (!currentConversation.value)
235
237
  return;
236
- await sendMessage(content, files.length > 0 ? files : void 0, model);
238
+ const sendPromise = sendMessage(content, files.length > 0 ? files : void 0, model);
239
+ if (createdConversationId) {
240
+ updateConversationId(createdConversationId);
241
+ }
242
+ await sendPromise;
237
243
  }
238
244
  async function handleRetry(messageId) {
239
245
  await retryMessage(messageId);
@@ -295,8 +301,8 @@ async function handleDuplicate() {
295
301
  return;
296
302
  const duplicated = await duplicateConversation(currentConversation.value.id);
297
303
  if (duplicated) {
304
+ setConversation(duplicated);
298
305
  updateConversationId(duplicated.id);
299
- await ensureConversationLoaded(duplicated.id);
300
306
  }
301
307
  }
302
308
  async function handleExport() {
@@ -322,9 +328,6 @@ async function handleDelete() {
322
328
  await handleNewConversation();
323
329
  }
324
330
  }
325
- onBeforeUnmount(() => {
326
- clearConversation();
327
- });
328
331
  </script>
329
332
 
330
333
  <template>
@@ -351,7 +354,16 @@ onBeforeUnmount(() => {
351
354
  </div>
352
355
 
353
356
  <div flex-1 overflow-y-auto px-4px>
354
- <div v-if="visibleConversations.length === 0" p-16px text-center>
357
+ <div v-if="conversationsLoading && visibleConversations.length === 0" flex="~ col" gap-1px px-8px py-8px>
358
+ <div
359
+ v-for="row in conversationListSkeletonRows"
360
+ :key="row.id"
361
+ flex items-center h-32px px-8px rounded-8px
362
+ >
363
+ <TelaSkeleton rounded-6px bg-neutral-200 :style="{ width: row.width, height: '13px' }" />
364
+ </div>
365
+ </div>
366
+ <div v-else-if="visibleConversations.length === 0" p-16px text-center>
355
367
  <p text-14px text-neutral-400>
356
368
  Nenhuma conversa ainda
357
369
  </p>
@@ -449,7 +461,10 @@ onBeforeUnmount(() => {
449
461
  </template>
450
462
 
451
463
  <template v-else-if="loading">
452
- <div flex-1 pt-64px />
464
+ <ChatMessageList
465
+ :messages="[]"
466
+ loading
467
+ />
453
468
  <ChatMessageInput
454
469
  v-model:model="selectedModel"
455
470
  :disabled="true"
@@ -0,0 +1,22 @@
1
+ import type { ContextFile } from '../../../server/db/schema.js';
2
+ type __VLS_Props = {
3
+ searchQuery: string;
4
+ filteredContextFiles: readonly ContextFile[];
5
+ filteredPendingFiles: readonly File[];
6
+ currentFileCount: number;
7
+ maxFiles: number;
8
+ getFileType: (fileName: string) => string;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
11
+ handleFileSelect: (event: Event) => any;
12
+ removeContextFile: (url: string) => any;
13
+ removePendingFile: (file: File) => any;
14
+ "update:searchQuery": (value: string) => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onHandleFileSelect?: ((event: Event) => any) | undefined;
17
+ onRemoveContextFile?: ((url: string) => any) | undefined;
18
+ onRemovePendingFile?: ((file: File) => any) | undefined;
19
+ "onUpdate:searchQuery"?: ((value: string) => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ declare const _default: typeof __VLS_export;
22
+ export default _default;
@@ -0,0 +1,133 @@
1
+ <script setup>
2
+ defineProps({
3
+ searchQuery: { type: String, required: true },
4
+ filteredContextFiles: { type: Array, required: true },
5
+ filteredPendingFiles: { type: Array, required: true },
6
+ currentFileCount: { type: Number, required: true },
7
+ maxFiles: { type: Number, required: true },
8
+ getFileType: { type: Function, required: true }
9
+ });
10
+ const emit = defineEmits(["update:searchQuery", "handleFileSelect", "removePendingFile", "removeContextFile"]);
11
+ const fileInputRef = ref(null);
12
+ function openFileDialog() {
13
+ fileInputRef.value?.click();
14
+ }
15
+ function onFileChange(event) {
16
+ emit("handleFileSelect", event);
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <div mb-24px>
22
+ <h2 text-16px font-semibold text-primary tracking="-.2px" leading-20px>
23
+ Arquivos de contexto
24
+ </h2>
25
+ <div h-px bg-gray-200 mt-24px />
26
+ </div>
27
+
28
+ <input ref="fileInputRef" type="file" multiple hidden @change="onFileChange">
29
+
30
+ <div flex items-center justify-between mb-24px>
31
+ <div
32
+ flex items-center gap-8px
33
+ w-240px pl-12px pr-8px py-4px
34
+ bg-white b=".5px gray-200" rounded-10px
35
+ shadow-sm
36
+ >
37
+ <div i-ph-magnifying-glass text-12px text-gray-400 />
38
+ <input
39
+ :value="searchQuery"
40
+ type="text"
41
+ placeholder="Buscar"
42
+ text-14px text-primary placeholder:text-gray-400
43
+ bg-transparent border-none outline-none
44
+ flex-1 leading-18px tracking="-.15px"
45
+ @input="emit('update:searchQuery', $event.target.value)"
46
+ >
47
+ </div>
48
+ <button
49
+ px-16px py-10px
50
+ bg-white b=".5px gray-200" rounded-12px
51
+ text-16px font-semibold text-gray-900
52
+ cursor-pointer hover:bg-gray-50 transition
53
+ :disabled="currentFileCount >= maxFiles"
54
+ :class="{ 'opacity-50 cursor-not-allowed': currentFileCount >= maxFiles }"
55
+ @click="openFileDialog"
56
+ >
57
+ Enviar
58
+ </button>
59
+ </div>
60
+
61
+ <div w-full>
62
+ <div flex items-center b-y=".5px gray-200" bg-white>
63
+ <div flex-1 px-20px py-12px>
64
+ <span text-12px font-semibold text-primary leading-16px>Nome</span>
65
+ </div>
66
+ <div w-100px px-20px py-12px>
67
+ <span text-12px font-semibold text-primary leading-16px>Tipo</span>
68
+ </div>
69
+ <div w-40px />
70
+ </div>
71
+
72
+ <div>
73
+ <div
74
+ v-for="file in filteredContextFiles"
75
+ :key="`existing-${file.url}`"
76
+ flex items-center b-b=".5px gray-100" bg-white
77
+ >
78
+ <div flex items-center gap-8px flex-1 h-48px px-20px py-12px>
79
+ <div i-ph-file-pdf text-16px text-red-500 shrink-0 />
80
+ <span text-14px text-gray-900 leading-18px tracking="-.15px" truncate>
81
+ {{ file.name }}
82
+ </span>
83
+ </div>
84
+ <div w-100px h-48px px-20px py-12px flex items-center>
85
+ <span text-12px text-tertiary leading-16px>
86
+ {{ getFileType(file.name) }}
87
+ </span>
88
+ </div>
89
+ <div w-40px px-8px py-12px>
90
+ <button
91
+ p-4px rounded-6px hover:bg-gray-100 transition cursor-pointer
92
+ @click="emit('removeContextFile', file.url)"
93
+ >
94
+ <div i-ph-x text-16px text-primary />
95
+ </button>
96
+ </div>
97
+ </div>
98
+
99
+ <div
100
+ v-for="file in filteredPendingFiles"
101
+ :key="`pending-${file.name}-${file.size}-${file.lastModified}`"
102
+ flex items-center b-b=".5px blue-200" bg-blue-50
103
+ >
104
+ <div flex items-center gap-8px flex-1 h-48px px-20px py-12px>
105
+ <div i-ph-file-pdf text-16px text-blue-500 shrink-0 />
106
+ <span text-14px text-blue-900 leading-18px tracking="-.15px" truncate>
107
+ {{ file.name }}
108
+ </span>
109
+ </div>
110
+ <div w-100px h-48px px-20px py-12px flex items-center>
111
+ <span text-12px text-blue-500 leading-16px>
112
+ {{ getFileType(file.name) }}
113
+ </span>
114
+ </div>
115
+ <div w-40px px-8px py-12px>
116
+ <button
117
+ p-4px rounded-6px hover:bg-blue-100 transition cursor-pointer
118
+ @click="emit('removePendingFile', file)"
119
+ >
120
+ <div i-ph-x text-16px text-blue-500 />
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+ <div
126
+ v-if="filteredContextFiles.length === 0 && filteredPendingFiles.length === 0"
127
+ flex items-center justify-center py-48px text-14px text-tertiary
128
+ >
129
+ Nenhum arquivo adicionado ainda
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </template>
@@ -0,0 +1,22 @@
1
+ import type { ContextFile } from '../../../server/db/schema.js';
2
+ type __VLS_Props = {
3
+ searchQuery: string;
4
+ filteredContextFiles: readonly ContextFile[];
5
+ filteredPendingFiles: readonly File[];
6
+ currentFileCount: number;
7
+ maxFiles: number;
8
+ getFileType: (fileName: string) => string;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
11
+ handleFileSelect: (event: Event) => any;
12
+ removeContextFile: (url: string) => any;
13
+ removePendingFile: (file: File) => any;
14
+ "update:searchQuery": (value: string) => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onHandleFileSelect?: ((event: Event) => any) | undefined;
17
+ onRemoveContextFile?: ((url: string) => any) | undefined;
18
+ onRemovePendingFile?: ((file: File) => any) | undefined;
19
+ "onUpdate:searchQuery"?: ((value: string) => any) | undefined;
20
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
21
+ declare const _default: typeof __VLS_export;
22
+ export default _default;
@@ -0,0 +1,10 @@
1
+ type __VLS_Props = {
2
+ searchQuery: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
+ "update:searchQuery": (value: string) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ "onUpdate:searchQuery"?: ((value: string) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,220 @@
1
+ <script setup>
2
+ const props = defineProps({
3
+ searchQuery: { type: String, required: true }
4
+ });
5
+ const emit = defineEmits(["update:searchQuery"]);
6
+ const localSearchQuery = computed({
7
+ get: () => props.searchQuery,
8
+ set: (value) => emit("update:searchQuery", value)
9
+ });
10
+ const {
11
+ credentials,
12
+ loading: credentialsLoading,
13
+ fetchCredentials,
14
+ filteredCredentials,
15
+ credentialModalOpen,
16
+ deleteModalOpen,
17
+ credentialForm,
18
+ credentialSaving,
19
+ credentialDeleting,
20
+ credentialError,
21
+ editingCredential,
22
+ deletingCredential,
23
+ openCreateModal,
24
+ closeModal,
25
+ handleSave,
26
+ handleDelete,
27
+ getMenuItems,
28
+ formatLastRotated
29
+ } = useWorkspaceCredentials(localSearchQuery);
30
+ onMounted(() => {
31
+ if (!credentials.value.length)
32
+ fetchCredentials();
33
+ });
34
+ </script>
35
+
36
+ <template>
37
+ <div mb-24px>
38
+ <h2 text-16px font-semibold text-primary tracking="-.2px" leading-20px>
39
+ Credenciais
40
+ </h2>
41
+ <div h-px bg-gray-200 mt-24px />
42
+ </div>
43
+
44
+ <div flex items-center justify-between mb-16px>
45
+ <div
46
+ flex items-center gap-8px
47
+ w-240px pl-12px pr-8px py-4px
48
+ bg-white b=".5px gray-200" rounded-10px
49
+ shadow-sm
50
+ >
51
+ <div i-ph-magnifying-glass text-12px text-gray-400 />
52
+ <input
53
+ v-model="localSearchQuery"
54
+ type="text"
55
+ placeholder="Buscar por key"
56
+ text-14px text-primary placeholder:text-gray-400
57
+ bg-transparent border-none outline-none
58
+ flex-1 leading-18px tracking="-.15px"
59
+ >
60
+ </div>
61
+ <TelaButton
62
+ variant="secondary"
63
+ @click="openCreateModal"
64
+ >
65
+ <div i-ph-plus text-16px />
66
+ <span>Adicionar</span>
67
+ </TelaButton>
68
+ </div>
69
+
70
+ <div v-if="credentialsLoading && !credentials.length" flex items-center justify-center py-48px>
71
+ <div i-ph-spinner animate-spin text-32px text-gray-400 />
72
+ </div>
73
+
74
+ <div v-else :class="{ 'opacity-50 pointer-events-none': credentialsLoading }">
75
+ <TelaTable v-if="credentials.length > 0" class="table-fixed" w-full overflow-x-hidden>
76
+ <TelaTableHeader>
77
+ <TelaTableRow>
78
+ <TelaTableHead class="!py-10px text-14px font-medium text-secondary">
79
+ Nome
80
+ </TelaTableHead>
81
+ <TelaTableHead class="!py-10px !w-200px text-14px font-medium text-secondary hide-narrow">
82
+ Valor
83
+ </TelaTableHead>
84
+ <TelaTableHead class="!py-10px !w-180px text-14px font-medium text-secondary">
85
+ Última rotação
86
+ </TelaTableHead>
87
+ <TelaTableHead class="!py-10px !w-48px" />
88
+ </TelaTableRow>
89
+ </TelaTableHeader>
90
+ <TelaTableBody>
91
+ <TelaTableRow
92
+ v-for="credential in filteredCredentials"
93
+ :key="credential.id"
94
+ >
95
+ <TelaTableCell class="!py-14px">
96
+ <span text-14px font-mono text-primary>{{ credential.key }}</span>
97
+ </TelaTableCell>
98
+ <TelaTableCell class="!py-14px !w-200px hide-narrow">
99
+ <span text-14px text-primary>••••••••••</span>
100
+ </TelaTableCell>
101
+ <TelaTableCell class="!py-14px !w-180px">
102
+ <span text-14px text-secondary>{{ formatLastRotated(credential.updatedAt, credential.createdAt) }}</span>
103
+ </TelaTableCell>
104
+ <TelaTableCell class="!py-14px !w-48px">
105
+ <TelaDropdownMenu :items="getMenuItems(credential)" align="end">
106
+ <button p-6px rounded-6px text-gray-400 hover:text-gray-600 transition-colors cursor-pointer>
107
+ <div i-ph-dots-three text-18px />
108
+ </button>
109
+ </TelaDropdownMenu>
110
+ </TelaTableCell>
111
+ </TelaTableRow>
112
+ <TelaTableRow v-if="filteredCredentials.length === 0 && searchQuery">
113
+ <TelaTableCell :colspan="4" class="!py-32px text-center">
114
+ <span text-14px text-secondary>Nenhum resultado para "{{ searchQuery }}"</span>
115
+ </TelaTableCell>
116
+ </TelaTableRow>
117
+ </TelaTableBody>
118
+ </TelaTable>
119
+
120
+ <div v-else flex="~ col" items-center justify-center py-48px gap-8px>
121
+ <div i-ph-password-bold-duotone text-32px text-gray-300 />
122
+ <p text-14px text-secondary>
123
+ Nenhuma credencial adicionada ainda
124
+ </p>
125
+ </div>
126
+ </div>
127
+
128
+ <TelaModal
129
+ v-model="credentialModalOpen"
130
+ :prevent-close="credentialSaving"
131
+ class="w-420px! relative p-20px! pt-16px!"
132
+ compact
133
+ :title="editingCredential ? 'Editar credencial' : 'Nova credencial'"
134
+ small-title
135
+ :is-close-icon="false"
136
+ hide-dividers
137
+ @keydown.esc.stop="credentialModalOpen = false"
138
+ >
139
+ <div relative flex="~ col" pt-24px gap-24px w-full>
140
+ <TelaIconButton
141
+ icon="i-ph-x"
142
+ size="md"
143
+ color="secondary"
144
+ outline-none
145
+ absolute p-8px top--32px right--12px
146
+ @click="credentialModalOpen = false"
147
+ />
148
+ <form flex="~ col" gap-24px w-full @submit.prevent="handleSave">
149
+ <div v-if="credentialError" body-12-regular text-red-500 bg-red-50 px-12px py-8px rounded-8px>
150
+ {{ credentialError }}
151
+ </div>
152
+
153
+ <div flex="~ col" gap-8px>
154
+ <TelaLabel for="credentialKey">
155
+ Nome
156
+ </TelaLabel>
157
+ <TelaInput
158
+ id="credentialKey"
159
+ v-model="credentialForm.key"
160
+ size="sm"
161
+ :disabled="!!editingCredential"
162
+ :autofocus="!editingCredential"
163
+ placeholder="DATABASE_URL"
164
+ input-class="!px-9px !py-4px"
165
+ />
166
+ <p body-12-regular text-secondary>
167
+ Apenas letras maiúsculas, números e underscore (ex: API_KEY)
168
+ </p>
169
+ </div>
170
+
171
+ <div flex="~ col" gap-8px>
172
+ <TelaLabel for="credentialValue">
173
+ Valor
174
+ </TelaLabel>
175
+ <TelaInput
176
+ id="credentialValue"
177
+ v-model="credentialForm.value"
178
+ size="sm"
179
+ type="password"
180
+ :autofocus="!!editingCredential"
181
+ :placeholder="editingCredential ? 'Novo valor (deixe vazio para manter)' : 'Valor da credencial'"
182
+ input-class="!px-9px !py-4px"
183
+ />
184
+ </div>
185
+
186
+ <div flex items-center justify-end gap-8px>
187
+ <TelaButton type="button" size="md" variant="secondary" @click="closeModal">
188
+ Cancelar
189
+ </TelaButton>
190
+ <TelaButton
191
+ type="submit"
192
+ size="md"
193
+ :loading="credentialSaving"
194
+ :disabled="!editingCredential && (!credentialForm.key || !credentialForm.value)"
195
+ >
196
+ {{ editingCredential ? "Salvar" : "Adicionar" }}
197
+ </TelaButton>
198
+ </div>
199
+ </form>
200
+ </div>
201
+ </TelaModal>
202
+
203
+ <TelaConfirmationModal
204
+ v-model:open="deleteModalOpen"
205
+ title="Excluir credencial?"
206
+ variant="danger"
207
+ confirm-button-text="Excluir"
208
+ cancel-button-text="Cancelar"
209
+ :loading="credentialDeleting"
210
+ @confirm="handleDelete"
211
+ @cancel="deleteModalOpen = false"
212
+ @keydown.esc.stop="deleteModalOpen = false"
213
+ >
214
+ A credencial <span font-mono font-semibold>{{ deletingCredential?.key }}</span> será excluída permanentemente.
215
+ </TelaConfirmationModal>
216
+ </template>
217
+
218
+ <style scoped>
219
+ @media (max-width:950px){.hide-narrow{display:none}}
220
+ </style>
@@ -0,0 +1,10 @@
1
+ type __VLS_Props = {
2
+ searchQuery: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
+ "update:searchQuery": (value: string) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
7
+ "onUpdate:searchQuery"?: ((value: string) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,32 @@
1
+ import type { ExternalSkill } from '../../../server/db/schema.js';
2
+ type __VLS_Props = {
3
+ searchQuery: string;
4
+ displayedSkills: readonly ExternalSkill[];
5
+ filteredSkills: readonly ExternalSkill[];
6
+ hasChanges: boolean;
7
+ isPending: (ref: string) => boolean;
8
+ skillModalOpen: boolean;
9
+ skillRefInput: string;
10
+ skillError: string;
11
+ discoveringSkills: boolean;
12
+ gitHubButtonText: string;
13
+ };
14
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
+ "update:searchQuery": (value: string) => any;
16
+ "update:skillModalOpen": (value: boolean) => any;
17
+ "update:skillRefInput": (value: string) => any;
18
+ removeSkill: (ref: string) => any;
19
+ closeSkillModal: () => any;
20
+ handleAddSkillAndClose: () => any;
21
+ connectGitHub: () => any;
22
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
+ "onUpdate:searchQuery"?: ((value: string) => any) | undefined;
24
+ "onUpdate:skillModalOpen"?: ((value: boolean) => any) | undefined;
25
+ "onUpdate:skillRefInput"?: ((value: string) => any) | undefined;
26
+ onRemoveSkill?: ((ref: string) => any) | undefined;
27
+ onCloseSkillModal?: (() => any) | undefined;
28
+ onHandleAddSkillAndClose?: (() => any) | undefined;
29
+ onConnectGitHub?: (() => any) | undefined;
30
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
31
+ declare const _default: typeof __VLS_export;
32
+ export default _default;