@meistrari/chat-nuxt 1.2.3 → 1.4.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 (93) hide show
  1. package/README.md +70 -38
  2. package/dist/module.d.mts +24 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +38 -29
  5. package/dist/runtime/components/MeistrariChatEmbed.d.vue.ts +9 -2
  6. package/dist/runtime/components/MeistrariChatEmbed.vue +11 -4
  7. package/dist/runtime/components/MeistrariChatEmbed.vue.d.ts +9 -2
  8. package/dist/runtime/components/app-markdown-render.d.vue.ts +3 -1
  9. package/dist/runtime/components/app-markdown-render.vue +15 -4
  10. package/dist/runtime/components/app-markdown-render.vue.d.ts +3 -1
  11. package/dist/runtime/components/chat/message-bubble.vue +10 -2
  12. package/dist/runtime/components/chat/thinking-indicator.vue +28 -17
  13. package/dist/runtime/composables/useChat.js +8 -1
  14. package/dist/runtime/composables/useChatAction.d.ts +10 -0
  15. package/dist/runtime/composables/useChatAction.js +12 -0
  16. package/dist/runtime/composables/useEmbedConfig.d.ts +10 -1
  17. package/dist/runtime/composables/useEmbedConfig.js +24 -3
  18. package/dist/runtime/embed/components/ChatConfigurationModal.vue +8 -6
  19. package/dist/runtime/embed/components/ChatEmbed.d.vue.ts +9 -2
  20. package/dist/runtime/embed/components/ChatEmbed.vue +22 -13
  21. package/dist/runtime/embed/components/ChatEmbed.vue.d.ts +9 -2
  22. package/dist/runtime/embed/components/ChatEmbedInner.vue +5 -1
  23. package/dist/runtime/server/api/conversations/[id]/messages/[messageId]/retry.post.js +7 -4
  24. package/dist/runtime/server/api/conversations/[id]/messages/index.get.js +1 -1
  25. package/dist/runtime/server/api/conversations/[id]/messages/index.post.js +15 -9
  26. package/dist/runtime/server/api/conversations/[id]/usage.get.js +1 -1
  27. package/dist/runtime/server/api/conversations/generate-title.post.js +18 -5
  28. package/dist/runtime/server/api/workspace/usage.get.js +1 -1
  29. package/dist/runtime/server/db/index.d.ts +20 -3
  30. package/dist/runtime/server/db/index.js +149 -10
  31. package/dist/runtime/server/db/schema/_schema.d.ts +2 -0
  32. package/dist/runtime/server/db/schema/_schema.js +3 -0
  33. package/dist/runtime/server/db/schema/conversation-usage.d.ts +1 -1
  34. package/dist/runtime/server/db/schema/conversation-usage.js +3 -2
  35. package/dist/runtime/server/db/schema/conversations.d.ts +1 -1
  36. package/dist/runtime/server/db/schema/conversations.js +3 -2
  37. package/dist/runtime/server/db/schema/external-skills.d.ts +1 -1
  38. package/dist/runtime/server/db/schema/external-skills.js +3 -2
  39. package/dist/runtime/server/db/schema/index.d.ts +1 -0
  40. package/dist/runtime/server/db/schema/index.js +1 -0
  41. package/dist/runtime/server/db/schema/messages.d.ts +1 -1
  42. package/dist/runtime/server/db/schema/messages.js +3 -2
  43. package/dist/runtime/server/db/schema/workspace-settings.d.ts +1 -1
  44. package/dist/runtime/server/db/schema/workspace-settings.js +3 -2
  45. package/dist/runtime/server/plugins/init-db.d.ts +2 -0
  46. package/dist/runtime/server/plugins/init-db.js +11 -0
  47. package/dist/runtime/server/utils/agent-api.d.ts +1 -1
  48. package/dist/runtime/server/utils/agent-api.js +2 -5
  49. package/dist/runtime/server/utils/billing.d.ts +26 -4
  50. package/dist/runtime/server/utils/billing.js +52 -13
  51. package/dist/runtime/server/utils/chat-workspace-settings.d.ts +2 -1
  52. package/dist/runtime/server/utils/chat-workspace-settings.js +11 -2
  53. package/dist/runtime/utils/chat-loading-messages.d.ts +12 -0
  54. package/dist/runtime/utils/chat-loading-messages.js +40 -0
  55. package/dist/runtime/utils/custom-markdown-components.d.ts +9 -0
  56. package/dist/runtime/utils/custom-markdown-components.js +54 -0
  57. package/dist/runtime/utils/features.d.ts +6 -0
  58. package/dist/runtime/utils/features.js +15 -0
  59. package/dist/types.d.mts +1 -1
  60. package/drizzle/0000_material_hiroim.sql +28 -0
  61. package/drizzle/0001_famous_scalphunter.sql +1 -0
  62. package/drizzle/0002_gorgeous_big_bertha.sql +1 -0
  63. package/drizzle/0003_massive_forge.sql +2 -0
  64. package/drizzle/0004_absent_black_tom.sql +2 -0
  65. package/drizzle/0005_first_kid_colt.sql +1 -0
  66. package/drizzle/0006_sloppy_mauler.sql +13 -0
  67. package/drizzle/0007_grey_rhino.sql +1 -0
  68. package/drizzle/0008_chilly_blue_blade.sql +27 -0
  69. package/drizzle/0009_cultured_maddog.sql +16 -0
  70. package/drizzle/0010_small_barracuda.sql +1 -0
  71. package/drizzle/0011_fat_lady_mastermind.sql +6 -0
  72. package/drizzle/0012_puzzling_rick_jones.sql +1 -0
  73. package/drizzle/0013_blue_gwen_stacy.sql +11 -0
  74. package/drizzle/meta/0000_snapshot.json +239 -0
  75. package/drizzle/meta/0001_snapshot.json +245 -0
  76. package/drizzle/meta/0002_snapshot.json +251 -0
  77. package/drizzle/meta/0003_snapshot.json +263 -0
  78. package/drizzle/meta/0004_snapshot.json +275 -0
  79. package/drizzle/meta/0005_snapshot.json +281 -0
  80. package/drizzle/meta/0006_snapshot.json +320 -0
  81. package/drizzle/meta/0007_snapshot.json +326 -0
  82. package/drizzle/meta/0008_snapshot.json +539 -0
  83. package/drizzle/meta/0009_snapshot.json +646 -0
  84. package/drizzle/meta/0010_snapshot.json +653 -0
  85. package/drizzle/meta/0011_snapshot.json +689 -0
  86. package/drizzle/meta/0012_snapshot.json +695 -0
  87. package/drizzle/meta/0013_snapshot.json +699 -0
  88. package/drizzle/meta/_journal.json +104 -0
  89. package/package.json +5 -3
  90. package/dist/runtime/server/tasks/send-daily-usage-report.d.ts +0 -2
  91. package/dist/runtime/server/tasks/send-daily-usage-report.js +0 -74
  92. package/dist/runtime/server/utils/auth-api.d.ts +0 -1
  93. package/dist/runtime/server/utils/auth-api.js +0 -27
@@ -0,0 +1,10 @@
1
+ export type ChatActionPayload = {
2
+ name: string;
3
+ data: unknown;
4
+ messageId?: string;
5
+ };
6
+ export type ChatActionHandler = (payload: ChatActionPayload) => void;
7
+ export declare function provideChatAction(handler: ChatActionHandler): void;
8
+ export declare function useChatAction(): {
9
+ emit(name: string, data?: unknown, messageId?: string): void;
10
+ };
@@ -0,0 +1,12 @@
1
+ const CHAT_ACTION_KEY = Symbol("chat-nuxt:action");
2
+ export function provideChatAction(handler) {
3
+ provide(CHAT_ACTION_KEY, handler);
4
+ }
5
+ export function useChatAction() {
6
+ const handler = inject(CHAT_ACTION_KEY, null);
7
+ return {
8
+ emit(name, data, messageId) {
9
+ handler?.({ name, data, messageId });
10
+ }
11
+ };
12
+ }
@@ -1,14 +1,23 @@
1
- import type { DeepReadonly, MaybeRefOrGetter, Ref } from 'vue';
1
+ import type { Component, DeepReadonly, MaybeRefOrGetter, Ref } from 'vue';
2
+ import type { ChatLoadingMessageMode } from '../utils/chat-loading-messages.js';
2
3
  import type { WorkspaceSettings } from './useWorkspaceSettings.js';
3
4
  export type EmbedConfig = {
4
5
  workspaceId: Ref<string>;
5
6
  workspaceSettings: Ref<DeepReadonly<WorkspaceSettings> | WorkspaceSettings | null | undefined>;
6
7
  hideSidebar: Ref<boolean>;
8
+ loadingMessages: Ref<readonly string[] | null | undefined>;
9
+ loadingMessagesMode: Ref<ChatLoadingMessageMode | null | undefined>;
10
+ customComponents: Ref<Record<string, Component> | undefined>;
11
+ customHtmlTags: Ref<readonly string[] | undefined>;
7
12
  };
8
13
  type EmbedConfigInput = {
9
14
  workspaceId: MaybeRefOrGetter<string>;
10
15
  workspaceSettings?: MaybeRefOrGetter<DeepReadonly<WorkspaceSettings> | WorkspaceSettings | null | undefined>;
11
16
  hideSidebar?: MaybeRefOrGetter<boolean | undefined>;
17
+ loadingMessages?: MaybeRefOrGetter<readonly string[] | null | undefined>;
18
+ loadingMessagesMode?: MaybeRefOrGetter<ChatLoadingMessageMode | null | undefined>;
19
+ customComponents?: MaybeRefOrGetter<Record<string, Component> | undefined>;
20
+ customHtmlTags?: MaybeRefOrGetter<readonly string[] | undefined>;
12
21
  };
13
22
  type ProvideEmbedConfigOptions = {
14
23
  syncGlobal?: boolean;
@@ -1,17 +1,30 @@
1
1
  const EMBED_CONFIG_KEY = "embed-config";
2
+ function useGlobalCustomComponentsState() {
3
+ const nuxtApp = useNuxtApp();
4
+ nuxtApp._chatEmbedConfigCustomComponents ??= shallowRef(void 0);
5
+ return nuxtApp._chatEmbedConfigCustomComponents;
6
+ }
2
7
  function useGlobalEmbedConfigState() {
3
8
  return {
4
9
  provided: useState("chat-embed-config-provided", () => false),
5
10
  workspaceId: useState("chat-embed-config-workspace-id", () => ""),
6
11
  workspaceSettings: useState("chat-embed-config-workspace-settings", () => void 0),
7
- hideSidebar: useState("chat-embed-config-hide-sidebar", () => void 0)
12
+ hideSidebar: useState("chat-embed-config-hide-sidebar", () => void 0),
13
+ loadingMessages: useState("chat-embed-config-loading-messages", () => void 0),
14
+ loadingMessagesMode: useState("chat-embed-config-loading-messages-mode", () => void 0),
15
+ customComponents: useGlobalCustomComponentsState(),
16
+ customHtmlTags: useState("chat-embed-config-custom-html-tags", () => void 0)
8
17
  };
9
18
  }
10
19
  export function provideEmbedConfig(input, options = {}) {
11
20
  const config = {
12
21
  workspaceId: computed(() => toValue(input.workspaceId)),
13
22
  workspaceSettings: computed(() => toValue(input.workspaceSettings)),
14
- hideSidebar: computed(() => toValue(input.hideSidebar) ?? true)
23
+ hideSidebar: computed(() => toValue(input.hideSidebar) ?? true),
24
+ loadingMessages: computed(() => toValue(input.loadingMessages)),
25
+ loadingMessagesMode: computed(() => toValue(input.loadingMessagesMode)),
26
+ customComponents: computed(() => toValue(input.customComponents)),
27
+ customHtmlTags: computed(() => toValue(input.customHtmlTags))
15
28
  };
16
29
  const globalConfig = useGlobalEmbedConfigState();
17
30
  if (options.syncGlobal ?? true) {
@@ -20,6 +33,10 @@ export function provideEmbedConfig(input, options = {}) {
20
33
  globalConfig.workspaceId.value = config.workspaceId.value;
21
34
  globalConfig.workspaceSettings.value = config.workspaceSettings.value;
22
35
  globalConfig.hideSidebar.value = config.hideSidebar.value;
36
+ globalConfig.loadingMessages.value = config.loadingMessages.value;
37
+ globalConfig.loadingMessagesMode.value = config.loadingMessagesMode.value;
38
+ globalConfig.customComponents.value = config.customComponents.value;
39
+ globalConfig.customHtmlTags.value = config.customHtmlTags.value;
23
40
  });
24
41
  }
25
42
  provide(EMBED_CONFIG_KEY, config);
@@ -34,6 +51,10 @@ export function useEmbedConfig() {
34
51
  return {
35
52
  workspaceId: computed(() => globalConfig.workspaceId.value),
36
53
  workspaceSettings: computed(() => globalConfig.workspaceSettings.value),
37
- hideSidebar: computed(() => globalConfig.hideSidebar.value ?? true)
54
+ hideSidebar: computed(() => globalConfig.hideSidebar.value ?? true),
55
+ loadingMessages: computed(() => globalConfig.loadingMessages.value),
56
+ loadingMessagesMode: computed(() => globalConfig.loadingMessagesMode.value),
57
+ customComponents: computed(() => globalConfig.customComponents.value),
58
+ customHtmlTags: computed(() => globalConfig.customHtmlTags.value)
38
59
  };
39
60
  }
@@ -18,6 +18,7 @@ const {
18
18
  fetchSettings,
19
19
  updateSettings
20
20
  } = useWorkspaceSettings();
21
+ const features = useChatNuxtFeatures();
21
22
  const searchQuery = ref("");
22
23
  const activeTab = ref(props.initialTab);
23
24
  const showDiscardConfirm = ref(false);
@@ -34,14 +35,14 @@ const telaTools = useWorkspaceTelaTools(canvas, projects, searchQuery, currentCa
34
35
  const knowledgeSources = useWorkspaceKnowledgeSources(workstations, canvas, projects, searchQuery, currentKnowledgeSources);
35
36
  const contextFiles = useWorkspaceContextFiles(searchQuery, currentContextFiles);
36
37
  const externalSkills = useWorkspaceExternalSkills(currentExternalSkills, searchQuery);
37
- const settingsTabs = [
38
+ const settingsTabs = computed(() => [
38
39
  { value: "personalization", label: "Personaliza\xE7\xE3o", icon: "i-ph-sliders" },
39
40
  { value: "context-files", label: "Arquivos de contexto", icon: "i-ph-file-text" },
40
41
  { value: "knowledge-sources", label: "Tela workstations", icon: "i-ph-database" },
41
42
  { value: "tela-tools", label: "Skills Tela", icon: "i-ph-puzzle-piece" },
42
43
  { value: "external-skills", label: "Skills Externas", icon: "i-ph-wrench" },
43
- { value: "credentials", label: "Credenciais", icon: "i-ph-password-bold" }
44
- ];
44
+ ...features.credentials ? [{ value: "credentials", label: "Credenciais", icon: "i-ph-password-bold" }] : []
45
+ ]);
45
46
  const hasChanges = computed(
46
47
  () => personalization.hasChanges.value || contextFiles.hasChanges.value || telaTools.hasChanges.value || knowledgeSources.hasChanges.value || externalSkills.hasChanges.value
47
48
  );
@@ -84,9 +85,10 @@ function initializeConfigurationState() {
84
85
  externalSkills.initialize();
85
86
  }
86
87
  function activateTab(tab) {
87
- activeTab.value = tab;
88
+ const isVisible = settingsTabs.value.some((t) => t.value === tab);
89
+ activeTab.value = isVisible ? tab : "personalization";
88
90
  searchQuery.value = "";
89
- if (tab === "external-skills")
91
+ if (activeTab.value === "external-skills")
90
92
  void externalSkills.fetchGitHubConnection();
91
93
  }
92
94
  async function loadSettings() {
@@ -336,7 +338,7 @@ function cancelDiscard() {
336
338
  />
337
339
 
338
340
  <ChatConfigurationCredentialsTab
339
- v-else-if="activeTab === 'credentials'"
341
+ v-else-if="activeTab === 'credentials' && features.credentials"
340
342
  :search-query="searchQuery"
341
343
  @update:search-query="searchQuery = $event"
342
344
  />
@@ -1,20 +1,27 @@
1
- import type { DeepReadonly } from 'vue';
1
+ import type { Component, DeepReadonly } from 'vue';
2
+ import { type ChatActionPayload } from '../../composables/useChatAction.js';
2
3
  import type { WorkspaceSettings } from '../../composables/useWorkspaceSettings.js';
4
+ import type { ChatLoadingMessageMode } from '../../utils/chat-loading-messages.js';
3
5
  import type { ChatActor, ChatFeatureConfig } from '../../utils/tela-chat.js';
4
6
  type EmbedUser = ChatActor;
5
7
  type __VLS_Props = {
6
- workspaceId: string;
7
8
  workspaceSettings?: DeepReadonly<WorkspaceSettings> | WorkspaceSettings | null;
8
9
  conversationId?: string | null;
9
10
  initialConversationId?: string | null;
10
11
  hideSidebar?: boolean;
11
12
  user?: EmbedUser | null;
12
13
  features?: Partial<ChatFeatureConfig>;
14
+ loadingMessages?: readonly string[] | null;
15
+ loadingMessagesMode?: ChatLoadingMessageMode | null;
16
+ customComponents?: Record<string, Component>;
17
+ customHtmlTags?: readonly string[];
13
18
  };
14
19
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
20
  "update:conversationId": (value: string | null) => any;
21
+ action: (payload: ChatActionPayload) => any;
16
22
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
23
  "onUpdate:conversationId"?: ((value: string | null) => any) | undefined;
24
+ onAction?: ((payload: ChatActionPayload) => any) | undefined;
18
25
  }>, {
19
26
  initialConversationId: string | null;
20
27
  hideSidebar: boolean;
@@ -1,32 +1,41 @@
1
1
  <script setup>
2
2
  import { provideEmbedConfig, useEmbedConfig } from "../../composables/useEmbedConfig";
3
+ import { provideChatAction } from "../../composables/useChatAction";
3
4
  import ChatEmbedInner from "./ChatEmbedInner.vue";
4
5
  const props = defineProps({
5
- workspaceId: { type: String, required: true },
6
6
  workspaceSettings: { type: null, required: false },
7
7
  conversationId: { type: [String, null], required: false },
8
8
  initialConversationId: { type: [String, null], required: false, default: null },
9
9
  hideSidebar: { type: Boolean, required: false, default: true },
10
10
  user: { type: [Object, null], required: false },
11
- features: { type: Object, required: false }
11
+ features: { type: Object, required: false },
12
+ loadingMessages: { type: [Array, null], required: false },
13
+ loadingMessagesMode: { type: [String, null], required: false },
14
+ customComponents: { type: Object, required: false },
15
+ customHtmlTags: { type: Array, required: false }
12
16
  });
13
- defineEmits(["update:conversationId"]);
17
+ const emit = defineEmits(["update:conversationId", "action"]);
14
18
  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 });
19
+ const { activeOrganization } = useTelaApplicationAuth();
20
+ const workspaceId = computed(() => activeOrganization.value?.id?.trim() ?? "");
21
+ provideEmbedConfig({
22
+ workspaceId,
23
+ workspaceSettings: computed(() => props.workspaceSettings),
24
+ hideSidebar: computed(() => props.hideSidebar),
25
+ loadingMessages: computed(() => props.loadingMessages),
26
+ loadingMessagesMode: computed(() => props.loadingMessagesMode),
27
+ customComponents: computed(() => props.customComponents),
28
+ customHtmlTags: computed(() => props.customHtmlTags)
29
+ }, { syncGlobal: !parentEmbedConfig });
30
+ provideChatAction((payload) => {
31
+ emit("action", payload);
32
+ });
25
33
  </script>
26
34
 
27
35
  <template>
28
36
  <!-- Force a clean state namespace when the workspace changes. -->
29
37
  <ChatEmbedInner
38
+ v-if="workspaceId"
30
39
  :key="workspaceId"
31
40
  :conversation-id="conversationId"
32
41
  :initial-conversation-id="initialConversationId"
@@ -1,20 +1,27 @@
1
- import type { DeepReadonly } from 'vue';
1
+ import type { Component, DeepReadonly } from 'vue';
2
+ import { type ChatActionPayload } from '../../composables/useChatAction.js';
2
3
  import type { WorkspaceSettings } from '../../composables/useWorkspaceSettings.js';
4
+ import type { ChatLoadingMessageMode } from '../../utils/chat-loading-messages.js';
3
5
  import type { ChatActor, ChatFeatureConfig } from '../../utils/tela-chat.js';
4
6
  type EmbedUser = ChatActor;
5
7
  type __VLS_Props = {
6
- workspaceId: string;
7
8
  workspaceSettings?: DeepReadonly<WorkspaceSettings> | WorkspaceSettings | null;
8
9
  conversationId?: string | null;
9
10
  initialConversationId?: string | null;
10
11
  hideSidebar?: boolean;
11
12
  user?: EmbedUser | null;
12
13
  features?: Partial<ChatFeatureConfig>;
14
+ loadingMessages?: readonly string[] | null;
15
+ loadingMessagesMode?: ChatLoadingMessageMode | null;
16
+ customComponents?: Record<string, Component>;
17
+ customHtmlTags?: readonly string[];
13
18
  };
14
19
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
15
20
  "update:conversationId": (value: string | null) => any;
21
+ action: (payload: ChatActionPayload) => any;
16
22
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
17
23
  "onUpdate:conversationId"?: ((value: string | null) => any) | undefined;
24
+ onAction?: ((payload: ChatActionPayload) => any) | undefined;
18
25
  }>, {
19
26
  initialConversationId: string | null;
20
27
  hideSidebar: boolean;
@@ -28,7 +28,11 @@ const activeConversationId = ref(
28
28
  );
29
29
  const loadingConversationId = ref(null);
30
30
  const isReady = ref(false);
31
- const resolvedFeatures = computed(() => resolveChatFeatures(props.features));
31
+ const moduleFeatures = useChatNuxtFeatures();
32
+ const resolvedFeatures = computed(() => resolveChatFeatures({
33
+ showUsageTab: moduleFeatures.usage ?? props.features?.showUsageTab,
34
+ showDebugOption: props.features?.showDebugOption
35
+ }));
32
36
  const dropZoneRef = ref(null);
33
37
  const activeTab = ref("chat");
34
38
  const messageInputRef = ref(null);
@@ -13,6 +13,7 @@ import {
13
13
  serializeResolvedWorkspaceSettings
14
14
  } from "#chat-runtime/server/utils/chat-workspace-settings";
15
15
  import { resolveWorkspaceCredentials } from "#chat-runtime/server/utils/workspace";
16
+ import { useChatNuxtFeatures } from "#chat-runtime/utils/features";
16
17
  import { mergeFileUrls } from "#chat-runtime/utils/file";
17
18
  import { isValidModel } from "#chat-runtime/types/chat";
18
19
  export default defineEventHandler(async (event) => {
@@ -107,10 +108,12 @@ export default defineEventHandler(async (event) => {
107
108
  try {
108
109
  if (needsNewThread) {
109
110
  let environmentVariables = {};
110
- try {
111
- environmentVariables = await resolveWorkspaceCredentials(event, context.workspaceId);
112
- } catch (error) {
113
- logger.warn({ conversationId, messageId, error }, "Failed to resolve workspace credentials on retry, proceeding without env vars");
111
+ if (useChatNuxtFeatures().credentials) {
112
+ try {
113
+ environmentVariables = await resolveWorkspaceCredentials(event, context.workspaceId);
114
+ } catch (error) {
115
+ logger.warn({ conversationId, messageId, error }, "Failed to resolve workspace credentials on retry, proceeding without env vars");
116
+ }
114
117
  }
115
118
  const hasEnvironmentVariables = Object.keys(environmentVariables).length > 0;
116
119
  const result = await createChatAgentSession(token, {
@@ -131,7 +131,7 @@ export default defineEventHandler(async (event) => {
131
131
  }
132
132
  if (agentSession.status === "completed" || agentSession.status === "waiting_messages") {
133
133
  if (hasNewReasoning) {
134
- const content = extractContentFromSession(agentSession, newReasoning);
134
+ const content = extractContentFromSession(agentSession);
135
135
  updateData.content = content;
136
136
  updateData.status = "completed";
137
137
  updateData.updatedAt = /* @__PURE__ */ new Date();
@@ -9,10 +9,12 @@ import { logger } from "#chat-runtime/server/utils/logger";
9
9
  import {
10
10
  getConversationAppliedSettings,
11
11
  getWorkspaceSettingsUpdatedAt,
12
+ parseWorkspaceSettingsSnapshot,
12
13
  resolveWorkspaceSettings,
13
14
  serializeResolvedWorkspaceSettings
14
15
  } from "#chat-runtime/server/utils/chat-workspace-settings";
15
16
  import { resolveWorkspaceCredentials } from "#chat-runtime/server/utils/workspace";
17
+ import { useChatNuxtFeatures } from "#chat-runtime/utils/features";
16
18
  import { mergeFileUrls } from "#chat-runtime/utils/file";
17
19
  import { isValidModel } from "#chat-runtime/types/chat";
18
20
  export default defineEventHandler(async (event) => {
@@ -52,14 +54,16 @@ export default defineEventHandler(async (event) => {
52
54
  });
53
55
  }
54
56
  const effectiveModel = body.model ?? (isValidModel(conversation.model) ? conversation.model : void 0);
55
- const trustedSettings = conversation.appliedSettingsSnapshot ? null : await resolveWorkspaceSettings(event, context, db);
56
- const appliedSettings = getConversationAppliedSettings(conversation, trustedSettings);
57
+ const explicitSettings = !conversation.appliedSettingsSnapshot && body.workspaceSettingsSnapshot !== void 0 ? parseWorkspaceSettingsSnapshot(body.workspaceSettingsSnapshot) : void 0;
58
+ const trustedSettings = conversation.appliedSettingsSnapshot || explicitSettings ? null : await resolveWorkspaceSettings(event, context, db);
59
+ const appliedSettings = getConversationAppliedSettings(conversation, trustedSettings, explicitSettings);
57
60
  const now = /* @__PURE__ */ new Date();
58
61
  const assistantCreatedAt = new Date(now.getTime() + 1);
59
- if (!conversation.appliedSettingsSnapshot && trustedSettings) {
62
+ const snapshotSettings = explicitSettings ?? trustedSettings;
63
+ if (!conversation.appliedSettingsSnapshot && snapshotSettings) {
60
64
  await db.update(schema.conversations).set({
61
- appliedSettingsAt: getWorkspaceSettingsUpdatedAt(trustedSettings),
62
- appliedSettingsSnapshot: serializeResolvedWorkspaceSettings(trustedSettings),
65
+ appliedSettingsAt: getWorkspaceSettingsUpdatedAt(snapshotSettings),
66
+ appliedSettingsSnapshot: serializeResolvedWorkspaceSettings(snapshotSettings),
63
67
  updatedAt: now
64
68
  }).where(and(
65
69
  eq(schema.conversations.id, conversationId),
@@ -113,10 +117,12 @@ export default defineEventHandler(async (event) => {
113
117
  const skills = externalSkillRefs.length > 0 ? externalSkillRefs : void 0;
114
118
  if (needsNewThread) {
115
119
  let environmentVariables = {};
116
- try {
117
- environmentVariables = await resolveWorkspaceCredentials(event, context.workspaceId);
118
- } catch (error) {
119
- logger.warn({ conversationId, error }, "Failed to resolve workspace credentials, proceeding without env vars");
120
+ if (useChatNuxtFeatures().credentials) {
121
+ try {
122
+ environmentVariables = await resolveWorkspaceCredentials(event, context.workspaceId);
123
+ } catch (error) {
124
+ logger.warn({ conversationId, error }, "Failed to resolve workspace credentials, proceeding without env vars");
125
+ }
120
126
  }
121
127
  const hasEnvironmentVariables = Object.keys(environmentVariables).length > 0;
122
128
  createChatAgentSession(token, {
@@ -31,7 +31,7 @@ export default defineEventHandler(async (event) => {
31
31
  eq(schema.conversationUsage.workspaceId, context.workspaceId)
32
32
  )
33
33
  );
34
- const billing = await getWorkspaceBilling(context.workspaceId);
34
+ const billing = await getWorkspaceBilling(event);
35
35
  if (!usage) {
36
36
  return { usage: null, billing };
37
37
  }
@@ -3,7 +3,20 @@ import { useRuntimeConfig } from "nitropack/runtime";
3
3
  import { createTelaClient } from "@meistrari/tela-sdk-js";
4
4
  import { logger } from "#chat-runtime/server/utils/logger";
5
5
  import { requireUser } from "#chat-runtime/server/utils/auth";
6
+ import { useChatNuxtFeatures } from "#chat-runtime/utils/features";
6
7
  const TITLE_CANVAS_ID = "ba06c4a0-ef05-4fb6-b4ef-5af3d63a1ba2";
8
+ const FALLBACK_TITLE_MAX_LENGTH = 60;
9
+ const FALLBACK_TITLE_DEFAULT = "New chat";
10
+ function buildFallbackTitle(userMessage) {
11
+ const normalized = userMessage.replace(/\s+/g, " ").trim();
12
+ if (!normalized) {
13
+ return FALLBACK_TITLE_DEFAULT;
14
+ }
15
+ if (normalized.length <= FALLBACK_TITLE_MAX_LENGTH) {
16
+ return normalized;
17
+ }
18
+ return `${normalized.slice(0, FALLBACK_TITLE_MAX_LENGTH - 1).trimEnd()}\u2026`;
19
+ }
7
20
  export default defineEventHandler(async (event) => {
8
21
  requireUser(event);
9
22
  const { userMessage, assistantResponse } = await readBody(event);
@@ -14,11 +27,11 @@ export default defineEventHandler(async (event) => {
14
27
  });
15
28
  }
16
29
  const { telaApiKey } = useRuntimeConfig();
17
- if (!telaApiKey) {
18
- throw createError({
19
- statusCode: 500,
20
- statusMessage: "Tela API key not configured"
21
- });
30
+ const features = useChatNuxtFeatures();
31
+ if (!features.generateTitle || !telaApiKey) {
32
+ const fallback = buildFallbackTitle(userMessage);
33
+ logger.debug({ titleLength: fallback.length, reason: !features.generateTitle ? "feature-disabled" : "no-tela-api-key" }, "Title generated via fallback");
34
+ return { title: fallback };
22
35
  }
23
36
  try {
24
37
  const tela = createTelaClient({
@@ -54,7 +54,7 @@ export default defineEventHandler(async (event) => {
54
54
  }).from(schema.conversationUsage).where(and(...conditions)).orderBy(desc(schema.conversationUsage.createdAt)).limit(limit + 1);
55
55
  const hasMore = records.length > limit;
56
56
  const paginatedRecords = records.slice(0, limit);
57
- const billing = await getWorkspaceBilling(session.workspace.id);
57
+ const billing = await getWorkspaceBilling(event);
58
58
  const formattedRecords = paginatedRecords.map((r) => {
59
59
  const transformedCosts = transformCostForBilling({
60
60
  promptCost: Number(r.promptCost),
@@ -1,6 +1,23 @@
1
+ import { drizzle as drizzlePostgres } from 'drizzle-orm/postgres-js';
2
+ import type { PgliteDatabase } from 'drizzle-orm/pglite';
1
3
  import * as schema from './schema.js';
2
4
  export type { ContextFile, ExternalSkill } from './schema.js';
3
- export declare function useDb(): import("drizzle-orm/postgres-js").PostgresJsDatabase<Record<string, unknown>> & {
4
- $client: import("postgres").Sql<{}>;
5
- };
5
+ type PostgresDb = ReturnType<typeof drizzlePostgres<typeof schema>>;
6
+ type PgliteDb = PgliteDatabase<typeof schema>;
7
+ type AnyDb = PostgresDb | PgliteDb;
8
+ export declare function isPgliteUrl(url: string): boolean;
9
+ /**
10
+ * Eagerly initializes the database during Nitro startup. Idempotent. Both
11
+ * Postgres and pglite paths now run migrations on boot, so the chat-nuxt
12
+ * tables in the `chat` Postgres schema are guaranteed to exist before the
13
+ * first request hits a route that calls useDb().
14
+ */
15
+ export declare function initDb(): Promise<AnyDb>;
16
+ /**
17
+ * Returns the initialized database instance. initDb() must have already run
18
+ * via the boot plugin so migrations have been applied — bare useDb() now
19
+ * throws if called before init, regardless of driver, instead of silently
20
+ * skipping migrations on the Postgres path.
21
+ */
22
+ export declare function useDb(): AnyDb;
6
23
  export { schema };
@@ -1,20 +1,159 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { dirname } from "node:path";
1
3
  import { useRuntimeConfig } from "nitropack/runtime";
4
+ import { sql } from "drizzle-orm";
2
5
  import postgres from "postgres";
3
- import { drizzle } from "drizzle-orm/postgres-js";
6
+ import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
7
+ import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";
4
8
  import * as schema from "./schema/index.js";
5
- import { logger } from "#chat-runtime/server/utils/logger";
9
+ import { CHAT_SCHEMA_NAME } from "./schema/_schema.js";
10
+ import { logger } from "../utils/logger.js";
6
11
  let dbInstance = null;
7
- export function useDb() {
12
+ let dbInitPromise = null;
13
+ const LEGACY_MIGRATIONS_SCHEMA = "drizzle";
14
+ const MIGRATIONS_TABLE = "__drizzle_migrations";
15
+ const MIGRATIONS_LEDGER_LOCK_KEY = 581073927;
16
+ export function isPgliteUrl(url) {
17
+ return url.startsWith("pglite:") || url.startsWith("file:");
18
+ }
19
+ function resolvePgliteDataDir(url) {
20
+ if (url.startsWith("pglite:")) {
21
+ return url.slice("pglite:".length);
22
+ }
23
+ if (url.startsWith("file://")) {
24
+ return url.slice("file://".length);
25
+ }
26
+ if (url.startsWith("file:")) {
27
+ return url.slice("file:".length);
28
+ }
29
+ return url;
30
+ }
31
+ const IGNORED_NOTICE_CODES = /* @__PURE__ */ new Set(["42P06", "42P07"]);
32
+ function getRows(result) {
33
+ if (Array.isArray(result)) {
34
+ return result;
35
+ }
36
+ if (result && typeof result === "object" && "rows" in result && Array.isArray(result.rows)) {
37
+ return result.rows;
38
+ }
39
+ return [];
40
+ }
41
+ async function tableExists(db, schemaName, tableName) {
42
+ const result = await db.execute(sql`
43
+ select exists (
44
+ select 1
45
+ from information_schema.tables
46
+ where table_schema = ${schemaName}
47
+ and table_name = ${tableName}
48
+ ) as "exists"
49
+ `);
50
+ const [row] = getRows(result);
51
+ return row?.exists ?? false;
52
+ }
53
+ async function tableHasRows(db, schemaName, tableName) {
54
+ const result = await db.execute(sql`
55
+ select exists (
56
+ select 1
57
+ from ${sql.identifier(schemaName)}.${sql.identifier(tableName)}
58
+ ) as "exists"
59
+ `);
60
+ const [row] = getRows(result);
61
+ return row?.exists ?? false;
62
+ }
63
+ async function seedChatMigrationsLedger(db) {
64
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS ${sql.identifier(CHAT_SCHEMA_NAME)}`);
65
+ await db.execute(sql`
66
+ CREATE TABLE IF NOT EXISTS ${sql.identifier(CHAT_SCHEMA_NAME)}.${sql.identifier(MIGRATIONS_TABLE)} (
67
+ id SERIAL PRIMARY KEY,
68
+ hash text NOT NULL,
69
+ created_at bigint
70
+ )
71
+ `);
72
+ if (await tableHasRows(db, CHAT_SCHEMA_NAME, MIGRATIONS_TABLE)) {
73
+ return;
74
+ }
75
+ if (!await tableExists(db, LEGACY_MIGRATIONS_SCHEMA, MIGRATIONS_TABLE)) {
76
+ return;
77
+ }
78
+ await db.execute(sql`
79
+ INSERT INTO ${sql.identifier(CHAT_SCHEMA_NAME)}.${sql.identifier(MIGRATIONS_TABLE)} ("hash", "created_at")
80
+ SELECT "hash", "created_at"
81
+ FROM ${sql.identifier(LEGACY_MIGRATIONS_SCHEMA)}.${sql.identifier(MIGRATIONS_TABLE)}
82
+ ORDER BY "created_at"
83
+ `);
84
+ }
85
+ async function initPostgresAsync(databaseUrl, migrationsDir) {
86
+ const client = postgres(databaseUrl, {
87
+ max: 10,
88
+ onnotice: (notice) => {
89
+ if (notice.code && IGNORED_NOTICE_CODES.has(notice.code))
90
+ return;
91
+ logger.info({ notice }, "Postgres notice");
92
+ }
93
+ });
94
+ const db = drizzlePostgres(client, { schema });
95
+ if (migrationsDir) {
96
+ await db.transaction(async (tx) => {
97
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(${MIGRATIONS_LEDGER_LOCK_KEY})`);
98
+ await seedChatMigrationsLedger(tx);
99
+ });
100
+ await migratePostgres(db, { migrationsFolder: migrationsDir, migrationsSchema: CHAT_SCHEMA_NAME });
101
+ logger.info({ migrationsSchema: CHAT_SCHEMA_NAME }, "Postgres connection pool initialized and migrations applied");
102
+ } else {
103
+ logger.warn("Postgres connection pool initialized without migrations directory \u2014 schema may be out of date");
104
+ }
105
+ return db;
106
+ }
107
+ async function initPgliteAsync(databaseUrl, migrationsDir) {
108
+ const dataDir = resolvePgliteDataDir(databaseUrl);
109
+ await mkdir(dirname(dataDir), { recursive: true });
110
+ const [{ PGlite }, { drizzle }, { migrate }] = await Promise.all([
111
+ import("@electric-sql/pglite"),
112
+ import("drizzle-orm/pglite"),
113
+ import("drizzle-orm/pglite/migrator")
114
+ ]);
115
+ const client = new PGlite(dataDir);
116
+ const db = drizzle(client, { schema });
117
+ if (migrationsDir) {
118
+ await seedChatMigrationsLedger(db);
119
+ await migrate(db, { migrationsFolder: migrationsDir, migrationsSchema: CHAT_SCHEMA_NAME });
120
+ logger.info({ dataDir, migrationsSchema: CHAT_SCHEMA_NAME }, "pglite database initialized and migrated");
121
+ } else {
122
+ logger.warn({ dataDir }, "pglite database initialized without migrations directory \u2014 schema may be empty");
123
+ }
124
+ return db;
125
+ }
126
+ export async function initDb() {
8
127
  if (dbInstance) {
9
128
  return dbInstance;
10
129
  }
11
- const config = useRuntimeConfig();
12
- if (!config.databaseUrl) {
13
- throw new Error("DATABASE_URL is not configured");
130
+ if (dbInitPromise) {
131
+ return dbInitPromise;
132
+ }
133
+ dbInitPromise = (async () => {
134
+ const config = useRuntimeConfig();
135
+ const databaseUrl = config.databaseUrl;
136
+ const migrationsDir = config.chatNuxt?.migrationsDir ?? "";
137
+ if (!databaseUrl) {
138
+ throw new Error("DATABASE_URL is not configured and no pglite fallback path was set");
139
+ }
140
+ const db = isPgliteUrl(databaseUrl) ? await initPgliteAsync(databaseUrl, migrationsDir) : await initPostgresAsync(databaseUrl, migrationsDir);
141
+ dbInstance = db;
142
+ return db;
143
+ })();
144
+ try {
145
+ return await dbInitPromise;
146
+ } catch (error) {
147
+ dbInitPromise = null;
148
+ throw error;
149
+ }
150
+ }
151
+ export function useDb() {
152
+ if (dbInstance) {
153
+ return dbInstance;
14
154
  }
15
- const client = postgres(config.databaseUrl, { max: 10 });
16
- dbInstance = drizzle(client, { schema });
17
- logger.info("Database connection pool initialized");
18
- return dbInstance;
155
+ throw new Error(
156
+ "chat-nuxt database is not initialized. The chat-nuxt boot plugin should have called initDb() before any request \u2014 check that the module registered server/plugins/init-db.ts."
157
+ );
19
158
  }
20
159
  export { schema };
@@ -0,0 +1,2 @@
1
+ export declare const CHAT_SCHEMA_NAME = "chat";
2
+ export declare const chatSchema: import("drizzle-orm/pg-core").PgSchema<"chat">;
@@ -0,0 +1,3 @@
1
+ import { pgSchema } from "drizzle-orm/pg-core";
2
+ export const CHAT_SCHEMA_NAME = "chat";
3
+ export const chatSchema = pgSchema(CHAT_SCHEMA_NAME);