@meistrari/chat-nuxt 1.6.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 (106) hide show
  1. package/README.md +25 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +23 -2
  4. package/dist/runtime/components/MeistrariChatEmbed.vue +8 -1
  5. package/dist/runtime/components/chat/bash-widget.vue +3 -3
  6. package/dist/runtime/components/chat/doc-sdk-widget.vue +3 -3
  7. package/dist/runtime/components/chat/files-view.vue +3 -64
  8. package/dist/runtime/components/chat/generic-tool-widget.vue +3 -3
  9. package/dist/runtime/components/chat/grep-widget.vue +3 -3
  10. package/dist/runtime/components/chat/message-bubble.vue +1 -1
  11. package/dist/runtime/components/chat/message-input.d.vue.ts +2 -0
  12. package/dist/runtime/components/chat/message-input.vue +21 -5
  13. package/dist/runtime/components/chat/message-input.vue.d.ts +2 -0
  14. package/dist/runtime/components/chat/message-list.vue +1 -1
  15. package/dist/runtime/components/chat/mobile/files/file-preview.d.vue.ts +11 -0
  16. package/dist/runtime/components/chat/mobile/files/file-preview.vue +112 -0
  17. package/dist/runtime/components/chat/mobile/files/file-preview.vue.d.ts +11 -0
  18. package/dist/runtime/components/chat/mobile/files/files-view.d.vue.ts +10 -0
  19. package/dist/runtime/components/chat/mobile/files/files-view.vue +384 -0
  20. package/dist/runtime/components/chat/mobile/files/files-view.vue.d.ts +10 -0
  21. package/dist/runtime/components/chat/mobile/home/header.d.vue.ts +19 -0
  22. package/dist/runtime/components/chat/mobile/home/header.vue +55 -0
  23. package/dist/runtime/components/chat/mobile/home/header.vue.d.ts +19 -0
  24. package/dist/runtime/components/chat/mobile/home/recents.d.vue.ts +14 -0
  25. package/dist/runtime/components/chat/mobile/home/recents.vue +48 -0
  26. package/dist/runtime/components/chat/mobile/home/recents.vue.d.ts +14 -0
  27. package/dist/runtime/components/chat/mobile/home/suggestions.d.vue.ts +7 -0
  28. package/dist/runtime/components/chat/mobile/home/suggestions.vue +31 -0
  29. package/dist/runtime/components/chat/mobile/home/suggestions.vue.d.ts +7 -0
  30. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.d.vue.ts +21 -0
  31. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.vue +48 -0
  32. package/dist/runtime/components/chat/mobile/primitives/bottom-sheet.vue.d.ts +21 -0
  33. package/dist/runtime/components/chat/mobile/primitives/info-bubble.d.vue.ts +6 -0
  34. package/dist/runtime/components/chat/mobile/primitives/info-bubble.vue +50 -0
  35. package/dist/runtime/components/chat/mobile/primitives/info-bubble.vue.d.ts +6 -0
  36. package/dist/runtime/components/chat/mobile/shell/drawer.d.vue.ts +22 -0
  37. package/dist/runtime/components/chat/mobile/shell/drawer.vue +143 -0
  38. package/dist/runtime/components/chat/mobile/shell/drawer.vue.d.ts +22 -0
  39. package/dist/runtime/components/chat/mobile/shell/header.d.vue.ts +40 -0
  40. package/dist/runtime/components/chat/mobile/shell/header.vue +139 -0
  41. package/dist/runtime/components/chat/mobile/shell/header.vue.d.ts +40 -0
  42. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.d.vue.ts +14 -0
  43. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.vue +47 -0
  44. package/dist/runtime/components/chat/mobile/shell/outdated-settings-sheet.vue.d.ts +14 -0
  45. package/dist/runtime/components/chat/mobile/shell/shell.d.vue.ts +122 -0
  46. package/dist/runtime/components/chat/mobile/shell/shell.vue +283 -0
  47. package/dist/runtime/components/chat/mobile/shell/shell.vue.d.ts +122 -0
  48. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.d.vue.ts +19 -0
  49. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.vue +51 -0
  50. package/dist/runtime/components/chat/mobile/shell/workspace-sheet.vue.d.ts +19 -0
  51. package/dist/runtime/components/chat/mobile/usage/usage-view.d.vue.ts +8 -0
  52. package/dist/runtime/components/chat/mobile/usage/usage-view.vue +243 -0
  53. package/dist/runtime/components/chat/mobile/usage/usage-view.vue.d.ts +8 -0
  54. package/dist/runtime/components/chat/model-selector.vue +6 -0
  55. package/dist/runtime/components/chat/read-widget.vue +3 -3
  56. package/dist/runtime/components/chat/reasoning-step-item.vue +1 -1
  57. package/dist/runtime/components/chat/reasoning-steps.vue +1 -1
  58. package/dist/runtime/components/chat/reasoning-thought.vue +2 -3
  59. package/dist/runtime/components/chat/tela-skill-widget.vue +3 -3
  60. package/dist/runtime/components/chat/thinking-widget.vue +3 -3
  61. package/dist/runtime/components/chat/usage-view.vue +20 -101
  62. package/dist/runtime/components/chat/web-search-widget.vue +3 -3
  63. package/dist/runtime/components/chat/workstation-query-widget.vue +6 -6
  64. package/dist/runtime/components/chat/write-widget.vue +3 -3
  65. package/dist/runtime/composables/useChat.js +2 -1
  66. package/dist/runtime/composables/useChatApi.js +4 -0
  67. package/dist/runtime/composables/useConversationFileDownloads.d.ts +6 -0
  68. package/dist/runtime/composables/useConversationFileDownloads.js +50 -0
  69. package/dist/runtime/composables/useConversationUsage.d.ts +13 -0
  70. package/dist/runtime/composables/useConversationUsage.js +121 -0
  71. package/dist/runtime/composables/useConversations.js +3 -1
  72. package/dist/runtime/composables/useEmbedConfig.d.ts +4 -0
  73. package/dist/runtime/composables/useEmbedConfig.js +9 -0
  74. package/dist/runtime/composables/useMentionAutocomplete.d.ts +6 -0
  75. package/dist/runtime/composables/useMentionAutocomplete.js +71 -23
  76. package/dist/runtime/composables/useWorkspaceSwitchModal.d.ts +5 -0
  77. package/dist/runtime/composables/useWorkspaceSwitchModal.js +14 -0
  78. package/dist/runtime/embed/components/ChatConfigurationModal.vue +119 -13
  79. package/dist/runtime/embed/components/ChatEmbed.vue +9 -2
  80. package/dist/runtime/embed/components/ChatEmbedInner.d.vue.ts +2 -0
  81. package/dist/runtime/embed/components/ChatEmbedInner.vue +201 -11
  82. package/dist/runtime/embed/components/ChatEmbedInner.vue.d.ts +2 -0
  83. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.d.vue.ts +34 -0
  84. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue +95 -0
  85. package/dist/runtime/embed/components/configuration/ChatConfigurationMobileShell.vue.d.ts +34 -0
  86. package/dist/runtime/server/api/conversations/[id]/duplicate.post.js +1 -0
  87. package/dist/runtime/server/api/conversations/[id]/messages/[messageId]/retry.post.js +3 -1
  88. package/dist/runtime/server/api/conversations/[id]/messages/index.get.js +29 -5
  89. package/dist/runtime/server/api/conversations/index.post.js +1 -0
  90. package/dist/runtime/server/db/schema/conversations.d.ts +17 -0
  91. package/dist/runtime/server/db/schema/conversations.js +1 -0
  92. package/dist/runtime/server/utils/chat-context.d.ts +2 -0
  93. package/dist/runtime/server/utils/chat-context.js +15 -1
  94. package/dist/runtime/server/utils/conversation-message-sync.d.ts +8 -2
  95. package/dist/runtime/server/utils/conversation-message-sync.js +52 -6
  96. package/dist/runtime/server/utils/conversation-scope.js +2 -1
  97. package/dist/runtime/types/chat-auth.d.ts +9 -8
  98. package/dist/runtime/types/embed.d.ts +21 -0
  99. package/dist/runtime/utils/breakpoints.d.ts +1 -0
  100. package/dist/runtime/utils/breakpoints.js +1 -0
  101. package/dist/runtime/utils/tela-chat.d.ts +3 -1
  102. package/dist/runtime/utils/tela-chat.js +11 -2
  103. package/drizzle/0015_illegal_blindfold.sql +1 -0
  104. package/drizzle/meta/0015_snapshot.json +738 -0
  105. package/drizzle/meta/_journal.json +7 -0
  106. package/package.json +6 -1
@@ -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
+ };
@@ -0,0 +1,50 @@
1
+ import { useClipboard } from "@vueuse/core";
2
+ export function useConversationFileDownloads() {
3
+ const statusToast = useStatusToast();
4
+ const { copy } = useClipboard();
5
+ async function resolveDownloadUrl(file) {
6
+ if (file.url.startsWith("vault://")) {
7
+ const { url } = await $fetch("/api/vault/resolve", {
8
+ method: "POST",
9
+ body: { reference: file.url }
10
+ });
11
+ return url;
12
+ }
13
+ return file.url;
14
+ }
15
+ async function downloadFile(file) {
16
+ statusToast.update({ text: "Baixando arquivo...", loading: true });
17
+ try {
18
+ const downloadUrl = await resolveDownloadUrl(file);
19
+ const response = await fetch(downloadUrl);
20
+ const blob = await response.blob();
21
+ const blobUrl = URL.createObjectURL(blob);
22
+ const link = document.createElement("a");
23
+ link.href = blobUrl;
24
+ link.download = file.name;
25
+ document.body.appendChild(link);
26
+ link.click();
27
+ document.body.removeChild(link);
28
+ URL.revokeObjectURL(blobUrl);
29
+ statusToast.update({ text: "Download conclu\xEDdo", icon: "i-ph-check" });
30
+ } catch (error) {
31
+ console.error("Failed to download file:", error);
32
+ statusToast.update({ text: "Erro ao baixar arquivo", icon: "i-ph-warning" });
33
+ }
34
+ }
35
+ async function copyFileLink(file) {
36
+ try {
37
+ const publicUrl = await resolveDownloadUrl(file);
38
+ await copy(publicUrl);
39
+ statusToast.update({ text: "Link copiado", icon: "i-ph-check" });
40
+ } catch (error) {
41
+ console.error("Failed to copy link:", error);
42
+ statusToast.update({ text: "Erro ao copiar link", icon: "i-ph-warning" });
43
+ }
44
+ }
45
+ return {
46
+ resolveDownloadUrl,
47
+ downloadFile,
48
+ copyFileLink
49
+ };
50
+ }
@@ -0,0 +1,13 @@
1
+ import type { Ref } from 'vue';
2
+ export declare function useConversationUsage(conversationId: Ref<string | null | undefined>): {
3
+ usageData: any;
4
+ billing: any;
5
+ loading: any;
6
+ error: any;
7
+ cacheInsight: any;
8
+ costBreakdown: any;
9
+ modelRows: any;
10
+ costBarSegments: any;
11
+ formatBillingCost: (cost: number) => string;
12
+ fetchUsage: () => Promise<void>;
13
+ };
@@ -0,0 +1,121 @@
1
+ import { formatCost, formatCredits, formatModelName, formatTokenCount } from "../utils/calculate-usage.js";
2
+ export function useConversationUsage(conversationId) {
3
+ const chatApi = useChatApi();
4
+ const usageData = ref(null);
5
+ const billing = ref(null);
6
+ const loading = ref(false);
7
+ const error = ref(null);
8
+ function formatBillingCost(cost) {
9
+ if (!billing.value || billing.value.mode === "credits") {
10
+ return formatCredits(cost);
11
+ }
12
+ return formatCost(cost);
13
+ }
14
+ async function fetchUsage() {
15
+ const id = conversationId.value;
16
+ if (!id) {
17
+ return;
18
+ }
19
+ loading.value = true;
20
+ error.value = null;
21
+ try {
22
+ const response = await $fetch(
23
+ chatApi.path(`/conversations/${id}/usage`),
24
+ chatApi.withChatHeaders()
25
+ );
26
+ if (response.error) {
27
+ error.value = response.error;
28
+ } else {
29
+ usageData.value = response.usage;
30
+ billing.value = response.billing;
31
+ }
32
+ } catch (err) {
33
+ console.error("Failed to fetch usage data:", err);
34
+ error.value = "Falha ao carregar dados de uso";
35
+ } finally {
36
+ loading.value = false;
37
+ }
38
+ }
39
+ const cacheInsight = computed(() => {
40
+ if (!usageData.value) {
41
+ return { percentage: 0, reusedTokens: 0, storedTokens: 0, hasData: false };
42
+ }
43
+ const hasData = usageData.value.tokens.cacheRead > 0 || usageData.value.tokens.cacheCreation > 0;
44
+ return {
45
+ percentage: Math.round(usageData.value.metrics.cacheHitRate * 100),
46
+ reusedTokens: usageData.value.tokens.cacheRead,
47
+ storedTokens: usageData.value.tokens.cacheCreation,
48
+ hasData
49
+ };
50
+ });
51
+ const costBreakdown = computed(() => {
52
+ if (!usageData.value) {
53
+ return [];
54
+ }
55
+ const total = usageData.value.cost.total;
56
+ return [
57
+ {
58
+ key: "input",
59
+ label: "Envio",
60
+ tooltip: "Texto enviado ao modelo: perguntas, contexto e instru\xE7\xF5es",
61
+ tokens: usageData.value.tokens.prompt,
62
+ cost: usageData.value.cost.prompt,
63
+ percentage: total > 0 ? Math.round(usageData.value.cost.prompt / total * 100) : 0
64
+ },
65
+ {
66
+ key: "output",
67
+ label: "Respostas",
68
+ tooltip: "Texto gerado pelo modelo",
69
+ tokens: usageData.value.tokens.completion,
70
+ cost: usageData.value.cost.completion,
71
+ percentage: total > 0 ? Math.round(usageData.value.cost.completion / total * 100) : 0
72
+ }
73
+ ];
74
+ });
75
+ const modelRows = computed(() => {
76
+ if (!usageData.value) {
77
+ return [];
78
+ }
79
+ const totalCost = usageData.value.cost.total;
80
+ return usageData.value.models.map((m) => {
81
+ const totalTokens = m.inputTokens + m.outputTokens + m.cacheReadTokens + m.cacheCreationTokens;
82
+ return {
83
+ modelId: m.model,
84
+ modelName: formatModelName(m.model),
85
+ tokens: formatTokenCount(totalTokens),
86
+ cost: formatBillingCost(m.cost),
87
+ costShare: totalCost > 0 ? m.cost / totalCost * 100 : 0
88
+ };
89
+ });
90
+ });
91
+ const costBarSegments = computed(() => {
92
+ if (!usageData.value || usageData.value.cost.total === 0) {
93
+ return [];
94
+ }
95
+ const total = usageData.value.cost.total;
96
+ return [
97
+ {
98
+ key: "input",
99
+ label: "Envio",
100
+ percentage: usageData.value.cost.prompt / total * 100
101
+ },
102
+ {
103
+ key: "output",
104
+ label: "Respostas",
105
+ percentage: usageData.value.cost.completion / total * 100
106
+ }
107
+ ];
108
+ });
109
+ return {
110
+ usageData,
111
+ billing,
112
+ loading,
113
+ error,
114
+ cacheInsight,
115
+ costBreakdown,
116
+ modelRows,
117
+ costBarSegments,
118
+ formatBillingCost,
119
+ fetchUsage
120
+ };
121
+ }
@@ -38,7 +38,8 @@ export function useConversations() {
38
38
  const isTelaAgentMode = computed(() => !!embedConfig?.telaAgentId.value);
39
39
  const workspaceScope = computed(() => resolveChatStateScope(
40
40
  embedConfig?.workspaceId.value || activeOrganization.value?.id,
41
- embedConfig?.telaAgentId.value
41
+ embedConfig?.telaAgentId.value,
42
+ embedConfig?.conversationScope?.value
42
43
  ));
43
44
  const conversations = computed({
44
45
  get: () => conversationBuckets.value[workspaceScope.value] ?? [],
@@ -230,6 +231,7 @@ export function useConversations() {
230
231
  appliedSettingsSnapshot: cloneAppliedSettingsSnapshot(updated.appliedSettingsSnapshot),
231
232
  telaAgentId: updated.telaAgentId,
232
233
  createdBy: updated.createdBy,
234
+ conversationScope: updated.conversationScope,
233
235
  createdAt: updated.createdAt,
234
236
  updatedAt: updated.updatedAt
235
237
  };
@@ -6,9 +6,11 @@ type ConfiguredTelaAgentInputs = DeepReadonly<TelaAgentExecutionInput[]> | null
6
6
  export type EmbedConfig = {
7
7
  workspaceId: Ref<string>;
8
8
  telaAgentId: Ref<string | null>;
9
+ conversationScope: Ref<string | null>;
9
10
  telaAgentInputs: Ref<ConfiguredTelaAgentInputs>;
10
11
  workspaceSettings: Ref<DeepReadonly<WorkspaceSettings> | WorkspaceSettings | null | undefined>;
11
12
  hideSidebar: Ref<boolean>;
13
+ hideSettings: Ref<boolean>;
12
14
  loadingMessages: Ref<readonly string[] | null | undefined>;
13
15
  loadingMessagesMode: Ref<ChatLoadingMessageMode | null | undefined>;
14
16
  customComponents: Ref<Record<string, Component> | undefined>;
@@ -17,9 +19,11 @@ export type EmbedConfig = {
17
19
  type EmbedConfigInput = {
18
20
  workspaceId: MaybeRefOrGetter<string>;
19
21
  telaAgentId?: MaybeRefOrGetter<string | null | undefined>;
22
+ conversationScope?: MaybeRefOrGetter<string | null | undefined>;
20
23
  telaAgentInputs?: MaybeRefOrGetter<ConfiguredTelaAgentInputs>;
21
24
  workspaceSettings?: MaybeRefOrGetter<DeepReadonly<WorkspaceSettings> | WorkspaceSettings | null | undefined>;
22
25
  hideSidebar?: MaybeRefOrGetter<boolean | undefined>;
26
+ hideSettings?: MaybeRefOrGetter<boolean | undefined>;
23
27
  loadingMessages?: MaybeRefOrGetter<readonly string[] | null | undefined>;
24
28
  loadingMessagesMode?: MaybeRefOrGetter<ChatLoadingMessageMode | null | undefined>;
25
29
  customComponents?: MaybeRefOrGetter<Record<string, Component> | undefined>;
@@ -1,3 +1,4 @@
1
+ import { normalizeConversationScope } from "../utils/tela-chat.js";
1
2
  const EMBED_CONFIG_KEY = "embed-config";
2
3
  function useGlobalCustomComponentsState() {
3
4
  const nuxtApp = useNuxtApp();
@@ -9,9 +10,11 @@ function useGlobalEmbedConfigState() {
9
10
  provided: useState("chat-embed-config-provided", () => false),
10
11
  workspaceId: useState("chat-embed-config-workspace-id", () => ""),
11
12
  telaAgentId: useState("chat-embed-config-tela-agent-id", () => null),
13
+ conversationScope: useState("chat-embed-config-conversation-scope", () => null),
12
14
  telaAgentInputs: useState("chat-embed-config-tela-agent-inputs", () => void 0),
13
15
  workspaceSettings: useState("chat-embed-config-workspace-settings", () => void 0),
14
16
  hideSidebar: useState("chat-embed-config-hide-sidebar", () => void 0),
17
+ hideSettings: useState("chat-embed-config-hide-settings", () => void 0),
15
18
  loadingMessages: useState("chat-embed-config-loading-messages", () => void 0),
16
19
  loadingMessagesMode: useState("chat-embed-config-loading-messages-mode", () => void 0),
17
20
  customComponents: useGlobalCustomComponentsState(),
@@ -22,9 +25,11 @@ export function provideEmbedConfig(input, options = {}) {
22
25
  const config = {
23
26
  workspaceId: computed(() => toValue(input.workspaceId)),
24
27
  telaAgentId: computed(() => toValue(input.telaAgentId) ?? null),
28
+ conversationScope: computed(() => normalizeConversationScope(toValue(input.conversationScope))),
25
29
  telaAgentInputs: computed(() => toValue(input.telaAgentInputs)),
26
30
  workspaceSettings: computed(() => toValue(input.workspaceSettings)),
27
31
  hideSidebar: computed(() => toValue(input.hideSidebar) ?? true),
32
+ hideSettings: computed(() => toValue(input.hideSettings) ?? false),
28
33
  loadingMessages: computed(() => toValue(input.loadingMessages)),
29
34
  loadingMessagesMode: computed(() => toValue(input.loadingMessagesMode)),
30
35
  customComponents: computed(() => toValue(input.customComponents)),
@@ -36,9 +41,11 @@ export function provideEmbedConfig(input, options = {}) {
36
41
  globalConfig.provided.value = true;
37
42
  globalConfig.workspaceId.value = config.workspaceId.value;
38
43
  globalConfig.telaAgentId.value = config.telaAgentId.value;
44
+ globalConfig.conversationScope.value = config.conversationScope.value;
39
45
  globalConfig.telaAgentInputs.value = config.telaAgentInputs.value;
40
46
  globalConfig.workspaceSettings.value = config.workspaceSettings.value;
41
47
  globalConfig.hideSidebar.value = config.hideSidebar.value;
48
+ globalConfig.hideSettings.value = config.hideSettings.value;
42
49
  globalConfig.loadingMessages.value = config.loadingMessages.value;
43
50
  globalConfig.loadingMessagesMode.value = config.loadingMessagesMode.value;
44
51
  globalConfig.customComponents.value = config.customComponents.value;
@@ -57,9 +64,11 @@ export function useEmbedConfig() {
57
64
  return {
58
65
  workspaceId: computed(() => globalConfig.workspaceId.value),
59
66
  telaAgentId: computed(() => globalConfig.telaAgentId.value),
67
+ conversationScope: computed(() => globalConfig.conversationScope.value),
60
68
  telaAgentInputs: computed(() => globalConfig.telaAgentInputs.value),
61
69
  workspaceSettings: computed(() => globalConfig.workspaceSettings.value),
62
70
  hideSidebar: computed(() => globalConfig.hideSidebar.value ?? true),
71
+ hideSettings: computed(() => globalConfig.hideSettings.value ?? false),
63
72
  loadingMessages: computed(() => globalConfig.loadingMessages.value),
64
73
  loadingMessagesMode: computed(() => globalConfig.loadingMessagesMode.value),
65
74
  customComponents: computed(() => globalConfig.customComponents.value),
@@ -6,5 +6,11 @@ type UseMentionAutocompleteOptions = {
6
6
  disabled?: Ref<boolean | undefined>;
7
7
  onInsert?: () => void;
8
8
  };
9
+ export declare function isMentionTriggerBoundary(textBeforeTrigger: string): boolean;
10
+ export declare function findMentionTrigger(textBeforeCursor: string): {
11
+ trigger: '@' | '/';
12
+ query: string;
13
+ offset: number;
14
+ } | null;
9
15
  export declare function useMentionAutocomplete(options: UseMentionAutocompleteOptions): any;
10
16
  export {};
@@ -1,3 +1,26 @@
1
+ export function isMentionTriggerBoundary(textBeforeTrigger) {
2
+ if (textBeforeTrigger.length === 0)
3
+ return true;
4
+ const previousChar = textBeforeTrigger[textBeforeTrigger.length - 1];
5
+ return previousChar === " " || previousChar === "\n" || previousChar === "\xA0";
6
+ }
7
+ export function findMentionTrigger(textBeforeCursor) {
8
+ for (let i = textBeforeCursor.length - 1; i >= 0; i--) {
9
+ const char = textBeforeCursor[i];
10
+ if (char === " " || char === "\n" || char === "\xA0")
11
+ return null;
12
+ if (char === "@" || char === "/") {
13
+ if (!isMentionTriggerBoundary(textBeforeCursor.slice(0, i)))
14
+ return null;
15
+ return {
16
+ trigger: char,
17
+ query: textBeforeCursor.slice(i + 1),
18
+ offset: i
19
+ };
20
+ }
21
+ }
22
+ return null;
23
+ }
1
24
  export function useMentionAutocomplete(options) {
2
25
  const { editorRef, fileItems, skillItems, disabled, onInsert } = options;
3
26
  const activeTrigger = ref(null);
@@ -22,36 +45,59 @@ export function useMentionAutocomplete(options) {
22
45
  if (!sel || sel.rangeCount === 0 || !editorRef.value)
23
46
  return "";
24
47
  const range = sel.getRangeAt(0);
25
- const preRange = document.createRange();
26
- preRange.selectNodeContents(editorRef.value);
27
- preRange.setEnd(range.startContainer, range.startOffset);
28
- return preRange.toString();
48
+ const editor = editorRef.value;
49
+ let text = "";
50
+ function walkNode(node) {
51
+ if (node === range.startContainer) {
52
+ if (node.nodeType === Node.TEXT_NODE) {
53
+ text += (node.textContent ?? "").slice(0, range.startOffset);
54
+ return true;
55
+ }
56
+ for (const child of Array.from(node.childNodes).slice(0, range.startOffset)) {
57
+ if (walkNode(child))
58
+ return true;
59
+ }
60
+ return true;
61
+ }
62
+ if (node.nodeType === Node.TEXT_NODE) {
63
+ text += node.textContent ?? "";
64
+ return false;
65
+ }
66
+ if (node.nodeName === "BR") {
67
+ if (!node.dataset?.sentinel)
68
+ text += "\n";
69
+ return false;
70
+ }
71
+ if (node.nodeType === Node.ELEMENT_NODE && node.contentEditable === "false") {
72
+ text += node.textContent ?? "";
73
+ return false;
74
+ }
75
+ for (const child of node.childNodes) {
76
+ if (walkNode(child))
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ walkNode(editor);
82
+ return text;
29
83
  }
30
84
  function detectTrigger() {
31
85
  if (disabled?.value) {
32
86
  activeTrigger.value = null;
33
87
  return;
34
88
  }
35
- const textBefore = getTextBeforeCursor();
36
- for (let i = textBefore.length - 1; i >= 0; i--) {
37
- const char = textBefore[i];
38
- if (char === " " || char === "\n" || char === "\xA0") {
39
- activeTrigger.value = null;
40
- return;
41
- }
42
- if (char === "@" || char === "/") {
43
- activeTrigger.value = char;
44
- triggerTextOffset = i;
45
- const newQuery = textBefore.slice(i + 1);
46
- if (newQuery !== lastQuery.value) {
47
- highlightedIndex.value = -1;
48
- lastQuery.value = newQuery;
49
- }
50
- query.value = newQuery;
51
- return;
52
- }
89
+ const mentionTrigger = findMentionTrigger(getTextBeforeCursor());
90
+ if (!mentionTrigger) {
91
+ activeTrigger.value = null;
92
+ return;
53
93
  }
54
- activeTrigger.value = null;
94
+ activeTrigger.value = mentionTrigger.trigger;
95
+ triggerTextOffset = mentionTrigger.offset;
96
+ if (mentionTrigger.query !== lastQuery.value) {
97
+ highlightedIndex.value = -1;
98
+ lastQuery.value = mentionTrigger.query;
99
+ }
100
+ query.value = mentionTrigger.query;
55
101
  }
56
102
  function getCaretRangeAtOffset(offset) {
57
103
  const editor = editorRef.value;
@@ -71,6 +117,8 @@ export function useMentionAutocomplete(options) {
71
117
  return null;
72
118
  }
73
119
  if (node.nodeName === "BR") {
120
+ if (!node.dataset?.sentinel)
121
+ current += 1;
74
122
  return null;
75
123
  }
76
124
  if (node.nodeType === Node.ELEMENT_NODE && node.contentEditable === "false") {
@@ -0,0 +1,5 @@
1
+ export declare function useWorkspaceSwitchModal(): {
2
+ isOpen: any;
3
+ openModal: () => void;
4
+ closeModal: () => void;
5
+ };
@@ -0,0 +1,14 @@
1
+ export function useWorkspaceSwitchModal() {
2
+ const isOpen = useState("chat-nuxt:workspace-switch-modal-open", () => false);
3
+ function openModal() {
4
+ isOpen.value = true;
5
+ }
6
+ function closeModal() {
7
+ isOpen.value = false;
8
+ }
9
+ return {
10
+ isOpen,
11
+ openModal,
12
+ closeModal
13
+ };
14
+ }
@@ -1,5 +1,7 @@
1
1
  <script setup>
2
- import { useMagicKeys } from "@vueuse/core";
2
+ import { useBreakpoints, useMagicKeys } from "@vueuse/core";
3
+ import { CHAT_MOBILE_BREAKPOINT } from "../../utils/breakpoints";
4
+ import ChatConfigurationMobileShell from "./configuration/ChatConfigurationMobileShell.vue";
3
5
  const props = defineProps({
4
6
  open: { type: Boolean, required: true },
5
7
  settings: { type: null, required: false },
@@ -11,6 +13,8 @@ const { uploadFiles, uploading } = useFileUpload();
11
13
  const { canvas, projects, workstations, loadingWorkstations } = useTela();
12
14
  const { setKnowledgeSources } = useKnowledgeSources();
13
15
  const statusToast = useStatusToast();
16
+ const breakpoints = useBreakpoints({ mobile: CHAT_MOBILE_BREAKPOINT });
17
+ const isMobile = breakpoints.smaller("mobile");
14
18
  const { escape } = useMagicKeys();
15
19
  const {
16
20
  settings: workspaceSettings,
@@ -21,6 +25,7 @@ const {
21
25
  const features = useChatNuxtFeatures();
22
26
  const searchQuery = ref("");
23
27
  const activeTab = ref(props.initialTab);
28
+ const activeMobileTab = ref(null);
24
29
  const showDiscardConfirm = ref(false);
25
30
  const loadingInitialSettings = ref(false);
26
31
  const hasSettingsOverride = computed(() => props.settings !== void 0);
@@ -56,6 +61,7 @@ watch(() => props.open, (isOpen) => {
56
61
  if (isOpen) {
57
62
  searchQuery.value = "";
58
63
  activateTab(props.initialTab);
64
+ activeMobileTab.value = null;
59
65
  if (!hasSettingsOverride.value && !mutableSettings.value) {
60
66
  void loadSettings();
61
67
  return;
@@ -91,6 +97,10 @@ function activateTab(tab) {
91
97
  if (activeTab.value === "external-skills")
92
98
  void externalSkills.fetchGitHubConnection();
93
99
  }
100
+ function activateMobileTab(tab) {
101
+ activateTab(tab);
102
+ activeMobileTab.value = activeTab.value;
103
+ }
94
104
  async function loadSettings() {
95
105
  loadingInitialSettings.value = true;
96
106
  try {
@@ -182,7 +192,103 @@ function cancelDiscard() {
182
192
  </script>
183
193
 
184
194
  <template>
195
+ <Teleport to="body">
196
+ <ChatConfigurationMobileShell
197
+ v-if="isMobile && open"
198
+ :tabs="settingsTabs"
199
+ :active-tab="activeMobileTab"
200
+ :has-changes="hasChanges"
201
+ :is-busy="isBusy"
202
+ @select-tab="activateMobileTab"
203
+ @back="activeMobileTab = null"
204
+ @close="handleClose"
205
+ @save="handleSave"
206
+ >
207
+ <div
208
+ v-if="loadingInitialSettings"
209
+ h-full min-h-320px flex="~ col" items-center justify-center gap-12px
210
+ >
211
+ <div i-ph-spinner animate-spin text-24px text-gray-500 />
212
+ <p text-14px text-gray-500>
213
+ Carregando configurações...
214
+ </p>
215
+ </div>
216
+
217
+ <ChatConfigurationPersonalizationTab
218
+ v-else-if="activeTab === 'personalization'"
219
+ v-model:system-message="personalization.localSystemMessage.value"
220
+ />
221
+
222
+ <ChatConfigurationContextFilesTab
223
+ v-else-if="activeTab === 'context-files'"
224
+ :search-query="searchQuery"
225
+ :filtered-context-files="contextFiles.filteredContextFiles.value"
226
+ :filtered-pending-files="contextFiles.filteredPendingFiles.value"
227
+ :current-file-count="contextFiles.currentFileCount.value"
228
+ :max-files="contextFiles.maxFiles"
229
+ :get-file-type="contextFiles.getFileType"
230
+ @update:search-query="searchQuery = $event"
231
+ @handle-file-select="contextFiles.handleFileSelect"
232
+ @remove-pending-file="contextFiles.removePendingFile"
233
+ @remove-context-file="contextFiles.removeContextFile"
234
+ />
235
+
236
+ <ChatConfigurationKnowledgeSourcesTab
237
+ v-else-if="activeTab === 'knowledge-sources'"
238
+ :search-query="searchQuery"
239
+ :workstations="workstations"
240
+ :loading-workstations="loadingWorkstations"
241
+ :filtered-workstations="knowledgeSources.filteredWorkstations.value"
242
+ :selected-ids="knowledgeSources.selectedKnowledgeSourceIds.value"
243
+ :get-project-name-for-workstation="knowledgeSources.getProjectNameForWorkstation"
244
+ @update:search-query="searchQuery = $event"
245
+ @toggle="knowledgeSources.toggleKnowledgeSource"
246
+ />
247
+
248
+ <ChatConfigurationTelaToolsTab
249
+ v-else-if="activeTab === 'tela-tools'"
250
+ :search-query="searchQuery"
251
+ :canvas="canvas"
252
+ :filtered-canvas-tools="telaTools.filteredCanvasTools.value"
253
+ :selected-tool-ids="telaTools.selectedToolIds.value"
254
+ :all-tools-selected="telaTools.allToolsSelected.value"
255
+ :get-project-name="telaTools.getProjectName"
256
+ @update:search-query="searchQuery = $event"
257
+ @toggle-tool="telaTools.toggleCanvasTool"
258
+ @toggle-all="telaTools.toggleAllTools"
259
+ />
260
+
261
+ <ChatConfigurationExternalSkillsTab
262
+ v-else-if="activeTab === 'external-skills'"
263
+ :search-query="searchQuery"
264
+ :displayed-skills="externalSkills.displayedSkills.value"
265
+ :filtered-skills="externalSkills.filteredSkills.value"
266
+ :has-changes="externalSkills.hasChanges.value"
267
+ :is-pending="externalSkills.isPending"
268
+ :skill-modal-open="externalSkills.skillModalOpen.value"
269
+ :skill-ref-input="externalSkills.skillRefInput.value"
270
+ :skill-error="externalSkills.skillError.value"
271
+ :discovering-skills="externalSkills.discoveringSkills.value"
272
+ :git-hub-button-text="gitHubButtonText"
273
+ @update:search-query="searchQuery = $event"
274
+ @update:skill-modal-open="externalSkills.skillModalOpen.value = $event"
275
+ @update:skill-ref-input="externalSkills.skillRefInput.value = $event"
276
+ @remove-skill="externalSkills.removeSkill"
277
+ @close-skill-modal="externalSkills.closeSkillModal"
278
+ @handle-add-skill-and-close="externalSkills.handleAddSkillAndClose"
279
+ @connect-git-hub="externalSkills.connectGitHub"
280
+ />
281
+
282
+ <ChatConfigurationCredentialsTab
283
+ v-else-if="activeTab === 'credentials' && features.credentials"
284
+ :search-query="searchQuery"
285
+ @update:search-query="searchQuery = $event"
286
+ />
287
+ </ChatConfigurationMobileShell>
288
+ </Teleport>
289
+
185
290
  <TelaModal
291
+ v-if="!isMobile"
186
292
  :model-value="open"
187
293
  modal-width="lg"
188
294
  compact
@@ -359,17 +465,17 @@ function cancelDiscard() {
359
465
  </div>
360
466
  </div>
361
467
  </div>
362
-
363
- <TelaConfirmationModal
364
- v-model:open="showDiscardConfirm"
365
- title="Descartar alterações?"
366
- variant="danger"
367
- confirm-button-text="Descartar"
368
- cancel-button-text="Continuar editando"
369
- @confirm="confirmDiscard"
370
- @cancel="cancelDiscard"
371
- >
372
- Você tem alterações não salvas. Deseja descartá-las?
373
- </TelaConfirmationModal>
374
468
  </TelaModal>
469
+
470
+ <TelaConfirmationModal
471
+ v-model:open="showDiscardConfirm"
472
+ title="Descartar alterações?"
473
+ variant="danger"
474
+ confirm-button-text="Descartar"
475
+ cancel-button-text="Continuar editando"
476
+ @confirm="confirmDiscard"
477
+ @cancel="cancelDiscard"
478
+ >
479
+ Você tem alterações não salvas. Deseja descartá-las?
480
+ </TelaConfirmationModal>
375
481
  </template>
@@ -2,12 +2,14 @@
2
2
  import { useChatAuth } from "#chat-auth";
3
3
  import { provideEmbedConfig, useEmbedConfig } from "../../composables/useEmbedConfig";
4
4
  import { provideChatAction } from "../../composables/useChatAction";
5
- import { normalizeTelaAgentId } from "../../utils/tela-chat";
5
+ import { normalizeConversationScope, normalizeTelaAgentId, resolveConversationScopeKeySegment } from "../../utils/tela-chat";
6
6
  import ChatEmbedInner from "./ChatEmbedInner.vue";
7
7
  const props = defineProps({
8
8
  conversationId: { type: [String, null], required: false },
9
9
  initialConversationId: { type: [String, null], required: false },
10
+ conversationScope: { type: [String, null], required: false },
10
11
  hideSidebar: { type: Boolean, required: false },
12
+ hideSettings: { type: Boolean, required: false },
11
13
  loadingMessages: { type: [Array, null], required: false },
12
14
  loadingMessagesMode: { type: [String, null], required: false },
13
15
  customComponents: { type: Object, required: false },
@@ -23,13 +25,17 @@ const parentEmbedConfig = useEmbedConfig();
23
25
  const { activeOrganization } = useChatAuth();
24
26
  const workspaceId = computed(() => activeOrganization.value?.id?.trim() ?? "");
25
27
  const normalizedTelaAgentId = computed(() => normalizeTelaAgentId(props.telaAgentId));
28
+ const normalizedConversationScope = computed(() => normalizeConversationScope(props.conversationScope));
29
+ const hideSettings = computed(() => props.hideSettings ?? false);
26
30
  const ignoredPropsWarningKey = ref(null);
27
31
  provideEmbedConfig({
28
32
  workspaceId,
29
33
  telaAgentId: normalizedTelaAgentId,
34
+ conversationScope: normalizedConversationScope,
30
35
  telaAgentInputs: computed(() => props.telaAgentInputs),
31
36
  workspaceSettings: computed(() => props.workspaceSettings),
32
37
  hideSidebar: computed(() => props.hideSidebar),
38
+ hideSettings,
33
39
  loadingMessages: computed(() => props.loadingMessages),
34
40
  loadingMessagesMode: computed(() => props.loadingMessagesMode),
35
41
  customComponents: computed(() => props.customComponents),
@@ -62,10 +68,11 @@ provideChatAction((payload) => {
62
68
  <template>
63
69
  <ChatEmbedInner
64
70
  v-if="workspaceId"
65
- :key="`${workspaceId}:${normalizedTelaAgentId ?? 'default-chat'}`"
71
+ :key="`${workspaceId}:${normalizedTelaAgentId ?? 'default-chat'}:scope:${resolveConversationScopeKeySegment(normalizedConversationScope)}`"
66
72
  :conversation-id="conversationId"
67
73
  :initial-conversation-id="initialConversationId"
68
74
  :hide-sidebar="hideSidebar"
75
+ :hide-settings="hideSettings"
69
76
  :user="normalizedTelaAgentId ? null : user"
70
77
  :features="features"
71
78
  @update:conversation-id="$emit('update:conversationId', $event)"