diff --git a/api/chat_message_handler.go b/api/chat_message_handler.go index 6c8a811f..c5a25258 100644 --- a/api/chat_message_handler.go +++ b/api/chat_message_handler.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/mux" "github.com/samber/lo" + "github.com/swuecho/chat_backend/models" "github.com/swuecho/chat_backend/sqlc_queries" ) @@ -32,6 +33,7 @@ func (h *ChatMessageHandler) Register(router *mux.Router) { router.HandleFunc("/uuid/chat_messages/{uuid}", h.GetChatMessageByUUID).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_messages/{uuid}", h.UpdateChatMessageByUUID).Methods(http.MethodPut) router.HandleFunc("/uuid/chat_messages/{uuid}", h.DeleteChatMessageByUUID).Methods(http.MethodDelete) + router.HandleFunc("/uuid/chat_messages/{uuid}/generate-suggestions", h.GenerateMoreSuggestions).Methods(http.MethodPost) router.HandleFunc("/uuid/chat_messages/chat_sessions/{uuid}", h.GetChatHistoryBySessionUUID).Methods(http.MethodGet) router.HandleFunc("/uuid/chat_messages/chat_sessions/{uuid}", h.DeleteChatMessagesBySesionUUID).Methods(http.MethodDelete) } @@ -231,3 +233,112 @@ func (h *ChatMessageHandler) DeleteChatMessagesBySesionUUID(w http.ResponseWrite } w.WriteHeader(http.StatusOK) } + +// GenerateMoreSuggestions generates additional suggested questions for a message +func (h *ChatMessageHandler) GenerateMoreSuggestions(w http.ResponseWriter, r *http.Request) { + messageUUID := mux.Vars(r)["uuid"] + + // Get the existing message + message, err := h.service.q.GetChatMessageByUUID(r.Context(), messageUUID) + if err != nil { + http.Error(w, "Message not found", http.StatusNotFound) + return + } + + // Only allow suggestions for assistant messages + if message.Role != "assistant" { + http.Error(w, "Suggestions can only be generated for assistant messages", http.StatusBadRequest) + return + } + + // Get the session to check if explore mode is enabled + session, err := h.service.q.GetChatSessionByUUID(r.Context(), message.ChatSessionUuid) + if err != nil { + http.Error(w, "Session not found", http.StatusNotFound) + return + } + + // Check if explore mode is enabled + if !session.ExploreMode { + http.Error(w, "Suggestions are only available in explore mode", http.StatusBadRequest) + return + } + + // Get conversation context - last 6 messages + contextMessages, err := h.service.q.GetLatestMessagesBySessionUUID(r.Context(), + sqlc_queries.GetLatestMessagesBySessionUUIDParams{ + ChatSessionUuid: session.Uuid, + Limit: 6, + }) + if err != nil { + http.Error(w, "Failed to get conversation context", http.StatusInternalServerError) + return + } + + // Convert to models.Message format for suggestion generation + var msgs []models.Message + for _, msg := range contextMessages { + msgs = append(msgs, models.Message{ + Role: msg.Role, + Content: msg.Content, + }) + } + + // Create a new ChatService to access suggestion generation methods + chatService := NewChatService(h.service.q) + + // Generate new suggested questions + newSuggestions := chatService.generateSuggestedQuestions(message.Content, msgs) + if len(newSuggestions) == 0 { + http.Error(w, "Failed to generate suggestions", http.StatusInternalServerError) + return + } + + // Parse existing suggestions + var existingSuggestions []string + if len(message.SuggestedQuestions) > 0 { + if err := json.Unmarshal(message.SuggestedQuestions, &existingSuggestions); err != nil { + // If unmarshal fails, treat as empty array + existingSuggestions = []string{} + } + } + + // Combine existing and new suggestions (avoiding duplicates) + allSuggestions := append(existingSuggestions, newSuggestions...) + + // Remove duplicates + seenSuggestions := make(map[string]bool) + var uniqueSuggestions []string + for _, suggestion := range allSuggestions { + if !seenSuggestions[suggestion] { + seenSuggestions[suggestion] = true + uniqueSuggestions = append(uniqueSuggestions, suggestion) + } + } + + // Update the message with new suggestions + suggestionsJSON, err := json.Marshal(uniqueSuggestions) + if err != nil { + http.Error(w, "Failed to serialize suggestions", http.StatusInternalServerError) + return + } + + _, err = h.service.q.UpdateChatMessageSuggestions(r.Context(), + sqlc_queries.UpdateChatMessageSuggestionsParams{ + Uuid: messageUUID, + SuggestedQuestions: suggestionsJSON, + }) + if err != nil { + http.Error(w, "Failed to update message with suggestions", http.StatusInternalServerError) + return + } + + // Return the new suggestions to the client + response := map[string]interface{}{ + "newSuggestions": newSuggestions, + "allSuggestions": uniqueSuggestions, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} diff --git a/api/sqlc/queries/chat_message.sql b/api/sqlc/queries/chat_message.sql index 3ce6126d..f58ab209 100644 --- a/api/sqlc/queries/chat_message.sql +++ b/api/sqlc/queries/chat_message.sql @@ -121,6 +121,11 @@ UPDATE chat_message SET content = $2, updated_at = now(), token_count = $3 WHERE uuid = $1 ; +-- name: UpdateChatMessageSuggestions :one +UPDATE chat_message +SET suggested_questions = $2, updated_at = now() +WHERE uuid = $1 +RETURNING *; -- name: DeleteChatMessagesBySesionUUID :exec UPDATE chat_message diff --git a/api/sqlc_queries/chat_message.sql.go b/api/sqlc_queries/chat_message.sql.go index 77cd922f..b046b7e6 100644 --- a/api/sqlc_queries/chat_message.sql.go +++ b/api/sqlc_queries/chat_message.sql.go @@ -827,3 +827,43 @@ func (q *Queries) UpdateChatMessageContent(ctx context.Context, arg UpdateChatMe _, err := q.db.ExecContext(ctx, updateChatMessageContent, arg.Uuid, arg.Content, arg.TokenCount) return err } + +const updateChatMessageSuggestions = `-- name: UpdateChatMessageSuggestions :one +UPDATE chat_message +SET suggested_questions = $2, updated_at = now() +WHERE uuid = $1 +RETURNING id, uuid, chat_session_uuid, role, content, reasoning_content, model, llm_summary, score, user_id, created_at, updated_at, created_by, updated_by, is_deleted, is_pin, token_count, raw, artifacts, suggested_questions +` + +type UpdateChatMessageSuggestionsParams struct { + Uuid string `json:"uuid"` + SuggestedQuestions json.RawMessage `json:"suggestedQuestions"` +} + +func (q *Queries) UpdateChatMessageSuggestions(ctx context.Context, arg UpdateChatMessageSuggestionsParams) (ChatMessage, error) { + row := q.db.QueryRowContext(ctx, updateChatMessageSuggestions, arg.Uuid, arg.SuggestedQuestions) + var i ChatMessage + err := row.Scan( + &i.ID, + &i.Uuid, + &i.ChatSessionUuid, + &i.Role, + &i.Content, + &i.ReasoningContent, + &i.Model, + &i.LlmSummary, + &i.Score, + &i.UserID, + &i.CreatedAt, + &i.UpdatedAt, + &i.CreatedBy, + &i.UpdatedBy, + &i.IsDeleted, + &i.IsPin, + &i.TokenCount, + &i.Raw, + &i.Artifacts, + &i.SuggestedQuestions, + ) + return i, err +} diff --git a/web/src/api/chat_message.ts b/web/src/api/chat_message.ts index 6d75631b..49d444f5 100644 --- a/web/src/api/chat_message.ts +++ b/web/src/api/chat_message.ts @@ -32,3 +32,14 @@ export const getChatMessagesBySessionUUID = async (uuid: string) => { throw error } } + +export const generateMoreSuggestions = async (messageUuid: string) => { + try { + const response = await request.post(`/uuid/chat_messages/${messageUuid}/generate-suggestions`) + return response.data + } + catch (error) { + console.error(error) + throw error + } +} diff --git a/web/src/store/modules/message/index.ts b/web/src/store/modules/message/index.ts index d9a99f32..7f9d2396 100644 --- a/web/src/store/modules/message/index.ts +++ b/web/src/store/modules/message/index.ts @@ -2,6 +2,7 @@ import { defineStore } from 'pinia' import { getChatMessagesBySessionUUID, clearSessionChatMessages, + generateMoreSuggestions, } from '@/api' import { useSessionStore } from '../session' @@ -60,7 +61,30 @@ export const useMessageStore = defineStore('message-store', { try { const messageData = await getChatMessagesBySessionUUID(sessionUuid) - this.chat[sessionUuid] = messageData + + // Initialize batching structure for messages with suggested questions + const processedMessageData = messageData.map((message: Chat.Message) => { + if (message.suggestedQuestions && message.suggestedQuestions.length > 0) { + // If batches don't exist, create the first batch from existing questions + if (!message.suggestedQuestionsBatches || message.suggestedQuestionsBatches.length === 0) { + // Split suggestions into batches of 3 (assuming original suggestions come in groups of 3) + const batches: string[][] = [] + for (let i = 0; i < message.suggestedQuestions.length; i += 3) { + batches.push(message.suggestedQuestions.slice(i, i + 3)) + } + + return { + ...message, + suggestedQuestionsBatches: batches, + currentSuggestedQuestionsBatch: batches.length - 1, // Show the last batch (most recent) + suggestedQuestions: batches[batches.length - 1] || message.suggestedQuestions, // Show last batch + } + } + } + return message + }) + + this.chat[sessionUuid] = processedMessageData // Update active session if needed const sessionStore = useSessionStore() @@ -71,7 +95,7 @@ export const useMessageStore = defineStore('message-store', { } } - return messageData + return processedMessageData } catch (error) { console.error(`Failed to sync messages for session ${sessionUuid}:`, error) throw error @@ -221,5 +245,90 @@ export const useMessageStore = defineStore('message-store', { const messages = this.chat[sessionUuid] || [] return messages.filter(msg => msg.isPrompt) }, + + // Generate more suggested questions for a message + async generateMoreSuggestedQuestions(sessionUuid: string, messageUuid: string) { + try { + // Set generating state for the message + this.updateMessage(sessionUuid, messageUuid, { suggestedQuestionsGenerating: true }) + + const response = await generateMoreSuggestions(messageUuid) + const { newSuggestions, allSuggestions } = response + + // Get existing message + const messages = this.chat[sessionUuid] || [] + const messageIndex = messages.findIndex(msg => msg.uuid === messageUuid) + + if (messageIndex !== -1) { + const message = messages[messageIndex] + + // Initialize batches if they don't exist + let suggestedQuestionsBatches = message.suggestedQuestionsBatches || [] + + // If this is the first time, create the first batch from existing questions + if (suggestedQuestionsBatches.length === 0 && message.suggestedQuestions) { + suggestedQuestionsBatches.push(message.suggestedQuestions) + } + + // Add the new suggestions as a new batch + suggestedQuestionsBatches.push(newSuggestions) + + // Update the message with new data - show the new batch, not all suggestions + this.updateMessage(sessionUuid, messageUuid, { + suggestedQuestions: newSuggestions, // Show only the new batch + suggestedQuestionsBatches, + currentSuggestedQuestionsBatch: suggestedQuestionsBatches.length - 1, // Set to the new batch + suggestedQuestionsGenerating: false, + }) + } + + return response + } catch (error) { + // Clear generating state on error + this.updateMessage(sessionUuid, messageUuid, { suggestedQuestionsGenerating: false }) + console.error('Failed to generate more suggestions:', error) + throw error + } + }, + + // Navigate to previous suggestions batch + previousSuggestedQuestionsBatch(sessionUuid: string, messageUuid: string) { + const messages = this.chat[sessionUuid] || [] + const messageIndex = messages.findIndex(msg => msg.uuid === messageUuid) + + if (messageIndex !== -1) { + const message = messages[messageIndex] + const batches = message.suggestedQuestionsBatches || [] + const currentBatch = message.currentSuggestedQuestionsBatch || 0 + + if (currentBatch > 0 && batches.length > 0) { + const newBatchIndex = currentBatch - 1 + this.updateMessage(sessionUuid, messageUuid, { + suggestedQuestions: batches[newBatchIndex], + currentSuggestedQuestionsBatch: newBatchIndex, + }) + } + } + }, + + // Navigate to next suggestions batch + nextSuggestedQuestionsBatch(sessionUuid: string, messageUuid: string) { + const messages = this.chat[sessionUuid] || [] + const messageIndex = messages.findIndex(msg => msg.uuid === messageUuid) + + if (messageIndex !== -1) { + const message = messages[messageIndex] + const batches = message.suggestedQuestionsBatches || [] + const currentBatch = message.currentSuggestedQuestionsBatch || 0 + + if (currentBatch < batches.length - 1) { + const newBatchIndex = currentBatch + 1 + this.updateMessage(sessionUuid, messageUuid, { + suggestedQuestions: batches[newBatchIndex], + currentSuggestedQuestionsBatch: newBatchIndex, + }) + } + } + }, }, }) \ No newline at end of file diff --git a/web/src/typings/chat.d.ts b/web/src/typings/chat.d.ts index fa14f25a..9fbc5f67 100644 --- a/web/src/typings/chat.d.ts +++ b/web/src/typings/chat.d.ts @@ -31,6 +31,9 @@ declare namespace Chat { artifacts?: Artifact[] suggestedQuestions?: string[] suggestedQuestionsLoading?: boolean + suggestedQuestionsBatches?: string[][] + currentSuggestedQuestionsBatch?: number + suggestedQuestionsGenerating?: boolean } interface Session { diff --git a/web/src/views/chat/components/Message/SuggestedQuestions.vue b/web/src/views/chat/components/Message/SuggestedQuestions.vue index f3ceaea6..80560074 100644 --- a/web/src/views/chat/components/Message/SuggestedQuestions.vue +++ b/web/src/views/chat/components/Message/SuggestedQuestions.vue @@ -1,11 +1,19 @@ diff --git a/web/src/views/chat/components/Message/index.vue b/web/src/views/chat/components/Message/index.vue index 3a72fd2d..7ee6b94c 100644 --- a/web/src/views/chat/components/Message/index.vue +++ b/web/src/views/chat/components/Message/index.vue @@ -24,6 +24,9 @@ interface Props { artifacts?: Chat.Artifact[] suggestedQuestions?: string[] suggestedQuestionsLoading?: boolean + suggestedQuestionsBatches?: string[][] + currentSuggestedQuestionsBatch?: number + suggestedQuestionsGenerating?: boolean exploreMode?: boolean } @@ -33,6 +36,9 @@ interface Emit { (ev: 'togglePin'): void (ev: 'afterEdit', index: number, text: string): void (ev: 'useQuestion', question: string): void + (ev: 'generateMoreSuggestions'): void + (ev: 'previousSuggestionsBatch'): void + (ev: 'nextSuggestionsBatch'): void } const props = defineProps() @@ -77,6 +83,18 @@ function handleDelete() { function handleUseQuestion(question: string) { emit('useQuestion', question) } + +function handleGenerateMoreSuggestions() { + emit('generateMoreSuggestions') +} + +function handlePreviousSuggestionsBatch() { + emit('previousSuggestionsBatch') +} + +function handleNextSuggestionsBatch() { + emit('nextSuggestionsBatch') +}