From 6eae664bb85dc4b9762a89d4ecea77b845a00381 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 20:36:49 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #149 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/deep-assistant/GPTutor/issues/149 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..cc9d1c3e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/deep-assistant/GPTutor/issues/149 +Your prepared branch: issue-149-0d2b41ba +Your prepared working directory: /tmp/gh-issue-solver-1757525787332 + +Proceed. \ No newline at end of file From 99d07809c9f4a11ffe2f4f31211840fff5c4ad3f Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 20:37:05 +0300 Subject: [PATCH 2/3] Remove CLAUDE.md - PR created successfully --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index cc9d1c3e..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/deep-assistant/GPTutor/issues/149 -Your prepared branch: issue-149-0d2b41ba -Your prepared working directory: /tmp/gh-issue-solver-1757525787332 - -Proceed. \ No newline at end of file From 1513f6767819c431f8e930c1eb2e093a12e6f380 Mon Sep 17 00:00:00 2001 From: konard Date: Wed, 10 Sep 2025 20:41:48 +0300 Subject: [PATCH 3/3] Refactor ChatGPT entity architecture for improved maintainability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major improvements: - Extract dialog restoration logic into DialogRestoreService - Create DialogRouterService for routing dialog restoration by type - Add ChatStateManager to handle chat state initialization - Simplify ChatGpt.ts by removing duplicated initialization logic - Refactor complex conditional chains in dialog routing - Remove TODO comment for refactoring (line 358 in ChatGptTemplate.ts) Benefits: - Better separation of concerns - Reduced code duplication - Improved testability and maintainability - More modular architecture - Easier to extend with new chat types πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- GPTutor-Frontend/src/entity/GPT/ChatGpt.ts | 63 +++---------- .../src/entity/GPT/ChatGptTemplate.ts | 44 ++------- .../src/entity/GPT/ChatStateManager.ts | 62 +++++++++++++ .../src/entity/GPT/DialogRestoreService.ts | 89 +++++++++++++++++++ .../src/entity/GPT/DialogRouterService.ts | 63 +++++++++++++ GPTutor-Frontend/src/entity/GPT/index.ts | 3 + 6 files changed, 237 insertions(+), 87 deletions(-) create mode 100644 GPTutor-Frontend/src/entity/GPT/ChatStateManager.ts create mode 100644 GPTutor-Frontend/src/entity/GPT/DialogRestoreService.ts create mode 100644 GPTutor-Frontend/src/entity/GPT/DialogRouterService.ts diff --git a/GPTutor-Frontend/src/entity/GPT/ChatGpt.ts b/GPTutor-Frontend/src/entity/GPT/ChatGpt.ts index ddc25295..07581e00 100644 --- a/GPTutor-Frontend/src/entity/GPT/ChatGpt.ts +++ b/GPTutor-Frontend/src/entity/GPT/ChatGpt.ts @@ -3,14 +3,15 @@ import { sig } from "dignals"; import { ChatGptFree } from "$/entity/GPT/ChatGptFree"; import { ChatGptLesson } from "$/entity/GPT/ChatGptLesson"; import { GptHistoryDialogs } from "$/entity/GPT/GptHistoryDialogs"; -import { LessonItem, lessonsController, ModeType } from "$/entity/lessons"; +import { LessonItem, ModeType } from "$/entity/lessons"; import { ChatGptTemplate } from "$/entity/GPT/ChatGptTemplate"; import { ChatGptInterview } from "$/entity/GPT/ChatGptInterview"; import { ChatGptLeetCode } from "$/entity/GPT/ChatGptLeetCode"; -import { interviews } from "$/entity/interview"; import { ChatGptTrainer } from "$/entity/GPT/ChatGptTrainer"; import { VkStorageService } from "$/services/VkStorageService"; import { ChatGptAnecdote } from "$/entity/GPT/ChatGptAnecdote"; +import { DialogRouterService } from "./DialogRouterService"; +import { ChatStateManager } from "./ChatStateManager"; export class ChatGpt { storageService = new VkStorageService(); @@ -46,37 +47,20 @@ export class ChatGpt { currentChatGpt$ = sig(this.chatGptFree); moveToFreeChat = (goToChat: () => void) => { - lessonsController.clearLesson(); - this.chatGptFree.currentHistory = null; - + ChatStateManager.clearLessonState(); + ChatStateManager.initializeFreeChat(this.chatGptFree); this.currentChatGpt$.set(this.chatGptFree); - - this.chatGptFree.clearMessages(); - this.chatGptFree.abortSend(); - - this.chatGptFree.resetSystemMessage(); - goToChat(); }; moveToLessonChat(lesson: LessonItem, goToChatLesson: () => void) { - this.chatGptLesson.clearMessages(); - this.chatGptLesson.resetSystemMessage(); - this.chatGptLesson.currentHistory = null; - + ChatStateManager.initializeLessonChat(this.chatGptLesson, lesson); this.currentChatGpt$.set(this.chatGptLesson); - this.chatGptLesson.setInitialSystemMessage( - lessonsController.currentChapter.get()?.systemMessage - ); - - lessonsController.setCurrentLesson(lesson.id); - goToChatLesson(); } moveToInterviewChat(interviewType: string, goToChatInterview: () => void) { - interviews.setCurrentInterview(interviewType as ModeType); - this.chatGptInterview.messages$.set([]); + ChatStateManager.initializeInterviewChat(this.chatGptInterview, interviewType); goToChatInterview(); } @@ -90,33 +74,12 @@ export class ChatGpt { const dialog = this.history.getDialogById(id); if (!dialog) return; - if (dialog.type === "Free") { - this.currentChatGpt$.set(this.chatGptFree); - await this.chatGptFree.restoreDialogFromHistory(dialog, goToChatFree); - return; - } - if (dialog.type === ModeType.LeetCode) { - this.currentChatGpt$.set(this.chatGptLeetCode); - await this.chatGptLeetCode.restoreDialogFromHistory( - dialog, - goToChatLeetCode - ); - return; - } - - if (dialog.type.includes("INTERVIEW")) { - this.currentChatGpt$.set(this.chatGptInterview); - await this.chatGptInterview.restoreDialogFromHistory( - dialog, - goToChatInterview - ); - } - - if (dialog.type && dialog.lessonName) { - this.currentChatGpt$.set(this.chatGptLesson); - await this.chatGptLesson.restoreDialogFromHistory(dialog, goToChatLesson); - return; - } + await DialogRouterService.routeDialog(this, dialog, { + goToChatFree, + goToChatLesson, + goToChatInterview, + goToChatLeetCode + }); } getCurrentChatGpt = () => this.currentChatGpt$.get(); diff --git a/GPTutor-Frontend/src/entity/GPT/ChatGptTemplate.ts b/GPTutor-Frontend/src/entity/GPT/ChatGptTemplate.ts index a30a67c7..13764063 100644 --- a/GPTutor-Frontend/src/entity/GPT/ChatGptTemplate.ts +++ b/GPTutor-Frontend/src/entity/GPT/ChatGptTemplate.ts @@ -18,6 +18,7 @@ import { SubscriptionGPT } from "$/entity/GPT/SubscriptionGPT"; import { getBannerName } from "$/entity/history/utils"; import { gptModels } from "$/entity/GPT/GptModels"; import { userInfo } from "$/entity/user/UserInfo"; +import { DialogRestoreService } from "./DialogRestoreService"; const initialSystemContent = `ВСбя Π·ΠΎΠ²ΡƒΡ‚ Deep.GPT, Π±ΡƒΠ΄ΡŒ ΠΏΠΎΠ»Π΅Π·Π½Ρ‹ΠΌ ΠΏΠΎΠΌΠΎΡ‰Π½ΠΈΠΊΠΎΠΌ.`; @@ -355,42 +356,11 @@ export abstract class ChatGptTemplate { lessonsController.clearLesson(); } - //todo Ρ€Π΅Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΈΠ½Π³ - async restoreDialogFromHistory(dialog: History, goToChat: () => void) { - this.closeDelay(); - - this.currentHistory = dialog; - - const messages = await this.getMessages$.run(dialog.id); - - if (this.getMessages$.error.get()) { - return snackbarNotify.notify({ - type: "error", - message: "Ошибка ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅ Π² Π΄ΠΈΠΏΠ»ΠΎΠ³", - }); - } - - await this.prepareDialog(dialog); - - this.initialSystemContent = dialog.systemMessage; - this.systemMessage = new GptMessage(dialog.systemMessage, GPTRoles.system); - - this.messages$.set( - messages.map((message) => { - const gptMessage = new GptMessage( - message.content, - message.role as GPTRoles, - false, - message.error - ); - - gptMessage.failedModeration$.set(message.isFailedModeration); - - return gptMessage; - }) - ); - - this.checkOnRunOutOfMessages(); - goToChat(); + /** + * Restore dialog from history using DialogRestoreService + * Refactored to improve separation of concerns and reduce complexity + */ + async restoreDialogFromHistory(dialog: History, goToChat: () => void): Promise { + return DialogRestoreService.restoreDialog(this, dialog, goToChat); } } diff --git a/GPTutor-Frontend/src/entity/GPT/ChatStateManager.ts b/GPTutor-Frontend/src/entity/GPT/ChatStateManager.ts new file mode 100644 index 00000000..10be5998 --- /dev/null +++ b/GPTutor-Frontend/src/entity/GPT/ChatStateManager.ts @@ -0,0 +1,62 @@ +import { ChatGptTemplate } from "./ChatGptTemplate"; +import { lessonsController, LessonItem } from "$/entity/lessons"; +import { interviews } from "$/entity/interview"; +import type { ModeType } from "$/entity/lessons"; + +/** + * Service for managing chat state transitions and initialization + * Extracted from ChatGpt class to reduce duplication and improve maintainability + */ +export class ChatStateManager { + /** + * Initialize free chat state + */ + static initializeFreeChat(chatInstance: ChatGptTemplate): void { + chatInstance.currentHistory = null; + chatInstance.clearMessages(); + chatInstance.abortSend(); + chatInstance.resetSystemMessage(); + } + + /** + * Initialize lesson chat state + */ + static initializeLessonChat( + chatInstance: ChatGptTemplate, + lesson: LessonItem + ): void { + chatInstance.clearMessages(); + chatInstance.resetSystemMessage(); + chatInstance.currentHistory = null; + + const systemMessage = lessonsController.currentChapter.get()?.systemMessage; + chatInstance.setInitialSystemMessage(systemMessage); + lessonsController.setCurrentLesson(lesson.id); + } + + /** + * Initialize interview chat state + */ + static initializeInterviewChat( + chatInstance: ChatGptTemplate, + interviewType: string + ): void { + interviews.setCurrentInterview(interviewType as ModeType); + chatInstance.messages$.set([]); + } + + /** + * Clear all lesson-related state + */ + static clearLessonState(): void { + lessonsController.clearLesson(); + } + + /** + * Clear all chapter-related state + */ + static clearChapterState(): void { + lessonsController.clearChapter(); + lessonsController.clearLesson(); + } +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/GPT/DialogRestoreService.ts b/GPTutor-Frontend/src/entity/GPT/DialogRestoreService.ts new file mode 100644 index 00000000..a44926b7 --- /dev/null +++ b/GPTutor-Frontend/src/entity/GPT/DialogRestoreService.ts @@ -0,0 +1,89 @@ +import { History } from "$/entity/history"; +import { ModeType } from "$/entity/lessons"; +import { ChatGptTemplate } from "./ChatGptTemplate"; + +/** + * Service for handling dialog restoration logic + * Extracted from ChatGptTemplate to improve separation of concerns + */ +export class DialogRestoreService { + static async restoreDialog( + chatGptInstance: ChatGptTemplate, + dialog: History, + goToChat: () => void + ): Promise { + chatGptInstance.closeDelay(); + chatGptInstance.currentHistory = dialog; + + const messages = await chatGptInstance.getMessages$.run(dialog.id); + + if (chatGptInstance.getMessages$.error.get()) { + const { snackbarNotify } = await import("$/entity/notify"); + return snackbarNotify.notify({ + type: "error", + message: "Ошибка ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄Π΅ Π² Π΄ΠΈΠΏΠ»ΠΎΠ³", + }); + } + + await this.prepareDialog(dialog); + this.setSystemMessage(chatGptInstance, dialog); + this.restoreMessages(chatGptInstance, messages); + + chatGptInstance.checkOnRunOutOfMessages(); + goToChat(); + } + + private static async prepareDialog(dialog: History): Promise { + if (dialog.type === ModeType.LeetCode) { + const { leetCode } = await import("$/entity/leetCode/LeetCode"); + await leetCode.loadDetailProblem(dialog.lessonName); + return; + } + + if (dialog.type.includes("INTERVIEW")) { + const { interviews } = await import("$/entity/interview"); + interviews.setCurrentInterview(dialog.type as ModeType); + return; + } + + if (dialog.lessonName && dialog.type) { + const { lessonsController } = await import("$/entity/lessons"); + lessonsController.setCurrentChapter(dialog.type as ModeType); + lessonsController.setCurrentLessonByName(dialog.lessonName); + return; + } + + const { lessonsController } = await import("$/entity/lessons"); + lessonsController.clearChapter(); + lessonsController.clearLesson(); + } + + private static setSystemMessage(chatGptInstance: ChatGptTemplate, dialog: History): void { + // Import at top level to avoid circular dependency issues + const { GptMessage } = require("./GptMessage"); + const { GPTRoles } = require("./types"); + + chatGptInstance.initialSystemContent = dialog.systemMessage; + chatGptInstance.systemMessage = new GptMessage(dialog.systemMessage, GPTRoles.system); + } + + private static restoreMessages(chatGptInstance: ChatGptTemplate, messages: any[]): void { + // Import at top level to avoid circular dependency issues + const { GptMessage } = require("./GptMessage"); + const { GPTRoles } = require("./types"); + + const gptMessages = messages.map((message) => { + const gptMessage = new GptMessage( + message.content, + message.role as typeof GPTRoles[keyof typeof GPTRoles], + false, + message.error + ); + + gptMessage.failedModeration$.set(message.isFailedModeration); + return gptMessage; + }); + + chatGptInstance.messages$.set(gptMessages); + } +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/GPT/DialogRouterService.ts b/GPTutor-Frontend/src/entity/GPT/DialogRouterService.ts new file mode 100644 index 00000000..d71aa008 --- /dev/null +++ b/GPTutor-Frontend/src/entity/GPT/DialogRouterService.ts @@ -0,0 +1,63 @@ +import { History } from "$/entity/history"; +import { ModeType } from "$/entity/lessons"; +import type { ChatGpt } from "./ChatGpt"; + +/** + * Service for handling dialog routing logic + * Extracted from ChatGpt class to improve maintainability + */ +export class DialogRouterService { + /** + * Routes dialog restoration to the appropriate chat type based on dialog properties + */ + static async routeDialog( + chatGpt: ChatGpt, + dialog: History, + navigationCallbacks: { + goToChatFree: () => void; + goToChatLesson: () => void; + goToChatInterview: () => void; + goToChatLeetCode: () => void; + } + ): Promise { + const { goToChatFree, goToChatLesson, goToChatInterview, goToChatLeetCode } = navigationCallbacks; + + // Route to Free Chat + if (dialog.type === "Free") { + chatGpt.currentChatGpt$.set(chatGpt.chatGptFree); + await chatGpt.chatGptFree.restoreDialogFromHistory(dialog, goToChatFree); + return; + } + + // Route to LeetCode Chat + if (dialog.type === ModeType.LeetCode) { + chatGpt.currentChatGpt$.set(chatGpt.chatGptLeetCode); + await chatGpt.chatGptLeetCode.restoreDialogFromHistory(dialog, goToChatLeetCode); + return; + } + + // Route to Interview Chat + if (dialog.type.includes("INTERVIEW")) { + chatGpt.currentChatGpt$.set(chatGpt.chatGptInterview); + await chatGpt.chatGptInterview.restoreDialogFromHistory(dialog, goToChatInterview); + return; + } + + // Route to Lesson Chat + if (dialog.type && dialog.lessonName) { + chatGpt.currentChatGpt$.set(chatGpt.chatGptLesson); + await chatGpt.chatGptLesson.restoreDialogFromHistory(dialog, goToChatLesson); + return; + } + } + + /** + * Get the appropriate chat instance for a given dialog type + */ + static getChatInstanceForDialogType(chatGpt: ChatGpt, dialogType: string) { + if (dialogType === "Free") return chatGpt.chatGptFree; + if (dialogType === ModeType.LeetCode) return chatGpt.chatGptLeetCode; + if (dialogType.includes("INTERVIEW")) return chatGpt.chatGptInterview; + return chatGpt.chatGptLesson; // default for lesson dialogs + } +} \ No newline at end of file diff --git a/GPTutor-Frontend/src/entity/GPT/index.ts b/GPTutor-Frontend/src/entity/GPT/index.ts index ae8eec2e..72ef285f 100644 --- a/GPTutor-Frontend/src/entity/GPT/index.ts +++ b/GPTutor-Frontend/src/entity/GPT/index.ts @@ -1,3 +1,6 @@ export * from "./ChatGpt"; export * from "./GptMessage"; export * from "./types"; +export * from "./DialogRestoreService"; +export * from "./DialogRouterService"; +export * from "./ChatStateManager";