From e865dc79c45b311e0e874b06c2afbe578cba15f1 Mon Sep 17 00:00:00 2001 From: dal Date: Fri, 12 Sep 2025 11:09:15 -0600 Subject: [PATCH] Enhance chat functionality by adding user last used shortcuts tracking and updating request handling to include metadata. Refactor shortcut listing to prioritize last used shortcuts in the response. --- apps/server/src/api/v2/chats/handler.ts | 25 ++++---- .../src/api/v2/chats/services/chat-helpers.ts | 8 ++- .../src/api/v2/chats/services/chat-service.ts | 4 +- apps/server/src/api/v2/shortcuts/GET.ts | 40 +++++++++++- apps/server/src/api/v2/shortcuts/[id]/PUT.ts | 5 +- apps/server/src/api/v2/shortcuts/index.ts | 10 +-- ...avengers.sql => 0096_outgoing_shotgun.sql} | 4 +- .../database/drizzle/meta/0096_snapshot.json | 18 +++++- packages/database/drizzle/meta/_journal.json | 4 +- packages/database/src/queries/chats/chats.ts | 2 + packages/database/src/queries/users/index.ts | 1 + .../queries/users/update-user-shortcuts.ts | 62 +++++++++++++++++++ packages/database/src/schema-types/user.ts | 5 ++ packages/database/src/schema.ts | 4 ++ .../database/src/schemas/message-schemas.ts | 7 +++ .../server-shared/src/chats/chat.types.ts | 9 +++ 16 files changed, 180 insertions(+), 28 deletions(-) rename packages/database/drizzle/{0096_secret_next_avengers.sql => 0096_outgoing_shotgun.sql} (87%) create mode 100644 packages/database/src/queries/users/update-user-shortcuts.ts diff --git a/apps/server/src/api/v2/chats/handler.ts b/apps/server/src/api/v2/chats/handler.ts index 3edb0ec68..ebe216909 100644 --- a/apps/server/src/api/v2/chats/handler.ts +++ b/apps/server/src/api/v2/chats/handler.ts @@ -1,4 +1,4 @@ -import { getUserOrganizationId } from '@buster/database'; +import { getUserOrganizationId, updateUserLastUsedShortcuts } from '@buster/database'; import type { User } from '@buster/database'; import { type ChatCreateHandlerRequest, @@ -9,7 +9,6 @@ import { import { tasks } from '@trigger.dev/sdk'; import { handleAssetChat, handleAssetChatWithPrompt } from './services/chat-helpers'; import { initializeChat } from './services/chat-service'; -import { enhanceMessageWithShortcut } from './services/shortcut-service'; /** * Handler function for creating a new chat. @@ -57,20 +56,13 @@ export async function createChatHandler( throw new ChatError(ChatErrorCode.INVALID_REQUEST, 'prompt or asset_id is required', 400); } - // Process shortcuts if present - let enhancedPrompt = request.prompt; - if (request.prompt) { - // Check for shortcut pattern in message (e.g., /weekly-report) - enhancedPrompt = await enhanceMessageWithShortcut(request.prompt, user.id, organizationId); - } - // Set message_analysis_mode if not provided (from staging) if (request.prompt && !request.message_analysis_mode) { request.message_analysis_mode = 'auto'; } - // Update request with enhanced prompt and message_analysis_mode - const processedRequest = { ...request, prompt: enhancedPrompt }; + // Update request with message_analysis_mode + const processedRequest = { ...request }; // Initialize chat (new or existing) // When we have both asset and prompt, we'll skip creating the initial message // since handleAssetChatWithPrompt will create both the import and prompt messages @@ -85,6 +77,17 @@ export async function createChatHandler( const { chatId, messageId, chat } = await initializeChat(modifiedRequest, user, organizationId); + // Update user's last used shortcuts if any were provided in metadata + if ( + processedRequest.metadata?.shortcutIds && + processedRequest.metadata.shortcutIds.length > 0 + ) { + await updateUserLastUsedShortcuts({ + userId: user.id, + shortcutIds: processedRequest.metadata.shortcutIds, + }); + } + // Handle asset-based chat if needed let finalChat: ChatWithMessages = chat; let actualMessageId = messageId; // Track the actual message ID to use for triggering diff --git a/apps/server/src/api/v2/chats/services/chat-helpers.ts b/apps/server/src/api/v2/chats/services/chat-helpers.ts index b06dd2981..90fc48b61 100644 --- a/apps/server/src/api/v2/chats/services/chat-helpers.ts +++ b/apps/server/src/api/v2/chats/services/chat-helpers.ts @@ -18,6 +18,7 @@ import type { ChatMessageResponseMessage, ChatWithMessages, MessageAnalysisMode, + MessageMetadata, } from '@buster/server-shared/chats'; import { ChatError, ChatErrorCode } from '@buster/server-shared/chats'; import { PostProcessingMessageSchema } from '@buster/server-shared/message'; @@ -204,7 +205,8 @@ export async function handleExistingChat( prompt: string | undefined, messageAnalysisMode: MessageAnalysisMode | undefined, user: User, - redoFromMessageId?: string + redoFromMessageId?: string, + metadata?: MessageMetadata ): Promise<{ chatId: string; messageId: string; @@ -262,6 +264,7 @@ export async function handleExistingChat( messageAnalysisMode: messageAnalysisMode, userId: user.id, messageId, + metadata, }) : Promise.resolve(null), getMessagesForChat(chatId), @@ -295,6 +298,7 @@ export async function handleNewChat({ messageAnalysisMode, user, organizationId, + metadata, }: { title: string; messageId: string; @@ -302,6 +306,7 @@ export async function handleNewChat({ messageAnalysisMode: MessageAnalysisMode | undefined; user: User; organizationId: string; + metadata?: MessageMetadata; }): Promise<{ chatId: string; messageId: string; @@ -346,6 +351,7 @@ export async function handleNewChat({ content: prompt, } as ModelMessage, ], + metadata: metadata || {}, }) .returning(); diff --git a/apps/server/src/api/v2/chats/services/chat-service.ts b/apps/server/src/api/v2/chats/services/chat-service.ts index 866b5ab1b..0a9ddb95a 100644 --- a/apps/server/src/api/v2/chats/services/chat-service.ts +++ b/apps/server/src/api/v2/chats/services/chat-service.ts @@ -47,7 +47,8 @@ export async function initializeChat( request.prompt, request.message_analysis_mode, user, - redoFromMessageId + redoFromMessageId, + request.metadata ); } @@ -59,6 +60,7 @@ export async function initializeChat( messageAnalysisMode: request.message_analysis_mode, user, organizationId, + metadata: request.metadata, }); } catch (error) { // Log detailed error context diff --git a/apps/server/src/api/v2/shortcuts/GET.ts b/apps/server/src/api/v2/shortcuts/GET.ts index 3f0192cfb..23de8d51d 100644 --- a/apps/server/src/api/v2/shortcuts/GET.ts +++ b/apps/server/src/api/v2/shortcuts/GET.ts @@ -1,5 +1,6 @@ import type { User } from '@buster/database'; -import { getUserOrganizationId, getUserShortcuts } from '@buster/database'; +import { db, getUserOrganizationId, getUserShortcuts, users } from '@buster/database'; +import { eq } from '@buster/database'; import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts'; import { HTTPException } from 'hono/http-exception'; @@ -16,14 +17,47 @@ export async function listShortcutsHandler(user: User): Promise { + const aIndex = lastUsedShortcutIds.indexOf(a.id); + const bIndex = lastUsedShortcutIds.indexOf(b.id); + + // If both are in lastUsed, sort by their position + if (aIndex !== -1 && bIndex !== -1) { + return aIndex - bIndex; + } + + // If only a is in lastUsed, it comes first + if (aIndex !== -1) { + return -1; + } + + // If only b is in lastUsed, it comes first + if (bIndex !== -1) { + return 1; + } + + // Neither is in lastUsed, sort alphabetically + return a.name.localeCompare(b.name); + }); + return { - shortcuts, + shortcuts: sortedShortcuts, }; } catch (error) { console.error('Error in listShortcutsHandler:', { diff --git a/apps/server/src/api/v2/shortcuts/[id]/PUT.ts b/apps/server/src/api/v2/shortcuts/[id]/PUT.ts index 3404c78f2..adecffe50 100644 --- a/apps/server/src/api/v2/shortcuts/[id]/PUT.ts +++ b/apps/server/src/api/v2/shortcuts/[id]/PUT.ts @@ -53,10 +53,11 @@ export async function updateShortcutHandler( // Only workspace_admin, data_admin, or the creator can update workspace shortcuts const isAdmin = userOrg.role === 'workspace_admin' || userOrg.role === 'data_admin'; const isCreator = existingShortcut.createdBy === user.id; - + if (!isAdmin && !isCreator) { throw new HTTPException(403, { - message: 'Only workspace admins, data admins, or the shortcut creator can update workspace shortcuts', + message: + 'Only workspace admins, data admins, or the shortcut creator can update workspace shortcuts', }); } } diff --git a/apps/server/src/api/v2/shortcuts/index.ts b/apps/server/src/api/v2/shortcuts/index.ts index de6ac99ab..ea859d2b8 100644 --- a/apps/server/src/api/v2/shortcuts/index.ts +++ b/apps/server/src/api/v2/shortcuts/index.ts @@ -8,11 +8,11 @@ import { z } from 'zod'; import { requireAuth } from '../../../middleware/auth'; import '../../../types/hono.types'; import { HTTPException } from 'hono/http-exception'; -import { createShortcutHandler } from './create-shortcut'; -import { deleteShortcutHandler } from './delete-shortcut'; -import { getShortcutHandler } from './get-shortcut'; -import { listShortcutsHandler } from './list-shortcuts'; -import { updateShortcutHandler } from './update-shortcut'; +import { listShortcutsHandler } from './GET'; +import { createShortcutHandler } from './POST'; +import { deleteShortcutHandler } from './[id]/DELETE'; +import { getShortcutHandler } from './[id]/GET'; +import { updateShortcutHandler } from './[id]/PUT'; // Schema for path params const shortcutIdParamSchema = z.object({ diff --git a/packages/database/drizzle/0096_secret_next_avengers.sql b/packages/database/drizzle/0096_outgoing_shotgun.sql similarity index 87% rename from packages/database/drizzle/0096_secret_next_avengers.sql rename to packages/database/drizzle/0096_outgoing_shotgun.sql index e6dfd2ee0..faeea261c 100644 --- a/packages/database/drizzle/0096_secret_next_avengers.sql +++ b/packages/database/drizzle/0096_outgoing_shotgun.sql @@ -12,7 +12,9 @@ CREATE TABLE "shortcuts" ( CONSTRAINT "shortcuts_personal_unique" UNIQUE("name","organization_id","created_by") ); --> statement-breakpoint -ALTER TABLE "users" ALTER COLUMN "suggested_prompts" SET DEFAULT '{"suggestedPrompts":{"report":["provide a trend analysis of quarterly profits","evaluate product performance across regions"],"dashboard":["create a sales performance dashboard","design a revenue forecast dashboard"],"visualization":["create a metric for monthly sales","show top vendors by purchase volume"],"help":["what types of analyses can you perform?","what questions can I as buster?","what data models are available for queries?","can you explain your forecasting capabilities?"]},"updatedAt":"2025-09-12T06:59:42.400Z"}'::jsonb;--> statement-breakpoint +ALTER TABLE "users" ALTER COLUMN "suggested_prompts" SET DEFAULT '{"suggestedPrompts":{"report":["provide a trend analysis of quarterly profits","evaluate product performance across regions"],"dashboard":["create a sales performance dashboard","design a revenue forecast dashboard"],"visualization":["create a metric for monthly sales","show top vendors by purchase volume"],"help":["what types of analyses can you perform?","what questions can I as buster?","what data models are available for queries?","can you explain your forecasting capabilities?"]},"updatedAt":"2025-09-12T16:57:43.191Z"}'::jsonb;--> statement-breakpoint +ALTER TABLE "messages" ADD COLUMN "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint +ALTER TABLE "users" ADD COLUMN "last_used_shortcuts" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint diff --git a/packages/database/drizzle/meta/0096_snapshot.json b/packages/database/drizzle/meta/0096_snapshot.json index 70b889073..6846b86c7 100644 --- a/packages/database/drizzle/meta/0096_snapshot.json +++ b/packages/database/drizzle/meta/0096_snapshot.json @@ -1,5 +1,5 @@ { - "id": "a2ebcfaa-f335-4dc4-8821-9d0bf865d27d", + "id": "0613296e-fdd2-4a35-9d79-2275d0c7a78e", "prevId": "f71f7e1c-314d-413d-8da9-a525bd4a3519", "version": "7", "dialect": "postgresql", @@ -3020,6 +3020,13 @@ "type": "text", "primaryKey": false, "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" } }, "indexes": { @@ -6907,7 +6914,7 @@ "type": "jsonb", "primaryKey": false, "notNull": true, - "default": "'{\"suggestedPrompts\":{\"report\":[\"provide a trend analysis of quarterly profits\",\"evaluate product performance across regions\"],\"dashboard\":[\"create a sales performance dashboard\",\"design a revenue forecast dashboard\"],\"visualization\":[\"create a metric for monthly sales\",\"show top vendors by purchase volume\"],\"help\":[\"what types of analyses can you perform?\",\"what questions can I as buster?\",\"what data models are available for queries?\",\"can you explain your forecasting capabilities?\"]},\"updatedAt\":\"2025-09-12T06:59:42.400Z\"}'::jsonb" + "default": "'{\"suggestedPrompts\":{\"report\":[\"provide a trend analysis of quarterly profits\",\"evaluate product performance across regions\"],\"dashboard\":[\"create a sales performance dashboard\",\"design a revenue forecast dashboard\"],\"visualization\":[\"create a metric for monthly sales\",\"show top vendors by purchase volume\"],\"help\":[\"what types of analyses can you perform?\",\"what questions can I as buster?\",\"what data models are available for queries?\",\"can you explain your forecasting capabilities?\"]},\"updatedAt\":\"2025-09-12T16:57:43.191Z\"}'::jsonb" }, "personalization_enabled": { "name": "personalization_enabled", @@ -6922,6 +6929,13 @@ "primaryKey": false, "notNull": true, "default": "'{}'::jsonb" + }, + "last_used_shortcuts": { + "name": "last_used_shortcuts", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" } }, "indexes": {}, diff --git a/packages/database/drizzle/meta/_journal.json b/packages/database/drizzle/meta/_journal.json index fd09ba6f9..338433bf2 100644 --- a/packages/database/drizzle/meta/_journal.json +++ b/packages/database/drizzle/meta/_journal.json @@ -677,8 +677,8 @@ { "idx": 96, "version": "7", - "when": 1757660382434, - "tag": "0096_secret_next_avengers", + "when": 1757696263222, + "tag": "0096_outgoing_shotgun", "breakpoints": true } ] diff --git a/packages/database/src/queries/chats/chats.ts b/packages/database/src/queries/chats/chats.ts index 3418d38b9..ad474aad3 100644 --- a/packages/database/src/queries/chats/chats.ts +++ b/packages/database/src/queries/chats/chats.ts @@ -35,6 +35,7 @@ export const CreateMessageInputSchema = z.object({ userId: z.string().uuid(), messageId: z.string().uuid().optional(), messageAnalysisMode: z.enum(messageAnalysisModeEnum.enumValues).optional(), + metadata: z.record(z.any()).optional(), }); export type CreateChatInput = z.infer; @@ -146,6 +147,7 @@ export async function createMessage(input: CreateMessageInput): Promise content: validated.content, }, ], + metadata: validated.metadata || {}, }) .returning(); diff --git a/packages/database/src/queries/users/index.ts b/packages/database/src/queries/users/index.ts index 8a651ecb3..a649e93e6 100644 --- a/packages/database/src/queries/users/index.ts +++ b/packages/database/src/queries/users/index.ts @@ -5,3 +5,4 @@ export * from './get-user-organizations'; export * from './user-queries'; export * from './user-suggested-prompts'; export * from './get-user-personalization'; +export * from './update-user-shortcuts'; diff --git a/packages/database/src/queries/users/update-user-shortcuts.ts b/packages/database/src/queries/users/update-user-shortcuts.ts new file mode 100644 index 000000000..020f6b36b --- /dev/null +++ b/packages/database/src/queries/users/update-user-shortcuts.ts @@ -0,0 +1,62 @@ +import { eq } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '../../connection'; +import { users } from '../../schema'; + +export const UpdateUserLastUsedShortcutsInputSchema = z.object({ + userId: z.string().uuid(), + shortcutIds: z.array(z.string().uuid()), +}); + +export type UpdateUserLastUsedShortcutsInput = z.infer< + typeof UpdateUserLastUsedShortcutsInputSchema +>; + +/** + * Updates the user's lastUsedShortcuts array with new shortcut IDs. + * New shortcuts are prepended to the front of the array. + * Duplicates are removed while maintaining the most recent usage order. + * + * @param input - Object containing userId and shortcutIds to add + * @returns Success status + */ +export async function updateUserLastUsedShortcuts( + input: UpdateUserLastUsedShortcutsInput +): Promise<{ success: boolean }> { + const validated = UpdateUserLastUsedShortcutsInputSchema.parse(input); + + try { + // First, get the current lastUsedShortcuts array + const [currentUser] = await db + .select({ lastUsedShortcuts: users.lastUsedShortcuts }) + .from(users) + .where(eq(users.id, validated.userId)) + .limit(1); + + if (!currentUser) { + throw new Error(`User not found: ${validated.userId}`); + } + + const currentShortcuts = currentUser.lastUsedShortcuts || []; + + // Remove any of the new shortcuts from their current positions + const filteredShortcuts = currentShortcuts.filter((id) => !validated.shortcutIds.includes(id)); + + // Prepend the new shortcuts to the front + const updatedShortcuts = [...validated.shortcutIds, ...filteredShortcuts]; + + // Update the user record + await db + .update(users) + .set({ + lastUsedShortcuts: updatedShortcuts, + updatedAt: new Date().toISOString(), + }) + .where(eq(users.id, validated.userId)); + + return { success: true }; + } catch (error) { + console.error('Failed to update user last used shortcuts:', error); + return { success: false }; + } +} diff --git a/packages/database/src/schema-types/user.ts b/packages/database/src/schema-types/user.ts index 2f2229fe4..0d2ddfee0 100644 --- a/packages/database/src/schema-types/user.ts +++ b/packages/database/src/schema-types/user.ts @@ -16,8 +16,13 @@ export const UserPersonalizationConfigSchema = z.object({ additionalInformation: z.string().optional(), }); +export const UserShortcutTrackingSchema = z.object({ + lastUsedShortcuts: z.array(z.string().uuid()).default([]), +}); + export type UserSuggestedPromptsType = z.infer; export type UserPersonalizationConfigType = z.infer; +export type UserShortcutTrackingType = z.infer; export const DEFAULT_USER_SUGGESTED_PROMPTS: UserSuggestedPromptsType = { suggestedPrompts: { diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index 6e638246b..6a149a25d 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -22,9 +22,11 @@ import { import type { OrganizationColorPalettes, UserPersonalizationConfigType, + UserShortcutTrackingType, UserSuggestedPromptsType, } from './schema-types'; import { DEFAULT_USER_SUGGESTED_PROMPTS } from './schema-types/user'; +import type { MessageMetadata } from './schemas/message-schemas'; export const assetPermissionRoleEnum = pgEnum('asset_permission_role_enum', [ 'owner', @@ -879,6 +881,7 @@ export const users = pgTable( .$type() .default({}) .notNull(), + lastUsedShortcuts: jsonb('last_used_shortcuts').$type().default([]).notNull(), }, (table) => [unique('users_email_key').on(table.email)] ); @@ -907,6 +910,7 @@ export const messages = pgTable( isCompleted: boolean('is_completed').default(false).notNull(), postProcessingMessage: jsonb('post_processing_message'), triggerRunId: text('trigger_run_id'), + metadata: jsonb().$type().default({}).notNull(), }, (table) => [ index('messages_chat_id_idx').using('btree', table.chatId.asc().nullsLast().op('uuid_ops')), diff --git a/packages/database/src/schemas/message-schemas.ts b/packages/database/src/schemas/message-schemas.ts index 40a47b9bc..cc15e7dca 100644 --- a/packages/database/src/schemas/message-schemas.ts +++ b/packages/database/src/schemas/message-schemas.ts @@ -139,3 +139,10 @@ export type ChatMessageResponseMessage_File = z.infer; export type ResponseMessageFileType = z.infer; export type ReasoingMessage_ThoughtFileType = z.infer; + +// Message metadata schema +export const MessageMetadataSchema = z.object({ + shortcutIds: z.array(z.string().uuid()).nullable().optional(), +}); + +export type MessageMetadata = z.infer; diff --git a/packages/server-shared/src/chats/chat.types.ts b/packages/server-shared/src/chats/chat.types.ts index a179c5916..558506505 100644 --- a/packages/server-shared/src/chats/chat.types.ts +++ b/packages/server-shared/src/chats/chat.types.ts @@ -4,6 +4,13 @@ import type { AssetPermissionRoleSchema } from '../share'; import { ShareConfigSchema } from '../share'; import { ChatMessageSchema } from './chat-message.types'; +// Message metadata schema (mirrored from database package) +export const MessageMetadataSchema = z.object({ + shortcutIds: z.array(z.string().uuid()).nullable().optional(), +}); + +export type MessageMetadata = z.infer; + // Asset Permission Role enum (matching database enum) export const ChatAssetTypeSchema = AssetTypeSchema.exclude(['chat', 'collection']); @@ -34,6 +41,7 @@ export const ChatCreateRequestSchema = z message_analysis_mode: MessageAnalysisModeSchema.optional(), asset_id: z.string().optional(), asset_type: ChatAssetTypeSchema.optional(), + metadata: MessageMetadataSchema.optional(), // Legacy fields for backward compatibility metric_id: z.string().optional(), dashboard_id: z.string().optional(), @@ -51,6 +59,7 @@ export const ChatCreateHandlerRequestSchema = z.object({ message_id: z.string().optional(), asset_id: z.string().optional(), asset_type: ChatAssetTypeSchema.optional(), + metadata: MessageMetadataSchema.optional(), }); // Cancel chat params schema